背景
前面文章介绍HarmonyOS Next使用系统相册和拍摄工具无权限获取图片和视频,在获取到图片和视频后一般情况我们需要对图片和视频进行压缩处理,以此来减低带宽和存储资源。用华为 Mate60 Pro系统相机拍摄了一段十秒的视频,生成的文件大概有38Mb,解析文件信息看到视频码率高达30mb/s,是相当高的,很多场景我们对视频的质量要求没那么高:
图片的压缩方法前面已经做过介绍,本文介绍基于HarmonyOS Next平台的视频压缩编码。
视频压缩原理
相关概念
视频是什么呢?
视频其实就是一张一张的连续图片,一般视频一秒18帧图片的话就不会产生视频效果不流畅的问题。
视频压缩是什么呢?
视频压缩是利用数字信号处理技术和算法将视频数据进行压缩,减少其占用的存储空间和传输带宽,而尽量保持原有视频质量的过程。
为什么可以压缩呢?因为图像内和图像之间有很多冗余信息,压缩就是去除这些冗余。比如一张图片内可能有一大片是空白或者同一样色,这样就是冗余信息,可以用较少的空间存储;还有图像与图像之间也有冗余,一个人在跑步,两帧之间可能只有腿部的位置稍有不同。压缩主要有以下技术:
- 帧内压缩:对单帧图像进行编码压缩,不考虑前后帧之间的关联,如DCT(离散余弦变换)。
- 帧间压缩:利用视频序列中相邻帧之间的相关性进行压缩,常见的有运动估计、运动补偿等。
- 变换编码:通过数学变换将图像空间域的信息转换到频域,提取重要信息进行编码。
- 量化:将变换后的系数进行量化,减少数据量,实现压缩。
压缩转码是什么呢?
如何来衡量一个视频的压缩程度呢?用一个叫码率的概念。码率(bit rate)又称比特率,是指单位时间内传输的数据位数,一般用千比特每秒(kbps)、兆比特每秒(Mbps)等单位来表示。在视频中,码率决定了视频的清晰度、流畅度和色彩还原度等方面的表现。高码率的视频通常具有更高的分辨率、更细腻的画面、更流畅的动态效果和更准确的色彩表现。
其实我们可以将码率理解为视频的压缩程度。举个最直观的例子同样是1280*270
,帧率是18的视频,RGBA色彩模式下,我们算一下一秒不压缩的情况是多大,一秒大小=1280*720*18
字节=3686400*18
字节=3686400*18*8
比特=530841kbps = 530Mbps,一秒视频有530M比特。
而一般高清、蓝光这种高品质的,所谓4k、8k的视频也才几Mbps,压缩编码可以完成百倍的压缩。
我们在手机使用系统相机拍摄的视频都比较大,但是有时候我们又用不了这么高质量的视频,需要压缩视频文件体积,这就是今天要分享的视频压缩编码。
音视频处理流程介绍
日常场景中音视频处理的一般流程如下图:
视频压缩编码的主要原理是先将视频解码解复用成原始数据,然后对原始数据进行处理(可选),然后重新编码,编码时设置新的码率,这样可以以最简单的方式实现压缩视频体积的办效果。
HarmonyOS Next音视频编解码API介绍
一般我们可以使用三方库对视频进行压缩,比如FFMPEG,但是基于三方库都是通过软件编解码实现的,即全靠CPU对视频做处理,而一般手机都会提供专门硬件对音视频做专门处理,要使用这些硬件能力需要使用系统提供的硬件编解码接口。接下来介绍下HarmonyOS Next提供的硬件音视频编解码接口。
软件编解码器和硬件编解码器定义如下:
- 软件编解码器: 指在CPU上进行编解码工作的编解码器,能力可灵活迭代,相比硬件编解码器具有更好的兼容性,更好的协议和规格扩展能力。
- 硬件编解码器: 指在专有硬件上进行编解码工作的编解码器,其特点是已在硬件平台硬化,能力随硬件平台迭代。相比软件编解码器具有更好的功耗、耗时和吞吐表现,同时能降低CPU负载。
HarmonyOS Next提供的音视频编解码接口只有C API方式,下面介绍HarmonyOS Next音视频解复用、解码、编码、复用对应的API。
解复用
解复用是对文件的操作,文件主要有本地文件和网络文件,所以一般的文件解析器支持从本地读取或从网络读取两种方式。读取到文件后开始对文件进行解析,一般我们可以解析到媒体的DRM(数字版权管理Digital Rights Management)相关信息以及音频、视频、字幕等媒体sample。
HarmonyOS Next 目前支持的数据输入类型有:远程连接(http协议)和文件描述符(fd),解析http文件需要申请网络权限(ohos.permission.INTERNET),解析本地文件需要读取媒体权限(ohos.permission.READ_MEDIA)
使用解复用器需要用到下面四个动态库:
- libnative_media_codecbase.so:编解码器基础库
- libnative_media_avdemuxer.so:解复用器库
- libnative_media_avsource.so:媒体源库
- libnative_media_core.so:媒体核心库
解复用可以理解为按一定结构读取解析文件,主要是IO相关操作,不会涉及到太多CPU资源占用。
接下来一步一步介绍解复用器API。
1. 添加头文件
#include <multimedia/player_framework/native_avdemuxer.h>
#include <multimedia/player_framework/native_avsource.h>
#include <multimedia/player_framework/native_avcodec_base.h>
#include <multimedia/player_framework/native_avformat.h>
#include <multimedia/player_framework/native_avbuffer.h>
#include <fcntl.h>
#include <sys/stat.h>
2. 创建资源管理对象
// 为 fd 资源文件创建 source 资源对象, 传入 offset 不为文件起始位置 或 size 不为文件大小时,可能会因不能获取完整数据导致 source 创建失败、或后续解封装失败等问题
OH_AVSource *source = OH_AVSource_CreateWithFD(fd, 0, fileSize);
if (source == nullptr) {
printf("create source failed");
return;
}
这里传入本地文件资源描述符,调用OH_AVSource_CreateWithFD创建AVSource。如果是解析网络资源使用OH_AVSource_CreateWithURI
// 为 uri 资源文件创建 source 资源对象(可选)
OH_AVSource *source = OH_AVSource_CreateWithURI(uri);
还有自定义数据源方式创建source对象,调用OH_AVSource_CreateWithDataSource方法:
// 为自定义数据源创建 source 资源对象(可选)。
// 当使用OH_AVSource_CreateWithDataSource时需要补充g_filePath
g_filePath = filePath ;
OH_AVDataSource dataSource = {fileSize, AVSourceReadAt};
OH_AVSource *source = OH_AVSource_CreateWithDataSource(&dataSource);
使用自定义数据源需要先实现AVSourceReadAt接口函数实现,本文不涉及自定义数据源,先不做过多介绍。
3. 创建解封装器实例对象
创建解封装器的方法为OH_AVDemuxer_CreateWithSource,需要传入上面生成的source:
// 为资源对象创建对应的解封装器
OH_AVDemuxer *demuxer = OH_AVDemuxer_CreateWithSource(source);
if (demuxer == nullptr) {
printf("create demuxer failed");
return;
}
4. 获取文件轨道数
使用OH_AVSource_GetSourceFormat方法根据上面的source得到Format后可以获取到视频文件的轨道数:
// 从文件 source 信息获取文件轨道数,用户可通过该接口获取文件级别属性
OH_AVFormat *sourceFormat = OH_AVSource_GetSourceFormat(source);
if (sourceFormat == nullptr) {
printf("get source format failed");
return;
}
int32_t trackCount = 0;
if (!OH_AVFormat_GetIntValue(sourceFormat, OH_MD_KEY_TRACK_COUNT, &trackCount)) {
printf("get track count from source format failed");
return;
}
OH_AVFormat_Destroy(sourceFormat);
获取完成需要调用OH_AVFormat_Destroy手动销毁format。
5. 获取轨道index及信息
一般音频文件里面有音频和视频两个轨道,通过OH_AVSource_GetTrackFormat 遍历index获取对应轨道信息的Format,然后根据轨道信息的type判断是音频还是视频,针对视频还可以通过OH_AVFormat_GetIntValue 获取视频的宽高等:
uint32_t audioTrackIndex = 0;
uint32_t videoTrackIndex = 0;
int32_t w = 0;
int32_t h = 0;
int32_t trackType;
for (uint32_t index = 0; index < (static_cast<uint32_t>(trackCount)); index++) {
// 获取轨道信息,用户可通过该接口获取对应轨道级别属性,具体支持信息参考附表 2
OH_AVFormat *trackFormat = OH_AVSource_GetTrackFormat(source, index);
if (trackFormat == nullptr) {
printf("get track format failed");
return;
}
if (!OH_AVFormat_GetIntValue(trackFormat, OH_MD_KEY_TRACK_TYPE, &trackType)) {
printf("get track type from track format failed");
return;
}
static_cast<OH_MediaType>(trackType) == OH_MediaType::MEDIA_TYPE_AUD ? audioTrackIndex = index : videoTrackIndex = index;
// 获取视频轨宽高
if (trackType == OH_MediaType::MEDIA_TYPE_VID) {
if (!OH_AVFormat_GetIntValue(trackFormat, OH_MD_KEY_WIDTH, &w)) {
printf("get track width from track format failed");
return;
}
if (!OH_AVFormat_GetIntValue(trackFormat, OH_MD_KEY_HEIGHT, &h)) {
printf("get track height from track format failed");
return;
}
}
OH_AVFormat_Destroy(trackFormat);
}
每次使用完通过OH_AVFormat_Destroy释放轨道Format信息。
6. 解封装器添加轨道
通过OH_AVDemuxer_SelectTrackByID 方法为解封装器添加音频或者视频轨道:
if(OH_AVDemuxer_SelectTrackByID(demuxer, audioTrackIndex) != AV_ERR_OK){
printf("select audio track failed: %d", audioTrackIndex);
return;
}
if(OH_AVDemuxer_SelectTrackByID(demuxer, videoTrackIndex) != AV_ERR_OK){
printf("select video track failed: %d", videoTrackIndex);
return;
}
使用完成后通过OH_AVDemuxer_UnselectTrackByID取消选择轨道。
7. 开始解复用,循环读取音视频包
首先需要通过OH_AVBuffer_Create 创建接受数据的Buffer,然后通过OH_AVDemuxer_ReadSampleBuffer读取音频或视频内容到Buffer中:
// 创建 buffer,用与保存用户解封装得到的数据
OH_AVBuffer *buffer = OH_AVBuffer_Create(w * h * 3 >> 1);
if (buffer == nullptr) {
printf("build buffer failed");
return;
}
OH_AVCodecBufferAttr info;
bool videoIsEnd = false;
bool audioIsEnd = false;
int32_t ret;
while (!audioIsEnd || !videoIsEnd) {
// 在调用 OH_AVDemuxer_ReadSampleBuffer 接口获取数据前,需要先调用 OH_AVDemuxer_SelectTrackByID 选中需要获取数据的轨道
// 获取音频sample
if(!audioIsEnd) {
ret = OH_AVDemuxer_ReadSampleBuffer(demuxer, audioTrackIndex, buffer);
if (ret == AV_ERR_OK) {
// 可通过 buffer 获取并处理音频sample
OH_AVBuffer_GetBufferAttr(buffer, &info);
printf("audio info.size: %d\n", info.size);
if (info.flags == OH_AVCodecBufferFlags::AVCODEC_BUFFER_FLAGS_EOS) {
audioIsEnd = true;
}
}
}
if(!videoIsEnd) {
ret = OH_AVDemuxer_ReadSampleBuffer(demuxer, videoTrackIndex, buffer);
if (ret == AV_ERR_OK) {
// 可通过 buffer 获取并处理视频sample
OH_AVBuffer_GetBufferAttr(buffer, &info);
printf("video info.size: %d\n", info.size);
if (info.flags == OH_AVCodecBufferFlags::AVCODEC_BUFFER_FLAGS_EOS) {
videoIsEnd = true;
}
}
}
}
OH_AVBuffer_Destroy(buffer);
读取完成后需要通过OH_AVBuffer_Destroy销毁Buffer。
也可以不全部解析,从视频文件的某个位置开始处理,通过OH_AVDemuxer_SeekToTime函数跳转到指定时间位置,作用类似于我们播放器里面推动进度条。
8. 销毁解封装器实例
通过OH_AVSource_Destroy销毁创建的Source,通过OH_AVDemuxer_Destroy销毁解复用器。
解码
当前HarmonyOS Next支持AVC(H.264)、HEVC(H.265) 软硬解类型。
解复用后的数据我们创建了Buffer用来接收,解码数据有两种接收方式:Buffer和Surface:
- Surface输出是指用OHNativeWindow来传递输出数据,可以与其他模块对接,例如XComponent。
- Buffer输出是指经过解码的数据会以共享内存的方式输出。
解码器的数据缓存由解码器自己管理,和Android的MediaCodec类似,HarmonyOS Next解码器也是基于状态机
定义了六个大状态:Initialized、Configured、Prepared、Executing、Error、Released。
每种状态切换都有对应方法:
- OH_VideoDecoder_Reset或者创建解码器后进入Initialized状态;
- OH_VideoDecoder_Configure进入Configured状态
- OH_VideoDecoder_Prepare进入Prepared状态
- OH_VideoDecoder_Start进入Executing状态
- OH_VideoDecoder_Destroy 进入Released状态
整体交互流程可以参考官方的流程图,还是比较清晰的:
要使用编解码能力需要依赖下面的动态库:
- libnative_media_codecbase.so:编解码基础库
- libnative_media_core.so:媒体处理核心库
- libnative_media_venc.so:视频编码
- libnative_media_vdec.so:视频解码
具体接口参考官方文档中的示例:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides...
我们要弄清楚,解码器传入的是复用器中读取到的Buffer,输出为YUV Buffer或者Surface中。通过OH_VideoDecoder_SetSurface(videoDec, window) 为编码器设置Surface,如果是解码后直接播放的场景使用Surface模式直接渲染到屏幕可以避免数据拷贝带来的性能开销。写入数据的时机依赖OH_VideoDecoder_RegisterCallback注册的回调函数调用:
// 解码异常回调OH_AVCodecOnError实现
static void OnError(OH_AVCodec *codec, int32_t errorCode, void *userData)
{
// 回调的错误码由调用者判断处理
(void)codec;
(void)errorCode;
(void)userData;
}
// 解码数据流变化回调OH_AVCodecOnStreamChanged实现
static void OnStreamChanged(OH_AVCodec *codec, OH_AVFormat *format, void *userData)
{
// 可通过format获取到变化后的视频宽、高、跨距等
(void)codec;
(void)userData;
OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &width);
OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &height);
OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_STRIDE, &widthStride);
OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_SLICE_HEIGHT, &heightStride);
}
// 解码输入回调OH_AVCodecOnNeedInputBuffer实现
static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData)
{
// 输入帧buffer对应的index,送入InIndexQueue队列
// 输入帧的数据buffer送入InBufferQueue队列
// 数据处理
// 写入解码码流
}
// 解码输出回调OH_AVCodecOnNewOutputBuffer实现,Surface模式buffer参数为空
static void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData)
{
// 完成帧buffer对应的index,送入outIndexQueue队列
// 完成帧的数据buffer送入outBufferQueue队列
// 数据处理
// 显示并释放解码帧
}
// 配置异步回调,调用 OH_VideoDecoder_RegisterCallback 接口
OH_AVCodecCallback cb = {&OnError, &OnStreamChanged, &OnNeedInputBuffer, &OnNewOutputBuffer};
// 配置异步回调
int32_t ret = OH_VideoDecoder_RegisterCallback(videoDec, cb, NULL); // NULL:用户特定数据userData为空
if (ret != AV_ERR_OK) {
// 异常处理
}
视频解码软/硬件解码存在差异,基于MimeType创建解码器时,软解当前仅支持 H264 (OH_AVCODEC_MIMETYPE_VIDEO_AVC),硬解则支持 H264 (OH_AVCODEC_MIMETYPE_VIDEO_AVC) 和 H265 (OH_AVCODEC_MIMETYPE_VIDEO_HEVC)。
编码
视频编码是解码的逆过程,目前仅支持硬件编码,基于MimeType创建编码器时,支持配置为H264 (OH_AVCODEC_MIMETYPE_VIDEO_AVC) 和 H265 (OH_AVCODEC_MIMETYPE_VIDEO_HEVC)。
编码器的输入支持Surface模式和Buffer模式,即可以将屏幕渲染的数据或者摄像头采集到的数据直接一Surface的方式传递给编码器。
编码器是状态机模式,和解码器类似:
具体编码流程参考官方文档:
具体接口参考官方文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides...
由于我们的目的是压缩编码,这里主要介绍下编码器配置接口。
通过OH_VideoEncoder_CreateByName创建编码器后,调用OH_VideoEncoder_Configure 配置编码器:
// 配置视频帧速率
double frameRate = 30.0;
// 配置视频YUV值范围标志
bool rangeFlag = false;
// 配置视频原色
int32_t primary = static_cast<int32_t>(OH_ColorPrimary::COLOR_PRIMARY_BT709);
// 配置传输特性
int32_t transfer = static_cast<int32_t>(OH_TransferCharacteristic::TRANSFER_CHARACTERISTIC_BT709);
// 配置最大矩阵系数
int32_t matrix = static_cast<int32_t>(OH_MatrixCoefficient::MATRIX_COEFFICIENT_IDENTITY);
// 配置编码Profile
int32_t profile = static_cast<int32_t>(OH_AVCProfile::AVC_PROFILE_BASELINE);
// 配置编码比特率模式
int32_t rateMode = static_cast<int32_t>(OH_VideoEncodeBitrateMode::CBR);
// 配置关键帧的间隔,单位为毫秒
int32_t iFrameInterval = 23000;
// 配置比特率
int64_t bitRate = 3000000;
// 配置编码质量
int64_t quality = 0;
OH_AVFormat *format = OH_AVFormat_Create();
OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, width); // 必须配置
OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, height); // 必须配置
OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, DEFAULT_PIXELFORMAT); // 必须配置
OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, frameRate);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_RANGE_FLAG, rangeFlag);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_COLOR_PRIMARIES, primary);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_TRANSFER_CHARACTERISTICS, transfer);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_MATRIX_COEFFICIENTS, matrix);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_I_FRAME_INTERVAL, iFrameInterval);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_PROFILE, profile);
//只有当OH_MD_KEY_BITRATE = CQ时,才需要配置OH_MD_KEY_QUALITY
if (rateMode == static_cast<int32_t>(OH_VideoEncodeBitrateMode::CQ)) {
OH_AVFormat_SetIntValue(format, OH_MD_KEY_QUALITY, quality);
} else if (rateMode == static_cast<int32_t>(OH_VideoEncodeBitrateMode::CBR) ||
rateMode == static_cast<int32_t>(OH_VideoEncodeBitrateMode::VBR)){
OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, bitRate);
}
OH_AVFormat_SetIntValue(format, OH_MD_KEY_VIDEO_ENCODE_BITRATE_MODE, rateMode);
int32_t ret = OH_VideoEncoder_Configure(videoEnc, format);
if (ret != AV_ERR_OK) {
// 异常处理
}
OH_AVFormat_Destroy(format);
这里面控制码率的是OH_MD_KEY_BITRATE。
复用
复用器创建时需要指定生成文件的目录,并且添加视频轨和音频轨。
通过OH_AVFormat_Create或OH_AVFormat_CreateAudioFormat创建视频或者音频轨,然后调用OH_AVMuxer_AddTrack添加到复用器。后面调用OH_AVMuxer_WriteSampleBuffer函数将Sample写入到复用器,最终被写入到指定的文件。
复用是解复用的逆过程,具体可以参考官方文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides...
HarmonyOS Next 平台视频压缩的最佳实践
由于占用视频文件体积的主要是视频流,我们这里对音频先不做处理,主要对视频码率进行处理。由于不需要对视频分辨率做调整,使用Surface模式可以较少数据拷贝带来的性能消耗,所以我们在视频压缩编码时可以先创建编码器,然后获取编码器的Surface,将编码器的Surface传到解码器:
将编码器的Surface作为编码器的输入,解码器的输出。
介绍完API和Surface技巧后我们还需要解决最后一个问题,数据队列问题。由于解复用器、解码器等各自都有自己的工作线程,解复用后的数据要以队列的形式缓存起来,等待被解码器线程消费,编码器和复用器也是类似的逻辑。
这里创建一个对象来管理这些队列:
typedef struct EncodeElement {
uint8_t *data;
OH_AVCodecBufferAttr attr;
};
typedef struct DemuxElement {
OH_AVMemory *mem;
OH_AVCodecBufferAttr attr;
};
class MutexContext {
public:
std::mutex videoMutex_;
std::deque<std::shared_ptr<DemuxElement>> VMem;
std::mutex videoEncMutex;
std::deque<EncodeElement> VRawQueue;
std::mutex audioMutex_;
std::deque<std::shared_ptr<DemuxElement>> AMem;
std::mutex AudioEncMutex;
std::deque<EncodeElement> ARawQueue;
OHNativeWindow *nativeWindow;
int32_t vTrackId;
int32_t aTrackId;
};
这里一个视频压缩转码器就基本成型了。
总结
本文围绕 HarmonyOS Next 的视频压缩编码展开。首先阐述了在该平台上对图片和视频进行压缩处理的背景需求。接着深入讲解视频压缩原理,包括相关概念如视频本质、压缩方式及码率衡量等,还介绍了音视频处理流程。重点介绍了 HarmonyOS Next 音视频编解码 API,如解复用、解码、编码、复用的具体操作及相关动态库。最后给出了该平台视频压缩的最佳实践,包括利用 Surface 技巧和管理数据队列,完成视频压缩转码器的构建。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。