文章目录

  • 1. 模块分割
  • 2. 解码器实现
  • 3. 播放控制
  • 4. 音视频同步
  • 5. 总结

之前的博客中已经使用了FFmpeg进行音频文件的解码,并且基于OpenSLES实现了一个简单的音乐播放器。最近正在学习《音视频开发进阶指南》,看到了视频部分。不如就干脆再写一个视频播放器。代码存放在我的github:Android-VideoPlayer。

1. 模块分割

首先对这个视频播放器所采用的一些部件要清楚。这个播放器主要可以拆分为4个部分:

  1. 解码:FFmpeg
  2. 音频输出:OpenSLES
  3. 视频渲染:OpenGLES

这些框架都是基于C的api,因此这次我们的主要工作将会集中在NDK部分。而关于NDK的一些知识,之前的博客也有讲过,所以这个工程会是对之前知识的一次综合运用。

按照视频播放器的功能,我们将分出以下几个模块:

  1. 图像显示
  2. 音频输出
  3. 解码
  4. 播放控制
  5. 音视频同步

为了提高可移植性,对关键部件使用接口来规范其API接口。

1. IAudioPlayer:音频播放器接口。它规定的接口如下

class IAudioPlayer {public:virtual bool create() = 0;virtual void release() = 0;virtual void start() = 0;virtual void stop() = 0;virtual bool isPlaying() = 0;virtual void setAudioFrameProvider(IAudioFrameProvider *provider) = 0;virtual void removeAudioFrameProvider(IAudioFrameProvider *provider) = 0;
};

2. IVideoPlayer:视频播放接口。

class IVideoPlayer {public:virtual bool create() = 0;virtual void release() = 0;virtual void refresh() = 0;virtual void setVideoFrameProvider(IVideoFrameProvider *provider) = 0;virtual void removeVideoFrameProvider(IVideoFrameProvider *provider) = 0;virtual void setWindow(void *window) = 0;virtual void setSize(int32_t width, int32_t height) = 0;virtual bool isReady() = 0;
};

3. AudioFrame:存储解码好的音频数据。
对于播放器内部,播放的音频数据格式为16位PCM,44.1kHz采样率,双声道。为了避免每一段音频数据都要重新申请内存,我们将会复用AudioFrame,因此要给它设置一个最大音频数据存储空间。

struct AudioFrame{// present time stampint64_t pts;int16_t *data;int32_t sampleCount;int32_t maxDataSizeInByte = 0;AudioFrame(int32_t dataLenInByte){this->maxDataSizeInByte = dataLenInByte;pts = 0;sampleCount = 0;data = (int16_t *)malloc(maxDataSizeInByte);memset(data, 0, maxDataSizeInByte);}~AudioFrame(){if(data != NULL){free(data);}}
};

4. VideoFrame:存储解码好的视频数据:
对于播放器内部使用的视频数据格式,分辨率为1920*1080,像素格式RGB888,每种颜色一个字节,一个像素占3个字节。对于VideoFrame同样会复用。

struct VideoFrame
{int64_t pts;uint8_t *data;int32_t width;int32_t height;int32_t maxDataSizeInByte = 0;;VideoFrame(int32_t dataLenInByte){this->maxDataSizeInByte = dataLenInByte;data = (uint8_t *)malloc(maxDataSizeInByte);memset(data, 0, maxDataSizeInByte);}~VideoFrame(){if(data != NULL){free(data);}}
};

5. IAudioFrameProvider:面向IAudioPlayer的音频数据的提供源,它为IAudioPlayer提供解码好的音频数据
由于要复用AudioFrame,因此要设置一个接口,让IAudioPlayer将使用完的AudioFrame归还给我们。

class IAudioFrameProvider {public:virtual AudioFrame* getAudioFrame() = 0;virtual void putBackUsed(AudioFrame *data) = 0;
};

6. IVideoFrameProvider:和IAudioFrameProvider一样。

class IVideoFrameProvider {public:virtual VideoFrame* getVideoFrame() = 0;virtual void putBackUsed(VideoFrame* data) = 0;
};

7. IMediaDataReceiver:用于接收解码好的音视频数据的接口。
它是用来维护并存储已经解码好的音视频数据和使用过的音视频数据。

class IMediaDataReceiver {public:virtual void receiveAudioFrame(AudioFrame *audioData) = 0;virtual void receiveVideoFrame(VideoFrame *videoData) = 0;virtual AudioFrame* getUsedAudioFrame() = 0;virtual VideoFrame* getUsedVideoFrame() = 0;virtual void putUsedAudioFrame(AudioFrame *audioData) = 0;virtual void putUsedVideoFrame(VideoFrame *videoData) = 0;
};

8. BlockRecyclerQueue:同步复用队列。
c++内并没有线程安全的队列模型。因此我们自己实现一个。并且由于播放器内很多的数据都会需要复用,因此给这个队列加一个复用功能。这样,这个类内部会有两个队列,一个存储未使用的数据,一个存储已使用的数据。使用两把锁,分别对两个队列进行线程保护。当然,实际上你也可以以更小的粒度来考虑这件事,只要使用一个队列,然后对队列进行线程保护即可,至于里面存储的到底是用过的数据还是没用过的数据,完全可以由上层来决定。

播放器中的多线程都使用c++11自带的thread。

这个同步复用队列实际上就是生产者消费者模式中的管道。它有以下几个特点:

  1. 如果设置capacity=-1,那么这个队列是不限大小的。如果限制了大小,当内部存储的数据满的时候,put操作就会等待,这是为了防止解码器过快导致内存占用过高。
  2. 对于get操作和put操作,你可以通过设置wait来决定当数据空或满的时候是否等待。对于get操作,队列空时,如果wait = true,那么它就会一直等待直到有数据;如果wait = false,那么它就会立刻返回NULL。对于put操作,队列满时,如果wait = true,它就会一直等待到队列不满;如果wait = false,那么它就不会顾及capacity,而直接向队列中存储,导致size > capacity。
  3. 为了防止播放结束时发生死锁,设置两个接口来解除所有的get和put操作的等待。这一点考虑到解码器解码完毕后,播放器却一直等待。
  4. 以上所有情况都是是对于有用的数据。而对于回收数据队列,所有的put和get操作只保证线程安全,而不会等待。它没有最大容量,所有的put操作都会在得到锁之后立刻执行。所有的get操作也会在得到线程锁之后立刻执行,如果没有回收数据,立刻返回NULL。
  5. 通过discardAll(void (*discardCallback)(T))方法可以将所有的有用数据一次性放到回收数据中,并且还可以传递一个函数指针,对所有的有用数据进行回收处理,之后再放入回收队列。这是为了seek操作考虑的,因为seek时要放弃所有已经解码好的数据。
template <class T>
class BlockRecyclerQueue {public:// if size == -1, then we don't limit the size of data queue, and all the put option will not wait.BlockRecyclerQueue(int capacity = -1);~BlockRecyclerQueue();int getCapacity();int getSize();// put a element, if wait = true, put option will wait until the length of data queue is less than specified size.void put(T t, bool wait = true);// get a element, if wait = true, it will wait until the data queue is not empty. If wait = false, it will return NULL if the data queue is empty.// It will still return NULL even wait = true, in this case, it must be someone call notifyWaitGet() but the data queue is still empty.T get(bool wait = true);void putToUsed(T t);T getUsed();void discardAll(void (*discardCallback)(T));// notify all the put option to not wait. This will cause put option succeed immediatelyvoid notifyWaitPut();// notify all the get option to return immediately. if data queue is still empty, get option will return a NULL.void notifyWaitGet();private:int capacity = 0;mutex queueMu;mutex usedQueueMu;condition_variable notFullSignal;condition_variable notEmptySignal;list<T> queue;list<T> usedQueue;bool allowNotifyPut = false;bool allowNotifyGet = false;};

2. 解码器实现

解码部分还是使用FFmpeg。解码过程和解码音频过程大同小异。

首先,我们肯定需要两个线程来分别解码音频和视频。

其次,还需要一个线程来读取文件,之前我们在解码音频时将从文件中读取packet将packet解码为frame的过程放在同一个线程中执行,因为音频文件我们只关注音频流。现在我们要将读packet这个操作单独放在一个线程里,然后解码器要维护两个队列,来分别存放音频的AVPacket和视频的AVPacket,这两个队列就可以使用之前的BlockRecyclerQueue。这相当于,读文件线程是生产者,而音频解码线程和视频解码线程都是消费者。具体代码可以查看VideoFileDecoder.cpp

需要注意的是,seek操作也是放在解码器中进行的,因为seek需要对媒体文件进行操作。在seek时,同样要将之前所有已经读出的AVPacket抛弃。

由于文件解码出的编码格式会不一样,因此我们需要FFmpeg的swr_convert来转码音频数据,用sws_scale转码视频数据。

3. 播放控制

我向外提供了一个播放器的统一操作接口:VideoPlayController.cpp,同时它还负责通知上层播放进度、管理音视频播放器和解码器、管理已解码好的数据等。因此它的声明如下:

class VideoPlayController: public IMediaDataReceiver, public IAudioFrameProvider, public IVideoFrameProvider

它实现了三个接口,可以接受解码器解码好的数据,并且向音视频播放器分别提供音频数据和视频数据。

4. 音视频同步

由于通常音频帧率要比视频帧率高很多,一般视频中的音频采样率多为44.1kHz或48kHz,而视频一般是25fps。

音视频同步通常有两种方式:

  1. 以音频时间基准播放视频,这是由于音频帧率更高。
  2. 以额外的时钟对音视频进行同步。

一般来说,额外时钟的方式会更好一些,一是因为它的精度高;二是这样一来,如果出现文件中只有视频或者只有音频的情况,适用性也会更高些;三是如果你的音频播放器不是主动请求音频数据的,那么你无论如何都需要一个额外时钟来向音频播放器和视频播放器定时发送数据。不过它的缺点在于多占资源。

我这里使用的是以音频时间为基准,因为OpenSLES是主动请求音频数据的。这样一来每次音频播放器请求数据时,我们可以拿到当前AudioFrame的pts,就可以得知当前的播放进度,也可以以这个播放进度来判断是否向视频播放器发送刷新指令。

自然而然,播放和暂停功能也是通过控制音频播放器的播放暂停来实现的。

音视频同步也放在VideoPlayController.cpp中。音视频同步部分的代码放在AudioFrame *VideoPlayController::getAudioFrame()方法中。

5. 总结

至此,这个播放器的关键部分就理清了。代码请上我的github上查看,链接在博客顶部。不过它仍然有很多问题:

  1. 某些情况下,退出视频播放会ANR,可能是某个线程进入了死锁或者死等待。
  2. 现在只能正常播放分辨率较低的视频,因为没有针对硬件加速做优化,导致解码视频过于耗时。测试得出解码一帧1920*1080的视频解码需要差不多70ms。

基于FFmpeg的简单Android视频播放器相关推荐

  1. 从零开始仿写一个抖音App——基于FFmpeg的极简视频播放器

    本文首发于微信公众号--世界上有意思的事,搬运转载请注明出处,否则将追究版权责任.微信号:a1018998632,交流qq群:859640274 1.从零开始仿写一个抖音app--开始 4.从零开始仿 ...

  2. android基于ffmpeg的简单视频播发器 跳到指定帧 av_seek_frame()

    跳到指定帧,在ffmpeg使用av_seek_frame()进行跳转,这个函数只能跳到关键帧,所以对关键帧时间差距比较大的视频很尴尬,总是不能调到想要的画面 还有av_seek_frame中的时间参数 ...

  3. 基于NDK、C++、FFmpeg的android视频播放器开发实战-夏曹俊-专题视频课程

    基于NDK.C++.FFmpeg的android视频播放器开发实战-1796人已学习 课程介绍         课程包含了对流媒体(拉流)的播放,演示了播放rtmp的香港卫视,支持rtsp摄像头和ht ...

  4. 视频教程-基于NDK、C++、FFmpeg的android视频播放器开发实战-Android

    基于NDK.C++.FFmpeg的android视频播放器开发实战 夏曹俊:南京捷帝科技有限公司创始人,南京大学计算机硕士毕业,有15年c++跨平台项目研发的经验,领导开发过大量的c++虚拟仿真,计算 ...

  5. 基于ffmpeg+opengl+opensl es的android视频播放器

    最近做了一个android视频播放器,在jni中采用c/c++现了播放器的播放,暂停,快进等基本的播放器功能. 使用开源库FFMpeg来解码,得到音视频数据,FFMPEG是一个功能强大的音视频解码,编 ...

  6. 最简单的基于FFMPEG+SDL的音频播放器 ver2 (采用SDL2.0)

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

  7. 视频教程-FFmpeg+OpenGL ES+OpenSL ES打造Android视频播放器-Android

    FFmpeg+OpenGL ES+OpenSL ES打造Android视频播放器 从事Android移动端开发多年.主导开发过直播.电商.聊天等各种类型APP和游戏SDK:熟悉Android音视频开发 ...

  8. 最简单的基于DirectShow的示例:视频播放器自定义版

    ===================================================== 最简单的基于DirectShow的示例文章列表: 最简单的基于DirectShow的示例:视 ...

  9. 最简单的基于DirectShow的示例:视频播放器图形界面版

    ===================================================== 最简单的基于DirectShow的示例文章列表: 最简单的基于DirectShow的示例:视 ...

  10. 最简单的基于FFMPEG+SDL的音频播放器

    ===================================================== 最简单的基于FFmpeg的音频播放器系列文章列表: <最简单的基于FFMPEG+SDL ...

最新文章

  1. Grunt的配置和使用
  2. 小猿圈Java学习心得之Java程序员能力提升在哪
  3. hashmap 循环取出所有值 取出特定的值 两种方法
  4. java 当前时间小时数,java获取当前时间前几个小时的时间
  5. android ExpandableListView
  6. Python编程学习笔记:列表
  7. openstack虚拟机支持USB 重定向(usb映射)
  8. 百度离线语音合成SDK使用
  9. Endnote导入中文文献格式
  10. 云服务器发现安全漏洞怎么解决?
  11. veu——引入iconfont图标
  12. 2022第十七届巴拿马春晚-113万海内外观众欢聚迎新春
  13. [线段树]打字练习记录
  14. 如何用计算机弹出斗地主的声音,玩斗地主没声音电脑瞎出牌。我点的没有.怎么办?...
  15. luckysheet 国产超强纯前端在线excel表格功能强大 简单使用记录 异常报错记录及处理
  16. linux图形界面没有输入法,fcitx 输入法看不到选词,上面键盘也不见了!
  17. 嵌入式之uboot源码分析-启动第二阶段学习笔记(下篇)
  18. mongo如何删除数据后相应的删除空间和内存占用
  19. scp 命令私钥下载
  20. 需求调研报告模板_中国脂肪醇市场需求调研与十四五投资战略规划分析报告2021-2026年...

热门文章

  1. 中文简历表格提取,手写汉字识别(Python+OpenCV)
  2. 乱OL, Ran OL[Ran2_Online]加解密工具源码
  3. html5 牧场游戏,手机QQ首批五款HTML5游戏名单 农场偷菜复活
  4. Mac 远程桌面 Windows 快捷键
  5. 随便谈谈alphago与人机大战
  6. 量子统计巨正则系综应用理想费米气体与波色气体性质详解
  7. 卸载McAfee for Mac
  8. 清华计算机学院教师名单,清华大学计算机科学与技术系导师教师师资介绍简介-艾海舟...
  9. html5音乐加大音量,怎么调大音乐声音 mp3音量增大器介绍【图解】
  10. 前装车载导航搭载率突破50%,谁在领跑背后的导航引擎