题记:通过上一篇可以得到解码数据,本章介绍如何展示这些数据——即图像渲染与声音播放,可以结合音视频Demo或ffplay进行理解。
数据播放
解码后的数据一般有三类,字幕、音频和视频数据。本章分别介绍播放流程,字幕、图像渲染与声音播放。
字幕
字幕流的处理前文介绍的比较少,因为字幕的处理流程相对独立,并且影响不大。字幕分为软字幕与硬字幕。硬字幕数据已经与视频数据融合,成为视频图像的一部分,不需要额外处理。这里讨论的软字幕,指字幕数据与视频数据分开存储,播放时根据需要动态叠加到视频上。
字幕介绍
打开一个ASS文件(下载tears_of_steel中文字幕),可以使用文本打开,可以看到如下内容(包含显示时间和文字)。
由于没有找到合适的包含字幕流资源(大多数是硬字幕),自己使用FFmpeg命令合成了一个tears_of_steel_with_subtitles.mp4文件:
ffmpeg -i tears_of_steel.mov -i TearsofStee.2012.Chs.ass -c:v copy -c:a copy -c:s mov_text tears_of_steel_with_subtitles.mp4
解码&播放
对于字幕的解码,解码相对简单,不需要视频和音频send、receive流程。只需要使用函数:
avcodec_decode_subtitle2
解码字幕数据包(AVPacket
),并将解码结果存储在AVSubtitle
结构中avctx
: 解码器上下文信息。sub
: 指向AVSubtitle
结构的指针,用于存储解码后的字幕数据。got_sub_ptr
: 用于指示是否成功解码出字幕。解码成功,该值会被设置为非零。avpkt
: 指向AVPacket
的指针
AVSubtitle
就是解码后的数据,为了统一处理ffplay把它封装到了Frame
中,统一放入了队列。主要结构如图
typedef struct AVSubtitle {
uint16_t format; // 字幕格式类型,
uint64_t pts; // 显示时间戳(以 AV_TIME_BASE 为单位)
uint64_t end_display_time; // 字幕显示的结束时间(以毫秒为单位)
unsigned int num_rects; // 字幕区域的数量
AVSubtitleRect **rects; // 指向字幕区域的指针数组
int64_t start_display_time; // 字幕显示的起始时间(以毫秒为单位)
} AVSubtitle;
其中AVSubtitleRect
的结构如下
typedef struct AVSubtitleRect {
int x, y, w, h; // 字幕区域的(仅用于位图字幕)
AVPicture pict; // 位图数据(仅用于位图字幕)
enum AVSubtitleType type; // 字幕类型
char *text; // 文本字幕内容(仅用于文本字幕)
char *ass; // ASS 格式字幕内容(仅用于 ASS 字幕)
} AVSubtitleRect;
由于合成时使用的是ASS结构,程序中的截图如下
这些字幕信息保存在
FrameQueue
中,在刷新视频时,ffplay通过video_image_display
叠加到视频帧上,具体会根据 format
判断字幕的具体格式(文本、位图或ASS),文本通过SDL
或Android 的 Canvas
或 iOS 的 CoreText
渲染到视频帧上,位图通过位图数据叠加到视频帧上。Demo中暂未处理字幕部分。
音频
音视频解码后数据被封装在AVFrame中,iOS原生播放可以参考iOS端PCM播放,要实现跨平台播放一般需要集成SDL。
SDL集成
iOS可以在Podfile上添加
pod 'SDL2', '3.2.0'
或者到SDL官网下载,集成到iOS或android上。如果遇到SDL_main
冲突问题,记得在引用头文件前定义一下#define SDL_MAIN_HANDLED
SDL播放相关函数:
SDL_SetMainReady
不需要标准main()
函数作为程序入口点的环境中,移动端集成需要。SDL_Init
&SDL_Quit
初始化SDL音频子系统 & 清理SDL- 打开音频设备
SDL_OpenAudio
/SDL_OpenAudioDevice
- 暂停/播放
SDL_PauseAudio
/SDL_PauseAudioDevice
1暂停 0播放 - 关闭音频设备
SDL_CloseAudio
/SDL_CloseAudioDevice
其中打开音频设备SDL_OpenAudio
中,需要传递设备参数SDL_AudioSpec
,这里需要设置合适的设备参数,如下(实际上ffplay或Demo
中SDL_AudioSpec wanted_spec, spec
代码段根据设备选择)
SDL_memset(&desired_spec, 0, sizeof(desired_spec));
desired_spec.freq = 44100; // 采样率(例如 44100 Hz)
desired_spec.format = AUDIO_S16SYS; // 采样格式(例如 16 位有符号整数)
desired_spec.channels = 2; // 声道数(例如立体声)
desired_spec.samples = 1024; // 音频缓冲区大小(样本数)
desired_spec.callback = audio_callback; // 音频回调函数
desired_spec.userdata = NULL; // 用户数据
- 参数中
callback
和userdata
就是填充数据的回调,callback参数userdata
C风格的传递参数形式stream
音频缓冲区,需要将PCM数据填充到该缓冲区。len
音频缓冲区的长度(字节数)
音频播放通过这个回调,将需要播放的数据填充到stream
中,就可以持续播放视频了。这里可以看到有个len
的参数,也就是需要填充的区域是有要求的长度,使得需要保存一个Cache
的逻辑。先来看看如何获取数据吧。
播放数据
解码后的数据放在FrameQueue中,先来看一下AVFrame的音频部分。
- 音频
AVFrame
中,相关主要字段:format
音频采样格式,表示存储方式和位深度,'P'在这里代表PlanarAV_SAMPLE_FMT_FLT
: 32 位浮点数。AV_SAMPLE_FMT_S16
: 16 位有符号整数。AV_SAMPLE_FMT_S32
: 32 位有符号整数。AV_SAMPLE_FMT_FLTP
: 平面格式的 32 位浮点数。AV_SAMPLE_FMT_S16P
: 平面格式的 16 位有符号整数
linesize
指向长度为8的数组的指针,对应data的长度data
指向长度为8的uint8_t
数组的指针,如果交错格式如AV_SAMPLE_FMT_S16,声道的数据交错存储在data[0]
,LRLRLR...,如果是平面格式如AV_SAMPLE_FMT_S16P,数据在data[0]-》LLL, data[1]-》RRRsample_rate
采样率,表示每秒的采样次数。channels
音频的声道数channel_layout
声道布局,表示每个声道的空间分布nb_samples
每一帧采样点数,pts
&duration
当前帧的开始时间和持续时长,需要结合stream->time_base
得到时间。
frame的数据格式和SDL_AudioSpec
格式不一定是相同的,这是需要一个转换函数:
swr_convert
用于音频重采样s
:SwrContext
指针,音频重采样器的上下文。out
: 输出缓冲区的指针数组。每个元素指向一个声道的输出数据。- `out_count: 输出缓冲区中每个声道的样本数。
in
: 输入缓冲区的指针数组。每个元素指向一个声道的输入数据。in_count
: 输入缓冲区中每个声道的样本数。
这里的SwrContext
参数,需要构建一个转换的上下文并初始化swr_init(swr_ctx)
,SwrContext
配置in、out相关参数如下,in就是当前frame的参数,out就是前一节SDL_AudioSpec
的参数。
struct SwrContext *swr_alloc_set_opts(struct SwrContext *s, int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate, int64_t in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate, int log_offset, void *log_ctx);
经过转换的out数据,就可以copy到上一小节的callback
的stream
中了,当然这里还有几个问题:
- 长度问题,frame转化的out数据长度不一定和
callback
中的len
相等- 如果数据不够填充,继续读取下一帧frame,如果没有frame了,填充0
- 如果转化的数据超过了
stream
的容量,把剩余数据记录下来保存到audio_buf
中 - 下一次填充先填充
audio_buf
数据,再读取frame数据
- 同步问题,常见播放以音频主时钟,但其他主时钟方式需要调整音频播放速度
swr_set_compensation
设置音频重采样过程中使用的补偿机制,下一章会介绍
- 音量问题
- 如果有设置视频音量,
SDL_MixAudioFormat
进行声音混合替代直接copy
- 如果有设置视频音量,
这样就可以播放出声音了。
图像
- 视频
AVFrame
中,相关主要字段:format
视频格式,常见格式AV_PIX_FMT_YUV420P
: YUV 4:2:0 平面格式。AV_PIX_FMT_YUV422P
: YUV 4:2:2 平面格式。AV_PIX_FMT_RGB24
: 24 位 RGB 格式。AV_PIX_FMT_BGRA
: 32 位带 Alpha 通道的 BGRA 格式
width
&height
: 图像宽高linesize
注意这里和音频不同
,每个维度上的行宽,这里和width
也不同,可能有字节对齐原因。data
存储视频帧的像素数据的指针数组,- 如对于 YUV 格式
data[0]
存储 Y 分量(亮度)data[1]
存储 U 分量(色度)data[2]
存储 V 分量(色度)
pts
&duration
当前帧的开始时间和持续时长,需要结合stream->time_base
得到时间。key_frame
是否为关键帧color_range
颜色值范围AVCOL_RANGE_MPEG
: 限制范围(16-235 用于 Y,16-240 用于 U/V)。AVCOL_RANGE_JPEG
: 全范围(0-255)。
colorspace
颜色空间AVCOL_SPC_BT709
: BT.709 标准。AVCOL_SPC_BT470BG
: BT.470BG 标准。
也就是在视频播放,展示Frame数据即可,线程处理如下:
- 视频保持循环
- 判断是否结束,如果播放结束,退出线程
- 判断是否暂停状态,如果是暂停,直接进入休眠,等待操作
- 判读是否当前帧是否已经播完,
- 未完,保持当前帧,计算剩余时间,设置休眠时间等于剩余时间
- 播完,从FrameQueue中取到新帧(最近时间的帧),根据帧的
duration
设置休眠。
而更新Frame时,移动端需要在各自平台上渲染,注意这里iOS段不需要到主线程处理,在本线程更新即可,android未验证
。对于视频的每一种格式,对应一种OpenGL渲染方式(这里也可以用sws_scale
转换,相当于CPU转换,性能会下降一些)。
最常见的格式AV_PIX_FMT_YUV420P
,处理如下,data[0]、data[1]、data[2]分别转换成3个纹理,再根据YUV到RGB的公式转换即可,OpenGL着色器如下:
顶点着色器
precision highp float;
varying highp vec2 vv2_texcoord;
attribute highp vec4 av4_position;
attribute highp vec2 av2_texcoord;
void main() {
vv2_texcoord = av2_texcoord;
gl_Position = av4_position;
}
片元着色器(BT.709标准),如果是BT.601格式需要调整参数
uniform highp sampler2D samplerY;
uniform highp sampler2D samplerU;
uniform highp sampler2D samplerV;
varying highp vec2 vv2_texcoord;
void main()
{
highp float y = texture2D(samplerY, vv2_texcoord).r;
highp float u = texture2D(samplerU, vv2_texcoord).r - 0.5;
highp float v = texture2D(samplerV, vv2_texcoord).r - 0.5;
highp float r = y + 1.402 * v;
highp float g = y - 0.344 * u - 0.714 * v;
highp float b = y + 1.772 * u;
gl_FragColor = vec4(r, g, b, 1.0);
}
关于OpenGL渲染,本节讲的比较粗糙,后续章节会有关于OpenGL的专门介绍。