开发者在开发影视、会议和直播等应用时,通常需要使用视频解码能力实现高性能的播放和视频处理功能。
本文中视频播控功能采用Surface模式解码能力实现,Surface模式是指在视频解码的过程中,用NativeWindow来传递输出数据,直接与XComponent组件对接,实现视频播放。与Buffer模式相比,开发者不需要处理解码后的视频数据与播放组件的对接。具体区别可参考视频解码。
在阅读本文之前,建议开发者先了解音视频编解码相关知识,同时建议IDE环境中的SDK版本不低于6.0.2(22)。
本文主要内容如下:
视频解码播放是将视频文件内压缩的数据通过解码还原为可视的图像,并进行渲染显示。视频解码常用于高清播放和低延时播放,也是视频编辑播放(如添加滤镜、水印)、实时弹幕叠加以及高级色彩处理等操作的基础。相较于直接使用AVPlayer进行播放(可参考基于AVPlayer基础播控实践),解码播放支持更多的视频格式和画面处理能力。
Surface模式视频解码播放是通过调用系统AVCodec模块的能力实现的。通过解封装视频文件,解码视频和音频数据进行送显和播放,具体流程如下:
选择视频进行播放后,使用视频文件初始化解封装器。
获取在XComponent初始化时OnSurfaceCreate回调中返回的NativeWindow。
初始化解码器,设置NativeWindow等参数后启动解码器。
创建解码输入和输出子线程,启动解码播放流程。

输入子线程通过OH_AVCodecCallback中的OnNeedInputBuffer异步回调函数指针获取可用的AVBuffer后,从解封装器中读取视频数据提交给解码器;实现向解码器输入待解码的视频数据。

解码输出子线程通过OnNeedOutputBuffer拿到解码后的帧数据,进行音画同步处理后,通知解码器在Surface上完成渲染。

创建解封装器、解码器等解码资源。
int32_t Player::Init(SampleInfo &info)
{
// ...
// Create decode sources pointer.
videoDecoder_ = std::make_unique<VideoDecoder>();
audioDecoder_ = std::make_unique<AudioDecoder>();
demuxer_ = std::make_unique<Demuxer>();
// Create demuxer by video info.
int32_t ret = demuxer_->Create(videoInfo_);
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, ret, "Create demuxer failed");
// Create and Configure audio ande video decoder.
ret = CreateAudioDecoder();
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, ret, "Create audio decoder failed");
ret = CreateVideoDecoder();
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, ret, "Create video decoder failed");
// ...
} 根据视频信息设置解码参数,以视频参数为例。
int32_t Player::CreateVideoDecoder()
{
// Create decoder by system mime.
int32_t ret = videoDecoder_->Create(videoInfo_.videoInfo.videoCodecMime);
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, MEDIA_ERR_ERROR, "Create video decoder failed, mime:%{public}s",
videoInfo_.videoInfo.videoCodecMime.c_str());
videoDecContext_ = new CodecUserData;
// Configure nativeWindow and video info to decoder.
videoInfo_.videoInfo.window = XComponentManager::GetInstance()->nativeWindow_;
ret = videoDecoder_->Config(videoInfo_, videoDecContext_);
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, ret, "Video Decoder config failed");
return MEDIA_ERR_OK;
} 启动解码器,创建解码输入、输出子线程,开始解码视频。
int32_t Player::Start()
{
// ...
if (videoDecContext_) {
// Start the video decoder.
ret = videoDecoder_->Start();
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, ret, "Video Decoder start failed");
// Set start state.
isStarted_ = true;
isPause_ = false;
// Create video decode input and output sub thread.
videoDecInputThread_ = std::make_unique<std::thread>(&Player::VideoDecInputThread, this);
videoDecOutputThread_ = std::make_unique<std::thread>(&Player::VideoDecOutputThread, this);
// ...
}
// ...
} 输入子线程从解封装器读取数据,送入解码器进行解码。
void Player::VideoDecInputThread()
{
while (isDecoding_) {
// ...
// Get AVBuffer and maintain the queue.
CodecBufferInfo bufferInfo = videoDecContext_->inputBufferInfoQueue.front();
videoDecContext_->inputBufferInfoQueue.pop();
videoDecContext_->inputFrameCount++;
// ...
// read sample from demuxer.
int32_t ret = demuxer_->ReadSample(demuxer_->GetVideoTrackId(),
reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "ReadSample failed, thread out");
// ...
// push the buffer to the decoder.
ret = videoDecoder_->PushInputBuffer(bufferInfo);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "Push data failed, thread out");
}
MEDIA_LOGI("VideoDecInputThread out.");
} 输出子线程从解码器拿到解码后的帧数据,处理后进行送显。
void Player::VideoDecOutputThread()
{
videoInfo_.videoInfo.frameInterval = MICROSECOND / videoInfo_.videoInfo.frameRate;
while (isDecoding_) {
// ...
CodecBufferInfo bufferInfo = GetBufferInfo();
// ...
// Notify the surface to render the data and release it.
lastPushTime = std::chrono::system_clock::now();
ret = videoDecoder_->RenderOutputBuffer(bufferInfo.bufferIndex, !dropFrame);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "Decoder output thread out");
}
// ...
} 调用OH_VideoDecoder_RenderOutputBufferAtTime()接口通知解码器在指定时间内完成在输出Surface上的渲染。
int32_t VideoDecoder::RenderOutputBuffer(uint32_t bufferIndex, bool render)
{
CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, MEDIA_ERR_ERROR, "Decoder is null");
int32_t ret = MEDIA_ERR_OK;
// Check if render.
if (render) {
// Get timestamp for render time.
int64_t renderTimestamp = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
// Notify video decode to render by surface.
ret = OH_VideoDecoder_RenderOutputBufferAtTime(decoder_, bufferIndex, renderTimestamp);
CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, MEDIA_ERR_ERROR, "Render output data failed, RET = %{public}d", ret);
} else {
// Free buffer that does not need to be rendered.
ret = OH_VideoDecoder_FreeOutputBuffer(decoder_, bufferIndex);
CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, MEDIA_ERR_ERROR, "Free output data failed, RET = %{public}d", ret);
}
return MEDIA_ERR_OK;
} 全屏播放通过UI侧的能力实现,主要流程如下。
应用切换全屏状态后,更新相应的状态变量。
该状态变量的@Watch监听方法,会根据当前状态,设置窗口旋转模式和沉浸式布局。
UI组件调整尺寸和位置,实现全屏页面布局。
在Native侧可通过OH_NativeWindow_NativeWindowSetScalingModeV2()接口设置XComponent的画面渲染缩放模式,通过设置OHScalingModeV2参数可选择按视频比例缩放画面播放或拉伸缩放以匹配视频窗口大小。

定义全屏状态的状态变量,并绑定监听方法。
// The horizontal full-screen playback state flag.
@Watch('fullScreenSet') @Link isFullScreen: boolean; 状态变量变化后,绑定的监听方法会根据当前状态设置窗口属性。
fullScreenSet() {
// Change window orientation and layout when setting full screen.
window.getLastWindow(this.getUIContext().getHostContext()).then((topWindow) => {
topWindow.setPreferredOrientation(this.isFullScreen ?
window.Orientation.AUTO_ROTATION_LANDSCAPE : window.Orientation.PORTRAIT).catch((error: BusinessError) => {
hilog.error(DOMAIN, TAG, `Failed to setPreferredOrientation. Cause: ${error.code}, message: ${error.message}`);
});
topWindow.setWindowLayoutFullScreen(this.isFullScreen ? true : false).catch((error: BusinessError) => {
hilog.error(DOMAIN, TAG,
`Failed to setWindowLayoutFullScreen. Cause: ${error.code}, message: ${error.message}`);
});
}).catch((error: BusinessError) => {
hilog.error(DOMAIN, TAG, `Failed to getLastWindow. Cause: ${error.code}, message: ${error.message}`);
});
} 以XComponent组件为例,组件高度会根据状态变量进行调整,实现全屏播放。
XComponent({
id: 'playerXC',
type: XComponentType.SURFACE,
libraryname: 'player'
})
.width(WIDTH_SIZE)
.height(this.isFullScreen ? NORMAL_SIZE : FULL_SIZE) 暂停播放通过阻塞输入和输出子线程实现。阻塞后输入子线程停止提交数据到解码器,输出子线程停止向Surface提供视频帧数据,达到暂停播放的效果。继续播放则是取消输入和输出子线程的阻塞状态,从而恢复播放状态。

解码输入和输出子线程均会执行条件变量 wait 操作,并在该操作内部判断当前是否处于暂停状态。
// Use condition to wait for decoder requests for data.
std::unique_lock<std::mutex> lock(videoDecContext_->inputMutex);
videoDecContext_->inputCond.wait(
lock, [this]() { return !isPause_ && (!isStarted_ || !videoDecContext_->inputBufferInfoQueue.empty()); }); 暂停播放时应用会同步设置暂停状态,并停止音频播放。
int32_t Player::Pause()
{
CHECK_AND_RETURN_RET_LOG(isStarted_, MEDIA_ERR_ERROR, "player do not start!");
// Set pause state.
isPause_.store(true);
// if the audio render, pause it.
if (audioRenderer_) {
OH_AudioRenderer_Pause(audioRenderer_);
}
return MEDIA_ERR_OK;
} 继续播放会取消暂停状态,唤醒等待在条件变量上的子线程并继续播放音频。
int32_t Player::Resume()
{
CHECK_AND_RETURN_RET_LOG(isStarted_, MEDIA_ERR_ERROR, "player do not start!");
isPause_.store(false); // Cancel the pause state.
// Notify the thread to continue work.
if (videoDecContext_) {
videoDecContext_->inputCond.notify_all();
videoDecContext_->outputCond.notify_all();
}
if (audioDecContext_) {
audioDecContext_->inputCond.notify_all();
audioDecContext_->outputCond.notify_all();
}
if (audioRenderer_) {
OH_AudioRenderer_Start(audioRenderer_); // if need audio to play, continue.
}
return MEDIA_ERR_OK;
} 跳转播放通过解封装器的跳转功能实现,具体步骤如下。
使用解封装器的OH_AVDemuxer_SeekToTime()方法,跳转到指定时间的上一个关键帧。
阻塞解码输入和输出子线程,暂停解码。
清空待送入解码器的数据和已解码完成待送显的数据。
使用视频解码的OH_VideoDecoder_Flush()方法和音频解码的OH_AudioCodec_Flush()方法,清除编码器中的缓存的输入和输出数据。
取消解码子线程的阻塞,在跳转位置重新解码播放。

解封装器seek到指定时间点的前一个关键帧。
int32_t Demuxer::Seek(int64_t position)
{
// Select video track.
int32_t ret = OH_AVDemuxer_SelectTrackByID(demuxer_, videoTrackId_);
CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, MEDIA_ERR_ERROR, "SelectTrackByID failed");
MEDIA_LOGI("Seek to %{public}" PRId64, position);
// Call the system function to seek to destination.
ret = OH_AVDemuxer_SeekToTime(demuxer_, position, SEEK_MODE_PREVIOUS_SYNC);
CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, MEDIA_ERR_ERROR, "Seek failed");
return MEDIA_ERR_OK;
} 刷新解码器,清空缓存,再进行解码播放。
int32_t Player::Seek(int64_t desTime)
{
isReadRenderTime_.store(false);
// ...
if (videoDecoder_) {
// Flush decoder, clear cache.
videoDecContext_->isFlushing.store(true);
videoDecContext_->ClearQueue();
ret = videoDecoder_->Flush(videoDecContext_);
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, MEDIA_ERR_ERROR, "video seek video flush failed");
videoDecContext_->isFlushing.store(false);
// Restart the decoder.
ret = videoDecoder_->Start();
CHECK_AND_RETURN_RET_LOG(ret == MEDIA_ERR_OK, MEDIA_ERR_ERROR, "video seek start videoDecoder failed");
}
// ...
} 视频解码器flush之后需要重新传入PPS/SPS,这里解封装器seek后读取的第一份数据包含了PPS/SPS信息,传入解码器即可。
// read sample from demuxer.
int32_t ret = demuxer_->ReadSample(demuxer_->GetVideoTrackId(),
reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "ReadSample failed, thread out");
// ...
// push the buffer to the decoder.
ret = videoDecoder_->PushInputBuffer(bufferInfo);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "Push data failed, thread out");
PPS:图像参数集 (Picture Parameter Set),包含了一帧或多帧图像的公共编码参数。
SPS:序列参数集 (Sequence Parameter Set),包含了一整个视频序列的全局信息。这些信息在整个序列播放期间基本不变,解码器需要用它们来初始化
循环播放需要在视频播放到最后一帧后,继续从第一帧开始播放,具体流程如下。

解码输入子线程从解封装器拿到视频结束帧后,seek到首帧进行解码。
// read sample from demuxer.
int32_t ret = demuxer_->ReadSample(demuxer_->GetVideoTrackId(),
reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "ReadSample failed, thread out");
// Check if the buffer flag include eos.
if (bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS) {
while (!isAudioWaitSeek_.load()) {
std::this_thread::sleep_for(std::chrono::microseconds(WAIT_TIME));
}
ret = demuxer_->Seek(0); // Seek to the first frame.
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "Loop failed, thread out");
// Read first frame data from demuxer.
ret = demuxer_->ReadSample(demuxer_->GetVideoTrackId(), reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer),
bufferInfo.attr);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "ReadSample failed, thread out");
} 拿到音频结束帧后,等待视频解码器解帧结束后,重新读取首帧数据。
// Read sample from demuxer.
int32_t ret = demuxer_->ReadSample(demuxer_->GetAudioTrackId(),
reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "ReadSample failed, thread out");
// Check if the buffer flag include eos.
while (bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS) {
// Wait the video thread seek to first frame.
std::unique_lock<std::mutex> lock(audioDecContext_->endMutex);
isAudioWaitSeek_.store(true);
audioDecContext_->endCond.wait(lock);
int32_t ret = demuxer_->ReadSample(demuxer_->GetAudioTrackId(),
reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
CHECK_AND_BREAK_LOG(ret == MEDIA_ERR_OK, "ReadSample failed, thread out");
}
isAudioWaitSeek_.store(false); 在解码播放逻辑中,倍速功能通常基于解码数据输出模块的渲染逻辑来实现。与直接使用播放器的倍速功能相比,基于底层解码的自定义方案支持更精细的速度控制(如0.1倍速微调),满足不同的应用场景。
设置音频播放速度并通过赋值传递给视频解码模块。
void Player::SetSpeed(float speed)
{
// ...
// Set audio play speed.
OH_AudioRenderer_SetSpeed(audioRenderer_, speed);
// Set speed value for sub thread.
speed_ = speed;
audioDecContext_->speed = speed;
} 输出子线程通过音画同步功能进行追帧。可参考音画同步的开发步骤。
// video buffer is too late, drop it
if (waitTimeUs < WAIT_TIME_US_THRESHOLD_WARNING) {
dropFrame = true;
MEDIA_LOGW("VD buffer is too late");
} else {
MEDIA_LOGW("VD buffer is too early waitTimeUs: %{public}ld", waitTimeUs);
// [0, ), render it with waitTimeUs, max 1s
// [-40,0), render it
if (waitTimeUs > WAIT_TIME_US_THRESHOLD) {
waitTimeUs = WAIT_TIME_US_THRESHOLD;
}
// per frame render time plus PER_SINK_TIME_THRESHOLD
if (waitTimeUs > videoInfo_.videoInfo.frameInterval + PER_SINK_TIME_THRESHOLD) {
waitTimeUs = videoInfo_.videoInfo.frameInterval + PER_SINK_TIME_THRESHOLD;
MEDIA_LOGW("VD buffer is too early and adjusted waitTimeUs: %{public}ld", waitTimeUs);
}
}
if (waitTimeUs > 0) {
std::this_thread::sleep_for(std::chrono::microseconds(waitTimeUs));
} 切换视频播放需要先释放当前视频的解码资源,再使用切换后的视频重新初始化,具体如下。

释放当前视频的解码器、子线程等资源。
void Player::Release()
{
// ...
// Notify the sub thread continue and over.
if (videoDecContext_) {
videoDecContext_->inputCond.notify_all();
videoDecContext_->outputCond.notify_all();
}
// ...
ReleaseThread();
currentRenderTime_.store(0);
if (audioRenderer_ != nullptr) {
OH_AudioRenderer_Release(audioRenderer_);
audioRenderer_ = nullptr;
}
// Release decode resource.
if (demuxer_ != nullptr) {
demuxer_->Release();
demuxer_.reset();
}
if (videoDecoder_ != nullptr) {
videoDecoder_->Release();
videoDecoder_.reset();
}
// ...
} 释放完成后通过切换后的视频创建资源,详见视频播放章节的开发步骤。