Flutter-Texture外接纹理上实现视频播放

有关于 Flutter Texture 的部分的相关资料都比较少,要么就是封装视频播放器的大佬,要么就是闲鱼,声网这些团队

先放参考文章:
万万没想到——flutter 这样外接纹理

实时渲染不是梦:通过共享内存优化 Flutter 外接纹理的渲染性能

Flutter  实时视频渲染:Texture 与 PlatformView 

Flutter 视频播放封装历程

Android 记录在 macOS 上使用 NDK20 编译 ffmpeg4.2.2 的过程

Android 使用 FFmpeg (二)——视屏流播放简单实现

咱们要做的还是视频播放,我并不是非要去反复造这个轮子,它对我之后的投屏起到了绝大的帮助
现有的视频播放器在安卓端的封装是 ExoPlayer,还有 ijkplayer 在 Flutter 的封装等,ExoPlayer 是硬件解码的播放器

什么是硬件解码?

硬件解码就是利用安卓底层已经有的解码器进行解码,它的性能较高,但兼容较低,是利用 GPU 进行处理的解码器(才入门音视频,任何说得不对的可以指出)

什么是软件解码?

软件解码则使用 CPU 进行解码,兼容性较高,需要在软件额外的添加解码库才能进行解码

需要的环境

我们这次使用软件解码器来进行解码,由于使用软件解码器,我们就需要使用解码的库,这里我使用强大的 FFmpeg,编译与引入安卓部分本篇就不提了,可以参考低调大佬的帖子,这里感谢低调大佬对我提供的帮助(入门 Flutter 看了他挺多帖子,在群里我都不认识 hhh)

首先看一下 Flutter 中 Texture 这个 Widget 的构造函数

1
2
3
4
5
const Texture({
Key key,
@required this.textureId,
}) : assert(textureId != null),
super(key: key);

如上,它只需要一个 TextureId 就能构造这个 widget,我看了绝大部分国内关于 Texture 的介绍,还一点一点研究了目前的视频播放器,得到了在安卓端创建 SurfaceView 的方法

我起初以为这个 Texture id 会是一个 hashcode,最后发现它在一个 app 内就是 0,1,2…,每次创建+1。
由于视频播放解码等都是耗时操作,在安卓会使用到 SurfaceView 这样一个组件,SurfaceView 是独立的线程,也就是说在它内部调用 jni 进行视频解码播放不会影响到其他的 UI 线程,SurfaceView 内部会将自己 View 对应的 Surface 这个对象的实例直接通过 jni 直接传给 native,native 就能直接操作这部分的 UI。

我们先看一下安卓原生怎么来播放视频

自定义 SurfaceView

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
public class MyVideoView extends SurfaceView {
FFmpegNativeUtil util;
Surface surface;
public MyVideoView(Context context) {
this(context,null);
}
public MyVideoView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
getHolder().setFormat(PixelFormat.RGBA_8888);
surface= getHolder().getSurface();
util=new FFmpegNativeUtil();
}
/**
* 开始播放
* @param videoPath
*/
public void startPlay(final String videoPath){
new Thread(new Runnable() {
@Override
public void run() {
Log.d("MyVideoView","------>>调用native方法");
util.videoStreamPlay(videoPath,surface);
}
}).start();
}
}

FFmpegNativeUtil 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.ffmpeg;

import android.view.Surface;

public class FFmpegNativeUtil {
static {
System.loadLibrary("avcodec-57");
System.loadLibrary("avdevice-57");
System.loadLibrary("avfilter-6");
System.loadLibrary("avformat-57");
System.loadLibrary("avutil-55");
System.loadLibrary("postproc-54");
System.loadLibrary("swresample-2");
System.loadLibrary("swscale-4");
System.loadLibrary("native-lib");
}
/**
* 播放视频流
* @param videoPath(本地)视频文件路径
* @param surface
*/
public native void videoStreamPlay(String videoPath, Surface surface);
}

播放视频

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
public class MainActivity extends AppCompatActivity {

// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}

MyVideoView myVideoView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

myVideoView = new MyVideoView(this);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(600, 600);
layoutParams.gravity = Gravity.CENTER;
addContentView(myVideoView, layoutParams);
// Example of a call to a native method
// TextView tv = findViewById(R.id.sample_d);
}

public void button_click(View view) {

myVideoView.startPlay("/storage/emulated/0/1.mp4");
}

/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
}

这里是动态添加了一个 SurfaceView 组件,没有用到 xml,并通过一个按钮来进行调用了他的 Play 方法,传入了需要播放视频的本地路径。

对应的 native

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
Java_com_example_ffmpeg_FFmpegNativeUtil_videoStreamPlay(JNIEnv *env, jobject instance,
jstring videoPath, jobject surface) {
const char *input = env->GetStringUTFChars(videoPath, NULL);
if (input == NULL) {
LOGD("字符串转换失败......");
return;
}
LOGD("......%s",input);
//注册FFmpeg所有编解码器,以及相关协议。
av_register_all();
//分配结构体
AVFormatContext *formatContext = avformat_alloc_context();
//打开视频数据源。由于Android 对SDK存储权限的原因,如果没有为当前项目赋予SDK存储权限,打开本地视频文件时会失败
int open_state = avformat_open_input(&formatContext, input, NULL, NULL);
if (open_state < 0) {
char errbuf[128];
if (av_strerror(open_state, errbuf, sizeof(errbuf)) == 0){
LOGD("打开视频输入流信息失败,失败原因: %s", errbuf);
}
return;
}
//为分配的AVFormatContext 结构体中填充数据
if (avformat_find_stream_info(formatContext, NULL) < 0) {
LOGD("读取输入的视频流信息失败。");
return;
}
int video_stream_index = -1;//记录视频流所在数组下标
LOGD("当前视频数据,包含的数据流数量:%d", formatContext->nb_streams);
//找到"视频流".AVFormatContext 结构体中的nb_streams字段存储的就是当前视频文件中所包含的总数据流数量——
//视频流,音频流,字幕流
for (int i = 0; i < formatContext->nb_streams; i++) {

//如果是数据流的编码格式为AVMEDIA_TYPE_VIDEO——视频流。
if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;//记录视频流下标
break;
}
}
if (video_stream_index == -1) {
LOGD("没有找到 视频流。");
return;
}
//通过编解码器的id——codec_id 获取对应(视频)流解码器
AVCodecParameters *codecParameters=formatContext->streams[video_stream_index]->codecpar;
AVCodec *videoDecoder = avcodec_find_decoder(codecParameters->codec_id);

if (videoDecoder == NULL) {
LOGD("未找到对应的流解码器。");
return;
}
//通过解码器分配(并用 默认值 初始化)一个解码器context
AVCodecContext *codecContext = avcodec_alloc_context3(videoDecoder);

if (codecContext == NULL) {
LOGD("分配 解码器上下文失败。");
return;
}
//更具指定的编码器值填充编码器上下文
if(avcodec_parameters_to_context(codecContext,codecParameters)<0){
LOGD("填充编解码器上下文失败。");
return;
}
//通过所给的编解码器初始化编解码器上下文
if (avcodec_open2(codecContext, videoDecoder, NULL) < 0) {
LOGD("初始化 解码器上下文失败。");
return;
}
AVPixelFormat dstFormat = AV_PIX_FMT_RGBA;
//分配存储压缩数据的结构体对象AVPacket
//如果是视频流,AVPacket会包含一帧的压缩数据。
//但如果是音频则可能会包含多帧的压缩数据
AVPacket *packet = av_packet_alloc();
//分配解码后的每一数据信息的结构体(指针)
AVFrame *frame = av_frame_alloc();
//分配最终显示出来的目标帧信息的结构体(指针)
AVFrame *outFrame = av_frame_alloc();
uint8_t *out_buffer = (uint8_t *) av_malloc(
(size_t) av_image_get_buffer_size(dstFormat, codecContext->width, codecContext->height,
1));
//更具指定的数据初始化/填充缓冲区
av_image_fill_arrays(outFrame->data, outFrame->linesize, out_buffer, dstFormat,
codecContext->width, codecContext->height, 1);
//初始化SwsContext
SwsContext *swsContext = sws_getContext(
codecContext->width //原图片的宽
,codecContext->height //源图高
,codecContext->pix_fmt //源图片format
,codecContext->width //目标图的宽
,codecContext->height //目标图的高
,dstFormat,SWS_BICUBIC
, NULL, NULL, NULL
);
if(swsContext==NULL){
LOGD("swsContext==NULL");
return;
}
//Android 原生绘制工具
ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
//定义绘图缓冲区
ANativeWindow_Buffer outBuffer;
//通过设置宽高限制缓冲区中的像素数量,而非屏幕的物流显示尺寸。
//如果缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像
ANativeWindow_setBuffersGeometry(nativeWindow, codecContext->width, codecContext->height,
WINDOW_FORMAT_RGBA_8888);
//循环读取数据流的下一帧
while (av_read_frame(formatContext, packet) == 0) {

if (packet->stream_index == video_stream_index) {
//讲原始数据发送到解码器
int sendPacketState = avcodec_send_packet(codecContext, packet);
if (sendPacketState == 0) {
int receiveFrameState = avcodec_receive_frame(codecContext, frame);
if (receiveFrameState == 0) {
//锁定窗口绘图界面
ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
//对输出图像进行色彩,分辨率缩放,滤波处理
sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0,
frame->height, outFrame->data, outFrame->linesize);
uint8_t *dst = (uint8_t *) outBuffer.bits;
//解码后的像素数据首地址
//这里由于使用的是RGBA格式,所以解码图像数据只保存在data[0]中。但如果是YUV就会有data[0]
//data[1],data[2]
uint8_t *src = outFrame->data[0];
//获取一行字节数
int oneLineByte = outBuffer.stride * 4;
//复制一行内存的实际数量
int srcStride = outFrame->linesize[0];
for (int i = 0; i < codecContext->height; i++) {
memcpy(dst + i * oneLineByte, src + i * srcStride, srcStride);
}
//解锁
ANativeWindow_unlockAndPost(nativeWindow);
//进行短暂休眠。如果休眠时间太长会导致播放的每帧画面有延迟感,如果短会有加速播放的感觉。
//一般一每秒60帧——16毫秒一帧的时间进行休眠
usleep(1000 * 20);//20毫秒

} else if (receiveFrameState == AVERROR(EAGAIN)) {
LOGD("从解码器-接收-数据失败:AVERROR(EAGAIN)");
} else if (receiveFrameState == AVERROR_EOF) {
LOGD("从解码器-接收-数据失败:AVERROR_EOF");
} else if (receiveFrameState == AVERROR(EINVAL)) {
LOGD("从解码器-接收-数据失败:AVERROR(EINVAL)");
} else {
LOGD("从解码器-接收-数据失败:未知");
}
} else if (sendPacketState == AVERROR(EAGAIN)) {//发送数据被拒绝,必须尝试先读取数据
LOGD("向解码器-发送-数据包失败:AVERROR(EAGAIN)");//解码器已经刷新数据但是没有新的数据包能发送给解码器
} else if (sendPacketState == AVERROR_EOF) {
LOGD("向解码器-发送-数据失败:AVERROR_EOF");
} else if (sendPacketState == AVERROR(EINVAL)) {//遍解码器没有打开,或者当前是编码器,也或者需要刷新数据
LOGD("向解码器-发送-数据失败:AVERROR(EINVAL)");
} else if (sendPacketState == AVERROR(ENOMEM)) {//数据包无法压如解码器队列,也可能是解码器解码错误
LOGD("向解码器-发送-数据失败:AVERROR(ENOMEM)");
} else {
LOGD("向解码器-发送-数据失败:未知");
}
}
av_packet_unref(packet);
}
//内存释放
ANativeWindow_release(nativeWindow);
av_frame_free(&outFrame);
av_frame_free(&frame);
av_packet_free(&packet);
avcodec_free_context(&codecContext);
avformat_close_input(&formatContext);
avformat_free_context(formatContext);
env->ReleaseStringUTFChars(videoPath, input);
}


Flutter 视频播放器在安卓端的实现

创建 SurfaceView 拿到 Texture ID

我们通过一个 Plugin 来实现创建
Android 端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new MethodChannel(getFlutterView(), "VideoCall").setMethodCallHandler((call, result) -> {
FFmpegNativeUtil util = new FFmpegNativeUtil();
TextureRegistry textures = this.registrarFor("nightmare/video").textures();
TextureRegistry.SurfaceTextureEntry textureEntry = textures.createSurfaceTexture();
Surface surface = new Surface(textureEntry.surfaceTexture());
new Thread(new Runnable() {
@Override
public void run() {
Log.d("MyVideoView", "------>>调用native方法");
util.videoStreamPlay("/storage/emulated/0/1.mp4", surface);
}
}).start();
result.success(textureEntry.id());
});

这就是一个简单的 Plugin,里面的代码就只有几行,拿到 Surface 传给 cpp native,通过 result 返回 ID,中间开启了新的线程去对视频进行解码播放,cpp 的 navtive 部分复用了纯安卓播放的 native

Flutter 端

在任何地方初始化这个对应的 Plugin

1
MethodChannel videoPlugin = const MethodChannel("VideoCall");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
***
@override
void initState() {
super.initState();
init();
}

init() async {
texTureId = await videoPlugin.invokeMethod("");
setState(() {});
}
@override
Widget build(BuildContext context) {
return Texture(textureId: texTureId);
***

Dart 则通过调用这个 Plugin 拿到一个 Texture ID,并刷新 UI。没有做太多的细化处理,当然你自己使用的时候最好加上一些判断语句。

这样就已经实现了 Flutter 简单的视频播放,
看过前面参考的文章,里面有提到这是个 GPU->CPU->GPU 的过程消耗,将安卓的 Surface 数据 copy 到 Flutter 内存中并通过 skia 渲染了出来。
如果通过 ffmpeg 解码,这个时候不去绘制安卓原生的 SurfaceView,直接将数据交给 Flutter engine,再渲染到屏幕,应该就会解决这样的消耗,并且不会依赖平台的组件,不过 Flutter 没有开放内部的 gl 供开发者使用,闲鱼的那篇文章有这样的尝试

为什么我一定要用这样的方式去实现视频播放呢?

目前的已有的视频播放器已能播放本地视频与 url 网络视频,ijkplayer 甚至能兼容一些其他的直播流协议,但并不方便去播放一些自定义协议,如来自 socket 流中的视频

下面是这两天的成果

等我弄得差不多的时候,一定会开源出来

作者

梦魇兽

发布于

2020-03-23

更新于

2023-03-11

许可协议

评论