Squashed commit of the following:
commit a80d48e19d05d6b9978cc293d5d3dd460c387d27 Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Mon Apr 20 08:34:50 2020 +0900 video: fixed status check commit d5af7cecd5c14c47b108456777da170052b7754f Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Sun Apr 19 22:13:58 2020 +0900 safer seek commit f84768dd9ac5a70a4489509bd944685298023550 Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Sun Apr 19 22:08:06 2020 +0900 use forked `flutter_ijkplayer` to support content URIs on Android < Q commit fde82bc213b0058cd990af2c7678f46b20c78bd7 Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Sun Apr 19 18:39:18 2020 +0900 packages upgrade commit 14414f32203a5caccdb61902ce75b0d83a1a8656 Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Sun Apr 19 14:57:38 2020 +0900 fixes for flutter_ijkplayer commit 2944d84d9f334bbe54303f7eb3b82a517664e84a Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Fri Apr 17 15:58:29 2020 +0900 draft for flutter_ijkplayer commit 0d82956b8e7e1d4500d09805a5d0fd59d2361ed3 Author: Thibault Deckers <thibault.deckers@gmail.com> Date: Fri Apr 17 13:00:14 2020 +0900 switch from video_player to fijkplayer
This commit is contained in:
parent
19976940a0
commit
e88568e706
7 changed files with 272 additions and 155 deletions
|
@ -19,10 +19,10 @@ 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:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
class FullscreenBody extends StatefulWidget {
|
class FullscreenBody extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -50,7 +50,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
Animation<Offset> _bottomOverlayOffset;
|
Animation<Offset> _bottomOverlayOffset;
|
||||||
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
||||||
FullscreenActionDelegate _actionDelegate;
|
FullscreenActionDelegate _actionDelegate;
|
||||||
final List<Tuple2<String, VideoPlayerController>> _videoControllers = [];
|
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
|
@ -114,6 +114,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
_overlayAnimationController.dispose();
|
_overlayAnimationController.dispose();
|
||||||
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
||||||
_videoControllers.forEach((kv) => kv.item2.dispose());
|
_videoControllers.forEach((kv) => kv.item2.dispose());
|
||||||
|
_videoControllers.clear();
|
||||||
_verticalPager.removeListener(_onVerticalPageControllerChange);
|
_verticalPager.removeListener(_onVerticalPageControllerChange);
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
@ -330,7 +331,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
|
|
||||||
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
|
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
|
||||||
|
|
||||||
void _initVideoController() {
|
Future<void> _initVideoController() async {
|
||||||
if (_entry == null || !_entry.isVideo) return;
|
if (_entry == null || !_entry.isVideo) return;
|
||||||
|
|
||||||
final uri = _entry.uri;
|
final uri = _entry.uri;
|
||||||
|
@ -338,9 +339,8 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
if (controllerEntry != null) {
|
if (controllerEntry != null) {
|
||||||
_videoControllers.remove(controllerEntry);
|
_videoControllers.remove(controllerEntry);
|
||||||
} else {
|
} else {
|
||||||
// unsupported by video_player 0.10.8+2 (backed by ExoPlayer): AVI
|
// do not set data source of IjkMediaController here
|
||||||
final controller = VideoPlayerController.uri(uri)..initialize();
|
controllerEntry = Tuple2(uri, IjkMediaController());
|
||||||
controllerEntry = Tuple2(uri, controller);
|
|
||||||
}
|
}
|
||||||
_videoControllers.insert(0, controllerEntry);
|
_videoControllers.insert(0, controllerEntry);
|
||||||
while (_videoControllers.length > 3) {
|
while (_videoControllers.length > 3) {
|
||||||
|
@ -352,7 +352,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
class FullscreenVerticalPageView extends StatefulWidget {
|
class FullscreenVerticalPageView extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final List<Tuple2<String, VideoPlayerController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
final PageController horizontalPager, verticalPager;
|
final PageController horizontalPager, verticalPager;
|
||||||
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
||||||
final VoidCallback onImageTap, onImagePageRequested;
|
final VoidCallback onImageTap, onImagePageRequested;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_view.dart';
|
import 'package:aves/widgets/fullscreen/image_view.dart';
|
||||||
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
class MultiImagePage extends StatefulWidget {
|
class MultiImagePage extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -12,7 +12,7 @@ class MultiImagePage extends StatefulWidget {
|
||||||
final ValueChanged<int> onPageChanged;
|
final ValueChanged<int> onPageChanged;
|
||||||
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, VideoPlayerController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
|
||||||
const MultiImagePage({
|
const MultiImagePage({
|
||||||
this.collection,
|
this.collection,
|
||||||
|
@ -65,7 +65,7 @@ class SingleImagePage extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, VideoPlayerController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
|
||||||
const SingleImagePage({
|
const SingleImagePage({
|
||||||
this.entry,
|
this.entry,
|
||||||
|
|
|
@ -4,18 +4,18 @@ import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
||||||
import 'package:aves/widgets/fullscreen/video_view.dart';
|
import 'package:aves/widgets/fullscreen/video_view.dart';
|
||||||
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
class ImageView extends StatelessWidget {
|
class ImageView extends StatelessWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final Object heroTag;
|
final Object heroTag;
|
||||||
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
final ValueChanged<PhotoViewScaleState> onScaleChanged;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final List<Tuple2<String, VideoPlayerController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
|
|
||||||
const ImageView({
|
const ImageView({
|
||||||
this.entry,
|
this.entry,
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/utils/time_utils.dart';
|
import 'package:aves/utils/time_utils.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
class VideoControlOverlay extends StatefulWidget {
|
class VideoControlOverlay extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final VideoPlayerController controller;
|
final IjkMediaController controller;
|
||||||
final EdgeInsets viewInsets, viewPadding;
|
final EdgeInsets viewInsets, viewPadding;
|
||||||
|
|
||||||
const VideoControlOverlay({
|
const VideoControlOverlay({
|
||||||
|
@ -32,16 +34,28 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
final GlobalKey _progressBarKey = GlobalKey();
|
final GlobalKey _progressBarKey = GlobalKey();
|
||||||
bool _playingOnDragStart = false;
|
bool _playingOnDragStart = false;
|
||||||
AnimationController _playPauseAnimation;
|
AnimationController _playPauseAnimation;
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
double _seekTargetPercent;
|
||||||
|
|
||||||
|
// video info is not refreshed by default, so we use a timer to do so
|
||||||
|
Timer _progressTimer;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
Animation<double> get scale => widget.scale;
|
Animation<double> get scale => widget.scale;
|
||||||
|
|
||||||
VideoPlayerController get controller => widget.controller;
|
IjkMediaController get controller => widget.controller;
|
||||||
|
|
||||||
VideoPlayerValue get value => widget.controller.value;
|
// `videoInfo` is never null (even if `toString` prints `null`)
|
||||||
|
// check presence with `hasData` instead
|
||||||
|
VideoInfo get videoInfo => controller.videoInfo;
|
||||||
|
|
||||||
double get progress => value.position != null && value.duration != null ? value.position.inMilliseconds / value.duration.inMilliseconds : 0;
|
// 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)
|
||||||
|
bool get isInitialized => videoInfo.hasData;
|
||||||
|
|
||||||
|
bool get isPlaying => controller.ijkStatus == IjkStatus.playing;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -51,7 +65,6 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
_onValueChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -69,11 +82,17 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget(VideoControlOverlay widget) {
|
void _registerWidget(VideoControlOverlay widget) {
|
||||||
widget.controller.addListener(_onValueChange);
|
_subscriptions.add(widget.controller.ijkStatusStream.listen(_onStatusChange));
|
||||||
|
_subscriptions.add(widget.controller.textureIdStream.listen(_onTextureIdChange));
|
||||||
|
_onStatusChange(widget.controller.ijkStatus);
|
||||||
|
_onTextureIdChange(widget.controller.textureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(VideoControlOverlay widget) {
|
void _unregisterWidget(VideoControlOverlay widget) {
|
||||||
widget.controller.removeListener(_onValueChange);
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
|
_stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -93,9 +112,14 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
padding: safePadding,
|
padding: safePadding,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: mqWidth - safePadding.horizontal,
|
width: mqWidth - safePadding.horizontal,
|
||||||
child: Row(
|
child: StreamBuilder<IjkStatus>(
|
||||||
|
stream: controller.ijkStatusStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
|
final status = controller.ijkStatus;
|
||||||
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: value.hasError
|
children: status == IjkStatus.error
|
||||||
? [
|
? [
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
@ -119,11 +143,12 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
progress: _playPauseAnimation,
|
progress: _playPauseAnimation,
|
||||||
),
|
),
|
||||||
onPressed: _playPause,
|
onPressed: _playPause,
|
||||||
tooltip: value.isPlaying ? 'Pause' : 'Play',
|
tooltip: isPlaying ? 'Pause' : 'Play',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -138,14 +163,14 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
borderRadius: progressBarBorderRadius,
|
borderRadius: progressBarBorderRadius,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTapDown: (TapDownDetails details) {
|
onTapDown: (TapDownDetails details) {
|
||||||
_seek(details.globalPosition);
|
_seekFromTap(details.globalPosition);
|
||||||
},
|
},
|
||||||
onHorizontalDragStart: (DragStartDetails details) {
|
onHorizontalDragStart: (DragStartDetails details) {
|
||||||
_playingOnDragStart = controller.value.isPlaying;
|
_playingOnDragStart = isPlaying;
|
||||||
if (_playingOnDragStart) controller.pause();
|
if (_playingOnDragStart) controller.pause();
|
||||||
},
|
},
|
||||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||||
_seek(details.globalPosition);
|
_seekFromTap(details.globalPosition);
|
||||||
},
|
},
|
||||||
onHorizontalDragEnd: (DragEndDetails details) {
|
onHorizontalDragEnd: (DragEndDetails details) {
|
||||||
if (_playingOnDragStart) controller.play();
|
if (_playingOnDragStart) controller.play();
|
||||||
|
@ -164,12 +189,25 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(formatDuration(value.position ?? Duration.zero)),
|
StreamBuilder<VideoInfo>(
|
||||||
|
stream: controller.videoInfoStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
|
final position = videoInfo.currentPosition?.floor() ?? 0;
|
||||||
|
return Text(formatDuration(Duration(seconds: position)));
|
||||||
|
}),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(formatDuration(value.duration ?? Duration.zero)),
|
Text(entry.durationText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
LinearProgressIndicator(value: progress),
|
StreamBuilder<VideoInfo>(
|
||||||
|
stream: controller.videoInfoStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
|
var progress = videoInfo.progress;
|
||||||
|
if (!progress.isFinite) progress = 0.0;
|
||||||
|
return LinearProgressIndicator(value: progress);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -178,23 +216,44 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onValueChange() {
|
void _startTimer() {
|
||||||
setState(() {});
|
if (controller.textureId == null) return;
|
||||||
updatePlayPauseIcon();
|
_progressTimer?.cancel();
|
||||||
|
_progressTimer = Timer.periodic(const Duration(milliseconds: 350), (timer) {
|
||||||
|
controller.refreshVideoInfo();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopTimer() {
|
||||||
|
_progressTimer?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTextureIdChange(int textureId) {
|
||||||
|
if (textureId != null) {
|
||||||
|
_startTimer();
|
||||||
|
} else {
|
||||||
|
_stopTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStatusChange(IjkStatus status) {
|
||||||
|
if (status == IjkStatus.playing && _seekTargetPercent != null) {
|
||||||
|
_seekFromTarget();
|
||||||
|
}
|
||||||
|
_updatePlayPauseIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _playPause() async {
|
Future<void> _playPause() async {
|
||||||
if (value.isPlaying) {
|
if (isPlaying) {
|
||||||
await controller.pause();
|
await controller.pause();
|
||||||
} else {
|
} else if (isInitialized) {
|
||||||
if (!value.initialized) await controller.initialize();
|
|
||||||
await controller.play();
|
await controller.play();
|
||||||
|
} else {
|
||||||
|
await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true);
|
||||||
}
|
}
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updatePlayPauseIcon() {
|
void _updatePlayPauseIcon() {
|
||||||
final isPlaying = value.isPlaying;
|
|
||||||
final status = _playPauseAnimation.status;
|
final status = _playPauseAnimation.status;
|
||||||
if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) {
|
if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) {
|
||||||
_playPauseAnimation.forward();
|
_playPauseAnimation.forward();
|
||||||
|
@ -203,10 +262,29 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _seek(Offset globalPosition) {
|
void _seekFromTap(Offset globalPosition) async {
|
||||||
final keyContext = _progressBarKey.currentContext;
|
final keyContext = _progressBarKey.currentContext;
|
||||||
final RenderBox box = keyContext.findRenderObject();
|
final RenderBox box = keyContext.findRenderObject();
|
||||||
final localPosition = box.globalToLocal(globalPosition);
|
final localPosition = box.globalToLocal(globalPosition);
|
||||||
controller.seekTo(value.duration * (localPosition.dx / box.size.width));
|
_seekTargetPercent = (localPosition.dx / box.size.width);
|
||||||
|
|
||||||
|
if (isInitialized) {
|
||||||
|
await _seekFromTarget();
|
||||||
|
} else {
|
||||||
|
// autoplay when seeking on uninitialized player, otherwise the texture is not updated
|
||||||
|
// as a workaround, pausing after a brief duration is possible, but fiddly
|
||||||
|
await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (videoInfo.duration == null) {
|
||||||
|
await controller.refreshVideoInfo();
|
||||||
|
} else {
|
||||||
|
await controller.seekToProgress(_seekTargetPercent);
|
||||||
|
_seekTargetPercent = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
|
||||||
|
|
||||||
class AvesVideo extends StatefulWidget {
|
class AvesVideo extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final VideoPlayerController controller;
|
final IjkMediaController controller;
|
||||||
|
|
||||||
const AvesVideo({
|
const AvesVideo({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -20,15 +21,16 @@ class AvesVideo extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvesVideoState extends State<AvesVideo> {
|
class AvesVideoState extends State<AvesVideo> {
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
|
||||||
VideoPlayerValue get value => widget.controller.value;
|
IjkMediaController get controller => widget.controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
_onValueChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -45,38 +47,78 @@ class AvesVideoState extends State<AvesVideo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registerWidget(AvesVideo widget) {
|
void _registerWidget(AvesVideo widget) {
|
||||||
widget.controller.addListener(_onValueChange);
|
_subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(AvesVideo widget) {
|
void _unregisterWidget(AvesVideo widget) {
|
||||||
widget.controller.removeListener(_onValueChange);
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPlayable(IjkStatus status) => [IjkStatus.prepared, IjkStatus.playing, IjkStatus.pause, IjkStatus.complete].contains(status);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (value == null) return const SizedBox();
|
if (controller == null) return const SizedBox();
|
||||||
if (value.hasError) {
|
return StreamBuilder<IjkStatus>(
|
||||||
return Image(
|
stream: widget.controller.ijkStatusStream,
|
||||||
image: UriImage(uri: entry.uri, mimeType: entry.mimeType),
|
builder: (context, snapshot) {
|
||||||
width: entry.width.toDouble(),
|
final status = snapshot.data;
|
||||||
height: entry.height.toDouble(),
|
return isPlayable(status)
|
||||||
);
|
? IjkPlayer(
|
||||||
}
|
mediaController: controller,
|
||||||
return Center(
|
controllerWidgetBuilder: (controller) => const SizedBox.shrink(),
|
||||||
child: AspectRatio(
|
statusWidgetBuilder: (context, controller, status) => const SizedBox.shrink(),
|
||||||
|
textureBuilder: (context, controller, info) {
|
||||||
|
var id = controller.textureId;
|
||||||
|
if (id == null) {
|
||||||
|
return AspectRatio(
|
||||||
aspectRatio: entry.displayAspectRatio,
|
aspectRatio: entry.displayAspectRatio,
|
||||||
child: VideoPlayer(widget.controller),
|
child: Container(
|
||||||
|
color: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onValueChange() {
|
Widget child = Container(
|
||||||
if (!value.isPlaying && value.position == value.duration) _goToStart();
|
color: Colors.blue,
|
||||||
setState(() {});
|
child: Texture(
|
||||||
|
textureId: id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!controller.autoRotate) {
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _goToStart() async {
|
final degree = entry.catalogMetadata?.videoRotation ?? 0;
|
||||||
await widget.controller.seekTo(Duration.zero);
|
if (degree != 0) {
|
||||||
await widget.controller.pause();
|
child = RotatedBox(
|
||||||
|
quarterTurns: degree ~/ 90,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child = AspectRatio(
|
||||||
|
aspectRatio: entry.displayAspectRatio,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
child: child,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: Colors.transparent,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Image(
|
||||||
|
image: UriImage(uri: entry.uri, mimeType: entry.mimeType),
|
||||||
|
width: entry.width.toDouble(),
|
||||||
|
height: entry.height.toDouble(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPlayFinish(IjkMediaController controller) => controller.seekTo(0);
|
||||||
}
|
}
|
||||||
|
|
72
pubspec.lock
72
pubspec.lock
|
@ -35,7 +35,7 @@ packages:
|
||||||
name: barcode
|
name: barcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.6.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -88,11 +88,9 @@ packages:
|
||||||
draggable_scrollbar:
|
draggable_scrollbar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "../flutter-draggable-scrollbar"
|
||||||
ref: HEAD
|
relative: true
|
||||||
resolved-ref: "3b823ae0a9def4edec62771f18e6348312bfce15"
|
source: path
|
||||||
url: "git://github.com/deckerst/flutter-draggable-scrollbar.git"
|
|
||||||
source: git
|
|
||||||
version: "0.0.4"
|
version: "0.0.4"
|
||||||
event_bus:
|
event_bus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
|
@ -104,26 +102,29 @@ packages:
|
||||||
expansion_tile_card:
|
expansion_tile_card:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "../expansion_tile_card"
|
||||||
ref: HEAD
|
relative: true
|
||||||
resolved-ref: edb6b11bb448fc2f30e566a20605b37093503176
|
source: path
|
||||||
url: "git://github.com/deckerst/expansion_tile_card.git"
|
|
||||||
source: git
|
|
||||||
version: "1.0.3"
|
version: "1.0.3"
|
||||||
flushbar:
|
flushbar:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: flushbar
|
||||||
ref: "13c55a8"
|
url: "https://pub.dartlang.org"
|
||||||
resolved-ref: "13c55a888c1693f1c8269ea30d55c614a1bfee16"
|
source: hosted
|
||||||
url: "https://github.com/AndreHaueisen/flushbar.git"
|
version: "1.10.0"
|
||||||
source: git
|
|
||||||
version: "1.9.1"
|
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_ijkplayer:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "../flutter_ijkplayer"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.3.6"
|
||||||
flutter_native_timezone:
|
flutter_native_timezone:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -144,7 +145,7 @@ packages:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.3+1"
|
version: "0.17.4"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -168,7 +169,7 @@ packages:
|
||||||
name: google_maps_flutter
|
name: google_maps_flutter
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.25+2"
|
version: "0.5.26"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -189,7 +190,7 @@ packages:
|
||||||
name: io
|
name: io
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3"
|
version: "0.3.4"
|
||||||
logger:
|
logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -266,7 +267,7 @@ packages:
|
||||||
name: pdf
|
name: pdf
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.0"
|
version: "1.6.1"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -280,14 +281,14 @@ packages:
|
||||||
name: percent_indicator
|
name: percent_indicator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1+1"
|
version: "2.1.3"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: permission_handler
|
name: permission_handler
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0+hotfix.2"
|
version: "5.0.0+hotfix.3"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -322,14 +323,14 @@ packages:
|
||||||
name: printing
|
name: printing
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.3.1"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.5"
|
||||||
qr:
|
qr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -489,27 +490,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.8"
|
||||||
video_player:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "../plugins/packages/video_player/video_player"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "0.10.8+2"
|
|
||||||
video_player_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player_platform_interface
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
video_player_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: video_player_web
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.2+1"
|
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
41
pubspec.yaml
41
pubspec.yaml
|
@ -13,6 +13,25 @@ description: A new Flutter application.
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
||||||
|
# - no URI handling by default but trivial by fork
|
||||||
|
# - support: AVI/XVID/MP3 nothing, MP2T nothing
|
||||||
|
# - cannot support more formats
|
||||||
|
# - playable only when both the video and audio streams are supported
|
||||||
|
|
||||||
|
# fijkplayer (as of v0.7.1, backed by IJKPlayer & ffmpeg):
|
||||||
|
# - URI handling
|
||||||
|
# - support: AVI/XVID/MP3 audio only, MP2T video only
|
||||||
|
# - possible support for more formats by customizing ffmpeg build,
|
||||||
|
# - playable when only the video or audio stream is supported
|
||||||
|
# - crash when calling `seekTo` for some files (e.g. TED talk videos)
|
||||||
|
|
||||||
|
# flutter_ijkplayer (as of v0.3.5+1, backed by IJKPlayer & ffmpeg):
|
||||||
|
# - URI handling (from v0.3.6, `DataSource.photoManagerUrl`)
|
||||||
|
# - support: AVI/XVID/MP3 video/audio, MP2T video only
|
||||||
|
# - possible support for more formats by TODO TLAD customizing ffmpeg build?
|
||||||
|
# - playable when only the video or audio stream is supported
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
@ -20,19 +39,19 @@ dependencies:
|
||||||
charts_flutter:
|
charts_flutter:
|
||||||
collection:
|
collection:
|
||||||
draggable_scrollbar:
|
draggable_scrollbar:
|
||||||
# path: ../flutter-draggable-scrollbar
|
path: ../flutter-draggable-scrollbar
|
||||||
git:
|
# git:
|
||||||
url: git://github.com/deckerst/flutter-draggable-scrollbar.git
|
# url: git://github.com/deckerst/flutter-draggable-scrollbar.git
|
||||||
event_bus:
|
event_bus:
|
||||||
expansion_tile_card:
|
expansion_tile_card:
|
||||||
# path: ../expansion_tile_card
|
path: ../expansion_tile_card
|
||||||
git:
|
# git:
|
||||||
url: git://github.com/deckerst/expansion_tile_card.git
|
# url: git://github.com/deckerst/expansion_tile_card.git
|
||||||
|
flutter_ijkplayer:
|
||||||
|
path: ../flutter_ijkplayer
|
||||||
|
# git:
|
||||||
|
# url: git://github.com/deckerst/flutter_ijkplayer.git
|
||||||
flushbar:
|
flushbar:
|
||||||
# flushbar-1.9.1 cannot be built with Flutter 1.15.17
|
|
||||||
git:
|
|
||||||
url: https://github.com/AndreHaueisen/flushbar.git
|
|
||||||
ref: 13c55a8
|
|
||||||
flutter_native_timezone:
|
flutter_native_timezone:
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
geocoder:
|
geocoder:
|
||||||
|
@ -55,8 +74,6 @@ dependencies:
|
||||||
transparent_image:
|
transparent_image:
|
||||||
tuple:
|
tuple:
|
||||||
uuid:
|
uuid:
|
||||||
video_player:
|
|
||||||
path: ../plugins/packages/video_player/video_player
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue