基于 FFmpeg 的自定义 Media Extractor(1):播放器基本原理及 Extractor 框架简析

本文介绍了Android多媒体框架中MediaExtractor的作用,包括播放器的基本原理,如解协议、解封装、解码和音视频同步,以及ExtractorService的工作原理和加载流程,重点讲解了MediaExtractor在媒体文件扫描和播放中的关键角色。
摘要由CSDN通过智能技术生成

前言

在 Android 多媒体框架中,Media Extractor 扮演着十分重要的角色。本文作为自定义 Media Extractor 系列的第一篇文章,将对播放器的一些基本原理进行普及,然后简要介绍 Android 系统中 Media Extractor 的框架结构,以及 Extractor 组件加载流程。本文 基于 Android 11,主要目的为普及 Android Extractor 的基本框架以及工作原理,不对 Android 源代码展开过多的分析。

一、 播放器基本原理

该章节内容引自 雷霄骅-视音频编解码技术零基础学习方法,感谢雷神留下的宝贵财富,为无数从事音视频开发工作的同学拨开了迷雾。

播放器框架简介

播放器是一种典型的输入输出模型。输入媒体文件,输出音视频原始数据到设备(音响、电视、显示器…)。播放器播放一个视频,需要经过解协议,解封装,解码音视频,音视频同步四个基本步骤。过程如图所示:
播放器基本原理

在播放器中,通常将负责解析协议模块称为 source(媒体源)、解封装模块为demuxer、解码模块为decoder(分为 audio decoder 和 video decoder)、音视频同步模块为 avsync。

解协议

音视频在网络上传播,需要采用各种网络协议(HTTP,RTMP,HLS等等)。从网络下载的数据,除了包含本身的音视频数据,可能还会包含信令数据。解协议的过程就是从网络下载数据并去除掉信令数据,只保留音视频数据的过程。例如,采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。播放已下载的本地视频时(如 MP4 文件),实质上没有解协议的过程,但可视为一种特殊的协议。

解封装

封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。解封装的目的就是将音视频数据从封装格式数据中分离出来。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。
常见封装格式:
常见媒体格式

解码

原始的音视频数据体积庞大,为了便于音视频的存储和传输,通常进行会压缩以降低音视频的数据量。音频的压缩编码标准包含 AAC,MP3,AC-3 等等,视频的压缩编码标准则包含 H.264,MPEG2,VC-1 等等。解码是整个播放器系统中最重要也是最复杂的环节,目的在于将压缩的音视频数据解压缩成为设备能够播放的数据。

音视频同步(音画同步)

由于播放音频和视频是不同的设备,如果送给音频设备和送给视频设备数据的时间戳不匹配,则会导致音视频不同步(音频快于视频或视频快于音频),因此在最终的音视频数据送给设备播放前,还需要对音视频数据进行同步。

播放器控制

常见的播放器控制逻辑:

  • 准备(prepare),一般会进行 source、demuxer、decoder、avsync 等模块的初始化
  • 播放(play),开始播放
  • 暂停(pause),暂停播放,暂停播放不释放 prepare 时已初始化的资源
  • 恢复(resume),暂停后恢复播放
  • 停止(stop),stop通常会释放 prepare 时初始化的资源
  • 定位(seek),从指定位置播放,常见的操作如拖动进度条

元数据(metadata)

用于描述媒体文件的数据被称为元数据,元数据包含了媒体文件的时长、大小、编码格式、封装格式等信息,如下图:
metadata

二、Media Extractor 框架简析

Media Extractor 直译为媒体提取器,顾名思义,其用途为提取媒体源中的音视频数据。在上一章节 播放器基本原理 中充当 demuxer 角色,完成解封装的任务:
解封装
在 Android Multimedia 框架中,Media Extractor 为媒体文件的扫描(MeidaProvider)以及播放(MediaPlayer)提供重要的支撑。

ExtractorService 的工作原理

Android 7.0 以上系统以 ExtractorService(Binder Service) 的形式为各类媒体应用提供 Extractor 支持(媒体框架强化):
媒体框架强化
Android 9.0 以上版本,Extractor 被编译为单个 .so 库 [4],ExtractorService 服务通过 dlopen 的方式加载 Extractor 组件[2]
在这里插入图片描述
ExtractorService 进程名为 media.extractor。我们可以使用 adb shell dumpsys media.extractor 来查看当前系统中已加载的 Extractor 组件。下图详细显示了系统中已加载的 Extractor 组件信息:
dumpsys 指令

Extractor 加载流程

上一节提到了 Extractor 以 .so 的形式被 ExtractorService 加载,在代码中这部分工作实际是在 MediaExtractorService 的构造函数中完成,我们打开 MediaExtractorService.cpp 文件,查看 MediaExtractorService 构造函数:

MediaExtractorService::MediaExtractorService() {
    MediaExtractorFactory::LoadExtractors();
}

继续跳转到 MediaExtractorFactory.cpp 中,查看 MediaExtractorFactory::LoadExtractors 函数:

void MediaExtractorFactory::LoadExtractors() {
    Mutex::Autolock autoLock(gPluginMutex);

    if (gPluginsRegistered) {
        return;
    }

    gIgnoreVersion = property_get_bool("debug.extractor.ignore_version", false);

    std::shared_ptr<std::list<sp<ExtractorPlugin>>> newList(new std::list<sp<ExtractorPlugin>>());

    android_namespace_t *mediaNs = android_get_exported_namespace("com_android_media");
    if (mediaNs != NULL) {
        const android_dlextinfo dlextinfo = {
            .flags = ANDROID_DLEXT_USE_NAMESPACE,
            .library_namespace = mediaNs,
        };
        RegisterExtractors("/apex/com.android.media/lib"
#ifdef __LP64__
                "64"
#endif
                "/extractors", &dlextinfo, *newList);

    } else {
        ALOGE("couldn't find media namespace.");
    }

    RegisterExtractors("/system/lib"
#ifdef __LP64__
            "64"
#endif
            "/extractors", NULL, *newList);

    RegisterExtractors("/system_ext/lib"
#ifdef __LP64__
            "64"
#endif
            "/extractors", NULL, *newList);

    newList->sort(compareFunc);
    gPlugins = newList;

    for (auto it = gPlugins->begin(); it != gPlugins->end(); ++it) {
        if ((*it)->def.def_version == EXTRACTORDEF_VERSION_NDK_V2) {
            for (size_t i = 0;; i++) {
                const char* ext = (*it)->def.u.v3.supported_types[i];
                if (ext == nullptr) {
                    break;
                }
                gSupportedExtensions.push_back(std::string(ext));
            }
        }
    }

    gPluginsRegistered = true;
}

从上述代码,我们可以知道 ExtractorService 会从 3 个路径中加载 Extractor:

/apex/com.android.media/lib[64]/extractors/
/system/lib[64]/extractors/
/system_ext/lib[64]/extractors/

根据 Android 官方开发者文档的描述 [3]如果默认的媒体提取器无法满足您的需求,您可以在 /system/lib[64]/extractors/ 中放置自定义提取器插件。提取器进程会自动从 Google 提供的 APEX 软件包和 /system/lib[64]/extractors/ 加载提取器插件。 根据描述:

  • 上述 Google 提供的 APEX 软件包 中的 Extractor 组件库,即 /apex/com.android.media/lib[64]/extractor 下的 so 库
  • 用户自定义的 Extractor 则可放置在 /system/lib[64]/extractors 或 /system_ext/lib[64]/extractor 目录中

我们继续分析 MediaExtractorFactory::RegisterExtractors 函数代码:

void MediaExtractorFactory::RegisterExtractors(
        const char *libDirPath, const android_dlextinfo* dlextinfo,
        std::list<sp<ExtractorPlugin>> &pluginList) {
    ALOGV("search for plugins at %s", libDirPath);

    DIR *libDir = opendir(libDirPath);
    if (libDir) {
        struct dirent* libEntry;
        while ((libEntry = readdir(libDir))) {
            if (libEntry->d_name[0] == '.') {
                continue;
            }
             
            // 如果文件名不包含 "extractor.so",跳过
            String8 libPath = String8(libDirPath) + "/" + libEntry->d_name;
            if (!libPath.contains("extractor.so")) {
                continue;
            }
            
            // 加载动态链接库
            void *libHandle = android_dlopen_ext(
                    libPath.string(),
                    RTLD_NOW | RTLD_LOCAL, dlextinfo);
            CHECK(libHandle != nullptr)
                    << "couldn't dlopen(" << libPath.string() << ") " << strerror(errno);

            // 获取动态链接库中的 GETEXTRACTORDEF 函数
            GetExtractorDef getDef =
                (GetExtractorDef) dlsym(libHandle, "GETEXTRACTORDEF");
            CHECK(getDef != nullptr)
                    << libPath.string() << " does not contain sniffer";

            ALOGV("registering sniffer for %s", libPath.string());
            RegisterExtractor(
                    new ExtractorPlugin(getDef(), libHandle, libPath), pluginList);
        }
        closedir(libDir);
    } else {
        ALOGE("couldn't opendir(%s)", libDirPath);
    }
}

上述代码加载 Extractor 组件 so 核心步骤为:查找 libDirPath 路径下所有非隐藏的以 “extractor.so” 结尾的 so 库;加载查找到的动态库,并获取库中的 GetExtractorDef 函数。进入 MediaExtractorPluginApi.h 继续查看 GetExtractorDef 的定义:

struct ExtractorDef {
    // ExtractorDef 结构体版本号
    const uint32_t def_version;

    // extractor 唯一标识符
    media_uuid_t extractor_uuid;

    // extractor 版本号,当遇到两个具有相同 uuid 的提取器时,将使用版本号最大的那个
    const uint32_t extractor_version;

    // extractor 名称
    const char *extractor_name;

    union {
        struct {
            SnifferFunc sniff;
        } v2;
        struct {
            SnifferFunc sniff;
            // 支持的媒体类型和/或文件扩展名的列表,以 NULL 结尾
            const char **supported_types;
        } v3;
    } u;
};

// 每个 extractor 插件库都会导出该特定类型的函数
typedef ExtractorDef (*GetExtractorDef)();

MediaExtractorPluginApi.h 文件中继续查看 SnifferFunc 的定义:

typedef CMediaExtractor* (*CreatorFunc)(CDataSource *source, void *meta);

// The sniffer can optionally fill in an opaque object, "meta", that helps
// the corresponding extractor initialize its state without duplicating
// effort already exerted by the sniffer. If "freeMeta" is given, it will be
// called against the opaque object when it is no longer used.
typedef CreatorFunc (*SnifferFunc)(
        CDataSource *source, float *confidence,
        void **meta, FreeMetaFunc *freeMeta);

分析上述代码,我们知道了 SnifferFunc 返回 CreatorFunc,通过 CreatorFunc 可以创建 CMediaExtractor。看到 CMediaExtractor 这个名字,想必大家已经明白,这就是本文主角:Media Extractor 插件本尊。相关定义如下:

struct CMediaTrack {
void *data;
void (*free)(void *data);

media_status_t (*start)(void *data, CMediaBufferGroup *bufferGroup);
media_status_t (*stop)(void *data);
media_status_t (*getFormat)(void *data, AMediaFormat *format);
media_status_t (*read)(void *data, CMediaBuffer **buffer, uint32_t options, int64_t seekPosUs);
bool (*supportsNonBlockingRead)(void *data);
};

struct CMediaExtractor {
void *data;

void (*free)(void *data);
size_t (*countTracks)(void *data);
CMediaTrack* (*getTrack)(void *data, size_t index);
media_status_t (*getTrackMetaData)(
void *data,
AMediaFormat *meta,
size_t index, uint32_t flags);

media_status_t (*getMetaData)(void *data, AMediaFormat *meta);
uint32_t (*flags)(void *data);
media_status_t (*setMediaCas)(void *data, const uint8_t* casToken, size_t size);
const char * (*name)(void *data);
};

分析到此处,有音视频相关开发经验的同学,对 ExtractorService 加载 Extractor 组件的整个流程已了然于心。CMediaExtractorgetTrack 函数用于提取媒体文件中的音视频轨道,每个轨道被封装为 CMediaTrack。而 CMediaTrack 中的 readgetFormat 则分别用于读取音频或视频数据,以及 metadta。

原生 libmp3extractor.so 简析

上一章节真是冗长而无趣,我们以 /apex/com.android.media/lib64/extractors/libmp3extractor.so 为例,来巩固上一节中的 Extractor 组件加载流程:

  1. 放置路径为 3 个指定的位置之一,此处为 /apex/com.android.media/lib64/extractors/;
  2. 全称需以 “extractor.so” 作为后缀,且不能为 . 开头的隐藏文件;
  3. 库中能找到 GetExtractorDef 函数入口。
    readelf

我们再看看 MP3Extractor.cpp 源码:


// CreatorFunc,作为 SnifferFunc 返回值
static CMediaExtractor* CreateExtractor(
        CDataSource *source,
        void *meta) {
    Mp3Meta *metaData = static_cast<Mp3Meta *>(meta);
    return wrap(new MP3Extractor(new DataSourceHelper(source), metaData));
}

// SnifferFunc
static CreatorFunc Sniff(
        CDataSource *source, float *confidence, void **meta,
        FreeMetaFunc *freeMeta) {
    off64_t pos = 0;
    off64_t post_id3_pos;
    uint32_t header;
    uint8_t mpeg_header[5];
    DataSourceHelper helper(source);
    if (helper.readAt(0, mpeg_header, sizeof(mpeg_header)) < (ssize_t)sizeof(mpeg_header)) {
        return NULL;
    }

    if (!memcmp("\x00\x00\x01\xba", mpeg_header, 4) && (mpeg_header[4] >> 4) == 2) {
        ALOGV("MPEG1PS container is not supported!");
        return NULL;
    }
    if (!Resync(&helper, 0, &pos, &post_id3_pos, &header)) {
        return NULL;
    }

    Mp3Meta *mp3Meta = new Mp3Meta;
    mp3Meta->pos = pos;
    mp3Meta->header = header;
    mp3Meta->post_id3_pos = post_id3_pos;
    *meta = mp3Meta;
    *freeMeta = ::free;

    *confidence = 0.2f;

    return CreateExtractor;
}

static const char *extensions[] = {
    "mp2",
    "mp3",
    "mpeg",
    "mpg",
    "mpga",
    NULL
};

// ExtractorDef 函数
extern "C" {
// This is the only symbol that needs to be exported
__attribute__ ((visibility ("default")))
ExtractorDef GETEXTRACTORDEF() {
    return {
        EXTRACTORDEF_VERSION,
        UUID("812a3f6c-c8cf-46de-b529-3774b14103d4"),
        1, // version
        "MP3 Extractor",
        { .v3 = {Sniff, extensions} }
    };
}

} // extern "C"

源码中也能看到 ExtractorDef 函数的定义。

参考资料

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值