video: package study, HW acceleration macroblock fix for fijk

This commit is contained in:
Thibault Deckers 2021-04-08 10:36:57 +09:00
parent 3ddf44b6cc
commit 37dde5cb38
15 changed files with 275 additions and 43 deletions

View file

@ -58,6 +58,7 @@ android {
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties['googleApiKey']]
multiDexEnabled true
}
signingConfigs {
@ -106,6 +107,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0-rc01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
implementation 'androidx.exifinterface:exifinterface:1.3.2'
implementation "androidx.multidex:multidex:2.0.1"
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a' // forked, built by JitPack

View file

@ -1,13 +1,9 @@
import 'package:aves/model/entry.dart';
import 'package:aves/widgets/common/video/fijkplayer.dart';
// import 'package:aves/widgets/common/video/flutter_ijkplayer.dart';
import 'package:flutter/material.dart';
abstract class AvesVideoController {
AvesVideoController();
factory AvesVideoController.ijkPlayer() => IjkPlayerAvesVideoController();
void dispose();
Future<void> setDataSource(String uri);
@ -44,7 +40,7 @@ abstract class AvesVideoController {
Stream<int> get positionStream;
Widget buildPlayerWidget(AvesEntry entry);
Widget buildPlayerWidget(BuildContext context, AvesEntry entry);
}
class AvesVideoInfo {

View file

@ -1,9 +1,10 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';
@ -12,10 +13,11 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
final List<StreamSubscription> _subscriptions = [];
final StreamController<FijkValue> _valueStreamController = StreamController.broadcast();
final AChangeNotifier _playFinishNotifier = AChangeNotifier();
Offset _macroBlockCrop = Offset.zero;
Stream<FijkValue> get _valueStream => _valueStreamController.stream;
IjkPlayerAvesVideoController() {
IjkPlayerAvesVideoController(AvesEntry entry) {
_instance = FijkPlayer();
// FFmpeg options
@ -30,15 +32,27 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
// `framedrop`: drop frames when cpu is too slow, default: 0, in [-1, 120]
option.setPlayerOption('framedrop', 5);
final hwAccel = settings.isVideoHardwareAccelerationEnabled ? 1 : 0;
final _hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled;
if (_hwAccelerationEnabled) {
// crop HW acceleration macroblock misalignment for videos with dimensions that do not fit 16x
final s = entry.displaySize % 16 * -1 % 16;
_macroBlockCrop = Offset(s.width, s.height);
}
// `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', hwAccel);
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);
@ -95,9 +109,6 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
@override
Stream<bool> get isVideoReadyStream => _valueStream.map((value) => value.videoRenderStart);
// 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 => _instance.isPlayable();
@ -111,11 +122,19 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
Stream<int> get positionStream => _instance.onCurrentPosUpdate.map((pos) => pos.inMilliseconds);
@override
Widget buildPlayerWidget(AvesEntry entry) => FijkView(
Widget buildPlayerWidget(BuildContext context, AvesEntry entry) {
return FijkView(
player: _instance,
fit: FijkFit(
sizeFactor: 1.0,
aspectRatio: -1,
alignment: Alignment.topLeft,
macroBlockCrop: _macroBlockCrop,
),
panelBuilder: (player, data, context, viewSize, texturePos) => SizedBox(),
color: Colors.transparent,
);
}
}
extension ExtraIjkStatus on FijkState {

View file

@ -2,7 +2,7 @@
//
// import 'package:aves/model/entry.dart';
// import 'package:aves/utils/change_notifier.dart';
// import 'package:aves/widgets/common/video/video.dart';
// import 'package:aves/widgets/common/video/controller.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
//
@ -57,7 +57,7 @@
// // 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;
// bool get isPlayable => _videoInfo.hasData && [VideoStatus.prepared, VideoStatus.playing, VideoStatus.paused, VideoStatus.completed].contains(status);
//
// @override
// bool get isVideoReady => _instance.textureId != null;
@ -79,7 +79,7 @@
// Stream<int> get positionStream => _instance.videoInfoStream.map((info) => info.currentPositionMillis);
//
// @override
// Widget buildPlayerWidget(AvesEntry entry) => IjkPlayer(
// Widget buildPlayerWidget(BuildContext context, AvesEntry entry) => IjkPlayer(
// mediaController: _instance,
// controllerWidgetBuilder: (controller) => SizedBox.shrink(),
// statusWidgetBuilder: (context, controller, status) => SizedBox.shrink(),

View file

@ -0,0 +1,119 @@
// import 'dart:async';
// import 'dart:io';
//
// 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/src/foundation/change_notifier.dart';
// import 'package:flutter/src/widgets/framework.dart';
// import 'package:flutter_vlc_player/flutter_vlc_player.dart';
// import 'package:provider/provider.dart';
//
// class VlcAvesVideoController extends AvesVideoController {
// VlcPlayerController _instance;
// final List<StreamSubscription> _subscriptions = [];
// final StreamController<VlcPlayerValue> _valueStreamController = StreamController.broadcast();
// final AChangeNotifier _playFinishNotifier = AChangeNotifier();
//
// Stream<VlcPlayerValue> get _valueStream => _valueStreamController.stream;
//
// VlcAvesVideoController();
//
// @override
// Future<void> setDataSource(String uri) async {
// _instance = VlcPlayerController.file(
// File(uri),
// );
// _instance.addListener(_onValueChanged);
// _subscriptions.add(_valueStream.where((value) => value.isEnded).listen((_) => _playFinishNotifier.notifyListeners()));
//
// // update value stream to:
// // 1) trigger playability check
// // 2) show the `VlcPlayer` widget
// // 3) initialize its `PlatformView`
// // 4) complete `VlcPlayerController` initialization
// _valueStreamController.add(_instance.value);
// }
//
// @override
// void dispose() {
// _instance?.removeListener(_onValueChanged);
// _valueStreamController.close();
// _subscriptions
// ..forEach((sub) => sub.cancel())
// ..clear();
// _instance?.dispose();
// }
//
// void _onValueChanged() => _valueStreamController.add(_instance.value);
//
// @override
// Future<void> refreshVideoInfo() => null;
//
// @override
// Future<void> play() => _instance.play();
//
// @override
// Future<void> pause() => _instance?.pause();
//
// @override
// 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
// VideoStatus get status => _instance?.value?.toAves ?? VideoStatus.idle;
//
// @override
// 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
// int get duration => _instance?.value?.duration?.inMilliseconds;
//
// @override
// int get currentPosition => _instance?.value?.position?.inMilliseconds;
//
// @override
// Stream<int> get positionStream => _valueStream.map((value) => value.position.inMilliseconds);
//
// @override
// Widget buildPlayerWidget(BuildContext context, AvesEntry entry) {
// // do not use `Magnifier` with `applyScale` enabled when using this widget,
// // as the original video size will be used to create the `PlatformView`
// // and a virtual display larger than the device screen may crash the app
// final mqWidth = context.select<MediaQueryData, double>((mq) => mq.size.width);
// final displaySize = entry.displaySize;
// final ratio = mqWidth / displaySize.width;
// return SizedBox.fromSize(
// size: displaySize * ratio,
// child: VlcPlayer(
// controller: _instance,
// aspectRatio: entry.displayAspectRatio,
// ),
// );
// }
// }
//
// extension ExtraVlcPlayerValue on VlcPlayerValue {
// VideoStatus get toAves {
// if (hasError) return VideoStatus.error;
// if (!isInitialized) return VideoStatus.idle;
// if (isEnded) return VideoStatus.completed;
// if (isPlaying) return VideoStatus.playing;
// return VideoStatus.paused;
// }
// }

View file

@ -0,0 +1,95 @@
// 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/src/foundation/change_notifier.dart';
// import 'package:flutter/src/widgets/framework.dart';
// import 'package:video_player/video_player.dart';
//
// class VideoPlayerAvesVideoController extends AvesVideoController {
// VideoPlayerController _instance;
// final List<StreamSubscription> _subscriptions = [];
// final StreamController<VideoPlayerValue> _valueStreamController = StreamController.broadcast();
// final AChangeNotifier _playFinishNotifier = AChangeNotifier();
//
// Stream<VideoPlayerValue> get _valueStream => _valueStreamController.stream;
//
// VideoPlayerAvesVideoController();
//
// @override
// Future<void> setDataSource(String uri) async {
// _instance = VideoPlayerController.network(uri);
// _instance.addListener(_onValueChanged);
// _subscriptions.add(_valueStream.where((value) => value.position > value.duration).listen((_) => _playFinishNotifier.notifyListeners()));
//
// await _instance.initialize();
// await play();
// }
//
// @override
// void dispose() {
// _instance?.removeListener(_onValueChanged);
// _valueStreamController.close();
// _subscriptions
// ..forEach((sub) => sub.cancel())
// ..clear();
// _instance?.dispose();
// }
//
// void _onValueChanged() => _valueStreamController.add(_instance.value);
//
// @override
// Future<void> refreshVideoInfo() => null;
//
// @override
// Future<void> play() => _instance.play();
//
// @override
// Future<void> pause() => _instance?.pause();
//
// @override
// 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
// VideoStatus get status => _instance?.value?.toAves ?? VideoStatus.idle;
//
// @override
// 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
// int get duration => _instance?.value?.duration?.inMilliseconds;
//
// @override
// int get currentPosition => _instance?.value?.position?.inMilliseconds;
//
// @override
// Stream<int> get positionStream => _valueStream.map((value) => value.position.inMilliseconds);
//
// @override
// Widget buildPlayerWidget(BuildContext context, AvesEntry entry) => VideoPlayer(_instance);
// }
//
// extension ExtraVideoPlayerValue on VideoPlayerValue {
// VideoStatus get toAves {
// if (hasError) return VideoStatus.error;
// if (!isInitialized) return VideoStatus.idle;
// if (isPlaying) return VideoStatus.playing;
// return VideoStatus.paused;
// }
// }

View file

@ -3,7 +3,7 @@ import 'package:aves/model/multipage.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart';
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:aves/widgets/viewer/multipage.dart';
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
import 'package:flutter/material.dart';

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/info/info_page.dart';
import 'package:aves/widgets/viewer/info/notifications.dart';

View file

@ -11,7 +11,8 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/utils/change_notifier.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:aves/widgets/common/video/fijkplayer.dart';
import 'package:aves/widgets/viewer/entry_action_delegate.dart';
import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
import 'package:aves/widgets/viewer/hero.dart';
@ -499,7 +500,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
_initViewSpecificController<AvesVideoController>(
uri,
_videoControllers,
() => AvesVideoController.ijkPlayer(),
() => IjkPlayerAvesVideoController(entry),
(_) => _.dispose(),
);
}

View file

@ -1,4 +1,3 @@
import 'package:provider/provider.dart';
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -9,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:latlong/latlong.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
class EntryLeafletMap extends StatefulWidget {

View file

@ -6,8 +6,8 @@ import 'package:aves/services/svg_metadata_service.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_thumbnail.dart';

View file

@ -8,7 +8,7 @@ import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/material.dart';
@ -175,7 +175,9 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
Text(entry.durationText),
],
),
StreamBuilder<int>(
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: StreamBuilder<int>(
stream: controller.positionStream,
builder: (context, snapshot) {
// do not use stream snapshot because it is obsolete when switching between videos
@ -183,6 +185,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
if (!progress.isFinite) progress = 0.0;
return LinearProgressIndicator(value: progress);
}),
),
],
),
),

View file

@ -12,7 +12,7 @@ import 'package:aves/widgets/common/magnifier/magnifier.dart';
import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.dart';
import 'package:aves/widgets/common/magnifier/scale/scale_level.dart';
import 'package:aves/widgets/common/magnifier/scale/state.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/visual/error.dart';
import 'package:aves/widgets/viewer/visual/raster.dart';

View file

@ -4,7 +4,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_images.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/video/video.dart';
import 'package:aves/widgets/common/video/controller.dart';
import 'package:flutter/material.dart';
class VideoView extends StatefulWidget {
@ -53,17 +53,14 @@ class _VideoViewState extends State<VideoView> {
widget.controller.playCompletedListenable.removeListener(_onPlayCompleted);
}
bool isPlayable(VideoStatus status) => controller != null && [VideoStatus.prepared, VideoStatus.playing, VideoStatus.paused, VideoStatus.completed].contains(status);
@override
Widget build(BuildContext context) {
if (controller == null) return SizedBox();
return StreamBuilder<VideoStatus>(
stream: widget.controller.statusStream,
builder: (context, snapshot) {
final status = snapshot.data;
return isPlayable(status)
? controller.buildPlayerWidget(entry)
return controller?.isPlayable == true
? controller.buildPlayerWidget(context, entry)
: Image(
image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)),
fit: BoxFit.contain,

View file

@ -211,7 +211,7 @@ packages:
description:
path: "."
ref: aves
resolved-ref: c48e515a98851e55c857308d5482cd7bf5c9faad
resolved-ref: "7171c3ede20f407b523c18692572cbcd12acc169"
url: "git://github.com/deckerst/fijkplayer.git"
source: git
version: "0.8.7"