Android NDK 示例(五)使用 mediandk 播放视频

在 Android ndk 中,我们可以使用 mediandk 原生库来播放视频。mediandk 库主要依赖 ANativeWindowAMediaExtractorAMediaCodec 这三个类来实现视频播放的。

  • 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 库。

参考

相关推荐
Kapaseker1 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴2 小时前
Android17 为什么重写 MessageQueue
android
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android