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

SuperPlayer support live for Android&IOS platform

上级 2a99865e
......@@ -47,6 +47,8 @@
### `pubspec.yaml`配置
**推荐flutter sdk 版本 3.0.0 及以上**
集成LiteAVSDK_Player版本,默认情况下也是集成此版本。在`pubspec.yaml`中增加配置
```yaml
......
......@@ -11,12 +11,11 @@ import androidx.annotation.NonNull;
import com.tencent.rtmp.ITXLivePlayListener;
import com.tencent.rtmp.TXLiveBase;
import com.tencent.rtmp.TXLiveConstants;
import com.tencent.rtmp.TXLivePlayConfig;
import com.tencent.rtmp.TXLivePlayer;
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;
......@@ -44,16 +43,45 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
private TXLivePlayer mLivePlayer;
private static final int Uninitialized = -101;
private boolean mEnableHardwareDecode = true;
private boolean mHardwareDecodeFail = false;
private TextureRegistry.SurfaceTextureEntry mSurfaceTextureEntry;
private Activity mActivity;
public FTXLivePlayer(FlutterPlugin.FlutterPluginBinding flutterPluginBinding, Activity activity) {
private final FTXPIPManager mPipManager;
private FTXPIPManager.PipParams mPipParams;
private final FTXPIPManager.PipCallback pipCallback = new FTXPIPManager.PipCallback() {
@Override
public void onPlayBack() {
// pip not support playback
}
@Override
public void onResumeOrPlay() {
boolean isPlaying = isPlaying();
if (isPlaying) {
pause();
} else {
resume();
}
// isPlaying取反,点击暂停/播放之后,播放状态会变化
mPipManager.updatePipActions(!isPlaying, mPipParams);
}
@Override
public void onPlayForward() {
// pip not support forward
}
};
public FTXLivePlayer(FlutterPlugin.FlutterPluginBinding flutterPluginBinding, Activity activity,
FTXPIPManager pipManager) {
super();
mFlutterPluginBinding = flutterPluginBinding;
mActivity = activity;
mPipManager = pipManager;
mSurfaceTextureEntry = mFlutterPluginBinding.getTextureRegistry().createSurfaceTexture();
mSurfaceTextureEntry = mFlutterPluginBinding.getTextureRegistry().createSurfaceTexture();
mSurfaceTexture = mSurfaceTextureEntry.surfaceTexture();
mSurface = new Surface(mSurfaceTexture);
......@@ -115,8 +143,17 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
}
@Override
public void onPlayEvent(int i, Bundle bundle) {
mEventSink.success(CommonUtil.getParams(i, bundle));
public void onPlayEvent(int event, Bundle bundle) {
if (event == TXLiveConstants.PLAY_EVT_CHANGE_RESOLUTION) {
int width = bundle.getInt(TXLiveConstants.EVT_PARAM1, 0);
int height = bundle.getInt(TXLiveConstants.EVT_PARAM2, 0);
if (!mEnableHardwareDecode || mHardwareDecodeFail) {
setDefaultBufferSizeForSoftDecode(width, height);
}
} else if (event == TXLiveConstants.PLAY_WARNING_HW_ACCELERATION_FAIL) {
mHardwareDecodeFail = true;
}
mEventSink.success(CommonUtil.getParams(event, bundle));
}
@Override
......@@ -124,68 +161,100 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
mNetStatusSink.success(CommonUtil.getParams(0, bundle));
}
// surface 的大小默认是宽高为1,当硬解失败时或使用软解时,软解会依赖surface的窗口渲染,不更新会导致只有1px的内容
private void setDefaultBufferSizeForSoftDecode(int width, int height) {
if (mSurfaceTextureEntry != null && mSurfaceTextureEntry.surfaceTexture() != null) {
SurfaceTexture surfaceTexture = mSurfaceTextureEntry.surfaceTexture();
surfaceTexture.setDefaultBufferSize(width, height);
if (mSurface != null) {
mSurface.release();
}
mSurface = new Surface(surfaceTexture);
mLivePlayer.setSurface(mSurface);
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if(call.method.equals("init")){
if (call.method.equals("init")) {
boolean onlyAudio = call.argument("onlyAudio");
long id = init(onlyAudio);
result.success(id);
}else if(call.method.equals("setIsAutoPlay")) {
} else if (call.method.equals("setIsAutoPlay")) {
boolean loop = call.argument("isAutoPlay");
setIsAutoPlay(loop);
result.success(null);
}else if(call.method.equals("play")){
} else if (call.method.equals("play")) {
String url = call.argument("url");
int type = call.argument("playType");
int r = startPlay(url,type);
int r = startPlay(url, type);
result.success(r);
}else if(call.method.equals("stop")){
} else if (call.method.equals("stop")) {
Boolean isNeedClear = call.argument("isNeedClear");
int r = stopPlay(isNeedClear);
result.success(r);
}else if(call.method.equals("isPlaying")){
} else if (call.method.equals("isPlaying")) {
boolean r = isPlaying();
result.success(r);
}else if(call.method.equals("pause")){
} else if (call.method.equals("pause")) {
pause();
result.success(null);
}else if(call.method.equals("resume")){
} else if (call.method.equals("resume")) {
resume();
result.success(null);
}else if(call.method.equals("setMute")){
} else if (call.method.equals("setMute")) {
boolean mute = call.argument("mute");
setMute(mute);
result.success(null);
}else if (call.method.equals("seek")) {
} else if (call.method.equals("seek")) {
result.notImplemented();
}else if (call.method.equals("setRate")) {
} else if (call.method.equals("setRate")) {
result.notImplemented();
}else if(call.method.equals("setVolume")) {
} else if (call.method.equals("setVolume")) {
Integer volume = call.argument("volume");
setVolume(volume);
result.success(null);
}else if(call.method.equals("setRenderRotation")) {
} else if (call.method.equals("setRenderRotation")) {
int rotation = call.argument("rotation");
setRenderRotation(rotation);
result.success(null);
}else if(call.method.equals("setLiveMode")){
} else if (call.method.equals("setLiveMode")) {
int type = call.argument("type");
setLiveMode(type);
result.success(null);
}else if(call.method.equals("switchStream")) {
} else if (call.method.equals("switchStream")) {
String url = call.argument("url");
switchStream(url);
result.success(null);
}else if(call.method.equals("setAppID")) {
int switchResult = switchStream(url);
result.success(switchResult);
} else if (call.method.equals("setAppID")) {
String appId = call.argument("appId");
setAppID(appId);
result.success(null);
}else if(call.method.equals("prepareLiveSeek")) {
} else if (call.method.equals("prepareLiveSeek")) {
result.notImplemented();
}else if(call.method.equals("resumeLive")) {
} else if (call.method.equals("resumeLive")) {
int r = resumeLive();
result.success(r);
}else {
} else if (call.method.equals("enableHardwareDecode")) {
Boolean enable = call.argument("enable");
boolean r = enableHardwareDecode(enable);
result.success(r);
} 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(), false, false, true);
int pipResult = mPipManager.enterPip(isPlaying(), mPipParams);
result.success(pipResult);
} else if (call.method.equals("setConfig")) {
Map<Object, Object> config = call.argument("config");
setPlayConfig(config);
result.success(null);
} else {
result.notImplemented();
}
}
......@@ -199,7 +268,8 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
return mSurfaceTextureEntry == null ? -1 : mSurfaceTextureEntry.id();
}
private int mSurfaceWidth,mSurfaceHeight = 0;
private int mSurfaceWidth, mSurfaceHeight = 0;
int startPlay(String url, int type) {
Log.d(TAG, "startPlay:");
if (mLivePlayer != null) {
......@@ -213,15 +283,15 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
public void onRenderVideoFrame(TXLivePlayer.TXLiteAVTexture texture) {
int width = texture.width;
int height = texture.height;
if(width != mSurfaceWidth || height != mSurfaceHeight){
Log.d(TAG, "onRenderVideoFrame: width="+texture.width+",height="+texture.height);
mLivePlayer.setSurfaceSize(width,height);
mSurfaceTexture.setDefaultBufferSize(width,height);
if (width != mSurfaceWidth || height != mSurfaceHeight) {
Log.d(TAG, "onRenderVideoFrame: width=" + texture.width + ",height=" + texture.height);
mLivePlayer.setSurfaceSize(width, height);
mSurfaceTexture.setDefaultBufferSize(width, height);
mSurfaceWidth = width;
mSurfaceHeight = height;
}
}
},null);
}, null);
return mLivePlayer.startPlay(url, type);
}
return Uninitialized;
......@@ -231,6 +301,7 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
if (mLivePlayer != null) {
return mLivePlayer.stopPlay(isNeedClearLastImg);
}
mHardwareDecodeFail = false;
return Uninitialized;
}
......@@ -297,12 +368,12 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
config.setAutoAdjustCacheTime(true);
config.setMinAutoAdjustCacheTime(1);
config.setMaxAutoAdjustCacheTime(3);
}else if(type == 1){
} else if (type == 1) {
//极速模式
config.setAutoAdjustCacheTime(true);
config.setMinAutoAdjustCacheTime(1);
config.setMaxAutoAdjustCacheTime(1);
}else{
} else {
//流畅模式
config.setAutoAdjustCacheTime(false);
config.setCacheTime(5);
......@@ -312,10 +383,11 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
}
}
void switchStream(String url) {
int switchStream(String url) {
if (mLivePlayer != null) {
mLivePlayer.switchStream(url);
return mLivePlayer.switchStream(url);
}
return -1;
}
private void setAppID(String appId) {
......@@ -326,7 +398,6 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
if (mLivePlayer != null) {
return mLivePlayer.prepareLiveSeek(domain, bizId);
}
return Uninitialized;
}
......@@ -334,13 +405,27 @@ public class FTXLivePlayer extends FTXBasePlayer implements MethodChannel.Method
if (mLivePlayer != null) {
return mLivePlayer.resumeLive();
}
return Uninitialized;
}
private boolean enableHardwareDecode(Boolean enable) {
if (mLivePlayer != null) {
mEnableHardwareDecode = enable;
return mLivePlayer.enableHardwareDecode(enable);
}
return false;
}
private void setRenderMode(int renderMode) {
if (mLivePlayer != null) {
mLivePlayer.setRenderMode(renderMode);
}
}
void setPlayConfig(Map<Object, Object> config) {
if (mLivePlayer != null) {
TXLivePlayConfig playConfig = FTXTransformation.transformToLiveConfig(config);
mLivePlayer.setConfig(playConfig);
}
}
}
......@@ -205,40 +205,45 @@ public class FTXPIPManager {
return;
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
List<RemoteAction> actions = new ArrayList<>();
// play back
Bundle backData = new Bundle();
backData.putInt(FTXEvent.EXTRA_NAME_PLAY_OP, FTXEvent.EXTRA_PIP_PLAY_BACK);
backData.putInt(FTXEvent.EXTRA_NAME_PLAYER_ID, params.mCurrentPlayerId);
Intent backIntent = new Intent(FTXEvent.ACTION_PIP_PLAY_CONTROL).putExtras(backData);
PendingIntent preIntent = PendingIntent.getBroadcast(mActivity, FTXEvent.EXTRA_PIP_PLAY_BACK, backIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
RemoteAction preAction = new RemoteAction(getBackIcon(params), "skipPre", "skip pre", preIntent);
if(params.mIsNeedPlayBack) {
Bundle backData = new Bundle();
backData.putInt(FTXEvent.EXTRA_NAME_PLAY_OP, FTXEvent.EXTRA_PIP_PLAY_BACK);
backData.putInt(FTXEvent.EXTRA_NAME_PLAYER_ID, params.mCurrentPlayerId);
Intent backIntent = new Intent(FTXEvent.ACTION_PIP_PLAY_CONTROL).putExtras(backData);
PendingIntent preIntent = PendingIntent.getBroadcast(mActivity, FTXEvent.EXTRA_PIP_PLAY_BACK, backIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
RemoteAction preAction = new RemoteAction(getBackIcon(params), "skipPre", "skip pre", preIntent);
actions.add(preAction);
}
// resume or pause
Bundle playOrPauseData = new Bundle();
playOrPauseData.putInt(FTXEvent.EXTRA_NAME_PLAYER_ID, params.mCurrentPlayerId);
playOrPauseData.putInt(FTXEvent.EXTRA_NAME_PLAY_OP, FTXEvent.EXTRA_PIP_PLAY_RESUME_OR_PAUSE);
Intent playOrPauseIntent =
new Intent(FTXEvent.ACTION_PIP_PLAY_CONTROL).putExtras(playOrPauseData);
Icon playIcon = isPlaying ? getPauseIcon(params) : getPlayIcon(params);
PendingIntent playIntent = PendingIntent.getBroadcast(mActivity, FTXEvent.EXTRA_PIP_PLAY_RESUME_OR_PAUSE,
playOrPauseIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
RemoteAction playOrPauseAction = new RemoteAction(playIcon, "playOrPause", "play Or Pause", playIntent);
if(params.mIsNeedPlayControl) {
Bundle playOrPauseData = new Bundle();
playOrPauseData.putInt(FTXEvent.EXTRA_NAME_PLAYER_ID, params.mCurrentPlayerId);
playOrPauseData.putInt(FTXEvent.EXTRA_NAME_PLAY_OP, FTXEvent.EXTRA_PIP_PLAY_RESUME_OR_PAUSE);
Intent playOrPauseIntent =
new Intent(FTXEvent.ACTION_PIP_PLAY_CONTROL).putExtras(playOrPauseData);
Icon playIcon = isPlaying ? getPauseIcon(params) : getPlayIcon(params);
PendingIntent playIntent = PendingIntent.getBroadcast(mActivity, FTXEvent.EXTRA_PIP_PLAY_RESUME_OR_PAUSE,
playOrPauseIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
RemoteAction playOrPauseAction = new RemoteAction(playIcon, "playOrPause", "play Or Pause", playIntent);
actions.add(playOrPauseAction);
}
// forward
Bundle forwardData = new Bundle();
forwardData.putInt(FTXEvent.EXTRA_NAME_PLAY_OP, FTXEvent.EXTRA_PIP_PLAY_FORWARD);
forwardData.putInt(FTXEvent.EXTRA_NAME_PLAYER_ID, params.mCurrentPlayerId);
Intent forwardIntent = new Intent(FTXEvent.ACTION_PIP_PLAY_CONTROL).putExtras(forwardData);
PendingIntent nextIntent = PendingIntent.getBroadcast(mActivity, FTXEvent.EXTRA_PIP_PLAY_FORWARD,
forwardIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
RemoteAction nextAction = new RemoteAction(getForwardIcon(params), "skipNext", "skip next", nextIntent);
List<RemoteAction> actions = new ArrayList<>();
actions.add(preAction);
actions.add(playOrPauseAction);
actions.add(nextAction);
if(params.mIsNeedPlayForward) {
Bundle forwardData = new Bundle();
forwardData.putInt(FTXEvent.EXTRA_NAME_PLAY_OP, FTXEvent.EXTRA_PIP_PLAY_FORWARD);
forwardData.putInt(FTXEvent.EXTRA_NAME_PLAYER_ID, params.mCurrentPlayerId);
Intent forwardIntent = new Intent(FTXEvent.ACTION_PIP_PLAY_CONTROL).putExtras(forwardData);
PendingIntent nextIntent = PendingIntent.getBroadcast(mActivity, FTXEvent.EXTRA_PIP_PLAY_FORWARD,
forwardIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
RemoteAction nextAction = new RemoteAction(getForwardIcon(params), "skipNext", "skip next", nextIntent);
actions.add(nextAction);
}
params.mPipParams.setActions(actions);
mActivity.setPictureInPictureParams(params.mPipParams.build());
......@@ -285,6 +290,9 @@ public class FTXPIPManager {
String mPlayForwardAssetPath;
int mCurrentPlayerId;
protected PictureInPictureParams.Builder mPipParams;
private boolean mIsNeedPlayBack = true;
private boolean mIsNeedPlayForward = true;
private boolean mIsNeedPlayControl = true;
/**
* @param mPlayBackAssetPath 回退按钮图片资源路径,传空则使用系统默认图标
......@@ -295,11 +303,21 @@ public class FTXPIPManager {
*/
public PipParams(String mPlayBackAssetPath, String mPlayResumeAssetPath, String mPlayPauseAssetPath,
String mPlayForwardAssetPath, int mCurrentPlayerId) {
this(mPlayBackAssetPath, mPlayResumeAssetPath, mPlayPauseAssetPath, mPlayForwardAssetPath,
mCurrentPlayerId, true, true, true);
}
public PipParams(String mPlayBackAssetPath, String mPlayResumeAssetPath, String mPlayPauseAssetPath,
String mPlayForwardAssetPath, int mCurrentPlayerId, boolean isNeedPlayBack,
boolean isNeedPlayForward, boolean isNeedPlayControl) {
this.mPlayBackAssetPath = mPlayBackAssetPath;
this.mPlayResumeAssetPath = mPlayResumeAssetPath;
this.mPlayPauseAssetPath = mPlayPauseAssetPath;
this.mPlayForwardAssetPath = mPlayForwardAssetPath;
this.mCurrentPlayerId = mCurrentPlayerId;
this.mIsNeedPlayBack = isNeedPlayBack;
this.mIsNeedPlayForward = isNeedPlayForward;
this.mIsNeedPlayControl = isNeedPlayControl;
}
}
......
......@@ -3,7 +3,7 @@ package com.tencent.vod.flutter;
import android.text.TextUtils;
import com.tencent.rtmp.TXPlayerGlobalSetting;
import com.tencent.rtmp.TXLivePlayConfig;
import com.tencent.rtmp.TXVodPlayConfig;
import java.util.HashMap;
......@@ -15,75 +15,61 @@ import java.util.Map;
public class FTXTransformation {
@SuppressWarnings("unchecked")
public static TXVodPlayConfig transformToConfig(Map<Object, Object> config) {
public static TXVodPlayConfig transformToVodConfig(Map<Object, Object> config) {
TXVodPlayConfig playConfig = new TXVodPlayConfig();
Integer connectRetryCount = (Integer) config.get("connectRetryCount");
if (intIsNotEmpty(connectRetryCount)) {
playConfig.setConnectRetryCount(connectRetryCount);
}
Integer connectRetryInterval = (Integer) config.get("connectRetryInterval");
if (intIsNotEmpty(connectRetryInterval)) {
playConfig.setConnectRetryInterval(connectRetryInterval);
}
Integer timeout = (Integer) config.get("timeout");
if (intIsNotEmpty(timeout)) {
playConfig.setTimeout(timeout);
}
Integer playerType = (Integer) config.get("playerType");
if(null != playerType) {
playConfig.setPlayerType(playerType);
}
Map<String, String> headers = (Map<String, String>) config.get("headers");
if (null == headers) {
headers = new HashMap<>();
}
playConfig.setHeaders(headers);
Boolean enableAccurateSeek = (Boolean) config.get("enableAccurateSeek");
if(null != enableAccurateSeek) {
playConfig.setEnableAccurateSeek(enableAccurateSeek);
}
Boolean autoRotate = (Boolean) config.get("autoRotate");
if(null != autoRotate) {
playConfig.setAutoRotate(autoRotate);
}
Boolean smoothSwitchBitrate = (Boolean) config.get("smoothSwitchBitrate");
if(null != smoothSwitchBitrate) {
playConfig.setSmoothSwitchBitrate(smoothSwitchBitrate);
}
String cacheMp4ExtName = (String) config.get("cacheMp4ExtName");
if (!TextUtils.isEmpty(cacheMp4ExtName)) {
playConfig.setCacheMp4ExtName(cacheMp4ExtName);
}
Integer progressInterval = (Integer) config.get("progressInterval");
if (intIsNotEmpty(progressInterval)) {
playConfig.setProgressInterval(progressInterval);
}
Integer maxBufferSize = (Integer) config.get("maxBufferSize");
if (intIsNotEmpty(maxBufferSize)) {
playConfig.setMaxBufferSize(maxBufferSize);
}
Integer maxPreloadSize = (Integer) config.get("maxPreloadSize");
if (intIsNotEmpty(maxPreloadSize)) {
playConfig.setMaxPreloadSize(maxPreloadSize);
}
Integer firstStartPlayBufferTime = (Integer) config.get("firstStartPlayBufferTime");
if(null != firstStartPlayBufferTime) {
playConfig.setFirstStartPlayBufferTime(firstStartPlayBufferTime);
}
Integer nextStartPlayBufferTime = (Integer) config.get("nextStartPlayBufferTime");
if(null != nextStartPlayBufferTime) {
playConfig.setNextStartPlayBufferTime(nextStartPlayBufferTime);
......@@ -93,23 +79,19 @@ public class FTXTransformation {
if (!TextUtils.isEmpty(overlayKey)) {
playConfig.setOverlayKey(overlayKey);
}
String overlayIv = (String) config.get("overlayIv");
if (!TextUtils.isEmpty(overlayIv)) {
playConfig.setOverlayIv(overlayIv);
}
Map<String, Object> extInfoMap = (Map<String, Object>) config.get("extInfoMap");
if (null == extInfoMap) {
extInfoMap = new HashMap<>();
}
playConfig.setExtInfo(extInfoMap);
Boolean enableRenderProcess = (Boolean) config.get("enableRenderProcess");
if(null != enableRenderProcess) {
playConfig.setEnableRenderProcess(enableRenderProcess);
}
String preferredResolutionStr = (String) config.get("preferredResolution");
if (null != preferredResolutionStr) {
long preferredResolution = Long.parseLong(preferredResolutionStr);
......@@ -119,8 +101,60 @@ public class FTXTransformation {
return playConfig;
}
public static TXLivePlayConfig transformToLiveConfig(Map<Object, Object> config) {
TXLivePlayConfig livePlayConfig = new TXLivePlayConfig();
Double cacheTime = (Double) config.get("cacheTime");
if(doubleIsNotEmpty(cacheTime)) {
livePlayConfig.setCacheTime(cacheTime.floatValue());
}
Double maxAutoAdjustCacheTime = (Double) config.get("maxAutoAdjustCacheTime");
if(doubleIsNotEmpty(maxAutoAdjustCacheTime)) {
livePlayConfig.setMaxAutoAdjustCacheTime(maxAutoAdjustCacheTime.floatValue());
}
Double minAutoAdjustCacheTime = (Double) config.get("minAutoAdjustCacheTime");
if(doubleIsNotEmpty(minAutoAdjustCacheTime)) {
livePlayConfig.setMinAutoAdjustCacheTime(minAutoAdjustCacheTime.floatValue());
}
Integer videoBlockThreshold = (Integer) config.get("videoBlockThreshold");
if(intIsNotEmpty(videoBlockThreshold)) {
livePlayConfig.setVideoBlockThreshold(videoBlockThreshold);
}
Integer connectRetryCount = (Integer) config.get("connectRetryCount");
if(intIsNotEmpty(connectRetryCount)) {
livePlayConfig.setConnectRetryCount(connectRetryCount);
}
Integer connectRetryInterval = (Integer) config.get("connectRetryInterval");
if(intIsNotEmpty(connectRetryInterval)) {
livePlayConfig.setConnectRetryInterval(connectRetryInterval);
}
Boolean autoAdjustCacheTime = (Boolean) config.get("autoAdjustCacheTime");
if(null != autoAdjustCacheTime) {
livePlayConfig.setAutoAdjustCacheTime(autoAdjustCacheTime);
}
Boolean enableAec= (Boolean) config.get("enableAec");
if(null != enableAec) {
livePlayConfig.setEnableAEC(enableAec);
}
Boolean enableMessage = (Boolean) config.get("enableMessage");
if(null != enableMessage) {
livePlayConfig.setEnableMessage(enableMessage);
}
Boolean enableMetaData = (Boolean) config.get("enableMetaData");
if(null != enableMetaData) {
livePlayConfig.setEnableMetaData(enableMetaData);
}
String flvSessionKey = (String) config.get("flvSessionKey");
if(!TextUtils.isEmpty(flvSessionKey)) {
livePlayConfig.setFlvSessionKey(flvSessionKey);
}
return livePlayConfig;
}
private static boolean intIsNotEmpty(Integer value) {
return null != value && value > 0;
}
private static boolean doubleIsNotEmpty(Double value) {
return null != value && value > 0;
}
}
......@@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
......@@ -510,7 +509,7 @@ public class FTXVodPlayer extends FTXBasePlayer implements MethodChannel.MethodC
void setPlayConfig(Map<Object, Object> config) {
if (mVodPlayer != null) {
TXVodPlayConfig playConfig = FTXTransformation.transformToConfig(config);
TXVodPlayConfig playConfig = FTXTransformation.transformToVodConfig(config);
mVodPlayer.setConfig(playConfig);
}
}
......
......@@ -133,7 +133,7 @@ public class SuperPlayerPlugin implements FlutterPlugin, MethodCallHandler, Acti
mPlayers.append(playerId, player);
result.success(playerId);
} else if (call.method.equals("createLivePlayer")) {
FTXLivePlayer player = new FTXLivePlayer(mFlutterPluginBinding, mActivityPluginBinding.getActivity());
FTXLivePlayer player = new FTXLivePlayer(mFlutterPluginBinding, mActivityPluginBinding.getActivity(), mTxPipManager);
int playerId = player.getPlayerId();
mPlayers.append(playerId, player);
result.success(playerId);
......@@ -210,6 +210,10 @@ public class SuperPlayerPlugin implements FlutterPlugin, MethodCallHandler, Acti
result.success(mTxPipManager.isSupportDevice());
} else if (call.method.equals("getLiteAVSDKVersion")) {
result.success(TXLiveBase.getSDKVersionStr());
} else if(call.method.equals("setGlobalEnv")) {
String envConfig = call.argument("envConfig");
int setResult = TXLiveBase.setGlobalEnv(envConfig);
result.success(setResult);
} else {
result.notImplemented();
}
......
......@@ -13,26 +13,12 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!-- <android:uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
</manifest>
......@@ -14,10 +14,20 @@ class DemoSuperplayer extends StatefulWidget {
}
class _DemoSuperplayerState extends State<DemoSuperplayer> {
static const DEFAULT_PLACE_HOLDER = "http://xiaozhibo-10055601.file.myqcloud.com/coverImg.jpg";
List<SuperPlayerModel> videoModels = [];
bool _isFullScreen = false;
late SuperPlayerController _controller;
StreamSubscription? simpleEventSubscription;
int tabSelectPos = 0;
TextStyle _textStyleSelected = new TextStyle(
fontSize: 16, color: Colors.white
);
TextStyle _textStyleUnSelected = new TextStyle(
fontSize: 16, color: Colors.grey
);
@override
void initState() {
......@@ -30,12 +40,14 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
simpleEventSubscription = _controller.onSimplePlayerEventBroadcast.listen((event) {
String evtName = event["event"];
if (evtName == SuperPlayerViewEvent.onStartFullScreenPlay) {
// enter fullscreen
} else if (evtName == SuperPlayerViewEvent.onStopFullScreenPlay) {
// exit fullscreen
} else {
print(evtName);
}
});
initData();
_getLiveListData();
}
@override
......@@ -69,6 +81,34 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
return !_controller.onBackPress();
}
Widget getTabRow() {
return new Container(
height: 40,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
new GestureDetector(
child: new Container(
child: Text(
"直播",
style: tabSelectPos == 0 ? _textStyleSelected : _textStyleUnSelected,
),
),
onTap: _getLiveListData,
),
new GestureDetector(
onTap: _getVodListData,
child: new Container(
child: Text(
"点播",
style: tabSelectPos == 1 ? _textStyleSelected : _textStyleUnSelected,
),
)),
],
),
);
}
Widget getBody() {
return Column(
children: [_getPlayArea(), Expanded(flex: 1, child: _getListArea()), _getAddArea()],
......@@ -85,32 +125,39 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
Widget _getListArea() {
return Container(
margin: EdgeInsets.only(top: 10),
child: ListView.builder(
shrinkWrap: true,
itemCount: videoModels.length,
itemBuilder: (context, i) => _buildVideoItem(videoModels[i]),
),
);
margin: EdgeInsets.only(top: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
getTabRow(),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: videoModels.length,
itemBuilder: (context, i) => _buildVideoItem(videoModels[i]),
))
],
));
}
Widget _buildVideoItem(SuperPlayerModel playModel) {
return Column(
children: [
ListTile(
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),
horizontalTitleGap: 10,),
leading: Image.network(
playModel.coverUrl.isEmpty ? DEFAULT_PLACE_HOLDER : 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),
horizontalTitleGap: 10,
),
Divider()
],
);
......@@ -124,6 +171,7 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
}
void onAddVideoTap(BuildContext context) {
bool isLive = tabSelectPos == 0;
showDialog(
context: context,
builder: (context) {
......@@ -144,7 +192,7 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
}
playCurrentModel(model);
}, needPisgn: true);
}, needPisgn: !isLive, showFileEdited: !isLive,);
});
}
......@@ -152,7 +200,35 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
_controller.playWithModel(model);
}
void initData() async {
void _getLiveListData() async {
setState(() {
tabSelectPos = 0;
});
List<SuperPlayerModel> models = [];
int playAction = SuperPlayerModel.PLAY_ACTION_AUTO_PLAY;
SuperPlayerModel model = SuperPlayerModel();
model.title = "测试视频";
model.videoURL = "http://liteavapp.qcloud.com/live/liteavdemoplayerstreamid_demo1080p.flv";
model.coverUrl = "http://1500005830.vod2.myqcloud.com/6c9a5118vodcq1500005830/66bc542f387702300661648850/0RyP1rZfkdQA.png";
model.playAction = playAction;
models.add(model);
videoModels.clear();
videoModels.addAll(models);
setState(() {
if (videoModels.isNotEmpty) {
playCurrentModel(videoModels[0]);
} else {
EasyLoading.showError("video list request error");
}
});
}
void _getVodListData() async {
setState(() {
tabSelectPos = 1;
});
List<SuperPlayerModel> models = [];
int playAction = SuperPlayerModel.PLAY_ACTION_AUTO_PLAY;
......@@ -196,13 +272,12 @@ class _DemoSuperplayerState extends State<DemoSuperplayer> {
List<Future<void>> requestList = [];
SuperVodDataLoader loader = SuperVodDataLoader();
for (SuperPlayerModel tempModel in models) {
requestList.add(loader.getVideoData(tempModel, (resultModel) {
videoModels.add(resultModel);
}));
requestList.add(loader.getVideoData(tempModel, (_) {}));
}
await Future.wait(requestList);
videoModels.clear();
videoModels.addAll(models);
setState(() {
if (videoModels.isNotEmpty) {
playCurrentModel(videoModels[0]);
......
......@@ -81,15 +81,16 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
await SuperPlayerPlugin.setConsoleEnabled(true);
await _controller.initialize();
await _controller.setConfig(FTXLivePlayConfig());
// 安卓需要设置hls格式才可正常播放
await _controller.play(_url, playType: TXPlayType.LIVE_FLV);
await _controller.startPlay(_url, playType: TXPlayType.LIVE_FLV);
}
@override
void initState() {
super.initState();
init();
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance.addObserver(this);
EasyLoading.show(status: 'loading...');
}
......@@ -207,7 +208,7 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
),
new GestureDetector(
onTap: () async {
_controller.play(_url, playType: TXPlayType.LIVE_FLV);
_controller.startPlay(_url, playType: TXPlayType.LIVE_FLV);
},
child: Container(
color: Colors.transparent,
......@@ -290,7 +291,7 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
playNetEventSubscription?.cancel();
_controller.dispose();
super.dispose();
WidgetsBinding.instance?.removeObserver(this);
WidgetsBinding.instance.removeObserver(this);
EasyLoading.dismiss();
}
......@@ -302,7 +303,7 @@ class _DemoTXLivelayerState extends State<DemoTXLivePlayer> with WidgetsBindingO
_url = url;
_controller.stop();
if (url.isNotEmpty) {
_controller.play(url);
_controller.startPlay(url);
}
}, showFileEdited: false);
});
......
......@@ -18,7 +18,7 @@ class DemoTXVodPlayer extends StatefulWidget {
class _DemoTXVodlayerState extends State<DemoTXVodPlayer>
with WidgetsBindingObserver {
late TXVodPlayerController _controller;
double _aspectRatio = 0;
double _aspectRatio = 16 / 9;
double _currentProgress = 0.0;
bool _isMute = false;
int _volume = 100;
......@@ -92,7 +92,7 @@ class _DemoTXVodlayerState extends State<DemoTXVodPlayer>
void initState() {
super.initState();
init();
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance.addObserver(this);
EasyLoading.show(status: 'loading...');
}
......@@ -386,7 +386,7 @@ class _DemoTXVodlayerState extends State<DemoTXVodPlayer>
playEventSubscription?.cancel();
_controller.dispose();
super.dispose();
WidgetsBinding.instance?.removeObserver(this);
WidgetsBinding.instance.removeObserver(this);
EasyLoading.dismiss();
}
......
......@@ -6,4 +6,5 @@ class ColorResource {
static const COLOR_GRAY = 0xFFBBBBBB;
static const COLOR_TRANS_BLACK = 0xBB000000;
static const COLOR_WHITE = 0xFFFFFFFF;
static const COLOR_TRANS_GRAY = 0x11BBBBBB;
}
\ No newline at end of file
......@@ -34,7 +34,7 @@ class SuperPlayerCode {
static const VOD_REQUEST_FILE_ID_FAIL = 40002;
}
abstract class SuperPlayerViewEvent {
class SuperPlayerViewEvent {
static const onStartFullScreenPlay = "onStartFullScreenPlay"; //进入全屏播放
static const onStopFullScreenPlay = "onStopFullScreenPlay"; //退出全屏播放
static const onSuperPlayerDidStart = "onSuperPlayerDidStart"; //播放开始通知
......@@ -43,4 +43,11 @@ abstract class SuperPlayerViewEvent {
static const onSuperPlayerBackAction = "onSuperPlayerBackAction"; //返回事件
}
/// 播放器插件当前所处的布局状态
class SuperPlayerUIStatus {
static const WINDOW_MODE = 0;
static const FULLSCREEN_MODE = 1;
static const PIP_MODE = 2;
}
......@@ -5,9 +5,7 @@ part of demo_super_player_lib;
class SuperPlayerController {
static const TAG = "SuperPlayerController";
final StreamController<Map<dynamic, dynamic>> _eventStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _simpleEventStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _playStateStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _playerNetStatusStreamController = StreamController.broadcast();
/// simple event,see SuperPlayerViewEvent
......@@ -16,16 +14,24 @@ class SuperPlayerController {
/// player net status,see TXVodNetEvent
Stream<Map<dynamic, dynamic>> get onPlayerNetStatusBroadcast => _playerNetStatusStreamController.stream;
TXVodPlayerController? _vodPlayerController;
late TXVodPlayerController _vodPlayerController;
late TXLivePlayerController _livePlayerController;
SuperPlayerModel? videoModel;
int _playAction = SuperPlayerModel.PLAY_ACTION_AUTO_PLAY;
PlayInfoProtocol? _currentProtocol;
_SuperPlayerObserver? _observer;
VideoQuality? currentQuality;
List<VideoQuality>? currentQualiyList;
StreamController<TXPlayerHolder> playerStreamController = StreamController.broadcast();
SuperPlayerState playerState = SuperPlayerState.INIT;
SuperPlayerType playerType = SuperPlayerType.VOD;
FTXVodPlayConfig _vodConfig = FTXVodPlayConfig();
FTXLivePlayConfig _liveConfig = FTXLivePlayConfig();
StreamSubscription? _vodPlayEventListener;
StreamSubscription? _vodNetEventListener;
StreamSubscription? _livePlayEventListener;
StreamSubscription? _liveNetEventListener;
String _currentPlayUrl = "";
......@@ -34,12 +40,13 @@ class SuperPlayerController {
bool _needToPause = false;
bool _isMultiBitrateStream = false; // 是否是多码流url播放
bool _changeHWAcceleration = false; // 切换硬解后接收到第一个关键帧前的标记位
bool _isFullScreen = false;
bool _isOpenHWAcceleration = true;
int _playerUIStatus = SuperPlayerUIStatus.WINDOW_MODE;
final BuildContext _context;
int currentDuration = 0;
int videoDuration = 0;
double currentDuration = 0;
double videoDuration = 0;
int _maxLiveProgressTime = 0;
double _seekPos = 0; // 记录切换硬解时的播放时间
/// 该值会改变新播放视频的播放开始时间点
......@@ -50,18 +57,31 @@ class SuperPlayerController {
SuperPlayerController(this._context) {
_initVodPlayer();
_initLivePlayer();
}
void _initVodPlayer() async {
_vodPlayerController = new TXVodPlayerController();
await _vodPlayerController?.initialize();
_vodPlayerController?.onPlayerEventBroadcast.listen((event) async {
await _vodPlayerController.initialize();
_setVodListener();
}
void _initLivePlayer() async {
_livePlayerController = TXLivePlayerController();
await _livePlayerController.initialize();
_setLiveListener();
}
void _setVodListener() {
_vodPlayEventListener?.cancel();
_vodNetEventListener?.cancel();
_vodPlayEventListener = _vodPlayerController.onPlayerEventBroadcast.listen((event) async {
int eventCode = event['event'];
switch (eventCode) {
case TXVodPlayEvent.PLAY_EVT_VOD_PLAY_PREPARED: // vodPrepared
isPrepared = true;
if (_isMultiBitrateStream) {
List<dynamic>? bitrateListTemp = await _vodPlayerController!.getSupportedBitrates();
List<dynamic>? bitrateListTemp = await _vodPlayerController.getSupportedBitrates();
List<FTXBitrateItem> bitrateList = [];
if (null != bitrateListTemp) {
for (Map<dynamic, dynamic> map in bitrateListTemp) {
......@@ -78,7 +98,7 @@ class SuperPlayerController {
videoQualities.add(quality);
}
int? bitrateIndex = await _vodPlayerController?.getBitrateIndex(); //获取默认码率的index
int? bitrateIndex = await _vodPlayerController.getBitrateIndex(); //获取默认码率的index
for (VideoQuality quality in videoQualities) {
if (quality.index == bitrateIndex) {
defaultQuality = quality;
......@@ -88,9 +108,9 @@ class SuperPlayerController {
}
if (_needToPause) {
_vodPlayerController?.pause();
_vodPlayerController.pause();
} else if (_needToResume) {
_vodPlayerController?.resume();
_vodPlayerController.resume();
}
break;
case TXVodPlayEvent.PLAY_EVT_PLAY_LOADING: // PLAY_EVT_PLAY_LOADING
......@@ -111,8 +131,7 @@ class SuperPlayerController {
}
_updatePlayerState(SuperPlayerState.PLAYING);
break;
case TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME:
// PLAY_EVT_RCV_FIRST_I_FRAME
case TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME: // PLAY_EVT_RCV_FIRST_I_FRAME
if (_needToPause) {
return;
}
......@@ -132,19 +151,18 @@ class SuperPlayerController {
dynamic progress = event[TXVodPlayEvent.EVT_PLAY_PROGRESS];
dynamic duration = event[TXVodPlayEvent.EVT_PLAY_DURATION];
if (null != progress) {
currentDuration = progress.toInt(); // 当前时间,转换后的单位 秒
currentDuration = progress.toDouble(); // 当前时间,转换后的单位 秒
}
if (null != duration) {
videoDuration = duration.toInt(); // 总播放时长,转换后的单位 秒
videoDuration = duration.toDouble(); // 总播放时长,转换后的单位 秒
}
if (videoDuration != 0) {
_observer?.onPlayProgress(currentDuration, videoDuration, await getPlayableDuration());
}
break;
}
_eventStreamController.add(event);
});
_vodPlayerController?.onPlayerNetStatusBroadcast.listen((event) {
_vodNetEventListener = _vodPlayerController.onPlayerNetStatusBroadcast.listen((event) {
dynamic wd = (event["VIDEO_WIDTH"]);
dynamic hd = (event["VIDEO_HEIGHT"]);
if (null != wd && null != hd) {
......@@ -163,26 +181,83 @@ class SuperPlayerController {
});
}
void _checkVodPlayerIsInit() {
if (null == _vodPlayerController) {
_initVodPlayer();
}
void _setLiveListener() {
_livePlayEventListener?.cancel();
_liveNetEventListener?.cancel();
_livePlayEventListener = _livePlayerController.onPlayerEventBroadcast.listen((event) async {
int eventCode = event['event'];
switch (eventCode) {
case TXVodPlayEvent.PLAY_EVT_VOD_PLAY_PREPARED: // vodPrepared
case TXVodPlayEvent.PLAY_EVT_PLAY_BEGIN:
_updatePlayerState(SuperPlayerState.PLAYING);
break;
case TXVodPlayEvent.PLAY_ERR_NET_DISCONNECT:
case TXVodPlayEvent.PLAY_EVT_PLAY_END:
if (playerType == SuperPlayerType.LIVE_SHIFT) {
_livePlayerController.resumeLive();
_observer?.onError(SuperPlayerCode.LIVE_SHIFT_FAIL, "时移失败,返回直播");
_updatePlayerState(SuperPlayerState.PLAYING);
} else {
_stop();
if (eventCode == TXVodPlayEvent.PLAY_ERR_NET_DISCONNECT) {
_observer?.onError(SuperPlayerCode.NET_ERROR, "网络不给力,点击重试");
} else {
_observer?.onError(SuperPlayerCode.LIVE_PLAY_END, event[TXVodPlayEvent.EVT_DESCRIPTION]);
}
}
break;
case TXVodPlayEvent.PLAY_EVT_PLAY_LOADING:
_updatePlayerState(SuperPlayerState.LOADING);
break;
case TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME:
_updatePlayerState(SuperPlayerState.PLAYING);
_observer?.onRcvFirstIframe();
break;
case TXVodPlayEvent.PLAY_EVT_STREAM_SWITCH_SUCC:
_observer?.onSwitchStreamEnd(true, SuperPlayerType.LIVE, currentQuality);
break;
case TXVodPlayEvent.PLAY_ERR_STREAM_SWITCH_FAIL:
_observer?.onSwitchStreamEnd(false, SuperPlayerType.LIVE, currentQuality);
break;
case TXVodPlayEvent.PLAY_EVT_PLAY_PROGRESS:
int progress = event[TXVodPlayEvent.EVT_PLAY_PROGRESS_MS];
_maxLiveProgressTime = progress > _maxLiveProgressTime ? progress : _maxLiveProgressTime;
_observer?.onPlayProgress(progress / 1000, _maxLiveProgressTime / 1000, await getPlayableDuration());
break;
}
});
_liveNetEventListener = _livePlayerController.onPlayerNetStatusBroadcast.listen((event) {
dynamic wd = (event["VIDEO_WIDTH"]);
dynamic hd = (event["VIDEO_HEIGHT"]);
if (null != wd && null != hd) {
double w = wd.toDouble();
double h = hd.toDouble();
if (w > 0 && h > 0) {
if (w != videoWidth) {
videoWidth = w;
}
if (h != videoHeight) {
videoHeight = h;
}
}
}
_playerNetStatusStreamController.add(event);
});
}
/// 播放视频
void playWithModel(SuperPlayerModel videoModel) {
Future<void> playWithModel(SuperPlayerModel videoModel) async {
this.videoModel = videoModel;
_playAction = videoModel.playAction;
resetPlayer();
await resetPlayer();
if (_playAction == SuperPlayerModel.PLAY_ACTION_AUTO_PLAY || _playAction == SuperPlayerModel.PLAY_ACTION_PRELOAD) {
_playWithModelInner(videoModel);
await _playWithModelInner(videoModel);
} else {
_observer?.onNewVideoPlay();
}
}
Future<void> _playWithModelInner(SuperPlayerModel videoModel) async {
_checkVodPlayerIsInit();
this.videoModel = videoModel;
_playAction = videoModel.playAction;
_observer?.onVideoImageSpriteAndKeyFrameChanged(null, null);
......@@ -206,7 +281,7 @@ class SuperPlayerController {
}
_playModeVideo(protocol);
_updatePlayerType(SuperPlayerType.VOD);
_observer?.onPlayProgress(0, resultModel.duration, await getPlayableDuration());
_observer?.onPlayProgress(0, resultModel.duration.toDouble(), await getPlayableDuration());
_observer?.onVideoImageSpriteAndKeyFrameChanged(protocol.getImageSpriteInfo(), protocol.getKeyFrameDescInfo());
}, (errCode, message) {
// onError
......@@ -216,8 +291,7 @@ class SuperPlayerController {
}
Future<double> getPlayableDuration() async {
double? playableDuration = await _vodPlayerController?.getPlayableDuration();
return playableDuration ?? 0;
return await _vodPlayerController.getPlayableDuration();
}
void _playModeVideo(PlayInfoProtocol protocol) {
......@@ -250,7 +324,7 @@ class SuperPlayerController {
void _playWithUrl(SuperPlayerModel model) {
List<VideoQuality> videoQualities = [];
VideoQuality? defaultVideoQuality;
late String videoUrl;
String? videoUrl;
if (model.multiVideoURLs.isNotEmpty) {
int i = 0;
for (SuperPlayerUrl superPlayerUrl in model.multiVideoURLs) {
......@@ -267,8 +341,19 @@ class SuperPlayerController {
_observer?.onError(SuperPlayerCode.PLAY_URL_EMPTY, "播放视频失败,播放链接为空");
return;
}
_playVodUrl(videoUrl);
_updatePlayerType(SuperPlayerType.VOD);
if (_isRTMPPlay(videoUrl)) {
_playLiveURL(videoUrl, TXPlayType.LIVE_RTMP);
} else if (_isFLVPlay(videoUrl)) {
_playTimeShiftLiveURL(model.appId, videoUrl);
if (model.multiVideoURLs.isNotEmpty) {
_startMultiStreamLiveURL(videoUrl);
}
} else {
_playVodUrl(videoUrl);
}
bool isLivePlay = (_isRTMPPlay(videoUrl) || _isFLVPlay(videoUrl));
_observer?.onPlayProgress(0, model.duration.toDouble(), 0);
_updatePlayerType(isLivePlay ? SuperPlayerType.LIVE : SuperPlayerType.VOD);
_updateVideoQualityList(videoQualities, defaultVideoQuality);
}
......@@ -278,44 +363,43 @@ class SuperPlayerController {
}
_isMultiBitrateStream = url.contains(".m3u8");
_currentPlayUrl = url;
if (null != _vodPlayerController) {
await _vodPlayerController?.setStartTime(startPos);
if (_playAction == SuperPlayerModel.PLAY_ACTION_PRELOAD) {
await _vodPlayerController?.setAutoPlay(isAutoPlay: false);
_playAction = SuperPlayerModel.PLAY_ACTION_AUTO_PLAY;
} else if (_playAction == SuperPlayerModel.PLAY_ACTION_AUTO_PLAY ||
_playAction == SuperPlayerModel.PLAY_ACTION_MANUAL_PLAY) {
await _vodPlayerController?.setAutoPlay(isAutoPlay: true);
await _vodPlayerController.setStartTime(startPos);
if (_playAction == SuperPlayerModel.PLAY_ACTION_PRELOAD) {
await _vodPlayerController.setAutoPlay(isAutoPlay: false);
_playAction = SuperPlayerModel.PLAY_ACTION_AUTO_PLAY;
} else if (_playAction == SuperPlayerModel.PLAY_ACTION_AUTO_PLAY ||
_playAction == SuperPlayerModel.PLAY_ACTION_MANUAL_PLAY) {
await _vodPlayerController.setAutoPlay(isAutoPlay: true);
}
_setVodListener();
String drmType = "plain";
if (_currentProtocol != null) {
LogUtils.d(TAG, "TOKEN: ${_currentProtocol!.getToken()}");
await _vodPlayerController.setToken(_currentProtocol!.getToken());
if (_currentProtocol!.getDRMType() != null && _currentProtocol!.getDRMType()!.isNotEmpty) {
drmType = _currentProtocol!.getDRMType()!;
}
String drmType = "plain";
if (_currentProtocol != null) {
LogUtils.d(TAG, "TOKEN: ${_currentProtocol!.getToken()}");
await _vodPlayerController?.setToken(_currentProtocol!.getToken());
if (_currentProtocol!.getDRMType() != null && _currentProtocol!.getDRMType()!.isNotEmpty) {
drmType = _currentProtocol!.getDRMType()!;
}
} else {
await _vodPlayerController.setToken(null);
}
if (videoModel!.videoId != null && videoModel!.appId != 0) {
Uri uri = Uri.parse(url);
String query = uri.query;
if (query == null || query.isEmpty) {
query = "";
} else {
await _vodPlayerController?.setToken(null);
}
if (videoModel!.videoId != null && videoModel!.appId != 0) {
Uri uri = Uri.parse(url);
String query = uri.query;
if (query == null || query.isEmpty) {
query = "";
} else {
query = query + "&";
if (query.contains("spfileid") || query.contains("spdrmtype") || query.contains("spappid")) {
LogUtils.d(TAG, "url contains superplay key. $query");
}
query = query + "&";
if (query.contains("spfileid") || query.contains("spdrmtype") || query.contains("spappid")) {
LogUtils.d(TAG, "url contains superplay key. $query");
}
query += "spfileid=${videoModel!.videoId!.fileId}" "&spdrmtype=$drmType&spappid=${videoModel!.appId}";
Uri newUri = Uri(path: url, query: query);
LogUtils.d(TAG, 'playVodURL: newurl = ${Uri.decodeFull(newUri.toString())} ;url= $url');
await _vodPlayerController?.startPlay(Uri.decodeFull(newUri.toString()));
} else {
LogUtils.d(TAG, "playVodURL url:$url");
await _vodPlayerController?.startPlay(url);
}
query += "spfileid=${videoModel!.videoId!.fileId}" "&spdrmtype=$drmType&spappid=${videoModel!.appId}";
Uri newUri = Uri(path: url, query: query);
LogUtils.d(TAG, 'playVodURL: newurl = ${Uri.decodeFull(newUri.toString())} ;url= $url');
await _vodPlayerController.startPlay(Uri.decodeFull(newUri.toString()));
} else {
LogUtils.d(TAG, "playVodURL url:$url");
await _vodPlayerController.startPlay(url);
}
}
......@@ -324,10 +408,10 @@ class SuperPlayerController {
void pause() {
if (playerType == SuperPlayerType.VOD) {
if (isPrepared) {
_vodPlayerController?.pause();
_vodPlayerController.pause();
}
} else {
// todo implements live player
_livePlayerController.pause();
}
_updatePlayerState(SuperPlayerState.PAUSE);
_needToPause = true;
......@@ -338,10 +422,10 @@ class SuperPlayerController {
if (playerType == SuperPlayerType.VOD) {
_needToResume = true;
if (isPrepared) {
_vodPlayerController?.resume();
_vodPlayerController.resume();
}
} else {
// todo implements live player
_livePlayerController.resume();
}
_needToPause = false;
_updatePlayerState(SuperPlayerState.PLAYING);
......@@ -350,17 +434,74 @@ class SuperPlayerController {
/// 重新开始播放视频
Future<void> reStart() async {
if (playerType == SuperPlayerType.LIVE || playerType == SuperPlayerType.LIVE_SHIFT) {
// todo implements live player
if (_isRTMPPlay(_currentPlayUrl)) {
_playLiveURL(_currentPlayUrl, TXPlayType.LIVE_RTMP);
} else if (_isFLVPlay(_currentPlayUrl) && null != videoModel) {
_playTimeShiftLiveURL(videoModel!.appId, _currentPlayUrl);
if (videoModel!.multiVideoURLs.isNotEmpty) {
_startMultiStreamLiveURL(_currentPlayUrl);
}
}
} else {
await _playVodUrl(_currentPlayUrl);
}
}
void _startMultiStreamLiveURL(String url) {
_liveConfig.autoAdjustCacheTime = false;
_liveConfig.maxAutoAdjustCacheTime = 5.0;
_liveConfig.minAutoAdjustCacheTime = 5.0;
_livePlayerController.setConfig(_liveConfig);
_observer?.onPlayTimeShiftLive(_livePlayerController, url);
}
/// 播放直播URL
void _playLiveURL(String url, int playType) async {
_currentPlayUrl = url;
_setLiveListener();
bool result = await _livePlayerController.startPlay(url, playType: playType);
if (result) {
_updatePlayerState(SuperPlayerState.PLAYING);
} else {
LogUtils.e(TAG, "playLiveURL videoURL:$url,result:$result");
}
}
/// 播放时移直播url
void _playTimeShiftLiveURL(int appId, String url) {
String bizid = url.substring(url.indexOf("//") + 2, url.indexOf("."));
String streamid = url.substring(url.lastIndexOf("/") + 1, url.lastIndexOf("."));
LogUtils.i(TAG, "bizid:$bizid,streamid:$streamid,appid:$appId");
_playLiveURL(url, TXPlayType.LIVE_FLV);
}
void _updatePlayerType(SuperPlayerType type) {
if (playerType != type) {
playerType = type;
updatePlayerView();
_observer?.onPlayerTypeChange(type);
}
}
void updatePlayerView() async {
TXPlayerController controller = getCurrentController();
TXPlayerHolder model = TXPlayerHolder(controller);
playerStreamController.sink.add(model);
}
Stream<TXPlayerHolder> getPlayerStream() {
return playerStreamController.stream;
}
/// 获得当前正在使用的controller
TXPlayerController getCurrentController() {
TXPlayerController controller;
if (playerType == SuperPlayerType.VOD) {
controller = _vodPlayerController;
} else {
controller = _livePlayerController;
}
_observer?.onPlayerTypeChange(type);
return controller;
}
void _updatePlayerState(SuperPlayerState state) {
......@@ -384,9 +525,6 @@ class SuperPlayerController {
_addSimpleEvent(SuperPlayerViewEvent.onSuperPlayerDidEnd);
break;
}
Map<String, SuperPlayerState> eventMap = new Map();
eventMap['event'] = state;
_playStateStreamController.add(eventMap);
}
String _getPlayName() {
......@@ -413,22 +551,30 @@ class SuperPlayerController {
_simpleEventStreamController.add(eventMap);
}
void _updateFullScreenState(bool fullScreen) {
_isFullScreen = fullScreen;
if (fullScreen) {
_addSimpleEvent(SuperPlayerViewEvent.onStartFullScreenPlay);
} else {
_addSimpleEvent(SuperPlayerViewEvent.onStopFullScreenPlay);
void _updatePlayerUIStatus(int status) {
if (_playerUIStatus != status) {
_playerUIStatus = status;
if (status == SuperPlayerUIStatus.FULLSCREEN_MODE) {
_addSimpleEvent(SuperPlayerViewEvent.onStartFullScreenPlay);
} else {
_addSimpleEvent(SuperPlayerViewEvent.onStopFullScreenPlay);
}
}
}
/// just for inner invoke
Future<void> _stopPlay() async {
await _vodPlayerController?.stop();
/// 是否是RTMP协议
bool _isRTMPPlay(String? videoURL) {
return null != videoURL && videoURL.startsWith("rtmp");
}
/// 是否是HTTP-FLV协议
bool _isFLVPlay(String? videoURL) {
return (null != videoURL && videoURL.startsWith("http://") || videoURL!.startsWith("https://")) &&
videoURL.contains(".flv");
}
/// 重置播放器状态
void resetPlayer() async {
Future<void> resetPlayer() async {
isPrepared = false;
_needToResume = false;
_needToPause = false;
......@@ -437,24 +583,35 @@ class SuperPlayerController {
currentQuality = null;
currentQualiyList?.clear();
_currentProtocol = null;
await _vodPlayerController?.stop(isNeedClear: true);
// 移除所有事件
_vodPlayEventListener?.cancel();
_vodNetEventListener?.cancel();
_livePlayEventListener?.cancel();
_liveNetEventListener?.cancel();
await _vodPlayerController.stop();
await _livePlayerController.stop();
_updatePlayerState(SuperPlayerState.INIT);
}
void _stop() async {
resetPlayer();
_updatePlayerState(SuperPlayerState.END);
}
/// 释放播放器,播放器释放之后,将不能再使用
Future<void> releasePlayer() async {
// 先移除widget的事件监听
_observer?.onDispose();
resetPlayer();
_vodPlayerController?.dispose();
_vodPlayerController = null;
playerStreamController.close();
_vodPlayerController.dispose();
_livePlayerController.dispose();
}
/// return true : 执行了退出全屏等操作,消耗了返回事件 false:未消耗事件
bool onBackPress() {
if (null != _vodPlayerController && _isFullScreen) {
if (_playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE) {
_observer?.onSysBackPress();
return true;
}
......@@ -469,43 +626,59 @@ class SuperPlayerController {
void switchStream(VideoQuality videoQuality) async {
currentQuality = videoQuality;
if (playerType == SuperPlayerType.VOD) {
if (null != _vodPlayerController) {
if (videoQuality.url.isNotEmpty) {
// url stream need manual seek
double currentTime = await _vodPlayerController!.getCurrentPlaybackTime();
await _vodPlayerController?.stop(isNeedClear: false);
LogUtils.d(TAG, "onQualitySelect quality.url:${videoQuality.url}");
await _vodPlayerController?.setStartTime(currentTime);
await _vodPlayerController?.startPlay(videoQuality.url);
} else {
LogUtils.d(TAG, "setBitrateIndex quality.index:${videoQuality.index}");
await _vodPlayerController?.setBitrateIndex(videoQuality.index);
}
_observer?.onSwitchStreamStart(true, SuperPlayerType.VOD, videoQuality);
if (videoQuality.url.isNotEmpty) {
// url stream need manual seek
double currentTime = await _vodPlayerController.getCurrentPlaybackTime();
await _vodPlayerController.stop(isNeedClear: false);
LogUtils.d(TAG, "onQualitySelect quality.url:${videoQuality.url}");
await _vodPlayerController.setStartTime(currentTime);
await _vodPlayerController.startPlay(videoQuality.url);
} else {
LogUtils.d(TAG, "setBitrateIndex quality.index:${videoQuality.index}");
await _vodPlayerController.setBitrateIndex(videoQuality.index);
}
_observer?.onSwitchStreamStart(true, SuperPlayerType.VOD, videoQuality);
} else {
// todo implements live player
bool success = false;
if (videoQuality.url.isNotEmpty) {
int result = await _livePlayerController.switchStream(videoQuality.url);
success = result >= 0;
}
_observer?.onSwitchStreamStart(success, SuperPlayerType.LIVE, videoQuality);
}
}
/// seek 到需要的时间点进行播放
Future<void> seek(double progress) async {
if (playerType == SuperPlayerType.VOD) {
await _vodPlayerController?.seek(progress);
bool? isPlaying = await _vodPlayerController?.isPlaying();
await _vodPlayerController.seek(progress);
bool isPlaying = await _vodPlayerController.isPlaying();
// resume when not playing.if isPlaying is null,not resume
if (!(isPlaying ?? true)) {
if (!isPlaying) {
resume();
}
} else {
// todo implements live player
_updatePlayerType(SuperPlayerType.LIVE_SHIFT);
_livePlayerController.seek(progress);
bool isPlaying = await _livePlayerController.isPlaying();
// resume when not playing.if isPlaying is null,not resume
if (!isPlaying) {
resume();
}
}
_observer?.onSeek(progress);
}
/// 配置播放器
/// 配置点播播放器
Future<void> setPlayConfig(FTXVodPlayConfig config) async {
await _vodPlayerController?.setConfig(config);
_vodConfig = config;
await _vodPlayerController.setConfig(config);
}
/// 配置直播播放器
Future<void> setLiveConfig(FTXLivePlayConfig config) async {
_liveConfig = config;
await _livePlayerController.setConfig(config);
}
/// 进入画中画模式,进入画中画模式,需要适配画中画模式的界面,安卓只支持7.0以上机型
......@@ -514,39 +687,52 @@ class SuperPlayerController {
/// </h1>
/// @param backIcon playIcon pauseIcon forwardIcon 为播放后退、播放、暂停、前进的图标,如果赋值的话,将会使用传递的图标,否则
/// 使用系统默认图标,只支持flutter本地资源图片,传递的时候,与flutter使用图片资源一致,例如: images/back_icon.png
Future<int?> enterPictureInPictureMode(
Future<int> enterPictureInPictureMode(
{String? backIcon, String? playIcon, String? pauseIcon, String? forwardIcon}) async {
return _vodPlayerController?.enterPictureInPictureMode(
backIcon: backIcon, playIcon: playIcon, pauseIcon: pauseIcon, forwardIcon: forwardIcon);
if (_playerUIStatus == SuperPlayerUIStatus.WINDOW_MODE) {
if (playerType == SuperPlayerType.VOD) {
return _vodPlayerController.enterPictureInPictureMode(
backIconForAndroid: backIcon,
playIconForAndroid: playIcon,
pauseIconForAndroid: pauseIcon,
forwardIconForAndroid: forwardIcon);
} else {
return _livePlayerController.enterPictureInPictureMode(
backIconForAndroid: backIcon,
playIconForAndroid: playIcon,
pauseIconForAndroid: pauseIcon,
forwardIconForAndroid: forwardIcon);
}
}
return TXVodPlayEvent.ERROR_PIP_CAN_NOT_ENTER;
}
/// 开关硬解编码播放
Future<void> enableHardwareDecode(bool enable) async {
_isOpenHWAcceleration = enable;
if (playerType == SuperPlayerType.VOD) {
if (null != _vodPlayerController) {
await _vodPlayerController?.enableHardwareDecode(enable);
if (playerState != SuperPlayerState.END) {
_changeHWAcceleration = true;
_seekPos = await _vodPlayerController!.getCurrentPlaybackTime();
LogUtils.d(TAG, "seek pos $_seekPos");
resetPlayer();
// 当protocol为空时,则说明当前播放视频为非v2和v4视频
if (_currentProtocol == null) {
_playUrlVideo(videoModel);
} else {
_playModeVideo(_currentProtocol!);
}
await _vodPlayerController.enableHardwareDecode(enable);
if (playerState != SuperPlayerState.END) {
_changeHWAcceleration = true;
_seekPos = await _vodPlayerController.getCurrentPlaybackTime();
LogUtils.d(TAG, "seek pos $_seekPos");
resetPlayer();
// 当protocol为空时,则说明当前播放视频为非v2和v4视频
if (_currentProtocol == null) {
_playUrlVideo(videoModel);
} else {
_playModeVideo(_currentProtocol!);
}
}
} else {
// todo implements live player
await _vodPlayerController.enableHardwareDecode(enable);
await playWithModel(videoModel!);
}
}
Future<void> setPlayRate(double rate) async {
currentPlayRate = rate;
_vodPlayerController?.setRate(rate);
_vodPlayerController.setRate(rate);
}
/// 获得当前播放器状态
......
......@@ -8,10 +8,10 @@ class _SuperPlayerObserver {
Function onPlayPause;
Function onPlayStop;
Function onPlayLoading;
Function(int current, int duration,double playableDuration) onPlayProgress;
Function(double current, double duration,double playableDuration) onPlayProgress;
Function(double position) onSeek;
Function(bool success, SuperPlayerType playerType, VideoQuality quality) onSwitchStreamStart;
Function(bool success, SuperPlayerType playerType, VideoQuality quality) onSwitchStreamEnd;
Function(bool success, SuperPlayerType playerType, VideoQuality? quality) onSwitchStreamStart;
Function(bool success, SuperPlayerType playerType, VideoQuality? quality) onSwitchStreamEnd;
Function(int code, String msg) onError;
Function(SuperPlayerType playerType) onPlayerTypeChange;
Function(TXLivePlayerController controller, String url) onPlayTimeShiftLive;
......
......@@ -17,15 +17,15 @@ class VideoBottomView extends StatefulWidget {
class _VideoBottomViewState extends State<VideoBottomView> {
static const TAG = "VideoBottomView";
double _currentProgress = 0;
double _playableProgress = 0;
int _currentDuration = 0;
int _videoDuration = 0;
double _currentDuration = 0;
double _videoDuration = 0;
double _bufferedDuration = 0;
bool _showFullScreenBtn = true;
bool _isPlayMode = false;
bool _isShowQuality = false; // only showed on fullscreen mode
bool _isOnDraging = false;
SuperPlayerType _playerType = SuperPlayerType.VOD;
VideoQuality? _currentQuality;
@override
......@@ -34,13 +34,15 @@ class _VideoBottomViewState extends State<VideoBottomView> {
_videoDuration = widget._playerController.videoDuration;
_currentDuration = widget._playerController.currentDuration;
} else if (null != widget._playerController.videoModel) {
_videoDuration = widget._playerController.videoModel!.duration;
_videoDuration = widget._playerController.videoModel!.duration.toDouble();
}
_isPlayMode = (widget._playerController.playerState == SuperPlayerState.PLAYING);
_showFullScreenBtn = !widget._playerController._isFullScreen;
_isShowQuality = widget._playerController._isFullScreen;
bool isFullScreen = widget._playerController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE;
_showFullScreenBtn = !isFullScreen;
_isShowQuality = isFullScreen;
_currentQuality = widget._playerController.currentQuality;
_playerType = widget._playerController.playerType;
_fixProgress();
super.initState();
......@@ -128,8 +130,8 @@ class _VideoBottomViewState extends State<VideoBottomView> {
return Expanded(
child: VideoSlider(
min: 0,
max: 1,
value: _currentProgress,
max: _videoDuration,
value: _currentDuration,
bufferedValue: _playableProgress,
activeColor: Color(ColorResource.COLOR_MAIN_THEME),
inactiveColor: Color(ColorResource.COLOR_GRAY),
......@@ -138,16 +140,18 @@ class _VideoBottomViewState extends State<VideoBottomView> {
progressHeight: 2,
sliderRadius: 4,
sliderOutterRadius: 10,
// 直播禁止时移
canDrag: _playerType == SuperPlayerType.VOD,
onDragUpdate: (value) {
_isOnDraging = true;
},
onDragEnd: (value) {
setState(() {
_isOnDraging = false;
_currentProgress = value;
widget._playerController.seek(_currentProgress * _videoDuration);
_currentDuration = value * _videoDuration;
widget._playerController.seek(_currentDuration);
LogUtils.d(TAG,
"_currentProgress:$_currentProgress,_videoDuration:$_videoDuration,currentDuration:${_currentProgress * _videoDuration}");
"_currentDuration:$_currentDuration,_videoDuration:$_videoDuration");
});
},
),
......@@ -166,7 +170,7 @@ class _VideoBottomViewState extends State<VideoBottomView> {
widget._controller.onTapQuality();
}
void updateDuration(int duration, int videoDuration, double bufferedDration) {
void updateDuration(double duration, double videoDuration, double bufferedDration) {
if (_isOnDraging) {
return;
}
......@@ -183,19 +187,6 @@ class _VideoBottomViewState extends State<VideoBottomView> {
}
void _fixProgress() {
// provent division zero problem
if (_videoDuration == 0) {
_currentProgress = 0;
} else {
_currentProgress = _currentDuration / _videoDuration;
}
if (_currentProgress < 0) {
_currentProgress = 0;
}
if (_currentProgress > 1) {
_currentProgress = 1;
}
if (_bufferedDuration == 0) {
_playableProgress = 0;
} else {
......@@ -218,15 +209,24 @@ class _VideoBottomViewState extends State<VideoBottomView> {
}
}
void updateFullScreen(bool showFullScreen) {
void updatePlayerType(SuperPlayerType type) {
if(_playerType != type) {
setState(() {
_playerType = type;
});
}
}
void updateUIStatus(int status) {
setState(() {
_showFullScreenBtn = !showFullScreen;
_isShowQuality = showFullScreen;
bool isFullScreen = widget._playerController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE;
_showFullScreenBtn = !isFullScreen;
_isShowQuality = isFullScreen;
});
}
String _buildTextString(int time) {
Duration duration = Duration(seconds: time);
String _buildTextString(double time) {
Duration duration = Duration(seconds: time.toInt());
// 返回此持续时间跨越的整秒数。
String inSeconds = (duration.inSeconds % 60).toString().padLeft(2, "0");
// 返回此持续时间跨越的整分钟数。
......
......@@ -43,7 +43,7 @@ class _SuperPlayerCoverViewState extends State<SuperPlayerCoverView> {
}
return Visibility(
visible: _isShowCover,
visible: _isShowCover && hasCover,
child: Positioned.fill(
top: topBottomOffset,
bottom: topBottomOffset,
......@@ -53,7 +53,11 @@ class _SuperPlayerCoverViewState extends State<SuperPlayerCoverView> {
onDoubleTap: _onDoubleTapVideo,
onTap: _onSingleTapVideo,
child: Container(
child: hasCover ? Image.network(coverUrl,fit: BoxFit.cover,) : Container(),
decoration: BoxDecoration(
// 增加一个半透明背景,防止透明封面图的出现
color:Color(ColorResource.COLOR_TRANS_GRAY)
),
child: Image.network(coverUrl,fit: BoxFit.cover),
)
)),
);
......
......@@ -18,6 +18,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
double _currentVolumn = 0;
bool _isShowMoreView = false;
bool _isOpenAccelerate = true;
bool _isVodPlay = false;
String _currentRate = "";
Map<String, double> playRateStr = {"1.0x": 1.0, "1.25x": 1.25, "1.5x": 1.5, "2.0x": 2.0};
StreamSubscription? volumeSubscription;
......@@ -25,6 +26,7 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
@override
void initState() {
super.initState();
_isVodPlay = widget.controller.getIsVodPlay();
double playerPlayRate = widget.controller.getPlayRate();
for(String rateStr in playRateStr.keys) {
if(playerPlayRate == playRateStr[rateStr]) {
......@@ -132,10 +134,13 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
),
));
}
return Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
child: Row(
children: playRateChild,
return Visibility(
visible: _isVodPlay,
child: Container(
margin: EdgeInsets.only(top: 10, bottom: 10),
child: Row(
children: playRateChild,
),
),
);
}
......@@ -243,6 +248,15 @@ class _SuperPlayerMoreViewState extends State<SuperPlayerMoreView> {
}
}
void updatePlayerType(SuperPlayerType playerType) {
bool isVodPlay = playerType == SuperPlayerType.VOD;
if(isVodPlay != _isVodPlay) {
setState(() {
_isVodPlay = isVodPlay;
});
}
}
@override
void dispose() {
super.dispose();
......@@ -255,6 +269,7 @@ class _MoreViewController {
DoubleFunction getPlayRate;
Function(bool value) siwtchAccelerate;
Function(double playRate) onChangedPlayRate;
BoolFunction getIsVodPlay;
_MoreViewController(this.getAccelerateIsOpen, this.getPlayRate, this.siwtchAccelerate, this.onChangedPlayRate);
_MoreViewController(this.getAccelerateIsOpen, this.getPlayRate, this.siwtchAccelerate, this.onChangedPlayRate, this.getIsVodPlay);
}
......@@ -76,9 +76,9 @@ class _VideoTitleViewState extends State<_VideoTitleView> {
}
}
void updateFullScreen(bool showFullScreen) {
void updateUIStatus(int status) {
setState(() {
_isFullScreen = showFullScreen;
_isFullScreen = status == SuperPlayerUIStatus.FULLSCREEN_MODE;
});
}
}
......
......@@ -17,6 +17,7 @@ class VideoSlider extends StatefulWidget {
final Color? bufferedColor;
final Color? sliderColor;
final Color? sliderOutterColor;
bool? canDrag = true;
// calback
final Function? onDragStart;
......@@ -38,12 +39,16 @@ class VideoSlider extends StatefulWidget {
this.sliderOutterColor,
this.onDragStart,
this.onDragUpdate,
this.onDragEnd}) {
this.onDragEnd,
this.canDrag}) {
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);
if(range <= 0) {
controller = _VideoSliderController(1, bufferedProgress: 1);
} else {
double currentProgress = value / range;
double? bufferedProgress = bufferedValue != null ? bufferedValue! / range : null;
controller = _VideoSliderController(currentProgress, bufferedProgress: bufferedProgress);
}
}
@override
......@@ -77,24 +82,31 @@ class VideoSliderState extends State<VideoSlider> {
double rightPadding = overlayRadius;
return GestureDetector(
onHorizontalDragStart: (DragStartDetails details) {
isDraging = true;
widget.onDragStart?.call();
if(widget.canDrag!) {
isDraging = true;
widget.onDragStart?.call();
}
},
onHorizontalDragUpdate: (DragUpdateDetails details) {
isDraging = true;
_seekToPosition(details.globalPosition);
widget.onDragUpdate?.call(widget.controller.progress);
if(widget.canDrag!) {
isDraging = true;
_seekToPosition(details.globalPosition);
widget.onDragUpdate?.call(widget.controller.progress);}
},
onHorizontalDragEnd: (DragEndDetails details) {
isDraging = false;
widget.onDragEnd?.call(widget.controller.progress);
if(widget.canDrag!) {
isDraging = false;
widget.onDragEnd?.call(widget.controller.progress);}
},
onHorizontalDragCancel: () {
isDraging = false;
widget.onDragEnd?.call(widget.controller.progress);
if(widget.canDrag!) {
isDraging = false;
widget.onDragEnd?.call(widget.controller.progress);}
},
onTapDown: (TapDownDetails details) {
_seekToPosition(details.globalPosition);
if(widget.canDrag!) {
_seekToPosition(details.globalPosition);
}
},
child: Center(
child: Container(
......
......@@ -20,7 +20,6 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
static const TAG = "SuperPlayerViewState";
late SuperPlayerController _playController;
bool _isFullScreen = false;
bool _isFloatingMode = false;
bool _isPlaying = false;
bool _isLoading = true;
......@@ -28,8 +27,6 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
double _radioWidth = 0;
double _radioHeight = 0;
double _topAndBottomMargin = 0;
double _leftAndRightMargin = 0;
double _aspectRatio = 16.0 / 9.0;
double _videoWidth = 0;
double _videoHeight = 0;
......@@ -56,6 +53,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
GlobalKey<_VideoTitleViewState> _videoTitleKey = GlobalKey();
GlobalKey<_SuperPlayerCoverViewState> _coverViewKey = GlobalKey();
GlobalKey<_SuperPlayerMoreViewState> _moreViewKey = GlobalKey();
GlobalKey<_SuperPlayerFloatState> floatPlayerKey = GlobalKey();
@override
void initState() {
......@@ -74,7 +72,8 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
() => _playController._isOpenHWAcceleration,
() => _playController.currentPlayRate,
(value) => _playController.enableHardwareDecode(value),
(playRate) => _playController.setPlayRate(playRate));
(playRate) => _playController.setPlayRate(playRate),
() => _playController.playerType == SuperPlayerType.VOD);
_playController.onPlayerNetStatusBroadcast.listen((event) {
dynamic wd = (event["VIDEO_WIDTH"]);
dynamic hd = (event["VIDEO_HEIGHT"]);
......@@ -94,7 +93,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
Navigator.of(context).pop();
if (_isPlaying) {
// pause play when exit PIP, prevent user just close PIP, but not back to app
_playController._vodPlayerController?.pause();
_playController.getCurrentController().pause();
}
} else if (Platform.isIOS) {
EasyLoading.dismiss();
......@@ -106,7 +105,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
// enter floatingMode
if (Platform.isAndroid) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return SuperPlayerFloatView(_playController, _aspectRatio);
return SuperPlayerFloatView(_playController, _aspectRatio, key: floatPlayerKey);
}));
} else if (Platform.isIOS) {
EasyLoading.showToast(StringResource.OPEN_PIP);
......@@ -138,6 +137,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
_onResume();
}
}
// 画中画模式下,不进行旋转屏幕操作
if (eventCode == TXVodPlayEvent.EVENT_ORIENTATION_CHANGED && !_isFloatingMode) {
int orientation = event[TXVodPlayEvent.EXTRA_NAME_ORIENTATION];
......@@ -151,11 +151,12 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
void _registerObserver() {
_playController._observer = _SuperPlayerObserver(() {
// onNewVideoPlay
_isFullScreen = false;
_isPlaying = false;
_isShowControlView = false;
_isShowCover = true;
_isLoading = true;
setState(() {
_isPlaying = false;
_isShowControlView = false;
_isShowCover = true;
_isLoading = true;
});
}, () {
// onPlayPrepare
_isShowCover = true;
......@@ -186,6 +187,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
}, (current, duration, playableDuration) {
// onPlayProgress
_videoBottomKey.currentState?.updateDuration(current, duration, playableDuration);
floatPlayerKey.currentState?.updateDuration(current, duration);
}, (position) {
// onSeek
}, (success, playerType, quality) {
......@@ -197,6 +199,8 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
_togglePlayUIState(false);
}, (playerType) {
// onPlayerTypeChange
_videoBottomKey.currentState?.updatePlayerType(playerType);
_moreViewKey.currentState?.updatePlayerType(playerType);
}, (controller, url) {
// onPlayTimeShiftLive
}, (qualityList, defaultQuality) {
......@@ -207,9 +211,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
// onVideoImageSpriteAndKeyFrameChanged
}, () {
// onSysBackPress
if (_isFullScreen) {
_onControlFullScreen();
}
_onControlFullScreen();
}, () {
// onDispose
_playController._observer = null; // close observer
......@@ -218,23 +220,23 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return SuperPlayerFullScreenView(_playController, _superPlayerFullUIController);
}));
WidgetsBinding.instance?.removeObserver(this);
_videoBottomKey.currentState?.updateFullScreen(true);
_videoTitleKey.currentState?.updateFullScreen(true);
_playController._updateFullScreenState(true);
WidgetsBinding.instance.removeObserver(this);
_videoBottomKey.currentState?.updateUIStatus(SuperPlayerUIStatus.FULLSCREEN_MODE);
_videoTitleKey.currentState?.updateUIStatus(SuperPlayerUIStatus.FULLSCREEN_MODE);
_playController._updatePlayerUIStatus(SuperPlayerUIStatus.FULLSCREEN_MODE);
hideControlView();
}, () {
Navigator.of(context).pop();
_videoBottomKey.currentState?.updateFullScreen(false);
_videoTitleKey.currentState?.updateFullScreen(false);
_playController._updateFullScreenState(false);
_videoBottomKey.currentState?.updateUIStatus(SuperPlayerUIStatus.WINDOW_MODE);
_videoTitleKey.currentState?.updateUIStatus(SuperPlayerUIStatus.WINDOW_MODE);
_playController._updatePlayerUIStatus(SuperPlayerUIStatus.WINDOW_MODE);
hideControlView();
});
WidgetsBinding.instance?.addObserver(this);
WidgetsBinding.instance.addObserver(this);
}
void _initPlayerState() {
_isFullScreen = _playController._isFullScreen;
SuperPlayerState superPlayerState = _playController.getPlayerState();
switch (superPlayerState) {
case SuperPlayerState.PLAYING:
......@@ -261,7 +263,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
_registerObserver();
// 由于pop之后,无法触发任何回调,并且fulscreen的controller调用setState无效,
// 所以这里向UI线程添加一个任务,这个任务会在回到这个界面之后触发,来保证播放状态正确。
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) => setState(() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => setState(() {
_initPlayerState();
_resizeVideo();
}));
......@@ -273,9 +275,10 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
if (state == AppLifecycleState.resumed) {
// 页面从后台回来
// 不更新状态,直接resume
_playController._vodPlayerController?.resume();
_playController.getCurrentController().resume();
// 从后台回来之后,如果手机横竖屏状态发生更改,被改为竖屏,那么这里根据判断切换横屏
if (_isFullScreen && defaultTargetPlatform == TargetPlatform.iOS) {
if (_playController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE &&
defaultTargetPlatform == TargetPlatform.iOS) {
Orientation currentOrientation = MediaQuery.of(context).orientation;
bool isLandscape = currentOrientation == Orientation.landscape;
if (!isLandscape) {
......@@ -285,13 +288,14 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
} else if (state == AppLifecycleState.inactive) {
// 页面退到后台
// 不更新状态,直接pause
_playController._vodPlayerController?.pause();
_playController.getCurrentController().pause();
}
}
}
void _calculateSize(double videoWidth, double videoHeight) {
if ((0 != videoWidth && 0 != videoHeight) && (_videoWidth != videoWidth && _videoHeight != videoHeight)) {
floatPlayerKey.currentState?._calculateSize(videoWidth, videoHeight);
_videoWidth = videoWidth;
_videoHeight = videoHeight;
_resizeVideo();
......@@ -306,7 +310,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
// 当有视频宽高数据的时候,按照 全屏:高度为基准,计算宽度。 非全屏:宽度为基准,计算高度的方式进行宽高计算
// 当没有视频宽高数据的时候,按照 全屏:等于屏幕宽高。 非全屏:16:9 的方式进行宽高计算
if (_videoWidth <= 0 || _videoHeight <= 0) {
if (_isFullScreen) {
if (_playController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE) {
_radioWidth = isLandscape ? size.width : size.height;
_radioHeight = isLandscape ? size.height : size.width;
_aspectRatio = _radioWidth / _radioHeight;
......@@ -316,7 +320,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
_aspectRatio = 16.0 / 9.0;
}
} else {
if (_isFullScreen) {
if (_playController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE) {
double playerHeight = isLandscape ? size.width : size.height;
// remain height
double videoRadio = _videoWidth / _videoHeight;
......@@ -359,20 +363,23 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
Widget _getPipEnterView() {
return Visibility(
visible: _isShowControlView && !_isFullScreen,
visible: _isShowControlView &&
_playController._playerUIStatus == SuperPlayerUIStatus.WINDOW_MODE,
child: Positioned(
right: 10,
top: 0,
bottom: 0,
child: Center(
child: InkWell(
onTap: _onEnterPipMode,
child: Image(
width: 30,
height: 30,
image: AssetImage("images/ic_pip_play_icon.png"),
),
),
onTap: _onEnterPipMode,
child: Container(
padding: EdgeInsets.all(5), // expand click area
child: Image(
width: 30,
height: 30,
image: AssetImage("images/ic_pip_play_icon.png"),
),
)),
),
),
);
......@@ -449,7 +456,9 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
splashColor: Colors.transparent,
child: Center(
child: AspectRatio(
aspectRatio: _aspectRatio, child: TXPlayerVideo(controller: _playController._vodPlayerController!)),
aspectRatio: _aspectRatio,
child: TXPlayerVideo(
controller: _playController.getCurrentController(), playerStream: _playController.getPlayerStream())),
),
);
}
......@@ -461,34 +470,34 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
top: topBottomOffset,
left: 0,
right: 0,
child: _VideoTitleView(_titleViewController, _isFullScreen, _playController._getPlayName(), _videoTitleKey),
child: _VideoTitleView(
_titleViewController,
_playController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE,
_playController._getPlayName(),
_videoTitleKey),
),
);
}
void _onEnterPipMode() async {
if (!_isFloatingMode) {
int? result = await _playController.enterPictureInPictureMode(
int result = await _playController.enterPictureInPictureMode(
backIcon: "images/ic_pip_play_replay.png",
playIcon: "images/ic_pip_play_normal.png",
pauseIcon: "images/ic_pip_play_pause.png",
forwardIcon: "images/ic_pip_play_forward.png");
if (null != result) {
String failedStr = "";
if (result != TXVodPlayEvent.NO_ERROR) {
if (result == TXVodPlayEvent.ERROR_PIP_LOWER_VERSION) {
failedStr = "enterPip failed,because android version is too low,Minimum supported version is android 24";
} else if (result == TXVodPlayEvent.ERROR_PIP_DENIED_PERMISSION) {
failedStr = "enterPip failed,because PIP feature is disabled or device not support";
} else if (result == TXVodPlayEvent.ERROR_PIP_ACTIVITY_DESTROYED) {
failedStr = "enterPip failed,because activity is destroyed";
} else {
failedStr = "enterPip failed,unkonw error";
}
LogUtils.e(TAG, failedStr);
String failedStr = "";
if (result != TXVodPlayEvent.NO_ERROR) {
if (result == TXVodPlayEvent.ERROR_PIP_LOWER_VERSION) {
failedStr = "enterPip failed,because android version is too low,Minimum supported version is android 24";
} else if (result == TXVodPlayEvent.ERROR_PIP_DENIED_PERMISSION) {
failedStr = "enterPip failed,because PIP feature is disabled or device not support";
} else if (result == TXVodPlayEvent.ERROR_PIP_ACTIVITY_DESTROYED) {
failedStr = "enterPip failed,because activity is destroyed";
} else {
failedStr = "enterPip failed,unkonw error";
}
} else {
LogUtils.e(TAG, "enterPip failed, vodPlayer is release");
LogUtils.e(TAG, failedStr);
}
}
}
......@@ -508,7 +517,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
}
void _onTapBack() async {
if (_isFullScreen) {
if (_playController._playerUIStatus == SuperPlayerUIStatus.FULLSCREEN_MODE) {
_onControlFullScreen();
}
_playController._onBackTap();
......@@ -563,9 +572,10 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
}
void _onControlFullScreen() {
_isFullScreen = !_isFullScreen;
_handleFullScreen(_isFullScreen);
_playController._updateFullScreenState(_isFullScreen);
if (_playController._playerUIStatus != SuperPlayerUIStatus.PIP_MODE) {
bool toSwitchFullScreen = _playController._playerUIStatus == SuperPlayerUIStatus.WINDOW_MODE;
_handleFullScreen(toSwitchFullScreen);
}
}
void _onControlQualityListView() {
......@@ -623,7 +633,7 @@ class SuperPlayerViewState extends State<SuperPlayerView> with WidgetsBindingObs
@override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
WidgetsBinding.instance.removeObserver(this);
_pipSubscription?.cancel();
_volumeSubscription?.cancel();
super.dispose();
......@@ -687,42 +697,24 @@ class SuperPlayerFloatView extends StatefulWidget {
final SuperPlayerController _controller;
final double initAspectRatio;
SuperPlayerFloatView(this._controller, this.initAspectRatio);
SuperPlayerFloatView(this._controller, this.initAspectRatio, {Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _SuperPlayerFloatState();
}
class _SuperPlayerFloatState extends State<SuperPlayerFloatView> {
double _currentProgress = 0;
int _videoDuration = 0;
int _currentDuration = 0;
double _videoDuration = 0;
double _currentDuration = 0;
double _aspectRatio = 16.0 / 9.0;
double _videoWidth = 0;
double _videoHeight = 0;
StreamSubscription? streamSubscription;
StreamSubscription? sizeStreamSubscription;
@override
void initState() {
super.initState();
_aspectRatio = widget.initAspectRatio;
streamSubscription = widget._controller._vodPlayerController?.onPlayerEventBroadcast.listen((event) {
int eventCode = event['event'];
if (eventCode == TXVodPlayEvent.PLAY_EVT_PLAY_PROGRESS) {
updateDuration(widget._controller.currentDuration, widget._controller.videoDuration);
}
});
sizeStreamSubscription = widget._controller.onPlayerNetStatusBroadcast.listen((event) {
dynamic wd = (event["VIDEO_WIDTH"]);
dynamic hd = (event["VIDEO_HEIGHT"]);
if (null != wd && null != hd) {
double w = wd.toDouble();
double h = hd.toDouble();
_calculateSize(w, h);
}
});
}
void _calculateSize(double videoWidth, double videoHeight) {
......@@ -743,33 +735,17 @@ class _SuperPlayerFloatState extends State<SuperPlayerFloatView> {
}
}
void updateDuration(int duration, int videoDuration) {
void updateDuration(double duration, double videoDuration) {
if (duration != _currentDuration || _videoDuration != videoDuration) {
if (duration <= videoDuration) {
setState(() {
_currentDuration = duration;
_videoDuration = videoDuration;
_fixProgress();
});
}
}
}
void _fixProgress() {
// provent division zero problem
if (_videoDuration == 0) {
_currentProgress = 0;
} else {
_currentProgress = _currentDuration / _videoDuration;
}
if (_currentProgress < 0) {
_currentProgress = 0;
}
if (_currentProgress > 1) {
_currentProgress = 1;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
......@@ -782,7 +758,9 @@ class _SuperPlayerFloatState extends State<SuperPlayerFloatView> {
Center(
child: AspectRatio(
aspectRatio: _aspectRatio,
child: TXPlayerVideo(controller: widget._controller._vodPlayerController!),
child: TXPlayerVideo(
controller: widget._controller.getCurrentController(),
),
),
),
Positioned(
......@@ -801,11 +779,11 @@ class _SuperPlayerFloatState extends State<SuperPlayerFloatView> {
data: ThemeResource.getMiniSliderTheme(),
child: Slider(
min: 0,
max: 1,
value: _currentProgress,
max: _videoDuration,
value: _currentDuration,
onChanged: (double value) {
setState(() {
_currentProgress = value;
_currentDuration = value;
});
},
));
......@@ -815,7 +793,6 @@ class _SuperPlayerFloatState extends State<SuperPlayerFloatView> {
void dispose() {
super.dispose();
// 移除的时候,解除对进度事件的订阅
streamSubscription?.cancel();
sizeStreamSubscription?.cancel();
}
}
......
......@@ -7,7 +7,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=1.20.0"
flutter: ">=2.0.0"
dependencies:
flutter:
......
......@@ -2,6 +2,7 @@
#import "FTXLivePlayer.h"
#import "FTXPlayerEventSinkQueue.h"
#import "FTXTransformation.h"
#import <TXLiteAVSDK_Professional/TXLiteAVSDK.h>
#import <Flutter/Flutter.h>
#import <stdatomic.h>
......@@ -25,7 +26,7 @@ static const int uninitialized = -1;
// 旧的一帧
CVPixelBufferRef _lastBuffer;
int64_t _textureId;
id<FlutterPluginRegistrar> _registrar;
id<FlutterTextureRegistry> _textureRegistry;
}
......@@ -39,20 +40,20 @@ static const int uninitialized = -1;
_textureId = -1;
_eventSink = [FTXPlayerEventSinkQueue new];
_netStatusSink = [FTXPlayerEventSinkQueue new];
__weak typeof(self) weakSelf = self;
_methodChannel = [FlutterMethodChannel methodChannelWithName:[@"cloud.tencent.com/txliveplayer/" stringByAppendingString:[self.playerId stringValue]] binaryMessenger:[registrar messenger]];
[_methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
[weakSelf handleMethodCall:call result:result];
}];
_eventChannel = [FlutterEventChannel eventChannelWithName:[@"cloud.tencent.com/txliveplayer/event/" stringByAppendingString:[self.playerId stringValue]] binaryMessenger:[registrar messenger]];
[_eventChannel setStreamHandler:self];
_netStatusChannel = [FlutterEventChannel eventChannelWithName:[@"cloud.tencent.com/txliveplayer/net/" stringByAppendingString:[self.playerId stringValue]] binaryMessenger:[registrar messenger]];
[_netStatusChannel setStreamHandler:self];
}
return self;
}
......@@ -61,7 +62,7 @@ static const int uninitialized = -1;
[self stopPlay];
[_txLivePlayer removeVideoWidget];
_txLivePlayer = nil;
if (_textureId >= 0) {
[_textureRegistry unregisterTexture:_textureId];
_textureId = -1;
......@@ -81,7 +82,7 @@ static const int uninitialized = -1;
CVPixelBufferRelease(_lastBuffer);
_lastBuffer = nil;
}
[_methodChannel setMethodCallHandler:nil];
_methodChannel = nil;
......@@ -101,7 +102,7 @@ static const int uninitialized = -1;
int64_t tId = [_textureRegistry registerTexture:self];
_textureId = tId;
}
if (_txLivePlayer != nil) {
TXLivePlayConfig *config = [TXLivePlayConfig new];
[config setPlayerPixelFormatType:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange];
......@@ -137,11 +138,12 @@ static const int uninitialized = -1;
}
}
- (void)switchStream:(NSString *)url
- (int)switchStream:(NSString *)url
{
if (_txLivePlayer != nil) {
[_txLivePlayer switchStream:url];
return [_txLivePlayer switchStream:url];
}
return -1;
}
- (int)seek:(float)progress
......@@ -207,7 +209,7 @@ static const int uninitialized = -1;
- (void)setLiveMode:(int)type
{
TXLivePlayConfig *config = _txLivePlayer.config;
if (type == 0) {
//自动模式
config.bAutoAdjustCacheTime = YES;
......@@ -238,7 +240,7 @@ static const int uninitialized = -1;
if (_txLivePlayer != nil) {
return [_txLivePlayer prepareLiveSeek:domain bizId:bizId];
}
return uninitialized;
}
......@@ -246,7 +248,7 @@ static const int uninitialized = -1;
if (_txLivePlayer != nil) {
return [_txLivePlayer resumeLive];
}
return uninitialized;
}
......@@ -262,12 +264,26 @@ static const int uninitialized = -1;
}
}
- (BOOL)enableHardwareDecode:(BOOL)enable {
if (_txLivePlayer != nil) {
_txLivePlayer.enableHWAcceleration = enable;
}
return false;
}
- (void)setPlayConfig:(NSDictionary *)args
{
if (_txLivePlayer != nil && [args[@"config"] isKindOfClass:[NSDictionary class]]) {
_txLivePlayer.config = [FTXTransformation transformToLiveConfig:args];
}
}
#pragma mark -
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result
{
NSDictionary *args = call.arguments;
if([@"init" isEqualToString:call.method]){
BOOL onlyAudio = [args[@"onlyAudio"] boolValue];
NSNumber* textureId = [self createPlayer:onlyAudio];
......@@ -309,12 +325,15 @@ static const int uninitialized = -1;
result(nil);
}else if([@"destory" isEqualToString:call.method]) {
[self destory];
result(nil);
}else if([@"setRenderRotation" isEqualToString:call.method]) {
int rotation = [args[@"rotation"] intValue];
[self setRenderRotation:rotation];
result(nil);
}else if([@"switchStream" isEqualToString:call.method]) {
NSString *url = args[@"url"];
[self switchStream:url];
int switchResult = [self switchStream:url];
result(@(switchResult));
}else if ([@"seek" isEqualToString:call.method]) {
result(FlutterMethodNotImplemented);
}else if ([@"setAppID" isEqualToString:call.method]) {
......@@ -327,6 +346,13 @@ static const int uninitialized = -1;
}else if([@"resumeLive" isEqualToString:call.method]) {
int r = [self resumeLive];
result(@(r));
}else if([@"enableHardwareDecode" isEqualToString:call.method]) {
BOOL enable = [args[@"enable"] boolValue];
int r = [self enableHardwareDecode:enable];
result(@(r));
}else if([@"setConfig" isEqualToString:call.method]){
[self setPlayConfig:args];
result(nil);
}else {
result(FlutterMethodNotImplemented);
}
......
......@@ -6,6 +6,8 @@ static NSString* cacheFolder = nil;
static int maxCacheItems = -1;
@interface FTXTransformation : NSObject
+ (TXVodPlayConfig *)transformToConfig:(NSDictionary*)map;
+ (TXVodPlayConfig *)transformToVodConfig:(NSDictionary*)map;
+ (TXLivePlayConfig *)transformToLiveConfig:(NSDictionary*)map;
@end
......@@ -4,7 +4,7 @@
@implementation FTXTransformation
+ (TXVodPlayConfig *)transformToConfig:(NSDictionary *)args
+ (TXVodPlayConfig *)transformToVodConfig:(NSDictionary *)args
{
TXVodPlayConfig *playConfig = [[TXVodPlayConfig alloc] init];
playConfig.connectRetryCount = [args[@"config"][@"connectRetryCount"] intValue];
......@@ -48,6 +48,25 @@
return playConfig;
}
+ (TXLivePlayConfig *)transformToLiveConfig:(NSDictionary *)args
{
TXLivePlayConfig *playConfig = [[TXLivePlayConfig alloc] init];
playConfig.cacheTime = [args[@"config"][@"cacheTime"] floatValue];
playConfig.maxAutoAdjustCacheTime = [args[@"config"][@"maxAutoAdjustCacheTime"] floatValue];
playConfig.minAutoAdjustCacheTime = [args[@"config"][@"minAutoAdjustCacheTime"] floatValue];
playConfig.videoBlockThreshold = [args[@"config"][@"videoBlockThreshold"] intValue];
playConfig.connectRetryCount = [args[@"config"][@"connectRetryCount"] intValue];
playConfig.connectRetryInterval = [args[@"config"][@"connectRetryInterval"] intValue];
playConfig.bAutoAdjustCacheTime = [args[@"config"][@"autoAdjustCacheTime"] boolValue];
playConfig.enableAEC = [args[@"config"][@"enableAec"] boolValue];
playConfig.enableMessage = [args[@"config"][@"enableMessage"] intValue];
playConfig.enableMetaData = [args[@"config"][@"enableMetaData"] intValue];
playConfig.flvSessionKey = [args[@"config"][@"flvSessionKey"] stringValue];
return playConfig;
}
@end
......@@ -583,7 +583,7 @@ BOOL volatile isStop = false;
- (void)setPlayConfig:(NSDictionary *)args
{
if (_txVodPlayer != nil && [args[@"config"] isKindOfClass:[NSDictionary class]]) {
_txVodPlayer.config = [FTXTransformation transformToConfig:args];
_txVodPlayer.config = [FTXTransformation transformToVodConfig:args];
}
}
......
......@@ -187,6 +187,10 @@ SuperPlayerPlugin* instance;
} else if ([@"isDeviceSupportPip" isEqualToString:call.method]) {
BOOL isSupport = [TXVodPlayer isSupportPictureInPicture];
result([NSNumber numberWithBool:isSupport]);
} else if([@"setGlobalEnv" isEqualToString:call.method]) {
NSString *envConfig = call.arguments[@"envConfig"];
int setResult = [TXLiveBase setGlobalEnv:[envConfig UTF8String]];
result(@(setResult));
} else {
result(FlutterMethodNotImplemented);
}
......
// Copyright (c) 2022 Tencent. All rights reserved.
part of SuperPlayer;
abstract class TXModel {
}
class TXPlayerHolder extends TXModel {
TXPlayerController controller;
TXPlayerHolder(this.controller);
void updateController(TXPlayerController playerController) {
controller = playerController;
}
}
......@@ -19,6 +19,7 @@ class SuperPlayerPlugin {
/// 原生交互,通用事件监听,来自插件的事件,例如 声音变化等事件
Stream<Map<dynamic, dynamic>> get onEventBroadcast => _eventStreamController.stream;
/// 原生交互,通用事件监听,来自原生容器的事件,例如 PIP事件、activity/controller 生命周期变化
Stream<Map<dynamic, dynamic>> get onExtraEventBroadcast => _eventPipStreamController.stream;
......@@ -153,4 +154,16 @@ class SuperPlayerPlugin {
static Future<String?> getLiteAVSDKVersion() async {
return await _channel.invokeMethod('getLiteAVSDKVersion');
}
///
/// 设置 liteav SDK 接入的环境。
/// 腾讯云在全球各地区部署的环境,按照各地区政策法规要求,需要接入不同地区接入点。
///
/// @param envConfig 需要接入的环境,SDK 默认接入的环境是:默认正式环境。
/// @return 0:成功;其他:错误
/// @note 目标市场为中国大陆的客户请不要调用此接口,如果目标市场为海外用户,请通过技术支持联系我们,了解 env_config 的配置方法,以确保 App 遵守 GDPR 标准。
///
static Future<int> setGlobalEnv(String envConfig) async {
return await _channel.invokeMethod("setGlobalEnv", {"envConfig": envConfig});
}
}
// Copyright (c) 2022 Tencent. All rights reserved.
part of SuperPlayer;
/// TXLivePlayer config
class FTXLivePlayConfig {
// 播放器缓存时间,单位秒,取值需要大于0,默认值:5
double cacheTime = 5.0;
// 播放器缓存自动调整的最大时间,单位秒,取值需要大于0,默认值:5
double maxAutoAdjustCacheTime = 5.0;
// 播放器缓存自动调整的最小时间,单位秒,取值需要大于0,默认值为1
double minAutoAdjustCacheTime = 1.0;
// 播放器视频卡顿报警阈值,单位毫秒,只有渲染间隔超过这个阈值的卡顿才会有 PLAY_WARNING_VIDEO_PLAY_LAG 通知
int videoBlockThreshold = 800;
// 播放器遭遇网络连接断开时 SDK 默认重试的次数,取值范围1 - 10,默认值:3。
int connectRetryCount = 3;
// 网络重连的时间间隔,单位秒,取值范围3 - 30,默认值:3。
int connectRetryInterval = 3;
// 是否自动调整播放器缓存时间,默认值:true
// true:启用自动调整,自动调整的最大值和最小值可以分别通过修改 maxCacheTime 和 minCacheTime 来设置
// false:关闭自动调整,采用默认的指定缓存时间(1s),可以通过修改 cacheTime 来调整缓存时间
bool autoAdjustCacheTime = true;
// 是否开启回声消除, 默认值为 false
bool enableAec = false;
// 是否开启消息通道, 默认值为 true
bool enableMessage = true;
// 是否开启 MetaData 数据回调,默认值为 NO。
// true:SDK 通过 EVT_PLAY_GET_METADATA 消息抛出视频流的 MetaData 数据;
// false:SDK 不抛出视频流的 MetaData 数据。
// 标准直播流都会在最开始的阶段有一个 MetaData 数据头,该数据头支持定制。
// 您可以通过 TXLivePushConfig 中的 metaData 属性设置一些自定义数据,再通过 TXLivePlayListener 中的
// onPlayEvent(EVT_PLAY_GET_METADATA) 消息接收到这些数据。
//【特别说明】每条音视频流中只能设置一个 MetaData 数据头,除非断网重连,否则 TXLivePlayer 的
// EVT_PLAY_GET_METADATA 消息也只会收到一次。
bool enableMetaData = false;
// 是否开启 HTTP 头信息回调,默认值为 “”
// HTTP
// 响应头中除了“content-length”、“content-type”等标准字段,不同云服务商还可能会添加一些非标准字段。
// 比如腾讯云会在直播 CDN 的 HTTP-FLV 格式的直播流中增加 “X-Tlive-SpanId”
// 响应头,并在其中设置一个随机字符串,用来唯一标识一次直播。
//
// 如果您在使用腾讯云的直播 CDN,可以设置 flvSessionKey 为 “X-Tlive-SpanId”,SDK 会在 HTTP
// 响应头里解析这个字段, 并通过 TXLivePlayListener 中的 onPlayEvent(EVT_PLAY_GET_FLVSESSIONKEY)
// 事件通知给您的 App。
//
//【特别说明】每条音视频流中只能解析一个 flvSessionKey,除非断网重连,否则
// EVT_PLAY_GET_FLVSESSIONKEY 只会抛送一次。
String flvSessionKey = "";
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
json["cacheTime"] = cacheTime;
json["maxAutoAdjustCacheTime"] = maxAutoAdjustCacheTime;
json["minAutoAdjustCacheTime"] = minAutoAdjustCacheTime;
json["videoBlockThreshold"] = videoBlockThreshold;
json["connectRetryCount"] = connectRetryCount;
json["connectRetryInterval"] = connectRetryInterval;
json["autoAdjustCacheTime"] = autoAdjustCacheTime;
json["enableAec"] = enableAec;
json["enableMessage"] = enableMessage;
json["enableMetaData"] = enableMetaData;
json["flvSessionKey"] = flvSessionKey;
return json;
}
}
\ No newline at end of file
......@@ -16,18 +16,17 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
StreamSubscription? _eventSubscription;
StreamSubscription? _netSubscription;
final StreamController<TXPlayerState?> _stateStreamController =
StreamController.broadcast();
final StreamController<TXPlayerState?> _stateStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic> > _eventStreamController =
StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _eventStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic> > _netStatusStreamController =
StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _netStatusStreamController = StreamController.broadcast();
Stream<TXPlayerState?> get onPlayerState => _stateStreamController.stream;
Stream<Map<dynamic, dynamic> > get onPlayerEventBroadcast => _eventStreamController.stream;
Stream<Map<dynamic, dynamic> > get onPlayerNetStatusBroadcast => _netStatusStreamController.stream;
Stream<Map<dynamic, dynamic>> get onPlayerEventBroadcast => _eventStreamController.stream;
Stream<Map<dynamic, dynamic>> get onPlayerNetStatusBroadcast => _netStatusStreamController.stream;
TXLivePlayerController()
: _initPlayer = Completer(),
......@@ -40,14 +39,12 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
Future<void> _create() async {
_playerId = await SuperPlayerPlugin.createLivePlayer();
_channel = MethodChannel("cloud.tencent.com/txliveplayer/$_playerId");
_eventSubscription =
EventChannel("cloud.tencent.com/txliveplayer/event/$_playerId")
.receiveBroadcastStream("event")
.listen(_eventHandler, onError: _errorHandler);
_netSubscription =
EventChannel("cloud.tencent.com/txliveplayer/net/$_playerId")
.receiveBroadcastStream("net")
.listen(_netHandler, onError: _errorHandler);
_eventSubscription = EventChannel("cloud.tencent.com/txliveplayer/event/$_playerId")
.receiveBroadcastStream("event")
.listen(_eventHandler, onError: _errorHandler);
_netSubscription = EventChannel("cloud.tencent.com/txliveplayer/net/$_playerId")
.receiveBroadcastStream("net")
.listen(_netHandler, onError: _errorHandler);
_initPlayer.complete(_playerId);
}
......@@ -56,21 +53,21 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
/// see:https://cloud.tencent.com/document/product/454/7886#.E6.92.AD.E6.94.BE.E4.BA.8B.E4.BB.B6
///
_eventHandler(event) {
if(event == null) return;
if (event == null) return;
final Map<dynamic, dynamic> map = event;
//debugPrint("= event = ${map.toString()}");
switch(map["event"]){
switch (map["event"]) {
case TXVodPlayEvent.PLAY_EVT_RTMP_STREAM_BEGIN:
break;
case TXVodPlayEvent.PLAY_EVT_RCV_FIRST_I_FRAME:
if(_isNeedDisposed) return;
if(_state == TXPlayerState.buffering) _changeState(TXPlayerState.playing);
if (_isNeedDisposed) return;
if (_state == TXPlayerState.buffering) _changeState(TXPlayerState.playing);
break;
case TXVodPlayEvent.PLAY_EVT_PLAY_BEGIN:
if(_isNeedDisposed) return;
if(_state == TXPlayerState.buffering) _changeState(TXPlayerState.playing);
if (_isNeedDisposed) return;
if (_state == TXPlayerState.buffering) _changeState(TXPlayerState.playing);
break;
case TXVodPlayEvent.PLAY_EVT_PLAY_PROGRESS://EVT_PLAY_PROGRESS
case TXVodPlayEvent.PLAY_EVT_PLAY_PROGRESS: //EVT_PLAY_PROGRESS
break;
case TXVodPlayEvent.PLAY_EVT_PLAY_END:
_changeState(TXPlayerState.stopped);
......@@ -78,32 +75,34 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
case TXVodPlayEvent.PLAY_EVT_PLAY_LOADING:
_changeState(TXPlayerState.buffering);
break;
case TXVodPlayEvent.PLAY_EVT_CHANGE_RESOLUTION://下行视频分辨率改变
case TXVodPlayEvent.PLAY_EVT_CHANGE_RESOLUTION: //下行视频分辨率改变
if (defaultTargetPlatform == TargetPlatform.android) {
resizeVideoWidth = (event["videoWidth"]).toDouble();
resizeVideoHeight = (event["videoHeight"]).toDouble();
if(resizeVideoWidth! > 0 && resizeVideoHeight! > 0) {
videoLeft = (event["videoLeft"]).toDouble();
videoTop = (event["videoTop"]).toDouble();
videoRight = (event["videoRight"]).toDouble();
videoBottom = (event["videoBottom"]).toDouble();
double? videoWidth = (event["videoWidth"]);
double? videoHeight = (event["videoHeight"]);
if ((videoWidth != null && videoWidth > 0) && (videoHeight != null && videoHeight > 0)) {
resizeVideoWidth = videoWidth;
resizeVideoHeight = videoHeight;
videoLeft = event["videoLeft"];
videoTop = event["videoTop"];
videoRight = event["videoRight"];
videoBottom = event["videoBottom"];
}
}
break;
case TXVodPlayEvent.PLAY_EVT_STREAM_SWITCH_SUCC://直播,切流成功(切流可以播放不同画面大小的视频)
case TXVodPlayEvent.PLAY_EVT_STREAM_SWITCH_SUCC: //直播,切流成功(切流可以播放不同画面大小的视频)
break;
case TXVodPlayEvent.PLAY_ERR_NET_DISCONNECT://disconnect
case TXVodPlayEvent.PLAY_ERR_NET_DISCONNECT: //disconnect
_changeState(TXPlayerState.failed);
break;
case TXVodPlayEvent.PLAY_WARNING_RECONNECT://reconnect
case TXVodPlayEvent.PLAY_WARNING_RECONNECT: //reconnect
break;
case TXVodPlayEvent.PLAY_WARNING_DNS_FAIL://dnsFail
case TXVodPlayEvent.PLAY_WARNING_DNS_FAIL: //dnsFail
break;
case TXVodPlayEvent.PLAY_WARNING_SEVER_CONN_FAIL://severConnFail
case TXVodPlayEvent.PLAY_WARNING_SEVER_CONN_FAIL: //severConnFail
break;
case TXVodPlayEvent.PLAY_WARNING_SHAKE_FAIL://shakeFail
case TXVodPlayEvent.PLAY_WARNING_SHAKE_FAIL: //shakeFail
break;
case TXVodPlayEvent.PLAY_ERR_STREAM_SWITCH_FAIL://failed
case TXVodPlayEvent.PLAY_ERR_STREAM_SWITCH_FAIL: //failed
_changeState(TXPlayerState.failed);
break;
default:
......@@ -117,12 +116,12 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
}
_netHandler(event) {
if(event == null) return;
if (event == null) return;
final Map<dynamic, dynamic> map = event;
_netStatusStreamController.add(map);
}
_changeState(TXPlayerState playerState){
_changeState(TXPlayerState playerState) {
value = _value!.copyWith(state: playerState);
_state = value!.state;
_stateStreamController.add(_state);
......@@ -131,21 +130,29 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
///
/// 当设置[LivePlayer] 类型播放器时,需要参数[playType]
/// 参考: [PlayType.LIVE_RTMP] ...
///
@deprecated
Future<bool> play(String url, {int? playType}) async {
return await startPlay(url, playType: playType);
}
///
/// 当设置[LivePlayer] 类型播放器时,需要参数[playType]
/// 参考: [PlayType.LIVE_RTMP] ...
@override
Future<bool> startPlay(String url, {int? playType}) async {
await _initPlayer.future;
await _createTexture.future;
_changeState(TXPlayerState.buffering);
final result =
await _channel.invokeMethod("play", {"url": url, "playType": playType});
final result = await _channel.invokeMethod("play", {"url": url, "playType": playType});
return result == 0;
}
/// 播放器初始化,创建共享纹理、初始化播放器
/// @param onlyAudio 是否是纯音频模式
Future<void> initialize({bool? onlyAudio}) async{
if(_isNeedDisposed) return;
@override
Future<void> initialize({bool? onlyAudio}) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
final textureId = await _channel.invokeMethod("init", {
"onlyAudio": onlyAudio ?? false,
......@@ -155,60 +162,71 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
}
/// 设置是否自动播放
Future<void> setIsAutoPlay({bool? isAutoPlay}) async{
if(_isNeedDisposed) return;
@deprecated
Future<void> setIsAutoPlay({bool? isAutoPlay}) async {
await setAutoPlay(isAutoPlay: isAutoPlay);
}
/// 设置是否自动播放
@override
Future<void> setAutoPlay({bool? isAutoPlay}) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("setIsAutoPlay", {"isAutoPlay": isAutoPlay ?? false});
await _channel.invokeMethod("setIsAutoPlay", {"isAutoPlay": isAutoPlay ?? false});
}
/// 停止播放
/// return 是否停止成功
Future<bool> stop({bool isNeedClear = true}) async {
if(_isNeedDisposed) return false;
@override
Future<bool> stop({bool isNeedClear = false}) async {
if (_isNeedDisposed) return false;
await _initPlayer.future;
final result =
await _channel.invokeMethod("stop", {"isNeedClear": isNeedClear});
final result = await _channel.invokeMethod("stop", {"isNeedClear": isNeedClear});
_changeState(TXPlayerState.stopped);
return result == 0;
}
/// 视频是否处于正在播放中
Future<bool?> isPlaying() async {
@override
Future<bool> isPlaying() async {
await _initPlayer.future;
return await _channel.invokeMethod("isPlaying");
}
/// 视频暂停,必须在播放器开始播放的时候调用
@override
Future<void> pause() async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("pause");
if(_state != TXPlayerState.paused) _changeState(TXPlayerState.paused);
if (_state != TXPlayerState.paused) _changeState(TXPlayerState.paused);
}
/// 继续播放,在暂停的时候调用
@override
Future<void> resume() async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("resume");
if(_state != TXPlayerState.playing) _changeState(TXPlayerState.playing);
if (_state != TXPlayerState.playing) _changeState(TXPlayerState.playing);
}
/// 设置直播模式,see TXPlayerLiveMode
Future<void> setLiveMode(TXPlayerLiveMode mode) async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("setLiveMode", {"type": mode.index});
}
/// 设置视频声音 0~100
Future<void> setVolume(int volume) async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("setVolume", {"volume": volume});
}
/// 设置是否静音
@override
Future<void> setMute(bool mute) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -216,23 +234,24 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
}
/// 切换播放流
Future<void> switchStream(String url) async {
if (_isNeedDisposed) return;
Future<int> switchStream(String url) async {
if (_isNeedDisposed) return -1;
await _initPlayer.future;
await _channel.invokeMethod("switchStream", {"url": url});
return await _channel.invokeMethod("switchStream", {"url": url});
}
/// 将视频播放进度定位到指定的进度进行播放
/// progress 要定位的视频时间,单位 秒
@override
Future<void> seek(double progress) async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("seek", {"progress": progress});
}
/// 设置appId
Future<void> setAppID(int appId) async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("seek", {"appId": appId});
}
......@@ -240,24 +259,69 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
/// 时移 暂不支持
@deprecated
Future<void> prepareLiveSeek(String domain, int bizId) async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("prepareLiveSeek", {"domain":domain, "bizId":bizId});
await _channel.invokeMethod("prepareLiveSeek", {"domain": domain, "bizId": bizId});
}
/// 停止时移播放,返回直播
Future<int> resumeLive() async {
if(_isNeedDisposed) return 0;
if (_isNeedDisposed) return 0;
await _initPlayer.future;
return await _channel.invokeMethod("resumeLive");
}
/// 设置播放速率,暂不支持
@deprecated
@override
Future<void> setRate(double rate) async {
if(_isNeedDisposed) return;
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("setRate", {"rate": rate});
}
/// 设置播放器配置
/// config @see [FTXLivePlayConfig]
Future<void> setConfig(FTXLivePlayConfig config) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
await _channel.invokeMethod("setConfig", {"config": config.toJson()});
}
/// 开启/关闭硬件编码
@override
Future<bool> enableHardwareDecode(bool enable) async {
if (_isNeedDisposed) return false;
await _initPlayer.future;
await _channel.invokeMethod("setRate", {"rate":rate});
return await _channel.invokeMethod("enableHardwareDecode", {"enable": enable});
}
/// 进入画中画模式,进入画中画模式,需要适配画中画模式的界面,安卓只支持7.0以上机型
/// <h1>
/// 由于android系统限制,传递的图标大小不得超过1M,否则无法显示
/// </h1>
/// @param backIcon playIcon pauseIcon forwardIcon 为播放后退、播放、暂停、前进的图标,仅适用于android,如果赋值的话,将会使用传递的图标,否则
/// 使用系统默认图标,只支持flutter本地资源图片,传递的时候,与flutter使用图片资源一致,例如: images/back_icon.png
@override
Future<int> enterPictureInPictureMode(
{String? backIconForAndroid,
String? playIconForAndroid,
String? pauseIconForAndroid,
String? forwardIconForAndroid}) async {
if (_isNeedDisposed) return -1;
await _initPlayer.future;
if (Platform.isAndroid) {
return await _channel.invokeMethod("enterPictureInPictureMode", {
"backIcon": backIconForAndroid,
"playIcon": playIconForAndroid,
"pauseIcon": pauseIconForAndroid,
"forwardIcon": forwardIconForAndroid
});
} else if (Platform.isIOS) {
return -1;
} else {
return -1;
}
}
/// 释放播放器资源占用
......@@ -267,12 +331,15 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
await SuperPlayerPlugin.releasePlayer(_playerId);
}
/// 释放controller
@override
void dispose() async{
void dispose() async {
_isNeedDisposed = true;
if(!_isDisposed){
if (!_isDisposed) {
await _eventSubscription!.cancel();
_eventSubscription = null;
await _netSubscription!.cancel();
_netSubscription = null;
await _release();
_changeState(TXPlayerState.disposed);
......@@ -289,7 +356,7 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
get value => _value;
set value(TXPlayerValue? val){
set value(TXPlayerValue? val) {
if (_value == val) return;
_value = val;
notifyListeners();
......@@ -297,7 +364,7 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
@override
// TODO: implement textureId
Future<int> get textureId async {
Future<int> get textureId async {
return _createTexture.future;
}
......@@ -307,5 +374,4 @@ class TXLivePlayerController extends ChangeNotifier implements ValueListenable<T
double? videoTop = 0;
double? videoRight = 0;
double? videoBottom = 0;
}
\ No newline at end of file
}
......@@ -9,4 +9,18 @@ abstract class TXPlayerController {
double? get videoTop;
double? get videoRight;
double? get videoBottom;
Future<bool> startPlay(String url);
Future<void> initialize({bool? onlyAudio});
Future<void> setAutoPlay({bool? isAutoPlay});
Future<bool> stop({bool isNeedClear = false});
Future<bool> isPlaying();
Future<void> pause();
Future<void> resume();
Future<void> setMute(bool mute);
Future<void> seek(double progress);
Future<void> setRate(double rate);
Future<bool> enableHardwareDecode(bool enable);
Future<int> enterPictureInPictureMode(
{String? backIconForAndroid, String? playIconForAndroid, String? pauseIconForAndroid, String? forwardIconForAndroid});
}
\ No newline at end of file
......@@ -116,7 +116,7 @@ abstract class TXVodPlayEvent {
static const ERROR_IOS_PIP_PLAYER_NOT_EXIST = -109; // pip 错误,播放器对象不存在 only support iOS
static const ERROR_IOS_PIP_IS_RUNNING = -110; // pip 错误,PIP功能已经运行 only support iOS
static const ERROR_IOS_PIP_NOT_RUNNING = -111; // pip 错误,PIP功能没有启动 only support iOS
static const ERROR_PIP_CAN_NOT_ENTER = -120; // pip 错误,当前不能进入pip模式,例如正处于全屏模式下
/// 视频下载相关事件
static const EVENT_PREDOWNLOAD_ON_COMPLETE = 200; // 视频预下载完成
......
......@@ -3,10 +3,9 @@ part of SuperPlayer;
class TXPlayerVideo extends StatefulWidget {
final TXPlayerController controller;
final Stream<TXPlayerHolder>? playerStream;
TXPlayerVideo({required this.controller, Key? key}) : super(key: key) {
assert(controller != null);
}
TXPlayerVideo({required this.controller, this.playerStream});
@override
TXPlayerVideoState createState() => TXPlayerVideoState();
......@@ -16,52 +15,82 @@ class TXPlayerVideoState extends State<TXPlayerVideo> {
static const TAG = "TXPlayerVideo";
int _textureId = -1;
StreamSubscription? streamSubscription;
late TXPlayerController controller;
@override
void initState() {
super.initState();
controller = widget.controller;
_checkStreamListen();
_resetControllerLink();
}
void _checkStreamListen() {
if(null != streamSubscription) {
streamSubscription!.cancel();
}
streamSubscription = widget.playerStream?.listen((event) {
controller = event.controller;
_resetControllerLink();
});
}
widget.controller.textureId.then((newTextureId) {
if (_textureId != newTextureId) {
void _resetControllerLink() async {
int remainTextureId = await controller.textureId;
if (remainTextureId >= 0) {
if (remainTextureId != _textureId) {
setState(() {
LogUtils.d(TAG, "_textureId = $newTextureId");
_textureId = newTextureId;
LogUtils.d(TAG, "_textureId = $remainTextureId");
_textureId = remainTextureId;
});
}
});
} else {
setState(() {
_textureId = -1;
});
controller.textureId.then((newTextureId) {
if (_textureId != newTextureId) {
setState(() {
LogUtils.d(TAG, "_textureId = $newTextureId");
_textureId = newTextureId;
});
}
});
}
}
@override
Widget build(BuildContext context) {
if ((defaultTargetPlatform == TargetPlatform.android) &&
(widget.controller.resizeVideoHeight! > 0 && widget.controller.resizeVideoWidth! > 0)) {
(controller.resizeVideoHeight! > 0 && controller.resizeVideoWidth! > 0)) {
return _textureId == -1
? Container()
: LayoutBuilder(builder: (context, constrains) {
var viewWidth = constrains.maxWidth;
var viewHeight = constrains.maxHeight;
var videoWidth = widget.controller.resizeVideoWidth!;
var videoHeight = widget.controller.resizeVideoHeight!;
var viewWidth = constrains.maxWidth;
var viewHeight = constrains.maxHeight;
var videoWidth = controller.resizeVideoWidth!;
var videoHeight = widget.controller.resizeVideoHeight!;
double left = widget.controller.videoLeft! * viewWidth / videoWidth;
double top = widget.controller.videoTop! * viewHeight / videoHeight;
double right = widget.controller.videoRight! * viewWidth / videoWidth;
double bottom = widget.controller.videoBottom! * viewHeight / videoHeight;
return Stack(
children: [
Positioned(
top: top,
left: left,
right: right,
bottom: bottom,
child: Texture(textureId: _textureId)
)
],
);
});
double left = controller.videoLeft! * viewWidth / videoWidth;
double top = controller.videoTop! * viewHeight / videoHeight;
double right = controller.videoRight! * viewWidth / videoWidth;
double bottom = controller.videoBottom! * viewHeight / videoHeight;
return Stack(
children: [
Positioned(
top: top, left: left, right: right, bottom: bottom, child: Texture(textureId: _textureId))
],
);
});
} else {
return _textureId == -1
? Container()
: Texture(textureId: _textureId);
return _textureId == -1 ? Container() : Texture(textureId: _textureId);
}
}
@override
void dispose() {
streamSubscription?.cancel();
super.dispose();
}
}
......@@ -17,9 +17,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
StreamSubscription? _netSubscription;
final StreamController<TXPlayerState?> _stateStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _eventStreamController = StreamController.broadcast();
final StreamController<Map<dynamic, dynamic>> _netStatusStreamController = StreamController.broadcast();
/// 播放状态监听,@see TXPlayerState
......@@ -137,6 +135,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
/// 通过url开始播放视频
/// @param url : 视频播放地址
/// return 是否播放成功
@override
Future<bool> startPlay(String url) async {
await _initPlayer.future;
await _createTexture.future;
......@@ -165,6 +164,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
/// 播放器初始化,创建共享纹理、初始化播放器
/// @param onlyAudio 是否是纯音频模式
@override
Future<void> initialize({bool? onlyAudio}) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -176,6 +176,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 设置是否自动播放
@override
Future<void> setAutoPlay({bool? isAutoPlay}) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -184,7 +185,8 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
/// 停止播放
/// return 是否停止成功
Future<bool> stop({bool isNeedClear = true}) async {
@override
Future<bool> stop({bool isNeedClear = false}) async {
if (_isNeedDisposed) return false;
await _initPlayer.future;
final result = await _channel.invokeMethod("stop", {"isNeedClear": isNeedClear});
......@@ -193,12 +195,14 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 视频是否处于正在播放中
Future<bool?> isPlaying() async {
@override
Future<bool> isPlaying() async {
await _initPlayer.future;
return await _channel.invokeMethod("isPlaying");
}
/// 视频暂停,必须在播放器开始播放的时候调用
@override
Future<void> pause() async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -207,6 +211,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 继续播放,在暂停的时候调用
@override
Future<void> resume() async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -214,6 +219,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 设置是否静音
@override
Future<void> setMute(bool mute) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -229,6 +235,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
/// 将视频播放进度定位到指定的进度进行播放
/// progress 要定位的视频时间,单位 秒
@override
Future<void> seek(double progress) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -236,6 +243,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 设置播放速率,默认速率 1
@override
Future<void> setRate(double rate) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -294,7 +302,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 设置播放器配置
/// config @see FTXVodPlayConfig
/// config @see [FTXVodPlayConfig]
Future<void> setConfig(FTXVodPlayConfig config) async {
if (_isNeedDisposed) return;
await _initPlayer.future;
......@@ -351,6 +359,7 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
/// 开启/关闭硬件编码
@override
Future<bool> enableHardwareDecode(bool enable) async {
if (_isNeedDisposed) return false;
await _initPlayer.future;
......@@ -363,12 +372,26 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
/// </h1>
/// @param backIcon playIcon pauseIcon forwardIcon 为播放后退、播放、暂停、前进的图标,如果赋值的话,将会使用传递的图标,否则
/// 使用系统默认图标,只支持flutter本地资源图片,传递的时候,与flutter使用图片资源一致,例如: images/back_icon.png
@override
Future<int> enterPictureInPictureMode(
{String? backIcon, String? playIcon, String? pauseIcon, String? forwardIcon}) async {
{String? backIconForAndroid,
String? playIconForAndroid,
String? pauseIconForAndroid,
String? forwardIconForAndroid}) async {
if (_isNeedDisposed) return -1;
await _initPlayer.future;
return await _channel.invokeMethod("enterPictureInPictureMode",
{"backIcon": backIcon, "playIcon": playIcon, "pauseIcon": pauseIcon, "forwardIcon": forwardIcon});
if (Platform.isAndroid) {
return await _channel.invokeMethod("enterPictureInPictureMode", {
"backIcon": backIconForAndroid,
"playIcon": playIconForAndroid,
"pauseIcon": pauseIconForAndroid,
"forwardIcon": forwardIconForAndroid
});
} else if (Platform.isIOS) {
return await _channel.invokeMethod("enterPictureInPictureMode");
} else {
return -1;
}
}
/// 获取总时长
......@@ -378,13 +401,15 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
return await _channel.invokeMethod("getDuration");
}
/// 废弃controller
/// 释放controller
@override
void dispose() async {
_isNeedDisposed = true;
if (!_isDisposed) {
await _eventSubscription!.cancel();
_eventSubscription = null;
await _netSubscription!.cancel();
_netSubscription = null;
await _release();
_changeState(TXPlayerState.disposed);
......@@ -398,8 +423,6 @@ class TXVodPlayerController extends ChangeNotifier implements ValueListenable<TX
}
@override
// TODO: implement value
get value => _value;
set value(TXPlayerValue? val) {
......
......@@ -3,6 +3,7 @@ library SuperPlayer;
import 'dart:async';
import 'dart:core';
import 'dart:io';
import 'dart:math';
import 'package:flutter/cupertino.dart';
......@@ -19,4 +20,6 @@ part 'Core/txplayer_widget.dart';
part 'Core/txvodplayer_config.dart';
part 'Core/txvodplayer_controller.dart';
part 'Core/txvoddownload_controller.dart';
part 'Core/tools/common_utils.dart';
\ No newline at end of file
part 'Core/txliveplayer_config.dart';
part 'Core/tools/common_utils.dart';
part 'Core/provider/txplayer_holder.dart';
\ No newline at end of file
name: super_player
description: player plugin.
version: 1.0.3
version: 1.0.5
author:
homepage:
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=1.20.0"
flutter: ">=2.0.0"
dependencies:
flutter:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论