• Flutter 使用FFI+CustomPainter实现全平台渲染视频


    Flutter视频渲染系列

    第一章 Android使用Texture渲染视频
    第二章 Windows使用Texture渲染视频
    第三章 Linux使用Texture渲染视频
    第四章 全平台FFI+CustomPainter渲染视频(本章)
    第五章 Windows使用Native窗口渲染视频
    第六章 桌面端使用texture_rgba_renderer渲染视频



    前言

    前面几章介绍了flutter使用texture渲染视频的方法,但是有个问题就是在每个平台都需要写一套原生代码去创建texture,这样对于代码的维护是比较不利的。最好的方法应该是一套代码每个平台都能运行,于是有了一个设想,使用c++实现跨平台的视频采集,通过ffi将数据传到dart界面,通过画布控件将图像绘制出来。最终通过测试发现能用的方案就是ffi结合CustomPainter实现视频渲染,这种方式实现的视频渲染可以做到一套代码所有平台(除web外)都可运行


    一、如何实现

    1、C/C++采集视频帧

    (1)、编写C++代码

    播放器就是一种视频采集,比如下列代码是一个简单的播放器的定义。
    在这里插入图片描述
    ffplay.h示例如下

    //播放回调方法原型
    typedef void(*DisplayEventHandler)(void*play,unsigned char* data[8], int linesize[8], int width, int height, AVPixelFormat format);
    //创建播放器
    void*play_create();
    //销毁播放器
    void play_destory(void*);
    //设置渲染回调
    void play_setDisplayCallback(void*, DisplayEventHandler callback);
    //开始播放(异步)
    void play_start(void*,const char*);
    //开始播放(同步)
    void play_exec(void*, const char*);
    //停止播放
    void play_stop(void*);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (2)编写CMakeList

    每个平台的cmake。

    • Windows、Linux的CMakeList(部分)
    # Project-level configuration.
    set(PROJECT_NAME "ffplay_plugin")
    project(${PROJECT_NAME} LANGUAGES CXX)
    
    # This value is used when generating builds using this plugin, so it must
    # not be changed.
    set(PLUGIN_NAME "ffplay_plugin_plugin")
    
    # Define the plugin library target. Its name must not be changed (see comment
    # on PLUGIN_NAME above).
    #
    # Any new source files that you add to the plugin should be added here.
    add_library(${PLUGIN_NAME} SHARED
      "ffplay_plugin.cc"
    "../ffi/ffplay.cpp"
    "../ffi/DllImportUtils.cpp"
    )
    target_link_libraries(${PLUGIN_NAME} PRIVATE flutter  )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • Android的jni CMakeList(部分)
    add_library( # Sets the name of the library.
            ffplay_plugin_plugin
            # Sets the library as a shared library.
            SHARED
            # Provides a relative path to your source file(s).
            ../../../../ffi/ffplay.cpp
            ../../../../ffi/DllImportUtils.cpp
            )
    target_link_libraries( # Specifies the target library.
                           ffplay_plugin_plugin
                           # Links the target library to the log library
                           # included in the NDK.
                           ${log-lib}
                           android
                           )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2、FFI导入C/C++方法

    (1)、依赖包

    import 'dart:ffi'; // For FFI
    import 'package:ffi/ffi.dart';
    import 'dart:io'; // For Platform.isX
    
    • 1
    • 2
    • 3

    (2)、加载动态库

    根据不同的平台加载动态库,通常windows为dll其他平台为so。动态库的名称由上面的CMakeList确定。

    final DynamicLibrary nativeLib = Platform.isWindows
        ? DynamicLibrary.open("ffplay_plugin_plugin.dll")
        : DynamicLibrary.open("libffplay_plugin_plugin.so");
    
    • 1
    • 2
    • 3

    (3)、定义方法

    比如ffplay.h中的方法对应dart定义如下:
    main.dart

    //播放回调方法原型
    typedef display_callback = Void Function(Pointer<Void>, Pointer<Pointer<Uint8>>,
        Pointer<Int32>, Int32, Int32, Int32);
    //创建播放器
    final Pointer<Void> Function() play_create = nativeLib
        .lookup<NativeFunction<Pointer<Void> Function()>>('play_create')
        .asFunction();
    //销毁播放器
    final void Function(Pointer<Void>) play_destory = nativeLib
        .lookup<NativeFunction<Void Function(Pointer<Void>)>>('play_destory')
        .asFunction();
    //设置渲染回调
    final void Function(Pointer<Void>, Pointer<NativeFunction<display_callback>>)
        play_setDisplayCallback = nativeLib
            .lookup<
                    NativeFunction<
                        Void Function(Pointer<Void>,
                            Pointer<NativeFunction<display_callback>>)>>(
                'play_setDisplayCallback')
            .asFunction();
    //开始播放(异步)
    final void Function(Pointer<Void>, Pointer<Int8>) play_start = nativeLib
        .lookup<NativeFunction<Void Function(Pointer<Void>, Pointer<Int8>)>>(
            'play_start')
        .asFunction();
    //开始播放(同步)
    final void Function(Pointer<Void>, Pointer<Int8>) play_exec = nativeLib
        .lookup<NativeFunction<Void Function(Pointer<Void>, Pointer<Int8>)>>(
            'play_exec')
        .asFunction();
    //停止播放
    final void Function(Pointer<Void>) play_stop = nativeLib
        .lookup<NativeFunction<Void Function(Pointer<Void>)>>('play_stop')
        .asFunction();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3、Isolate开启采集线程

    由于flutter的界面机制是不允许线程间数据共享,而且全局变量都是TLS,在C/C++中创建的线程无法将播放数据直接传给主线程渲染,所以需要使用dart创建一个Isolate让C/C++的播放器跑在上面,数据通过sendPort发送给主线程。

    (1)、定义入口方法

    入口方法相当于子线程方法。
    main.dart

    //Isolate通信端口
    SendPort? m_sendPort;
    //Isolate入口方法
      static isolateEntry(SendPort sendPort) async {
        //记录sendPort
        m_sendPort = sendPort;
        //播放逻辑,此处需要堵塞,简单点可以在播放逻辑中堵塞,也可以放一个C/C++消息队列给多路流线程通信做调度。
        //比如采用播放逻辑阻塞实现,阻塞后在渲染回调方法中使用sendPort将视频数据发送到主线程,回调必须在此线程中。
         
        //发送消息通知结束播放
        sendPort?.send([1]);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    (2)、创建Isolate

    有了入口方法就可以创建一个Isolate了,示例如下:
    main.dart

      startPlay() async {
        ReceivePort receivePort = ReceivePort();
        //创建一个Isolate相当于创建一个子线程
        await Isolate.spawn(isolateEntry, receivePort.sendPort);
        // 监听Isolate子线程消息port
        await for (var msg in receivePort) {
          //处理Isolate子线程发过来的视频数据
          
          int type=msg[0];
          if(type==1)
          //结束播放
            break;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4、CustomPainter绘制

    (1)、自定义绘制

    自定义绘制需要继承CustomPainter并实现paint方法,在paint方法中绘制ui.image。这个ui.image可以由argb数据转码得到。
    main.dart

    import 'dart:ui' as ui;
    //渲染的image
    ui.Image? image;
    //通知控件绘制
    ChangeNotifier notifier = ChangeNotifier();
    //自定义panter
    class MyCustomPainter extends CustomPainter {
      //触发绘制的标识
      ChangeNotifier flag;
      MyCustomPainter(this.flag) : super(repaint: flag);
      
      void paint(Canvas canvas, ui.Size size) {
        //绘制image
        if (image != null) canvas.drawImage(image!, Offset(0, 0), Paint());
      }
      
      bool shouldRepaint(MyCustomPainter oldDelegate) => true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    (2)、布局界面

    在界面中使用自定义的CustomPainter,并传入ChangeNotifier对象用于触发绘制。
    main.dart

      
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          //控件布局
          body: Center(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  width: 640,
                  height: 360,
                  child: Center(
                    child: CustomPaint(
                      foregroundPainter: MyCustomPainter(notifier),
                      child: Container(
                        width: 640,
                        height: 360,
                        color: Color(0x5a00C800),
                      ),
                    ),
                  ),
                )
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: onClick,
            tooltip: 'play or stop',
            child: Icon(Icons.add),
          ),
        );
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    (3)、绘制视频帧

    当播放数据发送到主线程后,需要将argb数据转换成ui.image对象,我们直接使用 ui.decodeImageFromPixels方法即可。
    main.dart

     ui.decodeImageFromPixels(pixels, width, height, PixelFormat.rgba8888,
                (result) {
              image = result;
              //通知绘制
              notifier.notifyListeners();
            }, rowBytes: linesize, targetWidth: 640, targetHeight: 360);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    二、效果预览

    基本的一个运行效果
    在这里插入图片描述


    三、性能对比

    其实在摸索过程中采用过RawImage的方式渲染视频,成功显示画面但是cpu占用率非常高,不能用于实际开发。最后找到本文的这种方法其实性能也不是很好,相对于Texture渲染还是有一些差距,但是也算是能够使用了。
    测试平台:Windows 11
    测试设备:i7 8750h gpu使用核显
    数据记录:30秒内取5次值计算均值

    本文渲染

    视频控件显示大小cpu使用率(%)gpu使用率(%)
    h264 320p 30fps320p1.824.56
    h264 1080p 30fps360p13.44.84
    h264 1080p 30fps1080p13.0415.14

    Texture渲染

    视频控件显示大小cpu使用率(%)gpu使用率(%)
    h264 320p 30fps320p1.285.06
    h264 1080p 30fps360p4.2612.66
    h264 1080p 30fps1080p4.7814.72

    可以看出本文的渲染方法在渲染小分辨率时性能还是可以接受,分辨率比较高时cpu使用率会上升很多,gpu使用率会受控件显示大小影响。 texture的方式则性能好一些且波动较小。


    四、完整代码

    https://download.csdn.net/download/u013113678/87121930
    注:本文的实现性能不算特别好,请根据需求下载。
    包含完整代码的flutter项目,版本3.0.4、3.3.8都成功运行,目前不包含ios、macos实现。目录说明如下。
    在这里插入图片描述


    总结

    以上就是今天要讲述的内容,使用FFI+CustomPainter实现视频渲染是一种笔者探索出来的方法,原理并不复杂,而且性能也只能说勉强能用,适合渲染小画面。编写成文章发出来,也是为了作为一个节点,在这基础上能够继续优化。总的来说,这是一个不错的示例也是一个值得继续探索的方案。

  • 相关阅读:
    【面试经典150题】除自身以外数组的乘积 JavaScript
    【Web】get请求和post请求的区别
    transformer库bert的使用(pytorch)链接
    数据结构初阶--堆排序+TOPK问题
    【Qt之JSON文件】QJsonDocument、QJsonObject、QJsonArray等类介绍及使用
    Kafka To HBase To Hive
    第5集丨理学对佛、道的复制
    neo4j入门并使用案例说明
    UE4.27 Ubuntu 像素流环境搭建
    怎样减少报表开发中的存储过程
  • 原文地址:https://blog.csdn.net/u013113678/article/details/127990764