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)
Directorio de artículos
- Serie de renderizado de video Flutter
- prefacio
- 1. Cómo lograrlo
- 2. Vista previa del efecto
- 3. Comparación de rendimiento
- 4. Código completo
- Resumir
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.
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
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.
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.