video: added loop mode setting, fixed pause-seek position update, cleanup
This commit is contained in:
parent
37dde5cb38
commit
52e4f96f7b
14 changed files with 139 additions and 273 deletions
|
@ -115,6 +115,13 @@
|
|||
"coordinateFormatDecimal": "Decimal degrees",
|
||||
"@coordinateFormatDecimal": {},
|
||||
|
||||
"videoLoopModeNever": "Never",
|
||||
"@videoLoopModeNever": {},
|
||||
"videoLoopModeShortOnly": "Short videos only",
|
||||
"@videoLoopModeShortOnly": {},
|
||||
"videoLoopModeAlways": "Always",
|
||||
"@videoLoopModeAlways": {},
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
|
@ -544,6 +551,10 @@
|
|||
"@settingsVideoShowVideos": {},
|
||||
"settingsVideoEnableHardwareAcceleration": "Enable hardware acceleration",
|
||||
"@settingsVideoEnableHardwareAcceleration": {},
|
||||
"settingsVideoLoopModeTile": "Loop mode",
|
||||
"@settingsVideoLoopModeTile": {},
|
||||
"settingsVideoLoopModeTitle": "Loop Mode",
|
||||
"@settingsVideoLoopModeTitle": {},
|
||||
|
||||
"settingsSectionPrivacy": "Privacy",
|
||||
"@settingsSectionPrivacy": {},
|
||||
|
|
|
@ -59,6 +59,10 @@
|
|||
"coordinateFormatDms": "도분초",
|
||||
"coordinateFormatDecimal": "소수점",
|
||||
|
||||
"videoLoopModeNever": "반복 안 함",
|
||||
"videoLoopModeShortOnly": "짧은 동영상만 반복",
|
||||
"videoLoopModeAlways": "항상 반복",
|
||||
|
||||
"mapStyleGoogleNormal": "구글 지도",
|
||||
"mapStyleGoogleHybrid": "구글 지도 (위성)",
|
||||
"mapStyleGoogleTerrain": "구글 지도 (지형)",
|
||||
|
@ -253,6 +257,8 @@
|
|||
"settingsSectionVideo": "동영상",
|
||||
"settingsVideoShowVideos": "미디어에 동영상 표시",
|
||||
"settingsVideoEnableHardwareAcceleration": "하드웨어 가속 사용",
|
||||
"settingsVideoLoopModeTile": "반복 모드",
|
||||
"settingsVideoLoopModeTitle": "반복 모드",
|
||||
|
||||
"settingsSectionPrivacy": "개인정보 보호",
|
||||
"settingsEnableAnalytics": "진단 데이터 보내기",
|
||||
|
|
|
@ -8,3 +8,5 @@ enum HomePageSetting { collection, albums }
|
|||
enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor }
|
||||
|
||||
enum KeepScreenOn { never, viewerOnly, always }
|
||||
|
||||
enum VideoLoopMode { never, shortOnly, always }
|
||||
|
|
|
@ -51,6 +51,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// video
|
||||
static const isVideoHardwareAccelerationEnabledKey = 'video_hwaccel_mediacodec';
|
||||
static const videoLoopModeKey = 'video_loop';
|
||||
|
||||
// info
|
||||
static const infoMapStyleKey = 'info_map_style';
|
||||
|
@ -232,6 +233,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
bool get isVideoHardwareAccelerationEnabled => getBoolOrDefault(isVideoHardwareAccelerationEnabledKey, true);
|
||||
|
||||
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values);
|
||||
|
||||
set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString());
|
||||
|
||||
// info
|
||||
|
||||
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);
|
||||
|
|
35
lib/model/settings/video_loop_mode.dart
Normal file
35
lib/model/settings/video_loop_mode.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
|
||||
extension ExtraVideoLoopMode on VideoLoopMode {
|
||||
String getName(BuildContext context) {
|
||||
switch (this) {
|
||||
case VideoLoopMode.never:
|
||||
return context.l10n.videoLoopModeNever;
|
||||
case VideoLoopMode.shortOnly:
|
||||
return context.l10n.videoLoopModeShortOnly;
|
||||
case VideoLoopMode.always:
|
||||
return context.l10n.videoLoopModeAlways;
|
||||
default:
|
||||
return toString();
|
||||
}
|
||||
}
|
||||
|
||||
static const shortVideoThreshold = Duration(seconds: 30);
|
||||
|
||||
bool shouldLoop(AvesEntry entry) {
|
||||
switch (this) {
|
||||
case VideoLoopMode.never:
|
||||
return false;
|
||||
case VideoLoopMode.shortOnly:
|
||||
if (entry.durationMillis == null) return false;
|
||||
return entry.durationMillis < shortVideoThreshold.inMilliseconds;
|
||||
case VideoLoopMode.always:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -6,9 +6,7 @@ abstract class AvesVideoController {
|
|||
|
||||
void dispose();
|
||||
|
||||
Future<void> setDataSource(String uri);
|
||||
|
||||
Future<void> refreshVideoInfo();
|
||||
Future<void> setDataSource(String uri, {int startMillis = 0});
|
||||
|
||||
Future<void> play();
|
||||
|
||||
|
@ -16,7 +14,11 @@ abstract class AvesVideoController {
|
|||
|
||||
Future<void> seekTo(int targetMillis);
|
||||
|
||||
Future<void> seekToProgress(double progress);
|
||||
Future<void> seekToProgress(double progress) async {
|
||||
if (duration != null) {
|
||||
await seekTo((duration * progress).toInt());
|
||||
}
|
||||
}
|
||||
|
||||
Listenable get playCompletedListenable;
|
||||
|
||||
|
@ -28,40 +30,22 @@ abstract class AvesVideoController {
|
|||
|
||||
bool get isPlaying => status == VideoStatus.playing;
|
||||
|
||||
bool get isVideoReady;
|
||||
|
||||
Stream<bool> get isVideoReadyStream;
|
||||
|
||||
int get duration;
|
||||
|
||||
int get currentPosition;
|
||||
|
||||
double get progress => (currentPosition ?? 0).toDouble() / (duration ?? 1);
|
||||
double get progress => duration == null ? 0 : (currentPosition ?? 0).toDouble() / duration;
|
||||
|
||||
Stream<int> get positionStream;
|
||||
|
||||
Widget buildPlayerWidget(BuildContext context, AvesEntry entry);
|
||||
}
|
||||
|
||||
class AvesVideoInfo {
|
||||
// in millis
|
||||
int duration, currentPosition;
|
||||
|
||||
AvesVideoInfo({
|
||||
this.duration,
|
||||
this.currentPosition,
|
||||
});
|
||||
}
|
||||
|
||||
enum VideoStatus {
|
||||
idle,
|
||||
initialized,
|
||||
preparing,
|
||||
prepared,
|
||||
playing,
|
||||
paused,
|
||||
playing,
|
||||
completed,
|
||||
stopped,
|
||||
disposed,
|
||||
error,
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
|||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/settings/video_loop_mode.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/widgets/common/video/controller.dart';
|
||||
import 'package:fijkplayer/fijkplayer.dart';
|
||||
|
@ -12,7 +13,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
FijkPlayer _instance;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final StreamController<FijkValue> _valueStreamController = StreamController.broadcast();
|
||||
final AChangeNotifier _playFinishNotifier = AChangeNotifier();
|
||||
final AChangeNotifier _completedNotifier = AChangeNotifier();
|
||||
Offset _macroBlockCrop = Offset.zero;
|
||||
|
||||
Stream<FijkValue> get _valueStream => _valueStreamController.stream;
|
||||
|
@ -25,41 +26,45 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
// cf https://www.jianshu.com/p/843c86a9e9ad
|
||||
|
||||
final option = FijkOption();
|
||||
// `fastseek`: enable fast, but inaccurate seeks for some formats
|
||||
option.setFormatOption('fflags', 'fastseek');
|
||||
// `enable-accurate-seek`: enable accurate seek, default: 0, in [0, 1]
|
||||
option.setPlayerOption('enable-accurate-seek', 1);
|
||||
// `framedrop`: drop frames when cpu is too slow, default: 0, in [-1, 120]
|
||||
option.setPlayerOption('framedrop', 5);
|
||||
|
||||
final _hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled;
|
||||
if (_hwAccelerationEnabled) {
|
||||
// crop HW acceleration macroblock misalignment for videos with dimensions that do not fit 16x
|
||||
// when accurate seek is enabled and seeking fails, it takes time (cf `accurate-seek-timeout`) to acknowledge the error and proceed
|
||||
// failure seems to happen when pause-seeking videos with an audio stream, whatever container or video stream
|
||||
// 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
|
||||
// TODO TLAD HW codecs sometimes fail when seek-starting some videos, e.g. MP2TS/h264(HDPR)
|
||||
final hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled;
|
||||
if (hwAccelerationEnabled) {
|
||||
final s = entry.displaySize % 16 * -1 % 16;
|
||||
_macroBlockCrop = Offset(s.width, s.height);
|
||||
}
|
||||
|
||||
final loopEnabled = settings.videoLoopMode.shouldLoop(entry);
|
||||
|
||||
// `fastseek`: enable fast, but inaccurate seeks for some formats
|
||||
// in practice the flag seems ineffective, but harmless too
|
||||
option.setFormatOption('fflags', 'fastseek');
|
||||
|
||||
// `enable-accurate-seek`: enable accurate seek, default: 0, in [0, 1]
|
||||
option.setPlayerOption('enable-accurate-seek', accurateSeekEnabled ? 1 : 0);
|
||||
|
||||
// `accurate-seek-timeout`: accurate seek timeout, default: 5000 ms, in [0, 5000]
|
||||
option.setPlayerOption('accurate-seek-timeout', 1000);
|
||||
|
||||
// `framedrop`: drop frames when cpu is too slow, default: 0, in [-1, 120]
|
||||
option.setPlayerOption('framedrop', 5);
|
||||
|
||||
// `loop`: set number of times the playback shall be looped, default: 1, in [INT_MIN, INT_MAX]
|
||||
option.setPlayerOption('loop', loopEnabled ? -1 : 1);
|
||||
|
||||
// `mediacodec-all-videos`: MediaCodec: enable all videos, default: 0, in [0, 1]
|
||||
// TODO TLAD enabling `mediacodec-all-videos` randomly fails to render some videos, e.g. MP2TS/h264(HDPR)
|
||||
option.setPlayerOption('mediacodec-all-videos', _hwAccelerationEnabled ? 1 : 0);
|
||||
|
||||
// option.setPlayerOption('analyzemaxduration', 200 * 1024);
|
||||
// option.setPlayerOption('analyzeduration', 200 * 1024);
|
||||
// option.setPlayerOption('probesize', 1024 * 1024);
|
||||
|
||||
// CJL options
|
||||
// option.setPlayerOption('reconnect', 5);
|
||||
// option.setPlayerOption('mediacodec', 1);
|
||||
// option.setPlayerOption('packet-buffering', 1);
|
||||
// option.setPlayerOption('soundtouch', 1);
|
||||
// option.setPlayerOption('start-on-prepared', 1);
|
||||
|
||||
// TODO TLAD check looping
|
||||
// option.setPlayerOption('loop', 42);
|
||||
option.setPlayerOption('mediacodec-all-videos', hwAccelerationEnabled ? 1 : 0);
|
||||
|
||||
_instance.applyOptions(option);
|
||||
|
||||
_instance.addListener(_onValueChanged);
|
||||
_subscriptions.add(_valueStream.where((value) => value.completed).listen((_) => _playFinishNotifier.notifyListeners()));
|
||||
_subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notifyListeners()));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -77,10 +82,13 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
// enable autoplay, 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) => _instance.setDataSource(uri, autoPlay: true);
|
||||
|
||||
@override
|
||||
Future<void> refreshVideoInfo() => null;
|
||||
Future<void> setDataSource(String uri, {int startMillis = 0}) async {
|
||||
if (startMillis > 0) {
|
||||
// `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);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() => _instance.start();
|
||||
|
@ -92,10 +100,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
Future<void> seekTo(int targetMillis) => _instance.seekTo(targetMillis);
|
||||
|
||||
@override
|
||||
Future<void> seekToProgress(double progress) => _instance.seekTo((duration * progress).toInt());
|
||||
|
||||
@override
|
||||
Listenable get playCompletedListenable => _playFinishNotifier;
|
||||
Listenable get playCompletedListenable => _completedNotifier;
|
||||
|
||||
@override
|
||||
VideoStatus get status => _instance.state.toAves;
|
||||
|
@ -103,12 +108,6 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
@override
|
||||
Stream<VideoStatus> get statusStream => _valueStream.map((value) => value.state.toAves);
|
||||
|
||||
@override
|
||||
bool get isVideoReady => _instance.value.videoRenderStart;
|
||||
|
||||
@override
|
||||
Stream<bool> get isVideoReadyStream => _valueStream.map((value) => value.videoRenderStart);
|
||||
|
||||
@override
|
||||
bool get isPlayable => _instance.isPlayable();
|
||||
|
||||
|
@ -141,23 +140,19 @@ extension ExtraIjkStatus on FijkState {
|
|||
VideoStatus get toAves {
|
||||
switch (this) {
|
||||
case FijkState.idle:
|
||||
case FijkState.end:
|
||||
case FijkState.stopped:
|
||||
return VideoStatus.idle;
|
||||
case FijkState.initialized:
|
||||
return VideoStatus.initialized;
|
||||
case FijkState.asyncPreparing:
|
||||
return VideoStatus.preparing;
|
||||
return VideoStatus.initialized;
|
||||
case FijkState.prepared:
|
||||
return VideoStatus.prepared;
|
||||
case FijkState.started:
|
||||
return VideoStatus.playing;
|
||||
case FijkState.paused:
|
||||
return VideoStatus.paused;
|
||||
case FijkState.started:
|
||||
return VideoStatus.playing;
|
||||
case FijkState.completed:
|
||||
return VideoStatus.completed;
|
||||
case FijkState.stopped:
|
||||
return VideoStatus.stopped;
|
||||
case FijkState.end:
|
||||
return VideoStatus.disposed;
|
||||
case FijkState.error:
|
||||
return VideoStatus.error;
|
||||
}
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
// import 'dart:async';
|
||||
//
|
||||
// import 'package:aves/model/entry.dart';
|
||||
// import 'package:aves/utils/change_notifier.dart';
|
||||
// import 'package:aves/widgets/common/video/controller.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||
//
|
||||
// class IjkPlayerAvesVideoController extends AvesVideoController {
|
||||
// IjkMediaController _instance;
|
||||
// final List<StreamSubscription> _subscriptions = [];
|
||||
// final AChangeNotifier _playFinishNotifier = AChangeNotifier();
|
||||
//
|
||||
// IjkPlayerAvesVideoController() {
|
||||
// _instance = IjkMediaController();
|
||||
// _subscriptions.add(_instance.playFinishStream.listen((_) => _playFinishNotifier.notifyListeners()));
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// _subscriptions
|
||||
// ..forEach((sub) => sub.cancel())
|
||||
// ..clear();
|
||||
// _instance?.dispose();
|
||||
// }
|
||||
//
|
||||
// // enable autoplay, 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) => _instance.setDataSource(DataSource.photoManagerUrl(uri), autoPlay: true);
|
||||
//
|
||||
// @override
|
||||
// Future<void> refreshVideoInfo() => _instance.refreshVideoInfo();
|
||||
//
|
||||
// @override
|
||||
// Future<void> play() => _instance.play();
|
||||
//
|
||||
// @override
|
||||
// Future<void> pause() => _instance.pause();
|
||||
//
|
||||
// @override
|
||||
// Future<void> seekTo(int targetMillis) => _instance.seekTo(targetMillis / 1000.0);
|
||||
//
|
||||
// @override
|
||||
// Future<void> seekToProgress(double progress) => _instance.seekToProgress(progress);
|
||||
//
|
||||
// @override
|
||||
// Listenable get playCompletedListenable => _playFinishNotifier;
|
||||
//
|
||||
// @override
|
||||
// VideoStatus get status => _instance.ijkStatus.toAves;
|
||||
//
|
||||
// @override
|
||||
// Stream<VideoStatus> get statusStream => _instance.ijkStatusStream.map((status) => status.toAves);
|
||||
//
|
||||
// // we check whether video info is ready instead of checking for `noDatasource` status,
|
||||
// // as the controller could also be uninitialized with the `pause` status
|
||||
// // (e.g. when switching between video entries without playing them the first time)
|
||||
// @override
|
||||
// bool get isPlayable => _videoInfo.hasData && [VideoStatus.prepared, VideoStatus.playing, VideoStatus.paused, VideoStatus.completed].contains(status);
|
||||
//
|
||||
// @override
|
||||
// bool get isVideoReady => _instance.textureId != null;
|
||||
//
|
||||
// @override
|
||||
// Stream<bool> get isVideoReadyStream => _instance.textureIdStream.map((id) => id != null);
|
||||
//
|
||||
// // `videoInfo` is never null (even if `toString` prints `null`)
|
||||
// // check presence with `hasData` instead
|
||||
// VideoInfo get _videoInfo => _instance.videoInfo;
|
||||
//
|
||||
// @override
|
||||
// int get duration => _videoInfo.durationMillis;
|
||||
//
|
||||
// @override
|
||||
// int get currentPosition => _videoInfo.currentPositionMillis;
|
||||
//
|
||||
// @override
|
||||
// Stream<int> get positionStream => _instance.videoInfoStream.map((info) => info.currentPositionMillis);
|
||||
//
|
||||
// @override
|
||||
// Widget buildPlayerWidget(BuildContext context, AvesEntry entry) => IjkPlayer(
|
||||
// mediaController: _instance,
|
||||
// controllerWidgetBuilder: (controller) => SizedBox.shrink(),
|
||||
// statusWidgetBuilder: (context, controller, status) => SizedBox.shrink(),
|
||||
// textureBuilder: (context, controller, info) {
|
||||
// var id = controller.textureId;
|
||||
// var child = id != null
|
||||
// ? Texture(
|
||||
// textureId: id,
|
||||
// )
|
||||
// : Container(
|
||||
// color: Colors.black,
|
||||
// );
|
||||
//
|
||||
// final degree = entry.rotationDegrees ?? 0;
|
||||
// if (degree != 0) {
|
||||
// child = RotatedBox(
|
||||
// quarterTurns: degree ~/ 90,
|
||||
// child: child,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return Center(
|
||||
// child: AspectRatio(
|
||||
// aspectRatio: entry.displayAspectRatio,
|
||||
// child: child,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// backgroundColor: Colors.transparent,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// extension ExtraVideoInfo on VideoInfo {
|
||||
// int get durationMillis => duration == null ? null : (duration * 1000).toInt();
|
||||
//
|
||||
// int get currentPositionMillis => currentPosition == null ? null : (currentPosition * 1000).toInt();
|
||||
// }
|
||||
//
|
||||
// extension ExtraIjkStatus on IjkStatus {
|
||||
// VideoStatus get toAves {
|
||||
// switch (this) {
|
||||
// case IjkStatus.noDatasource:
|
||||
// return VideoStatus.idle;
|
||||
// case IjkStatus.preparing:
|
||||
// return VideoStatus.preparing;
|
||||
// case IjkStatus.prepared:
|
||||
// return VideoStatus.prepared;
|
||||
// case IjkStatus.playing:
|
||||
// return VideoStatus.playing;
|
||||
// case IjkStatus.pause:
|
||||
// return VideoStatus.paused;
|
||||
// case IjkStatus.complete:
|
||||
// return VideoStatus.completed;
|
||||
// case IjkStatus.disposed:
|
||||
// return VideoStatus.disposed;
|
||||
// case IjkStatus.setDatasourceFail:
|
||||
// case IjkStatus.error:
|
||||
// return VideoStatus.error;
|
||||
// }
|
||||
// return VideoStatus.idle;
|
||||
// }
|
||||
// }
|
|
@ -21,7 +21,7 @@
|
|||
// VlcAvesVideoController();
|
||||
//
|
||||
// @override
|
||||
// Future<void> setDataSource(String uri) async {
|
||||
// Future<void> setDataSource(String uri, {int startMillis = 0}) async {
|
||||
// _instance = VlcPlayerController.file(
|
||||
// File(uri),
|
||||
// );
|
||||
|
@ -49,9 +49,6 @@
|
|||
// void _onValueChanged() => _valueStreamController.add(_instance.value);
|
||||
//
|
||||
// @override
|
||||
// Future<void> refreshVideoInfo() => null;
|
||||
//
|
||||
// @override
|
||||
// Future<void> play() => _instance.play();
|
||||
//
|
||||
// @override
|
||||
|
@ -61,9 +58,6 @@
|
|||
// Future<void> seekTo(int targetMillis) => _instance.seekTo(Duration(milliseconds: targetMillis));
|
||||
//
|
||||
// @override
|
||||
// Future<void> seekToProgress(double progress) => _instance.seekTo(Duration(milliseconds: (duration * progress).toInt()));
|
||||
//
|
||||
// @override
|
||||
// Listenable get playCompletedListenable => _playFinishNotifier;
|
||||
//
|
||||
// @override
|
||||
|
@ -73,12 +67,6 @@
|
|||
// Stream<VideoStatus> get statusStream => _valueStream.map((value) => value.toAves);
|
||||
//
|
||||
// @override
|
||||
// bool get isVideoReady => _instance != null && _instance.value.isInitialized && !_instance.value.hasError;
|
||||
//
|
||||
// @override
|
||||
// Stream<bool> get isVideoReadyStream => _valueStream.map((value) => value.isInitialized && !value.hasError);
|
||||
//
|
||||
// @override
|
||||
// bool get isPlayable => _instance != null;
|
||||
//
|
||||
// @override
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
// VideoPlayerAvesVideoController();
|
||||
//
|
||||
// @override
|
||||
// Future<void> setDataSource(String uri) async {
|
||||
// Future<void> setDataSource(String uri, {int startMillis = 0}) async {
|
||||
// _instance = VideoPlayerController.network(uri);
|
||||
// _instance.addListener(_onValueChanged);
|
||||
// _subscriptions.add(_valueStream.where((value) => value.position > value.duration).listen((_) => _playFinishNotifier.notifyListeners()));
|
||||
|
@ -40,9 +40,6 @@
|
|||
// void _onValueChanged() => _valueStreamController.add(_instance.value);
|
||||
//
|
||||
// @override
|
||||
// Future<void> refreshVideoInfo() => null;
|
||||
//
|
||||
// @override
|
||||
// Future<void> play() => _instance.play();
|
||||
//
|
||||
// @override
|
||||
|
@ -52,9 +49,6 @@
|
|||
// Future<void> seekTo(int targetMillis) => _instance.seekTo(Duration(milliseconds: targetMillis));
|
||||
//
|
||||
// @override
|
||||
// Future<void> seekToProgress(double progress) => _instance.seekTo(Duration(milliseconds: (duration * progress).toInt()));
|
||||
//
|
||||
// @override
|
||||
// Listenable get playCompletedListenable => _playFinishNotifier;
|
||||
//
|
||||
// @override
|
||||
|
@ -64,12 +58,6 @@
|
|||
// Stream<VideoStatus> get statusStream => _valueStream.map((value) => value.toAves);
|
||||
//
|
||||
// @override
|
||||
// bool get isVideoReady => _instance != null && _instance.value.isInitialized && !_instance.value.hasError;
|
||||
//
|
||||
// @override
|
||||
// Stream<bool> get isVideoReadyStream => _valueStream.map((value) => value.isInitialized && !value.hasError);
|
||||
//
|
||||
// @override
|
||||
// bool get isPlayable => _instance != null && _instance.value.isInitialized && !_instance.value.hasError;
|
||||
//
|
||||
// @override
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/settings/enums.dart';
|
|||
import 'package:aves/model/settings/home_page.dart';
|
||||
import 'package:aves/model/settings/screen_on.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/settings/video_loop_mode.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -244,6 +245,23 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
onChanged: (v) => settings.isVideoHardwareAccelerationEnabled = v,
|
||||
title: Text(context.l10n.settingsVideoEnableHardwareAcceleration),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVideoLoopModeTile),
|
||||
subtitle: Text(settings.videoLoopMode.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<VideoLoopMode>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
||||
initialValue: settings.videoLoopMode,
|
||||
options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsVideoLoopModeTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.videoLoopMode = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,9 +35,6 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
final List<StreamSubscription> _subscriptions = [];
|
||||
double _seekTargetPercent;
|
||||
|
||||
// video info is not refreshed by default, so we use a timer to do so
|
||||
Timer _progressTimer;
|
||||
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
Animation<double> get scale => widget.scale;
|
||||
|
@ -74,16 +71,13 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
|
||||
void _registerWidget(VideoControlOverlay widget) {
|
||||
_subscriptions.add(widget.controller.statusStream.listen(_onStatusChange));
|
||||
_subscriptions.add(widget.controller.isVideoReadyStream.listen(_onVideoReadinessChanged));
|
||||
_onStatusChange(widget.controller.status);
|
||||
_onVideoReadinessChanged(widget.controller.isVideoReady);
|
||||
}
|
||||
|
||||
void _unregisterWidget(VideoControlOverlay widget) {
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
_stopTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -194,24 +188,6 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
);
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
if (!controller.isVideoReady) return;
|
||||
_progressTimer?.cancel();
|
||||
_progressTimer = Timer.periodic(Durations.videoProgressTimerInterval, (_) => controller.refreshVideoInfo());
|
||||
}
|
||||
|
||||
void _stopTimer() {
|
||||
_progressTimer?.cancel();
|
||||
}
|
||||
|
||||
void _onVideoReadinessChanged(bool isVideoReady) {
|
||||
if (isVideoReady) {
|
||||
_startTimer();
|
||||
} else {
|
||||
_stopTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void _onStatusChange(VideoStatus status) {
|
||||
if (status == VideoStatus.playing && _seekTargetPercent != null) {
|
||||
_seekFromTarget();
|
||||
|
@ -247,16 +223,17 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
if (isPlayable) {
|
||||
await _seekFromTarget();
|
||||
} else {
|
||||
await controller.setDataSource(entry.uri);
|
||||
// controller duration is not set yet, so we use the expected duration instead
|
||||
final seekTargetMillis = (entry.durationMillis * _seekTargetPercent).toInt();
|
||||
await controller.setDataSource(entry.uri, startMillis: seekTargetMillis);
|
||||
_seekTargetPercent = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future _seekFromTarget() async {
|
||||
// `seekToProgress` is not safe as it can be called when the `duration` is not set yet
|
||||
// so we make sure the video info is up to date first
|
||||
if (controller.duration == null) {
|
||||
await controller.refreshVideoInfo();
|
||||
} else {
|
||||
if (controller.duration != null) {
|
||||
await controller.seekToProgress(_seekTargetPercent);
|
||||
_seekTargetPercent = null;
|
||||
}
|
||||
|
|
|
@ -68,5 +68,6 @@ class _VideoViewState extends State<VideoView> {
|
|||
});
|
||||
}
|
||||
|
||||
// not called when looping
|
||||
void _onPlayCompleted() => controller.seekTo(0);
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ packages:
|
|||
description:
|
||||
path: "."
|
||||
ref: aves
|
||||
resolved-ref: "7171c3ede20f407b523c18692572cbcd12acc169"
|
||||
resolved-ref: a4640923c7c6141ff543f676a8cb7d2fe8b0ffba
|
||||
url: "git://github.com/deckerst/fijkplayer.git"
|
||||
source: git
|
||||
version: "0.8.7"
|
||||
|
|
Loading…
Reference in a new issue