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

参考

相关推荐
pengyu1 小时前
系统化掌握Dart网络编程之Dio(二):配置管理篇
android·flutter·dart
Yang-Never1 小时前
Open GL ES ->纹理贴图,顶点坐标和纹理坐标组合到同一个顶点缓冲对象中进行解析
android·java·开发语言·android studio·贴图
a3158238062 小时前
SnapdragonCamera骁龙相机源码解析
android·数码相机·framework·高通
IT乐手2 小时前
adb logcat 写文件乱码的解决方案
android·python
xiaoduyyy2 小时前
【Android】View动画—XML动画、帧动画
android·xml
weixin_454102463 小时前
cordova android12+升级一些配置注意事项
android·前端·cordova
兰亭序咖啡4 小时前
学透Spring Boot — 007. 加载外部配置
android·java·spring boot
8931519604 小时前
Android穿山甲banner广告穿插到项目的banner中
android·android开发·android教程·穿山甲banner广告加入项目
利明的博客5 小时前
mediacodec服务启动时加载media_codecs.xml
android·xml·java
高林雨露6 小时前
Kotlin 基础语法解析
android·开发语言·kotlin