Flutter-Texture外接纹理上手实现视频播放

有关于Flutter Texture的部分的相关资料都比较少,要么就是封装视频播放器的大佬,要么就是闲鱼,声网这些团队java

先放参考文章: 万万没想到——flutter这样外接纹理android

实时渲染不是梦:经过共享内存优化Flutter外接纹理的渲染性能数组

Flutter 实时视频渲染:Texture与PlatformView markdown

Flutter视频播放封装历程网络

Android 记录在macOS上使用 NDK20 编译ffmpeg4.2.2的过程app

Android 使用 FFmpeg (二)——视屏流播放简单实现socket

我们要作的仍是视频播放,我并非非要去反复造这个轮子,它对我以后的投屏起到了绝大的帮助 现有的视频播放器在安卓端的封装是ExoPlayer,还有ijkplayer在Flutter的封装等,ExoPlayer是硬件解码的播放器async

什么是硬件解码?ide

硬件解码就是利用安卓底层已经有的解码器进行解码,它的性能较高,但兼容较低,是利用GPU进行处理的解码器(才入门音视频,任何说得不对的能够指出)函数

什么是软件解码?

软件解码则使用CPU进行解码,兼容性较高,须要在软件额外的添加解码库才能进行解码

须要的环境

咱们此次使用软件解码器来进行解码,因为使用软件解码器,咱们就须要使用解码的库,这里我使用强大的FFmpeg,编译与引入安卓部分本篇就不提了,能够参考低调大佬的帖子,这里感谢低调大佬对我提供的帮助(入门Flutter看了他挺多帖子,在群里我都不认识hhh)

首先看一下Flutter中Texture这个Widget的构造函数

const Texture({
    Key key,
    @required this.textureId,
  }) : assert(textureId != null),
       super(key: key);
复制代码

如上,它只须要一个TextureId就能构造这个widget,我看了绝大部分国内关于Texture的介绍,还一点一点研究了目前的视频播放器,获得了在安卓端建立SurfaceView的方法

我起初觉得这个Texture id会是一个hashcode,最后发现它在一个app内就是0,1,2...,每次建立+1。 因为视频播放解码等都是耗时操做,在安卓会使用到SurfaceView这样一个组件,SurfaceView是独立的线程,也就是说在它内部调用jni进行视频解码播放不会影响到其余的UI线程,SurfaceView内部会将本身View对应的Surface这个对象的实例直接经过jni直接传给native,native就能直接操做这部分的UI。

咱们先看一下安卓原生怎么来播放视频

自定义SurfaceView

public class MyVideoView extends SurfaceView {
    FFmpegNativeUtil util;
    Surface surface;
    public MyVideoView(Context context) {
        this(context,null);
    }
    public MyVideoView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }
    public MyVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init(){
        getHolder().setFormat(PixelFormat.RGBA_8888);
        surface= getHolder().getSurface();
        util=new FFmpegNativeUtil();
    }
    /** * 开始播放 * @param videoPath */
    public void startPlay(final String videoPath){
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("MyVideoView","------>>调用native方法");
                util.videoStreamPlay(videoPath,surface);
            }
        }).start();
    }
}
复制代码

FFmpegNativeUtil类

package com.example.ffmpeg;

import android.view.Surface;

public class FFmpegNativeUtil {
    static {
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avutil-55");
        System.loadLibrary("postproc-54");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
        System.loadLibrary("native-lib");
    }
    /** * 播放视频流 * @param videoPath(本地)视频文件路径 * @param surface */
    public native void videoStreamPlay(String videoPath, Surface surface);
}
复制代码

播放视频

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    MyVideoView myVideoView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myVideoView = new MyVideoView(this);
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(600, 600);
        layoutParams.gravity = Gravity.CENTER;
        addContentView(myVideoView, layoutParams);
        // Example of a call to a native method
// TextView tv = findViewById(R.id.sample_d);
    }

    public void button_click(View view) {

        myVideoView.startPlay("/storage/emulated/0/1.mp4");
    }

    /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */
}

复制代码

这里是动态添加了一个SurfaceView组件,没有用到xml,并经过一个按钮来进行调用了他的Play方法,传入了须要播放视频的本地路径。

对应的native

Java_com_example_ffmpeg_FFmpegNativeUtil_videoStreamPlay(JNIEnv *env, jobject instance,
                                                               jstring videoPath, jobject surface) {
    const char *input = env->GetStringUTFChars(videoPath, NULL);
    if (input == NULL) {
        LOGD("字符串转换失败......");
        return;
    }
    LOGD("......%s",input);
    //注册FFmpeg全部编解码器,以及相关协议。
    av_register_all();
    //分配结构体
    AVFormatContext *formatContext = avformat_alloc_context();
    //打开视频数据源。因为Android 对SDK存储权限的缘由,若是没有为当前项目赋予SDK存储权限,打开本地视频文件时会失败
    int open_state = avformat_open_input(&formatContext, input, NULL, NULL);
    if (open_state < 0) {
        char errbuf[128];
        if (av_strerror(open_state, errbuf, sizeof(errbuf)) == 0){
            LOGD("打开视频输入流信息失败,失败缘由: %s", errbuf);
        }
        return;
    }
    //为分配的AVFormatContext 结构体中填充数据
    if (avformat_find_stream_info(formatContext, NULL) < 0) {
        LOGD("读取输入的视频流信息失败。");
        return;
    }
    int video_stream_index = -1;//记录视频流所在数组下标
    LOGD("当前视频数据,包含的数据流数量:%d", formatContext->nb_streams);
    //找到"视频流".AVFormatContext 结构体中的nb_streams字段存储的就是当前视频文件中所包含的总数据流数量——
    //视频流,音频流,字幕流
    for (int i = 0; i < formatContext->nb_streams; i++) {

        //若是是数据流的编码格式为AVMEDIA_TYPE_VIDEO——视频流。
        if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_index = i;//记录视频流下标
            break;
        }
    }
    if (video_stream_index == -1) {
        LOGD("没有找到 视频流。");
        return;
    }
    //经过编解码器的id——codec_id 获取对应(视频)流解码器
    AVCodecParameters *codecParameters=formatContext->streams[video_stream_index]->codecpar;
    AVCodec *videoDecoder = avcodec_find_decoder(codecParameters->codec_id);

    if (videoDecoder == NULL) {
        LOGD("未找到对应的流解码器。");
        return;
    }
    //经过解码器分配(并用 默认值 初始化)一个解码器context
    AVCodecContext *codecContext = avcodec_alloc_context3(videoDecoder);

    if (codecContext == NULL) {
        LOGD("分配 解码器上下文失败。");
        return;
    }
    //更具指定的编码器值填充编码器上下文
    if(avcodec_parameters_to_context(codecContext,codecParameters)<0){
        LOGD("填充编解码器上下文失败。");
        return;
    }
    //经过所给的编解码器初始化编解码器上下文
    if (avcodec_open2(codecContext, videoDecoder, NULL) < 0) {
        LOGD("初始化 解码器上下文失败。");
        return;
    }
    AVPixelFormat dstFormat = AV_PIX_FMT_RGBA;
    //分配存储压缩数据的结构体对象AVPacket
    //若是是视频流,AVPacket会包含一帧的压缩数据。
    //但若是是音频则可能会包含多帧的压缩数据
    AVPacket *packet = av_packet_alloc();
    //分配解码后的每一数据信息的结构体(指针)
    AVFrame *frame = av_frame_alloc();
    //分配最终显示出来的目标帧信息的结构体(指针)
    AVFrame *outFrame = av_frame_alloc();
    uint8_t *out_buffer = (uint8_t *) av_malloc(
            (size_t) av_image_get_buffer_size(dstFormat, codecContext->width, codecContext->height,
                                              1));
    //更具指定的数据初始化/填充缓冲区
    av_image_fill_arrays(outFrame->data, outFrame->linesize, out_buffer, dstFormat,
                         codecContext->width, codecContext->height, 1);
    //初始化SwsContext
    SwsContext *swsContext = sws_getContext(
            codecContext->width   //原图片的宽
            ,codecContext->height  //源图高
            ,codecContext->pix_fmt //源图片format
            ,codecContext->width  //目标图的宽
            ,codecContext->height  //目标图的高
            ,dstFormat,SWS_BICUBIC
            , NULL, NULL, NULL
    );
    if(swsContext==NULL){
        LOGD("swsContext==NULL");
        return;
    }
    //Android 原生绘制工具
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
    //定义绘图缓冲区
    ANativeWindow_Buffer outBuffer;
    //经过设置宽高限制缓冲区中的像素数量,而非屏幕的物流显示尺寸。
    //若是缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像
    ANativeWindow_setBuffersGeometry(nativeWindow, codecContext->width, codecContext->height,
                                     WINDOW_FORMAT_RGBA_8888);
    //循环读取数据流的下一帧
    while (av_read_frame(formatContext, packet) == 0) {

        if (packet->stream_index == video_stream_index) {
            //讲原始数据发送到解码器
            int sendPacketState = avcodec_send_packet(codecContext, packet);
            if (sendPacketState == 0) {
                int receiveFrameState = avcodec_receive_frame(codecContext, frame);
                if (receiveFrameState == 0) {
                    //锁定窗口绘图界面
                    ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
                    //对输出图像进行色彩,分辨率缩放,滤波处理
                    sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0,
                              frame->height, outFrame->data, outFrame->linesize);
                    uint8_t *dst = (uint8_t *) outBuffer.bits;
                    //解码后的像素数据首地址
                    //这里因为使用的是RGBA格式,因此解码图像数据只保存在data[0]中。但若是是YUV就会有data[0]
                    //data[1],data[2]
                    uint8_t *src = outFrame->data[0];
                    //获取一行字节数
                    int oneLineByte = outBuffer.stride * 4;
                    //复制一行内存的实际数量
                    int srcStride = outFrame->linesize[0];
                    for (int i = 0; i < codecContext->height; i++) {
                        memcpy(dst + i * oneLineByte, src + i * srcStride, srcStride);
                    }
                    //解锁
                    ANativeWindow_unlockAndPost(nativeWindow);
                    //进行短暂休眠。若是休眠时间太长会致使播放的每帧画面有延迟感,若是短会有加速播放的感受。
                    //通常一每秒60帧——16毫秒一帧的时间进行休眠
                    usleep(1000 * 20);//20毫秒

                } else if (receiveFrameState == AVERROR(EAGAIN)) {
                    LOGD("从解码器-接收-数据失败:AVERROR(EAGAIN)");
                } else if (receiveFrameState == AVERROR_EOF) {
                    LOGD("从解码器-接收-数据失败:AVERROR_EOF");
                } else if (receiveFrameState == AVERROR(EINVAL)) {
                    LOGD("从解码器-接收-数据失败:AVERROR(EINVAL)");
                } else {
                    LOGD("从解码器-接收-数据失败:未知");
                }
            } else if (sendPacketState == AVERROR(EAGAIN)) {//发送数据被拒绝,必须尝试先读取数据
                LOGD("向解码器-发送-数据包失败:AVERROR(EAGAIN)");//解码器已经刷新数据可是没有新的数据包能发送给解码器
            } else if (sendPacketState == AVERROR_EOF) {
                LOGD("向解码器-发送-数据失败:AVERROR_EOF");
            } else if (sendPacketState == AVERROR(EINVAL)) {//遍解码器没有打开,或者当前是编码器,也或者须要刷新数据
                LOGD("向解码器-发送-数据失败:AVERROR(EINVAL)");
            } else if (sendPacketState == AVERROR(ENOMEM)) {//数据包没法压如解码器队列,也多是解码器解码错误
                LOGD("向解码器-发送-数据失败:AVERROR(ENOMEM)");
            } else {
                LOGD("向解码器-发送-数据失败:未知");
            }
        }
        av_packet_unref(packet);
    }
    //内存释放
    ANativeWindow_release(nativeWindow);
    av_frame_free(&outFrame);
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codecContext);
    avformat_close_input(&formatContext);
    avformat_free_context(formatContext);
    env->ReleaseStringUTFChars(videoPath, input);
}


复制代码

Flutter视频播放器在安卓端的实现

建立SurfaceView拿到Texture ID

咱们经过一个Plugin来实现建立 Android端

new MethodChannel(getFlutterView(), "VideoCall").setMethodCallHandler((call, result) -> {
    FFmpegNativeUtil util = new FFmpegNativeUtil();
    TextureRegistry textures = this.registrarFor("nightmare/video").textures();
    TextureRegistry.SurfaceTextureEntry textureEntry = textures.createSurfaceTexture();
    Surface surface = new Surface(textureEntry.surfaceTexture());
    new Thread(new Runnable() {
        @Override
        public void run() {
            Log.d("MyVideoView", "------>>调用native方法");
            util.videoStreamPlay("/storage/emulated/0/1.mp4", surface);
        }
    }).start();
    result.success(textureEntry.id());
});

复制代码

这就是一个简单的Plugin,里面的代码就只有几行,拿到Surface传给cpp native,经过result返回ID,中间开启了新的线程去对视频进行解码播放,cpp的navtive部分复用了纯安卓播放的native

Flutter端

在任何地方初始化这个对应的Plugin

MethodChannel videoPlugin = const MethodChannel("VideoCall");
复制代码
***
  @override
  void initState() {
    super.initState();
    init();
  }
  
  init() async {
    texTureId = await videoPlugin.invokeMethod("");
    setState(() {});
  }
  @override
  Widget build(BuildContext context) {
    return Texture(textureId: texTureId);
  ***
复制代码

Dart则经过调用这个Plugin拿到一个Texture ID,并刷新UI。没有作太多的细化处理,固然你本身使用的时候最好加上一些判断语句。

这样就已经实现了Flutter简单的视频播放, 看过前面参考的文章,里面有提到这是个GPU->CPU->GPU的过程消耗,将安卓的Surface数据copy到Flutter内存中并经过skia渲染了出来。 若是经过ffmpeg解码,这个时候不去绘制安卓原生的SurfaceView,直接将数据交给Flutter engine,再渲染到屏幕,应该就会解决这样的消耗,而且不会依赖平台的组件,不过Flutter没有开放内部的gl供开发者使用,闲鱼的那篇文章有这样的尝试

为何我必定要用这样的方式去实现视频播放呢?

目前的已有的视频播放器已能播放本地视频与url网络视频,ijkplayer甚至能兼容一些其余的直播流协议,但并不方便去播放一些自定义协议,如来自socket流中的视频

下面是这两天的成果

等我弄得差很少的时候,必定会开源出来

相关文章
相关标签/搜索