Android FFmpeg + MediaCodec 实现视频硬解码

字节流动

共 8622字,需浏览 18分钟

 · 2021-12-31

前面 FFmpeg 系列的文章中,已经实现了音视频的播放、录制、添加滤镜等功能:



本文将利用 FFmpeg+ MediaCodec 做一个播放器,实现视频的硬解码和音视频同步等功能。



MediaCodec 介绍

MediaCodec 是 Android 提供的用于对音视频进行编解码的类,它通过访问底层的 codec 来实现编解码的功能,是 Android media 基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface和AudioTrack 一起使用。


详细描述可参见官方文档:https://developer.android.com/reference/android/media/MediaCodec.html

AMediaCodec 是 MediaCodec 的 native 接口,Google 从 Android 5.0 开始提供,Native 代码编译时需要引入 mediandk 库,官方 demo :


https://github.com/android/ndk-samples/tree/main/native-codec

FFmpeg + ANativeCodec

在 Android 没有在 Native 层开放 ModecCodec 接口之前,FFmpeg 实现硬解码需要将视频和音频数据拷贝到 Java 层,在 Java 层调用 MediaCodec (通过 JNI 调用 Java 对象方法)。


本文将实现 FFmpeg 和 AMediaCodec 结合使用, FFmpeg 负责解复用和音频解码,MediaCodec 负责视频解码并输出到 Surface(ANativeWindow)对象,其中解复用、音频解码、视频解码分别在一个子线程进行,利用队列管理音视频数据包。


注意:本 demo 处理的视频是 H.264 编码,由于 AVPacket data 不是标准的 NALU ,需要利用 av_bitstream_filter_filter 将 AVPacket 前 4 个字节替换为 0x00000001 得到标准的 NALU 数据,这样保证 MediaCodec 解码正确。


配置 AMediaCodec 对象,只解码视频流:

        m_MediaExtractor = AMediaExtractor_new();
media_status_t err = AMediaExtractor_setDataSourceFd(m_MediaExtractor, fd,static_cast<off64_t>(outStart),static_cast<off64_t>(outLen));
close(fd);
if (err != AMEDIA_OK) {
result = -1;
LOGCATE("HWCodecPlayer::InitDecoder AMediaExtractor_setDataSourceFd fail. err=%d", err);
break;
}

int numTracks = AMediaExtractor_getTrackCount(m_MediaExtractor);

LOGCATE("HWCodecPlayer::InitDecoder AMediaExtractor_getTrackCount %d tracks", numTracks);
for (int i = 0; i < numTracks; i++) {
AMediaFormat *format = AMediaExtractor_getTrackFormat(m_MediaExtractor, i);
const char *s = AMediaFormat_toString(format);
LOGCATE("HWCodecPlayer::InitDecoder track %d format: %s", i, s);
const char *mime;
if (!AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) {
LOGCATE("HWCodecPlayer::InitDecoder no mime type");
result = -1;
break;
} else if (!strncmp(mime, "video/", 6)) {
// Omitting most error handling for clarity.
// Production code should check for errors.
AMediaExtractor_selectTrack(m_MediaExtractor, i);
m_MediaCodec = AMediaCodec_createDecoderByType(mime);
AMediaCodec_configure(m_MediaCodec, format, m_ANativeWindow, NULL, 0);
AMediaCodec_start(m_MediaCodec);
}
AMediaFormat_delete(format);
}

FFmpeg 在一个线程中解复用,分别将音频和视频的编码数据包放入 2 个队列。

int HWCodecPlayer::DoMuxLoop() {
LOGCATE("HWCodecPlayer::DoMuxLoop start");

int result = 0;
AVPacket avPacket = {0};
for(;;) {
double passTimes = 0;

......

if(m_SeekPosition >= 0) { // seek 操作
//seek to frame
LOGCATE("HWCodecPlayer::DoMuxLoop seeking m_SeekPosition=%f", m_SeekPosition);
......
}

result = av_read_frame(m_AVFormatContext, &avPacket);
if(result >= 0) {
double bufferDuration = m_VideoPacketQueue->GetDuration() * av_q2d(m_VideoTimeBase);
LOGCATE("HWCodecPlayer::DoMuxLoop bufferDuration=%lfs", bufferDuration);
//防止缓冲数据包过多
while (BUFF_MAX_VIDEO_DURATION < bufferDuration && m_PlayerState == PLAYER_STATE_PLAYING && m_SeekPosition < 0) {
bufferDuration = m_VideoPacketQueue->GetDuration() * av_q2d(m_VideoTimeBase);
usleep(10 * 1000);
}

//音频和视频的编码数据包分别放入 2 个队列。
if(avPacket.stream_index == m_VideoStreamIdx) {
m_VideoPacketQueue->PushPacket(&avPacket);
} else if(avPacket.stream_index == m_AudioStreamIdx) {
m_AudioPacketQueue->PushPacket(&avPacket);
} else {
av_packet_unref(&avPacket);
}
} else {
//解复用结束,暂停解码器
std::unique_lock<std::mutex> lock(m_Mutex);
m_PlayerState = PLAYER_STATE_PAUSE;
}
}
LOGCATE("HWCodecPlayer::DoMuxLoop end");
return 0;
}

视频解码线程中,Native 使用 AMediaCodec 对视频进行解码,从视频的 AVPacket 队列中取包进行解码。

void HWCodecPlayer::VideoDecodeThreadProc(HWCodecPlayer *player) {
LOGCATE("HWCodecPlayer::VideoDecodeThreadProc start");
AVPacketQueue* videoPacketQueue = player->m_VideoPacketQueue;
AMediaCodec* videoCodec = player->m_MediaCodec;
AVPacket *packet = av_packet_alloc();
for(;;) {

....

ssize_t bufIdx = -1;
bufIdx = AMediaCodec_dequeueInputBuffer(videoCodec, 0);
if (bufIdx >= 0) {
size_t bufSize;
auto buf = AMediaCodec_getInputBuffer(videoCodec, bufIdx, &bufSize);
av_bitstream_filter_filter(player->m_Bsfc, player->m_VideoCodecCtx, NULL, &packet->data, &packet->size, packet->data, packet->size,
packet->flags & AV_PKT_FLAG_KEY);
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc 0x%02X 0x%02X 0x%02X 0x%02X \n",packet->data[0],packet->data[1],packet->data[2],packet->data[3]);
memcpy(buf, packet->data, packet->size);
AMediaCodec_queueInputBuffer(videoCodec, bufIdx, 0, packet->size, packet->pts, 0);
}
av_packet_unref(packet);
AMediaCodecBufferInfo info;
auto status = AMediaCodec_dequeueOutputBuffer(videoCodec, &info, 1000);
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc status: %d\n", status);
uint8_t* buffer;
if (status >= 0) {
SyncClock* videoClock = &player->m_VideoClock;
double presentationNano = info.presentationTimeUs * av_q2d(player->m_VideoTimeBase) * 1000;
videoClock->SetClock(presentationNano, GetSysCurrentTime());
player->AVSync();//音视频同步

size_t size;
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc sync video curPts = %lf", presentationNano);
buffer = AMediaCodec_getOutputBuffer(videoCodec, status, &size);
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc buffer: %p, buffer size: %d", buffer, size);
AMediaCodec_releaseOutputBuffer(videoCodec, status, info.size != 0);
} else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc output buffers changed");
} else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc output format changed");
} else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc no output buffer right now");
} else {
LOGCATI("HWCodecPlayer::VideoDecodeThreadProc unexpected info code: %zd", status);
}
if(isLocked) lock.unlock();
}

if(packet != nullptr) {
av_packet_free(&packet);
packet = nullptr;
}
LOGCATE("HWCodecPlayer::VideoDecodeThreadProc end");
}

视频向音频同步,delay 时间根据实际帧率与目标帧率的时间差进行微调。

void HWCodecPlayer::AVSync() {
LOGCATE("HWCodecPlayer::AVSync");
double delay = m_VideoClock.curPts - m_VideoClock.lastPts;
int tickFrame = 1000 * m_FrameRate.den / m_FrameRate.num;
LOGCATE("HWCodecPlayer::AVSync tickFrame=%dms", tickFrame);
if(delay <= 0 || delay > VIDEO_FRAME_MAX_DELAY) {
delay = tickFrame;
}
double refClock = m_AudioClock.GetClock();// 视频向音频同步
double avDiff = m_VideoClock.lastPts - refClock;
m_VideoClock.lastPts = m_VideoClock.curPts;
double syncThreshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
LOGCATE("HWCodecPlayer::AVSync refClock=%lf, delay=%lf, avDiff=%lf, syncThreshold=%lf", refClock, delay, avDiff, syncThreshold);
if(avDiff <= -syncThreshold) { //视频比音频慢
delay = FFMAX(0, delay + avDiff);
}
else if(avDiff >= syncThreshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD) { //视频比音频快太多
delay = delay + avDiff;
}
else if(avDiff >= syncThreshold)
delay = 2 * delay;

LOGCATE("HWCodecPlayer::AVSync avDiff=%lf, delay=%lf", avDiff, delay);

double tickCur = GetSysCurrentTime();
double tickDiff = tickCur - m_VideoClock.frameTimer;//两帧实际的时间间隔
m_VideoClock.frameTimer = tickCur;

if(tickDiff - tickFrame > 5) delay-=5;//微调delay时间
if(tickDiff - tickFrame < -5) delay+=5;

LOGCATE("HWCodecPlayer::AVSync delay=%lf, tickDiff=%lf", delay, tickDiff);
if(delay > 0) {
usleep(1000 * delay);
}
}


实现代码路径:

https://github.com/githubhaohao/LearnFFmpeg


-- END --


最近掘金社区发起了一个创作者投票活动, 没想到自己的几十篇文章帮助了这么多人,后面会再接再励输出更多干货。


小伙伴!你愿意为我投上几票吗?(每人可投 7~28 票)



推荐:

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

抖音传送带特效是怎么实现的?

所有你想要的图片转场效果,都在这了

我用 OpenGL ES 给小姐姐做了几个抖音滤镜

浏览 60
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报