video: controller switch prep
This commit is contained in:
parent
f7ced5832a
commit
63de967468
10 changed files with 396 additions and 101 deletions
119
lib/widgets/common/video/fijkplayer.dart
Normal file
119
lib/widgets/common/video/fijkplayer.dart
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// import 'dart:async';
|
||||||
|
//
|
||||||
|
// import 'package:aves/model/entry.dart';
|
||||||
|
// import 'package:aves/utils/change_notifier.dart';
|
||||||
|
// import 'package:aves/widgets/common/video/video.dart';
|
||||||
|
// import 'package:fijkplayer/fijkplayer.dart';
|
||||||
|
// import 'package:flutter/material.dart';
|
||||||
|
//
|
||||||
|
// class FijkPlayerAvesVideoController extends AvesVideoController {
|
||||||
|
// FijkPlayer _instance;
|
||||||
|
// final List<StreamSubscription> _subscriptions = [];
|
||||||
|
// final StreamController<FijkValue> _valueStreamController = StreamController.broadcast();
|
||||||
|
// final AChangeNotifier _playFinishNotifier = AChangeNotifier();
|
||||||
|
//
|
||||||
|
// Stream<FijkValue> get _valueStream => _valueStreamController.stream;
|
||||||
|
//
|
||||||
|
// FijkPlayerAvesVideoController() {
|
||||||
|
// _instance = FijkPlayer();
|
||||||
|
// _instance.addListener(_onValueChanged);
|
||||||
|
// _subscriptions.add(_valueStream.where((value) => value.completed).listen((_) => _playFinishNotifier.notifyListeners()));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// void dispose() {
|
||||||
|
// _instance.removeListener(_onValueChanged);
|
||||||
|
// _valueStreamController.close();
|
||||||
|
// _subscriptions
|
||||||
|
// ..forEach((sub) => sub.cancel())
|
||||||
|
// ..clear();
|
||||||
|
// _instance.release();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// void _onValueChanged() => _valueStreamController.add(_instance.value);
|
||||||
|
//
|
||||||
|
// // 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;
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Future<void> play() => _instance.start();
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Future<void> pause() => _instance.pause();
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Future<void> seekTo(int targetMillis) => _instance.seekTo(targetMillis);
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Future<void> seekToProgress(double progress) => _instance.seekTo((duration * progress).toInt());
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Listenable get playCompletedListenable => _playFinishNotifier;
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// VideoStatus get status => _instance.state.toAves;
|
||||||
|
//
|
||||||
|
// @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);
|
||||||
|
//
|
||||||
|
// // 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();
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// int get duration => _instance.value.duration.inMilliseconds;
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// int get currentPosition => _instance.currentPos.inMilliseconds;
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Stream<int> get positionStream => _instance.onCurrentPosUpdate.map((pos) => pos.inMilliseconds);
|
||||||
|
//
|
||||||
|
// @override
|
||||||
|
// Widget buildPlayerWidget(AvesEntry entry) => FijkView(
|
||||||
|
// player: _instance,
|
||||||
|
// panelBuilder: (player, data, context, viewSize, texturePos) => SizedBox(),
|
||||||
|
// color: Colors.transparent,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// extension ExtraIjkStatus on FijkState {
|
||||||
|
// VideoStatus get toAves {
|
||||||
|
// switch (this) {
|
||||||
|
// case FijkState.idle:
|
||||||
|
// return VideoStatus.idle;
|
||||||
|
// case FijkState.initialized:
|
||||||
|
// return VideoStatus.initialized;
|
||||||
|
// case FijkState.asyncPreparing:
|
||||||
|
// return VideoStatus.preparing;
|
||||||
|
// case FijkState.prepared:
|
||||||
|
// return VideoStatus.prepared;
|
||||||
|
// case FijkState.started:
|
||||||
|
// return VideoStatus.playing;
|
||||||
|
// case FijkState.paused:
|
||||||
|
// return VideoStatus.paused;
|
||||||
|
// case FijkState.completed:
|
||||||
|
// return VideoStatus.completed;
|
||||||
|
// case FijkState.stopped:
|
||||||
|
// return VideoStatus.stopped;
|
||||||
|
// case FijkState.end:
|
||||||
|
// return VideoStatus.disposed;
|
||||||
|
// case FijkState.error:
|
||||||
|
// return VideoStatus.error;
|
||||||
|
// }
|
||||||
|
// return VideoStatus.idle;
|
||||||
|
// }
|
||||||
|
// }
|
144
lib/widgets/common/video/flutter_ijkplayer.dart
Normal file
144
lib/widgets/common/video/flutter_ijkplayer.dart
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
|
|
||||||
|
class FlutterIjkPlayerAvesVideoController extends AvesVideoController {
|
||||||
|
IjkMediaController _instance;
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
final AChangeNotifier _playFinishNotifier = AChangeNotifier();
|
||||||
|
|
||||||
|
FlutterIjkPlayerAvesVideoController() {
|
||||||
|
_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;
|
||||||
|
|
||||||
|
@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(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;
|
||||||
|
}
|
||||||
|
}
|
73
lib/widgets/common/video/video.dart
Normal file
73
lib/widgets/common/video/video.dart
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
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.flutterIjkPlayer() => FlutterIjkPlayerAvesVideoController();
|
||||||
|
|
||||||
|
// factory AvesVideoController.fijkPlayer() => FijkPlayerAvesVideoController();
|
||||||
|
|
||||||
|
void dispose();
|
||||||
|
|
||||||
|
Future<void> setDataSource(String uri);
|
||||||
|
|
||||||
|
Future<void> refreshVideoInfo();
|
||||||
|
|
||||||
|
Future<void> play();
|
||||||
|
|
||||||
|
Future<void> pause();
|
||||||
|
|
||||||
|
Future<void> seekTo(int targetMillis);
|
||||||
|
|
||||||
|
Future<void> seekToProgress(double progress);
|
||||||
|
|
||||||
|
Listenable get playCompletedListenable;
|
||||||
|
|
||||||
|
VideoStatus get status;
|
||||||
|
|
||||||
|
Stream<VideoStatus> get statusStream;
|
||||||
|
|
||||||
|
bool get isPlayable;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
Stream<int> get positionStream;
|
||||||
|
|
||||||
|
Widget buildPlayerWidget(AvesEntry entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AvesVideoInfo {
|
||||||
|
// in millis
|
||||||
|
int duration, currentPosition;
|
||||||
|
|
||||||
|
AvesVideoInfo({
|
||||||
|
this.duration,
|
||||||
|
this.currentPosition,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VideoStatus {
|
||||||
|
idle,
|
||||||
|
initialized,
|
||||||
|
preparing,
|
||||||
|
prepared,
|
||||||
|
playing,
|
||||||
|
paused,
|
||||||
|
completed,
|
||||||
|
stopped,
|
||||||
|
disposed,
|
||||||
|
error,
|
||||||
|
}
|
|
@ -3,10 +3,10 @@ import 'package:aves/model/multipage.dart';
|
||||||
import 'package:aves/model/source/collection_lens.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/gesture_detector_scope.dart';
|
||||||
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
import 'package:aves/widgets/viewer/multipage.dart';
|
import 'package:aves/widgets/viewer/multipage.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
|
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class MultiEntryScroller extends StatefulWidget {
|
||||||
final PageController pageController;
|
final PageController pageController;
|
||||||
final ValueChanged<int> onPageChanged;
|
final ValueChanged<int> onPageChanged;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||||
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||||
final void Function(String uri) onViewDisposed;
|
final void Function(String uri) onViewDisposed;
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
||||||
mainEntry: entry,
|
mainEntry: entry,
|
||||||
page: page,
|
page: page,
|
||||||
viewportSize: mqSize,
|
viewportSize: mqSize,
|
||||||
onTap: (_) => widget.onTap?.call(),
|
onTap: widget.onTap == null ? null : (_) => widget.onTap(),
|
||||||
videoControllers: widget.videoControllers,
|
videoControllers: widget.videoControllers,
|
||||||
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
|
onDisposed: () => widget.onViewDisposed?.call(entry.uri),
|
||||||
);
|
);
|
||||||
|
@ -108,7 +108,7 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
||||||
class SingleEntryScroller extends StatefulWidget {
|
class SingleEntryScroller extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||||
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||||
|
|
||||||
const SingleEntryScroller({
|
const SingleEntryScroller({
|
||||||
|
@ -163,7 +163,7 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
||||||
mainEntry: entry,
|
mainEntry: entry,
|
||||||
page: page,
|
page: page,
|
||||||
viewportSize: mqSize,
|
viewportSize: mqSize,
|
||||||
onTap: (_) => widget.onTap?.call(),
|
onTap: widget.onTap == null ? null : (_) => widget.onTap(),
|
||||||
videoControllers: widget.videoControllers,
|
videoControllers: widget.videoControllers,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_horizontal_pager.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/info_page.dart';
|
||||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||||
|
@ -10,13 +11,12 @@ import 'package:aves/widgets/viewer/multipage.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class ViewerVerticalPageView extends StatefulWidget {
|
class ViewerVerticalPageView extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ValueNotifier<AvesEntry> entryNotifier;
|
final ValueNotifier<AvesEntry> entryNotifier;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||||
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||||
final PageController horizontalPager, verticalPager;
|
final PageController horizontalPager, verticalPager;
|
||||||
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
||||||
|
@ -32,7 +32,7 @@ class ViewerVerticalPageView extends StatefulWidget {
|
||||||
@required this.horizontalPager,
|
@required this.horizontalPager,
|
||||||
@required this.onVerticalPageChanged,
|
@required this.onVerticalPageChanged,
|
||||||
@required this.onHorizontalPageChanged,
|
@required this.onHorizontalPageChanged,
|
||||||
@required this.onImageTap,
|
this.onImageTap,
|
||||||
@required this.onImagePageRequested,
|
@required this.onImagePageRequested,
|
||||||
@required this.onViewDisposed,
|
@required this.onViewDisposed,
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_action_delegate.dart';
|
import 'package:aves/widgets/viewer/entry_action_delegate.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
|
import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
|
||||||
import 'package:aves/widgets/viewer/hero.dart';
|
import 'package:aves/widgets/viewer/hero.dart';
|
||||||
|
@ -25,7 +26,6 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
||||||
Animation<Offset> _bottomOverlayOffset;
|
Animation<Offset> _bottomOverlayOffset;
|
||||||
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
||||||
EntryActionDelegate _actionDelegate;
|
EntryActionDelegate _actionDelegate;
|
||||||
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
|
final List<Tuple2<String, AvesVideoController>> _videoControllers = [];
|
||||||
final List<Tuple2<String, MultiPageController>> _multiPageControllers = [];
|
final List<Tuple2<String, MultiPageController>> _multiPageControllers = [];
|
||||||
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
|
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
|
||||||
final ValueNotifier<HeroInfo> _heroInfoNotifier = ValueNotifier(null);
|
final ValueNotifier<HeroInfo> _heroInfoNotifier = ValueNotifier(null);
|
||||||
|
@ -496,10 +496,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
||||||
(_) => _.dispose(),
|
(_) => _.dispose(),
|
||||||
);
|
);
|
||||||
if (entry.isVideo) {
|
if (entry.isVideo) {
|
||||||
_initViewSpecificController<IjkMediaController>(
|
_initViewSpecificController<AvesVideoController>(
|
||||||
uri,
|
uri,
|
||||||
_videoControllers,
|
_videoControllers,
|
||||||
() => IjkMediaController(),
|
() => AvesVideoController.flutterIjkPlayer(),
|
||||||
(_) => _.dispose(),
|
(_) => _.dispose(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,13 @@ import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
|
|
||||||
class VideoControlOverlay extends StatefulWidget {
|
class VideoControlOverlay extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final IjkMediaController controller;
|
final AvesVideoController controller;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
|
|
||||||
const VideoControlOverlay({
|
const VideoControlOverlay({
|
||||||
|
@ -42,18 +42,11 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
|
|
||||||
Animation<double> get scale => widget.scale;
|
Animation<double> get scale => widget.scale;
|
||||||
|
|
||||||
IjkMediaController get controller => widget.controller;
|
AvesVideoController get controller => widget.controller;
|
||||||
|
|
||||||
// `videoInfo` is never null (even if `toString` prints `null`)
|
bool get isPlayable => controller.isPlayable;
|
||||||
// check presence with `hasData` instead
|
|
||||||
VideoInfo get videoInfo => controller.videoInfo;
|
|
||||||
|
|
||||||
// we check whether video info is ready instead of checking for `noDatasource` status,
|
bool get isPlaying => controller.isPlaying;
|
||||||
// as the controller could also be uninitialized with the `pause` status
|
|
||||||
// (e.g. when switching between video entries without playing them the first time)
|
|
||||||
bool get isInitialized => videoInfo.hasData;
|
|
||||||
|
|
||||||
bool get isPlaying => controller.ijkStatus == IjkStatus.playing;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -80,10 +73,10 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget(VideoControlOverlay widget) {
|
void _registerWidget(VideoControlOverlay widget) {
|
||||||
_subscriptions.add(widget.controller.ijkStatusStream.listen(_onStatusChange));
|
_subscriptions.add(widget.controller.statusStream.listen(_onStatusChange));
|
||||||
_subscriptions.add(widget.controller.textureIdStream.listen(_onTextureIdChange));
|
_subscriptions.add(widget.controller.isVideoReadyStream.listen(_onVideoReadinessChanged));
|
||||||
_onStatusChange(widget.controller.ijkStatus);
|
_onStatusChange(widget.controller.status);
|
||||||
_onTextureIdChange(widget.controller.textureId);
|
_onVideoReadinessChanged(widget.controller.isVideoReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(VideoControlOverlay widget) {
|
void _unregisterWidget(VideoControlOverlay widget) {
|
||||||
|
@ -95,18 +88,18 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return StreamBuilder<IjkStatus>(
|
return StreamBuilder<VideoStatus>(
|
||||||
stream: controller.ijkStatusStream,
|
stream: controller.statusStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
// do not use stream snapshot because it is obsolete when switching between videos
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
final status = controller.ijkStatus;
|
final status = controller.status;
|
||||||
return TooltipTheme(
|
return TooltipTheme(
|
||||||
data: TooltipTheme.of(context).copyWith(
|
data: TooltipTheme.of(context).copyWith(
|
||||||
preferBelow: false,
|
preferBelow: false,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: status == IjkStatus.error
|
children: status == VideoStatus.error
|
||||||
? [
|
? [
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
@ -171,22 +164,22 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder<VideoInfo>(
|
StreamBuilder<int>(
|
||||||
stream: controller.videoInfoStream,
|
stream: controller.positionStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
// do not use stream snapshot because it is obsolete when switching between videos
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
final position = videoInfo.currentPosition?.floor() ?? 0;
|
final position = controller.currentPosition?.floor() ?? 0;
|
||||||
return Text(formatDuration(Duration(seconds: position)));
|
return Text(formatDuration(Duration(milliseconds: position)));
|
||||||
}),
|
}),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Text(entry.durationText),
|
Text(entry.durationText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
StreamBuilder<VideoInfo>(
|
StreamBuilder<int>(
|
||||||
stream: controller.videoInfoStream,
|
stream: controller.positionStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
// do not use stream snapshot because it is obsolete when switching between videos
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
var progress = videoInfo.progress;
|
var progress = controller.progress;
|
||||||
if (!progress.isFinite) progress = 0.0;
|
if (!progress.isFinite) progress = 0.0;
|
||||||
return LinearProgressIndicator(value: progress);
|
return LinearProgressIndicator(value: progress);
|
||||||
}),
|
}),
|
||||||
|
@ -199,7 +192,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
if (controller.textureId == null) return;
|
if (!controller.isVideoReady) return;
|
||||||
_progressTimer?.cancel();
|
_progressTimer?.cancel();
|
||||||
_progressTimer = Timer.periodic(Durations.videoProgressTimerInterval, (_) => controller.refreshVideoInfo());
|
_progressTimer = Timer.periodic(Durations.videoProgressTimerInterval, (_) => controller.refreshVideoInfo());
|
||||||
}
|
}
|
||||||
|
@ -208,16 +201,16 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
_progressTimer?.cancel();
|
_progressTimer?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTextureIdChange(int textureId) {
|
void _onVideoReadinessChanged(bool isVideoReady) {
|
||||||
if (textureId != null) {
|
if (isVideoReady) {
|
||||||
_startTimer();
|
_startTimer();
|
||||||
} else {
|
} else {
|
||||||
_stopTimer();
|
_stopTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStatusChange(IjkStatus status) {
|
void _onStatusChange(VideoStatus status) {
|
||||||
if (status == IjkStatus.playing && _seekTargetPercent != null) {
|
if (status == VideoStatus.playing && _seekTargetPercent != null) {
|
||||||
_seekFromTarget();
|
_seekFromTarget();
|
||||||
}
|
}
|
||||||
_updatePlayPauseIcon();
|
_updatePlayPauseIcon();
|
||||||
|
@ -226,10 +219,10 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
Future<void> _playPause() async {
|
Future<void> _playPause() async {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await controller.pause();
|
await controller.pause();
|
||||||
} else if (isInitialized) {
|
} else if (isPlayable) {
|
||||||
await controller.play();
|
await controller.play();
|
||||||
} else {
|
} else {
|
||||||
await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true);
|
await controller.setDataSource(entry.uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,19 +241,17 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
final localPosition = box.globalToLocal(globalPosition);
|
final localPosition = box.globalToLocal(globalPosition);
|
||||||
_seekTargetPercent = (localPosition.dx / box.size.width);
|
_seekTargetPercent = (localPosition.dx / box.size.width);
|
||||||
|
|
||||||
if (isInitialized) {
|
if (isPlayable) {
|
||||||
await _seekFromTarget();
|
await _seekFromTarget();
|
||||||
} else {
|
} else {
|
||||||
// autoplay when seeking on uninitialized player, otherwise the texture is not updated
|
await controller.setDataSource(entry.uri);
|
||||||
// as a workaround, pausing after a brief duration is possible, but fiddly
|
|
||||||
await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _seekFromTarget() async {
|
Future _seekFromTarget() async {
|
||||||
// `seekToProgress` is not safe as it can be called when the `duration` is not set yet
|
// `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
|
// so we make sure the video info is up to date first
|
||||||
if (videoInfo.duration == null) {
|
if (controller.duration == null) {
|
||||||
await controller.refreshVideoInfo();
|
await controller.refreshVideoInfo();
|
||||||
} else {
|
} else {
|
||||||
await controller.seekToProgress(_seekTargetPercent);
|
await controller.seekToProgress(_seekTargetPercent);
|
||||||
|
|
|
@ -12,6 +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_boundaries.dart';
|
||||||
import 'package:aves/widgets/common/magnifier/scale/scale_level.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/magnifier/scale/state.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
import 'package:aves/widgets/viewer/hero.dart';
|
import 'package:aves/widgets/viewer/hero.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/error.dart';
|
import 'package:aves/widgets/viewer/visual/error.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/raster.dart';
|
import 'package:aves/widgets/viewer/visual/raster.dart';
|
||||||
|
@ -20,7 +21,6 @@ import 'package:aves/widgets/viewer/visual/vector.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/video.dart';
|
import 'package:aves/widgets/viewer/visual/video.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
@ -31,7 +31,7 @@ class EntryPageView extends StatefulWidget {
|
||||||
final SinglePageInfo page;
|
final SinglePageInfo page;
|
||||||
final Size viewportSize;
|
final Size viewportSize;
|
||||||
final MagnifierTapCallback onTap;
|
final MagnifierTapCallback onTap;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||||
final VoidCallback onDisposed;
|
final VoidCallback onDisposed;
|
||||||
|
|
||||||
static const decorationCheckSize = 20.0;
|
static const decorationCheckSize = 20.0;
|
||||||
|
@ -138,7 +138,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
}
|
}
|
||||||
child ??= ErrorView(
|
child ??= ErrorView(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
onTap: () => onTap?.call(null),
|
onTap: onTap == null ? null : () => onTap(null),
|
||||||
);
|
);
|
||||||
return child;
|
return child;
|
||||||
},
|
},
|
||||||
|
@ -221,7 +221,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
||||||
initialScale: initialScale,
|
initialScale: initialScale,
|
||||||
scaleStateCycle: scaleStateCycle,
|
scaleStateCycle: scaleStateCycle,
|
||||||
applyScale: applyScale,
|
applyScale: applyScale,
|
||||||
onTap: (c, d, s, childPosition) => onTap?.call(childPosition),
|
onTap: onTap == null ? null : (c, d, s, childPosition) => onTap(childPosition),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:ui';
|
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/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/common/video/video.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
|
||||||
|
|
||||||
class VideoView extends StatefulWidget {
|
class VideoView extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
final IjkMediaController controller;
|
final AvesVideoController controller;
|
||||||
|
|
||||||
const VideoView({
|
const VideoView({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -23,11 +22,9 @@ class VideoView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoViewState extends State<VideoView> {
|
class _VideoViewState extends State<VideoView> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
IjkMediaController get controller => widget.controller;
|
AvesVideoController get controller => widget.controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -49,56 +46,24 @@ class _VideoViewState extends State<VideoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget(VideoView widget) {
|
void _registerWidget(VideoView widget) {
|
||||||
_subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish));
|
widget.controller.playCompletedListenable.addListener(_onPlayCompleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(VideoView widget) {
|
void _unregisterWidget(VideoView widget) {
|
||||||
_subscriptions
|
widget.controller.playCompletedListenable.removeListener(_onPlayCompleted);
|
||||||
..forEach((sub) => sub.cancel())
|
|
||||||
..clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isPlayable(IjkStatus status) => controller != null && [IjkStatus.prepared, IjkStatus.playing, IjkStatus.pause, IjkStatus.complete].contains(status);
|
bool isPlayable(VideoStatus status) => controller != null && [VideoStatus.prepared, VideoStatus.playing, VideoStatus.paused, VideoStatus.completed].contains(status);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (controller == null) return SizedBox();
|
if (controller == null) return SizedBox();
|
||||||
return StreamBuilder<IjkStatus>(
|
return StreamBuilder<VideoStatus>(
|
||||||
stream: widget.controller.ijkStatusStream,
|
stream: widget.controller.statusStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final status = snapshot.data;
|
final status = snapshot.data;
|
||||||
return isPlayable(status)
|
return isPlayable(status)
|
||||||
? IjkPlayer(
|
? controller.buildPlayerWidget(entry)
|
||||||
mediaController: controller,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
: Image(
|
: Image(
|
||||||
image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)),
|
image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
|
@ -106,5 +71,5 @@ class _VideoViewState extends State<VideoView> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPlayFinish(IjkMediaController controller) => controller.seekTo(0);
|
void _onPlayCompleted() => controller.seekTo(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ dependencies:
|
||||||
firebase_analytics:
|
firebase_analytics:
|
||||||
firebase_crashlytics:
|
firebase_crashlytics:
|
||||||
flutter_highlight:
|
flutter_highlight:
|
||||||
|
# fijkplayer:
|
||||||
flutter_ijkplayer:
|
flutter_ijkplayer:
|
||||||
# path: ../flutter_ijkplayer
|
# path: ../flutter_ijkplayer
|
||||||
git:
|
git:
|
||||||
|
@ -120,13 +121,15 @@ flutter:
|
||||||
# - does not support AVI/XVID, AC3
|
# - does not support AVI/XVID, AC3
|
||||||
# - cannot play if only the video or audio stream is supported
|
# - cannot play if only the video or audio stream is supported
|
||||||
|
|
||||||
# fijkplayer (as of v0.7.1, backed by IJKPlayer & ffmpeg):
|
# fijkplayer (as of v0.8.7, backed by IJKPlayer & ffmpeg):
|
||||||
# - support content URIs
|
# - support content URIs
|
||||||
# - does not support XVID, AC3 (by default, but possible by custom build)
|
# - does not support XVID, AC3 (by default, but possible by custom build)
|
||||||
# - can play if only the video or audio stream is supported
|
# - can play if only the video or audio stream is supported
|
||||||
# - crash when calling `seekTo` for some files (e.g. TED talk videos)
|
# - crash when calling `seekTo` for some files (e.g. TED talk videos)
|
||||||
|
# - no edge smear (with default build)
|
||||||
|
|
||||||
# flutter_ijkplayer (as of v0.3.5+1, backed by IJKPlayer & ffmpeg):
|
# flutter_ijkplayer (as of v0.3.5+1, backed by IJKPlayer & ffmpeg):
|
||||||
# - support content URIs (`DataSource.photoManagerUrl` from v0.3.6, but need fork to support content URIs on Android <Q)
|
# - support content URIs (`DataSource.photoManagerUrl` from v0.3.6, but need fork to support content URIs on Android <Q)
|
||||||
# - does not support AC3 (by default, but possible by custom build)
|
# - does not support AC3 (by default, but possible by custom build)
|
||||||
# - can play if only the video or audio stream is supported
|
# - can play if only the video or audio stream is supported
|
||||||
|
# - edge smear on some videos, depending on dimensions (dimension not multiple of 16?)
|
||||||
|
|
Loading…
Reference in a new issue