diff --git a/android/app/libs/fijkplayer-full-release.aar b/android/app/libs/fijkplayer-full-release.aar index 17f13e5b2..72547c9dc 100644 Binary files a/android/app/libs/fijkplayer-full-release.aar and b/android/app/libs/fijkplayer-full-release.aar differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 71663228b..2b2e4ded7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -549,8 +549,10 @@ "@settingsSectionVideo": {}, "settingsVideoShowVideos": "Show videos", "@settingsVideoShowVideos": {}, - "settingsVideoEnableHardwareAcceleration": "Enable hardware acceleration", + "settingsVideoEnableHardwareAcceleration": "Hardware acceleration", "@settingsVideoEnableHardwareAcceleration": {}, + "settingsVideoEnableAutoPlay": "Auto play", + "@settingsVideoEnableAutoPlay": {}, "settingsVideoLoopModeTile": "Loop mode", "@settingsVideoLoopModeTile": {}, "settingsVideoLoopModeTitle": "Loop Mode", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 670a2fa5c..cc48063c5 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -256,7 +256,8 @@ "settingsSectionVideo": "동영상", "settingsVideoShowVideos": "미디어에 동영상 표시", - "settingsVideoEnableHardwareAcceleration": "하드웨어 가속 사용", + "settingsVideoEnableHardwareAcceleration": "하드웨어 가속", + "settingsVideoEnableAutoPlay": "자동 재생", "settingsVideoLoopModeTile": "반복 모드", "settingsVideoLoopModeTitle": "반복 모드", diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index b7a9de9da..e837d2e64 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -50,7 +50,8 @@ class Settings extends ChangeNotifier { static const viewerQuickActionsKey = 'viewer_quick_actions'; // video - static const isVideoHardwareAccelerationEnabledKey = 'video_hwaccel_mediacodec'; + static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec'; + static const enableVideoAutoPlayKey = 'video_auto_play'; static const videoLoopModeKey = 'video_loop'; // info @@ -229,9 +230,13 @@ class Settings extends ChangeNotifier { // video - set isVideoHardwareAccelerationEnabled(bool newValue) => setAndNotify(isVideoHardwareAccelerationEnabledKey, newValue); + set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue); - bool get isVideoHardwareAccelerationEnabled => getBoolOrDefault(isVideoHardwareAccelerationEnabledKey, true); + bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, true); + + set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue); + + bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values); diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index e32faf46e..8e3edde97 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -10,6 +10,7 @@ import 'package:aves/utils/file_utils.dart'; import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves/widgets/common/video/fijkplayer.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; @@ -32,20 +33,7 @@ class VideoMetadataFormatter { static Future getVideoMetadata(AvesEntry entry) async { final player = FijkPlayer(); - await player.setDataSource(entry.uri, autoPlay: false); - - final completer = Completer(); - void onChange() { - if ([FijkState.prepared, FijkState.error].contains(player.state)) { - completer.complete(); - } - } - - player.addListener(onChange); - await player.prepareAsync(); - await completer.future; - player.removeListener(onChange); - + await player.setDataSourceUntilPrepared(entry.uri); final info = await player.getInfo(); await player.release(); return info; diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index ded2c0f31..a1300a711 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -40,6 +40,7 @@ class Durations { static const viewerOverlayChangeAnimation = Duration(milliseconds: 150); static const viewerOverlayPageScrollAnimation = Duration(milliseconds: 200); static const viewerOverlayPageShadeAnimation = Duration(milliseconds: 150); + static const viewerVideoPlayerTransition = Duration(milliseconds: 500); // info animations static const mapStyleSwitchAnimation = Duration(milliseconds: 300); diff --git a/lib/widgets/common/video/fijkplayer.dart b/lib/widgets/common/video/fijkplayer.dart index 18d2aba52..53f891cfc 100644 --- a/lib/widgets/common/video/fijkplayer.dart +++ b/lib/widgets/common/video/fijkplayer.dart @@ -24,9 +24,13 @@ class IjkPlayerAvesVideoController extends AvesVideoController { final ValueNotifier _selectedAudioStream = ValueNotifier(null); final ValueNotifier _selectedTextStream = ValueNotifier(null); final ValueNotifier> _sar = ValueNotifier(Tuple2(1, 1)); + Timer _initialPlayTimer; Stream get _valueStream => _valueStreamController.stream; + static const initialPlayDelay = Duration(milliseconds: 100); + static const gifLikeVideoDurationThreshold = Duration(seconds: 10); + IjkPlayerAvesVideoController(AvesEntry entry) { FijkLog.setLevel(FijkLogLevel.Warn); _instance = FijkPlayer(); @@ -42,10 +46,14 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // player cannot be dynamically set to use accurate seek only when playing const accurateSeekEnabled = false; - // when HW acceleration is enabled, videos with dimensions that do not fit 16x macroblocks need cropping + // playing with HW acceleration seems to skip the last frames of some videos + // so HW acceleration is always disabled for gif-like videos where the last frames may be significant + final hwAccelerationEnabled = settings.enableVideoHardwareAcceleration && entry.durationMillis > gifLikeVideoDurationThreshold.inMilliseconds; + // TODO TLAD HW codecs sometimes fail when seek-starting some videos, e.g. MP2TS/h264(HDPR) - final hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled; if (hwAccelerationEnabled) { + // when HW acceleration is enabled, videos with dimensions that do not fit 16x macroblocks need cropping + // TODO TLAD not all formats/devices need this correction, e.g. 498x278 MP4 on S7, 408x244 WEBM on S10e do not final s = entry.displaySize % 16 * -1 % 16; _macroBlockCrop = Offset(s.width, s.height); } @@ -83,6 +91,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { @override void dispose() { + _initialPlayTimer?.cancel(); _instance.removeListener(_onValueChanged); _valueStreamController.close(); _subscriptions @@ -142,7 +151,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { _valueStreamController.add(_instance.value); } - // enable autoplay, even when seeking on uninitialized player, otherwise the texture is not updated + // always start playing, even when seeking on uninitialized player, otherwise the texture is not updated // as a workaround, pausing after a brief duration is possible, but fiddly @override Future setDataSource(String uri, {int startMillis = 0}) async { @@ -150,14 +159,28 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // `seek-at-start`: set offset of player should be seeked, default: 0, in [0, INT_MAX] await _instance.setOption(FijkOption.playerCategory, 'seek-at-start', startMillis); } - await _instance.setDataSource(uri, autoPlay: true); + // calling `setDataSource()` with `autoPlay` starts as soon as possible, but often yields initial artifacts + // so we introduce a small delay after the player is declared `prepared`, before playing + await _instance.setDataSourceUntilPrepared(uri); + _initialPlayTimer = Timer(initialPlayDelay, play); } @override - Future play() => _instance.start(); + Future play() { + if (_instance.isPlayable()) { + _instance.start(); + } + return SynchronousFuture(null); + } @override - Future pause() => _instance.pause(); + Future pause() { + if (_instance.isPlayable()) { + _initialPlayTimer?.cancel(); + _instance.pause(); + } + return SynchronousFuture(null); + } @override Future seekTo(int targetMillis) => _instance.seekTo(targetMillis); @@ -199,7 +222,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { fit: FijkFit( sizeFactor: 1.0, aspectRatio: dar, - alignment: Alignment.topLeft, + alignment: _alignmentForRotation(entry.rotationDegrees), macroBlockCrop: _macroBlockCrop, ), panelBuilder: (player, data, context, viewSize, texturePos) => SizedBox(), @@ -207,6 +230,20 @@ class IjkPlayerAvesVideoController extends AvesVideoController { ); }); } + + Alignment _alignmentForRotation(int rotation) { + switch (rotation) { + case 90: + return Alignment.topRight; + case 180: + return Alignment.bottomRight; + case 270: + return Alignment.bottomLeft; + case 0: + default: + return Alignment.topLeft; + } + } } extension ExtraIjkStatus on FijkState { @@ -233,6 +270,32 @@ extension ExtraIjkStatus on FijkState { } } +extension ExtraFijkPlayer on FijkPlayer { + Future setDataSourceUntilPrepared(String uri) async { + await setDataSource(uri, autoPlay: false); + + final completer = Completer(); + void onChange() { + switch (state) { + case FijkState.prepared: + removeListener(onChange); + completer.complete(); + break; + case FijkState.error: + removeListener(onChange); + completer.completeError(value.exception); + break; + default: + break; + } + } + + addListener(onChange); + await prepareAsync(); + return completer.future; + } +} + enum StreamType { video, audio, text } extension ExtraStreamType on StreamType { diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 7b5a02a5d..d6d62725e 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -241,10 +241,15 @@ class _SettingsPageState extends State { title: Text(context.l10n.settingsVideoShowVideos), ), SwitchListTile( - value: settings.isVideoHardwareAccelerationEnabled, - onChanged: (v) => settings.isVideoHardwareAccelerationEnabled = v, + value: settings.enableVideoHardwareAcceleration, + onChanged: (v) => settings.enableVideoHardwareAcceleration = v, title: Text(context.l10n.settingsVideoEnableHardwareAcceleration), ), + SwitchListTile( + value: settings.enableVideoAutoPlay, + onChanged: (v) => settings.enableVideoAutoPlay = v, + title: Text(context.l10n.settingsVideoEnableAutoPlay), + ), ListTile( title: Text(context.l10n.settingsVideoLoopModeTile), subtitle: Text(settings.videoLoopMode.getName(context)), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 5b18d8251..b1a50090b 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -503,6 +503,9 @@ class _EntryViewerStackState extends State with SingleTickerPr () => IjkPlayerAvesVideoController(entry), (_) => _.dispose(), ); + if (settings.enableVideoAutoPlay) { + _playVideo(); + } } if (entry.isMultipage) { _initViewSpecificController( @@ -516,6 +519,22 @@ class _EntryViewerStackState extends State with SingleTickerPr setState(() {}); } + Future _playVideo() async { + await Future.delayed(Duration(milliseconds: 300)); + + final entry = _entryNotifier.value; + if (entry == null) return; + + final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; + if (videoController != null) { + if (videoController.isPlayable) { + await videoController.play(); + } else { + await videoController.setDataSource(entry.uri); + } + } + } + void _initViewSpecificController(String uri, List> controllers, T Function() builder, void Function(T controller) disposer) { var controller = controllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null); if (controller != null) { diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index 22fa53844..aeafd7123 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -116,7 +116,7 @@ class _VideoControlOverlayState extends State with SingleTi icon: AnimatedIcons.play_pause, progress: _playPauseAnimation, ), - onPressed: _playPause, + onPressed: _togglePlayPause, tooltip: isPlaying ? context.l10n.viewerPauseTooltip : context.l10n.viewerPlayTooltip, ), ), @@ -195,10 +195,16 @@ class _VideoControlOverlayState extends State with SingleTi _updatePlayPauseIcon(); } - Future _playPause() async { + Future _togglePlayPause() async { if (isPlaying) { await controller.pause(); - } else if (isPlayable) { + } else { + await _play(); + } + } + + Future _play() async { + if (isPlayable) { await controller.play(); } else { await controller.setDataSource(entry.uri); diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 488142ce8..5c809c6ce 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/video/controller.dart'; import 'package:flutter/material.dart'; @@ -57,14 +58,24 @@ class _VideoViewState extends State { Widget build(BuildContext context) { if (controller == null) return SizedBox(); return StreamBuilder( - stream: widget.controller.statusStream, + stream: controller.statusStream, builder: (context, snapshot) { - return controller?.isPlayable == true - ? controller.buildPlayerWidget(context, entry) - : Image( + return Stack( + fit: StackFit.expand, + children: [ + if (controller.isPlayable) controller.buildPlayerWidget(context, entry), + // fade out image to ease transition with the player as it starts with a black texture + AnimatedOpacity( + opacity: controller.isPlayable ? 0 : 1, + curve: Curves.easeInCirc, + duration: Durations.viewerVideoPlayerTransition, + child: Image( image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)), - fit: BoxFit.contain, - ); + fit: BoxFit.fill, + ), + ), + ], + ); }); } diff --git a/pubspec.lock b/pubspec.lock index c0adf8864..1e8fb5f47 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -211,7 +211,7 @@ packages: description: path: "." ref: aves - resolved-ref: "8fcf94a57e2a77a79d255f4499e26503ad411769" + resolved-ref: "0f25874db46d1af6fcfbeb8722915cbc211a10fb" url: "git://github.com/deckerst/fijkplayer.git" source: git version: "0.8.7"