Flutter usa FFI+CustomPainter para lograr una plataforma completa de renderizado de video

Serie de renderizado de video Flutter

Capítulo 1 Android usa Texture para renderizar video
Capítulo 2 Windows usa Texture para renderizar video
Capítulo 3 Linux usa Texture para renderizar video
Capítulo 4 Plataforma completa FFI+CustomPainter para renderizar video (este capítulo)



prefacio

Los capítulos anteriores presentaron cómo flutter usa texturas para renderizar videos, pero existe el problema de que se necesita escribir un conjunto de código nativo para crear texturas en cada plataforma, lo que no es propicio para el mantenimiento del código. La mejor manera debería ser que un conjunto de códigos se pueda ejecutar en cada plataforma, por lo que se me ocurrió la idea de usar c ++ para realizar la captura de video multiplataforma, transferir los datos a la interfaz de dart a través de ffi y dibujar la imagen a través de la mando de lona Finalmente, a través de las pruebas, se encuentra que la solución disponible se combina con CustomPainter para lograr la reproducción de video.La reproducción de video realizada de esta manera puede hacer que un conjunto de códigos se ejecute en todas las plataformas (excepto la web) .


1. Cómo lograrlo

1. C/C++ captura cuadros de video

(1), escribir código C++

El reproductor es una especie de colección de videos, por ejemplo, el siguiente código es una definición simple del reproductor.
inserte la descripción de la imagen aquí
El ejemplo de ffplay.h es el siguiente

//播放回调方法原型
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*);

(2) Escribir CMakeList

cmake para cada plataforma.

  • CMakeList para Windows y Linux (parcial)
# 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  )
  • Jni CMakeList de Android (parcial)
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
                       )

2. FFI importa el método C/C++

(1), paquete dependiente

import 'dart:ffi'; // For FFI
import 'package:ffi/ffi.dart';
import 'dart:io'; // For Platform.isX

(2), cargar la biblioteca dinámica

Cargue la biblioteca dinámica de acuerdo con las diferentes plataformas, por lo general, Windows es dll y otras plataformas lo son. El nombre de la biblioteca dinámica está determinado por el CMakeList anterior.

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

(3), método de definición

Por ejemplo, el método en ffplay.h corresponde a la definición de dart de la siguiente manera:
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();

3. Isolate inicia el hilo de recolección

Dado que el mecanismo de interfaz de flutter no permite compartir datos entre subprocesos , y las variables globales son todas TLS, los subprocesos creados en C/C++ no pueden transferir directamente los datos de reproducción al subproceso principal para la representación, por lo que debe usar dart para crear un Aislamiento para C/ C++ El reproductor se ejecuta en él y los datos se envían al subproceso principal a través de sendPort.

(1), definir el método de entrada

El método de entrada es equivalente al método del subproceso secundario.
dardo principal

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

(2), Crear Aislar

Con el método de entrada, puede crear un Isolate, el ejemplo es el siguiente:
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;
  }
}

4. Dibujo de pintor personalizado

(1), dibujo personalizado

La pintura personalizada debe heredar CustomPainter e implementar el método de pintura y dibujar ui.image en el método de pintura. Esta ui.image se puede obtener transcodificando los datos argb.
dardo principal

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;
}

(2), interfaz de diseño

Use un CustomPainter personalizado en la interfaz y pase un objeto ChangeNotifier para activar el dibujo.
dardo principal

  
  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),
      ),
    );
  }

(3), dibuja el cuadro de video

Después de enviar los datos de reproducción al subproceso principal, los datos argb deben convertirse en un objeto ui.image Podemos usar directamente el método ui.decodeImageFromPixels.
dardo principal

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

2. Vista previa del efecto

Un efecto de carrera básico
inserte la descripción de la imagen aquí


3. Comparación de rendimiento

De hecho, durante el proceso de exploración, se usó el método RawImage para renderizar el video y la pantalla se mostró correctamente, pero la tasa de uso de la CPU fue muy alta, por lo que no se pudo usar para el desarrollo real. Finalmente, descubrí que el rendimiento de este método en este artículo en realidad no es muy bueno Comparado con el renderizado de texturas, todavía hay una brecha, pero se puede usar.
Plataforma de prueba: Windows 11
Equipo de prueba: i7 8750h gpu usa pantalla nuclear
Grabación de datos: tome 5 veces en 30 segundos para calcular el promedio

Este artículo rinde

video controlar el tamaño de la pantalla uso de CPU (%) uso de GPU (%)
h264 320p 30fps 320p 1.82 4.56
h264 1080p 30fps 360p 13.4 4.84
h264 1080p 30fps 1080p 13.04 15.14

Representación de textura

video controlar el tamaño de la pantalla uso de CPU (%) uso de GPU (%)
h264 320p 30fps 320p 1.28 5.06
h264 1080p 30fps 360p 4.26 12.66
h264 1080p 30fps 1080p 4.78 14.72

Se puede ver que el rendimiento del método de renderizado en este artículo sigue siendo aceptable cuando se renderizan resoluciones pequeñas. Cuando la resolución es relativamente alta, la tasa de uso de la CPU aumentará mucho y la tasa de uso de la GPU se verá afectada por el tamaño de la pantalla. del mando El método de textura tiene un mejor rendimiento y menos fluctuaciones.


4. Código completo

https://download.csdn.net/download/u013113678/87121930
Nota: El rendimiento de implementación de este artículo no es particularmente bueno, descárguelo según sus necesidades.
El proyecto flutter que contiene el código completo, las versiones 3.0.4 y 3.3.8 se están ejecutando correctamente, actualmente no incluye la implementación de ios, macos. La descripción del catálogo es la siguiente.
inserte la descripción de la imagen aquí


Resumir

De lo anterior es de lo que quiero hablar hoy. El uso de FFI+CustomPainter para realizar la renderización de video es un método explorado por el autor. El principio no es complicado, y solo se puede decir que el rendimiento es apenas utilizable, adecuado para renderizar imágenes pequeñas . . Escribirlo como un artículo y enviarlo también sirve como un nodo y continúa optimizando sobre esta base. En general, este es un buen ejemplo y una solución que vale la pena explorar.

Supongo que te gusta

Origin blog.csdn.net/u013113678/article/details/127990764
Recomendado
Clasificación