第一章 Android使用Texture渲染视频
第二章 Windows使用Texture渲染视频
第三章 Linux使用Texture渲染视频
第四章 全平台FFI+CustomPainter渲染视频(本章)
前面几章介绍了flutter使用texture渲染视频的方法,但是有个问题就是在每个平台都需要写一套原生代码去创建texture,这样对于代码的维护是比较不利的。最好的方法应该是一套代码每个平台都能运行,于是有了一个设想,使用c++实现跨平台的视频采集,通过ffi将数据传到dart界面,通过画布控件将图像绘制出来。最终通过测试发现能用的方案就是ffi结合CustomPainter实现视频渲染,这种方式实现的视频渲染可以做到一套代码所有平台(除web外)都可运行。
播放器就是一种视频采集,比如下列代码是一个简单的播放器的定义。
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*);
每个平台的cmake。
# 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 )
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK )
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)
import 'dart:ffi'; // For FFI
import 'package:ffi/ffi.dart';
import 'dart:io'; // For Platform.isX
根据不同的平台加载动态库,通常windows为dll其他平台为so。动态库的名称由上面的CMakeList确定。
final DynamicLibrary nativeLib = Platform.isWindows? DynamicLibrary.open("ffplay_plugin_plugin.dll"): DynamicLibrary.open("libffplay_plugin_plugin.so");
比如ffplay.h中的方法对应dart定义如下:
main.dart
//播放回调方法原型
typedef display_callback = Void Function(Pointer, Pointer>,Pointer, Int32, Int32, Int32);
//创建播放器
final Pointer Function() play_create = nativeLib.lookup Function()>>('play_create').asFunction();
//销毁播放器
final void Function(Pointer) play_destory = nativeLib.lookup)>>('play_destory').asFunction();
//设置渲染回调
final void Function(Pointer, Pointer>)play_setDisplayCallback = nativeLib.lookup,Pointer>)>>('play_setDisplayCallback').asFunction();
//开始播放(异步)
final void Function(Pointer, Pointer) play_start = nativeLib.lookup, Pointer)>>('play_start').asFunction();
//开始播放(同步)
final void Function(Pointer, Pointer) play_exec = nativeLib.lookup, Pointer)>>('play_exec').asFunction();
//停止播放
final void Function(Pointer) play_stop = nativeLib.lookup)>>('play_stop').asFunction();
由于flutter的界面机制是不允许线程间数据共享,而且全局变量都是TLS,在C/C++中创建的线程无法将播放数据直接传给主线程渲染,所以需要使用dart创建一个Isolate让C/C++的播放器跑在上面,数据通过sendPort发送给主线程。
入口方法相当于子线程方法。
main.dart
//Isolate通信端口
SendPort? m_sendPort;
//Isolate入口方法static isolateEntry(SendPort sendPort) async {//记录sendPortm_sendPort = sendPort;//播放逻辑,此处需要堵塞,简单点可以在播放逻辑中堵塞,也可以放一个C/C++消息队列给多路流线程通信做调度。//比如采用播放逻辑阻塞实现,阻塞后在渲染回调方法中使用sendPort将视频数据发送到主线程,回调必须在此线程中。//发送消息通知结束播放sendPort?.send([1]);}
有了入口方法就可以创建一个Isolate了,示例如下:
main.dart
startPlay() async {ReceivePort receivePort = ReceivePort();//创建一个Isolate相当于创建一个子线程await Isolate.spawn(isolateEntry, receivePort.sendPort);// 监听Isolate子线程消息portawait for (var msg in receivePort) {//处理Isolate子线程发过来的视频数据int type=msg[0];if(type==1)//结束播放break;}
}
自定义绘制需要继承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);@overridevoid paint(Canvas canvas, ui.Size size) {//绘制imageif (image != null) canvas.drawImage(image!, Offset(0, 0), Paint());}@overridebool shouldRepaint(MyCustomPainter oldDelegate) => true;
}
在界面中使用自定义的CustomPainter,并传入ChangeNotifier对象用于触发绘制。
main.dart
@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title),),//控件布局body: Center(child: Row(mainAxisAlignment: MainAxisAlignment.center,children: [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),),);}
当播放数据发送到主线程后,需要将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);
基本的一个运行效果
其实在摸索过程中采用过RawImage的方式渲染视频,成功显示画面但是cpu占用率非常高,不能用于实际开发。最后找到本文的这种方法其实性能也不是很好,相对于Texture渲染还是有一些差距,但是也算是能够使用了。
测试平台:Windows 11
测试设备:i7 8750h gpu使用核显
数据记录:30秒内取5次值计算均值
本文渲染
视频 | 控件显示大小 | cpu使用率(%) | 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 |
Texture渲染
视频 | 控件显示大小 | cpu使用率(%) | 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 |
可以看出本文的渲染方法在渲染小分辨率时性能还是可以接受,分辨率比较高时cpu使用率会上升很多,gpu使用率会受控件显示大小影响。 texture的方式则性能好一些且波动较小。
https://download.csdn.net/download/u013113678/87121930
注:本文的实现性能不算特别好,请根据需求下载。
包含完整代码的flutter项目,版本3.0.4、3.3.8都成功运行,目前不包含ios、macos实现。目录说明如下。
以上就是今天要讲述的内容,使用FFI+CustomPainter实现视频渲染是一种笔者探索出来的方法,原理并不复杂,而且性能也只能说勉强能用,适合渲染小画面。编写成文章发出来,也是为了作为一个节点,在这基础上能够继续优化。总的来说,这是一个不错的示例也是一个值得继续探索的方案。