赞
踩
本篇作为 Android 音视频实战系列的第二篇文章,主要介绍视频解码与渲染过程。本系列文章目录如下:
Android 音视频基础知识
Android 音视频播放器 Demo(一)—— 视频解码与渲染
Android 音视频播放器 Demo(二)—— 音频解码与音视频同步
RTMP 直播推流 Demo(一)—— 项目配置与视频预览
RTMP 直播推流 Demo(二)—— 音频推流与视频推流
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-player SHARED ${sources}) target_link_libraries( video-player # FFmpeg 源码编译出的 6 个静态库 avcodec avfilter avformat avutil swresample swscale log z # 在 Native 进行视频渲染时要用到 ANativeWindow android # 在 Native 进行音频播放所需的库 OpenSLES)
在
cmake
块中的abiFilters
用于指定 CMake 构建系统编译和构建的 ABI。例如,如果在abiFilters
中设置为 “armeabi-v7a”,则 CMake 将只为 armeabi-v7a 架构编译和构建本机代码。
类似地,在ndk
块中的abiFilters
用于指定 NDK 构建系统编译和构建的 ABI。如果在abiFilters
中设置为 “armeabi-v7a”,则 NDK 将只为 armeabi-v7a 架构编译和构建本机代码。
视频播放器 Demo 可以分为上下两层:
Native 控制层示意图如下:
控制层的主要作用:
可以看到音视频各有一个保存 AVPacket 的队列,由于 AVPacket 是压缩数据,我们需要从队列中取出 AVPacket 解压为 AVFrame 再存入队列,因此 AVFrame 也是有一个队列的:
视频层作用:
音频层类似:
解压后的音频数据通过 OpenSLES 进行播放。
AudioTrack 底层实际上也是使用的 OpenSLES。
最后来介绍一下实现步骤:
准备阶段的主要工作是打开 FFmpeg 的解码器。在这个过程中,我们需要建立 Native 回调上层方法的机制 JNICallbackHelper,这样 Native 才能将播放器的准备状态、播放状态通知给上层。
简单说一下代码结构:
还是先从 Activity 开始,布局如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <SurfaceView android:id="@+id/surfaceView" android:layout_width="match_parent" android:layout_height="200dp" /> <!-- 进度条 --> <LinearLayout android:layout_width="match_parent" android:layout_height="30dp" android:layout_margin="5dp"> <TextView android:id="@+id/tv_time" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center" android:text="@string/init_time" android:visibility="gone" /> <SeekBar android:id="@+id/seekBar" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:max="100" android:visibility="gone" /> </LinearLayout> </LinearLayout>
代码端命令 VideoPlayer 执行准备工作:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var videoPlayer: VideoPlayer override 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(this@MainActivity, "准备就绪", Toast.LENGTH_LONG).show() } } }) videoPlayer.setOnErrorListener(object : VideoPlayer.OnErrorListener { override fun onError(errorMsg: String) { runOnUiThread { Toast.makeText(this@MainActivity, errorMsg, Toast.LENGTH_LONG).show() } } }) // 准备工作 videoPlayer.prepare(file_path) } }
VideoPlayer 将准备工作转交给 Native 层,同时还为外界提供了播放器准备就绪的监听器 OnPreparedListener 和发生错误的监听器 OnErrorListener:
class VideoPlayer { private lateinit var surfaceHolder: SurfaceHolder private var onPreparedListener: OnPreparedListener? = null private var onErrorListener: OnErrorListener? = null fun 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 的实现。
JNICallbackHelper 是一个在 Native 层调用上层方法的帮助类,在进行解码器初始化时需要通过它告知上层解码器的初始化状态。
首先我们要了解,Native 如何调用上层方法。实际上跟 Java/Kotlin 反射类似:
在上层的 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 对象,然后在初始化解码器时使用它。
上层的 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_cast<VideoPlayer *>(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); // 打开视频数据源,成功则返回 0 int 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,它会同时帮你释放解码器 AVCodec avcodec_free_context(&avCodecContext); avformat_close_input(&avFormatContext); LOGE("获取解码器上下文失败"); return; } // 3.3 根据解码器上下文参数填充解码器上下文 AVCodecContext result = 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("准备完成"); } }
到这里解码器初始化就完成了。
在 1.2 节介绍 Demo 结构时我们放了一张图,就是要从视频文件中不断读取 AVPacket 然后存放到 AVPacket 队列中。解码时不断地从 AVPacket 队列中取出 AVPacket 解码为 AVFrame 再存入 AVFrame 的队列。
由于上述两步都是循环的耗时操作,因此要放在子线程中操作。由于是在多线程环境中,因此保存 AVPacket 与 AVFrame 的队列需要是一个线程安全的队列,我们首先来实现这个队列。
SafeQueue 这个队列主要存放 AVPacket 和 AVFrame,因此将其设计为模板类。此外,由于释放队列元素的具体方法在 SafeQueue 内部是无法知晓的,只能通过回调接口,将释放元素的操作交给知道具体类型的对象如何释放的外部代码。参考代码如下:
/** * 线程安全队列,主要用于存放 AVFrame 和 AVPacket * 除了线程锁之外,还有两点需要注意: * 1. 由于使用泛型,需要释放队列元素时不知道具体类型该如何 * 释放,因此需要通过 ReleaseCallback 回调给外部释放 * 2.队列通过 enable 控制是否工作。比如存入元素时,如果 * 队列不工作,那么需要丢弃并回收该元素 * * 此外,还需注意,模板类的实现需要和头文件包含在同一个文件中, * 以便在编译时能够正确实例化模板类的具体类型。因此实现也放在 * 头文件中,而没有分离到 cpp 文件中 */ template<class T> class SafeQueue { // 释放 T 的回调类型,因为 SafeQueue 内部不知道 T 的具体类型, // 也就不知道具体的释放方式 typedef void (*ReleaseCallback)(T *value); private: std::queue<T> 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 中添加丢包的操作逻辑。
由于 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 队列 SafeQueue<AVPacket *> packets; // 解压后数据 AVFrame 队列 SafeQueue<AVFrame *> 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 的,所以成员函数不能直接转换为函数指针。只能是静态函数、全局函数或 C++11 以上的 Lambda 表达式可以转换,我们就使用了静态函数的方案。
之前我们完成了解码器的初始化,因为我们设置了 Native 对上层的回调,在准备就绪后会通知上层的 VideoPlayer,我们的解码工作就从这里开始:
override fun onCreate(savedInstanceState: Bundle?) {
...
videoPlayer.setOnPreparedListener(object : VideoPlayer.OnPreparedListener {
override fun onPrepared() {
runOnUiThread {
Toast.makeText(this@MainActivity, "准备就绪", 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();
}
解码的操作包含两部分:
很明显,由于第一步需要区分音视频,因此它应该在 VideoPlayer 内进行,而第二步则在各自通道内进行。那么 VideoPlayer 的 start() 就需要开启子线程执行第一步,驱动 VideoChannel 执行第二步:
void VideoPlayer::start() {
isPlaying = true;
if (videoChannel) {
videoChannel->start();
}
pthread_create(&pid_start, nullptr, task_start, this);
}
读取 AVPacket 是一个耗时操作,所以要放在子线程中。在 task_start() 内将具体操作交给 VideoPlayer 的 startInChildThread() 以便访问成员变量:
void *task_start(void *args) { auto videoPlayer = static_cast<VideoPlayer *>(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 的入队速度远远快于出队速度,因此需要进行速度控制以免内存爆炸。如果不添加速度控制,在播放长一点的视频时,程序会崩溃。
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);
}
这一节我们只看解码线程。主要步骤是:
void *task_decode(void *args) { auto videoChannel = static_cast<VideoChannel *>(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; } // 从队列中取出一个 AVPacket result = 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 出来的情况还要再回收一次 AVPacket av_packet_unref(packet); releaseAVPacket(&packet); }
这样解码就完成了。
视频渲染要从两个方向上看:
在 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? = null fun setSurfaceHolder(surfaceHolder: SurfaceHolder) { this.surfaceHolder?.removeCallback(this) this.surfaceHolder = surfaceHolder this.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 end private 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); // 先销毁之前的 ANativeWindow if (window) { ANativeWindow_release(window); window = nullptr; } // 再创建新的 ANativeWindow window = ANativeWindow_fromSurface(env, surface); pthread_mutex_unlock(&mutex); }
这次我们来看 VideoChannel 的渲染线程:
void *task_play(void *args) { auto videoChannel = static_cast<VideoChannel *>(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≈8MB av_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]); // 释放 AVFrame av_frame_unref(frame); releaseAVFrame(&frame); } av_frame_unref(frame); releaseAVFrame(&frame); isPlaying = false; av_free(&dst_data[0]); sws_freeContext(swsContext); }
VideoChannel 通过 renderCallback 将绘制所需数据先回调给它的直接上层 VideoPlayer,VideoPlayer 做同样的操作回调给 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_cast<uint8_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 的时候进行休眠。
到这里,可以顺利播放视频了,但是由于音频解码与渲染还没做,因此当前视频无声。下一篇文章我们再介绍音频如何处理。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。