From e1cb8baf533dd4c4742b0ad04ebbd5e99ee21388 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Mon, 27 Dec 2021 09:35:07 +0100 Subject: [PATCH] feat: Better in app video player --- assets/l10n/intl_en.arb | 9 +- lib/pages/chat/events/message.dart | 13 +- lib/pages/chat/events/message_content.dart | 36 +---- lib/pages/chat/events/video_player.dart | 123 ++++++++++++++++++ lib/pages/video_viewer/video_viewer.dart | 94 ------------- lib/pages/video_viewer/video_viewer_view.dart | 53 -------- pubspec.lock | 14 +- pubspec.yaml | 2 +- 8 files changed, 145 insertions(+), 199 deletions(-) create mode 100644 lib/pages/chat/events/video_player.dart delete mode 100644 lib/pages/video_viewer/video_viewer.dart delete mode 100644 lib/pages/video_viewer/video_viewer_view.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a5e6d44f..ab52561d 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2669,5 +2669,12 @@ "unsubscribeStories": "Unsubscribe stories", "thisUserHasNotPostedAnythingYet": "This user has not posted anything in their story yet", "yourStory": "Your story", - "replyHasBeenSent": "Reply has been sent" + "replyHasBeenSent": "Reply has been sent", + "videoWithSize": "Video ({size})", + "@videoWithSize": { + "type": "text", + "placeholders": { + "size": {} + } + } } diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 72ab6b28..d2e05a8f 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -4,8 +4,6 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -92,13 +90,10 @@ class Message extends StatelessWidget { bottomRight: const Radius.circular(AppConfig.borderRadius), ); final noBubble = { - MessageTypes.Video, - MessageTypes.Image, - MessageTypes.Sticker, - }.contains(event.messageType) && - !(event.messageType == MessageTypes.Video && - !((PlatformInfos.isMobile && event.showThumbnail) || - PlatformInfos.isWeb)); + MessageTypes.Video, + MessageTypes.Image, + MessageTypes.Sticker, + }.contains(event.messageType); if (ownMessage) { color = displayEvent.status.isError diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index a0d4e714..1bc68348 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -6,11 +6,10 @@ import 'package:matrix/matrix.dart'; import 'package:matrix_link_text/link_text.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart'; +import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions.dart/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../../config/app_config.dart'; -import '../../../pages/video_viewer/video_viewer.dart'; import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; import '../../bootstrap/bootstrap_dialog.dart'; @@ -90,38 +89,7 @@ class MessageContent extends StatelessWidget { return MessageDownloadContent(event, textColor); case MessageTypes.Video: if (PlatformInfos.isMobile || PlatformInfos.isWeb) { - if (event.showThumbnail) { - return Stack( - alignment: Alignment.center, - children: [ - ImageBubble( - event, - width: 400, - height: 300, - fit: BoxFit.cover, - tapToView: false, - ), - FloatingActionButton.extended( - onPressed: () => showDialog( - context: Matrix.of(context).navigatorContext, - useRootNavigator: false, - builder: (_) => VideoViewer(event), - ), - label: Text(L10n.of(context).play('Video')), - icon: const Icon(Icons.video_camera_front_outlined), - ), - ], - ); - } - return FloatingActionButton.extended( - onPressed: () => showDialog( - context: Matrix.of(context).navigatorContext, - useRootNavigator: false, - builder: (_) => VideoViewer(event), - ), - label: Text(L10n.of(context).play('Video')), - icon: const Icon(Icons.video_camera_front_outlined), - ); + return EventVideoPlayer(event); } return MessageDownloadContent(event, textColor); case MessageTypes.File: diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart new file mode 100644 index 00000000..828c76db --- /dev/null +++ b/lib/pages/chat/events/video_player.dart @@ -0,0 +1,123 @@ +//@dart=2.12 + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flick_video_player/flick_video_player.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:video_player/video_player.dart'; + +import 'package:fluffychat/pages/chat/events/image_bubble.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions.dart/event_extension.dart'; +import 'package:fluffychat/utils/sentry_controller.dart'; + +class EventVideoPlayer extends StatefulWidget { + final Event event; + const EventVideoPlayer(this.event, {Key? key}) : super(key: key); + + @override + _EventVideoPlayerState createState() => _EventVideoPlayerState(); +} + +class _EventVideoPlayerState extends State { + FlickManager? _flickManager; + bool _isDownloading = false; + String? _networkUri; + File? _tmpFile; + + void _downloadAction() async { + setState(() => _isDownloading = true); + try { + final videoFile = await widget.event.downloadAndDecryptAttachment(); + if (kIsWeb) { + final blob = html.Blob([videoFile.bytes]); + _networkUri = html.Url.createObjectUrlFromBlob(blob); + } else { + final tmpDir = await getTemporaryDirectory(); + final file = File(tmpDir.path + videoFile.name); + if (await file.exists() == false) { + await file.writeAsBytes(videoFile.bytes); + } + _tmpFile = file; + } + } on MatrixConnectionException catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(e.toLocalizedString(context)), + )); + } catch (e, s) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(e.toLocalizedString(context)), + )); + SentryController.captureException(e, s); + } finally { + setState(() => _isDownloading = false); + } + } + + @override + void dispose() { + _flickManager?.dispose(); + super.dispose(); + } + + static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; + + @override + Widget build(BuildContext context) { + final hasThumbnail = widget.event.hasThumbnail; + final blurHash = (widget.event.infoMap as Map) + .tryGet('xyz.amorgan.blurhash') ?? + fallbackBlurHash; + final videoFile = _tmpFile; + final networkUri = _networkUri; + if (kIsWeb && networkUri != null && _flickManager == null) { + _flickManager = FlickManager( + videoPlayerController: VideoPlayerController.network(networkUri), + ); + } else if (!kIsWeb && videoFile != null && _flickManager == null) { + _flickManager = FlickManager( + videoPlayerController: VideoPlayerController.file(videoFile), + autoPlay: true, + ); + } + + final flickManager = _flickManager; + return SizedBox( + width: 400, + height: 300, + child: Stack( + children: [ + if (flickManager == null) ...[ + if (hasThumbnail) + ImageBubble(widget.event) + else + BlurHash(hash: blurHash), + Center( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.surface, + ), + icon: _isDownloading + ? const CircularProgressIndicator.adaptive(strokeWidth: 2) + : const Icon(Icons.download_outlined), + label: Text( + L10n.of(context)! + .videoWithSize(widget.event.sizeString ?? '?MB'), + ), + onPressed: _isDownloading ? null : _downloadAction, + ), + ), + ] else + FlickVideoPlayer(flickManager: flickManager), + ], + ), + ); + } +} diff --git a/lib/pages/video_viewer/video_viewer.dart b/lib/pages/video_viewer/video_viewer.dart deleted file mode 100644 index c67096ad..00000000 --- a/lib/pages/video_viewer/video_viewer.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -import 'package:chewie/chewie.dart'; -import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:video_player/video_player.dart'; -import 'package:vrouter/vrouter.dart'; - -import '../../utils/matrix_sdk_extensions.dart/event_extension.dart'; -import '../../utils/platform_infos.dart'; -import '../../widgets/matrix.dart'; -import 'video_viewer_view.dart'; - -class VideoViewer extends StatefulWidget { - final Event event; - - const VideoViewer(this.event, {Key key}) : super(key: key); - - @override - VideoViewerController createState() => VideoViewerController(); -} - -class VideoViewerController extends State { - VideoPlayerController videoPlayerController; - ChewieController chewieController; - dynamic error; - - @override - void initState() { - super.initState(); - (() async { - try { - if (widget.event.content['file'] is Map) { - if (PlatformInfos.isWeb) { - throw 'Encrypted videos unavailable in web'; - } - final tempDirectory = (await getTemporaryDirectory()).path; - final mxcUri = widget.event.content - .tryGet>('file') - ?.tryGet('url'); - if (mxcUri == null) { - throw 'No mxc uri found'; - } - // somehow the video viewer doesn't like the uri-encoded slashes, so we'll just gonna replace them with hyphons - final file = File( - '$tempDirectory/videos/${mxcUri.replaceAll(':', '').replaceAll('/', '-')}'); - if (await file.exists() == false) { - final matrixFile = - await widget.event.downloadAndDecryptAttachmentCached(); - await file.create(recursive: true); - await file.writeAsBytes(matrixFile.bytes); - } - videoPlayerController = VideoPlayerController.file(file); - } else if (widget.event.content['url'] is String) { - videoPlayerController = VideoPlayerController.network( - widget.event.getAttachmentUrl()?.toString()); - } else { - throw 'invalid event'; - } - await videoPlayerController.initialize(); - - chewieController = ChewieController( - videoPlayerController: videoPlayerController, - autoPlay: true, - looping: false, - ); - setState(() => null); - } catch (e) { - setState(() => error = e); - } - })(); - } - - @override - void dispose() { - chewieController?.dispose(); - videoPlayerController?.dispose(); - super.dispose(); - } - - /// Forward this video to another room. - void forwardAction() { - Matrix.of(context).shareContent = widget.event.content; - VRouter.of(context).to('/rooms'); - } - - /// Save this file with a system call. - void saveFileAction() => widget.event.saveFile(context); - - @override - Widget build(BuildContext context) => VideoViewerView(this); -} diff --git a/lib/pages/video_viewer/video_viewer_view.dart b/lib/pages/video_viewer/video_viewer_view.dart deleted file mode 100644 index 78adc34b..00000000 --- a/lib/pages/video_viewer/video_viewer_view.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:chewie/chewie.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; - -import 'video_viewer.dart'; - -class VideoViewerView extends StatelessWidget { - final VideoViewerController controller; - - const VideoViewerView(this.controller, {Key key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - extendBodyBehindAppBar: true, - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: Navigator.of(context).pop, - color: Colors.white, - tooltip: L10n.of(context).close, - ), - backgroundColor: const Color(0x44000000), - actions: [ - IconButton( - icon: const Icon(Icons.reply_outlined), - onPressed: controller.forwardAction, - color: Colors.white, - tooltip: L10n.of(context).share, - ), - IconButton( - icon: const Icon(Icons.download_outlined), - onPressed: controller.saveFileAction, - color: Colors.white, - tooltip: L10n.of(context).downloadFile, - ), - ], - ), - body: Center( - child: controller.error != null - ? Text(controller.error.toString()) - : (controller.chewieController == null - ? const CircularProgressIndicator.adaptive(strokeWidth: 2) - : Chewie( - controller: controller.chewieController, - )), - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index 5c4ce32a..cb5b7cf1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -148,13 +148,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" - chewie: - dependency: "direct main" - description: - name: chewie - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.2" cli_util: dependency: transitive description: @@ -358,6 +351,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.2+1" + flick_video_player: + dependency: "direct main" + description: + name: flick_video_player + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" fluffybox: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 65301e60..9410ce1a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: audioplayers: ^0.20.1 blurhash_dart: ^1.1.0 cached_network_image: ^3.1.0 - chewie: ^1.2.2 cupertino_icons: any desktop_drop: ^0.2.0 desktop_notifications: ^0.6.1 @@ -23,6 +22,7 @@ dependencies: #fcm_shared_isolate: # git: https://gitlab.com/famedly/libraries/fcm_shared_isolate.git file_picker_cross: ^4.5.0 + flick_video_player: ^0.3.1 flutter: sdk: flutter flutter_app_badger: ^1.3.0