提交 abc714c0 authored 作者: kongdywang's avatar kongdywang

1. add short video demo

2. superPlayer widget Component 3. superPlayer image sprite & video play info 4. superPlayer add loop and mute api 5. global gradle repositories config
上级 6115b962
......@@ -9,6 +9,5 @@ rootProject.ext {
其中 latest.release 可指定为自己需要的版本号,例如 "com.tencent.liteav:LiteAVSDK_Player:9.5.29035" ,写成 latest.release 则默认使用最新版本
*/
// liteavSdk="com.tencent.liteav:LiteAVSDK_Player:latest.release"
// liteavSdk="com.tencent.liteav:LiteAVSDK_Professional:latest.release"
liteavSdk="com.tencent.liteav:LiteAVSDK_Professional:10.8.0.13052"
liteavSdk="com.tencent.liteav:LiteAVSDK_Professional:latest.release"
}
\ No newline at end of file
pluginManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = 'flutter_super_player'
......@@ -11,6 +11,7 @@ import androidx.annotation.NonNull;
import com.tencent.rtmp.ITXVodPlayListener;
import com.tencent.rtmp.TXBitrateItem;
import com.tencent.rtmp.TXImageSprite;
import com.tencent.rtmp.TXLiveConstants;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.TXPlayInfoParams;
......@@ -47,6 +48,7 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
final private FTXPlayerEventSink mNetStatusSink = new FTXPlayerEventSink();
private TXVodPlayer mVodPlayer;
private TXImageSprite mTxImageSprite;
private static final int Uninitialized = -101;
private TextureRegistry.SurfaceTextureEntry mSurfaceTextureEntry;
......@@ -358,7 +360,29 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
playForwardAssetPath, getPlayerId());
int pipResult = mPipManager.enterPip(isPlaying(), mPipParams);
result.success(pipResult);
} else if(call.method.equals("initImageSprite")) {
String vvtUrl = call.argument("vvtUrl");
List<String> imageUrls = call.argument("imageUrls");
mTxImageSprite.setVTTUrlAndImageUrls(vvtUrl,imageUrls);
result.success(null);
} else if(call.method.equals("getImageSprite")) {
final Double time = call.argument("time");
new Thread(new Runnable() {
@Override
public void run() {
Bitmap bitmap = mTxImageSprite.getThumbnail(time.floatValue());
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if(null != bitmap) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] byteArray = stream.toByteArray();
result.success(byteArray);
} else {
result.success(null);
}
}
}).start();
}
else {
result.notImplemented();
}
}
......@@ -368,6 +392,7 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
mVodPlayer = new TXVodPlayer(mFlutterPluginBinding.getApplicationContext());
mVodPlayer.setVodListener(this);
setPlayer(onlyAudio);
mTxImageSprite = new TXImageSprite(mFlutterPluginBinding.getApplicationContext());
}
return mSurfaceTextureEntry == null ? -1 : mSurfaceTextureEntry.id();
}
......
......@@ -40,12 +40,20 @@ Flutter播放器组件是基于flutter播放器SDK的扩展,播放器组件对
## 集成指引[](id:Guide)
1. 将项目中example下的 `superplayer` 目录复制到自己的flutter工程下
1. 将项目中 `superplayer_widget` 目录复制到自己的flutter工程下
2. 在需要使用到的页面,导入`superplayer`的依赖包,如下所示:
2. 在自己项目的配置文件 `pubspec.yaml` 下添加依赖
```yaml
superplayer_widget:
# 该路径根据superplayer_widget存放路径改变
path: ../superplayer_widget
```
3. 在需要使用到的页面,导入`superplayer_widget`的依赖包,如下所示:
```dart
import 'superplayer/demo_superplayer_lib.dart';
import 'package:superplayer_widget/demo_superplayer_lib.dart';
```
## SDK集成[](id:stepone)
......@@ -163,6 +171,8 @@ SuperPlayerModel model = SuperPlayerModel();
model.appId = 1500005830;
model.videoId = new SuperPlayerVideoId();
model.videoId.fileId = "8602268011437356984";
// psign 即播放器组件签名,签名介绍和生成方式参见链接:https://cloud.tencent.com/document/product/266/42436
model.videoId.pSign = "psignXXX" // 通过fileId播放必须填写
_controller.playWithModelNeedLicence(model);
```
......
......@@ -100,10 +100,10 @@ await _controller.startVodPlay(_url);
:::
::: 通过field方式
```dart
TXPlayerAuthParams authParams = TXPlayerAuthParams();
authParams.appId = 1252463788;
authParams.fileId = "4564972819220421305";
await _controller.startVodPlayWithParams(authParams);
// 通过fileId播放必须填写 psign, psign 即播放器签名,签名介绍和生成方式参见链接:https://cloud.tencent.com/document/product/266/42436
TXPlayInfoParams params = TXPlayInfoParams(appId: 1252463788,
fileId: "4564972819220421305", psign: "psignxxxxxxx");
await _controller.startVodPlayWithParams(params);
```
[媒资管理](https://console.cloud.tencent.com/vod/media) 找到对应的视频文件。在文件名下方可以看到 FileId。
......@@ -581,7 +581,7 @@ bool result = await TXVodDownloadController.instance.deleteDownloadMediaInfo(med
在腾讯云控制台提取到appId, 加密视频的fileId 和 psign后,可以通过下面的方式进行播放:
```dart
// 通过fileId播放必须填写 psign, psign 即播放器签名,签名介绍和生成方式参见链接:https://cloud.tencent.com/document/product/266/42436
TXPlayInfoParams params = TXPlayInfoParams(appId: 1252463788,
fileId: "4564972819220421305", psign: "psignxxxxxxx");
_controller.startVodPlayWithParams(params);
......
......@@ -43,6 +43,11 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled true // 关闭混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
profile {
}
......
......@@ -9,7 +9,7 @@ buildscript {
}
}
allprojects {
rootProject.allprojects {
repositories {
mavenCentral()
google()
......
// Copyright (c) 2022 Tencent. All rights reserved.
import 'package:flutter/cupertino.dart';
import 'package:super_player/super_player.dart';
import 'package:superplayer_widget/demo_superplayer_lib.dart';
import 'shortvideo/demo_short_video_lib.dart';
class DemoShortVideoPlayer extends StatefulWidget {
@override
_DemoShortVideoPlayerState createState() => _DemoShortVideoPlayerState();
}
class _DemoShortVideoPlayerState extends State<DemoShortVideoPlayer> with WidgetsBindingObserver {
static const TAG = "ShortVideo::ShortVideoListViewState";
int _currentIndex = 0;
List<SuperPlayerModel> superPlayerModelList = [];
@override
void initState() {
super.initState();
ShortVideoDataLoader loader = ShortVideoDataLoader();
loader.getPageListDataOneByOneFunction((dataModels) {
setState(() {
superPlayerModelList = dataModels;
});
});
WidgetsBinding.instance.addObserver(this);
}
@override
Widget build(BuildContext context) {
List<Widget> widgetList = [];
for (int i = 0; i < superPlayerModelList.length; i++) {
widgetList.add(ShortVideoPageWidget(
position: i,
videoUrl: superPlayerModelList[i].videoURL,
coverUrl: superPlayerModelList[i].coverUrl));
}
return PageView(
scrollDirection: Axis.vertical,
onPageChanged: (int index) {
LogUtils.i(TAG,"[onPageEndChanged] outside ${_currentIndex.toString()} ——》 ${index.toString()}");
_stopAndPlay(index);
},
children: widgetList,
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
LogUtils.i(TAG, "[AppLifecycleState.paused]");
_onApplicationPause();
break;
case AppLifecycleState.resumed:
LogUtils.i(TAG, "[AppLifecycleState.resumed]");
_onApplicationResume();
break;
}
}
_stopAndPlay(int index) async {
EventBusUtils.getInstance().fire(new StopAndResumeEvent(index));
_currentIndex = index;
}
_onApplicationPause() {
EventBusUtils.getInstance().fire(new ApplicationPauseEvent());
}
_onApplicationResume() {
EventBusUtils.getInstance().fire(new ApplicationResumeEvent());
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
......@@ -5,8 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:super_player/super_player.dart';
import 'package:super_player_example/ui/demo_inputdialog.dart';
import 'superplayer/demo_superplayer_lib.dart';
import 'package:superplayer_widget/demo_superplayer_lib.dart';
/// flutter superplayer demo
class DemoSuperplayer extends StatefulWidget {
......@@ -244,6 +243,14 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
model.playAction = playAction;
models.add(model);
model = SuperPlayerModel();
model.appId = 1252463788;
model.videoId = new SuperPlayerVideoId();
model.videoId!.fileId = "5285890781763144364";
model.title = "腾讯云";
model.playAction = playAction;
models.add(model);
model = SuperPlayerModel();
model.appId = 1252463788;
model.videoId = new SuperPlayerVideoId();
......
......@@ -28,7 +28,7 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
StreamSubscription? playNetEventSubscription;
StreamSubscription? playerStateEventSubscription;
GlobalKey<VideoSliderState> progressSliderKey = GlobalKey();
GlobalKey<VideoSliderViewState> progressSliderKey = GlobalKey();
Future<void> init() async {
if (!mounted) return;
......@@ -40,7 +40,7 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
if (event["event"] == TXVodPlayEvent.PLAY_EVT_PLAY_PROGRESS) {
_progress = event["EVT_PLAY_PROGRESS"].toDouble();
_maxLiveProgressTime = _progress >= _maxLiveProgressTime ? _progress : _maxLiveProgressTime;
progressSliderKey.currentState?.updatePorgess(1, _maxLiveProgressTime);
progressSliderKey.currentState?.updateProgress(1, _maxLiveProgressTime);
} else if (event["event"] == TXVodPlayEvent.PLAY_EVT_PLAY_BEGIN ||
event["event"] == TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME) {
//首帧出现
......
......@@ -34,7 +34,7 @@ class _DemoTXVodlayerState extends State<DemoTXVodPlayer>
StreamSubscription? playEventSubscription;
StreamSubscription? playNetEventSubscription;
GlobalKey<VideoSliderState> progressSliderKey = GlobalKey();
GlobalKey<VideoSliderViewState> progressSliderKey = GlobalKey();
Future<void> init() async {
if (!mounted) return;
......@@ -57,9 +57,9 @@ class _DemoTXVodlayerState extends State<DemoTXVodPlayer>
_currentProgress = event[TXVodPlayEvent.EVT_PLAY_PROGRESS].toDouble();
double videoDuration = event[TXVodPlayEvent.EVT_PLAY_DURATION].toDouble(); // 总播放时长,转换后的单位 秒
if (videoDuration == 0.0) {
progressSliderKey.currentState?.updatePorgess(0.0, 0.0);
progressSliderKey.currentState?.updateProgress(0.0, 0.0);
} else {
progressSliderKey.currentState?.updatePorgess(_currentProgress / videoDuration, videoDuration);
progressSliderKey.currentState?.updateProgress(_currentProgress / videoDuration, videoDuration);
}
} else if (event["event"] == TXVodPlayEvent.PLAY_EVT_GET_PLAYINFO_SUCC) {
String? playUrl = event[TXVodPlayEvent.EVT_PLAY_URL]?.toString();
......
......@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:super_player/super_player.dart';
import 'package:super_player_example/shortvideo/demo_short_video_lib.dart';
import 'ui/treePage.dart';
......@@ -106,4 +107,10 @@ class _MyAppState extends State<MyApp> {
builder: EasyLoading.init(),
);
}
@override
void dispose() {
super.dispose();
EventBusUtils.getInstance().destroy();
}
}
// Copyright (c) 2022 Tencent. All rights reserved.
library demo_short_video_player_lib;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:super_player/super_player.dart';
import 'package:superplayer_widget/demo_superplayer_lib.dart';
import 'package:super_player_example/ui/demo_video_slider_view.dart';
import 'package:event_bus/event_bus.dart';
part 'model/short_video_data_loader.dart';
part 'short_video_page_widget.dart';
part 'tools/event_bus_utils.dart';
\ No newline at end of file
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_short_video_player_lib;
class ShortVideoDataLoader {
List<SuperPlayerModel> _currentModels = [];
int _appId = 1500005830;
var _fileIdArray = [
"387702294394366256",
"387702294394228858",
"387702294394228636",
"387702294394228527",
"387702294167066523",
"387702294167066515",
"387702294168748446",
"387702294394227941"
];
List<SuperPlayerModel> _defaultData = [];
SuperVodDataLoader _loader = SuperVodDataLoader();
getPageListDataOneByOneFunction(
Function(List<SuperPlayerModel> model) callback) {
_currentModels.clear();
for (int i = 0; i < _fileIdArray.length; i++) {
SuperPlayerModel model = new SuperPlayerModel();
model.appId = _appId;
model.videoId = new SuperPlayerVideoId();
model.videoId?.fileId = _fileIdArray[i];
_defaultData.add(model);
}
for (var model in _defaultData) {
_getVodListData(model, callback);
}
}
_getVodListData(SuperPlayerModel model, Function(List<SuperPlayerModel> models) callback) async {
_loader.getVideoData(model, (resultModel) {
_currentModels.add(resultModel);
if (_currentModels.length == _defaultData.length) {
callback(_currentModels);
}
});
}
}
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_short_video_player_lib;
class ShortVideoPageWidget extends StatefulWidget {
String videoUrl;
String coverUrl;
int position;
late TXVodPlayerController _controller;
ShortVideoPageWidget(
{required this.position,
required this.videoUrl,
required this.coverUrl});
@override
State<StatefulWidget> createState() {
return _TXVodPlayerPageState();
}
}
class _TXVodPlayerPageState extends State<ShortVideoPageWidget> {
static const TAG = "ShortVideo::TXVodPlayerPageState";
late TXPlayerVideo _txPlayerVideo;
bool _isVideoPrepared = false;
bool _isVideoPlaying = true;
GlobalKey<VideoSliderViewState> _progressSliderKey = GlobalKey();
late StreamSubscription _streamSubscriptionStopAndPlay;
late StreamSubscription _streamSubscriptionApplicationResume;
late StreamSubscription _streamSubscriptionApplicationPause;
@override
void initState() {
super.initState();
_init();
}
_init() async {
widget._controller = new TXVodPlayerController();
_txPlayerVideo = new TXPlayerVideo(controller: widget._controller);
await widget._controller.initialize();
widget._controller.setConfig(FTXVodPlayConfig());
LogUtils.i(TAG, " [init] ${widget.position.toString()} ${this.hashCode.toString()} ${widget._controller.hashCode.toString()}");
_setPlayerListener();
_setEventBusListener();
if (widget.position == 0) {
_startPlay();
}
}
@override
Widget build(BuildContext context) {
return _getTXVodPlayerMainPage();
}
@override
void dispose() {
_dispose();
super.dispose();
}
_dispose() async {
_streamSubscriptionStopAndPlay.cancel();
_streamSubscriptionApplicationResume.cancel();
_streamSubscriptionApplicationPause.cancel();
await _stop();
widget._controller.dispose();
LogUtils.i(TAG, " [dispose] ${widget.position.toString()} ${this.hashCode.toString()} ${widget._controller.hashCode.toString()}");
}
Widget _getTXVodPlayerMainPage() {
return Stack(
children: <Widget>[
_getGestureDetectorView(),
_getPreviewImg(),
_getSeekBarView()
],
);
}
GestureDetector _getGestureDetectorView() {
return GestureDetector(
child: Stack(
children: <Widget>[
Container(
child: _txPlayerVideo,
),
_getPauseView()
],
),
onTap: () {
_onTapPageView();
});
}
_onTapPageView() {
widget._controller.isPlaying().then((value) {
value == true ? _pause() :_resume();
});
LogUtils.i(TAG, "tap ${_isVideoPlaying.toString()}");
}
Widget _getSeekBarView() {
return Positioned(
child: VideoSliderView(widget._controller, _progressSliderKey),
bottom: 20,
right: 0,
left: 0,
);
}
Widget _getPauseView() {
return Offstage(
offstage: _isVideoPlaying,
child: Align(
child: Container(
child: Image(image: AssetImage("images/superplayer_ic_vod_play_normal.png", package:StringResource.PKG_NAME)),
height: 50,
width: 50),
alignment: Alignment.center,
));
}
Widget _getPreviewImg() {
return Offstage(
offstage: _isVideoPrepared,
child: Container(
decoration: BoxDecoration(
color: Colors.black,
image: DecorationImage(
image: NetworkImage(widget.coverUrl),
fit: BoxFit.fill,
)),
child: Scaffold(
backgroundColor: Colors.transparent, //把scaffold的背景色改成透明
)));
}
_pause() {
LogUtils.i(TAG, "[_pause]");
widget._controller.pause();
setState(() {
_isVideoPlaying = false;
});
}
_resume() {
LogUtils.i(TAG, "[_resume]");
widget._controller.resume();
setState(() {
_isVideoPlaying = true;
});
}
_stopLastAndPlayCurrent(StopAndResumeEvent event) {
LogUtils.i(TAG, " [received at not current outside] ${widget.position.toString()} ${this.hashCode.toString()} ${widget.hashCode.toString()} ${widget._controller.hashCode.toString()}");
event.index != widget.position ? _stop() :_startPlay();
}
Future<void> _stop() async{
if (!mounted) return;
LogUtils.i(TAG, " [stop] ${widget.position.toString()} ${widget.hashCode.toString()}");
_isVideoPrepared = false;
_isVideoPlaying = true;
widget._controller.stop();
}
_startPlay() async {
LogUtils.i(TAG, " [_startPlay]");
setState(() {
_isVideoPlaying = true;
});
await widget._controller.setLoop(true);
widget._controller.startVodPlay(widget.videoUrl);
}
_hideCover() {
if (!mounted) return;
LogUtils.i(TAG, " [received] ${widget.position.toString()} ${widget.hashCode.toString()}");
setState(() {
_isVideoPrepared = true;
});
}
_setEventBusListener() {
_streamSubscriptionStopAndPlay = EventBusUtils.getInstance().on<StopAndResumeEvent>().listen((event) {
_stopLastAndPlayCurrent(event);
});
_streamSubscriptionApplicationResume = EventBusUtils.getInstance().on<ApplicationResumeEvent>().listen((event) {
_resume();
});
_streamSubscriptionApplicationPause = EventBusUtils.getInstance().on<ApplicationPauseEvent>().listen((event) {
_pause();
});
}
_setPlayerListener() {
widget._controller.onPlayerEventBroadcast.listen((event) async {
if (event["event"] == TXVodPlayEvent.PLAY_EVT_PLAY_PROGRESS) {
if (!mounted) return;
double currentProgress = event["EVT_PLAY_PROGRESS"].toDouble();
double videoDuration = event["EVT_PLAY_DURATION"].toDouble(); // 总播放时长,转换后的单位 秒
_progressSliderKey.currentState?.updateProgress(currentProgress / videoDuration, videoDuration);
} else if (event["event"] == TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME) {
LogUtils.i(TAG, " [received] TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME");
_hideCover();
}
});
}
}
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_short_video_player_lib;
class EventBusUtils {
static final EventBus _instance = EventBus();
static EventBus getInstance() {
return _instance;
}
}
class ApplicationPauseEvent{
ApplicationPauseEvent();
}
class StopAndResumeEvent{
int index;
StopAndResumeEvent(this.index);
}
class ApplicationResumeEvent{
ApplicationResumeEvent();
}
\ No newline at end of file
......@@ -11,16 +11,16 @@ class VideoSliderView extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return VideoSliderState();
return VideoSliderViewState();
}
}
class VideoSliderState extends State<VideoSliderView> {
class VideoSliderViewState extends State<VideoSliderView> {
double _currentProgress = 0.0;
double _videoDuration = 0.0;
bool isSliding = false;
bool _isSliding = false;
@override
Widget build(BuildContext context) {
......@@ -40,14 +40,14 @@ class VideoSliderState extends State<VideoSliderView> {
max: 1,
value: _currentProgress,
onChanged: (double value) {
isSliding = true;
_isSliding = true;
setState(() {
_currentProgress = value;
});
},
onChangeEnd: (double value) {
setState(() {
isSliding = false;
_isSliding = false;
_currentProgress = value;
if (widget._controller is TXVodPlayerController) {
TXVodPlayerController controller = widget._controller as TXVodPlayerController;
......@@ -64,8 +64,8 @@ class VideoSliderState extends State<VideoSliderView> {
));
}
void updatePorgess(double progress,double totalDuration) {
if(!isSliding) {
void updateProgress(double progress,double totalDuration) {
if(!_isSliding) {
setState(() {
if(progress > 1.0) {
progress = 1.0;
......
......@@ -6,6 +6,7 @@ import 'package:super_player_example/demo_superplayer.dart';
import 'package:super_player_example/demo_txLiveplayer.dart';
import 'package:super_player_example/demo_txvodplayer.dart';
import '../demo_short_video_player.dart';
import 'demo_define.dart';
class TreePage extends StatefulWidget {
......@@ -43,6 +44,7 @@ class _TreePageState extends State<TreePage> {
TreeDatachild("直播播放"),
TreeDatachild("点播播放"),
TreeDatachild("播放器组件"),
TreeDatachild("短视频播放")
], "播放器", false),
];
}
......@@ -84,7 +86,7 @@ class _TreePageState extends State<TreePage> {
},
//展开内容
body: Container(
height: 200,
height: 250,
padding: EdgeInsets.symmetric(horizontal: 5.0),
child: ListView.builder(
itemCount: treeData.children.length,
......@@ -133,8 +135,10 @@ class _TreePageState extends State<TreePage> {
return DemoTXLivePlayer();
}else if (i == 1) {
return DemoTXVodPlayer();
} else {
} else if (i==2){
return DemoSuperplayer();
} else {
return DemoShortVideoPlayer();
}
}
),
......
......@@ -12,6 +12,7 @@ environment:
dependencies:
flutter:
sdk: flutter
event_bus: ^2.0.0
auto_orientation: ^2.2.1
......@@ -24,6 +25,9 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
superplayer_widget:
# 该路径根据superplayer_widget存放路径改变
path: ../superplayer_widget
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
......@@ -44,22 +48,6 @@ flutter:
# the material Icons class.
uses-material-design: true
assets:
- images/ic_pip_play_icon.png
- images/ic_pip_play_normal.png
- images/ic_pip_play_pause.png
- images/ic_pip_play_forward.png
- images/ic_pip_play_replay.png
- images/superplayer_ic_vod_more_normal.png
- images/superplayer_ic_light_max.png
- images/superplayer_ic_light_min.png
- images/superplayer_ic_volume_max.png
- images/superplayer_ic_volume_min.png
- images/superplayer_ic_vod_pause_normal.png
- images/superplayer_ic_vod_play_normal.png
- images/superplayer_bottom_shadow.png
- images/superplayer_ic_vod_fullscreen.png
- images/superplayer_top_shadow.png
- images/superplayer_btn_back_play.png
- images/ic_new_vod_bg.png
- images/addp.png
# To add assets to your application, add an assets section, like this:
......
......@@ -25,6 +25,7 @@ static const int CODE_ON_RECEIVE_FIRST_FRAME = 2003;
*/
@implementation FTXVodPlayer {
TXVodPlayer *_txVodPlayer;
TXImageSprite *_txImageSprite;
FTXPlayerEventSinkQueue *_eventSink;
FTXPlayerEventSinkQueue *_netStatusSink;
FlutterMethodChannel *_methodChannel;
......@@ -149,7 +150,7 @@ BOOL volatile isStop = false;
_txVodPlayer.vodDelegate = self;
[self setupPlayerWithBool:onlyAudio];
}
_txImageSprite = [[TXImageSprite alloc] init];
return [NSNumber numberWithLongLong:_textureId];
}
......@@ -427,6 +428,32 @@ BOOL volatile isStop = false;
int ret = [self enterPictureInPictureMode];
result(@(ret));
}
else if ([@"initImageSprite" isEqualToString:call.method]) {
NSDictionary *args = call.arguments;
NSString *vvtStr = args[@"vvtUrl"];
NSArray *imageStrs = args[@"imageUrls"];
NSMutableArray *imageUrls = @[].mutableCopy;
for(NSString *url in imageStrs) {
NSURL *nsurl = [NSURL URLWithString:url];
if (nsurl) {
[imageUrls addObject:nsurl];
}
}
[_txImageSprite setVTTUrl: [NSURL URLWithString:vvtStr] imageUrls:imageUrls];
result(nil);
}
else if ([@"getImageSprite" isEqualToString:call.method]) {
NSDictionary *args = call.arguments;
NSInteger time = [args[@"time"] intValue];
UIImage *imageSprite = [_txImageSprite getThumbnail:time];
if(nil != imageSprite) {
NSData *data = UIImagePNGRepresentation(imageSprite);
result(data);
} else {
result(nil);
}
}
else {
result(FlutterMethodNotImplemented);
}
......
......@@ -21,6 +21,7 @@ class TXPlayerVideoState extends State<TXPlayerVideo> {
@override
void initState() {
super.initState();
controller = widget.controller;
_checkStreamListen();
_resetControllerLink();
......
......@@ -402,6 +402,17 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
}
Future<void> initImageSprite(String vvtUrl, List<String> imageUrls) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("initImageSprite", {"vvtUrl":vvtUrl, "imageUrls":imageUrls});
}
Future<Uint8List?> getImageSprite(double time) async {
await _initPlayer.future;
return await _channel.invokeMethod("getImageSprite",{"time":time});
}
/// 获取总时长
Future<double> getDuration() async {
if (_isNeedDisposed) return 0;
......
......@@ -5,6 +5,7 @@ import 'dart:async';
import 'dart:core';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
......
......@@ -20,4 +20,6 @@ class StringResource {
static const OPEN_PIP = "正在开启画中画";
static const CLOSE_PIP = "正在关闭画中画";
static const ERROR_PIP = "画中画开启失败";
static const PKG_NAME = "superplayer_widget";
}
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_super_player_lib;
typedef TaskCallback = void Function(bool success, dynamic result);
typedef TaskDetailFunc = Future Function();
/// 确保入栈的异步/非异步按顺序执行
/// 单线程模型
class TaskExecutors {
bool _isTaskRunning = false;
final LinkedList<TaskItem> _taskList = LinkedList<TaskItem>();
Future addTask(TaskDetailFunc task) {
Completer completer = Completer();
TaskItem taskItem = TaskItem(task, (success, result) {
if (success) {
completer.complete(result);
} else {
completer.completeError(result);
}
_isTaskRunning = false;
//递归任务
_doTask();
});
_taskList.add(taskItem);
_doTask();
return completer.future;
}
Future<void> _doTask() async {
if(_isTaskRunning) return;
if (_taskList.isEmpty) return;
_isTaskRunning = true;
TaskItem taskItem = _taskList.first;
_taskList.remove(taskItem);
try{
var result = await taskItem.function.call();
taskItem.taskCallback.call(true, result);
} catch(_){
taskItem.taskCallback.call(false, _.toString());
}
}
}
class TaskItem extends LinkedListEntry<TaskItem> {
TaskDetailFunc function;
TaskCallback taskCallback;
TaskItem(this.function, this.taskCallback);
}
......@@ -2,10 +2,12 @@
library demo_super_player_lib;
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/material.dart';
......@@ -24,6 +26,7 @@ part 'cgi/super_vod_data_loader.dart';
part 'model/superplayer_define.dart';
part 'model/superplayer_model.dart';
part 'tools/video_quality_utils.dart';
part 'tools/utils.dart';
part 'ui/superplayer_bottom_view.dart';
part 'ui/superplayer_quality_view.dart';
part 'ui/superplayer_title_view.dart';
......@@ -34,3 +37,4 @@ part 'ui/superplayer_video_slider.dart';
part 'common/color_resource.dart';
part 'common/string_resource.dart';
part 'common/theme_resource.dart';
part 'common/task_executors.dart';
\ No newline at end of file
......@@ -141,3 +141,9 @@ class SuperPlayerVideoId {
String fileId = ""; // 腾讯云视频fileId
String psign = ""; // v4 开启防盗链必填
}
/// 进度条打点信息
class SliderPoint {
Color pointColor = Colors.white;
double progress = 0;
}
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_super_player_lib;
class Utils {
static bool compareBuffer(Uint8List? buffer1, Uint8List? buffer2) {
if(buffer1 == null || buffer2 == null) {
return false;
}
if (buffer1.length != buffer2.length) {
return false;
}
for (int i = 0; i < buffer1.length; i++) {
if (buffer1[i] != buffer2[i]) {
return false;
}
}
return true;
}
/// 将秒数转换为hh:mm:ss的格式
static String formattedTime(double second) {
int h = second ~/ 3600;
int m = (second % 3600) ~/ 60;
int s = ((second % 3600) % 60).toInt();
String formatTime;
if (h == 0) {
formatTime = "${asTwoDigit(m)}:${asTwoDigit(s)}";
} else {
formatTime = "${asTwoDigit(h)}:${asTwoDigit(m)}:${asTwoDigit(s)}";
}
return formatTime;
}
static String asTwoDigit(int digit) {
String value = "";
if (digit < 10) {
value = "0";
}
value += digit.toString();
return value;
}
}
\ No newline at end of file
......@@ -4,9 +4,9 @@ part of demo_super_player_lib;
/// slider
class VideoBottomView extends StatefulWidget {
final SuperPlayerController _playerController;
final _BottomViewController _controller;
final BottomViewController _controller;
VideoBottomView(this._playerController, this._controller, GlobalKey<_VideoBottomViewState> key) : super(key: key);
const VideoBottomView(this._playerController, this._controller, Key key) : super(key: key);
@override
State<StatefulWidget> createState() {
......@@ -26,6 +26,10 @@ class _VideoBottomViewState extends State<VideoBottomView> {
bool _isOnDraging = false;
SuperPlayerType _playerType = SuperPlayerType.VOD;
VideoQuality? _currentQuality;
final GlobalKey<VideoSliderState> _sliderView = GlobalKey();
final List<SliderPoint> _playPoints = [];
List<PlayKeyFrameDescInfo>? keyFrameList;
PlayKeyFrameDescInfo? showedKeyFrameInfo;
@override
void initState() {
......@@ -45,6 +49,8 @@ class _VideoBottomViewState extends State<VideoBottomView> {
_isShowQuality = isFullScreen;
_currentQuality = widget._playerController.currentQuality;
_playerType = widget._playerController.playerType;
showedKeyFrameInfo = null;
setKeyFrame(widget._playerController.keyFrameInfo);
super.initState();
}
......@@ -55,26 +61,47 @@ class _VideoBottomViewState extends State<VideoBottomView> {
bottom: topBottomOffset,
right: 0,
left: 0,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage("images/superplayer_bottom_shadow.png"), fit: BoxFit.fill)),
padding: EdgeInsets.only(left: 6, right: 6),
child: Column(
children: [
Center(
child: Visibility(
visible: null != showedKeyFrameInfo,
child: null != showedKeyFrameInfo
? Container(
decoration: const BoxDecoration(
color: Color(ColorResource.COLOR_TRANS_BLACK), borderRadius: BorderRadius.all(Radius.circular(50))),
padding: const EdgeInsets.only(left: 10, right: 10, top: 5, bottom: 5),
child: Text(
"${Utils.formattedTime(showedKeyFrameInfo!.time)} ${showedKeyFrameInfo!.content}",
style: const TextStyle(color: Colors.white, fontSize: 14),
softWrap: true,
),
)
: const SizedBox()),
),
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("images/superplayer_bottom_shadow.png", package: StringResource.PKG_NAME), fit: BoxFit.fill)),
padding: const EdgeInsets.only(left: 6, right: 6),
child: Row(
children: [
_getPlayImage(),
Text(
_buildTextString(_currentDuration),
style: TextStyle(fontSize: 12, color: Colors.white),
style: const TextStyle(fontSize: 12, color: Colors.white),
),
_getSlider(),
Text(
_buildTextString(_videoDuration),
style: TextStyle(fontSize: 12, color: Colors.white),
style: const TextStyle(fontSize: 12, color: Colors.white),
),
_getFullScreenImage(),
_getQualityButton(),
],
),
)
],
),
);
}
......@@ -85,11 +112,11 @@ class _VideoBottomViewState extends State<VideoBottomView> {
child: InkWell(
onTap: onTapQualityView,
child: Container(
padding: EdgeInsets.only(left: 5),
padding: const EdgeInsets.only(left: 5),
child: _currentQuality != null
? Text(
VideoQualityUtils.transformToQualityName(_currentQuality!.title),
style: TextStyle(fontSize: 12, color: Colors.white),
style: const TextStyle(fontSize: 12, color: Colors.white),
)
: Container(),
)),
......@@ -100,15 +127,15 @@ class _VideoBottomViewState extends State<VideoBottomView> {
return InkWell(
onTap: onTapStartOrPause,
child: _isPlayMode
? Image(
? const Image(
width: 30,
height: 30,
image: AssetImage("images/superplayer_ic_vod_pause_normal.png"),
image: AssetImage("images/superplayer_ic_vod_pause_normal.png", package: StringResource.PKG_NAME),
)
: Image(
: const Image(
width: 30,
height: 30,
image: AssetImage("images/superplayer_ic_vod_play_normal.png"),
image: AssetImage("images/superplayer_ic_vod_play_normal.png", package: StringResource.PKG_NAME),
),
);
}
......@@ -118,10 +145,10 @@ class _VideoBottomViewState extends State<VideoBottomView> {
visible: _showFullScreenBtn,
child: InkWell(
onTap: onSwitchFullScreen,
child: Image(
child: const Image(
width: 30,
height: 30,
image: AssetImage("images/superplayer_ic_vod_fullscreen.png"),
image: AssetImage("images/superplayer_ic_vod_fullscreen.png", package: StringResource.PKG_NAME),
),
),
);
......@@ -130,29 +157,40 @@ class _VideoBottomViewState extends State<VideoBottomView> {
Widget _getSlider() {
return Expanded(
child: VideoSlider(
key: _sliderView,
min: 0,
max: _videoDuration,
value: _currentDuration,
bufferedValue: _bufferedDuration,
activeColor: Color(ColorResource.COLOR_MAIN_THEME),
inactiveColor: Color(ColorResource.COLOR_GRAY),
sliderColor: Color(ColorResource.COLOR_MAIN_THEME),
activeColor: const Color(ColorResource.COLOR_MAIN_THEME),
inactiveColor: const Color(ColorResource.COLOR_GRAY),
sliderColor: const Color(ColorResource.COLOR_MAIN_THEME),
sliderOutterColor: Colors.white,
progressHeight: 2,
sliderRadius: 4,
sliderOutterRadius: 10,
onPointClick: (double pointX, int pos) {
if (null != keyFrameList) {
setState(() {
showedKeyFrameInfo = keyFrameList![pos];
});
}
},
// 直播禁止时移
canDrag: _playerType == SuperPlayerType.VOD,
playPoints: !_showFullScreenBtn ? _playPoints : [],
onDragUpdate: (value) {
_isOnDraging = true;
_currentDuration = value * _videoDuration;
widget._controller.onSeekChanged(_currentDuration);
},
onDragEnd: (value) {
setState(() {
_isOnDraging = false;
_currentDuration = value * _videoDuration;
widget._playerController.seek(_currentDuration);
LogUtils.d(TAG,
"_currentDuration:$_currentDuration,_videoDuration:$_videoDuration");
LogUtils.d(TAG, "_currentDuration:$_currentDuration,_videoDuration:$_videoDuration");
widget._controller.onSeekEnd();
});
},
),
......@@ -198,7 +236,7 @@ class _VideoBottomViewState extends State<VideoBottomView> {
}
void updatePlayerType(SuperPlayerType type) {
if(_playerType != type) {
if (_playerType != type) {
setState(() {
_playerType = type;
});
......@@ -229,12 +267,28 @@ class _VideoBottomViewState extends State<VideoBottomView> {
});
}
}
void setKeyFrame(List<PlayKeyFrameDescInfo>? keyFrameList) {
this.keyFrameList = keyFrameList;
_playPoints.clear();
if (null != keyFrameList) {
for (PlayKeyFrameDescInfo info in keyFrameList) {
double progress = info.time / _videoDuration;
SliderPoint point = SliderPoint();
point.progress = progress;
_playPoints.add(point);
}
}
setState(() {});
}
}
class _BottomViewController {
class BottomViewController {
Function onTapStart;
Function onTapFullScreen;
Function onTapQuality;
Function(double value) onSeekChanged;
Function onSeekEnd;
_BottomViewController(this.onTapStart, this.onTapFullScreen, this.onTapQuality);
BottomViewController(this.onTapStart, this.onTapFullScreen, this.onTapQuality, this.onSeekChanged, this.onSeekEnd);
}
......@@ -2,10 +2,10 @@
part of demo_super_player_lib;
class SuperPlayerCoverView extends StatefulWidget {
final _CoverViewController _controller;
SuperPlayerModel? videoModel;
final CoverViewController _controller;
final SuperPlayerModel? videoModel;
SuperPlayerCoverView(this._controller, GlobalKey<_SuperPlayerCoverViewState> key, this.videoModel) : super(key: key);
const SuperPlayerCoverView(this._controller, Key key, this.videoModel) : super(key: key);
@override
State<StatefulWidget> createState() => _SuperPlayerCoverViewState();
......@@ -53,7 +53,7 @@ class _SuperPlayerCoverViewState extends State<SuperPlayerCoverView> {
onDoubleTap: _onDoubleTapVideo,
onTap: _onSingleTapVideo,
child: Container(
decoration: BoxDecoration(
decoration: const BoxDecoration(
// 增加一个半透明背景,防止透明封面图的出现
color:Color(ColorResource.COLOR_TRANS_GRAY)
),
......@@ -85,9 +85,9 @@ class _SuperPlayerCoverViewState extends State<SuperPlayerCoverView> {
}
}
class _CoverViewController {
class CoverViewController {
Function onDoubleTapVideo;
Function onSingleTapVideo;
_CoverViewController(this.onDoubleTapVideo, this.onSingleTapVideo);
CoverViewController(this.onDoubleTapVideo, this.onSingleTapVideo);
}
......@@ -5,9 +5,9 @@ typedef BoolFunction = bool Function();
typedef DoubleFunction = double Function();
/// 超级播放器更多菜单
class SuperPlayerMoreView extends StatefulWidget {
_MoreViewController controller;
final MoreViewController controller;
SuperPlayerMoreView(this.controller, {Key? key}) : super(key: key);
const SuperPlayerMoreView(this.controller, {Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _SuperPlayerMoreViewState();
......@@ -78,8 +78,8 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
child: Container(
height: double.infinity,
width: 320,
padding: EdgeInsets.only(left: 15, right: 20, top: 15, bottom: 15),
decoration: BoxDecoration(color: Color(ColorResource.COLOR_TRANS_BLACK)),
padding: const EdgeInsets.only(left: 15, right: 20, top: 15, bottom: 15),
decoration: const BoxDecoration(color: Color(ColorResource.COLOR_TRANS_BLACK)),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
......@@ -96,7 +96,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
Widget _getSwitchHardwareWidget() {
return Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
margin: const EdgeInsets.only(top: 10, bottom: 10),
child: Row(
children: [
Text(
......@@ -105,7 +105,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
style: ThemeResource.getCommonLabelTextStyle(),
),
Switch(
activeColor: Color(ColorResource.COLOR_MAIN_THEME),
activeColor: const Color(ColorResource.COLOR_MAIN_THEME),
value: _isOpenAccelerate,
onChanged: _onChangeAccelerate)
],
......@@ -123,7 +123,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
];
for (String rateStr in playRateStr.keys) {
playRateChild.add(Container(
padding: EdgeInsets.only(left: 5, right: 5),
padding: const EdgeInsets.only(left: 5, right: 5),
child: InkWell(
onTap: () => _onChangePlayRate(rateStr),
child: Text(
......@@ -137,7 +137,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
return Visibility(
visible: _isVodPlay,
child: Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
margin: const EdgeInsets.only(top: 10, bottom: 10),
child: Row(
children: playRateChild,
),
......@@ -147,14 +147,14 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
Widget _getBrightnessWidget() {
return Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
margin: const EdgeInsets.only(top: 10, bottom: 10),
child: Row(children: [
Text(
StringResource.BRIGHTNESS_LABEL,
textAlign: TextAlign.center,
style: ThemeResource.getCommonLabelTextStyle(),
),
Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_light_min.png")),
const Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_light_min.png", package:StringResource.PKG_NAME)),
Expanded(
child: Theme(
data: ThemeResource.getCommonSliderTheme(),
......@@ -165,14 +165,14 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
onChanged: _onChangeBrightness,
)),
),
Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_light_max.png")),
const Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_light_max.png", package:StringResource.PKG_NAME)),
]),
);
}
Widget _getVolumeWidget() {
return Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
margin: const EdgeInsets.only(top: 10, bottom: 10),
child: Row(
children: [
Text(
......@@ -180,7 +180,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
textAlign: TextAlign.center,
style: ThemeResource.getCommonLabelTextStyle(),
),
Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_volume_min.png")),
const Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_volume_min.png", package:StringResource.PKG_NAME)),
Expanded(
child: Theme(
data: ThemeResource.getCommonSliderTheme(),
......@@ -191,7 +191,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
onChanged: _onChangeVolume,
)),
),
Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_volume_max.png")),
const Image(width: 30, height: 30, image: AssetImage("images/superplayer_ic_volume_max.png", package:StringResource.PKG_NAME)),
],
),
);
......@@ -264,12 +264,12 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
}
}
class _MoreViewController {
class MoreViewController {
BoolFunction getAccelerateIsOpen;
DoubleFunction getPlayRate;
Function(bool value) siwtchAccelerate;
Function(double playRate) onChangedPlayRate;
BoolFunction getIsVodPlay;
_MoreViewController(this.getAccelerateIsOpen, this.getPlayRate, this.siwtchAccelerate, this.onChangedPlayRate, this.getIsVodPlay);
MoreViewController(this.getAccelerateIsOpen, this.getPlayRate, this.siwtchAccelerate, this.onChangedPlayRate, this.getIsVodPlay);
}
......@@ -4,9 +4,9 @@ part of demo_super_player_lib;
class QualityListView extends StatefulWidget {
final List<VideoQuality>? _qualityList;
final VideoQuality? _currentQuality;
final _QualityListViewController _controller;
final QualityListViewController _controller;
QualityListView(this._controller, this._qualityList, this._currentQuality, Key key) : super(key: key);
const QualityListView(this._controller, this._qualityList, this._currentQuality, Key key) : super(key: key);
@override
State<StatefulWidget> createState() => _QualityListViewState();
......@@ -32,8 +32,8 @@ class _QualityListViewState extends State<QualityListView> {
child: Container(
height: double.infinity,
width: 300,
padding: EdgeInsets.only(left: 15, right: 15, top: 15, bottom: 15),
decoration: BoxDecoration(color: Color(ColorResource.COLOR_TRANS_BLACK)),
padding: const EdgeInsets.only(left: 15, right: 15, top: 15, bottom: 15),
decoration: const BoxDecoration(color: Color(ColorResource.COLOR_TRANS_BLACK)),
child: Center(
child: null != _qualityList
? ListView.builder(
......@@ -44,7 +44,7 @@ class _QualityListViewState extends State<QualityListView> {
return InkWell(
onTap: () => widget._controller.onSwitchStream(_qualityList![index]),
child: Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
margin: const EdgeInsets.only(top: 10, bottom: 10),
child: Text(
_qualityList![index].title,
textAlign: TextAlign.center,
......@@ -71,8 +71,8 @@ class _QualityListViewState extends State<QualityListView> {
}
}
class _QualityListViewController {
class QualityListViewController {
Function(VideoQuality) onSwitchStream;
_QualityListViewController(this.onSwitchStream);
QualityListViewController(this.onSwitchStream);
}
......@@ -6,7 +6,7 @@ class _VideoTitleView extends StatefulWidget {
final _VideoTitleController _controller;
final bool initIsFullScreen;
_VideoTitleView(this._controller, this.initIsFullScreen, this._title, GlobalKey<_VideoTitleViewState> key)
const _VideoTitleView(this._controller, this.initIsFullScreen, this._title, GlobalKey<_VideoTitleViewState> key)
: super(key: key);
@override
......@@ -27,32 +27,32 @@ class _VideoTitleViewState extends State<_VideoTitleView> {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(left: 6, right: 6),
decoration: BoxDecoration(
image: DecorationImage(image: AssetImage("images/superplayer_top_shadow.png"), fit: BoxFit.fill)),
padding: const EdgeInsets.only(left: 6, right: 6),
decoration: const BoxDecoration(
image: DecorationImage(image: AssetImage("images/superplayer_top_shadow.png", package:StringResource.PKG_NAME), fit: BoxFit.fill)),
child: Row(
children: [
InkWell(
onTap: _onTapBackBtn,
child: Image(
child: const Image(
width: 30,
height: 30,
image: AssetImage("images/superplayer_btn_back_play.png"),
image: AssetImage("images/superplayer_btn_back_play.png", package:StringResource.PKG_NAME),
),
),
Text(
_title,
style: TextStyle(fontSize: 11, color: Colors.white),
style: const TextStyle(fontSize: 11, color: Colors.white),
),
Expanded(child: SizedBox()),
const Expanded(child: SizedBox()),
Visibility(
visible: _isFullScreen,
child: InkWell(
onTap: _onTapMore,
child: Image(
child: const Image(
width: 30,
height: 30,
image: AssetImage("images/superplayer_ic_vod_more_normal.png"),
image: AssetImage("images/superplayer_ic_vod_more_normal.png", package:StringResource.PKG_NAME),
),
))
],
......@@ -84,8 +84,8 @@ class _VideoTitleViewState extends State<_VideoTitleView> {
}
class _VideoTitleController {
Function _onTapBack;
Function _onTapMore;
final Function _onTapBack;
final Function _onTapMore;
_VideoTitleController(this._onTapBack, this._onTapMore);
}
......@@ -3,7 +3,7 @@ part of demo_super_player_lib;
/// 视频进度条,双进度
class VideoSlider extends StatefulWidget {
late final _VideoSliderController controller;
late final VideoSliderController controller;
final double? progressHeight;
final double min;
......@@ -17,12 +17,14 @@ class VideoSlider extends StatefulWidget {
final Color? bufferedColor;
final Color? sliderColor;
final Color? sliderOutterColor;
List<SliderPoint>? playPoints = [];
bool? canDrag = true;
// calback
final Function? onDragStart;
final Function(double value)? onDragUpdate;
final Function(double value)? onDragEnd;
final Function(double pointX, int pos)? onPointClick;
VideoSlider(
{this.progressHeight,
......@@ -40,14 +42,18 @@ class VideoSlider extends StatefulWidget {
this.onDragStart,
this.onDragUpdate,
this.onDragEnd,
this.canDrag}) {
this.canDrag,
this.playPoints,
this.onPointClick,
GlobalKey<VideoSliderState>? key})
: super(key: key) {
double range = (max - min);
if (range <= 0) {
controller = _VideoSliderController(1, bufferedProgress: 1);
controller = VideoSliderController(1, bufferedProgress: 1);
} else {
double currentProgress = remainTwoFixed(value / range);
double? bufferedProgress = bufferedValue != null ? remainTwoFixed(bufferedValue! / range) : null;
controller = _VideoSliderController(currentProgress, bufferedProgress: bufferedProgress);
controller = VideoSliderController(currentProgress, bufferedProgress: bufferedProgress);
}
}
......@@ -120,6 +126,19 @@ class VideoSliderState extends State<VideoSlider> {
},
onTapUp: (TapUpDetails details) {
if (widget.canDrag! && !isDraging) {
List<SliderPoint> tmp = widget.playPoints ?? [];
for (int i = 0; i < tmp.length; i++) {
SliderPoint point = tmp[i];
final box = context.findRenderObject()! as RenderBox;
final Offset tapPos = box.globalToLocal(details.globalPosition);
double width = box.size.width;
double pointStart = width * point.progress;
double clickRadius = radius * 3;
if (tapPos.dx > pointStart - clickRadius && tapPos.dx < pointStart + clickRadius) {
widget.onPointClick?.call(pointStart, i);
return;
}
}
_seekToPosition(details.globalPosition);
widget.onDragEnd?.call(widget.controller.progress);
}
......@@ -144,6 +163,7 @@ class VideoSliderState extends State<VideoSlider> {
bufferedPercent: widget.controller.bufferedProgress,
leftPadding: leftPadding,
rightPadding: rightPadding,
pointList: widget.playPoints ?? [],
sliderOverlayRadius: overlayRadius),
),
),
......@@ -151,14 +171,18 @@ class VideoSliderState extends State<VideoSlider> {
);
}
void _seekToPosition(Offset globalPosition) {
double _getProgressByPosition(Offset globalPosition) {
final box = context.findRenderObject()! as RenderBox;
final Offset tapPos = box.globalToLocal(globalPosition);
double progress = widget.controller.progress;
progress = tapPos.dx / box.size.width;
if (progress < 0) progress = 0;
if (progress > 1) progress = 1;
return progress;
}
void _seekToPosition(Offset globalPosition) {
double progress = _getProgressByPosition(globalPosition);
setState(() {
widget.controller.progress = progress;
});
......@@ -175,6 +199,7 @@ class _VideoSliderPainter extends CustomPainter {
final bool isDraging;
final double leftPadding;
final double rightPadding;
final List<SliderPoint> pointList;
_VideoSliderPainter(
{required this.shaders,
......@@ -185,6 +210,7 @@ class _VideoSliderPainter extends CustomPainter {
required this.sliderOverlayRadius,
required this.leftPadding,
required this.rightPadding,
required this.pointList,
this.bufferedPercent});
@override
......@@ -222,6 +248,13 @@ class _VideoSliderPainter extends CustomPainter {
Radius.circular(sliderRadius)),
shaders.progressPaint);
// draw point
for (SliderPoint point in pointList) {
double pointStart = start + (width * point.progress);
shaders.pointPaint.color = point.pointColor;
canvas.drawCircle(Offset(pointStart, size.height / 2), progressHeight, shaders.pointPaint);
}
// draw outer slider,only show when drag
if (isDraging) {
canvas.drawCircle(Offset(progressEndless, size.height / 2), sliderOverlayRadius, shaders.dragSliderOverlayPaint);
......@@ -242,6 +275,7 @@ class _VideoSliderShaders {
Paint progressPaint = Paint();
Paint dragSliderPaint = Paint();
Paint dragSliderOverlayPaint = Paint();
Paint pointPaint = Paint();
_VideoSliderShaders(
{Color? backgroundColor, Color? progressColor, Color? dragSliderColor, Color? bufferedColor, Color? drawSliderOverlayColor}) {
......@@ -253,11 +287,11 @@ class _VideoSliderShaders {
}
}
class _VideoSliderController {
class VideoSliderController {
double progress;
double? bufferedProgress;
_VideoSliderController(this.progress, {this.bufferedProgress});
VideoSliderController(this.progress, {this.bufferedProgress});
}
void _checkRange(double value, {String? valueName}) {
......
name: superplayer_widget
description: superplayer,base on vodplayer
version: "1.0.1"
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
auto_orientation: ^2.2.1
flutter_easyloading: ^3.0.0
super_player:
# When depending on this package from a real application you should use:
# super_player: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
uses-material-design: true
assets:
- images/ic_pip_play_icon.png
- images/ic_pip_play_normal.png
- images/ic_pip_play_pause.png
- images/ic_pip_play_forward.png
- images/ic_pip_play_replay.png
- images/superplayer_ic_vod_more_normal.png
- images/superplayer_ic_light_max.png
- images/superplayer_ic_light_min.png
- images/superplayer_ic_volume_max.png
- images/superplayer_ic_volume_min.png
- images/superplayer_ic_vod_pause_normal.png
- images/superplayer_ic_vod_play_normal.png
- images/superplayer_bottom_shadow.png
- images/superplayer_ic_vod_fullscreen.png
- images/superplayer_top_shadow.png
- images/superplayer_btn_back_play.png
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论