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流中的視頻

下面是這兩天的成果

等我弄得差很少的時候,必定會開源出來

相關文章
相關標籤/搜索