这篇文章主要是分析视频播放器的实现代码。代码地址:查看
<https://github.com/zhanxiaokai/Android-as_video_player>

整体设计框架

*
我们播放本地的视频文件需要封装出一个输入模块:
输入模块要开启一个线程来处理解封装和解码,把得到的裸数据放到音频和视频的队列中。

*
输出模块:
输出为音频的输出我们可以听到的声音,视频的输出,我们可以看到的画面。音频和视频的输出通过单独的线程来管理。

*
音视频同步

因为输出模块都在单独播放,没办法保证音画对齐播放,引入这个模块来解决这个问题,并且它包含输入模块、输出模块、音视频队列,然后给外界提供音视频数据的接口,这个接口保证了音视频的同步问题。

*
对外调度器
这个模块集成音视频同步模块、音视频输出模块,保证他们直接的数据正确输出,并且对外提供播放的控制接口。

整体大体类结构





* AudioOutput:音频输出模块,音频渲染放在一个单独的线程,在运行过程中通过注册过来的回掉函数获取音频数据
* VideoOutput:视频输出模块,使用OpenGL ES来渲染视频,开启子线程运行OpenGL ES的渲染,在运行过程中注册回掉函数来获取视频数据
*
AVSynchronizer:音视频同步模块,为VideoPlayerController调度器提供接口,包括开始、结束、以及获取音频数据和获取对应时间戳的视频帧。还维护一个解码线程。
* AudioFrame:音频帧,记录音频的数据格式以及具体数据、时间戳等信息
* VideoFrame:视频帧,记录视频格式以及具体的数据、宽、高、以及时间戳等信息
*
AudioFrameQueue:音频队列,存储音频帧,由于解码线程和音频播放线程作为生产者和消费者同时访问队列中的元素,它为客户端代码音视频同步模块提供压入和弹出操作,保证线程安全性
* VideoFrameQueue:视频队列,存储视频帧,功能同上处理视频。
* VideoDecoder:输入解码模块,它主要想同步模块提供接口:打开文件资源、关闭文件资源、解码出一定时间长度的音视频帧
音视频输入模块实现

主要针对VideoDecoder类的实现。其使用FFmpeg来处理协议解析,封装格式拆分,解码操作等。

* 打开文件openFile
* decodeFrames循环读取数据进行解封装、解码、处理数据
* startUploader注册对外的数据解码好的数据回掉
* 释放资源
主要的功能函数如上所述


在解码的时候很容易的看到音频数据直接进入av_synchronizer.cpp的音频队列audioFrameQueue,而视频数据通过上面的回掉到circleFrameTextureQueue中的
音视频输出模块实现

这个模块主要由audio_output.cpp负责音频的输出,video_output.cpp负责视频的输出。

音频输出

音频输出参考前篇的使用OpenSL ES进行输出音频。主要类实现audio_output.cpp。

* video_player_controller.cpp中初始化AudioOutput类传入获取数据的回掉函数
* audio_output.cpp中registerPlayerCallback播放完成时从上面的传入回掉函数中获取音频数据并放入自己的队列中
* audio_output.cpp中提供播放的控制:start、play、pause、stop




视频输出

封装在类文件video_output.cpp中。使用OpenGL ES进行渲染视频画面,开启一个单独的线程来渲染视频帧。

数据的处理方式和音频类似,通过回掉来获取要渲染的视频帧数据。看源码分析既可,但是在看源码的时候在video_output.cpp中有一个函数的实现有点没搞明白:
bool VideoOutput::renderVideo() { FrameTexture* texture = NULL;
produceDataCallback(&texture, ctx, forceGetFrame); if (NULL != texture && NULL
!= renderer) { // LOGI("VideoOutput::renderVideo() "); eglCore->
makeCurrent(renderTexSurface); renderer->renderToViewWithAutoFill(texture->
texId, screenWidth, screenHeight, texture->width, texture->height); if (!eglCore
->swapBuffers(renderTexSurface)) { LOGE("eglSwapBuffers(renderTexSurface)
returned error %d", eglGetError()); } } if(forceGetFrame){ forceGetFrame = false
; }return true; }

这里eglCore和renderer没有发现在哪里绑定了,那么这个renderer的数据是如何渲染到EGL的display到我们的View上面的呢?这里有明白的可以指点一下,没有找到合适的答案,看到了一篇实现和这里相同的答案:

https://stackoverflow.com/questions/30061753/drawing-on-multiple-surfaces-by-using-only-eglsurface

<https://stackoverflow.com/questions/30061753/drawing-on-multiple-surfaces-by-using-only-eglsurface>
。但是还是不太明白

音视频同步模块实现

音视频模块主要做的工作是维护解码线程、音视频同步。文件为av_synchronizer.cpp
维护解码线程在上面有提及它的主要交接函数。开启解码线程的代码为
void AVSynchronizer::decode(){ //
todo:这里有可能isSeeking开始false,但是期间执行了seekCurrent代码,变成true,就在这里wait住不动了。。。因为音频停止 //
先假定seek之前已经pause了 // LOGI("before pthread_cond_wait"); pthread_mutex_lock(&
videoDecoderLock); pthread_cond_wait(&videoDecoderCondition,&videoDecoderLock);
pthread_mutex_unlock(&videoDecoderLock); // LOGI("after pthread_cond_wait");
isDecodingFrames= true; decodeFrames(); isDecodingFrames = false; }
这个方法会循环,每次循环完会wait住,当有signal信号来再进行解码。发出信号的代码为
void AVSynchronizer::signalDecodeThread() { if (NULL == decoder ||
isDestroyed) { LOGI("NULL == decoder || isDestroyed == true"); return; }
//如果没有剩余的帧了或者当前缓存的长度大于我们的最小缓冲区长度的时候,就再一次开始解码 bool
isBufferedDurationDecreasedToMin = bufferedDuration <= minBufferedDuration ||
(circleFrameTextureQueue->getValidSize() <= minBufferedDuration*getVideoFPS());
if (!isDestroyed && (decoder->hasSeekReq()) || ((!isDecodingFrames) &&
isBufferedDurationDecreasedToMin)) {int getLockCode =
pthread_mutex_lock(&videoDecoderLock);
pthread_cond_signal(&videoDecoderCondition);
pthread_mutex_unlock(&videoDecoderLock); } }
音视频同步

音视频同步方法:

*
音频向视频对齐

依据视频帧的时间戳来渲染音频帧,渲染视频帧的时候不用管,渲染音频帧的时候和视频帧的时间戳进行比较,这个差值如果不在阀值范围内,就要进行对齐。对齐操作的主要是音频帧,如果音频帧的时间戳比视频帧的时间戳小,那就要进行跳帧操作,就像快进操作;如果音频帧的时间戳比视频帧的时间戳大,那就要等待视频帧的播放,实现方法为填充空的音频数据进行播放,当视频帧赶上来了就播放当前的音频帧数据。
优点:视频的每一帧都可以让用户看到,看上去很流畅
缺点:音频会出现丢帧或者空帧静音

*
视频向音频对齐
这个和上面的相反

*
统一向外部时钟对齐

外部提供一个时钟,获取音视频帧的数据的时候,和外部的时钟来进行对齐,如果没有超过阀值,就直接返回音视频帧,如果超过了就进行上述的对齐操作,音频和视频和外部时钟各自比较。

人的耳朵比眼睛要敏感很多,如果音频有跳帧或者空帧,我们很容易察觉;而视频有跳帧或者重复帧,眼睛不容易分辨出来。该项目使用的是视频向音频对齐。

在获取到音频帧的时候,来比对视频的主要代码如下:
FrameTexture* AVSynchronizer::getCorrectRenderTexture(bool forceGetFrame) {
FrameTexture *texture = NULL; if (!circleFrameTextureQueue) { LOGE(
"getCorrectRenderTexture::circleFrameTextureQueue is NULL"); return texture; }
int leftVideoFrames = decoder->validVideo() ?
circleFrameTextureQueue->getValidSize() :0; if (leftVideoFrames == 1) { return
texture; } while (true) { int ret = circleFrameTextureQueue->front(&texture); if
(ret >0){ if (forceGetFrame) { return texture; } const float delta =
(moviePosition - DEFAULT_AUDIO_BUFFER_DURATION_IN_SECS) -texture->position; if
(delta < (0 - syncMaxTimeDiff)) { //视频比音频快了好多,我们还是渲染上一帧 //
LOGI("视频比音频快了好多,我们还是渲染上一帧 moviePosition is %.4f texture->position is %.4f",
moviePosition, texture->position); texture = NULL; break; }
circleFrameTextureQueue->pop();if (delta > syncMaxTimeDiff) {
//视频比音频慢了好多,我们需要继续从queue拿到合适的帧 // LOGI("视频比音频慢了好多,我们需要继续从queue拿到合适的帧
moviePosition is %.4f texture->position is %.4f", moviePosition,
texture->position); continue; } else { break; } } else{ texture = NULL; break;
} }return texture; }
集合控制系统

实现在video_player_controller.cpp中。

初始化

初始化包含音频播放器初始化和视频渲染界面的初始化。下面通过图片来描述这个初始化过程
音频初始化图解:




视频初始化图解:




运行

初始化开启了音频输出模块,OpenGL
ES播放完缓冲区的音频数据后,回掉到video_palyer_controller.cpp填充数据。填充数据方法有一些列判断,最后都不满足则调用同步模块的音频数据填充方法,填充了音频数据以后,向视频输出发送指令,让它更新画面,当视频输出收到方法以后则回掉到控制系统获取视频帧数据,控制系统调用同步模块获取视频帧返回给视频播放模块。整个运行都是音频播放来驱动的,所以播放暂停等操作只要控制音频播放模块即可。

销毁

销毁过程为创建的逆过程,主要代码如下:
void VideoPlayerController::destroy() { LOGI("enter
VideoPlayerController::destroy..."); userCancelled = true; if (synchronizer){
//中断request synchronizer->interruptRequest(); } pthread_join(initThreadThreadId,
0); if (NULL != videoOutput) { videoOutput->stopOutput(); delete videoOutput;
videoOutput= NULL; } if (NULL != synchronizer) { synchronizer->isDestroyed =
true; this->pause(); LOGI("stop synchronizer ..."); synchronizer->destroy();
LOGI("stop audioOutput ..."); if (NULL != audioOutput) { audioOutput->stop();
delete audioOutput; audioOutput= NULL; } synchronizer->clearFrameMeta(); delete
synchronizer; synchronizer= NULL; } if(NULL != requestHeader){ requestHeader->
destroy(); delete requestHeader; requestHeader= NULL; } LOGI("leave
VideoPlayerController::destroy..."); }
总结


整个项目的代码看了几天,弄清了里面的一个整体的逻辑,其中的实现细节有很多值得学习。其中的Opegl部分的实现细节没有去细扣,总之就是不明白就看到自己明白就梳理出了代码的思路。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信