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:
Thibault Deckers 2020-04-20 08:36:44 +09:00
parent 19976940a0
commit e88568e706
7 changed files with 272 additions and 155 deletions

View file

@ -19,10 +19,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart';
class FullscreenBody extends StatefulWidget {
final CollectionLens collection;
@ -50,7 +50,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
Animation<Offset> _bottomOverlayOffset;
EdgeInsets _frozenViewInsets, _frozenViewPadding;
FullscreenActionDelegate _actionDelegate;
final List<Tuple2<String, VideoPlayerController>> _videoControllers = [];
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
CollectionLens get collection => widget.collection;
@ -114,6 +114,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
_overlayAnimationController.dispose();
_overlayVisible.removeListener(_onOverlayVisibleChange);
_videoControllers.forEach((kv) => kv.item2.dispose());
_videoControllers.clear();
_verticalPager.removeListener(_onVerticalPageControllerChange);
_unregisterWidget(widget);
super.dispose();
@ -330,7 +331,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
void _initVideoController() {
Future<void> _initVideoController() async {
if (_entry == null || !_entry.isVideo) return;
final uri = _entry.uri;
@ -338,9 +339,8 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
if (controllerEntry != null) {
_videoControllers.remove(controllerEntry);
} else {
// unsupported by video_player 0.10.8+2 (backed by ExoPlayer): AVI
final controller = VideoPlayerController.uri(uri)..initialize();
controllerEntry = Tuple2(uri, controller);
// do not set data source of IjkMediaController here
controllerEntry = Tuple2(uri, IjkMediaController());
}
_videoControllers.insert(0, controllerEntry);
while (_videoControllers.length > 3) {
@ -352,7 +352,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
class FullscreenVerticalPageView extends StatefulWidget {
final CollectionLens collection;
final ImageEntry entry;
final List<Tuple2<String, VideoPlayerController>> videoControllers;
final List<Tuple2<String, IjkMediaController>> videoControllers;
final PageController horizontalPager, verticalPager;
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
final VoidCallback onImageTap, onImagePageRequested;

View file

@ -1,10 +1,10 @@
import 'package:aves/model/collection_lens.dart';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/fullscreen/image_view.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart';
class MultiImagePage extends StatefulWidget {
final CollectionLens collection;
@ -12,7 +12,7 @@ class MultiImagePage extends StatefulWidget {
final ValueChanged<int> onPageChanged;
final ValueChanged<PhotoViewScaleState> onScaleChanged;
final VoidCallback onTap;
final List<Tuple2<String, VideoPlayerController>> videoControllers;
final List<Tuple2<String, IjkMediaController>> videoControllers;
const MultiImagePage({
this.collection,
@ -65,7 +65,7 @@ class SingleImagePage extends StatefulWidget {
final ImageEntry entry;
final ValueChanged<PhotoViewScaleState> onScaleChanged;
final VoidCallback onTap;
final List<Tuple2<String, VideoPlayerController>> videoControllers;
final List<Tuple2<String, IjkMediaController>> videoControllers;
const SingleImagePage({
this.entry,

View file

@ -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_picture_provider.dart';
import 'package:aves/widgets/fullscreen/video_view.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_view/photo_view.dart';
import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart';
class ImageView extends StatelessWidget {
final ImageEntry entry;
final Object heroTag;
final ValueChanged<PhotoViewScaleState> onScaleChanged;
final VoidCallback onTap;
final List<Tuple2<String, VideoPlayerController>> videoControllers;
final List<Tuple2<String, IjkMediaController>> videoControllers;
const ImageView({
this.entry,

View file

@ -1,18 +1,20 @@
import 'dart:async';
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/fx/blurred.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:outline_material_icons/outline_material_icons.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart';
class VideoControlOverlay extends StatefulWidget {
final ImageEntry entry;
final Animation<double> scale;
final VideoPlayerController controller;
final IjkMediaController controller;
final EdgeInsets viewInsets, viewPadding;
const VideoControlOverlay({
@ -32,16 +34,28 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
final GlobalKey _progressBarKey = GlobalKey();
bool _playingOnDragStart = false;
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;
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
void initState() {
@ -51,7 +65,6 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
vsync: this,
);
_registerWidget(widget);
_onValueChange();
}
@override
@ -69,11 +82,17 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
}
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) {
widget.controller.removeListener(_onValueChange);
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
_stopTimer();
}
@override
@ -93,37 +112,43 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
padding: safePadding,
child: SizedBox(
width: mqWidth - safePadding.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: value.hasError
? [
OverlayButton(
scale: scale,
child: IconButton(
icon: Icon(OMIcons.openInNew),
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype),
tooltip: 'Open',
),
),
]
: [
Expanded(
child: _buildProgressBar(),
),
const SizedBox(width: 8),
OverlayButton(
scale: scale,
child: IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _playPauseAnimation,
),
onPressed: _playPause,
tooltip: value.isPlaying ? 'Pause' : 'Play',
),
),
],
),
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,
children: status == IjkStatus.error
? [
OverlayButton(
scale: scale,
child: IconButton(
icon: Icon(OMIcons.openInNew),
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype),
tooltip: 'Open',
),
),
]
: [
Expanded(
child: _buildProgressBar(),
),
const SizedBox(width: 8),
OverlayButton(
scale: scale,
child: IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _playPauseAnimation,
),
onPressed: _playPause,
tooltip: isPlaying ? 'Pause' : 'Play',
),
),
],
);
}),
),
);
},
@ -138,14 +163,14 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
borderRadius: progressBarBorderRadius,
child: GestureDetector(
onTapDown: (TapDownDetails details) {
_seek(details.globalPosition);
_seekFromTap(details.globalPosition);
},
onHorizontalDragStart: (DragStartDetails details) {
_playingOnDragStart = controller.value.isPlaying;
_playingOnDragStart = isPlaying;
if (_playingOnDragStart) controller.pause();
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
_seek(details.globalPosition);
_seekFromTap(details.globalPosition);
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_playingOnDragStart) controller.play();
@ -164,12 +189,25 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
children: [
Row(
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(),
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() {
setState(() {});
updatePlayPauseIcon();
void _startTimer() {
if (controller.textureId == null) return;
_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 {
if (value.isPlaying) {
if (isPlaying) {
await controller.pause();
} else {
if (!value.initialized) await controller.initialize();
} else if (isInitialized) {
await controller.play();
} else {
await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true);
}
setState(() {});
}
void updatePlayPauseIcon() {
final isPlaying = value.isPlaying;
void _updatePlayPauseIcon() {
final status = _playPauseAnimation.status;
if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) {
_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 RenderBox box = keyContext.findRenderObject();
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;
}
}
}

View file

@ -1,13 +1,14 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
class AvesVideo extends StatefulWidget {
final ImageEntry entry;
final VideoPlayerController controller;
final IjkMediaController controller;
const AvesVideo({
Key key,
@ -20,15 +21,16 @@ class AvesVideo extends StatefulWidget {
}
class AvesVideoState extends State<AvesVideo> {
final List<StreamSubscription> _subscriptions = [];
ImageEntry get entry => widget.entry;
VideoPlayerValue get value => widget.controller.value;
IjkMediaController get controller => widget.controller;
@override
void initState() {
super.initState();
_registerWidget(widget);
_onValueChange();
}
@override
@ -45,38 +47,78 @@ class AvesVideoState extends State<AvesVideo> {
}
void _registerWidget(AvesVideo widget) {
widget.controller.addListener(_onValueChange);
_subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish));
}
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
Widget build(BuildContext context) {
if (value == null) return const SizedBox();
if (value.hasError) {
return Image(
image: UriImage(uri: entry.uri, mimeType: entry.mimeType),
width: entry.width.toDouble(),
height: entry.height.toDouble(),
);
}
return Center(
child: AspectRatio(
aspectRatio: entry.displayAspectRatio,
child: VideoPlayer(widget.controller),
),
);
if (controller == null) return const SizedBox();
return StreamBuilder<IjkStatus>(
stream: widget.controller.ijkStatusStream,
builder: (context, snapshot) {
final status = snapshot.data;
return isPlayable(status)
? IjkPlayer(
mediaController: controller,
controllerWidgetBuilder: (controller) => const SizedBox.shrink(),
statusWidgetBuilder: (context, controller, status) => const SizedBox.shrink(),
textureBuilder: (context, controller, info) {
var id = controller.textureId;
if (id == null) {
return AspectRatio(
aspectRatio: entry.displayAspectRatio,
child: Container(
color: Colors.green,
),
);
}
Widget child = Container(
color: Colors.blue,
child: Texture(
textureId: id,
),
);
if (!controller.autoRotate) {
return child;
}
final degree = entry.catalogMetadata?.videoRotation ?? 0;
if (degree != 0) {
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 _onValueChange() {
if (!value.isPlaying && value.position == value.duration) _goToStart();
setState(() {});
}
Future<void> _goToStart() async {
await widget.controller.seekTo(Duration.zero);
await widget.controller.pause();
}
void _onPlayFinish(IjkMediaController controller) => controller.seekTo(0);
}

View file

@ -35,7 +35,7 @@ packages:
name: barcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
version: "1.6.0"
boolean_selector:
dependency: transitive
description:
@ -88,11 +88,9 @@ packages:
draggable_scrollbar:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "3b823ae0a9def4edec62771f18e6348312bfce15"
url: "git://github.com/deckerst/flutter-draggable-scrollbar.git"
source: git
path: "../flutter-draggable-scrollbar"
relative: true
source: path
version: "0.0.4"
event_bus:
dependency: "direct main"
@ -104,26 +102,29 @@ packages:
expansion_tile_card:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: edb6b11bb448fc2f30e566a20605b37093503176
url: "git://github.com/deckerst/expansion_tile_card.git"
source: git
path: "../expansion_tile_card"
relative: true
source: path
version: "1.0.3"
flushbar:
dependency: "direct main"
description:
path: "."
ref: "13c55a8"
resolved-ref: "13c55a888c1693f1c8269ea30d55c614a1bfee16"
url: "https://github.com/AndreHaueisen/flushbar.git"
source: git
version: "1.9.1"
name: flushbar
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_ijkplayer:
dependency: "direct main"
description:
path: "../flutter_ijkplayer"
relative: true
source: path
version: "0.3.6"
flutter_native_timezone:
dependency: "direct main"
description:
@ -144,7 +145,7 @@ packages:
name: flutter_svg
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.3+1"
version: "0.17.4"
flutter_test:
dependency: "direct dev"
description: flutter
@ -168,7 +169,7 @@ packages:
name: google_maps_flutter
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.25+2"
version: "0.5.26"
image:
dependency: transitive
description:
@ -189,7 +190,7 @@ packages:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
version: "0.3.4"
logger:
dependency: "direct main"
description:
@ -266,7 +267,7 @@ packages:
name: pdf
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
version: "1.6.1"
pedantic:
dependency: "direct main"
description:
@ -280,14 +281,14 @@ packages:
name: percent_indicator
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1+1"
version: "2.1.3"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0+hotfix.2"
version: "5.0.0+hotfix.3"
permission_handler_platform_interface:
dependency: transitive
description:
@ -322,14 +323,14 @@ packages:
name: printing
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.1"
version: "3.3.1"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.4"
version: "4.0.5"
qr:
dependency: transitive
description:
@ -489,27 +490,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:

View file

@ -13,6 +13,25 @@ description: A new Flutter application.
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
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:
flutter:
sdk: flutter
@ -20,19 +39,19 @@ dependencies:
charts_flutter:
collection:
draggable_scrollbar:
# path: ../flutter-draggable-scrollbar
git:
url: git://github.com/deckerst/flutter-draggable-scrollbar.git
path: ../flutter-draggable-scrollbar
# git:
# url: git://github.com/deckerst/flutter-draggable-scrollbar.git
event_bus:
expansion_tile_card:
# path: ../expansion_tile_card
git:
url: git://github.com/deckerst/expansion_tile_card.git
path: ../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-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_svg:
geocoder:
@ -55,8 +74,6 @@ dependencies:
transparent_image:
tuple:
uuid:
video_player:
path: ../plugins/packages/video_player/video_player
dev_dependencies:
flutter_test: