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

介绍

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

基本原理

虚拟显示集成基本原理

原生端处理

  1. 创建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() {}
}
  1. 创建一个用来创建上面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去做相对应的事情。

  1. 插件或者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端处理

  1. 进行如下处理我们就可以在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及相关消息通道,确保点对点的一个播放器对应一对消息进出通道

原生端创建

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

    }
}
  1. 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);
    }
}
  1. 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) {

            }
        });
    }
}
  1. 创建播放器及进出的消息通道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端

  1. 创建XdfPlayerView
less 复制代码
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 {}方法中创建消息通道

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

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

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

  1. 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虚拟显示的方式,调研也能正常进行视频播放。不过目前由于手头机型有限,只拿手中现有测试机器进行了数据采集,至于后续各种安卓机型适配还需要做深入了解。

相关推荐
钛态10 小时前
Flutter for OpenHarmony:mockito 单元测试的替身演员,轻松模拟复杂依赖(测试驱动开发必备) 深度解析与鸿蒙适配指南
服务器·驱动开发·安全·flutter·华为·单元测试·harmonyos
念格13 小时前
Flutter 弹窗 UI 不刷新?用 StatefulBuilder 解决
flutter
程序员老刘15 小时前
2026春招Flutter岗位为何变少?我看到的3个招聘逻辑变化
flutter·ai编程·客户端
念格16 小时前
Flutter 实现点击任意位置收起键盘的最佳实践
flutter
念格16 小时前
Flutter ListView Physics 滚动物理效果详解
flutter
国医中兴16 小时前
ClickHouse的数据模型设计:从理论到实践
flutter·harmonyos·鸿蒙·openharmony
国医中兴18 小时前
ClickHouse数据导入导出最佳实践:从性能到可靠性
flutter·harmonyos·鸿蒙·openharmony
国医中兴19 小时前
大数据处理的性能优化技巧:从理论到实践
flutter·harmonyos·鸿蒙·openharmony
●VON20 小时前
Flutter 入门指南:从基础组件到状态管理核心机制
前端·学习·flutter·von
西西学代码21 小时前
Flutter---SingleChildScrollView
前端·javascript·flutter