手机怎样做刷赞网站,wordpress替换文章标题内容,wordpress如何加入会员登陆,广西壮族自治区有几个市本篇作为 Android 音视频实战系列的第二篇文章#xff0c;主要介绍视频解码与渲染过程。本系列文章目录如下#xff1a; Android 音视频基础知识 Android 音视频播放器 Demo#xff08;一#xff09;—— 视频解码与渲染 Android 音视频播放器 Demo#xff08;二#xff…本篇作为 Android 音视频实战系列的第二篇文章主要介绍视频解码与渲染过程。本系列文章目录如下 Android 音视频基础知识 Android 音视频播放器 Demo一—— 视频解码与渲染 Android 音视频播放器 Demo二—— 音频解码与音视频同步 RTMP 直播推流 Demo一—— 项目配置与视频预览 RTMP 直播推流 Demo二—— 音频推流与视频推流 1、项目概述
1.1 项目配置
FFmpeg 的交叉编译我们在前面介绍过这里就不再赘述了有需要可以去参考NDK 编译二—— NDK 编译与集成 FFmpeg。
这里主要介绍 FFmpeg 的环境配置分三步 FFmpeg 编译产物的静态库6 个 .a 文件复制到 libs/armeabi-v7a 下include 文件夹复制到 src/main/cpp 目录下 更改 app 模块下的 build.gradle 文件添加 abiFilter 只编译 arm-v7a android {defaultConfig {externalNativeBuild {cmake {abiFilters armeabi-v7a}}ndk {abiFilters armeabi-v7a}}
}修改 CMakeLists.txt # 定义源文件
file(GLOB sources *.cpp)# 定义 FFmpeg 路径
set(FFMPEG ${CMAKE_SOURCE_DIR}/ffmpeg)# 导入 FFmpeg 头文件
include_directories(${FFMPEG}/include)# 添加 FFmpeg 库文件路径到编译标记中
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -L${FFMPEG}/lib/${CMAKE_ANDROID_ARCH_ABI})add_library(video-playerSHARED${sources})target_link_libraries(video-player# FFmpeg 源码编译出的 6 个静态库avcodec avfilter avformat avutil swresample swscalelogz# 在 Native 进行视频渲染时要用到 ANativeWindowandroid# 在 Native 进行音频播放所需的库OpenSLES)在 cmake 块中的 abiFilters 用于指定 CMake 构建系统编译和构建的 ABI。例如如果在 abiFilters 中设置为 “armeabi-v7a”则 CMake 将只为 armeabi-v7a 架构编译和构建本机代码。 类似地在 ndk 块中的 abiFilters 用于指定 NDK 构建系统编译和构建的 ABI。如果在 abiFilters 中设置为 “armeabi-v7a”则 NDK 将只为 armeabi-v7a 架构编译和构建本机代码。 1.2 Demo 结构 视频播放器 Demo 可以分为上下两层
上层主要是 UI 方面的提供 SurfaceView 进行视频渲染。此外还需要根据生命周期调用 Native 方法控制底层的播放Native 层Native 层接收上层发来的播放指令还需要通过 CallbackHelper 通知上层播放状态。此外Native 层需要抽离出一个控制层对音视频解码线程进行控制接收解码的数据后要渲染到屏幕/麦克风上
Native 控制层示意图如下 控制层的主要作用
初始化 FFmpeg 参数控制播放进度播放、停止、控制播放速度等从视频文件视频流中解析出 AVPacket 存入视频/音频队列
可以看到音视频各有一个保存 AVPacket 的队列由于 AVPacket 是压缩数据我们需要从队列中取出 AVPacket 解压为 AVFrame 再存入队列因此 AVFrame 也是有一个队列的 视频层作用
不断地从 Packet 队列中取出 AVPacket 解压为 AVFrame 后存入 AVFrame 队列。这是通过死循环进行的耗时操作因此需要放入特定的解压线程中操作不断地从 AVFrame 队列中取出 AVFrame 放入播放线程的 buffer 中最终要回到控制层将 AVFrame 渲染到屏幕上
音频层类似 解压后的音频数据通过 OpenSLES 进行播放。 AudioTrack 底层实际上也是使用的 OpenSLES。 最后来介绍一下实现步骤
准备阶段 实现 Native 反射调用上层的机制 JNICallbackHelper初始化 FFmpeg 解码器 视频解码 创建一个同步队列 SafeQueue 用于承载 AVPacket 和 AVFrame 数据创建专门处理视频解码工作的通道 VideoChannel读取 AVPacket 并解码为 AVFrame 视频渲染 将上层 SurfaceView 的 Surface 传给 Native 控制层设置好 Native 层的窗口对象 ANativeWindow将 VideoChannel 解码后的帧数据回调给 Native 控制层渲染在 ANativeWindow 上 音频解码与渲染 创建专门处理音频解码工作的通道 AudioChannel 进行音频解码具体方式与视频解码几乎一致将解码后的音频数据交给 OpenSLES 进行播放创建一个通道的基类 BaseChannel 用于定义视频通道 VideoChannel 和音频通道 AudioChannel 的共同操作 音视频同步添加进度条与播放时间
2、准备阶段
准备阶段的主要工作是打开 FFmpeg 的解码器。在这个过程中我们需要建立 Native 回调上层方法的机制 JNICallbackHelper这样 Native 才能将播放器的准备状态、播放状态通知给上层。
2.1 代码框架
简单说一下代码结构
Activity 布局主要有一个负责渲染视频的 SurfaceView 和控制播放进度的 SeekBar对视频的控制都通过 VideoPlayer 类完成VideoPlayer 是上层与 Native 交互的桥梁定义了很多控制播放的 Native 方法此外还有 Native 为了通知播放器状态要回调的方法Native 层的入口在 native-lib负责创建 Native 层的 VideoPlayer 并将上层的请求转交给它Native 的 VideoPlayer 负责 FFmpeg 解码器的创建以及相关操作还要把视频流和音频流交给对应的通道进行解码处理
还是先从 Activity 开始布局如下
?xml version1.0 encodingutf-8?
LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:toolshttp://schemas.android.com/toolsandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:orientationverticaltools:context.MainActivitySurfaceViewandroid:idid/surfaceViewandroid:layout_widthmatch_parentandroid:layout_height200dp /!-- 进度条 --LinearLayoutandroid:layout_widthmatch_parentandroid:layout_height30dpandroid:layout_margin5dpTextViewandroid:idid/tv_timeandroid:layout_widthwrap_contentandroid:layout_heightmatch_parentandroid:gravitycenterandroid:textstring/init_timeandroid:visibilitygone /SeekBarandroid:idid/seekBarandroid:layout_width0dpandroid:layout_heightmatch_parentandroid:layout_weight1android:max100android:visibilitygone //LinearLayout
/LinearLayout代码端命令 VideoPlayer 执行准备工作
class MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingprivate lateinit var videoPlayer: VideoPlayeroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 设置屏幕常亮window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)binding ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)checkPermissionAndFile()videoPlayer VideoPlayer()videoPlayer.setOnPreparedListener(object : VideoPlayer.OnPreparedListener {override fun onPrepared() {runOnUiThread {Toast.makeText(thisMainActivity, 准备就绪, Toast.LENGTH_LONG).show()}}})videoPlayer.setOnErrorListener(object : VideoPlayer.OnErrorListener {override fun onError(errorMsg: String) {runOnUiThread {Toast.makeText(thisMainActivity, errorMsg, Toast.LENGTH_LONG).show()}}})// 准备工作videoPlayer.prepare(file_path)}
}VideoPlayer 将准备工作转交给 Native 层同时还为外界提供了播放器准备就绪的监听器 OnPreparedListener 和发生错误的监听器 OnErrorListener
class VideoPlayer {private lateinit var surfaceHolder: SurfaceHolderprivate var onPreparedListener: OnPreparedListener? nullprivate var onErrorListener: OnErrorListener? nullfun setSurfaceHolder(surfaceHolder: SurfaceHolder) {this.surfaceHolder surfaceHolder}/*** 准备工作让 Native 层对解码器进行初始化*/fun prepare(dataSource: String) {nativePrepare(dataSource)}/*** 供 Native 回调上层通知解码器准备就绪的方法*/fun onPrepared() {onPreparedListener?.onPrepared()}/*** 供 Native 回调上层通知解码器初始化发生错误的方法*/fun onError(errorCode: Int) {onErrorListener?.onError(getMsgFromCode(errorCode))}private fun getMsgFromCode(errorCode: Int): String when (errorCode) {Constants.FFMPEG_CAN_NOT_OPEN_URL - 打不开视频Constants.FFMPEG_CAN_NOT_FIND_STREAMS - 找不到流媒体Constants.FFMPEG_FIND_DECODER_FAIL - 找不到解码器Constants.FFMPEG_ALLOC_CODEC_CONTEXT_FAIL - 无法根据解码器创建上下文Constants.FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL - 根据流信息配置上下文参数失败Constants.FFMPEG_OPEN_DECODER_FAIL - 打开解码器失败Constants.FFMPEG_NO_MEDIA - 没有音视频else - 未知错误}fun setOnPreparedListener(onPreparedListener: OnPreparedListener) {this.onPreparedListener onPreparedListener}fun setOnErrorListener(onErrorListener: OnErrorListener) {this.onErrorListener onErrorListener}private external fun nativePrepare(dataSource: String)interface OnPreparedListener {fun onPrepared()}interface OnErrorListener {fun onError(errorMsg: String)}
}在 Native 层的入口也是控制层 native-lib.cpp 中创建 nativePrepare() 对应的 Native 函数
extern C
JNIEXPORT void JNICALL
Java_com_video_player_VideoPlayer_nativePrepare(JNIEnv *env, jobject thiz, jstring data_source) {// 创建 Native 层的 VideoPlayer 并将准备工作交给它
}VideoPlayer 执行准备工作时需要将结果通知给上层因此到这里我们先来看 JNICallbackHelper 的实现。
2.2 JNICallbackHelper
JNICallbackHelper 是一个在 Native 层调用上层方法的帮助类在进行解码器初始化时需要通过它告知上层解码器的初始化状态。
首先我们要了解Native 如何调用上层方法。实际上跟 Java/Kotlin 反射类似
获取到上层方法所在的类对象 jclass根据上层方法的名字和签名获取该方法的 jmethodID调用 JNI 提供的函数 JNIEnv-CallVoidMethod(jclass,jmethodID,methodArgs) 就可调用上层方法了
在上层的 VideoPlayer 中提供了 onPrepared() 和 onError() 供 Native 通知解码器初始化完成或者发生了错误
class VideoPlayer {/*** 供 Native 回调上层通知解码器准备就绪的方法*/fun onPrepared() {onPreparedListener?.onPrepared()}/*** 供 Native 回调上层通知解码器初始化发生错误的方法*/fun onError(errorCode: Int) {onErrorListener?.onError(getMsgFromCode(errorCode))}
}为了帮助 Native 回调 onPrepared() 和 onError()JNICallbackHelper 可以这样实现
JNICallbackHelper::JNICallbackHelper(JavaVM *jvm, JNIEnv *jEnv, jobject jObj) {javaVM jvm;jniEnv jEnv;// jobject 默认作用域就在当前函数内不能跨越线程和函数必须声明为全局引用才可以jObject jEnv-NewGlobalRef(jObj);// 反射获取上层方法对象需要方法所在的类对象jclass clazz jEnv-GetObjectClass(jObject);// 获取要反射的方法 ID实际上是拿到了方法的 ArtMethod 结构体onPreparedId jEnv-GetMethodID(clazz, onPrepared, ()V);onErrorId jEnv-GetMethodID(clazz, onError, (I)V);
}/*** 释放成员从作用域小的开始释放*/
JNICallbackHelper::~JNICallbackHelper() {if (jObject) {jniEnv-DeleteGlobalRef(jObject);jObject nullptr;}if (jniEnv) {delete jniEnv;jniEnv nullptr;}if (javaVM) {delete javaVM;javaVM nullptr;}
}/*** 回调上层的 onPrepared()通知 Native 这边已经完成了* 解码器初始化*/
void JNICallbackHelper::onPrepared(int thread_mode) {if (thread_mode MAIN_THREAD) {// 在主线程中可以直接使用主线程的 JNIEnv 调用上层方法jniEnv-CallVoidMethod(jObject, onPreparedId);} else {// 在子线程中需要先获取子线程的 JNIEnv 再调用上层方法JNIEnv *childEnv;javaVM-AttachCurrentThread(childEnv, nullptr);childEnv-CallVoidMethod(jObject, onPreparedId);javaVM-DetachCurrentThread();}
}/*** 回调上侧的 onError()通知上层在初始化解码器时发生了错误* param thread_mode 运行在主线程还是子线程中* param error_code 错误码上层根据不同的错误码返回响应的提示*/
void JNICallbackHelper::onError(int thread_mode, int error_code) {if (thread_mode MAIN_THREAD) {// 在主线程中可以直接使用主线程的 JNIEnv 调用上层方法jniEnv-CallVoidMethod(jObject, onErrorId, error_code);} else {// 在子线程中需要先获取子线程的 JNIEnv 再调用上层方法JNIEnv *childEnv;javaVM-AttachCurrentThread(childEnv, nullptr);childEnv-CallVoidMethod(jObject, onErrorId, error_code);javaVM-DetachCurrentThread();}
}你能看到在 onPrepared() 和 onError() 会对所在线程加以区分这是因为初始化解码器是耗时操作要放在子线程中执行而 JNIEnv 是与线程绑定的不同线程的 JNIEnv 不同因此在子线程中执行时需要切换到子线程的 JNIEnv 再执行 CallVoidMethod()。
类似的情况还有 jobject它不仅不能跨越线程还不能跨越函数因此在 JNICallbackHelper 的构造函数中是将其声明为全局变量后才保存到成员变量中而 JavaVM 作为全局唯一的表示虚拟机对象的变量它的作用域最大可以跨越线程需要通过固定函数获取它
JavaVM *javaVm nullptr;/*** 获取全局的 JavaVm*/
jint JNI_OnLoad(JavaVM *jvm, void *args) {javaVm jvm;return JNI_VERSION_1_6;
}有了它我们就可以在 native-lib 中创建 JNICallbackHelper 对象然后在初始化解码器时使用它。
2.3 初始化解码器
上层的 VideoPlayer 提供 prepare() 供外界发出初始化解码器的请求然后通过 Native 方法把这个请求转发到 Native 层 /*** 准备工作让 Native 层对解码器进行初始化*/fun prepare(dataSource: String) {nativePrepare(dataSource)}private external fun nativePrepare(dataSource: String)native-lib 接收到请求要创建 Native 层的 VideoPlayer 并让它来初始化解码器
extern C
JNIEXPORT void JNICALL
Java_com_video_player_VideoPlayer_nativePrepare(JNIEnv *env, jobject thiz, jstring data_source) {const char *dataSource env-GetStringUTFChars(data_source, nullptr);auto jniCallbackHelper new JNICallbackHelper(javaVm, env, thiz);// 当前 VideoPlayer 需要数据源以及回调帮助对象videoPlayer new VideoPlayer(dataSource, jniCallbackHelper);videoPlayer-prepare();env-ReleaseStringUTFChars(data_source, dataSource);
}VideoPlayer 初始化时要对数据源进行深拷贝
VideoPlayer::VideoPlayer(const char *data_source, JNICallbackHelper *helper) {// 由于参数传入的 data_source 指针在调用完当前构造函数后会被回收// 为了避免 dataSource 成为悬空指针需要对 data_source 进行深拷贝// 声明 char 数组时不要忘记为 \0 预留出一个字节的空间dataSource new char[strlen(data_source) 1];strcpy(dataSource, data_source);jniCallbackHelper helper;
}由于初始化解码器是一个耗时操作不能放在主线程中进行因此我们开辟一个子线程进行准备工作
/*** 我们在 Activity 的主线程中开启准备工作因此 prepare()* 是在主线程中运行的该函数的任务是解析数据源不论是本地文件* 还是网络地址解析过程都是耗时操作因此要放在子线程中进行*/
void VideoPlayer::prepare() {pthread_create(pid_prepare, nullptr, task_prepare, this);
}线程的任务并没有直接开始初始化解码器因为线程环境访问不到数据源还是要在 VideoPlayer 的成员函数中进行
void *task_prepare(void *args) {// 因为我们现在是在子线程环境中不是 VideoPlayer 的成员函数不能// 直接访问 dataSource因此绕一圈在新的成员函数中做具体的准备工作auto videoPlayer static_castVideoPlayer *(args);videoPlayer-prepareInChildThread();// 线程的任务函数一定要返回 nullptr否则运行会崩溃return nullptr;
}调用 FFmpeg 的 API 去初始化解码器需要按照固定的步骤已经在注释中用标号给出。解码器初始化完毕后就要查找媒体流如果找到了音视频流就创建对应的通道分开处理
/*** 在子线程中做具体的准备工作初始化解码器*/
void VideoPlayer::prepareInChildThread() {/** 1.打开数据源*/// 总上下文AVFormatContext *avFormatContext avformat_alloc_context();// 字典可以以键值对形式添加参数AVDictionary *avDictionary nullptr;// 设置超时时间为 3 秒av_dict_set(avDictionary, timeout, 3000000, 0);// 打开视频数据源成功则返回 0int result avformat_open_input(avFormatContext, dataSource, nullptr, avDictionary);// 及时回收用完的变量av_dict_free(avDictionary);// 打开失败的话要通知上层if (result) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_CAN_NOT_OPEN_URL);}// 打开失败需要回收上下文avformat_close_input(avFormatContext);LOGE(无法打开数据源);return;}/** 2.查找媒体中的音视频流信息存入 AVFormatContext*/result avformat_find_stream_info(avFormatContext, nullptr);if (result 0) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_CAN_NOT_FIND_STREAMS);}avformat_close_input(avFormatContext);// 实际上 FFmpeg 也提供了根据错误码转换成字符串的函数char *errorMsg av_err2str(result);LOGE(%s, errorMsg);return;}// 获取视频的时长信息// avformat_find_stream_info() 会去尝试获取所有视频格式的总时长// 因此在它之后使用 mAVFormatContext-duration 才更加合适如果在// 它之前使用则可以获取 mp4 格式的时长但无法获取 flv 等格式的int duration avFormatContext-duration / AV_TIME_BASE;/** 3.打开解码器对音视频流分别创建对应的处理通道*/// 编解码器上下文AVCodecContext *avCodecContext nullptr;for (int i 0; i avFormatContext-nb_streams; i) {// 3.1 根据媒体流的信息获取相应的解码器流的类型可能是音频、视频、字幕AVStream *stream avFormatContext-streams[i];// 获取这个流的编解码参数AVCodecParameters *codecParameters stream-codecpar;// 根据参数获取对应的解码器AVCodec *codec avcodec_find_decoder(codecParameters-codec_id);if (!codec) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_FIND_DECODER_FAIL);}avformat_close_input(avFormatContext);LOGE(获取解码器失败);return;}// 3.2 有了解码器才能获取解码器上下文avCodecContext avcodec_alloc_context3(codec);if (!avCodecContext) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);}// 从这开始比之前多释放一个解码器上下文 AVCodecContext它会同时帮你释放解码器 AVCodecavcodec_free_context(avCodecContext);avformat_close_input(avFormatContext);LOGE(获取解码器上下文失败);return;}// 3.3 根据解码器上下文参数填充解码器上下文 AVCodecContextresult avcodec_parameters_to_context(avCodecContext, codecParameters);if (result 0) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);}avcodec_free_context(avCodecContext);avformat_close_input(avFormatContext);LOGE(设置解码器上下文失败);return;}// 3.4 打开解码器result avcodec_open2(avCodecContext, codec, nullptr);if (result 0) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_OPEN_DECODER_FAIL);}avcodec_free_context(avCodecContext);avformat_close_input(avFormatContext);LOGD(打开解码器失败);return;}// 3.5 根据媒体流的类型创建对应的处理通道if (codecParameters-codec_type AVMEDIA_TYPE_VIDEO) {// 有的视频类型只有一帧封面图片这种情况需要跳过if (stream-disposition AV_DISPOSITION_ATTACHED_PIC) {continue;}// 创建视频通道videoChannel new VideoChannel;} else if (codecParameters-codec_type AVMEDIA_TYPE_AUDIO) {// 创建音频通道audioChannel new AudioChannel;} else if (codecParameters-codec_type AVMEDIA_TYPE_SUBTITLE) {// 创建字幕通道...省略}}// 3.6 健壮性校验if (!videoChannel !audioChannel) {if (jniCallbackHelper) {jniCallbackHelper-onError(CHILD_THREAD, FFMPEG_NO_MEDIA);}if (avCodecContext) {avcodec_free_context(avCodecContext);}avformat_close_input(avFormatContext);LOGE(媒体文件没有音视频流);return;}/** 4.回调上层方法通知准备就绪*/if (jniCallbackHelper) {jniCallbackHelper-onPrepared(CHILD_THREAD);LOGD(准备完成);}
}到这里解码器初始化就完成了。
3、视频解码
在 1.2 节介绍 Demo 结构时我们放了一张图就是要从视频文件中不断读取 AVPacket 然后存放到 AVPacket 队列中。解码时不断地从 AVPacket 队列中取出 AVPacket 解码为 AVFrame 再存入 AVFrame 的队列。
由于上述两步都是循环的耗时操作因此要放在子线程中操作。由于是在多线程环境中因此保存 AVPacket 与 AVFrame 的队列需要是一个线程安全的队列我们首先来实现这个队列。
3.1 SafeQueue
SafeQueue 这个队列主要存放 AVPacket 和 AVFrame因此将其设计为模板类。此外由于释放队列元素的具体方法在 SafeQueue 内部是无法知晓的只能通过回调接口将释放元素的操作交给知道具体类型的对象如何释放的外部代码。参考代码如下
/*** 线程安全队列主要用于存放 AVFrame 和 AVPacket* 除了线程锁之外还有两点需要注意* 1. 由于使用泛型需要释放队列元素时不知道具体类型该如何* 释放因此需要通过 ReleaseCallback 回调给外部释放* 2.队列通过 enable 控制是否工作。比如存入元素时如果* 队列不工作那么需要丢弃并回收该元素** 此外还需注意模板类的实现需要和头文件包含在同一个文件中* 以便在编译时能够正确实例化模板类的具体类型。因此实现也放在* 头文件中而没有分离到 cpp 文件中*/
templateclass T
class SafeQueue {// 释放 T 的回调类型因为 SafeQueue 内部不知道 T 的具体类型// 也就不知道具体的释放方式typedef void (*ReleaseCallback)(T *value);private:std::queueT queue;pthread_mutex_t mutex;pthread_cond_t cond;bool enabled false;ReleaseCallback releaseCallback;public:SafeQueue() {pthread_mutex_init(mutex, nullptr);pthread_cond_init(cond, nullptr);}~SafeQueue() {pthread_mutex_destroy(mutex);pthread_cond_destroy(cond);}void setEnable(bool enable) {this-enabled enable;}/*** 向队列中存入元素如果队列不在工作状态就要丢弃该元素*/void put(T value) {pthread_mutex_lock(mutex);if (enabled) {queue.push(value);pthread_cond_signal(cond);} else {if (releaseCallback) {releaseCallback(value);}}pthread_mutex_unlock(mutex);}/*** 获取元素成功则返回 true。* 参数是一个入参出参采用引用形式避免了参数的复制* 将元素赋给形参就会直接给到实参*/bool get(T value) {bool success false;pthread_mutex_lock(mutex);// 阻塞函数如果队列中没有元素就等着while (enabled queue.empty()) {pthread_cond_wait(cond, mutex);}if (!queue.empty()) {value queue.front();queue.pop();success true;}pthread_mutex_unlock(mutex);return success;}void clear() {pthread_mutex_lock(mutex);while (!queue.empty()) {T value queue.front();if (releaseCallback) {releaseCallback(value);}queue.pop();}pthread_mutex_unlock(mutex);}/*** 因为函数指针不包含 this 指针因此带有隐藏的 this 指针的成员函数无法直接转换* 为函数指针。而静态函数不依赖于特定对象也没有 this 指针它可以直接转换为函数* 指针。因此方法参数可以传静态函数而不能传成员函数否则会报 Reference to* non-static member function must be called 的错误*/void setReleaseCallback(ReleaseCallback callback) {releaseCallback callback;}bool isEmpty() {return queue.empty();}int size() {return queue.size();}
};当然这不是 SafeQueue 的最终形态因为后续在做音视频同步需要丢包时还要向 SafeQueue 中添加丢包的操作逻辑。
3.2 BaseChannel
由于 VideoChannel 和 AudioChannel 会有很多类似的操作以及属性因此我们考虑抽取出 BaseChannel 作为它们的父类
class BaseChannel {public:BaseChannel(int stream_index, AVCodecContext *codecContext);virtual ~BaseChannel();static void releaseAVPacket(AVPacket **packet);static void releaseAVFrame(AVFrame **frame);// 解码器上下文AVCodecContext *avCodecContext;// 是否在播放中bool isPlaying;// 媒体流对应的索引int stream_index;// 压缩数据 AVPacket 队列SafeQueueAVPacket * packets;// 解压后数据 AVFrame 队列SafeQueueAVFrame * frames;
};成员函数的实现如下
BaseChannel::BaseChannel(int stream_index, AVCodecContext *avCodecContext) :stream_index(stream_index), avCodecContext(avCodecContext) {// 设置释放 AVPacket 和 AVFrame 的函数packets.setReleaseCallback(releaseAVPacket);frames.setReleaseCallback(releaseAVFrame);
}BaseChannel::~BaseChannel() {packets.clear();frames.clear();
}void BaseChannel::releaseAVPacket(AVPacket **packet) {if (*packet) {av_packet_free(packet);*packet nullptr;}
}void BaseChannel::releaseAVFrame(AVFrame **frame) {if (*frame) {av_frame_free(frame);*frame nullptr;}
}VideoChannel 继承 BaseChannel做出相应修改
class VideoChannel : public BaseChannel {...
}源文件需要修改构造函数
VideoChannel::VideoChannel(int stream_index, AVCodecContext *avCodecContext): BaseChannel(stream_index, avCodecContext) {}AudioChannel 也是类似的修改。当然这不是 BaseChannel 的最终形态后续还会添加功能。 是否对 BaseChannel 的 releaseAVPacket() 和 releaseAVFrame() 两个成员函数声明为 static 有所疑问因为 SafeQueue.setReleaseCallback() 的参数是函数指针因此参数必须是或者可以转为函数指针。由于函数指针没有 this而成员函数是有隐藏 this 的所以成员函数不能直接转换为函数指针。只能是静态函数、全局函数或 C11 以上的 Lambda 表达式可以转换我们就使用了静态函数的方案。 3.3 解码
之前我们完成了解码器的初始化因为我们设置了 Native 对上层的回调在准备就绪后会通知上层的 VideoPlayer我们的解码工作就从这里开始 override fun onCreate(savedInstanceState: Bundle?) {...videoPlayer.setOnPreparedListener(object : VideoPlayer.OnPreparedListener {override fun onPrepared() {runOnUiThread {Toast.makeText(thisMainActivity, 准备就绪, Toast.LENGTH_LONG).show()}// 开始解码videoPlayer.start()}})...}VideoPlayer 直接交给 Native 层处理 fun start() {nativeStart()}private external fun nativeStart()native-lib 将请求转发给底层的 VideoPlayer
extern C
JNIEXPORT void JNICALL
Java_com_video_player_VideoPlayer_nativeStart(JNIEnv *env, jobject thiz) {videoPlayer-start();
}解码的操作包含两部分
首先从媒体流中读取出 AVPacket既可能是音频也可能是视频区分类型后存入相应通道的 AVPacket 队列中从 AVPacket 队列中取出 AVPacket 解码为 AVFrame 存入队列
很明显由于第一步需要区分音视频因此它应该在 VideoPlayer 内进行而第二步则在各自通道内进行。那么 VideoPlayer 的 start() 就需要开启子线程执行第一步驱动 VideoChannel 执行第二步
void VideoPlayer::start() {isPlaying true;if (videoChannel) {videoChannel-start();}pthread_create(pid_start, nullptr, task_start, this);
}读取 AVPacket
读取 AVPacket 是一个耗时操作所以要放在子线程中。在 task_start() 内将具体操作交给 VideoPlayer 的 startInChildThread() 以便访问成员变量
void *task_start(void *args) {auto videoPlayer static_castVideoPlayer *(args);videoPlayer-startInChildThread();return nullptr;
}/*** 解码器从媒体流中读取出 AVPacket 存入对应通道的 AVPacket 队列中*/
void VideoPlayer::startInChildThread() {int result;while (isPlaying) {// 因为将 AVPacket 存入队列的速度远远快于取出 AVPacket 解码的速度// 因此需要添加速度控制以防队列体积过大而撑爆内存if (videoChannel videoChannel-packets.size() 100) {// 休眠 10 毫秒av_usleep(10 * 1000);continue;}if (audioChannel audioChannel-packets.size() 100) {av_usleep(10 * 1000);continue;}// 不要想着将 packet 拿到 while 外面复用因为在当前方法只会将其存入// AVPacket 队列在 Channel 那边取出 AVPacket 使用完并释放之前就// 复用会导致 Channel 那边解码失败AVPacket *packet av_packet_alloc();// 读取一帧AVPacket 可能是视频帧也可能是音频帧加以区分后存入相应的队列中result av_read_frame(avFormatContext, packet);if (!result) {// 读取成功将其加入相应通道的队列中if (videoChannel videoChannel-stream_index packet-stream_index) {videoChannel-packets.put(packet);} else if (audioChannel audioChannel-stream_index packet-stream_index) {audioChannel-packets.put(packet);}} else if (result AVERROR_EOF) {// 如果读取到文件末尾了那就等音视频通道的 AVPacket 队列都为空后再跳出循环结束播放if (videoChannel videoChannel-packets.isEmpty() audioChannel audioChannel-packets.isEmpty()) {break;}} else {// 其他情况就是读取错误直接结束循环break;}}// 结束播放isPlaying false;if (videoChannel) {videoChannel-stop();}if (audioChannel) {audioChannel-stop();}
}整个过程的核心 API 就是先用 av_packet_alloc() 创建一个 AVPacket 对象再传入 av_read_frame() 读取出 AVPacket 的内容。
此外需要注意的是由于 SafeQueue 内没有进行容量限制并且 AVPacket 的入队速度远远快于出队速度因此需要进行速度控制以免内存爆炸。如果不添加速度控制在播放长一点的视频时程序会崩溃。
将 AVPacket 解码为 AVFrame
VideoChannel 的 start() 会启动两个线程一个负责将 AVPacket 解码为 AVFrame一个负责取出 AVFrame 的像素数据回调给控制层进行屏幕渲染
void VideoChannel::start() {// 是否在解码和渲染过程中isPlaying true;// 开启两个队列packets.setEnable(true);frames.setEnable(true);// 开启解码和渲染线程pthread_create(pid_decode, nullptr, task_decode, this);pthread_create(pid_play, nullptr, task_play, this);
}这一节我们只看解码线程。主要步骤是
从 AVPacket 队列中不断取出 AVPacket先通过 avcodec_send_packet() 将其发送给解码器通过 av_frame_alloc() 创建一个 AVFrame再通过 avcodec_receive_frame() 读取到解码后的 AVFrame将 AVFrame 存入队列通过 av_packet_unref() 将 AVFrame 的引用计数减 1最后回收 AVFrame
void *task_decode(void *args) {auto videoChannel static_castVideoChannel *(args);videoChannel-decode();return nullptr;
}/*** 解码就是从 AVPacket 队列中的 AVPacket 解码* 为 AVFrame 再存入 AVFrame 队列中*/
void VideoChannel::decode() {// 由于从队列中取出的 AVPacket 在使用完后直接// 就释放了因此可以放在 while 外复用AVPacket *packet nullptr;int result;while (isPlaying) {// 由于解码速度要快于音视频的渲染/播放速度因此需要控制// frames 队列的入队速度以防队列过大而撑爆内存if (isPlaying frames.size() 100) {av_usleep(10 * 1000);continue;}// 从队列中取出一个 AVPacketresult packets.get(packet);// 如果此时已经设置停止播放则跳出循环if (!isPlaying) {break;}// 如果取 AVPacket 失败可能是因为队列中尚未有// AVPacket继续循环等待 AVPacket 被读取到队列中if (!result) {continue;}// 将 AVPacket 发送给解码器result avcodec_send_packet(avCodecContext, packet);if (result ! 0) {break;}// 从解码器中获取解码后的 AVFrame 存入 frames 队列中av_frame_alloc()// 会在堆区开辟内存空间使用完毕需要回收AVFrame *frame av_frame_alloc();result avcodec_receive_frame(avCodecContext, frame);LOGD(解码结果%d, result);if (!result) {frames.put(frame);// 每当调用 av_read_frame() 时就会对相应的 AVPacket 引用计数加一// 对 AVPacket 的 *data 指向的内存区域的引用计数减 1减到 0 时会回收av_packet_unref(packet);// 回收 AVPacket 指针本身releaseAVPacket(packet);} else if (result AVERROR(EAGAIN)) {continue;} else {// 解码失败但是 AVFrame 有值需要释放if (frame) {releaseAVFrame(frame);}break;}LOGD(解码mFrames 中完成解码的帧数%d, frames.size());}// 对于从 while 循环 break 出来的情况还要再回收一次 AVPacketav_packet_unref(packet);releaseAVPacket(packet);
}这样解码就完成了。
4、视频渲染
视频渲染要从两个方向上看
一方面从上至下我们要将上层的 SurfaceView 传递给 Native 层的 native-lib因为我们要在 Native 层进行渲染另一方面从下至上解码后的 AVFrame 队列保存在 VideoChannel 中而渲染屏幕的对象在 native-lib 中需要将 AVFrame 回调给 native-lib
4.1 窗口设置
在 Activity 中将 SurfaceHolder 传递给 VideoPlayer override fun onCreate(savedInstanceState: Bundle?) {...videoPlayer.setSurfaceHolder(binding.surfaceView.holder)...}VideoPlayer 需要实现 SurfaceHolder.Callback 以便在 SurfaceView 窗口尺寸发生变化时将新的窗口传递到 Native 层
class VideoPlayer : SurfaceHolder.Callback {private var surfaceHolder: SurfaceHolder? nullfun setSurfaceHolder(surfaceHolder: SurfaceHolder) {this.surfaceHolder?.removeCallback(this)this.surfaceHolder surfaceHolderthis.surfaceHolder?.addCallback(this)}// SurfaceHolder.Callback start// 只在创建时回调override fun surfaceCreated(holder: SurfaceHolder) {}// 创建时回调Surface 的格式与尺寸变化时也会回调override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {nativeSetSurface(holder.surface)}override fun surfaceDestroyed(holder: SurfaceHolder) {}// SurfaceHolder.Callback endprivate external fun nativeSetSurface(surface: Surface)
}native-lib 接收 Surface 并创建 Native 层的
// 创建窗口和渲染时需要用锁这里采用静态初始化方式
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;
ANativeWindow *window nullptr;extern C
JNIEXPORT void JNICALL
Java_com_video_player_VideoPlayer_nativeSetSurface(JNIEnv *env, jobject thiz, jobject surface) {pthread_mutex_lock(mutex);// 先销毁之前的 ANativeWindowif (window) {ANativeWindow_release(window);window nullptr;}// 再创建新的 ANativeWindowwindow ANativeWindow_fromSurface(env, surface);pthread_mutex_unlock(mutex);
}4.2 回调绘制数据与渲染
这次我们来看 VideoChannel 的渲染线程
void *task_play(void *args) {auto videoChannel static_castVideoChannel *(args);videoChannel-play();return nullptr;
}/*** 播放任务实际上就是要将 AVFrame 内的像素数据取出回调给负责进行* 渲染的 native-lib。具体操作有* 1.将 AVFrame 队列中的 AVFrame 取出将像素数据转为 RGB 格式* 2.将转换后的数据保存到矩阵中回调给上一层的 VideoPlayer后者* 再次回调给持有 ANativeWindow 的 native-lib 进行绘制*/
void VideoChannel::play() {// 存放 RGBA 数据的指针数组uint8_t *dst_data[4];// 存放 dst_data 四个指针首地址的数组int dst_lineSize[4];// 根据图片的宽高和格式为其分配内存并为 dst_data 和 dst_lineSize 赋值// 比如一张 1920*1080 使用 AV_PIX_FMT_RGBA即 RGBA 8:8:8:8, 32bpp, RGBARGBA...// 的图片其内存占用为 1920*1080*4≈8MBav_image_alloc(dst_data, dst_lineSize, avCodecContext-width, avCodecContext-height,AV_PIX_FMT_RGBA, 1);// 转换上下文将 YUV 转换为 RGB 所需的上下文SwsContext *swsContext sws_getContext(avCodecContext-width, avCodecContext-height, avCodecContext-pix_fmt,avCodecContext-width, avCodecContext-height, AV_PIX_FMT_RGBA,SWS_BILINEAR, nullptr, nullptr, nullptr);AVFrame *frame nullptr;int result;while (isPlaying) {result frames.get(frame);if (!isPlaying) {break;}if (!result) {continue;}// 执行 YUV - RGBA 转换转换后的数据保存在 dst_data 和 dst_lineSize 中sws_scale(swsContext, frame-data, frame-linesize, 0,avCodecContext-height, dst_data, dst_lineSize);renderCallback(dst_data[0], avCodecContext-width, avCodecContext-height, dst_lineSize[0]);// 释放 AVFrameav_frame_unref(frame);releaseAVFrame(frame);}av_frame_unref(frame);releaseAVFrame(frame);isPlaying false;av_free(dst_data[0]);sws_freeContext(swsContext);
}VideoChannel 通过 renderCallback 将绘制所需数据先回调给它的直接上层 VideoPlayerVideoPlayer 做同样的操作回调给 native-lib渲染只需将数据拷贝到 ANativeWindow_Buffer 中即可后续的渲染工作无需我们操作
/*** 渲染*/
void renderFrame(uint8_t *src_data, int width, int height, int src_lineSize) {pthread_mutex_lock(mutex);if (!window) {// 如果 ANativeWindow 不存在要释放锁避免死锁pthread_mutex_unlock(mutex);return;}// 设置 ANativeWindow 的宽高以及图像格式ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);ANativeWindow_Buffer window_buffer;// 渲染之前要对 ANativeWindow 上锁如果上锁失败要结束渲染过程if (ANativeWindow_lock(window, window_buffer, nullptr)) {ANativeWindow_release(window);window nullptr;pthread_mutex_unlock(mutex);return;}// 将像素数据填入 ANativeWindow_Buffer 就算渲染完成了auto *dst_data static_castuint8_t *(window_buffer.bits);int dst_lineSize window_buffer.stride * 4;// 行遍历for (int i 0; i window_buffer.height; i) {// 从 src_data 拷贝一行数据到 dst_data 中memcpy(dst_data i * dst_lineSize, src_data i * src_lineSize, dst_lineSize);}// 数据刷新ANativeWindow_unlockAndPost(window);pthread_mutex_unlock(mutex);
}在底层的绘制都是通过缓冲区进行绘制的。ANativeWindow 自带一个相同大小的缓冲区OpenCV、WebRTC、FFmpeg 都是通过这样的缓冲区进行绘制的。缓冲区实际上是一个字节数组将像素数据赋值给字节数组就完成了渲染。因此底层的渲染实际上就是一个内存的拷贝。
渲染这里要注意空间的分配与回收问题否则长时间播放可能会耗尽内存导致应用崩溃。可能的原因是解码速度远远快于渲染速度导致解码队列溢出了所以我们才添加了对 VideoChannel 与 AudioChannel 内 AVPacket 和 AVFrame 队列的流量控制队列容量大于 100 的时候进行休眠。
到这里可以顺利播放视频了但是由于音频解码与渲染还没做因此当前视频无声。下一篇文章我们再介绍音频如何处理。