#34 video: auto play
This commit is contained in:
parent
9128380017
commit
a0f8b32440
12 changed files with 139 additions and 38 deletions
Binary file not shown.
|
@ -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",
|
||||
|
|
|
@ -256,7 +256,8 @@
|
|||
|
||||
"settingsSectionVideo": "동영상",
|
||||
"settingsVideoShowVideos": "미디어에 동영상 표시",
|
||||
"settingsVideoEnableHardwareAcceleration": "하드웨어 가속 사용",
|
||||
"settingsVideoEnableHardwareAcceleration": "하드웨어 가속",
|
||||
"settingsVideoEnableAutoPlay": "자동 재생",
|
||||
"settingsVideoLoopModeTile": "반복 모드",
|
||||
"settingsVideoLoopModeTitle": "반복 모드",
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<Map> 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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -24,9 +24,13 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
final ValueNotifier<StreamSummary> _selectedAudioStream = ValueNotifier(null);
|
||||
final ValueNotifier<StreamSummary> _selectedTextStream = ValueNotifier(null);
|
||||
final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(Tuple2(1, 1));
|
||||
Timer _initialPlayTimer;
|
||||
|
||||
Stream<FijkValue> 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<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]
|
||||
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<void> play() => _instance.start();
|
||||
Future<void> play() {
|
||||
if (_instance.isPlayable()) {
|
||||
_instance.start();
|
||||
}
|
||||
return SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() => _instance.pause();
|
||||
Future<void> pause() {
|
||||
if (_instance.isPlayable()) {
|
||||
_initialPlayTimer?.cancel();
|
||||
_instance.pause();
|
||||
}
|
||||
return SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<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 }
|
||||
|
||||
extension ExtraStreamType on StreamType {
|
||||
|
|
|
@ -241,10 +241,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
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)),
|
||||
|
|
|
@ -503,6 +503,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
|||
() => IjkPlayerAvesVideoController(entry),
|
||||
(_) => _.dispose(),
|
||||
);
|
||||
if (settings.enableVideoAutoPlay) {
|
||||
_playVideo();
|
||||
}
|
||||
}
|
||||
if (entry.isMultipage) {
|
||||
_initViewSpecificController<MultiPageController>(
|
||||
|
@ -516,6 +519,22 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
|||
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) {
|
||||
var controller = controllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null);
|
||||
if (controller != null) {
|
||||
|
|
|
@ -116,7 +116,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> 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<VideoControlOverlay> with SingleTi
|
|||
_updatePlayPauseIcon();
|
||||
}
|
||||
|
||||
Future<void> _playPause() async {
|
||||
Future<void> _togglePlayPause() async {
|
||||
if (isPlaying) {
|
||||
await controller.pause();
|
||||
} else if (isPlayable) {
|
||||
} else {
|
||||
await _play();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _play() async {
|
||||
if (isPlayable) {
|
||||
await controller.play();
|
||||
} else {
|
||||
await controller.setDataSource(entry.uri);
|
||||
|
|
|
@ -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<VideoView> {
|
|||
Widget build(BuildContext context) {
|
||||
if (controller == null) return SizedBox();
|
||||
return StreamBuilder<VideoStatus>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue