介绍
在立项初期,我们参考了官方video_player视频插件的相同方式(Texture外接纹理),进行flutter播放器的集成。但是由于我们的XXXPlayer,在集成后无法正常播放视频(有视频无画面)。所以换了一种官方推荐的新模式。混合集成(将平台view显示到flutter界面上)的方式进行flutter视频播放器的封装。
基本原理
虚拟显示集成基本原理
原生端处理
- 创建
PlatformView
子类并实现getView
方法返回平台View
less
class NativeView implements PlatformView {
@NonNull
private final TextView textView;
NativeView(@NonNull Context context, int id, @Nullable Map<String, Object> creationParams) {
textView = new TextView(context);
textView.setTextSize(72);
textView.setBackgroundColor(Color.rgb(255, 255, 255));
textView.setText("Rendered on a native Android view (id: " + id + ")");
}
@NonNull
@Override
public View getView() {
return textView;
}
@Override
public void dispose() {}
}
- 创建一个用来创建上面
PlatformView
的工厂类PlatformViewFactory
并实现create
方法返回PlatformView
less
class NativeViewFactory extends PlatformViewFactory {
NativeViewFactory() {
super(StandardMessageCodec.INSTANCE);
}
@NonNull
@Override
public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
final Map<String, Object> creationParams = (Map<String, Object>) args;
return new NativeView(context, id, creationParams);
}
}
注:create方法中返回的id是系统生成的,此时平台端的id与FlutterView创建的Id是一一对应的。我们可以通过这个id去做相对应的事情。
- 在
插件
或者FlutterActivity
中注册上面准备好的平台视图:
- FlutterActivity中注册
scala
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory("<platform-view-type>", new NativeViewFactory());
}
}
- 插件中注册(因为需要做成播放器插件,我们采用的这种方式)
less
public class PlatformViewPlugin implements FlutterPlugin {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
binding
.getPlatformViewRegistry()
.registerViewFactory("<platform-view-type>", new NativeViewFactory());
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
}
注:上面的<platform-view-type>我们可以定义好名字,与Flutter端一致即可
Flutter端处理
- 进行如下处理我们就可以在Flutter端显示一个平台view了。
php
return AndroidView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
注:把此View当成普通的flutter Widget处理就行。其中viewType要与平台端对应好
XXXPlayer集成的实际应用
说完上面原理,再讲一下,我们的XXXPlayer播放器是如何集成进项目中的。这次我们按照在插件中使用先后的顺序。我先大致说一下流程:
序号 | 处理步骤 |
---|---|
1 | 插件注册时注册PlatformViewFactory (平台view创建的工厂)给引擎 |
2 | 当Flutter端要创建平台view时会调用平台工厂的create方法要求插件端创建PlatformView。工厂根据create方法过来的参数进行实际PlatformView创建并返回 |
3 | PlatformView中会创建具体的平台View(比如TextView等)并通过getView()方法中返回,由于是播放器项目,这里返回的是TextureView。 |
4 | 在创建TextureView时在FlutterXXXPlayer中创建player及相关消息通道,确保点对点的一个播放器对应一对消息进出通道 |
原生端创建
- 插件注册
PlatformViewFactory
less
public class KooFlutterPlayerAndroidPlugin implements FlutterPlugin {
@Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding)
{
/**
* 接收binding
*/
FlutterXdfPlayerViewFactory mViewFactory = new FlutterXdfPlayerViewFactory(flutterPluginBinding);
flutterPluginBinding.getPlatformViewRegistry().registerViewFactory("flutter_xdfplayer_view", mViewFactory);
}
public static void registerWith(Registrar registrar)
{}
@Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding)
{
}
}
PlatformViewFactory
返回PlatformView
scala
public class FlutterXdfPlayerViewFactory extends PlatformViewFactory {
private FlutterPlugin.FlutterPluginBinding mFlutterPluginBinding;
public FlutterXdfPlayerViewFactory(FlutterPlugin.FlutterPluginBinding flutterPluginBinding)
{
super(StandardMessageCodec.INSTANCE);
this.mFlutterPluginBinding = flutterPluginBinding;
}
/**
* 返回要显示到flutter程序中的plantformView
*/
@Override public PlatformView create(Context context, int viewId, Object args)
{
Log.i("---->viewId","--------------->原生viewId="+viewId);
return new PlayerView(viewId,mFlutterPluginBinding);
}
}
- 在
PlayerView
中创建原生平台View -> TextureView 并初始化FlutterXdfPlayer
java
public class PlayerView implements PlatformView{
private TextureView mTextureView;
private int mId;
public PlayerView(int id, FlutterPlugin.FlutterPluginBinding flutterPluginBinding){
this.mId = id;
mTextureView = new TextureView(flutterPluginBinding.getApplicationContext());
FlutterXdfPlayer mFlutterXdfPlayer =new FlutterXdfPlayer(id,flutterPluginBinding.getApplicationContext(),flutterPluginBinding.getBinaryMessenger());
initRenderView(mFlutterXdfPlayer.getXdfPlayer());
}
@Nullable
@Override
public View getView() {
return mTextureView;
}
@Override
public void dispose() {
}
private void initRenderView(XdfPlayer player) {
//利用此回调,将Surface注册到player中。
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
Surface mSurface = new Surface(surface);
player.setSurface(mSurface);
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
player.redraw();
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
player.setSurface(null);
return false;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
}
});
}
}
- 创建播放器及进出的消息通道
FlutterXdfPlayer
,其中此类利用接收flutter端通过通道传过来消息,去处理外界调用播放器的行为消息,比如说准备,开始暂停等。与播放器相关的所有回调消息,如播放完成,播放进度回调,播放错误等,通过此类的event消息发出给到flutter端。
ini
public class FlutterXdfPlayer implements EventChannel.StreamHandler, MethodCallHandler {
private final Gson mGson;
private Context mContext;
private EventChannel.EventSink mEventSink;
private EventChannel mEventChannel;
private XdfPlayer mXdfPlayer;
private MethodChannel mXdfPlayerMethodChannel;
private String mSnapShotPath;
public FlutterXdfPlayer(int id, Context context, BinaryMessenger binaryMessenger)
{
this.mContext = context;
mGson = new Gson();
mXdfPlayer = XdfPlayerFactory.createXdfPlayer(mContext);
Log.i("------->","注册flutter_xdfplayer 通道消息了");
mXdfPlayerMethodChannel = new MethodChannel(binaryMessenger, "flutter_xdfplayer"+id);
mXdfPlayerMethodChannel.setMethodCallHandler(this);
mEventChannel = new EventChannel(binaryMessenger, "flutter_xdfplayer_event"+id);
mEventChannel.setStreamHandler(this);
initListener(mXdfPlayer);
}
public XdfPlayer getXdfPlayer()
{
return mXdfPlayer;
}
private void initListener(final XdfPlayer player)
{
player.setOnPreparedListener(new XdfPlayer.OnPreparedListener() {
@Override public void onPrepared()
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onPrepared");
mEventSink.success(map);
start();
}
});
player.setOnRenderingStartListener(new XdfPlayer.OnRenderingStartListener() {
@Override public void onRenderingStart()
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onRenderingStart");
mEventSink.success(map);
}
});
player.setOnVideoSizeChangedListener(new XdfPlayer.OnVideoSizeChangedListener() {
@Override public void onVideoSizeChanged(int width, int height)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onVideoSizeChanged");
map.put("width", width);
map.put("height", height);
mEventSink.success(map);
}
});
// player.setOnSnapShotListener(new XdfPlayer.OnSnapShotListener() {
// @Override
// public void onSnapShot(final Bitmap bitmap, int width, int height) {
// final Map<String,Object> map = new HashMap<>();
// map.put("method","onSnapShot");
// map.put("snapShotPath",mSnapShotPath);
//
// ThreadManager.threadPool.execute(new Runnable() {
// @Override
// public void run() {
// File f = new File(mSnapShotPath);
// FileOutputStream out = null;
// if (f.exists()) {
// f.delete();
// }
// try {
// out = new FileOutputStream(f);
// bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
// out.flush();
// out.close();
// } catch (FileNotFoundException e) {
// e.printStackTrace();
// } catch (IOException e) {
// e.printStackTrace();
// }finally{
// if(out != null){
// try {
// out.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
// }
// });
//
// mEventSink.success(map);
//
// }
// });
player.setOnTrackChangedListener(new XdfPlayer.OnTrackChangedListener() {
@Override public void onChangedSuccess(TrackInfo trackInfo)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onTrackChanged");
Map<String, Object> infoMap = new HashMap<>();
// infoMap.put("vodFormat",trackInfo.getVodFormat());
infoMap.put("videoHeight", trackInfo.getVideoHeight());
infoMap.put("videoWidth", trackInfo.getVideoHeight());
infoMap.put("subtitleLanguage", trackInfo.getSubtitleLang());
infoMap.put("trackBitrate", trackInfo.getVideoBitrate());
// infoMap.put("vodFileSize",trackInfo.getVodFileSize());
infoMap.put("trackIndex", trackInfo.getIndex());
// infoMap.put("trackDefinition",trackInfo.getVodDefinition());
infoMap.put("audioSampleFormat", trackInfo.getAudioSampleFormat());
infoMap.put("audioLanguage", trackInfo.getAudioLang());
// infoMap.put("vodPlayUrl",trackInfo.getVodPlayUrl());
infoMap.put("trackType", trackInfo.getType().ordinal());
infoMap.put("audioSamplerate", trackInfo.getAudioSampleRate());
infoMap.put("audioChannels", trackInfo.getAudioChannels());
map.put("info", infoMap);
mEventSink.success(map);
}
@Override public void onChangedFail(TrackInfo trackInfo, ErrorInfo errorInfo)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onChangedFail");
mEventSink.success(map);
}
});
player.setOnSeekCompleteListener(new XdfPlayer.OnSeekCompleteListener() {
@Override public void onSeekComplete()
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onSeekComplete");
mEventSink.success(map);
}
});
// player.setOnSeiDataListener(new XdfPlayer.OnSeiDataListener() {
// @Override
// public void onSeiData(int type, byte[] bytes) {
// Map<String,Object> map = new HashMap<>();
// map.put("method","onSeiData");
// //TODO
// mEventSink.success(map);
// }
// });
player.setOnLoadingStatusListener(new XdfPlayer.OnLoadingStatusListener() {
@Override public void onLoadingBegin()
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onLoadingBegin");
mEventSink.success(map);
}
@Override public void onLoadingProgress(int percent, float netSpeed)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onLoadingProgress");
map.put("percent", percent);
map.put("netSpeed", netSpeed);
mEventSink.success(map);
}
@Override public void onLoadingEnd()
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onLoadingEnd");
mEventSink.success(map);
}
});
player.setOnStateChangedListener(new XdfPlayer.OnStateChangedListener() {
@Override public void onStateChanged(int newState)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onStateChanged");
map.put("newState", newState);
mEventSink.success(map);
}
});
player.setOnSubtitleDisplayListener(new XdfPlayer.OnSubtitleDisplayListener() {
@Override public void onSubtitleExtAdded(int trackIndex, String url)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onSubtitleExtAdded");
map.put("trackIndex", trackIndex);
map.put("url", url);
mEventSink.success(map);
}
@Override public void onSubtitleShow(int trackIndex, long id, String data)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onSubtitleShow");
map.put("trackIndex", trackIndex);
map.put("subtitleID", id);
map.put("subtitle", data);
mEventSink.success(map);
}
@Override public void onSubtitleHide(int trackIndex, long id)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onSubtitleHide");
map.put("trackIndex", trackIndex);
map.put("subtitleID", id);
mEventSink.success(map);
}
@Override
public void onSubtitleHeader(int trackIndex, String header) {
}
});
player.setOnInfoListener(new XdfPlayer.OnInfoListener() {
@Override public void onInfo(InfoBean infoBean)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onInfo");
map.put("infoCode", infoBean.getCode().getValue());
map.put("extraValue", infoBean.getExtraValue());
map.put("extraMsg", infoBean.getExtraMsg());
mEventSink.success(map);
}
});
player.setOnErrorListener(new XdfPlayer.OnErrorListener() {
@Override public void onError(ErrorInfo errorInfo)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onError");
map.put("errorCode", errorInfo.getCode().getValue());
map.put("errorExtra", errorInfo.getExtra());
map.put("errorMsg", errorInfo.getMsg());
mEventSink.success(map);
}
});
player.setOnTrackReadyListener(new XdfPlayer.OnTrackReadyListener() {
@Override public void onTrackReady(MediaInfo mediaInfo)
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onTrackReady");
mEventSink.success(map);
}
});
player.setOnCompletionListener(new XdfPlayer.OnCompletionListener() {
@Override public void onCompletion()
{
Map<String, Object> map = new HashMap<>();
map.put("method", "onCompletion");
mEventSink.success(map);
}
});
}
@Override
public void onListen(Object arguments, EventChannel.EventSink events)
{
Log.i("----------> 初始化 ","mEventSink开始初始化");
this.mEventSink = events;
}
@Override
public void onCancel(Object arguments)
{}
@Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result)
{
switch (methodCall.method) {
case "createXdfPlayer":
createXdfPlayer();
break;
case "setUrl":
String url = methodCall.arguments.toString();
setDataSource(url);
break;
case "prepare":
prepare();
break;
case "play":
start();
break;
case "pause":
pause();
break;
case "stop":
stop();
break;
case "destroy":
release();
break;
case "seekTo": {
Map<String, Object> seekToMap = (Map<String, Object>) methodCall.arguments;
Integer position = (Integer) seekToMap.get("position");
Integer seekMode = (Integer) seekToMap.get("seekMode");
seekTo(position, seekMode);
} break;
case "getMediaInfo": {
MediaInfo mediaInfo = getMediaInfo();
if (mediaInfo != null) {
Map<String, Object> getMediaInfoMap = new HashMap<>();
// getMediaInfoMap.put("title",mediaInfo.getTitle());
// getMediaInfoMap.put("status",mediaInfo.getStatus());
// getMediaInfoMap.put("mediaType",mediaInfo.getMediaType());
// getMediaInfoMap.put("duration",mediaInfo.getDuration());
// getMediaInfoMap.put("transcodeMode",mediaInfo.getTransCodeMode());
// getMediaInfoMap.put("coverURL",mediaInfo.getCoverUrl());
List<TrackInfo> trackInfos = mediaInfo.getTrackInfos();
List<Map<String, Object>> trackInfoList = new ArrayList<>();
for (TrackInfo trackInfo : trackInfos) {
Map<String, Object> map = new HashMap<>();
// map.put("vodFormat",trackInfo.getVodFormat());
map.put("videoHeight", trackInfo.getVideoHeight());
map.put("videoWidth", trackInfo.getVideoHeight());
map.put("subtitleLanguage", trackInfo.getSubtitleLang());
map.put("trackBitrate", trackInfo.getVideoBitrate());
// map.put("vodFileSize",trackInfo.getVodFileSize());
map.put("trackIndex", trackInfo.getIndex());
// map.put("trackDefinition",trackInfo.getVodDefinition());
map.put("audioSampleFormat", trackInfo.getAudioSampleFormat());
map.put("audioLanguage", trackInfo.getAudioLang());
// map.put("vodPlayUrl",trackInfo.getVodPlayUrl());
map.put("trackType", trackInfo.getType().ordinal());
map.put("audioSamplerate", trackInfo.getAudioSampleRate());
map.put("audioChannels", trackInfo.getAudioChannels());
trackInfoList.add(map);
getMediaInfoMap.put("tracks", trackInfoList);
}
result.success(getMediaInfoMap);
}
} break;
case "getDuration":
result.success(mXdfPlayer.getDuration());
break;
case "snapshot":
mSnapShotPath = methodCall.arguments.toString();
snapshot();
break;
case "setLoop":
setLoop((Boolean) methodCall.arguments);
break;
case "isLoop":
result.success(isLoop());
break;
case "setAutoPlay":
setAutoPlay((Boolean) methodCall.arguments);
break;
case "isAutoPlay":
result.success(isAutoPlay());
break;
case "setMuted":
setMuted((Boolean) methodCall.arguments);
break;
case "isMuted":
result.success(isMuted());
break;
case "setEnableHardwareDecoder":
Boolean setEnableHardwareDecoderArgumnt = (Boolean) methodCall.arguments;
setEnableHardWareDecoder(setEnableHardwareDecoderArgumnt);
break;
case "setScalingMode":
setScaleMode((Integer) methodCall.arguments);
break;
case "getScalingMode":
result.success(getScaleMode());
break;
case "setMirrorMode":
setMirrorMode((Integer) methodCall.arguments);
break;
case "getMirrorMode":
result.success(getMirrorMode());
break;
case "setRotateMode":
setRotateMode((Integer) methodCall.arguments);
break;
case "getRotateMode":
result.success(getRotateMode());
break;
case "setRate":
setSpeed((Double) methodCall.arguments);
break;
case "getRate":
result.success(getSpeed());
break;
case "setVideoBackgroundColor":
setVideoBackgroundColor((Long) methodCall.arguments);
break;
case "setVolume":
setVolume((Double) methodCall.arguments);
break;
case "getVolume":
result.success(getVolume());
break;
case "setConfig": {
Map<String, Object> setConfigMap = (Map<String, Object>) methodCall.arguments;
PlayerConfig config = getConfig();
if (config != null) {
String configJson = mGson.toJson(setConfigMap);
config = mGson.fromJson(configJson, PlayerConfig.class);
setConfig(config);
}
} break;
case "getConfig":
PlayerConfig config = getConfig();
String json = mGson.toJson(config);
Map<String, Object> configMap = mGson.fromJson(json, Map.class);
result.success(configMap);
break;
case "getCacheConfig":
CacheConfig cacheConfig = getCacheConfig();
String cacheConfigJson = mGson.toJson(cacheConfig);
Map<String, Object> cacheConfigMap = mGson.fromJson(cacheConfigJson, Map.class);
result.success(cacheConfigMap);
break;
case "setCacheConfig":
Map<String, Object> setCacheConnfigMap = (Map<String, Object>) methodCall.arguments;
String setCacheConfigJson = mGson.toJson(setCacheConnfigMap);
CacheConfig setCacheConfig = mGson.fromJson(setCacheConfigJson, CacheConfig.class);
setCacheConfig(setCacheConfig);
break;
case "getCurrentTrack":
Integer currentTrackIndex = (Integer) methodCall.arguments;
TrackInfo currentTrack = getCurrentTrack(currentTrackIndex);
if (currentTrack != null) {
Map<String, Object> map = new HashMap<>();
// map.put("vodFormat",currentTrack.getVodFormat());
map.put("videoHeight", currentTrack.getVideoHeight());
map.put("videoWidth", currentTrack.getVideoHeight());
map.put("subtitleLanguage", currentTrack.getSubtitleLang());
map.put("trackBitrate", currentTrack.getVideoBitrate());
// map.put("vodFileSize",currentTrack.getVodFileSize());
map.put("trackIndex", currentTrack.getIndex());
// map.put("trackDefinition",currentTrack.getVodDefinition());
map.put("audioSampleFormat", currentTrack.getAudioSampleFormat());
map.put("audioLanguage", currentTrack.getAudioLang());
// map.put("vodPlayUrl",currentTrack.getVodPlayUrl());
map.put("trackType", currentTrack.getType().ordinal());
map.put("audioSamplerate", currentTrack.getAudioSampleRate());
map.put("audioChannels", currentTrack.getAudioChannels());
result.success(map);
}
break;
case "selectTrack":
Map<String, Object> selectTrackMap = (Map<String, Object>) methodCall.arguments;
Integer trackIdx = (Integer) selectTrackMap.get("trackIdx");
Integer accurate = (Integer) selectTrackMap.get("accurate");
selectTrack(trackIdx, accurate == 1);
break;
case "addExtSubtitle":
String extSubtitlUrl = (String) methodCall.arguments;
addExtSubtitle(extSubtitlUrl);
break;
case "selectExtSubtitle":
Map<String, Object> selectExtSubtitleMap = (Map<String, Object>) methodCall.arguments;
Integer trackIndex = (Integer) selectExtSubtitleMap.get("trackIndex");
Boolean selectExtSubtitlEnable = (Boolean) selectExtSubtitleMap.get("enable");
selectExtSubtitle(trackIndex, selectExtSubtitlEnable);
result.success(null);
break;
case "enableConsoleLog":
Boolean enableLog = (Boolean) methodCall.arguments;
enableConsoleLog(enableLog);
break;
case "setLogLevel":
Integer level = (Integer) methodCall.arguments;
setLogLevel(level);
break;
case "getLogLevel":
result.success(getLogLevel());
break;
case "createDeviceInfo":
result.success(createDeviceInfo());
break;
case "addBlackDevice":
Map<String, String> addBlackDeviceMap = methodCall.arguments();
String blackType = addBlackDeviceMap.get("black_type");
String blackDevice = addBlackDeviceMap.get("black_device");
addBlackDevice(blackType, blackDevice);
break;
// case "createThumbnailHelper":
// String thhumbnailUrl = (String) methodCall.arguments;
// createThumbnailHelper(thhumbnailUrl);
// break;
// case "requestBitmapAtPosition":
// Integer requestBitmapProgress = (Integer) methodCall.arguments;
// requestBitmapAtPosition(requestBitmapProgress);
// break;
case "getSDKVersion":
result.success(XdfPlayerFactory.getSdkVersion());
break;
// case "setPreferPlayerName":
// String playerName = methodCall.arguments();
// setPlayerName(playerName);
// break;
// case "getPlayerName":
// result.success(getPlayerName());
// break;
case "setStreamDelayTime":
Map<String, Object> streamDelayTimeMap = (Map<String, Object>) methodCall.arguments;
Integer index = (Integer) streamDelayTimeMap.get("index");
Integer time = (Integer) streamDelayTimeMap.get("time");
setStreamDelayTime(index, time);
break;
default:
result.notImplemented();
}
}
public void createXdfPlayer()
{
mXdfPlayer = XdfPlayerFactory.createXdfPlayer(mContext);
initListener(mXdfPlayer);
}
public void setDataSource(String url)
{
if (mXdfPlayer != null) {
UrlSource urlSource = new UrlSource();
urlSource.setUri(url);//播放地址,可以是第三方点播地址
urlSource.setKoolToken("75192220");//坑,不调用它会崩溃。
mXdfPlayer.setDataSource(urlSource);
}
}
public void prepare()
{
if (mXdfPlayer != null) {
mXdfPlayer.prepare();
}
}
private void start()
{
if (mXdfPlayer != null) {
mXdfPlayer.start();
}
}
private void pause()
{
if (mXdfPlayer != null) {
mXdfPlayer.pause();
}
}
private void stop()
{
if (mXdfPlayer != null) {
mXdfPlayer.stop();
}
}
private void release()
{
if (mXdfPlayer != null) {
mXdfPlayer.release();
mXdfPlayer = null;
}
}
private void seekTo(long position, int seekMode)
{
if (mXdfPlayer != null) {
XdfPlayer.SeekMode mSeekMode;
if (seekMode == XdfPlayer.SeekMode.Accurate.getValue()) {
mSeekMode = XdfPlayer.SeekMode.Accurate;
} else {
mSeekMode = XdfPlayer.SeekMode.Inaccurate;
}
mXdfPlayer.seekTo(position, mSeekMode);
}
}
private MediaInfo getMediaInfo()
{
if (mXdfPlayer != null) {
return mXdfPlayer.getMediaInfo();
}
return null;
}
private void snapshot()
{
if (mXdfPlayer != null) {
mXdfPlayer.snapshot();
}
}
private void setLoop(Boolean isLoop)
{
if (mXdfPlayer != null) {
mXdfPlayer.setLoop(isLoop);
}
}
private Boolean isLoop()
{
return mXdfPlayer != null && mXdfPlayer.isLoop();
}
private void setAutoPlay(Boolean isAutoPlay)
{
if (mXdfPlayer != null) {
mXdfPlayer.setAutoPlay(isAutoPlay);
}
}
private Boolean isAutoPlay()
{
if (mXdfPlayer != null) {
return mXdfPlayer.isAutoPlay();
}
return false;
}
private void setMuted(Boolean muted)
{
if (mXdfPlayer != null) {
mXdfPlayer.setMute(muted);
}
}
private Boolean isMuted()
{
if (mXdfPlayer != null) {
return mXdfPlayer.isMute();
}
return false;
}
private void setEnableHardWareDecoder(Boolean mEnableHardwareDecoder)
{
if (mXdfPlayer != null) {
mXdfPlayer.enableHardwareDecoder(mEnableHardwareDecoder);
}
}
private void setScaleMode(int model)
{
if (mXdfPlayer != null) {
XdfPlayer.ScaleMode mScaleMode = XdfPlayer.ScaleMode.SCALE_ASPECT_FIT;
if (model == XdfPlayer.ScaleMode.SCALE_ASPECT_FIT.getValue()) {
mScaleMode = XdfPlayer.ScaleMode.SCALE_ASPECT_FIT;
} else if (model == XdfPlayer.ScaleMode.SCALE_ASPECT_FILL.getValue()) {
mScaleMode = XdfPlayer.ScaleMode.SCALE_ASPECT_FILL;
} else if (model == XdfPlayer.ScaleMode.SCALE_TO_FILL.getValue()) {
mScaleMode = XdfPlayer.ScaleMode.SCALE_TO_FILL;
}
mXdfPlayer.setScaleMode(mScaleMode);
}
}
private int getScaleMode()
{
int scaleMode = XdfPlayer.ScaleMode.SCALE_ASPECT_FIT.getValue();
if (mXdfPlayer != null) {
scaleMode = mXdfPlayer.getScaleMode().getValue();
}
return scaleMode;
}
private void setMirrorMode(int mirrorMode)
{
if (mXdfPlayer != null) {
XdfPlayer.MirrorMode mMirrorMode;
if (mirrorMode == XdfPlayer.MirrorMode.MIRROR_MODE_HORIZONTAL.getValue()) {
mMirrorMode = XdfPlayer.MirrorMode.MIRROR_MODE_HORIZONTAL;
} else if (mirrorMode == XdfPlayer.MirrorMode.MIRROR_MODE_VERTICAL.getValue()) {
mMirrorMode = XdfPlayer.MirrorMode.MIRROR_MODE_VERTICAL;
} else {
mMirrorMode = XdfPlayer.MirrorMode.MIRROR_MODE_NONE;
}
mXdfPlayer.setMirrorMode(mMirrorMode);
}
}
private int getMirrorMode()
{
int mirrorMode = XdfPlayer.MirrorMode.MIRROR_MODE_NONE.getValue();
if (mXdfPlayer != null) {
mirrorMode = mXdfPlayer.getMirrorMode().getValue();
}
return mirrorMode;
}
private void setRotateMode(int rotateMode)
{
if (mXdfPlayer != null) {
XdfPlayer.RotateMode mRotateMode;
if (rotateMode == XdfPlayer.RotateMode.ROTATE_90.getValue()) {
mRotateMode = XdfPlayer.RotateMode.ROTATE_90;
} else if (rotateMode == XdfPlayer.RotateMode.ROTATE_180.getValue()) {
mRotateMode = XdfPlayer.RotateMode.ROTATE_180;
} else if (rotateMode == XdfPlayer.RotateMode.ROTATE_270.getValue()) {
mRotateMode = XdfPlayer.RotateMode.ROTATE_270;
} else {
mRotateMode = XdfPlayer.RotateMode.ROTATE_0;
}
mXdfPlayer.setRotateMode(mRotateMode);
}
}
private int getRotateMode()
{
int rotateMode = XdfPlayer.RotateMode.ROTATE_0.getValue();
if (mXdfPlayer != null) {
rotateMode = mXdfPlayer.getRotateMode().getValue();
}
return rotateMode;
}
private void setSpeed(double speed)
{
if (mXdfPlayer != null) {
mXdfPlayer.setSpeed((float) speed);
}
}
private double getSpeed()
{
double speed = 0;
if (mXdfPlayer != null) {
speed = mXdfPlayer.getSpeed();
}
return speed;
}
private void setVideoBackgroundColor(long color)
{
if (mXdfPlayer != null) {
mXdfPlayer.setVideoBackgroundColor((int) color);
}
}
private void setVolume(double volume)
{
if (mXdfPlayer != null) {
mXdfPlayer.setVolume((float) volume);
}
}
private double getVolume()
{
double volume = 1.0;
if (mXdfPlayer != null) {
volume = mXdfPlayer.getVolume();
}
return volume;
}
private void setConfig(PlayerConfig playerConfig)
{
if (mXdfPlayer != null) {
mXdfPlayer.setConfig(playerConfig);
}
}
private PlayerConfig getConfig()
{
if (mXdfPlayer != null) {
return mXdfPlayer.getConfig();
}
return null;
}
private CacheConfig getCacheConfig()
{
return new CacheConfig();
}
private void setCacheConfig(CacheConfig cacheConfig)
{
if (mXdfPlayer != null) {
mXdfPlayer.setCacheConfig(cacheConfig);
}
}
private TrackInfo getCurrentTrack(int currentTrackIndex)
{
if (mXdfPlayer != null) {
return mXdfPlayer.currentTrack(currentTrackIndex);
} else {
return null;
}
}
private void selectTrack(int trackId, boolean accurate)
{
if (mXdfPlayer != null) {
mXdfPlayer.selectTrack(trackId);
}
}
private void addExtSubtitle(String url)
{
if (mXdfPlayer != null) {
mXdfPlayer.addExtSubtitle(url);
}
}
private void selectExtSubtitle(int trackIndex, boolean enable)
{
if (mXdfPlayer != null) {
mXdfPlayer.selectExtSubtitle(trackIndex, enable);
}
}
private void enableConsoleLog(Boolean enableLog)
{
Logger.getInstance(mContext).enableConsoleLog(enableLog);
}
private void setLogLevel(int level)
{
Logger.LogLevel mLogLevel;
if (level == Logger.LogLevel.AF_LOG_LEVEL_NONE.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_NONE;
} else if (level == Logger.LogLevel.AF_LOG_LEVEL_FATAL.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_FATAL;
} else if (level == Logger.LogLevel.AF_LOG_LEVEL_ERROR.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_ERROR;
} else if (level == Logger.LogLevel.AF_LOG_LEVEL_WARNING.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_WARNING;
} else if (level == Logger.LogLevel.AF_LOG_LEVEL_INFO.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_INFO;
} else if (level == Logger.LogLevel.AF_LOG_LEVEL_DEBUG.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_DEBUG;
} else if (level == Logger.LogLevel.AF_LOG_LEVEL_TRACE.getValue()) {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_TRACE;
} else {
mLogLevel = Logger.LogLevel.AF_LOG_LEVEL_NONE;
}
Logger.getInstance(mContext).setLogLevel(mLogLevel);
}
private Integer getLogLevel()
{
return Logger.getInstance(mContext).getLogLevel().getValue();
}
private String createDeviceInfo()
{
XdfPlayerFactory.DeviceInfo deviceInfo = new XdfPlayerFactory.DeviceInfo();
deviceInfo.model = Build.MODEL;
return deviceInfo.model;
}
private void addBlackDevice(String blackType, String modelInfo)
{
XdfPlayerFactory.DeviceInfo deviceInfo = new XdfPlayerFactory.DeviceInfo();
deviceInfo.model = modelInfo;
XdfPlayerFactory.BlackType XdfPlayerBlackType;
if (!TextUtils.isEmpty(blackType) && "HW_Decode_H264".equals(blackType)) {
XdfPlayerBlackType = XdfPlayerFactory.BlackType.HW_Decode_H264;
} else {
XdfPlayerBlackType = XdfPlayerFactory.BlackType.HW_Decode_HEVC;
}
XdfPlayerFactory.addBlackDevice(XdfPlayerBlackType, deviceInfo);
}
// private void setPlayerName(String playerName) {
// if(mXdfPlayer != null){
// mXdfPlayer.setPreferPlayerName(playerName);
// }
// }
//
// private String getPlayerName(){
// return mXdfPlayer == null ? "" : mXdfPlayer.getPlayerName();
// }
private void setStreamDelayTime(int index, int time)
{
if (mXdfPlayer != null) {
mXdfPlayer.setStreamDelayTime(index, time);
}
}
}
Flutter端
- 创建
XdfPlayerView
less
XdfPlayerView xdfPlayerView = XdfPlayerView(
onCreated: onViewPlayerCreated,
x: x,
y: y,
width: width,
height: height);
解释:onViewPlayerCreated 类似于一个内部回调方法,当XdfPlayerView内部 AndroidView 被创建时调用并传入相应的id,此处onViewPlayerCreated根据此id,去创建消息通道,初始化监听等
- 创建消息通道
在void onViewPlayerCreated(int id) async {}方法中创建消息通道
ini
FlutterXdfPlayer? fXdfPlayer = FlutterXdfPlayerFactory().createXdfPlayer(id);
上述方法根据flutter端播放器承载view创建时返回的id创建指定的消息通道.后续我们可以拿到此fXdfPlayer去控制原生播放器的 播放 暂停 停止 等一系列动作
FlutterXdfPlayerFactory.dart
csharp
class FlutterXdfPlayerFactory {
// MethodChannel _methodChannel = MethodChannel("plugins.flutter_xdfplayer_factory");
FlutterXdfPlayer createXdfPlayer(int id) {
print("---------->创建播放器 createXdfPlayer");
// if (Platform.isAndroid) {
// _methodChannel.invokeMethod("createXdfPlayer");
// }
FlutterXdfPlayer flutterXdfPlayer = FlutterXdfPlayer.init(id);
return flutterXdfPlayer;
}
}
FlutterXdfPlayer.dart
1各种回调的定义
2原生与flutter消息通道的绑定
3接收原生消息
4发送消息到原生
ini
typedef OnPrepared = void Function();
typedef OnRenderingStart = void Function();
typedef OnVideoSizeChanged = void Function(int width, int height);
typedef OnSnapShot = void Function(String path);
typedef OnSeekComplete = void Function();
typedef OnSeiData = void Function(); //TODO
typedef OnLoadingBegin = void Function();
typedef OnLoadingProgress = void Function(int percent, double netSpeed);
typedef OnLoadingEnd = void Function();
typedef OnStateChanged = void Function(int newState);
typedef OnSubtitleExtAdded = void Function(int trackIndex, String url);
typedef OnSubtitleShow = void Function(
int trackIndex, int subtitleID, String subtitle);
typedef OnSubtitleHide = void Function(int trackIndex, int subtitleID);
typedef OnTrackReady = void Function();
typedef OnInfo = void Function(int infoCode, int extraValue, String extraMsg);
typedef OnError = void Function(
int errorCode, String errorExtra, String errorMsg);
typedef OnCompletion = void Function();
typedef OnTrackChanged = void Function(dynamic value);
typedef OnThumbnailPreparedSuccess = void Function();
typedef OnThumbnailPreparedFail = void Function();
typedef OnThumbnailGetSuccess = void Function(
Uint8List bitmap, Int64List range);
typedef OnThumbnailGetFail = void Function();
class FlutterXdfPlayer {
OnLoadingBegin? onLoadingBegin;
OnLoadingProgress? onLoadingProgress;
OnLoadingEnd? onLoadingEnd;
OnPrepared? onPrepared;
OnRenderingStart? onRenderingStart;
OnVideoSizeChanged? onVideoSizeChanged;
OnSeekComplete? onSeekComplete;
OnStateChanged? onStateChanged;
OnInfo? onInfo;
OnCompletion? onCompletion;
OnTrackReady? onTrackReady;
OnError? onError;
OnSnapShot? onSnapShot;
OnTrackChanged? onTrackChanged;
OnThumbnailPreparedSuccess? onThumbnailPreparedSuccess;
OnThumbnailPreparedFail? onThumbnailPreparedFail;
OnThumbnailGetSuccess? onThumbnailGetSuccess;
OnThumbnailGetFail? onThumbnailGetFail;
//外挂字幕
OnSubtitleExtAdded? onSubtitleExtAdded;
OnSubtitleHide? onSubtitleHide;
OnSubtitleShow? onSubtitleShow;
//向原生发消息
late MethodChannel channel;
//接收原生传过来的消息
late EventChannel eventChannel;
FlutterXdfPlayer.init(int id) {
channel = new MethodChannel('flutter_xdfplayer$id');
eventChannel = EventChannel('flutter_xdfplayer_event$id');
eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
void setOnPrepared(OnPrepared prepared) {
this.onPrepared = prepared;
}
void setOnRenderingStart(OnRenderingStart renderingStart) {
this.onRenderingStart = renderingStart;
}
void setOnVideoSizeChanged(OnVideoSizeChanged videoSizeChanged) {
this.onVideoSizeChanged = videoSizeChanged;
}
void setOnSnapShot(OnSnapShot snapShot) {
this.onSnapShot = snapShot;
}
void setOnSeekComplete(OnSeekComplete seekComplete) {
this.onSeekComplete = seekComplete;
}
void setOnError(OnError onError) {
this.onError = onError;
}
void setOnLoadingStatusListener(
{OnLoadingBegin? loadingBegin,
OnLoadingProgress? loadingProgress,
OnLoadingEnd? loadingEnd}) {
this.onLoadingBegin = loadingBegin;
this.onLoadingProgress = loadingProgress;
this.onLoadingEnd = loadingEnd;
}
void setOnStateChanged(OnStateChanged stateChanged) {
this.onStateChanged = stateChanged;
}
void setOnInfo(OnInfo info) {
this.onInfo = info;
}
void setOnCompletion(OnCompletion completion) {
this.onCompletion = completion;
}
void setOnTrackReady(OnTrackReady onTrackReady) {
this.onTrackReady = onTrackReady;
}
void setOnTrackChanged(OnTrackChanged onTrackChanged) {
this.onTrackChanged = onTrackChanged;
}
void setOnThumbnailPreparedListener(
{OnThumbnailPreparedSuccess? preparedSuccess,
OnThumbnailPreparedFail? preparedFail}) {
this.onThumbnailPreparedSuccess = preparedSuccess;
this.onThumbnailPreparedFail = preparedFail;
}
void setOnThumbnailGetListener(
{OnThumbnailGetSuccess? onThumbnailGetSuccess,
OnThumbnailGetFail? onThumbnailGetFail}) {
this.onThumbnailGetSuccess = onThumbnailGetSuccess;
this.onThumbnailGetSuccess = onThumbnailGetSuccess;
}
void setOnSubtitleShow(OnSubtitleShow onSubtitleShow) {
this.onSubtitleShow = onSubtitleShow;
}
void setOnSubtitleHide(OnSubtitleHide onSubtitleHide) {
this.onSubtitleHide = onSubtitleHide;
}
void setOnSubtitleExtAdded(OnSubtitleExtAdded onSubtitleExtAdded) {
this.onSubtitleExtAdded = onSubtitleExtAdded;
}
Future<void> createXdfPlayer() async {
return channel.invokeMethod('createXdfPlayer');
}
Future<void> setUrl(String url) async {
assert(url != null);
return channel.invokeMethod('setUrl', url);
}
Future<void> prepare() async {
return channel.invokeMethod('prepare');
}
Future<void> play() async {
return channel.invokeMethod('play');
}
Future<void> pause() async {
return channel.invokeMethod('pause');
}
Future<dynamic> snapshot(String path) async {
return channel.invokeMethod('snapshot', path);
}
Future<void> stop() async {
return channel.invokeMethod('stop');
}
Future<void> destroy() async {
return channel.invokeMethod('destroy');
}
Future<void> seekTo(int position, int seekMode) async {
var map = {"position": position, "seekMode": seekMode};
return channel.invokeMethod("seekTo", map);
}
Future<bool?> isLoop() async {
return channel.invokeMethod('isLoop');
}
Future<void> setLoop(bool isloop) async {
return channel.invokeMethod('setLoop', isloop);
}
Future<bool?> isAutoPlay() async {
return channel.invokeMethod('isAutoPlay');
}
Future<void> setAutoPlay(bool isAutoPlay) async {
return channel.invokeMethod('setAutoPlay', isAutoPlay);
}
Future<bool?> isMuted() async {
return channel.invokeMethod('isMuted');
}
Future<void> setMuted(bool isMuted) async {
return channel.invokeMethod('setMuted', isMuted);
}
Future<bool?> enableHardwareDecoder() async {
return channel.invokeMethod('enableHardwareDecoder');
}
Future<void> setEnableHardwareDecoder(bool isHardWare) async {
return channel.invokeMethod('setEnableHardwareDecoder', isHardWare);
}
Future<int?> getRotateMode() async {
return channel.invokeMethod('getRotateMode');
}
Future<int?> getDuration() async {
return channel.invokeMethod('getDuration');
}
Future<void> setRotateMode(int mode) async {
return channel.invokeMethod('setRotateMode', mode);
}
Future<int?> getScXdfngMode() async {
return channel.invokeMethod('getScXdfngMode');
}
Future<void> setScXdfngMode(int mode) async {
return channel.invokeMethod('setScXdfngMode', mode);
}
Future<int?> getMirrorMode() async {
return channel.invokeMethod('getMirrorMode');
}
Future<void> setMirrorMode(int mode) async {
return channel.invokeMethod('setMirrorMode', mode);
}
Future<double?> getRate() async {
return channel.invokeMethod('getRate');
}
Future<void> setRate(double mode) async {
return channel.invokeMethod('setRate', mode);
}
Future<void> setVideoBackgroundColor(var color) async {
return channel.invokeMethod('setVideoBackgroundColor', color);
}
Future<void> setVolume(double volume) async {
return channel.invokeMethod('setVolume', volume);
}
Future<double?> getVolume() async {
return channel.invokeMethod('getVolume');
}
Future<dynamic> getConfig() async {
return channel.invokeMethod("getConfig");
}
Future<void> setConfig(Map map) async {
return channel.invokeMethod("setConfig", map);
}
Future<dynamic> getCacheConfig() async {
return channel.invokeMethod("getCacheConfig");
}
Future<void> setCacheConfig(Map map) async {
return channel.invokeMethod("setCacheConfig", map);
}
///return deviceInfo
Future<String?> createDeviceInfo() async {
return channel.invokeMethod("createDeviceInfo");
}
///type : {FlutterAvpdef.BLACK_DEVICES_H264 / FlutterAvpdef.BLACK_DEVICES_HEVC}
Future<void> addBlackDevice(String type, String model) async {
var map = {
'black_type': type,
'black_device': model,
};
return channel.invokeMethod("addBlackDevice", map);
}
Future<String?> getSDKVersion() async {
return channel.invokeMethod("getSDKVersion");
}
Future<void> enableMix(bool enable) {
return channel.invokeMethod("enableMix", enable);
}
Future<void> enableConsoleLog(bool enable) {
return channel.invokeMethod("enableConsoleLog", enable);
}
Future<void> setLogLevel(int level) async {
return channel.invokeMethod("setLogLevel", level);
}
Future<int?> getLogLevel() {
return channel.invokeMethod("getLogLevel");
}
Future<dynamic> getMediaInfo() {
return channel.invokeMethod("getMediaInfo");
}
Future<dynamic> getCurrentTrack(int trackIdx) {
return channel.invokeMethod("getCurrentTrack", trackIdx);
}
Future<dynamic> createThumbnailHelper(String thumbnail) {
return channel.invokeMethod("createThumbnailHelper", thumbnail);
}
Future<dynamic> requestBitmapAtPosition(int position) {
return channel.invokeMethod("requestBitmapAtPosition", position);
}
Future<void> addExtSubtitle(String url) {
return channel.invokeMethod("addExtSubtitle", url);
}
Future<void> selectExtSubtitle(int trackIndex, bool enable) {
var map = {'trackIndex': trackIndex, 'enable': enable};
return channel.invokeMethod("selectExtSubtitle", map);
}
// accurate 0 为不精确 1 为精确 不填为忽略
Future<void> selectTrack(int trackIdx, {int accurate = -1}) {
var map = {
'trackIdx': trackIdx,
'accurate': accurate,
};
return channel.invokeMethod("selectTrack", map);
}
Future<void> setPrivateService(Int8List data) {
return channel.invokeMethod("setPrivateService", data);
}
Future<void> setStreamDelayTime(int trackIdx, int time) {
var map = {'index': trackIdx, 'time': time};
return channel.invokeMethod("setStreamDelayTime", map);
}
void _onEvent(dynamic event) {
String method = event[EventChanneldef.TYPE_KEY];
switch (method) {
case "onPrepared":
if (onPrepared != null) {
onPrepared?.call();
}
break;
case "onRenderingStart":
if (onRenderingStart != null) {
onRenderingStart?.call();
}
break;
case "onVideoSizeChanged":
if (onVideoSizeChanged != null) {
int width = event['width'];
int height = event['height'];
onVideoSizeChanged?.call(width, height);
}
break;
case "onSnapShot":
if (onSnapShot != null) {
String snapShotPath = event['snapShotPath'];
onSnapShot?.call(snapShotPath);
}
break;
case "onChangedSuccess":
break;
case "onChangedFail":
break;
case "onSeekComplete":
if (onSeekComplete != null) {
onSeekComplete?.call();
}
break;
case "onSeiData":
break;
case "onLoadingBegin":
if (onLoadingBegin != null) {
onLoadingBegin?.call();
}
break;
case "onLoadingProgress":
int percent = event['percent'];
double netSpeed = event['netSpeed'];
if (onLoadingProgress != null) {
onLoadingProgress?.call(percent, netSpeed);
}
break;
case "onLoadingEnd":
if (onLoadingEnd != null) {
print("onLoadingEnd");
onLoadingEnd?.call();
}
break;
case "onStateChanged":
if (onStateChanged != null) {
int newState = event['newState'];
onStateChanged?.call(newState);
}
break;
case "onInfo":
if (onInfo != null) {
// print("-------------------> info "+onInfo.toString());
int infoCode = event['infoCode'];
int extraValue = event['extraValue'];
String? extraMsg = event['extraMsg'];
onInfo?.call(infoCode, extraValue, extraMsg??"");
}
break;
case "onError":
if (onError != null) {
int errorCode = event['errorCode'];
String? errorExtra = event['errorExtra'];
String? errorMsg = event['errorMsg'];
onError?.call(errorCode, errorExtra??"errorExtra", errorMsg??"errorMsg");
}
break;
case "onCompletion":
if (onCompletion != null) {
onCompletion?.call();
}
break;
case "onTrackReady":
if (onTrackReady != null) {
this.onTrackReady?.call();
}
break;
case "onTrackChanged":
if (onTrackChanged != null) {
dynamic info = event['info'];
this.onTrackChanged?.call(info);
}
break;
case "thumbnail_onPrepared_Success":
if (onThumbnailPreparedSuccess != null) {
onThumbnailPreparedSuccess?.call();
}
break;
case "thumbnail_onPrepared_Fail":
if (onThumbnailPreparedFail != null) {
onThumbnailPreparedFail?.call();
}
break;
case "onThumbnailGetSuccess":
dynamic bitmap = event['thumbnailbitmap'];
dynamic range = event['thumbnailRange'];
if (onThumbnailGetSuccess != null) {
if (Platform.isIOS) {
range = Int64List.fromList(range.cast<int>());
}
onThumbnailGetSuccess?.call(bitmap, range);
}
break;
case "onThumbnailGetFail":
if (onThumbnailGetFail != null) {
onThumbnailGetFail?.call();
}
break;
case "onSubtitleExtAdded":
if (onSubtitleExtAdded != null) {
int trackIndex = event['trackIndex'];
String url = event['url'];
onSubtitleExtAdded?.call(trackIndex, url);
}
break;
case "onSubtitleShow":
if (onSubtitleShow != null) {
int trackIndex = event['trackIndex'];
int subtitleID = event['subtitleID'];
String subtitle = event['subtitle'];
onSubtitleShow?.call(trackIndex, subtitleID, subtitle);
}
break;
case "onSubtitleHide":
if (onSubtitleHide != null) {
int trackIndex = event['trackIndex'];
int subtitleID = event['subtitleID'];
onSubtitleHide?.call(trackIndex, subtitleID);
}
break;
}
}
void _onError(dynamic error) {}
}
typedef void XdfPlayerViewCreatedCallback(int id);
scala
class XdfPlayerView extends StatefulWidget {
final XdfPlayerViewCreatedCallback? onCreated;
final x;
final y;
final width;
final height;
XdfPlayerView({
Key? key,
@required this.onCreated,
@required this.x,
@required this.y,
@required this.width,
@required this.height,
});
@override
State<StatefulWidget> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<XdfPlayerView> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return nativeView();
}
//返回安卓或ios播放器本地view
Widget nativeView() {
if (Platform.isAndroid) {
return AndroidView(
viewType: 'flutter_xdfplayer_view',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams: <String, dynamic>{
"x": widget.x,
"y": widget.y,
"width": widget.width,
"height": widget.height,
},
creationParamsCodec: const StandardMessageCodec(),
);
} else {
return UiKitView(
viewType: 'plugins.flutter_xdfplayer',
onPlatformViewCreated: _onPlatformViewCreated,
creationParams: <String, dynamic>{
"x": widget.x,
"y": widget.y,
"width": widget.width,
"height": widget.height,
},
creationParamsCodec: const StandardMessageCodec(),
);
}
}
Future<void> _onPlatformViewCreated(id) async {
if (widget.onCreated != null) {
widget.onCreated?.call(id);
}
}
}
Flutter中一些类说明
- FlutterXdfPlayer 是flutter播放器最重要的一个类。它处理一下逻辑
各种回调的定义
原生与flutter消息通道的绑定
接收原生消息
发送消息到原生
- XdfPlayerView 是flutter播放器承载类,它根据传入的位置宽高初始化平台view 外界只是把它当做一个普通的Widget即可。
一些指标
注:此指标跟集成底层视频播放SDK相关。不同的SDK,可能有不同的数据表现
Flutter端:
格式 | 清晰度 | 原帧率 | 播放帧率 | 其他情况备注 |
---|---|---|---|---|
mp4 | 360p | 30fps | 29fps-31fps | 1倍速:播放正常无卡顿。 2倍速播放正常 |
mp4 | 720p | 30fps | 29fps-31fps | 1倍速:播放正常无卡顿。 2倍速播放正常 |
mp4(flutter demo) | 1080p | 60fps | 60fps (偶尔不稳定) | 1倍速:播放正常,偶尔有掉帧现象不过很快能恢复。 2倍速:会卡死(画面不动,音频在播放)关掉硬解恢复正常但丢帧严重 |
mp4(xdf原生demo) | 1080p | 60fps | 60fps(偶尔不稳定) | 1倍速:播放正常,偶尔有掉帧现象不过很快能恢复。 2倍速:会卡死(画面不动,音频在播放)关掉硬解恢复正常但丢帧严重 |
mp4 | 4K | 24fps | 24fps | 1倍速:播放正常无卡顿。2倍速播放正常 |
经调研,flutter在播放视频时与纯原生播放相差不大。但是在播放高帧率视频60fps时倍速播放会卡顿,跟原生播放器SDK有关
总结
在Flutter刚出来时,我们之前就已经做过一些播放器相关调研。之前采用的是外接纹理的方式进行集成。用公司原有底层播放器so库进行包装,进行flutter视频播放。最终结果是,播放mp4文件正常。播放公司加密m3u8文件会有卡顿,播放速度明显变慢等现象(比如1.0倍速,播起来像是0.5倍速。)最终放弃此方案。
后来随着技术的演进官方适配等,此次调研结果有明显改善,虽然这次使用的集团SDK无法适配外接纹理方案(有声音没视频,这种方案是flutter官方video_player供的一种视频集成方案)。但是采用flutter虚拟显示的方式,调研也能正常进行视频播放。不过目前由于手头机型有限,只拿手中现有测试机器进行了数据采集,至于后续各种安卓机型适配还需要做深入了解。