在 Android ndk 中,我们可以使用 mediandk
原生库来播放视频。mediandk
库主要依赖 ANativeWindow
、AMediaExtractor
和 AMediaCodec
这三个类来实现视频播放的。
ANativeWindow
:显示视频的窗口,用来渲染视频AMediaExtractor
:媒体提取器,用来从源媒体文件提取数据AMediaCodec
:用于编码/解码的组件
这里以 Google 官方的 native-codec 使用示例作为例子,介绍 mediandk
的使用。如下图所示:
looper.cpp
looper.cpp
这段代码实现了一个简单的消息循环机制,通过信号量和线程来实现消息的发送、处理和退出。代码如下所示:
cpp
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__)
struct loopermessage;
typedef struct loopermessage loopermessage;
// 定义消息结构体,用于存储消息信息
struct loopermessage {
int what; // 消息的标识,用于区分不同类型的消息
void *obj; // 消息携带的数据指针
loopermessage *next; // 指向下一个消息的指针,用于构建消息链表
bool quit; // 标志位,用于指示是否退出循环
};
// 静态成员函数,作为线程的入口点
// 该函数接收一个指向 looper 对象的指针,并调用其 loop 方法
void *looper::trampoline(void *p) {
((looper *)p)->loop();
return NULL;
}
// looper 类的构造函数
looper::looper() {
// 初始化信号量 headdataavailable,初始值为 0
// 用于表示消息队列中是否有可用的消息
sem_init(&headdataavailable, 0, 0);
// 初始化信号量 headwriteprotect,初始值为 1
// 用于保护对消息队列头部的访问,实现互斥
sem_init(&headwriteprotect, 0, 1);
// 定义线程属性变量
pthread_attr_t attr;
// 初始化线程属性
pthread_attr_init(&attr);
// 创建一个新的线程,线程函数为 trampoline,传入当前 looper 对象的指针
pthread_create(&worker, &attr, trampoline, this);
// 标记 looper 正在运行
running = true;
}
// looper 类的析构函数
looper::~looper() {
// 如果 looper 仍在运行
if (running) {
// 打印日志,提示有消息可能不会被处理
LOGV(
"Looper deleted while still running. Some messages will not be "
"processed");
// 调用 quit 方法停止 looper
quit();
}
}
// 向消息队列中发送一个消息
void looper::post(int what, void *data, bool flush) {
// 创建一个新的消息对象
loopermessage *msg = new loopermessage();
// 设置消息的标识
msg->what = what;
// 设置消息携带的数据
msg->obj = data;
// 初始化消息的下一个指针为 NULL
msg->next = NULL;
// 设置消息的退出标志为 false
msg->quit = false;
// 调用 addmsg 方法将消息添加到消息队列中
addmsg(msg, flush);
}
// 将消息添加到消息队列中
void looper::addmsg(loopermessage *msg, bool flush) {
// 等待 headwriteprotect 信号量,确保对消息队列头部的互斥访问
sem_wait(&headwriteprotect);
// 获取消息队列的头部指针
loopermessage *h = head;
// 如果 flush 为 true,表示需要清空消息队列
if (flush) {
// 遍历消息队列
while (h) {
// 保存下一个消息的指针
loopermessage *next = h->next;
// 删除当前消息
delete h;
// 移动到下一个消息
h = next;
}
// 清空消息队列后,将头部指针置为 NULL
h = NULL;
}
// 如果消息队列不为空
if (h) {
// 遍历到消息队列的末尾
while (h->next) {
h = h->next;
}
// 将新消息添加到消息队列的末尾
h->next = msg;
} else {
// 如果消息队列为空,将新消息设置为头部消息
head = msg;
}
// 打印日志,记录发送的消息标识
LOGV("post msg %d", msg->what);
// 释放 headwriteprotect 信号量,允许其他线程访问消息队列头部
sem_post(&headwriteprotect);
// 释放 headdataavailable 信号量,表示有新的消息可用
sem_post(&headdataavailable);
}
// 消息循环函数,不断从消息队列中取出消息并处理
void looper::loop() {
while (true) {
// 等待 headdataavailable 信号量,直到有可用的消息
sem_wait(&headdataavailable);
// 等待 headwriteprotect 信号量,确保对消息队列头部的互斥访问
sem_wait(&headwriteprotect);
// 获取消息队列的头部消息
loopermessage *msg = head;
// 如果消息队列为空
if (msg == NULL) {
// 打印日志,提示没有消息
LOGV("no msg");
// 释放 headwriteprotect 信号量,允许其他线程访问消息队列头部
sem_post(&headwriteprotect);
// 继续下一次循环
continue;
}
// 将消息队列的头部指针指向下一个消息
head = msg->next;
// 释放 headwriteprotect 信号量,允许其他线程访问消息队列头部
sem_post(&headwriteprotect);
// 如果消息的退出标志为 true
if (msg->quit) {
// 打印日志,提示正在退出
LOGV("quitting");
// 删除消息对象
delete msg;
// 退出循环
return;
}
// 打印日志,记录正在处理的消息标识
LOGV("processing msg %d", msg->what);
// 调用 handle 方法处理消息
handle(msg->what, msg->obj);
// 删除消息对象
delete msg;
}
}
// 停止 looper 的运行
void looper::quit() {
// 打印日志,提示正在退出
LOGV("quit");
// 创建一个新的消息对象,用于指示退出
loopermessage *msg = new loopermessage();
// 设置消息的标识为 0
msg->what = 0;
// 设置消息携带的数据为 NULL
msg->obj = NULL;
// 初始化消息的下一个指针为 NULL
msg->next = NULL;
// 设置消息的退出标志为 true
msg->quit = true;
// 调用 addmsg 方法将退出消息添加到消息队列中
addmsg(msg, false);
// 定义一个指针,用于存储线程的返回值
void *retval;
// 等待工作线程退出,并获取其返回值
pthread_join(worker, &retval);
// 销毁 headdataavailable 信号量
sem_destroy(&headdataavailable);
// 销毁 headwriteprotect 信号量
sem_destroy(&headwriteprotect);
// 标记 looper 停止运行
running = false;
}
// 处理消息的虚函数,默认实现只是打印日志并丢弃消息
void looper::handle(int what, void *obj) {
// 打印日志,记录丢弃的消息标识和数据指针
LOGV("dropping msg %d %p", what, obj);
}
native-codec-jni.cpp
native-codec-jni.cpp
内部就是视频播放器的实现了,其核心代码在 doCodecWork
方法中。该方法的流程是固定。
cpp
// 引入自定义的 looper 头文件
#include "looper.h"
// 引入 Android NDK 媒体编解码器和提取器的头文件
#include "media/NdkMediaCodec.h"
#include "media/NdkMediaExtractor.h"
#include <android/log.h>
// 定义日志标签
#define TAG "NativeCodec"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
// 引入 Android 原生窗口和资产管理器的 JNI 头文件
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <android/native_window_jni.h>
// 定义工作数据结构体,用于存储编解码过程中的相关信息
typedef struct {
int fd; // 文件描述符
ANativeWindow *window; // 原生窗口指针,用于渲染视频
AMediaExtractor *ex; // 媒体提取器指针,用于提取媒体数据
AMediaCodec *codec; // 媒体编解码器指针,用于解码媒体数据
int64_t renderstart; // 渲染开始时间
bool sawInputEOS; // 标记是否到达输入数据的末尾
bool sawOutputEOS; // 标记是否到达输出数据的末尾
bool isPlaying; // 标记是否正在播放
bool renderonce; // 标记是否只渲染一次
} workerdata;
// 全局工作数据对象,初始化为默认值
workerdata data = {-1, NULL, NULL, NULL, 0, false, false, false, false};
// 定义消息枚举,用于区分不同类型的消息
enum {
kMsgCodecBuffer, // 编解码缓冲区消息
kMsgPause, // 暂停消息
kMsgResume, // 恢复播放消息
kMsgPauseAck, // 暂停确认消息
kMsgDecodeDone, // 解码完成消息
kMsgSeek, // 定位消息
};
// 自定义 looper 类,继承自 looper 类
class mylooper : public looper {
// 重写 handle 方法,用于处理不同类型的消息
virtual void handle(int what, void *obj);
};
// 全局 mylooper 对象指针
static mylooper *mlooper = NULL;
// 获取系统当前时间(纳秒)
int64_t systemnanotime() {
timespec now;
// 获取单调时钟的时间
clock_gettime(CLOCK_MONOTONIC, &now);
// 将秒和纳秒转换为纳秒并返回
return now.tv_sec * 1000000000LL + now.tv_nsec;
}
// 执行编解码工作
void doCodecWork(workerdata *d) {
ssize_t bufidx = -1;
// 如果还未到达输入数据的末尾
if (!d->sawInputEOS) {
// 从编解码器获取一个可用的输入缓冲区,超时时间为 2000 微秒
bufidx = AMediaCodec_dequeueInputBuffer(d->codec, 2000);
LOGV("input buffer %zd", bufidx);
if (bufidx >= 0) {
size_t bufsize;
// 获取输入缓冲区的指针和大小
auto buf = AMediaCodec_getInputBuffer(d->codec, bufidx, &bufsize);
// 从媒体提取器读取样本数据到输入缓冲区
auto sampleSize = AMediaExtractor_readSampleData(d->ex, buf, bufsize);
if (sampleSize < 0) {
sampleSize = 0;
// 标记到达输入数据的末尾
d->sawInputEOS = true;
LOGV("EOS");
}
// 获取样本的呈现时间(微秒)
auto presentationTimeUs = AMediaExtractor_getSampleTime(d->ex);
// 将输入缓冲区数据入队到编解码器
AMediaCodec_queueInputBuffer(
d->codec, bufidx, 0, sampleSize, presentationTimeUs,
d->sawInputEOS ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0);
// 移动到下一个样本
AMediaExtractor_advance(d->ex);
}
}
// 如果还未到达输出数据的末尾
if (!d->sawOutputEOS) {
AMediaCodecBufferInfo info;
// 从编解码器获取一个可用的输出缓冲区,超时时间为 0 微秒
auto status = AMediaCodec_dequeueOutputBuffer(d->codec, &info, 0);
if (status >= 0) {
if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) {
LOGV("output EOS");
// 标记到达输出数据的末尾
d->sawOutputEOS = true;
}
// 将呈现时间(微秒)转换为纳秒
int64_t presentationNano = info.presentationTimeUs * 1000;
if (d->renderstart < 0) {
// 计算渲染开始时间
d->renderstart = systemnanotime() - presentationNano;
}
// 计算渲染延迟时间
int64_t delay = (d->renderstart + presentationNano) - systemnanotime();
if (delay > 0) {
// 休眠延迟时间
usleep(delay / 1000);
}
// 释放输出缓冲区并渲染
AMediaCodec_releaseOutputBuffer(d->codec, status, info.size != 0);
if (d->renderonce) {
// 只渲染一次后重置标记
d->renderonce = false;
return;
}
} else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
LOGV("output buffers changed");
} else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
// 获取输出格式
auto format = AMediaCodec_getOutputFormat(d->codec);
LOGV("format changed to: %s", AMediaFormat_toString(format));
// 释放输出格式对象
AMediaFormat_delete(format);
} else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
LOGV("no output buffer right now");
} else {
LOGV("unexpected info code: %zd", status);
}
}
// 如果输入或输出数据还未处理完,继续发送编解码缓冲区消息
if (!d->sawInputEOS || !d->sawOutputEOS) {
mlooper->post(kMsgCodecBuffer, d);
}
}
// 处理不同类型的消息
void mylooper::handle(int what, void *obj) {
switch (what) {
case kMsgCodecBuffer:
// 处理编解码缓冲区消息
doCodecWork((workerdata *)obj);
break;
case kMsgDecodeDone: {
workerdata *d = (workerdata *)obj;
// 停止编解码器
AMediaCodec_stop(d->codec);
// 删除编解码器对象
AMediaCodec_delete(d->codec);
// 删除媒体提取器对象
AMediaExtractor_delete(d->ex);
// 标记输入和输出数据都已处理完
d->sawInputEOS = true;
d->sawOutputEOS = true;
} break;
case kMsgSeek: {
workerdata *d = (workerdata *)obj;
// 媒体提取器定位到指定位置
AMediaExtractor_seekTo(d->ex, 0, AMEDIAEXTRACTOR_SEEK_NEXT_SYNC);
// 刷新编解码器
AMediaCodec_flush(d->codec);
// 重置渲染开始时间
d->renderstart = -1;
// 重置输入和输出数据的末尾标记
d->sawInputEOS = false;
d->sawOutputEOS = false;
if (!d->isPlaying) {
// 如果暂停状态,只渲染一次
d->renderonce = true;
// 发送编解码缓冲区消息
post(kMsgCodecBuffer, d);
}
LOGV("seeked");
} break;
case kMsgPause: {
workerdata *d = (workerdata *)obj;
if (d->isPlaying) {
// 标记为暂停状态
d->isPlaying = false;
// 发送暂停确认消息,清空其他未处理的编解码缓冲区消息
post(kMsgPauseAck, NULL, true);
}
} break;
case kMsgResume: {
workerdata *d = (workerdata *)obj;
if (!d->isPlaying) {
// 重置渲染开始时间
d->renderstart = -1;
// 标记为播放状态
d->isPlaying = true;
// 发送编解码缓冲区消息
post(kMsgCodecBuffer, d);
}
} break;
}
}
// 声明为 C 语言链接方式,用于 JNI 调用
extern "C" {
// 创建流媒体播放器
jboolean Java_com_example_nativecodec_NativeCodec_createStreamingMediaPlayer(
JNIEnv *env, jclass clazz, jobject assetMgr, jstring filename) {
LOGV("@@@ create");
// 将 Java 字符串转换为 UTF-8 字符串
const char *utf8 = env->GetStringUTFChars(filename, NULL);
LOGV("opening %s", utf8);
off_t outStart, outLen;
// 从资产管理器打开文件并获取文件描述符
int fd = AAsset_openFileDescriptor(
AAssetManager_open(AAssetManager_fromJava(env, assetMgr), utf8, 0),
&outStart, &outLen);
// 释放 Java 字符串
env->ReleaseStringUTFChars(filename, utf8);
if (fd < 0) {
LOGE("failed to open file: %s %d (%s)", utf8, fd, strerror(errno));
return JNI_FALSE;
}
// 保存文件描述符
data.fd = fd;
workerdata *d = &data;
// 创建媒体提取器对象
AMediaExtractor *ex = AMediaExtractor_new();
// 设置媒体提取器的数据源
media_status_t err = AMediaExtractor_setDataSourceFd(
ex, d->fd, static_cast<off64_t>(outStart), static_cast<off64_t>(outLen));
// 关闭文件描述符
close(d->fd);
if (err != AMEDIA_OK) {
LOGV("setDataSource error: %d", err);
return JNI_FALSE;
}
// 获取媒体文件的轨道数量
int numtracks = AMediaExtractor_getTrackCount(ex);
AMediaCodec *codec = NULL;
LOGV("input has %d tracks", numtracks);
for (int i = 0; i < numtracks; i++) {
// 获取轨道的格式信息
AMediaFormat *format = AMediaExtractor_getTrackFormat(ex, i);
const char *s = AMediaFormat_toString(format);
LOGV("track %d format: %s", i, s);
const char *mime;
if (!AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) {
LOGV("no mime type");
return JNI_FALSE;
} else if (!strncmp(mime, "video/", 6)) {
// 选择视频轨道
AMediaExtractor_selectTrack(ex, i);
// 创建解码器对象
codec = AMediaCodec_createDecoderByType(mime);
// 配置解码器
AMediaCodec_configure(codec, format, d->window, NULL, 0);
// 保存媒体提取器和编解码器指针
d->ex = ex;
d->codec = codec;
// 初始化渲染开始时间
d->renderstart = -1;
// 初始化输入和输出数据的末尾标记
d->sawInputEOS = false;
d->sawOutputEOS = false;
// 初始化播放状态
d->isPlaying = false;
// 标记只渲染一次
d->renderonce = true;
// 启动编解码器
AMediaCodec_start(codec);
}
// 释放格式信息对象
AMediaFormat_delete(format);
}
// 创建自定义 looper 对象
mlooper = new mylooper();
// 发送编解码缓冲区消息
mlooper->post(kMsgCodecBuffer, d);
return JNI_TRUE;
}
// 设置流媒体播放器的播放状态
void Java_com_example_nativecodec_NativeCodec_setPlayingStreamingMediaPlayer(
JNIEnv *env, jclass clazz, jboolean isPlaying) {
LOGV("@@@ playpause: %d", isPlaying);
if (mlooper) {
if (isPlaying) {
// 发送恢复播放消息
mlooper->post(kMsgResume, &data);
} else {
// 发送暂停消息
mlooper->post(kMsgPause, &data);
}
}
}
// 关闭原生媒体系统
void Java_com_example_nativecodec_NativeCodec_shutdown(JNIEnv *env,
jclass clazz) {
LOGV("@@@ shutdown");
if (mlooper) {
// 发送解码完成消息,清空其他未处理的消息
mlooper->post(kMsgDecodeDone, &data, true /* flush */);
// 停止 looper
mlooper->quit();
// 删除 looper 对象
delete mlooper;
mlooper = NULL;
}
if (data.window) {
// 释放原生窗口对象
ANativeWindow_release(data.window);
data.window = NULL;
}
}
// 设置播放的表面
void Java_com_example_nativecodec_NativeCodec_setSurface(JNIEnv *env,
jclass clazz,
jobject surface) {
// 从 Java 表面获取原生窗口对象
if (data.window) {
// 释放之前的原生窗口对象
ANativeWindow_release(data.window);
data.window = NULL;
}
data.window = ANativeWindow_fromSurface(env, surface);
LOGV("@@@ setsurface %p", data.window);
}
// 重播
void Java_com_example_nativecodec_NativeCodec_rewindStreamingMediaPlayer(
JNIEnv *env, jclass clazz) {
LOGV("@@@ rewind");
if (mlooper) {
// 定位消息
mlooper->post(kMsgSeek, &data);
}
}
}
ndk 中除了 mediandk 库可以处理多媒体数据外,还可以使用libOpenMAXAL 库。 mediandk 库和 libOpenMAXAL 库的关系:libOpenMAXAL 是更底层的多媒体处理,mediandk 库的视频解码等功能就是依赖于 libOpenMAXAL 库。