Browse Source

Merge pull request #3 from CaiJingLong/ios-framework

update ios framework
Caijinglong 6 năm trước cách đây
mục cha
commit
c99a5b1af3

+ 17 - 0
README-EN.md

@@ -127,6 +127,16 @@ IjkMediaController controller = IjkMediaController();
   );
 ```
 
+### about dispose
+
+Users need to call `dispose` method to release resources when they decide that they will no longer use controllers. If they do not call `dispose` method, the resources will not be released.
+
+Since a `controller` may be attached by multiple `IjkPlayers`, leading to a `controller` controlling multiple `IjkPlayers`, it is not in principle possible to agree with `dispose`of `IjkPlayer`, so the caller needs to dispose of itself here.
+
+```dart
+controller.dispose();
+```
+
 ### Usage of controller
 
 #### DataSource
@@ -141,6 +151,13 @@ await controller.setAssetDataSource("assets/test.mp4");
 // file
 await controller.setFileDataSource(File("/sdcard/1.mp4"));
 
+// dataSource
+var dataSource = DataSource.file(File("/sdcard/1.mp4"));
+var dataSource = DataSource.network("https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4");
+var dataSource = DataSource.asset("assets/test.mp4");
+await controller.setDataSource(dataSource);
+
+
 // autoplay param
 await controller.setNetworkDataSource("https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4",autoPlay : true);
 

+ 16 - 0
README.md

@@ -140,6 +140,16 @@ IjkMediaController controller = IjkMediaController();
   );
 ```
 
+### 关于销毁
+
+用户在确定不再使用 controller 时,需要自己调用 dispose 方法以释放资源,如果不调用,则会造成资源无法释放
+
+因为一个`controller`可能被多个`IjkPlayer`附着, 导致一个`controller`同时控制多个`IjkPlayer`,所以原则上不能与`IjkPlayer`的`dispose`达成一致,所以这里需要调用者自行 dispose
+
+```dart
+controller.dispose();
+```
+
 ### 控制器的使用
 
 #### 设置资源
@@ -156,6 +166,12 @@ await controller.setAssetDataSource("assets/test.mp4");
 // 文件
 await controller.setFileDataSource(File("/sdcard/1.mp4"));
 
+// 通过数据源的方式
+var dataSource = DataSource.file(File("/sdcard/1.mp4"));
+var dataSource = DataSource.network("https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4");
+var dataSource = DataSource.asset("assets/test.mp4");
+await controller.setDataSource(dataSource);
+
 // 还可以添加autoplay参数,这样会在资源准备完成后自动播放
 await controller.setNetworkDataSource("https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4",autoPlay : true);
 

+ 1 - 1
TODOLIST.md

@@ -38,4 +38,4 @@
   - [x] 播放相册
   - [x] 播放 asset
   - [ ] 设置选项的使用
-  - [ ] 全屏播放的示例代码
+  - [ ] 切换全屏播放的示例代码

+ 8 - 8
example/ios/Podfile.lock

@@ -2,15 +2,15 @@ PODS:
   - Flutter (1.0.0)
   - flutter_ijkplayer (0.0.1):
     - Flutter
-    - FlutterIJK (~> 0.0.6)
-  - FlutterIJK (0.0.6)
+    - FlutterIJK (~> 0.0.7)
+  - FlutterIJK (0.0.7)
   - photo_manager (0.0.1):
     - Flutter
 
 DEPENDENCIES:
   - Flutter (from `.symlinks/flutter/ios`)
-  - flutter_ijkplayer (from `/Users/cai/Documents/GitHub/flutter_ijkplayer/ios/flutter_ijkplayer.podspec`)
-  - photo_manager (from `/Users/cai/.pub-cache/hosted/pub.flutter-io.cn/photo_manager-0.3.3/ios/photo_manager.podspec`)
+  - flutter_ijkplayer (from `/Users/caijinglong/Documents/GitHub/flutter_ijkplayer/ios/flutter_ijkplayer.podspec`)
+  - photo_manager (from `/Users/caijinglong/.pub-cache/hosted/pub.flutter-io.cn/photo_manager-0.3.3/ios/photo_manager.podspec`)
 
 SPEC REPOS:
   https://github.com/cocoapods/specs.git:
@@ -20,14 +20,14 @@ EXTERNAL SOURCES:
   Flutter:
     :path: ".symlinks/flutter/ios"
   flutter_ijkplayer:
-    :path: "/Users/cai/Documents/GitHub/flutter_ijkplayer/ios/flutter_ijkplayer.podspec"
+    :path: "/Users/caijinglong/Documents/GitHub/flutter_ijkplayer/ios/flutter_ijkplayer.podspec"
   photo_manager:
-    :path: "/Users/cai/.pub-cache/hosted/pub.flutter-io.cn/photo_manager-0.3.3/ios/photo_manager.podspec"
+    :path: "/Users/caijinglong/.pub-cache/hosted/pub.flutter-io.cn/photo_manager-0.3.3/ios/photo_manager.podspec"
 
 SPEC CHECKSUMS:
   Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a
-  flutter_ijkplayer: 6e17431ef86df3ab4c9e8acf91a34d174a20d3db
-  FlutterIJK: fef71db3a6d8d26b32e793cd9c2cf30b3ca56b4d
+  flutter_ijkplayer: 7973985ab7087e6716f1ac753387e9694bbb6609
+  FlutterIJK: 70be1c85cbf7eeefc849702ac2f794f4e631d526
   photo_manager: d47ddf6cb25cbfa837dc334540eb9a99b208e191
 
 PODFILE CHECKSUM: a14c63eb48a9d676ed6d876506b3258fd9c85235

+ 67 - 0
example/lib/page/full_screen.dart

@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
+
+class FullScreen extends StatefulWidget {
+  @override
+  _FullScreenState createState() => _FullScreenState();
+}
+
+class _FullScreenState extends State<FullScreen> {
+  var controller = IjkMediaController();
+
+  Orientation get orientation => MediaQuery.of(context).orientation;
+  DataSource source = DataSource.network(
+    "https://www.sample-videos.com/video123/mp4/360/big_buck_bunny_360p_30mb.mp4",
+  );
+
+  @override
+  void initState() {
+    super.initState();
+    controller.setDataSource(source, autoPlay: true);
+  }
+
+  @override
+  void dispose() {
+    controller?.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (orientation == Orientation.landscape) {
+      return _buildFullScreenPlayer();
+    }
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("切换横竖屏可以看到界面变化"),
+      ),
+      body: ListView(
+        children: <Widget>[
+          _buildPlayerItem(),
+        ],
+      ),
+    );
+  }
+
+  _buildPlayerItem() {
+    return Container(
+      height: 200,
+      child: IjkPlayer(
+        mediaController: controller,
+      ),
+    );
+  }
+
+  _buildFullScreenPlayer() {
+    var data = MediaQuery.of(context);
+    return Material(
+      child: Container(
+        width: data.size.width,
+        height: data.size.height,
+        child: IjkPlayer(
+          mediaController: controller,
+        ),
+      ),
+    );
+  }
+}

+ 4 - 0
example/lib/page/index.dart

@@ -1,7 +1,9 @@
 import 'package:flutter/material.dart';
 import 'package:ijkplayer_example/page/asset_page.dart';
+import 'package:ijkplayer_example/page/full_screen.dart';
 import 'package:ijkplayer_example/page/gallery_page.dart';
 import 'package:ijkplayer_example/page/network.dart';
+import 'package:ijkplayer_example/page/video_list.dart';
 
 class IndexPage extends StatefulWidget {
   @override
@@ -20,6 +22,8 @@ class _IndexPageState extends State<IndexPage> {
           buildButton("播放网络视频", NetworkPage()),
           buildButton("播放相册视频", PlayGalleryPage()),
           buildButton("播放应用asset", AssetPage()),
+          buildButton("ListView中插入视频", VideoList()),
+          buildButton("全屏切换示例", FullScreen()),
         ],
       ),
     );

+ 150 - 0
example/lib/page/video_list.dart

@@ -0,0 +1,150 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
+
+class VideoList extends StatefulWidget {
+  @override
+  _VideoListState createState() => _VideoListState();
+}
+
+class _VideoListState extends State<VideoList> {
+  var list = <DataSource>[
+    DataSource.network(
+        "https://www.sample-videos.com/video123/mp4/360/big_buck_bunny_360p_30mb.mp4"),
+    DataSource.network(
+        "https://www.sample-videos.com/video123/mp4/360/big_buck_bunny_360p_30mb.mp4"),
+    DataSource.asset("assets/sample1.mp4")
+  ];
+
+  var map = <DataSource, IjkMediaController>{};
+
+  @override
+  void initState() {
+    super.initState();
+    for (var data in list) {
+      var controller = IjkMediaController();
+      map[data] = controller;
+//      controller.setDataSource(data);
+    }
+  }
+
+  @override
+  void dispose() {
+    map.values.forEach((c) {
+      c.dispose();
+    });
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text("列表中IJKPlayer"),
+      ),
+      body: ListView.separated(
+        itemBuilder: _buildItem,
+        separatorBuilder: (BuildContext context, int index) {
+          return Container(
+            height: 10.0,
+            color: Colors.blue,
+          );
+        },
+        itemCount: list.length,
+      ),
+    );
+  }
+
+  Widget _buildItem(BuildContext context, int index) {
+    return Container(
+      child: VideoItem(list[index]),
+      height: 150,
+    );
+  }
+}
+
+class VideoItem extends StatefulWidget {
+  final DataSource dataSource;
+
+  VideoItem(this.dataSource);
+
+  @override
+  _VideoItemState createState() => _VideoItemState();
+}
+
+class _VideoItemState extends State<VideoItem> {
+  IjkMediaController controller;
+
+  var isInit = false;
+  var isLoading = false;
+
+  @override
+  void initState() {
+    super.initState();
+    controller = IjkMediaController();
+  }
+
+  @override
+  void dispose() {
+    controller?.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (!isInit) {
+      Widget stateButton = IconButton(
+        icon: Icon(Icons.play_arrow),
+        onPressed: _play,
+        color: Colors.white,
+      );
+
+      if (isLoading) {
+        stateButton = CircularProgressIndicator();
+      }
+
+      var center = Center(
+        child: Container(
+          width: 60.0,
+          height: 60.0,
+          child: stateButton,
+        ),
+      );
+
+      return Stack(
+        children: <Widget>[
+          Container(
+            color: Colors.black,
+          ),
+          center,
+        ],
+      );
+    }
+
+    var ijkPlayer = IjkPlayer(
+      mediaController: controller,
+      controllerWidgetBuilder: _buildControllerWidget,
+    );
+    return ijkPlayer;
+  }
+
+  void _play() async {
+    setState(() {
+      isLoading = true;
+    });
+    await controller.setDataSource(widget.dataSource, autoPlay: true);
+    await controller.pauseOtherIjkMedia();
+    setState(() {
+      isLoading = false;
+      isInit = true;
+    });
+  }
+
+  Widget _buildControllerWidget(IjkMediaController controller) {
+    return DefaultControllerWidget(
+      controller: controller,
+      verticalGesture: false,
+    );
+  }
+}
+
+class PauseOtherNotification extends Notification {}

+ 4 - 1
ios/flutter_ijkplayer.podspec

@@ -18,7 +18,10 @@ A new flutter plugin project.
 
   s.ios.deployment_target = '8.0'
 
-  s.dependency 'FlutterIJK', '~> 0.0.6'
+  # s.ios.vendored_frameworks = 'IJKMediaFramework.framework'
+  # s.frameworks  = "AudioToolbox", "AVFoundation", "CoreGraphics", "CoreMedia", "CoreVideo", "MobileCoreServices", "OpenGLES", "QuartzCore", "VideoToolbox", "Foundation", "UIKit", "MediaPlayer"
+  # s.libraries   = "bz2", "z", "stdc++"
+  s.dependency 'FlutterIJK', '~> 0.0.7'
 
 end
 

+ 86 - 4
lib/src/controller.dart

@@ -5,7 +5,9 @@ class IjkMediaController {
   /// MediaController
   IjkMediaController({
     this.autoRotate = true,
-  });
+  }) {
+    IjkMediaPlayerManager().add(this);
+  }
 
   /// texture id from native
   int _textureId;
@@ -113,6 +115,8 @@ class IjkMediaController {
     _videoInfoController = null;
     _textureIdController = null;
     _volumeController = null;
+
+    IjkMediaPlayerManager().remove(this);
   }
 
   /// dispose all resource
@@ -146,6 +150,29 @@ class IjkMediaController {
     }, autoPlay);
   }
 
+  /// Set datasource with [DataSource]
+  Future<void> setDataSource(
+    DataSource source, {
+    bool autoPlay = false,
+  }) async {
+    switch (source.type) {
+      case DataSourceType.asset:
+        await setAssetDataSource(
+          source._assetName,
+          package: source._assetPackage,
+          autoPlay: autoPlay,
+        );
+        break;
+      case DataSourceType.file:
+        await setFileDataSource(source._file, autoPlay: autoPlay);
+        break;
+      case DataSourceType.network:
+        await setNetworkDataSource(source._netWorkUrl, autoPlay: autoPlay);
+        break;
+      default:
+    }
+  }
+
   /// set file DataSource
   Future<void> setFileDataSource(
     File file, {
@@ -172,13 +199,15 @@ class IjkMediaController {
   }
 
   /// Play or pause according to your current status
-  Future<void> playOrPause() async {
+  Future<void> playOrPause({
+    pauseOther = false,
+  }) async {
     var videoInfo = await getVideoInfo();
     var playing = videoInfo.isPlaying;
     if (playing) {
       await _plugin?.pause();
     } else {
-      await _plugin?.play();
+      await _plugin?.play(pauseOther: pauseOther);
     }
     refreshVideoInfo();
   }
@@ -229,6 +258,8 @@ class IjkMediaController {
   void _autoPlay(bool autoPlay) {
     if (autoPlay) {
       eventChannel?.autoPlay(this);
+    } else {
+      eventChannel?.disableAutoPlay(this);
     }
   }
 
@@ -255,6 +286,10 @@ class IjkMediaController {
   Future<void> setSystemVolume(int volume) async {
     await IjkManager.setSystemVolume(volume);
   }
+
+  Future<void> pauseOtherIjkMedia() async {
+    await IjkMediaPlayerManager().pauseOther(this);
+  }
 }
 
 /// about channel
@@ -277,7 +312,7 @@ class _IjkPlugin {
     await _globalChannel.invokeMethod("dispose", {"id": textureId});
   }
 
-  Future<void> play() async {
+  Future<void> play({bool pauseOther = false}) async {
     await channel.invokeMethod("play");
   }
 
@@ -336,3 +371,50 @@ class _IjkPlugin {
     });
   }
 }
+
+/// Entity classe for data sources.
+class DataSource {
+  /// See [DataSourceType]
+  DataSourceType type;
+
+  File _file;
+
+  String _assetName;
+
+  String _assetPackage;
+
+  String _netWorkUrl;
+
+  DataSource._();
+
+  /// Create file data source
+  factory DataSource.file(File file) {
+    var ds = DataSource._();
+    ds._file = file;
+    ds.type = DataSourceType.file;
+    return ds;
+  }
+
+  /// Create network data source
+  factory DataSource.network(String url) {
+    var ds = DataSource._();
+    ds._netWorkUrl = url;
+    ds.type = DataSourceType.network;
+    return ds;
+  }
+
+  /// Create asset data source
+  factory DataSource.asset(String assetName, {String package}) {
+    var ds = DataSource._();
+    ds._assetName = assetName;
+    ds._assetPackage = package;
+    ds.type = DataSourceType.asset;
+    return ds;
+  }
+}
+
+enum DataSourceType {
+  network,
+  file,
+  asset,
+}

+ 39 - 0
lib/src/engine/ijk_controller_manager.dart

@@ -0,0 +1,39 @@
+import 'package:flutter_ijkplayer/src/ijkplayer.dart';
+
+class IjkMediaPlayerManager {
+  final ijkPlayerList = <IjkMediaController>[];
+
+  static IjkMediaPlayerManager _instance;
+
+  factory IjkMediaPlayerManager() {
+    _instance ??= IjkMediaPlayerManager._();
+    return _instance;
+  }
+
+  IjkMediaPlayerManager._();
+
+  void add(IjkMediaController ijkMediaController) {
+    ijkPlayerList.add(ijkMediaController);
+  }
+
+  void remove(IjkMediaController ijkMediaController) {
+    ijkPlayerList.remove(ijkMediaController);
+  }
+
+  Future<void> pauseOther(IjkMediaController ijkMediaController) async {
+    await todoOther(ijkMediaController, (ctl) {
+      ctl.pause();
+    });
+  }
+
+  Future<void> todoOther(
+    IjkMediaController ijkMediaController,
+    Future<void> todo(IjkMediaController ijkMediaController),
+  ) async {
+    for (var item in ijkPlayerList) {
+      if (item != ijkMediaController) {
+        await todo(item);
+      }
+    }
+  }
+}

+ 23 - 0
lib/src/helper/time_helper.dart

@@ -0,0 +1,23 @@
+class TimeHelper {
+  static String getTimeText(double seconds) {
+    var duration = Duration(milliseconds: (seconds * 1000).toInt());
+
+    String twoDigits(int n) {
+      if (n >= 10) return "$n";
+      return "0$n";
+    }
+
+    String twoDigitMinutes =
+        twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour));
+    String twoDigitSeconds =
+        twoDigits(duration.inSeconds.remainder(Duration.secondsPerMinute));
+
+    var str = "$twoDigitMinutes:$twoDigitSeconds";
+
+    if (duration.inHours == 0) {
+      return str;
+    }
+    var hour = duration.inHours.toString().padLeft(2, "0");
+    return "$hour:$str";
+  }
+}

+ 10 - 1
lib/src/ijk_event_channel.dart

@@ -72,7 +72,7 @@ class _IJKEventChannel {
     return _prepareCompleter.future;
   }
 
-  void autoPlay(IjkMediaController ijkMediaController) async {
+  autoPlay(IjkMediaController ijkMediaController) async {
     try {
       await waitPrepare();
       ijkMediaController.play();
@@ -81,6 +81,15 @@ class _IJKEventChannel {
     }
   }
 
+  disableAutoPlay(IjkMediaController ijkMediaController) async {
+    try {
+      await waitPrepare();
+      ijkMediaController.pause();
+    } catch (e) {
+      LogUtils.log(e);
+    }
+  }
+
   void onRotateChanged(MethodCall call) {
     var info = getInfo(call);
     LogUtils.log("onRotateChanged , info = $info");

+ 10 - 1
lib/src/ijkplayer.dart

@@ -9,6 +9,7 @@ import 'logutil.dart';
 import 'video_info.dart';
 import 'widget/controller_widget_builder.dart';
 import 'widget/ijkplayer_builder.dart';
+import 'engine/ijk_controller_manager.dart';
 
 part 'controller.dart';
 
@@ -19,7 +20,11 @@ part 'manager.dart';
 /// Main Classes of Library
 class IjkPlayer extends StatefulWidget {
   final IjkMediaController mediaController;
+
+  /// See [DefaultControllerWidget]
   final ControllerWidgetBuilder controllerWidgetBuilder;
+
+  /// See [buildDefaultIjkPlayer]
   final PlayerBuilder playerBuilder;
 
   /// Main Classes of Library
@@ -79,7 +84,11 @@ class IjkPlayerState extends State<IjkPlayer> {
         controllerWidget,
       ],
     );
-    return stack;
+//    return stack;
+    return Material(
+      child: stack,
+      color: Colors.black,
+    );
   }
 
   Widget _buildTexture(int id, VideoInfo info) {

+ 91 - 30
lib/src/widget/controller_widget_builder.dart

@@ -1,7 +1,9 @@
 import 'dart:async';
 
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
+import 'package:flutter_ijkplayer/src/helper/time_helper.dart';
 import 'package:flutter_ijkplayer/src/logutil.dart';
 import 'package:flutter_ijkplayer/src/widget/progress_bar.dart';
 
@@ -49,7 +51,8 @@ class DefaultControllerWidget extends StatefulWidget {
       _DefaultControllerWidgetState();
 }
 
-class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
+class _DefaultControllerWidgetState extends State<DefaultControllerWidget>
+    implements TooltipDelegate {
   IjkMediaController get controller => widget.controller;
 
   bool _isShow = true;
@@ -148,12 +151,13 @@ class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
     return PortraitController(
       controller: controller,
       info: info,
+      tooltipDelegate: this,
     );
   }
 
   OverlayEntry _tipOverlay;
 
-  Widget createTipWidgetWrapper(Widget widget) {
+  Widget createTooltipWidgetWrapper(Widget widget) {
     var typography = Typography(platform: TargetPlatform.android);
     var theme = typography.white;
     const style = const TextStyle(
@@ -176,8 +180,8 @@ class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
     );
   }
 
-  void showTipWidget(Widget widget) {
-    hideTipWidget();
+  void showTooltip(Widget widget) {
+    hideTooltip();
     _tipOverlay = OverlayEntry(
       builder: (BuildContext context) {
         return IgnorePointer(
@@ -190,7 +194,7 @@ class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
     Overlay.of(context).insert(_tipOverlay);
   }
 
-  void hideTipWidget() {
+  void hideTooltip() {
     _tipOverlay?.remove();
     _tipOverlay = null;
   }
@@ -244,11 +248,11 @@ class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
       ],
     );
 
-    showTipWidget(createTipWidgetWrapper(w));
+    showTooltip(createTooltipWidgetWrapper(w));
   }
 
   void _onHorizontalDragEnd(DragEndDetails details) async {
-    hideTipWidget();
+    hideTooltip();
     var targetSeek = _calculator.getTargetSeek(details);
     _calculator = null;
     await controller.seekTo(targetSeek);
@@ -282,11 +286,11 @@ class _DefaultControllerWidgetState extends State<DefaultControllerWidget> {
       ],
     );
 
-    showTipWidget(createTipWidgetWrapper(column));
+    showTooltip(createTooltipWidgetWrapper(column));
   }
 
   void _onVerticalDragEnd(DragEndDetails details) {
-    hideTipWidget();
+    hideTooltip();
   }
 
   Future<int> getVolume() async {
@@ -356,31 +360,28 @@ class _ProgressCalculator {
   }
 }
 
-String _getTimeText(double durationSecond) {
-  var duration = Duration(milliseconds: ((durationSecond ?? 0) * 1000).toInt());
-  var minute = (duration.inMinutes % 60).toString().padLeft(2, "0");
-  var second = (duration.inSeconds % 60).toString().padLeft(2, "0");
-  var text = "$minute:$second";
-//  LogUtils.log("$durationSecond = $text");
-  return text;
-}
-
 class PortraitController extends StatelessWidget {
   final IjkMediaController controller;
   final VideoInfo info;
+  final TooltipDelegate tooltipDelegate;
 
   const PortraitController({
     Key key,
     this.controller,
     this.info,
+    this.tooltipDelegate,
   }) : super(key: key);
 
+  bool get haveTime {
+    return info.hasData && info.duration > 0;
+  }
+
   @override
   Widget build(BuildContext context) {
     if (!info.hasData) {
       return Container();
     }
-    Widget bottomBar = buildBottomBar();
+    Widget bottomBar = buildBottomBar(context);
     return Column(
       children: <Widget>[
         Expanded(
@@ -391,16 +392,12 @@ class PortraitController extends StatelessWidget {
     );
   }
 
-  Widget buildBottomBar() {
-    var currentTime = Text(
-      _getTimeText(info.currentPosition),
-    );
-    var maxTime = Text(
-      _getTimeText(info.duration),
-    );
+  Widget buildBottomBar(BuildContext context) {
+    var currentTime = buildCurrentText();
+    var maxTime = buildMaxTimeText();
     var progress = buildProgress(info);
 
-    var playButton = buildPlayButton();
+    var playButton = buildPlayButton(context);
 
     Widget widget = Row(
       children: <Widget>[
@@ -430,7 +427,7 @@ class PortraitController extends StatelessWidget {
   }
 
   Widget buildProgress(VideoInfo info) {
-    if (info.duration == 0) {
+    if (!info.hasData || info.duration == 0) {
       return Container();
     }
     return Container(
@@ -440,13 +437,32 @@ class PortraitController extends StatelessWidget {
         max: info.duration,
         changeProgressHandler: (progress) async {
           await controller.seekToProgress(progress);
+          tooltipDelegate?.hideTooltip();
+        },
+        tapProgressHandler: (progress) {
+          showProgressTooltip(info, progress);
         },
-        tapProgressHandler: (progress) {},
       ),
     );
   }
 
-  buildPlayButton() {
+  buildCurrentText() {
+    return haveTime
+        ? Text(
+            TimeHelper.getTimeText(info.currentPosition),
+          )
+        : Container();
+  }
+
+  buildMaxTimeText() {
+    return haveTime
+        ? Text(
+            TimeHelper.getTimeText(info.duration),
+          )
+        : Container();
+  }
+
+  buildPlayButton(BuildContext context) {
     return IconButton(
       onPressed: () {
         controller.playOrPause();
@@ -456,6 +472,51 @@ class PortraitController extends StatelessWidget {
       iconSize: 25.0,
     );
   }
+
+  void showProgressTooltip(VideoInfo info, double progress) {
+    var target = info.duration * progress;
+
+    var diff = info.currentPosition - target;
+
+    String diffString;
+    if (diff < 1 && diff > -1) {
+      diffString = "0s";
+    } else if (diff < 0) {
+      diffString = "+${TimeHelper.getTimeText(diff.abs())}";
+    } else if (diff > 0) {
+      diffString = "-${TimeHelper.getTimeText(diff.abs())}";
+    } else {
+      diffString = "0s";
+    }
+
+    Widget text = Container(
+      alignment: Alignment.center,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: <Widget>[
+          Text(
+            TimeHelper.getTimeText(target),
+            style: TextStyle(fontSize: 20),
+          ),
+          Container(
+            height: 10,
+          ),
+          Text(diffString),
+        ],
+      ),
+    );
+
+    var tooltip = tooltipDelegate?.createTooltipWidgetWrapper(text);
+    tooltipDelegate?.showTooltip(tooltip);
+  }
+}
+
+abstract class TooltipDelegate {
+  void showTooltip(Widget widget);
+
+  Widget createTooltipWidgetWrapper(Widget widget);
+
+  void hideTooltip();
 }
 
 enum VolumeType {

+ 21 - 1
lib/src/widget/progress_bar.dart

@@ -36,12 +36,21 @@ class ProgressBar extends StatefulWidget {
 class _ProgressBarState extends State<ProgressBar> {
   GlobalKey _progressKey = GlobalKey();
 
+  double tempLeft;
+
+  double get left {
+    var l = widget.current / widget.max;
+    if (tempLeft != null) {
+      return tempLeft;
+    }
+    return l;
+  }
+
   @override
   Widget build(BuildContext context) {
     if (widget.max == null || widget.current == null || widget.max == 0)
       return _buildEmpty();
 
-    var left = widget.current / widget.max;
     var mid = (widget.buffered ?? 0) / widget.max - left;
     if (mid < 0) {
       mid = 0;
@@ -58,6 +67,7 @@ class _ProgressBarState extends State<ProgressBar> {
         behavior: HitTestBehavior.translucent,
         onPanUpdate: _onPanUpdate,
         onHorizontalDragUpdate: _onHorizontalDragUpdate,
+        onHorizontalDragEnd: _onHorizontalDragEnd,
         onTapDown: _onTapDown,
         onTapUp: _onTapUp,
       );
@@ -126,6 +136,9 @@ class _ProgressBarState extends State<ProgressBar> {
 
   void _onHorizontalDragUpdate(DragUpdateDetails details) {
     var progress = getProgress(details.globalPosition);
+    setState(() {
+      tempLeft = progress;
+    });
     widget.tapProgressHandler(progress);
   }
 
@@ -147,4 +160,11 @@ class _ProgressBarState extends State<ProgressBar> {
       globalPosition,
     );
   }
+
+  void _onHorizontalDragEnd(DragEndDetails details) {
+    if (tempLeft != null) {
+      widget.changeProgressHandler(tempLeft);
+      tempLeft = null;
+    }
+  }
 }