#34 video: auto play

This commit is contained in:
Thibault Deckers 2021-04-16 12:28:56 +09:00
parent 9128380017
commit a0f8b32440
12 changed files with 139 additions and 38 deletions

View file

@ -549,8 +549,10 @@
"@settingsSectionVideo": {}, "@settingsSectionVideo": {},
"settingsVideoShowVideos": "Show videos", "settingsVideoShowVideos": "Show videos",
"@settingsVideoShowVideos": {}, "@settingsVideoShowVideos": {},
"settingsVideoEnableHardwareAcceleration": "Enable hardware acceleration", "settingsVideoEnableHardwareAcceleration": "Hardware acceleration",
"@settingsVideoEnableHardwareAcceleration": {}, "@settingsVideoEnableHardwareAcceleration": {},
"settingsVideoEnableAutoPlay": "Auto play",
"@settingsVideoEnableAutoPlay": {},
"settingsVideoLoopModeTile": "Loop mode", "settingsVideoLoopModeTile": "Loop mode",
"@settingsVideoLoopModeTile": {}, "@settingsVideoLoopModeTile": {},
"settingsVideoLoopModeTitle": "Loop Mode", "settingsVideoLoopModeTitle": "Loop Mode",

View file

@ -256,7 +256,8 @@
"settingsSectionVideo": "동영상", "settingsSectionVideo": "동영상",
"settingsVideoShowVideos": "미디어에 동영상 표시", "settingsVideoShowVideos": "미디어에 동영상 표시",
"settingsVideoEnableHardwareAcceleration": "하드웨어 가속 사용", "settingsVideoEnableHardwareAcceleration": "하드웨어 가속",
"settingsVideoEnableAutoPlay": "자동 재생",
"settingsVideoLoopModeTile": "반복 모드", "settingsVideoLoopModeTile": "반복 모드",
"settingsVideoLoopModeTitle": "반복 모드", "settingsVideoLoopModeTitle": "반복 모드",

View file

@ -50,7 +50,8 @@ class Settings extends ChangeNotifier {
static const viewerQuickActionsKey = 'viewer_quick_actions'; static const viewerQuickActionsKey = 'viewer_quick_actions';
// video // video
static const isVideoHardwareAccelerationEnabledKey = 'video_hwaccel_mediacodec'; static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
static const enableVideoAutoPlayKey = 'video_auto_play';
static const videoLoopModeKey = 'video_loop'; static const videoLoopModeKey = 'video_loop';
// info // info
@ -229,9 +230,13 @@ class Settings extends ChangeNotifier {
// video // 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); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values);

View file

@ -10,6 +10,7 @@ import 'package:aves/utils/file_utils.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/string_utils.dart';
import 'package:aves/utils/time_utils.dart'; import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/video/fijkplayer.dart';
import 'package:fijkplayer/fijkplayer.dart'; import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -32,20 +33,7 @@ class VideoMetadataFormatter {
static Future<Map> getVideoMetadata(AvesEntry entry) async { static Future<Map> getVideoMetadata(AvesEntry entry) async {
final player = FijkPlayer(); final player = FijkPlayer();
await player.setDataSource(entry.uri, autoPlay: false); await player.setDataSourceUntilPrepared(entry.uri);
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);
final info = await player.getInfo(); final info = await player.getInfo();
await player.release(); await player.release();
return info; return info;

View file

@ -40,6 +40,7 @@ class Durations {
static const viewerOverlayChangeAnimation = Duration(milliseconds: 150); static const viewerOverlayChangeAnimation = Duration(milliseconds: 150);
static const viewerOverlayPageScrollAnimation = Duration(milliseconds: 200); static const viewerOverlayPageScrollAnimation = Duration(milliseconds: 200);
static const viewerOverlayPageShadeAnimation = Duration(milliseconds: 150); static const viewerOverlayPageShadeAnimation = Duration(milliseconds: 150);
static const viewerVideoPlayerTransition = Duration(milliseconds: 500);
// info animations // info animations
static const mapStyleSwitchAnimation = Duration(milliseconds: 300); static const mapStyleSwitchAnimation = Duration(milliseconds: 300);

View file

@ -24,9 +24,13 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
final ValueNotifier<StreamSummary> _selectedAudioStream = ValueNotifier(null); final ValueNotifier<StreamSummary> _selectedAudioStream = ValueNotifier(null);
final ValueNotifier<StreamSummary> _selectedTextStream = ValueNotifier(null); final ValueNotifier<StreamSummary> _selectedTextStream = ValueNotifier(null);
final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(Tuple2(1, 1)); final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(Tuple2(1, 1));
Timer _initialPlayTimer;
Stream<FijkValue> get _valueStream => _valueStreamController.stream; Stream<FijkValue> get _valueStream => _valueStreamController.stream;
static const initialPlayDelay = Duration(milliseconds: 100);
static const gifLikeVideoDurationThreshold = Duration(seconds: 10);
IjkPlayerAvesVideoController(AvesEntry entry) { IjkPlayerAvesVideoController(AvesEntry entry) {
FijkLog.setLevel(FijkLogLevel.Warn); FijkLog.setLevel(FijkLogLevel.Warn);
_instance = FijkPlayer(); _instance = FijkPlayer();
@ -42,10 +46,14 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
// player cannot be dynamically set to use accurate seek only when playing // player cannot be dynamically set to use accurate seek only when playing
const accurateSeekEnabled = false; 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) // TODO TLAD HW codecs sometimes fail when seek-starting some videos, e.g. MP2TS/h264(HDPR)
final hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled;
if (hwAccelerationEnabled) { 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; final s = entry.displaySize % 16 * -1 % 16;
_macroBlockCrop = Offset(s.width, s.height); _macroBlockCrop = Offset(s.width, s.height);
} }
@ -83,6 +91,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
@override @override
void dispose() { void dispose() {
_initialPlayTimer?.cancel();
_instance.removeListener(_onValueChanged); _instance.removeListener(_onValueChanged);
_valueStreamController.close(); _valueStreamController.close();
_subscriptions _subscriptions
@ -142,7 +151,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
_valueStreamController.add(_instance.value); _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 // as a workaround, pausing after a brief duration is possible, but fiddly
@override @override
Future<void> setDataSource(String uri, {int startMillis = 0}) async { Future<void> 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] // `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.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 @override
Future<void> play() => _instance.start(); Future<void> play() {
if (_instance.isPlayable()) {
_instance.start();
}
return SynchronousFuture(null);
}
@override @override
Future<void> pause() => _instance.pause(); Future<void> pause() {
if (_instance.isPlayable()) {
_initialPlayTimer?.cancel();
_instance.pause();
}
return SynchronousFuture(null);
}
@override @override
Future<void> seekTo(int targetMillis) => _instance.seekTo(targetMillis); Future<void> seekTo(int targetMillis) => _instance.seekTo(targetMillis);
@ -199,7 +222,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
fit: FijkFit( fit: FijkFit(
sizeFactor: 1.0, sizeFactor: 1.0,
aspectRatio: dar, aspectRatio: dar,
alignment: Alignment.topLeft, alignment: _alignmentForRotation(entry.rotationDegrees),
macroBlockCrop: _macroBlockCrop, macroBlockCrop: _macroBlockCrop,
), ),
panelBuilder: (player, data, context, viewSize, texturePos) => SizedBox(), 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 { extension ExtraIjkStatus on FijkState {
@ -233,6 +270,32 @@ extension ExtraIjkStatus on FijkState {
} }
} }
extension ExtraFijkPlayer on FijkPlayer {
Future<void> 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 } enum StreamType { video, audio, text }
extension ExtraStreamType on StreamType { extension ExtraStreamType on StreamType {

View file

@ -241,10 +241,15 @@ class _SettingsPageState extends State<SettingsPage> {
title: Text(context.l10n.settingsVideoShowVideos), title: Text(context.l10n.settingsVideoShowVideos),
), ),
SwitchListTile( SwitchListTile(
value: settings.isVideoHardwareAccelerationEnabled, value: settings.enableVideoHardwareAcceleration,
onChanged: (v) => settings.isVideoHardwareAccelerationEnabled = v, onChanged: (v) => settings.enableVideoHardwareAcceleration = v,
title: Text(context.l10n.settingsVideoEnableHardwareAcceleration), title: Text(context.l10n.settingsVideoEnableHardwareAcceleration),
), ),
SwitchListTile(
value: settings.enableVideoAutoPlay,
onChanged: (v) => settings.enableVideoAutoPlay = v,
title: Text(context.l10n.settingsVideoEnableAutoPlay),
),
ListTile( ListTile(
title: Text(context.l10n.settingsVideoLoopModeTile), title: Text(context.l10n.settingsVideoLoopModeTile),
subtitle: Text(settings.videoLoopMode.getName(context)), subtitle: Text(settings.videoLoopMode.getName(context)),

View file

@ -503,6 +503,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
() => IjkPlayerAvesVideoController(entry), () => IjkPlayerAvesVideoController(entry),
(_) => _.dispose(), (_) => _.dispose(),
); );
if (settings.enableVideoAutoPlay) {
_playVideo();
}
} }
if (entry.isMultipage) { if (entry.isMultipage) {
_initViewSpecificController<MultiPageController>( _initViewSpecificController<MultiPageController>(
@ -516,6 +519,22 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
setState(() {}); setState(() {});
} }
Future<void> _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<T>(String uri, List<Tuple2<String, T>> controllers, T Function() builder, void Function(T controller) disposer) { void _initViewSpecificController<T>(String uri, List<Tuple2<String, T>> controllers, T Function() builder, void Function(T controller) disposer) {
var controller = controllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null); var controller = controllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null);
if (controller != null) { if (controller != null) {

View file

@ -116,7 +116,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
icon: AnimatedIcons.play_pause, icon: AnimatedIcons.play_pause,
progress: _playPauseAnimation, progress: _playPauseAnimation,
), ),
onPressed: _playPause, onPressed: _togglePlayPause,
tooltip: isPlaying ? context.l10n.viewerPauseTooltip : context.l10n.viewerPlayTooltip, tooltip: isPlaying ? context.l10n.viewerPauseTooltip : context.l10n.viewerPlayTooltip,
), ),
), ),
@ -195,10 +195,16 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
_updatePlayPauseIcon(); _updatePlayPauseIcon();
} }
Future<void> _playPause() async { Future<void> _togglePlayPause() async {
if (isPlaying) { if (isPlaying) {
await controller.pause(); await controller.pause();
} else if (isPlayable) { } else {
await _play();
}
}
Future<void> _play() async {
if (isPlayable) {
await controller.play(); await controller.play();
} else { } else {
await controller.setDataSource(entry.uri); await controller.setDataSource(entry.uri);

View file

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_images.dart'; import 'package:aves/model/entry_images.dart';
import 'package:aves/model/settings/settings.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/collection/collection_page.dart';
import 'package:aves/widgets/common/video/controller.dart'; import 'package:aves/widgets/common/video/controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -57,13 +58,23 @@ class _VideoViewState extends State<VideoView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (controller == null) return SizedBox(); if (controller == null) return SizedBox();
return StreamBuilder<VideoStatus>( return StreamBuilder<VideoStatus>(
stream: widget.controller.statusStream, stream: controller.statusStream,
builder: (context, snapshot) { builder: (context, snapshot) {
return controller?.isPlayable == true return Stack(
? controller.buildPlayerWidget(context, entry) fit: StackFit.expand,
: Image( 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)), image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)),
fit: BoxFit.contain, fit: BoxFit.fill,
),
),
],
); );
}); });
} }

View file

@ -211,7 +211,7 @@ packages:
description: description:
path: "." path: "."
ref: aves ref: aves
resolved-ref: "8fcf94a57e2a77a79d255f4499e26503ad411769" resolved-ref: "0f25874db46d1af6fcfbeb8722915cbc211a10fb"
url: "git://github.com/deckerst/fijkplayer.git" url: "git://github.com/deckerst/fijkplayer.git"
source: git source: git
version: "0.8.7" version: "0.8.7"