这篇文章主要是分析视频播放器的实现代码。代码地址:查看

整体设计框架

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

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

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

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

整体大体类结构

  • 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。但是还是不太明白

音视频同步模块实现

音视频模块主要做的工作是维护解码线程、音视频同步。文件为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){//中断requestsynchronizer->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部分的实现细节没有去细扣,总之就是不明白就看到自己明白就梳理出了代码的思路。

Android端视频播放器源码分析相关推荐

  1. android在线视频播放器源码,请问谁有Android视频播放器简单源码

    qq_岁月永恒_0 2018-12-18 10:51 package com.example.mp4; import java.io.IOException; import com.example.m ...

  2. Android上百实例源码分析以及开源分析集合打包

    感谢网友banketree的收集,压缩包的内容如下: 1.360新版特性界面源代码 实现了360新版特性界面的效果,主要涉及到Qt的一些事件处理与自定义控件.但源码好像是c++. 2.aidl跨进程调 ...

  3. Android 系统(78)---《android framework常用api源码分析》之 app应用安装流程

    <android framework常用api源码分析>之 app应用安装流程 <android framework常用api源码分析>android生态在中国已经发展非常庞大 ...

  4. Android 双开沙箱 VirtualApp 源码分析(一)

    最近发现了一个非常好的开源项目,基本实现了一个 Android 上的沙箱环境,不过应用场景最多的还是应用双开. VA github: https://github.com/asLody/Virtual ...

  5. 一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇

    https://www.cnblogs.com/harlanc/p/9693983.html 目录 OpenSL ES & AudioTrack 源码分析 创建播放器音频输出对象 配置并创建音 ...

  6. Android录音下————AudioRecord源码分析

    Android录音下----AudioRecord源码分析 文章目录 Android录音下----AudioRecord源码分析 一.概述 1.主要分析点 2.储备知识 二.getMinBufferS ...

  7. 分布式定时任务—xxl-job学习(四)——调度中心web页面端api调用源码分析

    分布式定时任务-xxl-job学习(四)--调度中心web页面端api调用源码分析 前言 一.controller目录下非controller类 1.1 PermissionLimit自定义注解 1. ...

  8. 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇...

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  9. 搭建直播平台过程中Android端直播APP源码是如何实现连麦功能的?

    直播平台强大的变现能力是大家有目共睹的,很多开发商在搭建直播平台时为了增加用户黏性,纷纷将直播中加入连麦功能. 目前市场上通用的有两种连麦方案:本地混流和云端混流.本地混流即主播和连麦观众分别推一路流 ...

最新文章

  1. centos7 卸载Qt5
  2. Spring注解@Resource和@Autowired区别对比
  3. android.support.v7.app.actionbaractivity 报错
  4. Excel合并多个文件
  5. 【社招】量化研究员(机器学习)-Akuna Capital -上海
  6. 前端内容安全策略(csp)
  7. oracle 官网下载
  8. mac pro 2015 升级1T固态硬盘极简版本(三星970 evo plus)
  9. ios开发者证书申请
  10. design短语的用法总结_design的用法
  11. 使用Metasploit对MSSQL渗透测试步骤——学习笔记
  12. 舒亦梵:这十个小技巧,教你炒黄金长久获利
  13. 利用Java计算计算贷款的月支付金额和总偿还金额
  14. java属于高级语言_一文告诉你java是高级语言吗?
  15. Dockercompose创建redis主从复制
  16. ExoPlayer详解(官方文档-入门)
  17. 计算机培训日志小学,小学信息技术研修日志
  18. phpstudy-linux您修改了面板程序,这是不允许的 请进入服务器命令行输入xp查看修复方法 +解决方法
  19. 嵌入式常用网站一站式汇总
  20. 大数据时代,如何构建国家地质基础数据更新体系

热门文章

  1. 让CS的头像支持GIF动画
  2. 拥有一台服务器后,我竟然这么酷?
  3. Windows下谨慎使用动态磁盘
  4. fffffffffffffffffffffffffffffff
  5. Vant Icon 图标
  6. 关于笔记本键盘进水,之后需注意的要点以及关闭win10内置键盘的步骤
  7. 刚开始学Java,很懵逼,不知道怎么学,能不能给点建议?
  8. 暴涨375%超越ZOOM,Fastly靠“网络快递”成为华尔街新宠?
  9. 最火的android开源项目(三)
  10. 2.18 haas506 2.0开发教程 - 阿里云M2M设备间通信 - 规则引擎/Topic消息路由(仅支持2.2以上版本)