diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 624d6c977c..14dc60e499 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -16,7 +16,7 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -42,6 +42,7 @@ jobs: sudo apt-get install -y libsdl2-dev sudo apt-get install -y libwebkit2gtk-4.1-dev sudo apt-get install -y libopus-dev libogg-dev + sudo apt-get install -y libmpv-dev mpv sudo apt-get install -y libcurl4-openssl-dev flutter doctor diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml index bffa6947de..8e9bbcc643 100644 --- a/.github/workflows/manual-build.yml +++ b/.github/workflows/manual-build.yml @@ -45,10 +45,10 @@ jobs: matrix: include: - arch: amd64 - runner: ubuntu-22.04 + runner: ubuntu-24.04 flutter_arch: x64 - arch: arm64 - runner: ubuntu-22.04-arm + runner: ubuntu-24.04-arm flutter_arch: arm64 steps: - run: g++ --version @@ -95,6 +95,7 @@ jobs: sudo apt-get install -y libsdl2-dev sudo apt-get install -y libwebkit2gtk-4.1-dev sudo apt-get install -y libopus-dev libogg-dev + sudo apt-get install -y libmpv-dev mpv sudo apt-get install -y libcurl4-openssl-dev flutter doctor @@ -169,11 +170,11 @@ jobs: matrix: include: - arch: amd64 - runner: ubuntu-22.04 + runner: ubuntu-24.04 flutter_arch: x64 artifact_suffix: "" - arch: arm64 - runner: ubuntu-22.04-arm + runner: ubuntu-24.04-arm flutter_arch: arm64 artifact_suffix: "-arm64" steps: @@ -227,13 +228,13 @@ jobs: matrix: include: - arch: amd64 - runner: ubuntu-22.04 + runner: ubuntu-24.04 flutter_arch: x64 appimage_arch: x86_64 recipe: ./AppImageBuilder.yml artifact_suffix: "" - arch: arm64 - runner: ubuntu-22.04-arm + runner: ubuntu-24.04-arm flutter_arch: arm64 appimage_arch: aarch64 recipe: ./AppImageBuilder-arm64.yml @@ -255,7 +256,7 @@ jobs: - name: Install appimage-builder dependencies run: | sudo apt-get update - sudo apt-get install -y python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace binutils coreutils + sudo apt-get install -y python3-venv patchelf desktop-file-utils libgdk-pixbuf-2.0-dev fakeroot strace binutils coreutils - name: Download appimagetool run: | @@ -267,10 +268,15 @@ jobs: sudo chmod +x /usr/local/bin/appimagetool - name: Install appimage-builder - run: sudo pip3 install appimage-builder==1.1.0 + run: | + python3 -m venv "${{ runner.temp }}/appimage-builder-venv" + # appimage-builder 1.1.0 relies on LegacyVersion when comparing + # Debian-style versions such as 1.22.6ubuntu6. + "${{ runner.temp }}/appimage-builder-venv/bin/pip" install appimage-builder==1.1.0 "packaging<22" - name: Build AppImage - run: appimage-builder --recipe ${{ matrix.recipe }} --skip-test + run: | + "${{ runner.temp }}/appimage-builder-venv/bin/appimage-builder" --recipe "${{ matrix.recipe }}" --skip-test - run: mv "Mixin Messenger-latest-${{ matrix.appimage_arch }}.AppImage" mixin_desktop_linux_${{ matrix.arch }}.AppImage - name: Upload AppImage Artifact diff --git a/AppImageBuilder-arm64.yml b/AppImageBuilder-arm64.yml index a66e70fa2a..8de512388a 100644 --- a/AppImageBuilder-arm64.yml +++ b/AppImageBuilder-arm64.yml @@ -18,18 +18,19 @@ AppDir: arch: arm64 allow_unauthenticated: true sources: - - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe multiverse - - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe multiverse - - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse - - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble main restricted universe multiverse + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble-updates main restricted universe multiverse + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble-backports main restricted universe multiverse + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ noble-security main restricted universe multiverse include: - - libgtk-3-0 + - libgtk-3-0t64 - libwebkit2gtk-4.1-0 # Bundle GTK input method clients, but keep the input method daemon on the host. - ibus-gtk3 - fcitx5-frontend-gtk3 - libsdl2-2.0-0 - libopus0 + - libmpv2 exclude: - humanity-icon-theme - hicolor-icon-theme @@ -45,7 +46,7 @@ AppDir: after_bundle: # Package extraction does not run Debian's GTK cache trigger. AppRun v2 maps # the generated build-time AppDir paths to the runtime AppDir mount point. - - LD_LIBRARY_PATH=AppDir/lib/aarch64-linux-gnu:AppDir/usr/lib/aarch64-linux-gnu AppDir/usr/lib/aarch64-linux-gnu/libgtk-3-0/gtk-query-immodules-3.0 AppDir/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules/*.so > AppDir/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules.cache + - LD_LIBRARY_PATH=AppDir/lib/aarch64-linux-gnu:AppDir/usr/lib/aarch64-linux-gnu AppDir/usr/lib/aarch64-linux-gnu/libgtk-3-0t64/gtk-query-immodules-3.0 AppDir/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules/*.so > AppDir/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules.cache runtime: env: GIO_MODULE_DIR: $APPDIR/usr/lib/aarch64-linux-gnu/gio/modules/ diff --git a/AppImageBuilder.yml b/AppImageBuilder.yml index 5d770ca2fe..e4895f4c62 100644 --- a/AppImageBuilder.yml +++ b/AppImageBuilder.yml @@ -18,18 +18,19 @@ AppDir: arch: amd64 allow_unauthenticated: true sources: - - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse - - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security main restricted universe multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ noble-updates main restricted universe multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse + - sourceline: deb http://security.ubuntu.com/ubuntu noble-security main restricted universe multiverse include: - - libgtk-3-0 + - libgtk-3-0t64 - libwebkit2gtk-4.1-0 # Bundle GTK input method clients, but keep the input method daemon on the host. - ibus-gtk3 - fcitx5-frontend-gtk3 - libsdl2-2.0-0 - libopus0 + - libmpv2 exclude: - humanity-icon-theme - hicolor-icon-theme @@ -45,7 +46,7 @@ AppDir: after_bundle: # Package extraction does not run Debian's GTK cache trigger. AppRun v2 maps # the generated build-time AppDir paths to the runtime AppDir mount point. - - LD_LIBRARY_PATH=AppDir/lib/x86_64-linux-gnu:AppDir/usr/lib/x86_64-linux-gnu AppDir/usr/lib/x86_64-linux-gnu/libgtk-3-0/gtk-query-immodules-3.0 AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/*.so > AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache + - LD_LIBRARY_PATH=AppDir/lib/x86_64-linux-gnu:AppDir/usr/lib/x86_64-linux-gnu AppDir/usr/lib/x86_64-linux-gnu/libgtk-3-0t64/gtk-query-immodules-3.0 AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/*.so > AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules.cache runtime: env: GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ diff --git a/README.md b/README.md index ddaa8ad145..2795f11d5f 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,15 @@ sudo apt-get install libsqlite3-dev sudo apt-get install webkit2gtk-4.1 sudo apt-get install libsdl2-dev sudo apt-get install libopus-dev libogg-dev +sudo apt-get install libmpv-dev mpv ``` +### Portable archive runtime + +The portable Linux archive uses the system's shared libraries. Install the +`libmpv` runtime before using it (`libmpv2` on Debian and Ubuntu). + # License Released under the [GPLv3](https://github.com/MixinNetwork/flutter-app/blob/master/LICENSE) license. - diff --git a/dist/deb/DEBIAN/control b/dist/deb/DEBIAN/control index 5db60cbcc0..e69094c466 100644 --- a/dist/deb/DEBIAN/control +++ b/dist/deb/DEBIAN/control @@ -5,5 +5,5 @@ Homepage: https://mixin.one/messenger Package: mixin-desktop Maintainer: Mixin Team Architecture: amd64 -Depends: libgtk-3-0 (>= 3.10.0), libsdl2-2.0-0, libwebkit2gtk-4.1-0, libopus0 +Depends: libgtk-3-0t64 (>= 3.24.41), libsdl2-2.0-0, libwebkit2gtk-4.1-0, libopus0, libmpv2 Description: Mixin messenger desktop diff --git a/dist/snap/snap/snapcraft.yaml b/dist/snap/snap/snapcraft.yaml index 73f0e0c562..601ecf98f0 100644 --- a/dist/snap/snap/snapcraft.yaml +++ b/dist/snap/snap/snapcraft.yaml @@ -36,6 +36,7 @@ parts: plugin: dump stage-packages: - libsdl2-2.0-0 - - libgtk-3-0 + - libgtk-3-0t64 - libwebkit2gtk-4.1-0 - - libopus0 \ No newline at end of file + - libopus0 + - libmpv2 diff --git a/lib/main.dart b/lib/main.dart index 29de4f3a96..d78561fcd6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:isolate/isolate.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:overlay_support/overlay_support.dart'; import 'package:protocol_handler/protocol_handler.dart'; import 'package:rhttp/rhttp.dart'; @@ -43,6 +44,7 @@ Future _runApp(List args) async { await Rhttp.init(); WidgetsFlutterBinding.ensureInitialized(); + MediaKit.ensureInitialized(); await loadFallbackFonts(); diff --git a/lib/ui/home/chat/files_preview.dart b/lib/ui/home/chat/files_preview.dart index 4abeaf28d3..c8f49c0d03 100644 --- a/lib/ui/home/chat/files_preview.dart +++ b/lib/ui/home/chat/files_preview.dart @@ -14,12 +14,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image/image.dart' as image; +import 'package:media_kit_video/media_kit_video.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:super_context_menu/super_context_menu.dart'; import 'package:video_compress/video_compress.dart'; -import 'package:video_player/video_player.dart'; import '../../../constants/brightness_theme_data.dart'; import '../../../constants/constants.dart'; @@ -41,6 +41,7 @@ import '../../../widgets/high_light_text.dart'; import '../../../widgets/image.dart'; import '../../../widgets/interactive_decorated_box.dart'; import '../../../widgets/menu.dart'; +import '../../../widgets/message/item/video/media_kit_video_player.dart'; import '../../../widgets/mixin_image.dart'; import '../../provider/conversation_provider.dart'; import '../../provider/quote_message_provider.dart'; @@ -779,11 +780,12 @@ class _AnimatedListBuilder extends HookConsumerWidget { } final _videoControllerProvider = ChangeNotifierProvider.autoDispose - .family( - (ref, file) => VideoPlayerController.file(File(file.path)) - ..initialize() - ..setVolume(0) - ..setLooping(true), + .family( + (ref, file) => MediaKitVideoPlayer( + file.path, + looping: true, + muted: true, + ), ); final _videoBlurHashProvider = FutureProvider.autoDispose @@ -832,7 +834,10 @@ class _TileBigVideo extends HookConsumerWidget { Center( child: AspectRatio( aspectRatio: aspectRatio, - child: VideoPlayer(controller), + child: Video( + controller: controller.controller, + controls: null, + ), ), ), Positioned( diff --git a/lib/ui/home/chat/voice_recorder_bottom_bar.dart b/lib/ui/home/chat/voice_recorder_bottom_bar.dart index 17151b644e..e0a57baea0 100644 --- a/lib/ui/home/chat/voice_recorder_bottom_bar.dart +++ b/lib/ui/home/chat/voice_recorder_bottom_bar.dart @@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart' hide ChangeNotifierProvider; +import 'package:media_kit/media_kit.dart'; import 'package:ogg_opus_player/ogg_opus_player.dart'; import 'package:provider/provider.dart'; diff --git a/lib/ui/home/chat/voice_recorder_state.dart b/lib/ui/home/chat/voice_recorder_state.dart index e32af59861..e4172a473f 100644 --- a/lib/ui/home/chat/voice_recorder_state.dart +++ b/lib/ui/home/chat/voice_recorder_state.dart @@ -170,39 +170,45 @@ class VoiceRecorderNotifier extends ValueNotifier { } class _Player { - _Player(this.path); + _Player(this.path) : _player = Player() { + _subscriptions.addAll([ + _player.stream.playing.listen((playing) { + isPlaying.value = playing; + }), + _player.stream.completed.listen((completed) { + if (completed) { + unawaited(stop()); + } + }), + ]); + } final String path; final isPlaying = ValueNotifier(false); - double get position => _player?.currentPosition ?? 0.0; + double get position => + _player.state.position.inMicroseconds / Duration.microsecondsPerSecond; - OggOpusPlayer? _player; + final Player _player; + final _subscriptions = >[]; Future start() async { await AudioSession.instance.activePlayback(); - final player = OggOpusPlayer(path); - player.state.addListener(() { - final state = player.state.value; - isPlaying.value = state == PlayerState.playing; - if (state == PlayerState.ended) { - stop(); - } - }); - player.play(); - _player = player; + await _player.open(Media(path)); } Future stop() async { - _player?.pause(); - _player?.dispose(); - _player = null; + await _player.stop(); await AudioSession.instance.deactivate(); isPlaying.value = false; } void dispose() { - stop(); + unawaited(stop()); + for (final subscription in _subscriptions) { + unawaited(subscription.cancel()); + } + unawaited(_player.dispose()); } } diff --git a/lib/utils/audio_message_player/ogg_opus_audio_player.dart b/lib/utils/audio_message_player/ogg_opus_audio_player.dart index a523b5aa1f..f91b35acf8 100644 --- a/lib/utils/audio_message_player/ogg_opus_audio_player.dart +++ b/lib/utils/audio_message_player/ogg_opus_audio_player.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:ogg_opus_player/ogg_opus_player.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:rxdart/rxdart.dart'; import '../../db/mixin_database.dart'; @@ -31,9 +31,19 @@ enum PlaybackState { } class AudioMessagePlayer { + AudioMessagePlayer() { + _subscriptions.addAll([ + _player.stream.playing.listen(_handlePlaying), + _player.stream.completed.listen(_handleCompleted), + _player.stream.error.listen(_handleError), + ]); + } + final _currentPlaying = BehaviorSubject(); - OggOpusPlayer? _player; + final Player _player = Player(); + + final _subscriptions = >[]; final List _medias = []; @@ -49,6 +59,10 @@ class AudioMessagePlayer { void dispose() { stop(); + for (final subscription in _subscriptions) { + unawaited(subscription.cancel()); + } + unawaited(_player.dispose()); _medias.clear(); _index = -1; } @@ -75,8 +89,6 @@ class AudioMessagePlayer { void _playNext() { if (_index >= _medias.length - 1) { - // play ended. - _disposeCurrentPlayer(); _playbackState.value = PlaybackState.completed; return; } @@ -85,79 +97,65 @@ class AudioMessagePlayer { } else { _index++; } - _disposeCurrentPlayer(); final media = _medias[_index]; - final player = OggOpusPlayer(media.mediaPath); - assert(_player == null); - _player = player; - player.state.addListener(_handlePlayerState); - player - ..play() - ..setPlaybackRate(_playbackSpeed.value); _currentPlaying.value = current; + unawaited(_open(media)); } - void _handlePlayerState() { - final player = _player; - if (player == null) { - return; + Future _open(MessageMedia media) async { + try { + await _player.setRate(_playbackSpeed.value); + await _player.open(Media(media.mediaPath)); + } catch (error, stacktrace) { + e('play ${media.mediaPath} failed: $error $stacktrace'); + stop(); } - const interceptedEventType = { - PlayerState.idle, - PlayerState.error, - PlayerState.ended, - }; - final state = player.state.value; - if (!interceptedEventType.contains(state)) { - if (state == PlayerState.paused) { - _playbackState.value = PlaybackState.paused; - } else if (state == PlayerState.playing) { - _playbackState.value = PlaybackState.playing; - } + } + + void _handlePlaying(bool playing) { + if (playing) { + _playbackState.value = PlaybackState.playing; + } else if (_playbackState.value == PlaybackState.playing && + !_player.state.completed) { + _playbackState.value = PlaybackState.paused; } - if (state == PlayerState.ended) { + } + + void _handleCompleted(bool completed) { + if (completed) { _playNext(); - } else if (state == PlayerState.error) { - i('play ${current?.mediaPath} failed.'); - stop(); } } - Stream get playbackStream => _playbackState.stream; - - void _disposeCurrentPlayer() { - _player?.state.removeListener(_handlePlayerState); - _player?.dispose(); - _player = null; + void _handleError(String error) { + e('play ${current?.mediaPath} failed: $error'); + stop(); } + Stream get playbackStream => _playbackState.stream; + void stop() { - _disposeCurrentPlayer(); + unawaited(_player.stop()); _playbackState.value = PlaybackState.idle; } void pause() { - _player?.pause(); + unawaited(_player.pause()); } void resume() { - if (_player == null) { - e('resume failed, player is null.'); - return; - } assert( _playbackState.value == PlaybackState.paused, 'resume failed, player is not paused.', ); - _player?.play(); + unawaited(_player.play()); _playbackState.value = PlaybackState.playing; } - Duration currentPosition() => - Duration(milliseconds: ((_player?.currentPosition ?? 0) * 1000).toInt()); + Duration currentPosition() => _player.state.position; void setPlaybackSpeed(double speed) { _playbackSpeed.value = speed; - _player?.setPlaybackRate(speed); + unawaited(_player.setRate(speed)); } } diff --git a/lib/widgets/message/item/video/media_kit_video_player.dart b/lib/widgets/message/item/video/media_kit_video_player.dart new file mode 100644 index 0000000000..95d7da19bb --- /dev/null +++ b/lib/widgets/message/item/video/media_kit_video_player.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:mixin_logger/mixin_logger.dart'; + +class MediaKitVideoValue { + const MediaKitVideoValue({ + required this.position, + required this.duration, + required this.buffer, + required this.volume, + required this.isPlaying, + required this.width, + required this.height, + }); + + final Duration position; + final Duration duration; + final Duration buffer; + final double volume; + final bool isPlaying; + final int? width; + final int? height; + + bool get isInitialized => + width != null && height != null && duration > Duration.zero; + + double get aspectRatio { + final width = this.width; + final height = this.height; + if (width == null || height == null || width == 0 || height == 0) { + return 1; + } + return width / height; + } +} + +class MediaKitVideoPlayer extends ChangeNotifier { + MediaKitVideoPlayer( + this.path, { + bool autoPlay = false, + bool looping = false, + bool muted = false, + }) : player = Player() { + controller = VideoController(player); + _subscriptions.addAll([ + player.stream.playing.listen(_notify), + player.stream.position.listen(_notify), + player.stream.duration.listen(_notify), + player.stream.buffer.listen(_notify), + player.stream.volume.listen(_notify), + player.stream.width.listen(_notify), + player.stream.height.listen(_notify), + ]); + _initialized = _open( + path, + autoPlay: autoPlay, + looping: looping, + muted: muted, + ); + } + + final Player player; + final String path; + late final VideoController controller; + + final _subscriptions = >[]; + late final Future _initialized; + var _disposed = false; + + MediaKitVideoValue get value { + final state = player.state; + return MediaKitVideoValue( + position: state.position, + duration: state.duration, + buffer: state.buffer, + volume: (state.volume / 100).clamp(0.0, 1.0), + isPlaying: state.playing, + width: state.width, + height: state.height, + ); + } + + Future _open( + String path, { + required bool autoPlay, + required bool looping, + required bool muted, + }) async { + try { + if (muted) { + await player.setVolume(0); + } + await player.open(Media(path), play: autoPlay); + if (looping) { + await player.setPlaylistMode(PlaylistMode.single); + } + } catch (error, stackTrace) { + if (!_disposed) { + e('video playback failed: $path $error $stackTrace'); + } + } + } + + Future play() async { + await _initialized; + await player.play(); + } + + Future pause() => player.pause(); + + Future seekTo(Duration position) => player.seek(position); + + Future setVolume(double volume) => + player.setVolume(volume.clamp(0.0, 1.0) * 100); + + void _notify(T _) { + if (!_disposed) { + notifyListeners(); + } + } + + @override + void dispose() { + _disposed = true; + for (final subscription in _subscriptions) { + unawaited(subscription.cancel()); + } + unawaited(player.dispose()); + super.dispose(); + } +} diff --git a/lib/widgets/message/item/video/progress_bar.dart b/lib/widgets/message/item/video/progress_bar.dart index d0b18b9716..102adf16e3 100644 --- a/lib/widgets/message/item/video/progress_bar.dart +++ b/lib/widgets/message/item/video/progress_bar.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; import '../../../../utils/logger.dart'; +import 'media_kit_video_player.dart'; /// From project: https://github.com/fluttercommunity/chewie class CupertinoVideoProgressBar extends StatelessWidget { @@ -16,7 +16,7 @@ class CupertinoVideoProgressBar extends StatelessWidget { super.key, }); - final VideoPlayerController controller; + final MediaKitVideoPlayer controller; final ChewieProgressColors? colors; final Function()? onDragStart; final Function()? onDragEnd; @@ -72,7 +72,7 @@ class VideoProgressBar extends StatefulWidget { super.key, }) : colors = colors ?? ChewieProgressColors(); - final VideoPlayerController controller; + final MediaKitVideoPlayer controller; final ChewieProgressColors colors; final Function()? onDragStart; final Function()? onDragEnd; @@ -96,7 +96,7 @@ class _VideoProgressBarState extends State { Offset? _latestDraggableOffset; - VideoPlayerController get controller => widget.controller; + MediaKitVideoPlayer get controller => widget.controller; @override void initState() { @@ -190,7 +190,7 @@ class StaticProgressBar extends StatelessWidget { }); final Offset? latestDraggableOffset; - final VideoPlayerValue value; + final MediaKitVideoValue value; final ChewieProgressColors colors; final double barHeight; @@ -224,7 +224,7 @@ class _ProgressBarPainter extends CustomPainter { required this.draggableValue, }); - VideoPlayerValue value; + MediaKitVideoValue value; ChewieProgressColors colors; final double barHeight; @@ -260,30 +260,30 @@ class _ProgressBarPainter extends CustomPainter { final playedPart = playedPartPercent > 1 ? size.width : playedPartPercent * size.width; - for (final range in value.buffered) { - final start = range.startFraction(value.duration) * size.width; - final end = range.endFraction(value.duration) * size.width; - canvas.drawRRect( + final bufferedPart = + value.buffer.inMilliseconds / value.duration.inMilliseconds; + final bufferedEnd = bufferedPart.clamp(0.0, 1.0) * size.width; + canvas + ..drawRRect( RRect.fromRectAndRadius( Rect.fromPoints( - Offset(start, baseOffset), - Offset(end, baseOffset + barHeight), + Offset(0, baseOffset), + Offset(bufferedEnd, baseOffset + barHeight), ), const Radius.circular(4), ), colors.bufferedPaint, - ); - } - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromPoints( - Offset(0, baseOffset), - Offset(playedPart, baseOffset + barHeight), + ) + ..drawRRect( + RRect.fromRectAndRadius( + Rect.fromPoints( + Offset(0, baseOffset), + Offset(playedPart, baseOffset + barHeight), + ), + const Radius.circular(4), ), - const Radius.circular(4), - ), - colors.playedPaint, - ); + colors.playedPaint, + ); if (drawShadow) { final shadowPath = Path() diff --git a/lib/widgets/message/item/video/video_message.dart b/lib/widgets/message/item/video/video_message.dart index 5ab1dc5541..c0243ba718 100644 --- a/lib/widgets/message/item/video/video_message.dart +++ b/lib/widgets/message/item/video/video_message.dart @@ -112,14 +112,14 @@ class MessageVideo extends HookConsumerWidget { message, isTranscriptPage, ); - if (kPlatformIsDarwin) { + if (kPlatformIsDarwin || Platform.isLinux || Platform.isWindows) { showVideoPreviewPage( context, path, message: message, isTranscriptPage: isTranscriptPage, ); - } else if (Platform.isIOS || Platform.isAndroid) { + } else if (Platform.isAndroid) { OpenFile.open(path); } else { openUri(context, Uri.file(path).toString()); diff --git a/lib/widgets/message/item/video/video_preview_page.dart b/lib/widgets/message/item/video/video_preview_page.dart index e4aa9f0411..ce4578d1b9 100644 --- a/lib/widgets/message/item/video/video_preview_page.dart +++ b/lib/widgets/message/item/video/video_preview_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math' as math; import 'dart:ui' as ui; @@ -9,7 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:video_player/video_player.dart'; +import 'package:media_kit_video/media_kit_video.dart'; import '../../../../constants/resources.dart'; import '../../../../db/mixin_database.dart'; @@ -19,6 +18,7 @@ import '../../../action_button.dart'; import '../../../avatar_view/avatar_view.dart'; import '../../../user_selector/conversation_selector.dart'; import '../../message.dart'; +import 'media_kit_video_player.dart'; import 'progress_bar.dart'; import 'slider.dart'; @@ -37,7 +37,7 @@ Future showVideoPreviewPage( barrierDismissible: false, ); -final videoPlayerProvider = ChangeNotifierProvider(( +final videoPlayerProvider = ChangeNotifierProvider(( ref, ) { throw UnimplementedError(); @@ -122,12 +122,9 @@ class _VideoPreviewPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final controller = useMemoized(() { - final controller = VideoPlayerController.file(File(path)); - return controller - ..initialize() - ..play(); - }); + final controller = useMemoized( + () => MediaKitVideoPlayer(path, autoPlay: true), + ); useEffect(() => controller.dispose, [controller]); @@ -273,9 +270,6 @@ class _PlayerShortcuts extends HookConsumerWidget { ), _CloseIntent: CallbackAction<_CloseIntent>( onInvoke: (intent) { - ref.read(videoPlayerProvider) - ..pause() - ..dispose(); Navigator.pop(context); }, ), @@ -406,7 +400,7 @@ class _Bar extends ConsumerWidget { ); } -class VideoFrame extends ConsumerWidget { +class VideoFrame extends HookConsumerWidget { const VideoFrame({super.key}); @override @@ -415,8 +409,38 @@ class VideoFrame extends ConsumerWidget { videoPlayerProvider.select((value) => value.value.aspectRatio), ); final controller = ref.watch(videoPlayerProvider.select((value) => value)); + final firstFrameRendered = useState(false); + + useEffect(() { + var disposed = false; + unawaited(() async { + try { + await controller.controller.waitUntilFirstFrameRendered; + if (!disposed) { + firstFrameRendered.value = true; + } + } catch (_) { + // Keep the transparent placeholder when the native video output fails. + } + }()); + return () { + disposed = true; + }; + }, [controller]); + + if (!firstFrameRendered.value) { + return const SizedBox.expand(); + } + return Center( - child: AspectRatio(aspectRatio: aspect, child: VideoPlayer(controller)), + child: AspectRatio( + aspectRatio: aspect, + child: Video( + controller: controller.controller, + fill: Colors.transparent, + controls: null, + ), + ), ); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 728e2ddc80..041b835618 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,6 +41,12 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_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 daffb27115..15bdefada0 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -9,6 +9,8 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_app_icon_badge irondash_engine_context + media_kit_libs_linux + media_kit_video open_file_linux screen_retriever_linux sqlite3_flutter_libs diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 017f859b1e..ff90c80276 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,6 +16,8 @@ import flutter_app_icon_badge import flutter_local_notifications import irondash_engine_context import local_auth_darwin +import media_kit_libs_macos_video +import media_kit_video import network_info_plus import ogg_opus_player import open_file_mac @@ -27,7 +29,7 @@ import sqlite3_flutter_libs import super_native_extensions import url_launcher_macos import video_compress -import video_player_avfoundation +import wakelock_plus import webview_flutter_wkwebview import window_manager import window_size @@ -44,6 +46,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) + MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) + MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) OggOpusPlayerPlugin.register(with: registry.registrar(forPlugin: "OggOpusPlayerPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) @@ -55,7 +59,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) - VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) WindowSizePlugin.register(with: registry.registrar(forPlugin: "WindowSizePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1d2e41e5c9..fa954700cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1183,10 +1183,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -1195,14 +1195,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.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: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: "direct main" description: @@ -1668,6 +1732,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: "494b982d5edb71030650ea463d939670e91b232b588323dc75229d2c5f23e7b7" + url: "https://pub.dev" + source: hosted + version: "2.0.6" screen_retriever: dependency: transitive description: @@ -1925,10 +1997,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" thirds: dependency: transitive description: @@ -1970,6 +2042,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + 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: @@ -2070,10 +2158,10 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.2" very_good_analysis: dependency: "direct dev" description: @@ -2090,46 +2178,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: "20ef311160e2ced020a62f38e4cc399a1bf289e722fc5fe37c9d7b18e44c9e8d" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: e229676f8fade3e0124482495aaa2b548872cc4018b6268adfca4868be16aa74 - url: "https://pub.dev" - source: hosted - version: "2.12.0" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: c238f5f0a26845cd0bcc2956049b63065a4d3c40ddfc22c3414cd170bef7fff9 - url: "https://pub.dev" - source: hosted - version: "2.11.0" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce" - url: "https://pub.dev" - source: hosted - version: "6.9.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.dev" - source: hosted - version: "2.4.0" visibility_detector: dependency: "direct main" description: @@ -2146,6 +2194,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1 + url: "https://pub.dev" + source: hosted + version: "1.5.2" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "0618d1799f0b28bcf98255b4ee8313e6fc4d38589dc4ee5fe5840d57d1aff6da" + url: "https://pub.dev" + source: hosted + version: "1.6.0" watcher: dependency: "direct main" description: @@ -2294,4 +2358,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.0 <4.0.0" - flutter: ">=3.44.0" + flutter: ">=3.44.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 69a4672dec..2647524b33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,9 @@ dependencies: lottie: ^3.3.3 map: ^2.0.2 markdown_widget: ^2.3.2+2 + media_kit: ^1.2.6 + media_kit_libs_video: ^1.0.7 + media_kit_video: ^2.0.1 mime: ^2.0.0 mixin_bot_sdk_dart: ^1.5.0 mixin_logger: ^0.1.3 @@ -142,7 +145,6 @@ dependencies: path: plugins/window_size ref: 12decbe0f592e14e03223f6f2c0c7e0e2dbd70a1 html: ^0.15.6 - video_player: ^2.11.1 cupertino_icons: ^1.0.9 video_compress: ^3.1.4 webcrypto: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index aad0feca15..71c1610f16 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -38,6 +40,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); + MediaKitVideoPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 904207eea4..daeefbde67 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -10,6 +10,8 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_app_icon_badge irondash_engine_context local_auth_windows + media_kit_libs_windows_video + media_kit_video protocol_handler_windows screen_retriever_windows sqlite3_flutter_libs