提交 7bc259aa authored 作者: dokieyang's avatar dokieyang

1、add PIP for Android

2、add preDownload api
上级 80f476c9
......@@ -23,13 +23,13 @@ The Player SDK is a subproduct of RT-Cube, which provides VOD and live players b
This project provides the VOD and live player SDKs which you can use to set up your own playback services.
- [VOD player SDK](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%82%B9%E6%92%AD%E6%92%AD%E6%94%BE%E5%99%A8.md): `TXVodPlayerController` encapsulates the APIs of the VOD player SDKs for Android and iOS. You can integrate it to develop your VOD service. For the detailed code sample, see `DemoTXVodPlayer`.
- [VOD player SDK](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%82%B9%E6%92%AD%E6%92%AD%E6%94%BE-EN.md): `TXVodPlayerController` encapsulates the APIs of the VOD player SDKs for Android and iOS. You can integrate it to develop your VOD service. For the detailed code sample, see `DemoTXVodPlayer`.
- [Live player SDK](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%9B%B4%E6%92%AD%E6%92%AD%E6%94%BE%E5%99%A8.md): `TXLivePlayerController` encapsulates the APIs of the live player SDKs for Android and iOS. You can integrate it to develop your live playback service. For the detailed code sample, see `DemoTXLivePlayer`.
- [Live player SDK](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%9B%B4%E6%92%AD%E6%92%AD%E6%94%BE-EN.md): `TXLivePlayerController` encapsulates the APIs of the live player SDKs for Android and iOS. You can integrate it to develop your live playback service. For the detailed code sample, see `DemoTXLivePlayer`.
To reduce the connection costs, the Superplayer component (player with UIs) is provided in `example`. You can set up your own video playback service based on a few lines of simple code. You can apply the Superplayer code to your project and adjust UI and interaction details based on your project requirements.
- [Superplayer component](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E8%B6%85%E7%BA%A7%E6%92%AD%E6%94%BE%E5%99%A8.md): `SuperPlayerController` is the Superplayer component, which combines the VOD and live player SDKs. It is currently in beta testing, and its features are being optimized. For the detailed code sample, see `DemoSuperplayer`.
- [Superplayer component](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E6%92%AD%E6%94%BE%E5%99%A8%E7%BB%84%E4%BB%B6-EN.md): `SuperPlayerController` is the Superplayer component, which combines the VOD and live player SDKs. It is currently in beta testing, and its features are being optimized. For the detailed code sample, see `DemoSuperplayer`.
## Intended Audience
......
......@@ -11,25 +11,25 @@
├── ios // 播放器插件iOS源代码
├── lib // 播放器插件dart源代码
├── docs // 帮助文档
└── example // 超级播放器组件
└── example // 播放器相关demo代码
├── android // android的demo源代码
├── ios // iOS的demo源代码
└── lib // 点播播放器、直播播放器、超播放器使用例子
└── lib // 点播播放、直播播放、播放器组件使用例子
```
## 项目简介
腾讯云视立方·播放器 SDK 是音视频终端 SDK(腾讯云视立方)的子产品 SDK 之一,基于腾讯云强大的后台能力与 AI 技术,提供视频点播和直播播放能力的强大播放载体。结合腾讯云点播或云直播使用,可以快速体验流畅稳定的播放性能。充分覆盖多类应用场景,满足客户多样需求,让客户轻松聚焦于业务发展本身,畅享极速高清播放新体验。
此项目提供了点播播放器和直播播放器SDK,您可以基于播放器搭建自己的播放业务:
此项目提供了点播播放和直播播放,您可以基于播放器搭建自己的播放业务:
- [点播播放器SDK](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%82%B9%E6%92%AD%E6%92%AD%E6%94%BE%E5%99%A8.md)`TXVodPlayerController`对Android和iOS两个平台的点播播放器SDK进行接口封装, 你可以通过集成`TXVodPlayerController`进行点播播放业务开发。详细使用例子可以参考`DemoTXVodPlayer`
- [点播播放](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%82%B9%E6%92%AD%E6%92%AD%E6%94%BE.md)`TXVodPlayerController`对Android和iOS两个平台的点播播放器SDK进行接口封装, 你可以通过集成`TXVodPlayerController`进行点播播放业务开发。详细使用例子可以参考`DemoTXVodPlayer`
- [直播播放器SDK](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%9B%B4%E6%92%AD%E6%92%AD%E6%94%BE%E5%99%A8.md)`TXLivePlayerController`对Android和iOS两个平台的直播播放器SDK进行接口封装, 你可以通过集成`TXLivePlayerController`进行直播播放业务开发。详细使用例子可以参考`DemoTXLivePlayer`
- [直播播放](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E7%9B%B4%E6%92%AD%E6%92%AD%E6%94%BE.md)`TXLivePlayerController`对Android和iOS两个平台的直播播放器SDK进行接口封装, 你可以通过集成`TXLivePlayerController`进行直播播放业务开发。详细使用例子可以参考`DemoTXLivePlayer`
为了减少接入成本, 在example里提供了超级播放器组件(带UI的播放器),基于超级播放器简单的几行代码就可以搭建视频播放业务。您可以根据自己项目的需求, 把超级播放器的相关代码应用到项目中去,根据需求进行调整UI和交互细节。
为了减少接入成本, 在example里提供了播放器组件(带UI的播放器),基于播放器组件简单的几行代码就可以搭建视频播放业务。您可以根据自己项目的需求, 把播放组件的相关代码应用到项目中去,根据需求进行调整UI和交互细节。
- [超级播放器组件](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E8%B6%85%E7%BA%A7%E6%92%AD%E6%94%BE%E5%99%A8.md)`SuperPlayerController` 超级播放器组件,对点播和直播SDK进行了二次封装,可以方便你快速简单集成。目前是Beta版本,功能还在完善中。详细使用例子可以参考`DemoSuperplayer`
- [播放器组件](https://github.com/LiteAVSDK/Player_Flutter/blob/main/Flutter/docs/%E6%92%AD%E6%94%BE%E5%99%A8%E7%BB%84%E4%BB%B6.md)`SuperPlayerController` 播放器组件,对点播和直播进行了二次封装,可以方便你快速简单集成。目前是Beta版本,功能还在完善中。详细使用例子可以参考`DemoSuperplayer`
## 阅读对象
......@@ -52,7 +52,7 @@
```yaml
super_player:
git:
url: https://github.com/tencentyun/SuperPlayer
url: https://github.com/LiteAVSDK/Player_Flutter
path: Flutter
```
......@@ -61,7 +61,7 @@ super_player:
```yaml
super_player:
git:
url: https://github.com/tencentyun/SuperPlayer
url: https://github.com/LiteAVSDK/Player_Flutter
path: Flutter
ref: Professional
```
......@@ -140,9 +140,9 @@ String licenceKey = ""; // 获取到的 licence key
SuperPlayerPlugin.setGlobalLicense(licenceURL, licenceKey);
```
## 点播播放使用
## 点播播放使用
点播播放核心类`TXVodPlayerController`,详细Demo可参考`DemoTXVodPlayer`
点播播放核心类`TXVodPlayerController`,详细Demo可参考`DemoTXVodPlayer`
```dart
import 'package:flutter/material.dart';
......@@ -191,9 +191,9 @@ class _TestState extends State<Test> {
}
}
```
## 超级播放器使用
## 播放器组件使用
超级播放器核心类`SuperPlayerVideo`,创建后即可播放视频。
播放器组件核心类`SuperPlayerVideo`,创建后即可播放视频。
```dart
import 'package:flutter/material.dart';
......@@ -324,11 +324,11 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
腾讯云播放器SDK Flutter插件对原生播放器能力进行了封装, 如果您要进行深度定制开发,建议采用如下方法:
- 基于点播播放器SDK,接口类为`TXVodPlayerController` 或直播播放器SDK,接口类为`TXLivePlayerController`,进行定制开发,项目中提供了定制开发Demo,可参考example工程里的`DemoTXVodPlayer``DemoTXLivePlayer`
- 基于点播播放,接口类为`TXVodPlayerController` 或直播播放,接口类为`TXLivePlayerController`,进行定制开发,项目中提供了定制开发Demo,可参考example工程里的`DemoTXVodPlayer``DemoTXLivePlayer`
- 超级播放器组件`SuperPlayerController` 对播放器SDK进行了封装,同时提供了简单的UI交互, 由于此部分代码在example目录。如果您有对超级播放器组件定制化的需求,您可以进行如下操作:
- 播放器组件`SuperPlayerController` 对点播和直播进行了封装,同时提供了简单的UI交互, 由于此部分代码在example目录。如果您有对播放器组件定制化的需求,您可以进行如下操作:
超级播放器组件相关的代码,代码目录:`exmple/lib/superplayer`,复制到您的项目中,进行定制化开发。
把播放器组件相关的代码,代码目录:`exmple/lib/superplayer`,复制到您的项目中,进行定制化开发。
## 文档链接
......
package com.tencent.vod.flutter;
import android.os.Bundle;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class CommonUtil {
public static Map<String, Object> getParams(int event, Bundle bundle) {
Map<String, Object> param = new HashMap<>();
if (event != 0) {
param.put("event", event);
}
if (bundle != null && !bundle.isEmpty()) {
Set<String> keySet = bundle.keySet();
for (String key : keySet) {
Object val = bundle.get(key);
param.put(key, val);
}
}
return param;
}
}
// Copyright (c) 2022 Tencent. All rights reserved.
package com.tencent.vod.flutter;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* 音频管理
*/
public class FTXAudioManager {
private final static String TAG = "FTXAudioManager";
private AudioManager mAudioManager;
private AudioFocusRequest mFocusRequest;
private AudioAttributes mAudioAttributes;
private int volumeUIFlag = 0;
private List<AudioFocusChangeListener> mAudioFocusListeners = new ArrayList<>();
AudioManager.OnAudioFocusChangeListener afChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
//长时间丢失焦点,当其他应用申请的焦点为AUDIOFOCUS_GAIN时,会触发此回调事件
//例如播放QQ音乐,网易云音乐等
//此时应当暂停音频并释放音频相关的资源。
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
onAudioFocusPause();
}
});
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
//短暂性丢失焦点,当其他应用申请AUDIOFOCUS_GAIN_TRANSIENT或AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE时,会触发此回调事件
//例如播放短视频,拨打电话等。
//通常需要暂停音乐播放
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
onAudioFocusPause();
}
});
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
//短暂性丢失焦点并作降音处理,当其他应用申请AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时,会触发此回调事件
//通常需要降低音量
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
onAudioFocusPause();
}
});
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
//当其他应用申请焦点之后又释放焦点会触发此回调
//可重新播放音乐
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
onAudioFocusPlay();
}
});
}
}
};
public FTXAudioManager(Context context) {
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
public void setVolumeUIVisible(boolean visible) {
if (visible) {
volumeUIFlag = AudioManager.FLAG_SHOW_UI;
} else {
volumeUIFlag = 0;
}
}
public float getSystemCurrentVolume() {
int curVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
return (float) curVolume / maxVolume;
}
public void setSystemVolume(Double volume) {
if (null != volume) {
if (volume < 0) {
volume = 0d;
}
if (volume > 1) {
volume = 1d;
}
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
int newVolume = (int) (volume * maxVolume);
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newVolume, volumeUIFlag);
}
}
public void abandonAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (mFocusRequest != null) {
mAudioManager.abandonAudioFocusRequest(mFocusRequest);
}
} else {
if (afChangeListener != null) {
mAudioManager.abandonAudioFocus(afChangeListener);
}
}
}
public void requestAudioFocus() {
int result;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (mFocusRequest == null) {
if (mAudioAttributes == null) {
mAudioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
}
mFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(mAudioAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(afChangeListener)
.build();
}
result = mAudioManager.requestAudioFocus(mFocusRequest);
} else {
result = mAudioManager.requestAudioFocus(afChangeListener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
}
Log.e(TAG, "requestAudioFocus result:" + result);
}
public void addAudioFocusChangedListener(AudioFocusChangeListener listener) {
if(!mAudioFocusListeners.contains(listener)) {
mAudioFocusListeners.add(listener);
}
}
public void removeAudioFocusChangedListener(AudioFocusChangeListener listener) {
mAudioFocusListeners.remove(listener);
}
void onAudioFocusPause() {
for(AudioFocusChangeListener listener : mAudioFocusListeners) {
listener.onAudioFocusPause();
}
}
void onAudioFocusPlay() {
for(AudioFocusChangeListener listener : mAudioFocusListeners) {
listener.onAudioFocusPlay();
}
}
interface AudioFocusChangeListener {
void onAudioFocusPause();
void onAudioFocusPlay();
}
}
package com.tencent.vod.flutter;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import com.tencent.rtmp.downloader.ITXVodPreloadListener;
import com.tencent.rtmp.downloader.TXVodPreloadManager;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class FTXDownloadManager implements MethodChannel.MethodCallHandler{
private FlutterPlugin.FlutterPluginBinding mFlutterPluginBinding;
final private MethodChannel mMethodChannel;
private final EventChannel mEventChannel;
final private FTXPlayerEventSink mEventSink = new FTXPlayerEventSink();
private Handler mMainHandler;
public FTXDownloadManager(FlutterPlugin.FlutterPluginBinding flutterPluginBinding) {
mFlutterPluginBinding = flutterPluginBinding;
mMainHandler= new Handler(mFlutterPluginBinding.getApplicationContext().getMainLooper());
mMethodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent.com/txvodplayer/download/api");
mMethodChannel.setMethodCallHandler(this);
mEventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent.com/txvodplayer/download/event");
mEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
mEventSink.setEventSinkProxy(eventSink);
}
@Override
public void onCancel(Object o) {
mEventSink.setEventSinkProxy(null);
}
});
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if (call.method.equals("startPreLoad")) {
String playUrl = call.argument("playUrl");
int preloadSizeMB = call.argument("preloadSizeMB");
int preferredResolution = call.argument("preferredResolution");
final TXVodPreloadManager downloadManager = TXVodPreloadManager.getInstance(mFlutterPluginBinding.getApplicationContext());
final int retTaskID = downloadManager.startPreload(playUrl, preloadSizeMB, preferredResolution, new ITXVodPreloadListener() {
@Override
public void onComplete(int taskID, String url) {
onCompleteEvent(taskID,url);
}
@Override
public void onError(int taskID, String url, int code, String msg) {
onErrorEvent(taskID, url, msg);
}
});
result.success(retTaskID);
} else if (call.method.equals("stopPreLoad")) {
final TXVodPreloadManager downloadManager = TXVodPreloadManager.getInstance(mFlutterPluginBinding.getApplicationContext());
int taskId = call.argument("taskId");
downloadManager.stopPreload(taskId);
result.success(null);
}
}
private void onCompleteEvent(int taskId, String url) {
Bundle bundle = new Bundle();
bundle.putInt("taskId", taskId);
bundle.putString("url", url);
sendSuccessEvent(CommonUtil.getParams(FTXEvent.EVENT_PREDOWNLOAD_ON_COMPLETE, bundle));
}
private void onErrorEvent(int taskId, String url, String msg) {
Bundle bundle = new Bundle();
bundle.putInt("taskId", taskId);
bundle.putString("url", url);
bundle.putString("msg", msg);
sendSuccessEvent(CommonUtil.getParams(FTXEvent.EVENT_PREDOWNLOAD_ON_ERROR, bundle));
}
private void sendSuccessEvent(final Object event) {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mEventSink.success(event);
}
});
}
public void destroy() {
mMethodChannel.setMethodCallHandler(null);
mEventChannel.setStreamHandler(null);
mMainHandler.getLooper().quitSafely();
}
}
......@@ -8,13 +8,43 @@ public class FTXEvent {
/*
音量变化
*/
public static final int EVENT_VOLUME_CHANGED = 0x01;
public static final int EVENT_VOLUME_CHANGED = 1;
/*
失去音量输出播放焦点
*/
public static final int EVENT_AUDIO_FOCUS_PAUSE = 0x02;
public static final int EVENT_AUDIO_FOCUS_PAUSE = 2;
/*
获得音量输出焦点
*/
public static final int EVENT_AUDIO_FOCUS_PLAY = 0x03;
public static final int EVENT_AUDIO_FOCUS_PLAY = 3;
// 视频预下载完成
public static final int EVENT_PREDOWNLOAD_ON_COMPLETE = 200;
// 视频预下载出错
public static final int EVENT_PREDOWNLOAD_ON_ERROR = 201;
public static final int NO_ERROR = 0;
/**
* pip 事件
*/
// pip广播action
public final static String ACTION_PIP_PLAY_CONTROL = "vodPlayControl";
// pip 操作
public static final String EXTRA_NAME_PLAY_OP = "vodPlayOp";
// pip需要操作的播放器
public static final String EXTRA_NAME_PLAYER_ID = "vodPlayerId";
// 进度回退
public static final int EXTRA_PIP_PLAY_BACK = 101;
// 继续/暂停
public static final int EXTRA_PIP_PLAY_RESUME_OR_PAUSE = 102;
// 进度前进
public static final int EXTRA_PIP_PLAY_FORWARD = 103;
// pip 错误,android版本过低
public static final int ERROR_PIP_LOWER_VERSION = -101;
// pip 错误,画中画权限关闭/设备不支持画中画
public static final int ERROR_PIP_DENIED_PERMISSION = -102;
// pip 错误,当前界面已销毁
public static final int ERROR_PIP_ACTIVITY_DESTROYED = -103;
}
......@@ -116,12 +116,12 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
@Override
public void onPlayEvent(int i, Bundle bundle) {
mEventSink.success(getParams(i, bundle));
mEventSink.success(CommonUtil.getParams(i, bundle));
}
@Override
public void onNetStatus(Bundle bundle) {
mNetStatusSink.success(getParams(0, bundle));
mNetStatusSink.success(CommonUtil.getParams(0, bundle));
}
@Override
......@@ -343,21 +343,4 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
mLivePlayer.setRenderMode(renderMode);
}
}
private Map<String, Object> getParams(int event, Bundle bundle) {
Map<String, Object> param = new HashMap();
if (event != 0) {
param.put("event", event);
}
if (bundle != null && !bundle.isEmpty()) {
Set<String> keySet = bundle.keySet();
for (String key : keySet) {
Object val = bundle.get(key);
param.put(key, val);
}
}
return param;
}
}
......@@ -14,7 +14,6 @@ import com.tencent.rtmp.TXBitrateItem;
import com.tencent.rtmp.TXLiveConstants;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.TXPlayInfoParams;
import com.tencent.rtmp.TXPlayerAuthBuilder;
import com.tencent.rtmp.TXVodPlayConfig;
import com.tencent.rtmp.TXVodPlayer;
......@@ -50,20 +49,63 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
private TXVodPlayer mVodPlayer;
private static final int Uninitialized = -101;
private static final int Uninitialized = -101;
private TextureRegistry.SurfaceTextureEntry mSurfaceTextureEntry;
private boolean mEnableHardwareDecode = true;
private boolean mHardwareDecodeFail = false;
private boolean mEnableHardwareDecode = true;
private boolean mHardwareDecodeFail = false;
private final FTXPIPManager mPipManager;
private FTXPIPManager.PipParams mPipParams;
private final FTXPIPManager.PipCallback pipCallback = new FTXPIPManager.PipCallback() {
@Override
public void onPlayBack() {
boolean isPlaying = isPlaying();
if (isPlaying) {
float backPlayTime = getCurrentPlaybackTime() - 10;
if (backPlayTime < 0) {
backPlayTime = 0;
}
seek(backPlayTime);
}
}
@Override
public void onResumeOrPlay() {
boolean isPlaying = isPlaying();
if (isPlaying) {
pause();
} else {
resume();
}
// isPlaying取反,点击暂停/播放之后,播放状态会变化
mPipManager.updatePipActions(!isPlaying, mPipParams);
}
@Override
public void onPlayForward() {
boolean isPlaying = isPlaying();
if (isPlaying) {
float forwardPlayTime = getCurrentPlaybackTime() + 10;
float duration = mVodPlayer.getDuration();
if (forwardPlayTime > duration) {
forwardPlayTime = duration;
}
seek(forwardPlayTime);
}
}
};
public FTXVodPlayer(FlutterPlugin.FlutterPluginBinding flutterPluginBinding) {
public FTXVodPlayer(FlutterPlugin.FlutterPluginBinding flutterPluginBinding, FTXPIPManager pipManager) {
super();
mPipManager = pipManager;
mFlutterPluginBinding = flutterPluginBinding;
mMethodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent.com/txvodplayer/" + super.getPlayerId());
mMethodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent" +
".com/txvodplayer/" + super.getPlayerId());
mMethodChannel.setMethodCallHandler(this);
mEventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent.com/txvodplayer/event/" + super.getPlayerId());
mEventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent" +
".com/txvodplayer/event/" + super.getPlayerId());
mEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
......@@ -76,7 +118,8 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
}
});
mNetChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent.com/txvodplayer/net/" + super.getPlayerId());
mNetChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "cloud.tencent" +
".com/txvodplayer/net/" + super.getPlayerId());
mNetChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
......@@ -116,6 +159,9 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
mMethodChannel.setMethodCallHandler(null);
mEventChannel.setStreamHandler(null);
mNetChannel.setStreamHandler(null);
if (null != mPipManager) {
mPipManager.releaseCallback(getPlayerId());
}
}
@Override
......@@ -137,7 +183,7 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
bundle.putInt("videoTop", videoTop);
bundle.putInt("videoRight", videoRight);
bundle.putInt("videoBottom", videoBottom);
mEventSink.success(getParams(event, bundle));
mEventSink.success(CommonUtil.getParams(event, bundle));
return;
}
}
......@@ -149,24 +195,31 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
}
} else if (event == TXLiveConstants.PLAY_WARNING_HW_ACCELERATION_FAIL) {
mHardwareDecodeFail = true;
} else if (event == TXLiveConstants.PLAY_EVT_PLAY_END) {
if (null != mPipManager && mPipManager.isInPipMode()) {
mPipManager.updatePipActions(false, mPipParams);
}
}
mEventSink.success(getParams(event, bundle));
mEventSink.success(CommonUtil.getParams(event, bundle));
}
// surface 的大小默认是宽高为1,当硬解失败时或使用软解时,软解会依赖surface的窗口渲染,不更新会导致只有1px的内容
private void setDefaultBufferSizeForSoftDecode(int width, int height) {
mSurfaceTextureEntry.surfaceTexture().setDefaultBufferSize(width, height);
if (mSurface != null) {
mSurface.release();
if (mSurfaceTextureEntry!= null && mSurfaceTextureEntry.surfaceTexture() != null) {
SurfaceTexture surfaceTexture = mSurfaceTextureEntry.surfaceTexture();
surfaceTexture.setDefaultBufferSize(width, height);
if (mSurface != null) {
mSurface.release();
}
mSurface = new Surface(surfaceTexture);
mVodPlayer.setSurface(mSurface);
}
mSurface = new Surface(mSurfaceTextureEntry.surfaceTexture());
mVodPlayer.setSurface(mSurface);
}
@Override
public void onNetStatus(TXVodPlayer txVodPlayer, Bundle bundle) {
mNetStatusSink.success(getParams(0, bundle));
mNetStatusSink.success(CommonUtil.getParams(0, bundle));
}
@Override
......@@ -291,10 +344,21 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
result.success(time);
} else if (call.method.equals("getDuration")) {
float duration = 0;
if(null != mVodPlayer) {
if (null != mVodPlayer) {
duration = mVodPlayer.getDuration();
}
result.success(duration);
} else if (call.method.equals("enterPictureInPictureMode")) {
String playBackAssetPath = call.argument("backIcon");
String playResumeAssetPath = call.argument("playIcon");
String playPauseAssetPath = call.argument("pauseIcon");
String playForwardAssetPath = call.argument("forwardIcon");
mPipManager.addCallback(getPlayerId(), pipCallback);
mPipParams = new FTXPIPManager.PipParams(playBackAssetPath, playResumeAssetPath,
playPauseAssetPath,
playForwardAssetPath, getPlayerId());
int pipResult = mPipManager.enterPip(isPlaying(), mPipParams);
result.success(pipResult);
} else {
result.notImplemented();
}
......@@ -399,10 +463,10 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
List<?> getSupportedBitrates() {
if (mVodPlayer != null) {
ArrayList<TXBitrateItem> bitrates = mVodPlayer.getSupportedBitrates();
ArrayList<Map<Object,Object>> jsons = new ArrayList<>();
ArrayList<Map<Object, Object>> jsons = new ArrayList<>();
for (TXBitrateItem item :
bitrates) {
Map<Object,Object> map = new HashMap<>();
Map<Object, Object> map = new HashMap<>();
map.put("bitrate", item.bitrate);
map.put("width", item.width);
map.put("height", item.height);
......@@ -444,30 +508,13 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
}
}
void setPlayConfig(Map<Object,Object> config) {
void setPlayConfig(Map<Object, Object> config) {
if (mVodPlayer != null) {
TXVodPlayConfig playConfig = FTXTransformation.transformToConfig(config);
mVodPlayer.setConfig(playConfig);
}
}
private Map<String, Object> getParams(int event, Bundle bundle) {
Map<String, Object> param = new HashMap<>();
if (event != 0) {
param.put("event", event);
}
if (bundle != null && !bundle.isEmpty()) {
Set<String> keySet = bundle.keySet();
for (String key : keySet) {
Object val = bundle.get(key);
param.put(key, val);
}
}
return param;
}
float getCurrentPlaybackTime() {
if (mVodPlayer != null) {
return mVodPlayer.getCurrentPlaybackTime();
......@@ -505,7 +552,7 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
void setToken(String token) {
if (mVodPlayer != null) {
if(TextUtils.isEmpty(token)) {
if (TextUtils.isEmpty(token)) {
mVodPlayer.setToken(null);
} else {
mVodPlayer.setToken(token);
......
English| [简体中文](./超级播放器.md)
English| [简体中文](./播放器组件.md)
## SDK Download
......
简体中文| [English](./超级播放器-EN.md)
简体中文| [English](./播放器组件-EN.md)
## SDK 下载
腾讯云视立方 Flutter 超级播放器项目的地址是 [SuperPlayer Flutter](https://github.com/LiteAVSDK/Player_Flutter/tree/main/Flutter)
腾讯云视立方 Flutter 播放器SDK的地址是 [SuperPlayer Flutter](https://github.com/LiteAVSDK/Player_Flutter/tree/main/Flutter)
## 阅读对象
......@@ -10,14 +10,13 @@
## 通过本文你可以学会
* 如何集成腾讯云视立方 Flutter 超级播放器SDK
* 如何使用超级播放器组件进行点播播放
* 如何集成腾讯云视立方 Flutter播放器SDK
* 如何使用播放器组件进行点播播放
## 基础知识
flutter超级播放器是基于flutter点播播放器的扩展,超级播放器相对于点播播放器,集成了更多的功能,包括全屏切换、清晰度切换、
进度条、播放控制、封面标题展示等常用功能,并且相对于点播播放器使用起来更加方便,如果想更加方便快捷的集成flutter视频播放能力,
可以选择flutter超级播放器使用。
flutter播放器组件是基于flutter播放器SDK的扩展,播放器组件对于点播播放器,集成了更多的功能,包括全屏切换、清晰度切换、
进度条、播放控制、封面标题展示等常用功能,并且相对于点播播放器使用起来更加方便,如果想更加方便快捷的集成flutter视频播放能力,可以选择flutter播放器组件使用。
## 集成指引[](id:Guide)
......@@ -449,6 +448,33 @@ String licenseKey = "填入您购买的 license 的 key";
SuperPlayerPlugin.setGlobalLicense(licenceUrl, licenceKey);
```
#### 进入画中画模式
**说明**
调动该方法之后,视频将会进入画中画模式,该模式只支持android 7.0以上,并且支持画中画模式的机型
**接口**
```dart
_controller.enterPictureInPictureMode(
backIcon: "images/superplayer_ic_vod_play_pre.png",
playIcon: "images/ic_pip_play_normal.png",
pauseIcon: "images/ic_pip_play_pause.png",
forwardIcon: "images/superplayer_ic_vod_play_next.png");
```
**参数说明**
该参数只适用于android平台
| 参数名 | 类型 | 描述 |
| ------ | ------ | ------------------ |
| backIcon | String | 回退按钮图标,由于android平台限制,图标大小不得超过1M,可不传,不传则使用系统自带图标 |
| playIcon | String | 播放按钮图标,由于android平台限制,图标大小不得超过1M,可不传,不传则使用系统自带图标 |
| pauseIcon | String | 暂停按钮图标,由于android平台限制,图标大小不得超过1M,可不传,不传则使用系统自带图标 |
| forwardIcon | String | 快进按钮图标,由于android平台限制,图标大小不得超过1M,可不传,不传则使用系统自带图标 |
## 事件通知
#### 播放状态监听
......@@ -536,4 +562,71 @@ loader.getVideoData(model, (resultModel) {
```
#### 画中画模式的使用
##### android平台
1. 添加原生配置
在自己项目的android包下,找到 AndroidManifest.xml ,在项目入口activity节点下,增加如下配置
```xml
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
```
在自己项目android包下,找到build.gralde,确认 compileSdkVersion 和 targetSdkVersion 的版本为31或以上
2. 继承pip activity
将github项目中example/android 中的 FTXFlutterPipActivity.java 复制到自己入口 Activity 的同目录下,并将自己 Activity 的父类修改为该类。
3. 复制superPlayer示例代码
将github项目中example/lib 中的 superplayer 包复制到自己的 lib 目录下,仿照示例代码中的 demo_superplayer.dart 集成好超级播放器。
然后就可以在超级播放器的播放界面右边中间看到画中画模式按钮,点击即可进入画中画模式。
4. 监听画中画模式生命周期
使用 SuperPlayerPlugin 中的 onExtraEventBroadcast 可监听到画中画模式的生命周期,示例代码如下
```dart
SuperPlayerPlugin.instance.onExtraEventBroadcast.listen((event) {
int eventCode = event["event"];
if (eventCode == TXVodPlayEvent.EVENT_PIP_MODE_ALREADY_EXIT) {
// exit pip mode
} else if (eventCode == TXVodPlayEvent.EVENT_PIP_MODE_REQUEST_START) {
// enter pip mode
} else if (eventCode == TXVodPlayEvent.EVENT_PIP_MODE_ALREADY_ENTER) {
// already enter pip mode
}
});
```
5. 画中画模式进入错误码
进入画中画模式失败的时候,除了有log提示以外,还会有toast提示,可在 superplayer_widget.dart 的 _onEnterPipMode 方法内修改错误情况处理。错误码含义如下:
| 参数名 | 值 | 描述 |
| ------ | ------ | ------------------ |
| NO_ERROR | 0 | 启动成功,没有错误 |
| ERROR_PIP_LOWER_VERSION | -101 | android版本过低,不支持画中画模式 |
| ERROR_PIP_DENIED_PERMISSION | -102 | 画中画模式权限未打开,或者当前设备不支持画中画 |
| ERROR_PIP_ACTIVITY_DESTROYED | -103 | 当前界面已经销毁 |
6. 判断当前设备是否支持画中画
使用 SuperPlayerPlugin 中的 isDeviceSupportPip 可判断当前是否能够开启画中画,代码示例如下
```dart
int result = await SuperPlayerPlugin.isDeviceSupportPip();
if(result == TXVodPlayEvent.NO_ERROR) {
// pip support
}
```
result的返回结果的含义和画中画模式错误码一致
English| [简体中文](./点播播放.md)
English| [简体中文](./点播播放.md)
## SDK Download
......
简体中文| [English](./点播播放-EN.md)
简体中文| [English](./点播播放-EN.md)
## SDK 下载
......@@ -69,7 +69,7 @@ flutter pub upgrade
```yaml
super_player:
git:
url: https://github.com/tencentyun/SuperPlayer
url: https://github.com/LiteAVSDK/Player_Flutter
path: Flutter
ref: Professional
```
......
English| [简体中文](./直播播放.md)
English| [简体中文](./直播播放.md)
## Basics
......
......@@ -7,12 +7,16 @@
tools:replace="android:label">
<activity
android:name=".MainActivity"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:exported="true"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="stateHidden">
android:windowSoftInputMode="stateHidden"
tools:ignore="UnusedAttribute">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
......
// Copyright (c) 2022 Tencent. All rights reserved.
package com.example.super_player_example;
import android.app.PictureInPictureParams;
import android.app.PictureInPictureUiState;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.plugin.common.EventChannel;
/**
* 画中画模式activity父类,使用画中画模式,需要将自己项目中的activity修改为继承该类
*/
public class FTXFlutterPipActivity extends FlutterActivity {
private static final String PIP_CHANNEL_NAME = "cloud.tencent.com/playerPlugin/pipEvent";
private static final int EVENT_PIP_MODE_ALREADY_ENTER = 1;
private static final int EVENT_PIP_MODE_ALREADY_EXIT = 2;
private static final int EVENT_PIP_MODE_REQUEST_START = 3;
private static final int EVENT_PIP_MODE_UI_STATE_CHANGED = 4;
private EventChannel mPipEventChannel;
private EventChannel.EventSink mEventSink;
/**
* 这里使用needToExitPip作为标志位,在出现onPictureInPictureModeChanged回调画中画状态和isInPictureInPictureMode不一致的时候。
* 标志位true,然后在onConfigurationChanged监听到界面宽高发生变化的时候,进行画中画模式退出的事件通知。
* for MIUI 12.5.1
*/
private boolean needToExitPip = false;
private int configWidth = 0;
private int configHeight = 0;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPipEventChannel();
}
private void initPipEventChannel() {
if (null == mPipEventChannel && null != getFlutterEngine()) {
mPipEventChannel = new EventChannel(getFlutterEngine().getDartExecutor().getBinaryMessenger(),
PIP_CHANNEL_NAME);
mPipEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
mEventSink = events;
}
@Override
public void onCancel(Object arguments) {
}
});
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
boolean isInPictureInPictureMode = isInPictureInPictureMode();
if (isInPictureInPictureMode) {
configWidth = newConfig.screenWidthDp;
configHeight = newConfig.screenHeightDp;
} else if (needToExitPip && configWidth != newConfig.screenWidthDp && configHeight != newConfig.screenHeightDp) {
if (null != mEventSink) {
mEventSink.success(getParams(EVENT_PIP_MODE_ALREADY_EXIT, null));
}
needToExitPip = false;
}
}
}
/**
* 为了兼容MIUI 12.5,PIP模式下,打开其他app然后上滑退出,再点击画中画窗口,onPictureInPictureModeChanged会异常回调关闭的情况
*
* @param ignore 校对画中画状态
*/
@Override
public void onPictureInPictureModeChanged(boolean ignore) {
boolean isInPictureInPictureMode = ignore;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
isInPictureInPictureMode = isInPictureInPictureMode();
}
if (isInPictureInPictureMode != ignore) {
needToExitPip = true;
} else {
if (null != mEventSink) {
if (isInPictureInPictureMode) {
mEventSink.success(getParams(EVENT_PIP_MODE_ALREADY_ENTER, null));
} else {
mEventSink.success(getParams(EVENT_PIP_MODE_ALREADY_EXIT, null));
}
}
}
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
}
@Override
public void onPictureInPictureUiStateChanged(@NonNull PictureInPictureUiState pipState) {
super.onPictureInPictureUiStateChanged(pipState);
if (null != mEventSink) {
mEventSink.success(getParams(EVENT_PIP_MODE_UI_STATE_CHANGED, null));
}
}
/**
* enterPictureInPictureMode生效后的回调通知,only for android > 31
*/
@Override
public boolean onPictureInPictureRequested() {
return super.onPictureInPictureRequested();
}
@Override
public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) {
if (null != mEventSink) {
mEventSink.success(getParams(EVENT_PIP_MODE_REQUEST_START, null));
}
return super.enterPictureInPictureMode(params);
}
private Map<String, Object> getParams(int event, Bundle bundle) {
Map<String, Object> param = new HashMap<>();
if (event != 0) {
param.put("event", event);
}
if (bundle != null && !bundle.isEmpty()) {
Set<String> keySet = bundle.keySet();
for (String key : keySet) {
Object val = bundle.get(key);
param.put(key, val);
}
}
return param;
}
}
// Copyright (c) 2022 Tencent. All rights reserved.
package com.example.super_player_example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
public class MainActivity extends FTXFlutterPipActivity {
}
......@@ -50,7 +50,7 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
? null
: AppBar(
backgroundColor: Colors.transparent,
title: const Text('SuperPlayer'),
title: const Text('播放器组件'),
),
body: SafeArea(
child: Builder(
......@@ -74,6 +74,7 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
Widget _getPlayArea() {
return Container(
decoration: BoxDecoration(color: Colors.black),
height: 220,
child: SuperPlayerView(_controller),
);
......@@ -92,15 +93,21 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
Widget _buildVideoItem(SuperPlayerModel playModel) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListTile(
leading: Image.network(playModel.coverUrl),
leading: Image.network(
playModel.coverUrl,
width: 100,
height: 60,
fit: BoxFit.cover,
alignment: Alignment.center,
),
title: new Text(
playModel.title,
style: TextStyle(color: Colors.white),
),
onTap: () => playCurrentModel(playModel)),
onTap: () => playCurrentModel(playModel),
horizontalTitleGap: 10,),
Divider()
],
);
......
......@@ -128,7 +128,7 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text('直播'),
title: const Text('直播播放'),
),
body: SafeArea(
child: Column(
......
......@@ -118,7 +118,7 @@ class _DemoTXVodlayerState extends State<DemoTXVodPlayer>
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text('点播'),
title: const Text('点播播放'),
),
body: SafeArea(
child: Container(
......
......@@ -20,12 +20,14 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
String? _liteAVSdkVersion = 'Unknown';
@override
void initState() {
super.initState();
initPlatformState();
initPlayerLicense();
_getflutterSdkVersion();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
LogUtils.logOpen = true;
}
......@@ -57,6 +59,13 @@ class _MyAppState extends State<MyApp> {
});
}
Future<void> _getflutterSdkVersion() async {
String? liteavSdkVersion = await SuperPlayerPlugin.getLiteAVSDKVersion();
setState(() {
_liteAVSdkVersion = liteavSdkVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
......@@ -71,13 +80,24 @@ class _MyAppState extends State<MyApp> {
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text('腾讯视频云', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w400),),
title: const Text('腾讯云Flutter播放器', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w400),),
),
body: Builder(
builder: (context) {
return Container(
color: Colors.transparent,
child: TreePage(),
child: Stack(
children: [
TreePage(),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: new EdgeInsets.all(20.0) ,
child: Text('LiteAVSDKVersion: ${_liteAVSdkVersion}'),
)
),
],
),
);
}
),
......
......@@ -17,6 +17,21 @@ class ThemeResource {
));
}
/// 获得通用进度条样式
static ThemeData getMiniSliderTheme() {
return ThemeData(
sliderTheme: SliderThemeData(
trackHeight: 1,
thumbColor: Color(ColorResource.COLOR_MAIN_THEME),
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 0, disabledThumbRadius: 0, elevation: 0, pressedElevation: 0),
overlayColor: Colors.white,
overlayShape: RoundSliderOverlayShape(overlayRadius: 1),
activeTrackColor: Color(ColorResource.COLOR_MAIN_THEME),
inactiveTrackColor: Color(ColorResource.COLOR_GRAY),
));
}
static TextStyle getCommonLabelTextStyle() {
return TextStyle(fontSize: 14, color: Colors.white);
}
......
......@@ -29,6 +29,7 @@ part 'ui/superplayer_title_view.dart';
part 'ui/superplayer_widget.dart';
part 'ui/superplayer_cover_view.dart';
part 'ui/superplayer_more_view.dart';
part 'ui/superplayer_video_slider.dart';
part 'common/color_resource.dart';
part 'common/string_resource.dart';
part 'common/theme_resource.dart';
\ No newline at end of file
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_super_player_lib;
/// superplayer play controller
class SuperPlayerController {
static const TAG = "SuperPlayerController";
......@@ -11,6 +12,7 @@ class SuperPlayerController {
/// simple event,see SuperPlayerViewEvent
Stream<Map<dynamic, dynamic>> get onSimplePlayerEventBroadcast => _simpleEventStreamController.stream;
/// player net status,see TXVodNetEvent
Stream<Map<dynamic, dynamic>> get onPlayerNetStatusBroadcast => _playerNetStatusStreamController.stream;
......@@ -53,7 +55,7 @@ class SuperPlayerController {
void _initVodPlayer() async {
_vodPlayerController = new TXVodPlayerController();
await _vodPlayerController?.initialize();
_vodPlayerController?.onPlayerEventBroadcast?.listen((event) async {
_vodPlayerController?.onPlayerEventBroadcast.listen((event) async {
int eventCode = event['event'];
switch (eventCode) {
case TXVodPlayEvent.PLAY_EVT_VOD_PLAY_PREPARED: // vodPrepared
......@@ -136,7 +138,7 @@ class SuperPlayerController {
videoDuration = duration.toInt(); // 总播放时长,转换后的单位 秒
}
if (videoDuration != 0) {
_observer?.onPlayProgress(currentDuration, videoDuration);
_observer?.onPlayProgress(currentDuration, videoDuration, await getPlayableDuration());
}
break;
}
......@@ -197,14 +199,14 @@ class SuperPlayerController {
}
Future<void> _sendRequest() async {
_currentProtocol?.sendRequest((protocol, resultModel) {
_currentProtocol?.sendRequest((protocol, resultModel) async {
// onSuccess
if (videoModel != resultModel) {
return;
}
_playModeVideo(protocol);
_updatePlayerType(SuperPlayerType.VOD);
_observer?.onPlayProgress(0, resultModel.duration);
_observer?.onPlayProgress(0, resultModel.duration, await getPlayableDuration());
_observer?.onVideoImageSpriteAndKeyFrameChanged(protocol.getImageSpriteInfo(), protocol.getKeyFrameDescInfo());
}, (errCode, message) {
// onError
......@@ -213,6 +215,11 @@ class SuperPlayerController {
});
}
Future<double> getPlayableDuration() async {
double? playableDuration = await _vodPlayerController?.getPlayableDuration();
return playableDuration ?? 0;
}
void _playModeVideo(PlayInfoProtocol protocol) {
String? videoUrl = protocol.getUrl();
_playVodUrl(videoUrl);
......@@ -497,6 +504,18 @@ class SuperPlayerController {
await _vodPlayerController?.setConfig(config);
}
/// 进入画中画模式,进入画中画模式,需要适配画中画模式的界面,安卓只支持7.0以上机型
/// <h1>
/// 由于android系统限制,传递的图标大小不得超过1M,否则无法显示
/// </h1>
/// @param backIcon playIcon pauseIcon forwardIcon 为播放后退、播放、暂停、前进的图标,如果赋值的话,将会使用传递的图标,否则
/// 使用系统默认图标,只支持flutter本地资源图片,传递的时候,与flutter使用图片资源一致,例如: images/back_icon.png
Future<int?> enterPictureInPictureMode(
{String? backIcon, String? playIcon, String? pauseIcon, String? forwardIcon}) async {
return _vodPlayerController?.enterPictureInPictureMode(
backIcon: backIcon, playIcon: playIcon, pauseIcon: pauseIcon, forwardIcon: forwardIcon);
}
/// 开关硬解编码播放
Future<void> enableHardwareDecode(bool enable) async {
_isOpenHWAcceleration = enable;
......
......@@ -8,7 +8,7 @@ class _SuperPlayerObserver {
Function onPlayPause;
Function onPlayStop;
Function onPlayLoading;
Function(int current, int duration) onPlayProgress;
Function(int current, int duration,double playableDuration) onPlayProgress;
Function(double position) onSeek;
Function(bool success, SuperPlayerType playerType, VideoQuality quality) onSwitchStreamStart;
Function(bool success, SuperPlayerType playerType, VideoQuality quality) onSwitchStreamEnd;
......@@ -42,5 +42,5 @@ class _SuperPlayerObserver {
this.onVideoQualityListChange,
this.onVideoImageSpriteAndKeyFrameChanged,
this.onSysBackPress,
this.onDispose);
this.onDispose,);
}
......@@ -18,11 +18,14 @@ class _VideoBottomViewState extends State<VideoBottomView> {
static const TAG = "VideoBottomView";
double _currentProgress = 0;
double _playableProgress = 0;
int _currentDuration = 0;
int _videoDuration = 0;
double _bufferedDuration = 0;
bool _showFullScreenBtn = true;
bool _isPlayMode = false;
bool _isShowQuality = false; // only showed on fullscreen mode
bool _isOnDraging = false;
VideoQuality? _currentQuality;
@override
......@@ -123,26 +126,31 @@ class _VideoBottomViewState extends State<VideoBottomView> {
Widget _getSlider() {
return Expanded(
child: Theme(
data: ThemeResource.getCommonSliderTheme(),
child: Slider(
min: 0,
max: 1,
value: _currentProgress,
onChanged: (double value) {
setState(() {
_currentProgress = value;
});
},
onChangeEnd: (double value) {
setState(() {
_currentProgress = value;
widget._playerController.seek(_currentProgress * _videoDuration);
LogUtils.d(TAG,
"_currentProgress:$_currentProgress,_videoDuration:$_videoDuration,currentDuration:${_currentProgress * _videoDuration}");
});
},
)),
child: VideoSlider(
min: 0,
max: 1,
value: _currentProgress,
bufferedValue: _playableProgress,
activeColor: Color(ColorResource.COLOR_MAIN_THEME),
inactiveColor: Color(ColorResource.COLOR_GRAY),
sliderColor: Color(ColorResource.COLOR_MAIN_THEME),
sliderOutterColor: Colors.white,
progressHeight: 2,
sliderRadius: 4,
sliderOutterRadius: 10,
onDragUpdate: (value) {
_isOnDraging = true;
},
onDragEnd: (value) {
setState(() {
_isOnDraging = false;
_currentProgress = value;
widget._playerController.seek(_currentProgress * _videoDuration);
LogUtils.d(TAG,
"_currentProgress:$_currentProgress,_videoDuration:$_videoDuration,currentDuration:${_currentProgress * _videoDuration}");
});
},
),
);
}
......@@ -158,12 +166,16 @@ class _VideoBottomViewState extends State<VideoBottomView> {
widget._controller.onTapQuality();
}
void updateDuration(int duration, int videoDuration) {
if (duration != _currentDuration || _videoDuration != videoDuration) {
void updateDuration(int duration, int videoDuration, double bufferedDration) {
if (_isOnDraging) {
return;
}
if (duration != _currentDuration || _videoDuration != videoDuration || _bufferedDuration != bufferedDration) {
if (duration <= videoDuration) {
setState(() {
_currentDuration = duration;
_videoDuration = videoDuration;
_bufferedDuration = bufferedDration;
_fixProgress();
});
}
......@@ -172,17 +184,30 @@ class _VideoBottomViewState extends State<VideoBottomView> {
void _fixProgress() {
// provent division zero problem
if(_videoDuration == 0) {
if (_videoDuration == 0) {
_currentProgress = 0;
} else {
_currentProgress = _currentDuration / _videoDuration;
}
if(_currentProgress < 0) {
if (_currentProgress < 0) {
_currentProgress = 0;
}
if(_currentProgress > 1) {
if (_currentProgress > 1) {
_currentProgress = 1;
}
if (_bufferedDuration == 0) {
_playableProgress = 0;
} else {
_playableProgress = _bufferedDuration / _videoDuration;
}
if (_playableProgress < 0) {
_playableProgress = 0;
}
if (_playableProgress > 1) {
_playableProgress = 1;
}
}
void updatePlayState(bool playing) {
......
// Copyright (c) 2022 Tencent. All rights reserved.
part of demo_super_player_lib;
/// 视频进度条,双进度
class VideoSlider extends StatefulWidget {
late final _VideoSliderController controller;
final double? progressHeight;
final double min;
final double max;
final double value;
final double? bufferedValue;
final double? sliderRadius;
final double? sliderOutterRadius;
final Color? activeColor;
final Color? inactiveColor;
final Color? bufferedColor;
final Color? sliderColor;
final Color? sliderOutterColor;
// calback
final Function? onDragStart;
final Function(double value)? onDragUpdate;
final Function(double value)? onDragEnd;
VideoSlider(
{this.progressHeight,
required this.min,
required this.max,
required this.value,
this.bufferedValue,
this.sliderRadius,
this.sliderOutterRadius,
this.activeColor,
this.inactiveColor,
this.bufferedColor,
this.sliderColor,
this.sliderOutterColor,
this.onDragStart,
this.onDragUpdate,
this.onDragEnd}) {
double range = (max - min);
_checkRange(range, valueName: "max - min");
double currentProgress = value / range;
double? bufferedProgress = bufferedValue != null ? bufferedValue! / range : null;
controller = _VideoSliderController(currentProgress, bufferedProgress: bufferedProgress);
}
@override
State<StatefulWidget> createState() => VideoSliderState();
}
class VideoSliderState extends State<VideoSlider> {
late _VideoSliderShaders shaders;
final defaultHeight = 10.0;
final defaultRadius = 5.0;
bool isDraging = false;
@override
void initState() {
super.initState();
shaders = _VideoSliderShaders(
backgroundColor: widget.inactiveColor,
progressColor: widget.activeColor,
dragSliderColor: widget.sliderColor,
bufferedColor: widget.bufferedColor,
drawSliderOverlayColor: widget.sliderOutterColor);
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
double height = widget.progressHeight ?? defaultHeight;
double radius = widget.sliderRadius ?? defaultRadius;
double overlayRadius = widget.sliderOutterRadius ?? radius * 3;
double leftPadding = overlayRadius;
double rightPadding = overlayRadius;
return GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
isDraging = true;
widget.onDragStart?.call();
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
isDraging = true;
_seekToPosition(details.globalPosition);
widget.onDragUpdate?.call(widget.controller.progress);
},
onHorizontalDragEnd: (DragEndDetails details) {
isDraging = false;
widget.onDragEnd?.call(widget.controller.progress);
},
onHorizontalDragCancel: () {
isDraging = false;
widget.onDragEnd?.call(widget.controller.progress);
},
onTapDown: (TapDownDetails details) {
_seekToPosition(details.globalPosition);
},
child: Center(
child: Container(
width: size.width,
height: max(height, 2 * radius),
child: CustomPaint(
painter: _VideoSliderPainter(
shaders: shaders,
progressHeight: height,
sliderRadius: radius,
isDraging: isDraging,
percent: widget.controller.progress,
bufferedPercent: widget.controller.bufferedProgress,
leftPadding: leftPadding,
rightPadding: rightPadding,
sliderOverlayRadius: overlayRadius),
),
),
),
);
}
void _seekToPosition(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;
setState(() {
widget.controller.progress = progress;
});
}
}
class _VideoSliderPainter extends CustomPainter {
final _VideoSliderShaders shaders;
final double progressHeight;
final double sliderRadius;
double sliderOverlayRadius;
final double percent; // must range in 0.0 ~ 1.0
final double? bufferedPercent; // must range in 0.0 ~ 1.0
final bool isDraging;
final double leftPadding;
final double rightPadding;
_VideoSliderPainter(
{required this.shaders,
required this.progressHeight,
required this.sliderRadius,
required this.percent,
required this.isDraging,
required this.sliderOverlayRadius,
required this.leftPadding,
required this.rightPadding,
this.bufferedPercent});
@override
void paint(Canvas canvas, Size size) {
final baseVerticalOffset = size.height / 2 - progressHeight / 2;
final start = leftPadding;
final end = size.width - rightPadding;
final width = size.width - leftPadding - rightPadding;
// draw background
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(Offset(start, baseVerticalOffset), Offset(end, baseVerticalOffset + progressHeight)),
Radius.circular(sliderRadius)),
shaders.backgroundPaint);
// draw bufferdProgress
if (null != bufferedPercent) {
_checkRange(bufferedPercent!);
final double bPercent = bufferedPercent!;
double bufferedEndless = start + (width * bPercent);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(start, baseVerticalOffset), Offset(bufferedEndless, baseVerticalOffset + progressHeight)),
Radius.circular(sliderRadius)),
shaders.bufferedPaint);
}
// draw progress
_checkRange(percent);
final double ppercent = percent;
double progressEndless = start + (width * ppercent);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(start, baseVerticalOffset), Offset(progressEndless, baseVerticalOffset + progressHeight)),
Radius.circular(sliderRadius)),
shaders.progressPaint);
// draw outer slider,only show when drag
if (isDraging) {
canvas.drawCircle(Offset(progressEndless, size.height / 2), sliderOverlayRadius, shaders.dragSliderOverlayPaint);
}
// draw inner slider
canvas.drawCircle(Offset(progressEndless, size.height / 2), sliderRadius, shaders.dragSliderPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class _VideoSliderShaders {
Paint backgroundPaint = Paint();
Paint bufferedPaint = Paint();
Paint progressPaint = Paint();
Paint dragSliderPaint = Paint();
Paint dragSliderOverlayPaint = Paint();
_VideoSliderShaders(
{Color? backgroundColor,
Color? progressColor,
Color? dragSliderColor,
Color? bufferedColor,
Color? drawSliderOverlayColor}) {
backgroundPaint.color = backgroundColor ?? Colors.grey;
bufferedPaint.color = bufferedColor ?? Colors.blueGrey;
progressPaint.color = progressColor ?? Colors.blueAccent;
dragSliderPaint.color = dragSliderColor ?? Colors.blue;
dragSliderOverlayPaint.color = drawSliderOverlayColor ?? Colors.white;
}
}
class _VideoSliderController {
double progress;
double? bufferedProgress;
_VideoSliderController(this.progress, {this.bufferedProgress});
}
void _checkRange(double value, {String? valueName}) {
if (value < 0.0 || value > 1.0) {
throw ArgumentError("${valueName ?? "value"} must range in 0.0 to 1.0,please check your param");
}
}
......@@ -42,32 +42,11 @@ class _TreePageState extends State<TreePage> {
TreeData([
TreeDatachild("直播播放"),
TreeDatachild("点播播放"),
TreeDatachild("超级播放器"),
TreeDatachild("播放器组件"),
], "播放器", false),
];
// getHttp();
}
// void getHttp() async {
// try {
// var response = await HttpUtil().get(Api.TREE);
// Map userMap = json.decode(response.toString());
// var treeEntity = TreeEntity.fromJson(userMap);
//
// //遍历赋值isExpanded标识,默认全部合并
// for (int i = 0; i < treeEntity.data.length; i++) {
// treeEntity.data[i].isExpanded = false;
// }
//
// setState(() {
// _datas = treeEntity.data;
// });
// } catch (e) {
// print(e);
// }
// }
@override
Widget build(BuildContext context) {
return Scaffold(
......
......@@ -44,6 +44,11 @@ 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/superplayer_ic_vod_play_next.png
- images/superplayer_ic_vod_play_pre.png
- images/superplayer_ic_vod_more_normal.png
- images/superplayer_ic_light_max.png
- images/superplayer_ic_light_min.png
......
// Copyright (c) 2022 Tencent. All rights reserved.
#import <MediaPlayer/MediaPlayer.h>
#import <AVFAudio/AVAudioSession.h>
@interface FTXAudioManager : NSObject
- (float)getVolume;
- (void)setVolume:(float)value;
- (void)setVolumeUIVisible:(BOOL)volumeUIVisible;
- (void)registerVolumeChangeListener:(_Nonnull id)observer selector:(_Nonnull SEL)aSelector
name:(nullable NSNotificationName)aName object:(nullable id)anObject;
- (void)destory:(_Nonnull id)observer name:(nullable NSNotificationName)aName
object:(nullable id)anObject;
@end
// Copyright (c) 2022 Tencent. All rights reserved.
#import "FTXAudioManager.h"
#import <Foundation/Foundation.h>
@implementation FTXAudioManager
UISlider *_volumeSlider;
MPVolumeView *volumeView;
- (instancetype)init
{
if(self = [super init]) {
CGRect frame = CGRectMake(0, -100, 10, 0);
volumeView = [[MPVolumeView alloc] initWithFrame:frame];
volumeView.hidden = YES;
[volumeView sizeToFit];
// 单例slider
_volumeSlider = nil;
for (UIView *view in [volumeView subviews]) {
if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
_volumeSlider = (UISlider *)view;
break;
}
}
}
return self;
};
- (float)getVolume
{
return _volumeSlider.value > 0 ? _volumeSlider.value : [[AVAudioSession sharedInstance]outputVolume];
}
- (void)setVolume:(float)value
{
// 需要设置 showsVolumeSlider 为 YES
volumeView.showsVolumeSlider = YES;
[_volumeSlider setValue:value animated:NO];
[_volumeSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
[_volumeSlider sizeToFit];
}
- (void)setVolumeUIVisible:(BOOL)volumeUIVisible
{
volumeView.hidden = !volumeUIVisible;
}
- (void)registerVolumeChangeListener:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName object:(id)anObject
{
[[NSNotificationCenter defaultCenter] addObserver:observer selector:aSelector name:aName object:anObject];
}
- (void)destory:(id)observer name:(NSNotificationName)aName object:(id)anObject
{
// destory volume view
[volumeView removeFromSuperview];
// destory volume observer
[[NSNotificationCenter defaultCenter] removeObserver:observer name:aName object:anObject];
}
@end
// Copyright (c) 2022 Tencent. All rights reserved.
#import <Foundation/Foundation.h>
@protocol FlutterPluginRegistrar;
@interface FTXDownloadManager : NSObject
- (instancetype)initWithRegistrar:(id<FlutterPluginRegistrar>)registrar;
- (void)destroy;
@end
// Copyright (c) 2022 Tencent. All rights reserved.
#import "SuperPlayerPlugin.h"
#import "FTXPlayerEventSinkQueue.h"
#import "FTXEvent.h"
#import "FTXDownloadManager.h"
#import "TXVodPreloadManager.h"
#import "FTXEvent.h"
@interface FTXDownloadManager ()<FlutterStreamHandler, TXVodPreloadManagerDelegate>
@end
@implementation FTXDownloadManager {
FlutterMethodChannel *_methodChannel;
FlutterEventChannel *_eventChannel;
FTXPlayerEventSinkQueue *_eventSink;
}
- (instancetype)initWithRegistrar:(id<FlutterPluginRegistrar>)registrar
{
if (self = [self init]) {
__weak typeof(self) weakSelf = self;
_methodChannel = [FlutterMethodChannel methodChannelWithName:@"cloud.tencent.com/txvodplayer/download/api" binaryMessenger:[registrar messenger]];
[_methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
[weakSelf handleMethodCall:call result:result];
}];
_eventSink = [FTXPlayerEventSinkQueue new];
_eventChannel = [FlutterEventChannel eventChannelWithName:@"cloud.tencent.com/txvodplayer/download/event" binaryMessenger:[registrar messenger]];
[_eventChannel setStreamHandler:self];
NSLog(@"dokie initWithRegistrar");
}
return self;
}
- (void)destroy
{
[_methodChannel setMethodCallHandler:nil];
_methodChannel = nil;
[_eventChannel setStreamHandler:nil];
_eventChannel = nil;
[_eventSink setDelegate:nil];
_eventSink = nil;
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
NSDictionary *args = call.arguments;
if([@"startPreLoad" isEqualToString:call.method]) {
NSString *playUrl = args[@"playUrl"];
int preloadSizeMB = [args[@"preloadSizeMB"] intValue];
int preferredResolution = [args[@"preferredResolution"] intValue];
int taskID = [[TXVodPreloadManager sharedManager] startPreload:playUrl
preloadSize:preloadSizeMB
preferredResolution:preferredResolution
delegate:self];
result(@(taskID));
} else if([@"stopPreLoad" isEqualToString:call.method]) {
int taskId = [args[@"taskId"] intValue];
[[TXVodPreloadManager sharedManager] stopPreload:taskId];
result(nil);
}
}
#pragma mark - FlutterStreamHandler
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
eventSink:(FlutterEventSink)events
{
if ([arguments isKindOfClass:NSString.class]) {
if ([arguments isEqualToString:@"event"]) {
[_eventSink setDelegate:events];
}
}
return nil;
}
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments
{
if ([arguments isKindOfClass:NSString.class]) {
if ([arguments isEqualToString:@"event"]) {
[_eventSink setDelegate:nil];
}
}
return nil;
}
+ (NSDictionary *)getParamsWithEvent:(int)EvtID withParams:(NSDictionary *)params
{
NSMutableDictionary<NSString*,NSObject*> *dict = [NSMutableDictionary dictionaryWithObject:@(EvtID) forKey:@"event"];
if (params != nil && params.count != 0) {
[dict addEntriesFromDictionary:params];
}
return dict;
}
#pragma mark - TXVodPreloadManager delegate
- (void)onComplete:(int)taskID url:(NSString *)url
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@(taskID) forKey:@"taskId"];
[dict setObject:url forKey:@"url"];
[_eventSink success:[FTXDownloadManager getParamsWithEvent:EVENT_PREDOWNLOAD_ON_COMPLETE withParams:dict]];
}
- (void)onError:(int)taskID url:(NSString *)url error:(NSError *)error
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:@(taskID) forKey:@"taskId"];
[dict setObject:url forKey:@"url"];
[dict setObject:@(error.code) forKey:@"code"];
if (nil != error.userInfo.description) {
[dict setObject:error.userInfo.description forKey:@"msg"];
}
[_eventSink success:[FTXDownloadManager getParamsWithEvent:EVENT_PREDOWNLOAD_ON_ERROR withParams:dict]];
}
@end
......@@ -7,4 +7,8 @@
#define EVENT_AUDIO_FOCUS_PAUSE 0x02
#define EVENT_AUDIO_FOCUS_PLAY 0x03
// 视频预下载完成
#define EVENT_PREDOWNLOAD_ON_COMPLETE 200
// 视频预下载出错
#define EVENT_PREDOWNLOAD_ON_ERROR 201
#endif /* FTXEvent_h */
......@@ -8,7 +8,4 @@ static int maxCacheItems = -1;
+ (TXVodPlayConfig *)transformToConfig:(NSDictionary*)map;
+ (void)setCacheFolder:(NSString*)path;
+ (void)setMaxCacheItemSize:(int)size;
@end
......@@ -2,18 +2,7 @@
#import <Foundation/Foundation.h>
#import "FTXTransformation.h"
@implementation FTXTransformation
+ (void)setMaxCacheItemSize:(int)size
{
maxCacheItems = size;
}
+ (void)setCacheFolder:(NSString *)path
{
cacheFolder = path;
}
+ (TXVodPlayConfig *)transformToConfig:(NSDictionary *)args
{
......@@ -36,13 +25,6 @@
NSString *preferredResolutionStr = args[@"config"][@"preferredResolution"];
playConfig.preferredResolution = [preferredResolutionStr longLongValue];
if(maxCacheItems > 0) {
playConfig.maxCacheItems = maxCacheItems;
}
if(cacheFolder != nil && cacheFolder.length > 0) {
playConfig.cacheFolderPath = cacheFolder;
}
NSString *overlayKey = args[@"config"][@"overlayKey"];
if(overlayKey != nil && overlayKey.length > 0) {
playConfig.overlayKey = overlayKey;
......
......@@ -7,6 +7,8 @@
#import "FTXEvent.h"
#import <MediaPlayer/MediaPlayer.h>
#import <TXLiteAVSDK_Professional/TXLiteAVSDK.h>
#import "FTXAudioManager.h"
#import "FTXDownloadManager.h"
@interface SuperPlayerPlugin ()<FlutterStreamHandler>
......@@ -17,12 +19,12 @@
@implementation SuperPlayerPlugin {
float orginBrightness;
MPVolumeView *volumeView;
FlutterEventChannel *_eventChannel;
FTXPlayerEventSinkQueue *_eventSink;
FTXAudioManager *audioManager;
FTXDownloadManager *_FTXDownloadManager;
}
static UISlider *_volumeSlider;
SuperPlayerPlugin* instance;
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
......@@ -37,6 +39,9 @@ SuperPlayerPlugin* instance;
if(nil != instance) {
[instance destory];
}
if (nil != _FTXDownloadManager) {
[_FTXDownloadManager destroy];
}
}
- (instancetype)initWithRegistrar:
......@@ -49,26 +54,14 @@ SuperPlayerPlugin* instance;
// light componet init
orginBrightness = [UIScreen mainScreen].brightness;
// volume componet init
CGRect frame = CGRectMake(0, -100, 10, 0);
volumeView = [[MPVolumeView alloc] initWithFrame:frame];
volumeView.hidden = YES;
[volumeView sizeToFit];
// 单例slider
_volumeSlider = nil;
for (UIView *view in [volumeView subviews]) {
if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
_volumeSlider = (UISlider *)view;
break;
}
}
audioManager = [[FTXAudioManager alloc] init];
// volume event stream
_eventSink = [FTXPlayerEventSinkQueue new];
_eventChannel = [FlutterEventChannel eventChannelWithName:@"cloud.tencent.com/playerPlugin/event" binaryMessenger:[registrar messenger]];
[_eventChannel setStreamHandler:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(systemVolumeDidChangeNoti:)
name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
[audioManager registerVolumeChangeListener:self selector:@selector(systemVolumeDidChangeNoti:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
_FTXDownloadManager = [[FTXDownloadManager alloc] initWithRegistrar:registrar];
return self;
}
......@@ -105,14 +98,28 @@ SuperPlayerPlugin* instance;
result(nil);
}else if([@"setGlobalMaxCacheSize" isEqualToString:call.method]){
NSDictionary *args = call.arguments;
int size = [args[@"size"] intValue];
[FTXTransformation setMaxCacheItemSize:size];
NSInteger maxCacheItemSize = [args[@"size"] integerValue];
if (maxCacheItemSize > 0) {
[TXPlayerGlobalSetting setMaxCacheSize:maxCacheItemSize];
}
result(nil);
}else if([@"setGlobalCacheFolderPath" isEqualToString:call.method]){
NSDictionary *args = call.arguments;
NSString* path = args[@"path"];
[FTXTransformation setCacheFolder:path];
result(nil);
NSString* postfixPath = args[@"postfixPath"];
if(postfixPath != nil && postfixPath.length > 0) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [[paths objectAtIndex:0] stringByAppendingString:@"/"];
NSString *preloadDataPath = [documentDirectory stringByAppendingPathComponent:postfixPath];
if (![[NSFileManager defaultManager] fileExistsAtPath:preloadDataPath]) {
NSError *error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:preloadDataPath withIntermediateDirectories:NO attributes:nil error:&error];
[TXPlayerGlobalSetting setCacheFolderPath:preloadDataPath];
}
result([NSNumber numberWithBool:true]);
} else {
result([NSNumber numberWithBool:false]);
}
}else if([@"setGlobalLicense" isEqualToString:call.method]) {
NSDictionary *args = call.arguments;
NSString *licenceUrl = args[@"licenceUrl"];
......@@ -137,7 +144,7 @@ SuperPlayerPlugin* instance;
NSNumber *brightness = [NSNumber numberWithFloat:[UIScreen mainScreen].brightness];
result(brightness);
} else if([@"getSystemVolume" isEqualToString:call.method]) {
NSNumber *volume = [NSNumber numberWithFloat:[self getVolume]];
NSNumber *volume = [NSNumber numberWithFloat:[audioManager getVolume]];
result(volume);
} else if([@"setSystemVolume" isEqualToString:call.method]) {
NSNumber *volume = call.arguments[@"volume"];
......@@ -147,7 +154,7 @@ SuperPlayerPlugin* instance;
if (volume.floatValue > 1) {
volume = [NSNumber numberWithFloat:1];
}
[self setVolume:volume.floatValue];
[audioManager setVolume:volume.floatValue];
result(nil);
} else if ([@"abandonAudioFocus" isEqualToString:call.method]) {
// only for android
......@@ -160,28 +167,13 @@ SuperPlayerPlugin* instance;
int logLevel = [args[@"logLevel"] intValue];
[TXLiveBase setLogLevel:logLevel];
result(nil);
} else if([@"getLiteAVSDKVersion" isEqualToString:call.method]) {
result([TXLiveBase getSDKVersionStr]);
} else {
result(FlutterMethodNotImplemented);
}
}
-(float)getVolume{
return _volumeSlider.value > 0 ? _volumeSlider.value : [[AVAudioSession sharedInstance]outputVolume];
}
- (void)setVolume:(float)value {
// 需要设置 showsVolumeSlider 为 YES
volumeView.showsVolumeSlider = YES;
[_volumeSlider setValue:value animated:NO];
[_volumeSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
[_volumeSlider sizeToFit];
}
// 是否显示右边音量的UI
- (void)setVolumeUIVisible:(BOOL)volumeUIVisible {
volumeView.hidden = !volumeUIVisible;
}
+ (NSDictionary *)getParamsWithEvent:(int)EvtID withParams:(NSDictionary *)params
{
NSMutableDictionary<NSString*,NSObject*> *dict = [NSMutableDictionary dictionaryWithObject:@(EvtID) forKey:@"event"];
......@@ -193,10 +185,7 @@ SuperPlayerPlugin* instance;
-(void) destory
{
// destory volume view
[volumeView removeFromSuperview];
// destory volume observer
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
[audioManager destory:self name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];
}
#pragma mark - FlutterStreamHandler
......
......@@ -15,13 +15,26 @@ class SuperPlayerPlugin {
}
final StreamController<Map<dynamic, dynamic>> _eventStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _eventPipStreamController = StreamController.broadcast();
/// 原生交互,通用事件监听
/// 原生交互,通用事件监听,来自插件的事件,例如 声音变化等事件
Stream<Map<dynamic, dynamic>> get onEventBroadcast => _eventStreamController.stream;
/// 原生交互,通用事件监听,来自原生容器的事件,例如 PIP事件、activity/controller 生命周期变化
Stream<Map<dynamic, dynamic>> get onExtraEventBroadcast => _eventPipStreamController.stream;
SuperPlayerPlugin._internal() {
EventChannel eventChannel = EventChannel("cloud.tencent.com/playerPlugin/event");
eventChannel.receiveBroadcastStream("event").listen(_eventHandler, onError: _errorHandler);
EventChannel pipEventChanne = EventChannel("cloud.tencent.com/playerPlugin/pipEvent");
pipEventChanne.receiveBroadcastStream("event").listen(_pipEventHandler, onError: _errorHandler);
}
_pipEventHandler(event) {
if (null == event) {
return;
}
_eventPipStreamController.add(event);
}
_eventHandler(event) {
......@@ -65,9 +78,19 @@ class SuperPlayerPlugin {
return await _channel.invokeMethod('setGlobalMaxCacheSize', {"size": size});
}
/// 设置全局点播缓存目录。点播MP4、HLS有效
static Future<void> setGlobalCacheFolderPath(String path) async {
return await _channel.invokeMethod('setGlobalCacheFolderPath', {"path": path});
/// 在短视频播放场景中,视频文件的本地缓存是很刚需的一个特性,对于普通用户而言,一个已经看过的视频再次观看时,不应该再消耗一次流量。
/// @格式支持:SDK 支持 HLS(m3u8) 和 MP4 两种常见点播格式的缓存功能。
/// @开启时机:SDK 并不默认开启缓存功能,对于用户回看率不高的场景,也并不推荐您开启此功能。
/// @开启方式:全局生效,在使用播放器开启。开启此功能需要配置两个参数:本地缓存目录及缓存大小。
///
/// 该缓存路径默认设置到app沙盒目录下,postfixPath只需要传递相对缓存目录即可,不需要传递整个绝对路径。
/// e.g. postfixPath = 'testCache'
/// Android 平台:视频将会缓存到sdcard的Android/data/your-pkg-name/files/testCache 目录。
/// iOS 平台:视频将会缓存到沙盒的Documents/testCache 目录。
/// @param postfixPath 缓存目录
/// @return true 设置成功 false 设置失败
static Future<bool> setGlobalCacheFolderPath(String postfixPath) async {
return await _channel.invokeMethod('setGlobalCacheFolderPath', {"postfixPath": postfixPath});
}
/// 设置全局license
......@@ -87,7 +110,7 @@ class SuperPlayerPlugin {
/// 恢复当前界面亮度
static Future<void> restorePageBrightness() async {
return await _channel.invokeMethod("setBrightness", {"brightness": -1});
return await _channel.invokeMethod("setBrightness", {"brightness": -1.0});
}
/// 获得当前界面亮度 0.0 ~ 1.0
......@@ -114,4 +137,19 @@ class SuperPlayerPlugin {
static Future<double> requestAudioFocus() async {
return await _channel.invokeMethod("requestAudioFocus");
}
/// 当前设备是否支持画中画模式
/// @return [TXVodPlayEvent]
/// 0 可开启画中画模式
/// -101 android版本过低
/// -102 画中画权限关闭/设备不支持画中画
/// -103 当前界面已销毁
static Future<int> isDeviceSupportPip() async {
return await _channel.invokeMethod("isDeviceSupportPip");
}
/// 获取依赖Native端的 LiteAVSDK 的版本
static Future<String?> getLiteAVSDKVersion() async {
return await _channel.invokeMethod('getLiteAVSDKVersion');
}
}
......@@ -81,14 +81,28 @@ abstract class TXVodPlayEvent {
static const EVT_PLAY_DURATION = "EVT_PLAY_DURATION"; // 播放总长
static const EVT_PLAYABLE_DURATION_MS = "EVT_PLAYABLE_DURATION_MS"; // 点播可播放时长(毫秒)
static const EVT_PLAYABLE_RATE = "EVT_PLAYABLE_RATE"; //播放速率
static const String EVT_IMAGESPRIT_WEBVTTURL = "EVT_IMAGESPRIT_WEBVTTURL"; // 雪碧图web vtt描述文件下载URL
static const String EVT_IMAGESPRIT_IMAGEURL_LIST = "EVT_IMAGESPRIT_IMAGEURL_LIST"; // 雪碧图图片下载URL
static const String EVT_DRM_TYPE = "EVT_DRM_TYPE"; // 加密类型
/// superplayer plugin volume evnet
static const EVENT_VOLUME_CHANGED = 0x01; // 音量变化
static const EVENT_AUDIO_FOCUS_PAUSE = 0x02; // 失去音量输出播放焦点 only for android
static const EVENT_AUDIO_FOCUS_PLAY = 0x03; // 获得音量输出焦点 only for android
static const String EVT_IMAGESPRIT_WEBVTTURL = "EVT_IMAGESPRIT_WEBVTTURL"; // 雪碧图web vtt描述文件下载URL
static const String EVT_IMAGESPRIT_IMAGEURL_LIST = "EVT_IMAGESPRIT_IMAGEURL_LIST"; // 雪碧图图片下载URL
static const String EVT_DRM_TYPE = "EVT_DRM_TYPE"; // 加密类型
/// superplayer plugin volume event
static const EVENT_VOLUME_CHANGED = 1; // 音量变化
static const EVENT_AUDIO_FOCUS_PAUSE = 2; // 失去音量输出播放焦点 only for android
static const EVENT_AUDIO_FOCUS_PLAY = 3; // 获得音量输出焦点 only for android
/// pip event
static const EVENT_PIP_MODE_ALREADY_ENTER = 1; // 已经进入画中画模式
static const EVENT_PIP_MODE_ALREADY_EXIT = 2; // 已经退出画中画模式
static const EVENT_PIP_MODE_REQUEST_START = 3; // 开始请求进入画中画模式
static const EVENT_PIP_MODE_UI_STATE_CHANGED = 4; // pip UI状态发生变动,only support android > 31
static const NO_ERROR = 0;
static const ERROR_PIP_LOWER_VERSION = -101; // pip 错误,android版本过低
static const ERROR_PIP_DENIED_PERMISSION = -102; // pip 错误,画中画权限关闭/设备不支持画中画
static const ERROR_PIP_ACTIVITY_DESTROYED = -103; // pip 错误,当前界面已销毁
/// 视频下载相关事件
static const int EVENT_PREDOWNLOAD_ON_COMPLETE = 200; // 视频预下载完成
static const int EVENT_PREDOWNLOAD_ON_ERROR = 201; // 视频预下载出错
}
abstract class TXVodNetEvent {
......@@ -106,9 +120,12 @@ abstract class TXVodNetEvent {
static const NET_STATUS_VIDEO_DROP = "VIDEO_DROP"; // 推流:发送端视频丢帧数(有用:实时推流有丢帧逻辑) 拉流:接收端视频丢帧数(未用:播放端有视频加速,不丢帧)
static const NET_STATUS_V_SUM_CACHE_SIZE = "V_SUM_CACHE_SIZE"; // 拉流专用:接收端已接收但未渲染的视频帧数(包括JitterBuffer和解码器两部分缓存)
static const NET_STATUS_V_DEC_CACHE_SIZE = "V_DEC_CACHE_SIZE"; // 拉流专用:接收端解码器里缓存的视频帧数
static const NET_STATUS_AV_PLAY_INTERVAL = "AV_PLAY_INTERVAL"; // 拉流专用:视频当前渲染帧的timestamp和音频当前播放帧的timestamp的差值,标示当时音画同步的状态
static const NET_STATUS_AV_RECV_INTERVAL = "AV_RECV_INTERVAL"; // 拉流专用:jitterbuffer最新收到的视频帧和音频帧的timestamp的差值,标示当时jitterbuffer收包同步的状态
static const NET_STATUS_AUDIO_CACHE_THRESHOLD = "AUDIO_CACHE_THRESHOLD"; // 拉流专用:播放端音频缓存时长阀值,单位:秒,当缓存的音频时长大于该阀值时会触发jitterbuffer的加速播放,以保证播放时延
static const NET_STATUS_AV_PLAY_INTERVAL =
"AV_PLAY_INTERVAL"; // 拉流专用:视频当前渲染帧的timestamp和音频当前播放帧的timestamp的差值,标示当时音画同步的状态
static const NET_STATUS_AV_RECV_INTERVAL =
"AV_RECV_INTERVAL"; // 拉流专用:jitterbuffer最新收到的视频帧和音频帧的timestamp的差值,标示当时jitterbuffer收包同步的状态
static const NET_STATUS_AUDIO_CACHE_THRESHOLD =
"AUDIO_CACHE_THRESHOLD"; // 拉流专用:播放端音频缓存时长阀值,单位:秒,当缓存的音频时长大于该阀值时会触发jitterbuffer的加速播放,以保证播放时延
static const NET_STATUS_AUDIO_BLOCK_TIME = "AUDIO_BLOCK_TIME"; // 拉流专用:音频卡顿时长,单位ms
static const NET_STATUS_AUDIO_INFO = "AUDIO_PLAY_INFO"; // 当前流的音频信息,包括采样率信息和声道数信息
static const NET_STATUS_NET_JITTER = "NET_JITTER"; // 网络抖动情况,数值越大表示抖动越大,网络越不稳定
......@@ -142,24 +159,21 @@ enum TXPlayerEvent {
}
class TXLogLevel {
static const int LOG_LEVEL_VERBOSE = 0; // 输出所有级别的log
static const int LOG_LEVEL_DEBUG = 1; // 输出 DEBUG,INFO,WARNING,ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_INFO = 2; // 输出 INFO,WARNNING,ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_WARN = 3; // 输出WARNNING,ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_ERROR = 4; // 输出ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_FATAL = 5; // 只输出FATAL 级别的log
static const int LOG_LEVEL_NULL = 6; // 不输出任何sdk log
static const int LOG_LEVEL_VERBOSE = 0; // 输出所有级别的log
static const int LOG_LEVEL_DEBUG = 1; // 输出 DEBUG,INFO,WARNING,ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_INFO = 2; // 输出 INFO,WARNNING,ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_WARN = 3; // 输出WARNNING,ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_ERROR = 4; // 输出ERROR 和 FATAL 级别的log
static const int LOG_LEVEL_FATAL = 5; // 只输出FATAL 级别的log
static const int LOG_LEVEL_NULL = 6; // 不输出任何sdk log
}
class TXPlayInfoParams {
final int appId; // Tencent Cloud video appId, required
final String fileId; // Tencent Cloud video fileId, required
final String? psign; // encent cloud video encryption signature, required for encrypted video
final int appId; // Tencent Cloud video appId, required
final String fileId; // Tencent Cloud video fileId, required
final String? psign; // encent cloud video encryption signature, required for encrypted video
const TXPlayInfoParams(
{required this.appId,
required this.fileId,
this.psign});
const TXPlayInfoParams({required this.appId, required this.fileId, this.psign});
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
......@@ -168,4 +182,9 @@ class TXPlayInfoParams {
json["psign"] = psign;
return json;
}
}
\ No newline at end of file
}
//视频预下载事件回调Listener
typedef FTXPredownlodOnCompleteListener = void Function(int taskId, String url);
typedef FTXPredownlodOnErrorListener = void Function(int taskId, String url, int code, String msg);
// Copyright (c) 2022 Tencent. All rights reserved.
part of SuperPlayer;
/// include features:
/// 1. Video predownlaod
/// 2. Video download
class TXVodDownlaodController {
static const String TAG = 'TXVodDownlaodController';
static TXVodDownlaodController? _instance;
static TXVodDownlaodController get instance => _sharedInstance();
StreamSubscription? _downloadEventSubscription;
final StreamController<Map<dynamic, dynamic>> _downloadEventStreamController = StreamController.broadcast();
Stream<Map<dynamic, dynamic>> get onDownlaodEventBroadcast => _downloadEventStreamController.stream;
late MethodChannel _methodChannel;
FTXPredownlodOnCompleteListener? _onPreDownloadOnCompleteListener;
FTXPredownlodOnErrorListener? _onPreDownloadOnErrorListener;
static TXVodDownlaodController _sharedInstance() {
if (_instance == null) {
_instance = TXVodDownlaodController._createInsatnce();
}
return _instance!;
}
TXVodDownlaodController._createInsatnce() {
EventChannel eventChannel = EventChannel("cloud.tencent.com/txvodplayer/download/event");
_downloadEventSubscription = eventChannel.receiveBroadcastStream('event')
.listen(_eventHandler, onError: _errorHandler);
_methodChannel = MethodChannel("cloud.tencent.com/txvodplayer/download/api");
}
/// 启动预下载。
/// 【重要】启动预下载前,请先设置好播放引擎的缓存目录[SuperPlayerPlugin.setGlobalCacheFolderPath]和缓存大小[SuperPlayerPlugin.setGlobalMaxCacheSize],这个设置是全局配置需和播放器保持一致,否则会造成播放缓存失效。
/// playUrl: 要预下载的url
/// preloadSizeMB: 预下载的大小(单位:MB)
/// preferredResolution 期望分辨率,long类型,值为高x宽。可参考如720*1080。不支持多分辨率或不需指定时,传-1。
/// onCompleteListener:预下载成功回调
/// onErrorListener:预下载失败回调
/// 返回值:任务ID,可用这个任务ID停止预下载 [stopPreload]
Future<int> startPreLoad(final String playUrl,
final int preloadSizeMB,
final int preferredResolution,
{ FTXPredownlodOnCompleteListener? onCompleteListener,
FTXPredownlodOnErrorListener? onErrorListener,
}) async {
_onPreDownloadOnCompleteListener = onCompleteListener;
_onPreDownloadOnErrorListener = onErrorListener;
var map = {"playUrl": playUrl,
"preloadSizeMB": preloadSizeMB,
"preferredResolution": preferredResolution };
return await _methodChannel.invokeMethod("startPreLoad", map);
}
/// 停止预下载。
/// taskId: 任务id,[startPreLoad]返回值
Future<void> stopPreLoad(final int taskId) async {
var map = {"taskId": taskId};
await _methodChannel.invokeMethod("stopPreLoad", map);
}
_eventHandler(event) {
if (null == event) {
return;
}
LogUtils.d(TAG, '_eventHandler, event= ${event}');
final Map<dynamic, dynamic> map = event;
switch (map["event"]) {
case TXVodPlayEvent.EVENT_PREDOWNLOAD_ON_COMPLETE:
int taskId = map['taskId'] as int;
String url = map['url'] as String;
LogUtils.d(TAG, 'receive EVENT_PREDOWNLOAD_ON_COMPLETE, taskID=${taskId} ,url=${url}');
if (_onPreDownloadOnCompleteListener != null) {
_onPreDownloadOnCompleteListener!(taskId, url);
}
break;
case TXVodPlayEvent.EVENT_PREDOWNLOAD_ON_ERROR:
int taskId = map['taskId'] as int;
String url = map['url'] as String;
int code = map['code'] as int;
String msg = map['msg'] ?? '';
LogUtils.d(TAG, 'receive EVENT_PREDOWNLOAD_ON_ERROR, taskID=${taskId} ,url=${url}, code=${code} , msg=${msg}');
if (_onPreDownloadOnErrorListener != null) {
_onPreDownloadOnErrorListener!(taskId, url, code, msg);
}
break;
default:
break;
}
_downloadEventStreamController.add(event);
}
_errorHandler(error) {
}
}
\ No newline at end of file
......@@ -357,6 +357,20 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
return await _channel.invokeMethod("enableHardwareDecode", {"enable": enable});
}
/// 进入画中画模式,进入画中画模式,需要适配画中画模式的界面,安卓只支持7.0以上机型
/// <h1>
/// 由于android系统限制,传递的图标大小不得超过1M,否则无法显示
/// </h1>
/// @param backIcon playIcon pauseIcon forwardIcon 为播放后退、播放、暂停、前进的图标,如果赋值的话,将会使用传递的图标,否则
/// 使用系统默认图标,只支持flutter本地资源图片,传递的时候,与flutter使用图片资源一致,例如: images/back_icon.png
Future<int> enterPictureInPictureMode(
{String? backIcon, String? playIcon, String? pauseIcon, String? forwardIcon}) async {
if (_isNeedDisposed) return -1;
await _initPlayer.future;
return await _channel.invokeMethod("enterPictureInPictureMode",
{"backIcon": backIcon, "playIcon": playIcon, "pauseIcon": pauseIcon, "forwardIcon": forwardIcon});
}
/// 获取总时长
Future<double> getDuration() async {
if (_isNeedDisposed) return 0;
......
......@@ -4,6 +4,7 @@ library SuperPlayer;
import 'dart:async';
import 'dart:core';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
......@@ -15,4 +16,5 @@ part 'Core/txplayer_controller.dart';
part 'Core/txplayer_define.dart';
part 'Core/txplayer_widget.dart';
part 'Core/txvodplayer_config.dart';
part 'Core/txvodplayer_controller.dart';
\ No newline at end of file
part 'Core/txvodplayer_controller.dart';
part 'Core/txvoddownload_controller.dart';
\ No newline at end of file
......@@ -8,6 +8,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
SuperPlayerPlugin.instance;
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
});
......@@ -20,4 +21,18 @@ void main() {
test('getPlatformVersion', () async {
expect(await SuperPlayerPlugin.platformVersion, '42');
});
test('TXVodPreDownlaodController_startPreLoad', () async {
await SuperPlayerPlugin.setGlobalCacheFolderPath("tx");
await SuperPlayerPlugin.setGlobalMaxCacheSize(200);
String _url =
"http://1400329073.vod2.myqcloud.com/d62d88a7vodtranscq1400329073/59c68fe75285890800381567412/adp.10.m3u8";
int taskId = await TXVodDownlaodController.instance.startPreLoad(_url, 20, 720*1080,
onCompleteListener:(int taskId,String url) {
print('taskID=${taskId} ,url=${url}');
}, onErrorListener: (int taskId, String url, int code, String msg) {
print('taskID=${taskId} ,url=${url}, code=${code} , msg=${msg}');
} );
expect(taskId != -1 , true);
});
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论