likes
comments
collection
share

Flutter 以PlantformView形式集成原生视频播放器(android)端

作者站长头像
站长
· 阅读数 14

介绍

在立项初期,我们参考了官方video_player视频插件的相同方式(Texture外接纹理),进行flutter播放器的集成。但是由于我们的XXXPlayer,在集成后无法正常播放视频(有视频无画面)。所以换了一种官方推荐的新模式。混合集成(将平台view显示到flutter界面上)的方式进行flutter视频播放器的封装。

基本原理

虚拟显示集成基本原理

原生端处理

  1. 创建PlatformView子类并实现getView方法返回平台View
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() {}
}
  1. 创建一个用来创建上面PlatformView的工厂类PlatformViewFactory并实现create方法返回PlatformView
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去做相对应的事情。

  1. 插件或者FlutterActivity中注册上面准备好的平台视图:
  • FlutterActivity中注册
public class MainActivity extends FlutterActivity {
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        flutterEngine
            .getPlatformViewsController()
            .getRegistry()
            .registerViewFactory("<platform-view-type>", new NativeViewFactory());
    }
}
  • 插件中注册(因为需要做成播放器插件,我们采用的这种方式)
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端处理

  1. 进行如下处理我们就可以在Flutter端显示一个平台view了。
  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创建并返回
3PlatformView中会创建具体的平台View(比如TextView等)并通过getView()方法中返回,由于是播放器项目,这里返回的是TextureView。
4在创建TextureView时在FlutterXXXPlayer中创建player及相关消息通道,确保点对点的一个播放器对应一对消息进出通道

原生端创建

  1. 插件注册 PlatformViewFactory
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)
    {

    }
}
  1. PlatformViewFactory返回 PlatformView
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);
    }
}
  1. PlayerView 中创建原生平台View -> TextureView 并初始化FlutterXdfPlayer
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) {

            }
        });
    }
}
  1. 创建播放器及进出的消息通道FlutterXdfPlayer,其中此类利用接收flutter端通过通道传过来消息,去处理外界调用播放器的行为消息,比如说准备,开始暂停等。与播放器相关的所有回调消息,如播放完成,播放进度回调,播放错误等,通过此类的event消息发出给到flutter端。
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端

  1. 创建XdfPlayerView
XdfPlayerView xdfPlayerView = XdfPlayerView(
    onCreated: onViewPlayerCreated,
    x: x,
    y: y,
    width: width,
    height: height);

解释:onViewPlayerCreated 类似于一个内部回调方法,当XdfPlayerView内部 AndroidView 被创建时调用并传入相应的id,此处onViewPlayerCreated根据此id,去创建消息通道,初始化监听等

  1. 创建消息通道

在void onViewPlayerCreated(int id) async {}方法中创建消息通道

FlutterXdfPlayer? fXdfPlayer = FlutterXdfPlayerFactory().createXdfPlayer(id);

上述方法根据flutter端播放器承载view创建时返回的id创建指定的消息通道.后续我们可以拿到此fXdfPlayer去控制原生播放器的 播放 暂停 停止 等一系列动作

FlutterXdfPlayerFactory.dart

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发送消息到原生

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);
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中一些类说明

  1. FlutterXdfPlayer 是flutter播放器最重要的一个类。它处理一下逻辑

各种回调的定义 原生与flutter消息通道的绑定 接收原生消息 发送消息到原生

  1. XdfPlayerView 是flutter播放器承载类,它根据传入的位置宽高初始化平台view 外界只是把它当做一个普通的Widget即可。

一些指标

注:此指标跟集成底层视频播放SDK相关。不同的SDK,可能有不同的数据表现

Flutter端:

格式清晰度原帧率播放帧率其他情况备注
mp4360p30fps29fps-31fps1倍速:播放正常无卡顿。 2倍速播放正常
mp4720p30fps29fps-31fps1倍速:播放正常无卡顿。 2倍速播放正常
mp4(flutter demo)1080p60fps60fps (偶尔不稳定)1倍速:播放正常,偶尔有掉帧现象不过很快能恢复。 2倍速:会卡死(画面不动,音频在播放)关掉硬解恢复正常但丢帧严重
mp4(xdf原生demo)1080p60fps60fps(偶尔不稳定)1倍速:播放正常,偶尔有掉帧现象不过很快能恢复。 2倍速:会卡死(画面不动,音频在播放)关掉硬解恢复正常但丢帧严重
mp44K24fps24fps1倍速:播放正常无卡顿。2倍速播放正常

经调研,flutter在播放视频时与纯原生播放相差不大。但是在播放高帧率视频60fps时倍速播放会卡顿,跟原生播放器SDK有关

总结

在Flutter刚出来时,我们之前就已经做过一些播放器相关调研。之前采用的是外接纹理的方式进行集成。用公司原有底层播放器so库进行包装,进行flutter视频播放。最终结果是,播放mp4文件正常。播放公司加密m3u8文件会有卡顿,播放速度明显变慢等现象(比如1.0倍速,播起来像是0.5倍速。)最终放弃此方案。

后来随着技术的演进官方适配等,此次调研结果有明显改善,虽然这次使用的集团SDK无法适配外接纹理方案(有声音没视频,这种方案是flutter官方video_player供的一种视频集成方案)。但是采用flutter虚拟显示的方式,调研也能正常进行视频播放。不过目前由于手头机型有限,只拿手中现有测试机器进行了数据采集,至于后续各种安卓机型适配还需要做深入了解。