Flutter ビデオ レンダリング シリーズ
第 1 章 Android は Texture を使用してビデオをレンダリングする
第 2 章 Windows は Texture を使用してビデオをレンダリングする
第 3 章 Linux は Texture を使用してビデオをレンダリングする
第 4 章 フルプラットフォーム FFI+CustomPainter を使用してビデオをレンダリングする (この章)
序文
これまでの章では、フラッターがテクスチャを使用してビデオをレンダリングする方法を紹介しましたが、プラットフォームごとにテクスチャを作成するための一連のネイティブ コードを記述する必要があり、コードのメンテナンスに役立たないという問題があります。最善の方法は、一連のコードをすべてのプラットフォームで実行できるようにすることです。そのため、C++ を使用してクロスプラットフォームのビデオ キャプチャを実現し、ffi を介してデータを dart インターフェイスに転送し、dart インターフェイスを介して画像を描画するというアイデアを思いつきました。キャンバス コントロール。最後に、テストを通じて、利用可能なソリューションは、CustomPainter と組み合わせてビデオ レンダリングを実現することであることがわかりました.この方法で実現されたビデオ レンダリングは、すべてのプラットフォーム (Web を除く) で一連のコードを実行できます.
1. 達成方法
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*);
(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 )
- 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
)
2. FFI インポート C/C++ メソッド
(1)、依存パッケージ
import 'dart:ffi'; // For FFI
import 'package:ffi/ffi.dart';
import 'dart:io'; // For Platform.isX
(2)、動的ライブラリをロードします
さまざまなプラットフォームに従って動的ライブラリをロードします。通常、windows は dll で、他のプラットフォームはそうです。動的ライブラリの名前は、上記の CMakeList によって決定されます。
final DynamicLibrary nativeLib = Platform.isWindows
? DynamicLibrary.open("ffplay_plugin_plugin.dll")
: DynamicLibrary.open("libffplay_plugin_plugin.so");
(3)、定義方法
たとえば、ffplay.h のメソッドは、次のように 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 は収集スレッドを開始します
flutter のインターフェイス メカニズムはスレッド間のデータ共有を許可せず、グローバル変数はすべて TLS であるため、C/C++ で作成されたスレッドは再生データをメイン スレッドに直接転送してレンダリングすることはできないため、dart を使用して C/ 用の Isolate を作成する必要があります。 C++ プレーヤーはその上で実行され、データは sendPort を介してメイン スレッドに送信されます。
(1)、入力方法を定義する
entry メソッドは、子スレッド メソッドと同等です。
main.dart
//Isolate通信端口
SendPort? m_sendPort;
//Isolate入口方法
static isolateEntry(SendPort sendPort) async {
//记录sendPort
m_sendPort = sendPort;
//播放逻辑,此处需要堵塞,简单点可以在播放逻辑中堵塞,也可以放一个C/C++消息队列给多路流线程通信做调度。
//比如采用播放逻辑阻塞实现,阻塞后在渲染回调方法中使用sendPort将视频数据发送到主线程,回调必须在此线程中。
//发送消息通知结束播放
sendPort?.send([1]);
}
(2)、アイソレートを作成
entry メソッドを使用すると、Isolate を作成できます。例は次のとおりです
。
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.カスタムペインターの描画
(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;
}
(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),
),
);
}
(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);
2.効果プレビュー
基本的なランニング効果
3.性能比較
実際、探索過程でRawImageメソッドを使って動画をレンダリングしたところ、画面表示には成功しましたが、CPU使用率が非常に高く、実際の開発には使えませんでした。最後に、この記事のこの方法のパフォーマンスは実際にはあまり良くないことがわかりました. テクスチャ レンダリングと比較すると、まだ多少のギャップがありますが、使用できます.
テストプラットフォーム: 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 |
テクスチャ レンダリング
ビデオ | コントロール表示サイズ | 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 使用率はディスプレイ サイズの影響を受けます.コントロールの。テクスチャ方式は、パフォーマンスが向上し、変動が少なくなります。
4. 完全なコード
https://download.csdn.net/download/u013113678/87121930
注: この記事の実装パフォーマンスは特に優れているわけではありません。必要に応じてダウンロードしてください。
完全なコードを含む flutter プロジェクト、バージョン 3.0.4 および 3.3.8 は正常に実行されています。現在、ios、macos の実装は含まれていません。カタログの説明は以下の通りです。
要約する
以上が今日お話ししたいことです. FFI+CustomPainter を使って動画描画を実現するのは筆者が模索した方法です. 原理は複雑ではなく, 性能はギリギリとしか言いようがありません. 小さな画像の描画に適しています. . 記事として書いて発信することも、ノードとして機能し、これをベースに最適化し続けることです。全体として、これは良い例であり、検討する価値のあるソリューションです。