diff --git a/lib/main.dart b/lib/main.dart index 69fdff1bf..d25c8298d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:timezone/data/latest.dart' as tz; +import 'package:media_kit/media_kit.dart'; import 'package:extera_next/config/app_config.dart'; import 'package:extera_next/utils/client_manager.dart'; @@ -40,6 +41,8 @@ void main() async { // widget bindings are initialized already. WidgetsFlutterBinding.ensureInitialized(); + MediaKit.ensureInitialized(); + tz.initializeTimeZones(); FlutterForegroundTask.initCommunicationPort(); diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 1c12601be..05ec9f529 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -38,7 +38,7 @@ class ImageViewerController extends State { (event) => { MessageTypes.Image, MessageTypes.Sticker, - if (PlatformInfos.supportsVideoPlayer) MessageTypes.Video, + MessageTypes.Video, }.contains(event.messageType), ) .toList() diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 72e07d098..717e9d707 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -4,6 +4,8 @@ import 'package:matrix/matrix.dart'; import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:extera_next/pages/image_viewer/video_player.dart'; +import 'package:extera_next/utils/clipboard_utils.dart'; +import 'package:extera_next/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:extera_next/utils/platform_infos.dart'; import 'package:extera_next/widgets/hover_builder.dart'; import 'package:extera_next/widgets/mxc_image.dart'; @@ -195,21 +197,83 @@ class _ZoomableImageState extends State<_ZoomableImage> { super.dispose(); } + void _onSecondaryTapUp(TapUpDetails details) { + if (!PlatformInfos.isDesktop) return; + final overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; + showMenu( + context: context, + position: RelativeRect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + overlay.size.width - details.globalPosition.dx, + overlay.size.height - details.globalPosition.dy, + ), + items: [ + PopupMenuItem( + value: 0, + child: Row( + children: [ + const Icon(Icons.copy_outlined, size: 20), + const SizedBox(width: 8), + Text(L10n.of(context).copy), + ], + ), + ), + PopupMenuItem( + value: 1, + child: Row( + children: [ + const Icon(Icons.save_alt_outlined, size: 20), + const SizedBox(width: 8), + Text(L10n.of(context).downloadFile), + ], + ), + ), + ], + ).then((value) { + if (value == 0) { + _copyImageToClipboard(); + } else if (value == 1) { + widget.event.saveFile(context); + } + }); + } + + Future _copyImageToClipboard() async { + try { + final data = await widget.event.downloadAndDecryptAttachment( + getThumbnail: false, + ); + await writeImageToClipboard(data.bytes); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context).copiedToClipboard), + duration: const Duration(seconds: 1), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to copy image: $e')), + ); + } + } + } + @override Widget build(BuildContext context) { return InteractiveViewer( transformationController: _transformController, minScale: 1.0, maxScale: 10.0, - // When the user puts fingers on screen, disable PageView scroll onInteractionStart: (_) { widget.onZoomStatusChanged(true); }, - // When interaction ends, check if we are still zoomed in onInteractionEnd: (details) { widget.controller.onInteractionEnds(details); - - // Identity matrix means scale is 1.0 and offset is 0,0 final isZoomed = _transformController.value.row0[0] != 1.0; if (!isZoomed) { widget.onZoomStatusChanged(false); @@ -220,6 +284,7 @@ class _ZoomableImageState extends State<_ZoomableImage> { tag: widget.event.eventId, child: GestureDetector( onTap: () {}, + onSecondaryTapUp: _onSecondaryTapUp, child: MxcImage( key: ValueKey(widget.event.eventId), event: widget.event, diff --git a/lib/pages/image_viewer/video_player.dart b/lib/pages/image_viewer/video_player.dart index 74674dedc..7c1c915e1 100644 --- a/lib/pages/image_viewer/video_player.dart +++ b/lib/pages/image_viewer/video_player.dart @@ -3,15 +3,13 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:chewie/chewie.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:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; import 'package:extera_next/pages/image_viewer/image_viewer.dart'; import 'package:extera_next/utils/localized_exception_extension.dart'; -import 'package:extera_next/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:extera_next/utils/platform_infos.dart'; import 'package:extera_next/widgets/blur_hash.dart'; import '../../../utils/error_reporter.dart'; @@ -28,25 +26,17 @@ class EventVideoPlayer extends StatefulWidget { } class EventVideoPlayerState extends State { - ChewieController? _chewieController; - VideoPlayerController? _videoPlayerController; + Player? _mediaKitPlayer; + VideoController? _mediaKitController; double? _downloadProgress; - // The video_player package only doesn't support Windows and Linux. - final _supportsVideoPlayer = - !PlatformInfos.isWindows && !PlatformInfos.isLinux; - void _downloadAction() async { - if (!_supportsVideoPlayer) { - widget.event.saveFile(context); - return; - } - try { - // Dispose the controllers if we already have them. _disposeControllers(); - late VideoPlayerController videoPlayerController; + final player = Player(); + _mediaKitPlayer = player; + _mediaKitController = VideoController(player); if (widget.event.room.encrypted) { final fileSize = widget.event.content @@ -65,68 +55,54 @@ class EventVideoPlayerState extends State { }); }, ); - // Create the VideoPlayerController from the contents of videoFile. - if (kIsWeb) { - final blob = html.Blob([videoFile.bytes]); - final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob)); - videoPlayerController = VideoPlayerController.networkUrl(networkUri); - } else { - final tempDir = await getTemporaryDirectory(); - final fileName = Uri.encodeComponent( - widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, - ); - final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); - if (await file.exists() == false) { - await file.writeAsBytes(videoFile.bytes); - } - videoPlayerController = VideoPlayerController.file(file); + + final tempDir = await getTemporaryDirectory(); + final fileName = Uri.encodeComponent( + widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + ); + final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); + if (!await file.exists()) { + await file.writeAsBytes(videoFile.bytes); } + await player.open(Media(file.path)); } else { final videoUrl = await widget.event.attachmentMxcUrl!.getDownloadUri( widget.event.room.client, ); Logs().d("Video url: $videoUrl"); - videoPlayerController = VideoPlayerController.networkUrl( - videoUrl, - httpHeaders: { - 'authorization': 'Bearer ${widget.event.room.client.accessToken}', - }, + await player.open( + Media( + videoUrl.toString(), + httpHeaders: { + 'authorization': 'Bearer ${widget.event.room.client.accessToken}', + }, + ), ); } - _videoPlayerController = videoPlayerController; - - await videoPlayerController.initialize(); - if (widget.ivController.currentEvent.eventId != widget.event.eventId) { dispose(); return; } - // Create a ChewieController on top. - setState(() { - _chewieController = ChewieController( - videoPlayerController: videoPlayerController, - useRootNavigator: !kIsWeb, - autoPlay: true, - autoInitialize: true, - looping: true, - ); - }); + setState(() {}); } on IOException catch (e) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); + } } catch (e, s) { - ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); + if (mounted) { + ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); + } } } void _disposeControllers() { - _chewieController?.dispose(); - _videoPlayerController?.dispose(); - _chewieController = null; - _videoPlayerController = null; + _mediaKitPlayer?.dispose(); + _mediaKitPlayer = null; + _mediaKitController = null; } @override @@ -159,47 +135,53 @@ class EventVideoPlayerState extends State { final height = MediaQuery.of(context).size.height - 52; final width = videoWidth * (height / videoHeight); - final chewieController = _chewieController; - return chewieController != null - ? Center( - child: SizedBox( - width: width, - height: height, - child: Chewie(controller: chewieController), - ), - ) - : Stack( - children: [ - Center( - child: Hero( - tag: widget.event.eventId, - child: hasThumbnail - ? MxcImage( - event: widget.event, - isThumbnail: true, - width: width, - height: height, - fit: BoxFit.cover, - placeholder: (context) => BlurHash( - blurhash: blurHash, - width: width, - height: height, - fit: BoxFit.cover, - ), - ) - : BlurHash( - blurhash: blurHash, - width: width, - height: height, - ), - ), - ), - Center( - child: CircularProgressIndicator.adaptive( - value: _downloadProgress, - ), - ), - ], - ); + final mediaKitController = _mediaKitController; + + if (mediaKitController != null) { + return Center( + child: SizedBox( + width: width, + height: height, + child: Video( + controller: mediaKitController, + controls: MaterialVideoControls, + ), + ), + ); + } + + return Stack( + children: [ + Center( + child: Hero( + tag: widget.event.eventId, + child: hasThumbnail + ? MxcImage( + event: widget.event, + isThumbnail: true, + width: width, + height: height, + fit: BoxFit.cover, + placeholder: (context) => BlurHash( + blurhash: blurHash, + width: width, + height: height, + fit: BoxFit.cover, + ), + ) + : BlurHash( + blurhash: blurHash, + width: width, + height: height, + ), + ), + ), + Center( + child: CircularProgressIndicator.adaptive( + value: _downloadProgress, + ), + ), + ], + ); } } diff --git a/lib/utils/clipboard_utils.dart b/lib/utils/clipboard_utils.dart index f9728abe1..d493cf402 100644 --- a/lib/utils/clipboard_utils.dart +++ b/lib/utils/clipboard_utils.dart @@ -3,6 +3,27 @@ import 'dart:typed_data'; import 'package:pasteboard/pasteboard.dart'; +Future writeImageToClipboard(Uint8List bytes) async { + if (Platform.isWindows || Platform.isIOS || Platform.isMacOS || Platform.isAndroid) { + await Pasteboard.writeImage(bytes); + } else if (Platform.isLinux) { + final tempFile = File('${Directory.systemTemp.path}/clipboard_image.png'); + await tempFile.writeAsBytes(bytes); + try { + await Process.run('xclip', [ + '-selection', 'clipboard', + '-t', 'image/png', + '-i', tempFile.path, + ]); + } catch (_) { + try { + await Process.run('wl-copy', ['--type', 'image/png', tempFile.path]); + } catch (_) {} + } + await tempFile.delete(); + } +} + Future getImageFromClipboardLinux() async { final cmds = [ ['wl-paste', '-t', 'image/png'], diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index a56808357..dc6fa62b3 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -30,7 +30,7 @@ abstract class PlatformInfos { static bool get usesTouchscreen => !isMobile; - static bool get supportsVideoPlayer => !isWindows && !isLinux; + static bool get supportsVideoPlayer => true; /// Web could also record in theory but currently only wav which is too large static bool get platformCanRecord => (isMobile || isMacOS); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a236cb121..7dba15c96 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -61,6 +63,12 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) livekit_client_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "LiveKitPlugin"); live_kit_plugin_register_with_registrar(livekit_client_registrar); + g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); + media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); + g_autoptr(FlPluginRegistrar) media_kit_video_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); + media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); g_autoptr(FlPluginRegistrar) open_file_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 35928031a..0a39883c5 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -14,6 +14,8 @@ list(APPEND FLUTTER_PLUGIN_LIST handy_window hotkey_manager_linux livekit_client + media_kit_libs_linux + media_kit_video open_file_linux pasteboard record_linux diff --git a/pubspec.lock b/pubspec.lock index 6ab772312..7a902e127 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -274,14 +274,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" - chewie: - dependency: "direct main" - description: - name: chewie - sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca" - url: "https://pub.dev" - source: hosted - version: "1.13.0" cli_config: dependency: transitive description: @@ -1438,6 +1430,70 @@ packages: relative: true source: path version: "7.3.0" + media_kit: + dependency: "direct main" + description: + name: media_kit + sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643 + url: "https://pub.dev" + source: hosted + version: "1.2.6" + media_kit_libs_android_video: + dependency: transitive + description: + name: media_kit_libs_android_video + sha256: "3f6274e5ab2de512c286a25c327288601ee445ed8ac319e0ef0b66148bd8f76c" + url: "https://pub.dev" + source: hosted + version: "1.3.8" + media_kit_libs_ios_video: + dependency: transitive + description: + name: media_kit_libs_ios_video + sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 + url: "https://pub.dev" + source: hosted + version: "1.1.4" + media_kit_libs_linux: + dependency: transitive + description: + name: media_kit_libs_linux + sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + media_kit_libs_macos_video: + dependency: transitive + description: + name: media_kit_libs_macos_video + sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d + url: "https://pub.dev" + source: hosted + version: "1.1.4" + media_kit_libs_video: + dependency: "direct main" + description: + name: media_kit_libs_video + sha256: "2b235b5dac79c6020e01eef5022c6cc85fedc0df1738aadc6ea489daa12a92a9" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + media_kit_libs_windows_video: + dependency: transitive + description: + name: media_kit_libs_windows_video + sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab + url: "https://pub.dev" + source: hosted + version: "1.0.11" + media_kit_video: + dependency: "direct main" + description: + name: media_kit_video + sha256: afaa509e7b7e0bf247557a3a740cde903a52c34ace9810f94500e127bd7b043d + url: "https://pub.dev" + source: hosted + version: "2.0.1" meta: dependency: transitive description: @@ -2014,6 +2070,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: "7483b3d5e8976f0bd263647c03b96131ee8e43f48b56fa8a8ec459e8515d74b0" + url: "https://pub.dev" + source: hosted + version: "2.0.4" safe_url_check: dependency: transitive description: @@ -2507,6 +2571,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.2" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" + url: "https://pub.dev" + source: hosted + version: "3.0.2" url_launcher: dependency: "direct main" description: @@ -2619,46 +2691,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" - url: "https://pub.dev" - source: hosted - version: "2.11.1" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" - url: "https://pub.dev" - source: hosted - version: "2.9.5" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e - url: "https://pub.dev" - source: hosted - version: "2.9.4" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" - url: "https://pub.dev" - source: hosted - version: "6.6.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.dev" - source: hosted - version: "2.4.0" vm_service: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5eaf76521..703043a49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,6 @@ dependencies: badges: ^3.1.2 blurhash_dart: ^1.2.1 camera: ^0.12.0+1 - chewie: ^1.13.0 collection: ^1.18.0 cross_file: ^0.3.5+2 cupertino_icons: any @@ -102,7 +101,9 @@ dependencies: universal_html: ^2.3.0 url_launcher: ^6.3.2 video_compress: ^3.1.4 - video_player: ^2.11.1 + media_kit: ^1.2.6 + media_kit_video: ^2.0.1 + media_kit_libs_video: ^1.0.7 vodozemac: ^0.5.0 wakelock_plus: ^1.4.0 webrtc_interface: ^1.4.0 diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d2071a..903f4899d 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b67a3aab2..2d2d53702 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -54,6 +56,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); LiveKitPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LiveKitPlugin")); + MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); + MediaKitVideoPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); PasteboardPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PasteboardPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b4362e475..76a778fe3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -15,6 +15,8 @@ list(APPEND FLUTTER_PLUGIN_LIST geolocator_windows hotkey_manager_windows livekit_client + media_kit_libs_windows_video + media_kit_video pasteboard permission_handler_windows record_windows