AOSP思绎|Android 多媒体:一段视频为什么需要这么多服务

在 Android 应用中,用 MediaPlayer 播放一段视频,通常从设置数据源、准备播放和调用 start() 开始。接口看起来简单,系统内部却要完成容器解析、音视频解码、播放同步、音频输出和画面显示。这些工作由不同组件协作完成,分布在应用进程和多个系统服务中。

这种分工也解释了开发中常见的现象:能读取视频时长,不代表解码器能处理其中的视频轨道;已经解码出视频帧,也不代表画面已经显示到屏幕上。遇到播放问题,先弄清数据走到哪一步、这一环节由谁负责,才能找到合适的排查入口。

本文从 AOSP 源码出发,以 MediaPlayer 的播放路径串起多媒体框架,再展开相机与录制链路,说明 MediaCodec、音频服务、显示系统以及媒体版权保护和资源管理各自承担什么职责。希望读完后,你既能把熟悉的应用 API 放回系统架构中,也能沿着控制请求和媒体数据的流向理解各模块如何协作。

从一段视频看多媒体框架的分工

先看播放架构。沿着媒体数据的流向,可以找到数据源、解封装、解码、音视频同步和设备输出;沿着控制请求,则可以找到负责接收调用、组织播放和管理资源的组件。

图 1:蓝色虚线表示控制请求,绿色实线表示媒体数据传递或处理结果。音频和视频在下方分别进入输出链路;侧栏是媒体版权保护、资源管理和编解码适配说明。灰色分组按职责划分,进程位置以节点中的标注为准。

模块 主要职责 代表组件
应用接口 设置数据源、配置参数、发起播放或录制 MediaPlayer / MediaRecorder / MediaCodec
播放服务与播放器 接收播放请求,协调数据源、解码和输出同步 MediaPlayerService / NuPlayerDriver / NuPlayer
容器解析 识别容器、读取轨道信息和轨道数据 MediaExtractorFactory / Extractor
编解码适配 配置组件,提交输入和接收输出 MediaCodec / ACodec / CCodec
音频策略与输出 选择输出设备,组织音频数据处理并对接 HAL AudioPolicyService / AudioFlinger
相机采集 配置相机会话,将采集数据交给输出目标 CameraService / Camera3Device / Camera HAL
媒体版权保护 管理 DRM 会话和密钥交互,提供内容解密接口 DrmHal / CryptoHal
媒体资源管理 记录资源占用,协调客户端释放资源 ResourceManagerService
视频显示 承载视频 Buffer,管理窗口,合成画面并输出 Surface / WMS / SurfaceFlinger

表中列的是不同职责,不能把每一行都当成一个独立进程,也不能把同一行中的组件理解成一条串行调用链。比如 Surface 承载视频输出,WMS 管理窗口布局和可见性,SurfaceFlinger 负责画面合成;视频像素并不需要先交给 WMS 再传给 SurfaceFlinger。

本文选择 MediaPlayer 的 NuPlayer 播放路径。应用直接使用 MediaCodec 实现播放器时,可以自行组织数据读取、解码和输出,不必经过 MediaPlayerService。

播放请求怎样进入系统

MediaPlayerService 接收请求,NuPlayer 组织播放

在这条路径中,MediaPlayerService 是服务入口,其 Client 对象保存对应客户端的播放器实例,并将 prepareAsync()、start() 等请求转交给它。播放器的具体类型由工厂根据输入等信息选择。

对于本文讨论的 NuPlayer 路径,NuPlayerDriver 对接服务侧的播放器接口,在内部创建 NuPlayer。后者协调 Source、Decoder 和 Renderer:Source 提供媒体数据,Decoder 对接编解码组件,Renderer 处理输出时序和音视频同步。

因此,架构图中的"播放服务与播放器协调"框列出了服务入口、播放器实现和内部职责。它们需要按各自的关系理解,不能把 Source、Decoder、Renderer 都解释成依次接收播放控制命令的服务。

源码:Client 选择播放器、Client 转交播放请求。

源码:NuPlayerDriver 创建 NuPlayer、NuPlayer 请求 Source 准备。

服务分工和进程部署是两回事

这里具体看三个入口:init 配置说明启动哪个进程,main_mediaserver.cpp 说明 mediaserver 注册哪些服务,main_audioserver.cpp 说明音频服务如何创建并注册。本文所用版本是 Android 17 的 android17-release 基线,源码链接固定到对应仓库提交。

先确认进程。 两份 init 配置分别启动不同的可执行文件:

源码位置:frameworks/av/media/mediaserver/mediaserver.rc。

text 复制代码
service media /system/bin/mediaserver
    class main
    user media
    group audio camera inet net_bt net_bt_admin net_bw_acct drmrpc mediadrm

源码位置:frameworks/av/media/audioserver/audioserver.rc。

text 复制代码
service audioserver /system/bin/audioserver
    class core
    user audioserver

这里 service media 和 service audioserver 是 init 服务名,后面的路径是各自启动的程序。仅凭 MediaPlayerService、AudioFlinger 这些类名,无法判断它们是否位于同一进程;这两份配置才是部署关系的直接入口。

再看 mediaserver 的服务注册。 在它的 main() 中,可以直接找到:

源码位置:frameworks/av/media/mediaserver/main_mediaserver.cpp。

cpp 复制代码
MediaPlayerService::instantiate();
ResourceManagerService::instantiate();

这说明 mediaserver 启动时注册播放服务和资源管理服务。接下来转到 audioserver 的 main(),音频服务的创建与公开分成了两个步骤:

源码位置:frameworks/av/media/audioserver/main_audioserver.cpp。

cpp 复制代码
const auto af = sp<AudioFlinger>::make();
const auto afAdapter = sp<AudioFlingerServerAdapter>::make(af);

// ...省略与当前结论无关的代码...

const auto aps = sp<AudioPolicyService>::make();
af->initAudioPolicyLocal(aps);

// ...省略与当前结论无关的代码...

aps->onAudioSystemReady();

// ...省略与当前结论无关的代码...

// Add AudioFlinger and AudioPolicy to ServiceManager.
sp<IServiceManager> sm = defaultServiceManager();
sm->addService(String16(IAudioFlinger::DEFAULT_SERVICE_NAME), afAdapter,
        false /* allowIsolated */, IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT);
sm->addService(String16(AudioPolicyService::getServiceName()), aps,
        false /* allowIsolated */, IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT);

先创建 AudioFlinger 和 AudioPolicyService,建立本地关联并完成相关初始化,再通过 addService() 向 ServiceManager 注册。这里的 afAdapter 是 AudioFlinger 的服务适配对象,aps 是音频策略服务对象。源码注释解释了这个顺序:如果先公开 AudioFlinger,远程调用可能在 AudioPolicy 尚未初始化完成时到达。

从这几段代码可以得到明确的部署结论:播放服务与资源管理位于 mediaserver;这里的音频执行和音频策略服务位于 audioserver。它们如何传递控制请求和媒体数据,则需要继续阅读接口和 Buffer 相关实现。服务注册代码本身不能证明"每一帧都通过 Binder 搬运",因此不能用进程划分直接替代数据通路分析。

源码:mediaserver 注册播放器与资源管理、audioserver 的本地初始化与注册。

媒体数据经过哪些处理

先解封装,再交给解码器

MP4、WebM 等是容器格式,H.264、HEVC、AAC 等是轨道使用的编码格式。Extractor 负责从容器中读出轨道信息和数据;解码器再根据编码格式及配置处理这些数据。

MediaExtractorFactory 负责选择提取器,MediaCodec 负责配置编解码组件。因此,能读取时长和轨道信息,只能说明部分容器解析工作已经完成,不能证明解码器支持这条轨道的编码规格(profile)、分辨率和其他参数。

源码位置:frameworks/av/media/libstagefright/MediaExtractorFactory.cpp。

cpp 复制代码
sp<IMediaExtractor> MediaExtractorFactory::Create(
        const sp<DataSource> &source, const char *mime) {
    ALOGV("MediaExtractorFactory::Create %s", mime);

    if (!property_get_bool("media.stagefright.extractremote", true)) {
        // local extractor
        ALOGW("creating media extractor in calling process");
        return CreateFromService(source, mime);

// ...省略与当前结论无关的代码...

sp<IBinder> binder = defaultServiceManager()->getService(String16("media.extractor"));

if (binder != 0) {
    sp<IMediaExtractorService> mediaExService(
            interface_cast<IMediaExtractorService>(binder));
    sp<IMediaExtractor> ex;
    mediaExService->makeExtractor(
            CreateIDataSourceFromDataSource(source),
            mime ? std::optional<std::string>(mime) : std::nullopt,
            &ex);
    return ex;

// ...省略与当前结论无关的代码...

sp<IMediaExtractor> MediaExtractorFactory::CreateFromService(
        const sp<DataSource> &source, const char *mime) {

// ...省略与当前结论无关的代码...

void *meta = nullptr;
void *creator = NULL;
FreeMetaFunc freeMeta = nullptr;
float confidence;
sp<ExtractorPlugin> plugin;
uint32_t creatorVersion = 0;
creator = sniff(source, &confidence, &meta, &freeMeta, plugin, &creatorVersion);
if (!creator) {

这里有两层判断。Create() 先根据 media.stagefright.extractremote 选择本地创建或请求 media.extractor 服务;实际提取器创建逻辑再通过 sniff() 识别输入并选择插件。creator 为空时进入失败处理。因此"找到提取服务"和"找到能识别该容器的插件"也是两个不同结果。

源码:MediaExtractorFactory::Create、MediaCodec::configure。

讨论这条通路时,下面几个术语需要分清:

术语 在本文中的含义
编码数据 经音视频编码格式表示的数据,是压缩轨道解码前的输入
容器轨道样本(Sample) 容器按轨道组织的样本及其时间、格式信息
访问单元(Access Unit,AU) 编码格式与接口所定义的处理单元,NuPlayer 的 Source/Decoder 接口使用这一名称
PCM 音频数据 常规音频解码输出或音频采集得到的数据,可进入混音处理
视频帧与 Surface Buffer 图像内容与承载这些内容的 Buffer;具体像素格式取决于组件和配置
容器复用与封装(Muxing) 将音视频轨道数据和时间信息组织到 MP4 等容器中

Sample 是容器层的概念,AU 是编解码接口中的概念,二者不能在所有格式下都简单画等号。容器内也可以存在 PCM 轨道,所以"Extractor 输出"不应一概写成"压缩码流"。

在 NuPlayer 中,GenericSource::dequeueAccessUnit() 提供轨道数据,Decoder 获取输入后提交给 MediaCodec,并将相应输出交给 Renderer。Renderer 根据媒体时间安排输出,而不是再做一次解码。

源码位置:frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp。

cpp 复制代码
status_t NuPlayer::Decoder::fetchInputData(sp<AMessage> &reply) {
    sp<ABuffer> accessUnit;
    bool dropAccessUnit = true;
    do {
        status_t err = mSource->dequeueAccessUnit(mIsAudio, &accessUnit);

        if (err == -EWOULDBLOCK) {
            return err;

// ...省略与当前结论无关的代码...

err = mCodec->queueInputBuffer(
        bufferIx,
        codecBuffer->offset(),
        codecBuffer->size(),
        timeUs,
        flags,
        &errorDetailMsg);

// ...省略与当前结论无关的代码...

mRenderer->queueBuffer(mIsAudio, buffer, reply);

三个片段分别来自输入获取、输入提交和输出处理路径:mSource 提供 Access Unit,mCodec 接收输入 Buffer,mRenderer 接收相应输出。timeUs 是提交输入时携带的媒体时间戳,后续输出同步需要使用媒体时间信息。

源码:GenericSource::dequeueAccessUnit()、Decoder 获取 Access Unit、Decoder 提交输入 Buffer、Renderer::queueBuffer()。

ACodec 与 CCodec 是两种编解码适配实现

MediaCodec 提供统一的编解码控制接口,通过适配实现对接具体组件:ACodec 对接 OMX,CCodec 对接 Codec2。它们都属于组件控制与适配层;组件本身可以由软件或硬件实现。

创建时,GetCodecBase() 先检查组件的归属标识 owner。如果归属标识无法确定适配实现,再检查组件名称。源码中的顺序如下:

顺序 条件 结果
1 owner 为 default 创建 ACodec
2 owner 以 codec2 开头 创建 CCodec
3 前两项不满足,组件名以 c2. 开头 创建 CCodec
4 前三项不满足,组件名以 omx. 开头 创建 ACodec
5 都不满足 返回空对象

组件名称前缀的匹配不区分大小写。这张表说明的是创建时的选择规则,不能据此推导出解码失败后的自动重试策略。

graph TD M[MediaCodec 创建编解码适配对象] --> S[检查组件归属标识 owner] S -->|default| A[ACodec] S -->|codec2 前缀| C[CCodec] S -->|其他或未提供| N[检查组件名称] N -->|c2. 前缀| C N -->|omx. 前缀| A N -->|其他| E[返回空对象] A --> O[对接 OMX 组件] C --> K[对接 Codec2 组件]

源码位置:frameworks/av/media/libstagefright/MediaCodec.cpp。

cpp 复制代码
sp<CodecBase> MediaCodec::GetCodecBase(const AString &name, const char *owner) {
    if (owner) {
        if (strcmp(owner, "default") == 0) {
            return new ACodec;
        } else if (strncmp(owner, "codec2", 6) == 0) {
            return CreateCCodec();
        }
    }

    if (name.startsWithIgnoreCase("c2.")) {
        return CreateCCodec();
    } else if (name.startsWithIgnoreCase("omx.")) {
        // at this time only ACodec specifies a mime type.
        return new ACodec;
    } else {
        return NULL;
    }
}

归属标识一旦匹配,函数就立即返回对应的适配对象;只有归属标识未匹配时,才继续检查组件名称。

源码:MediaCodec::GetCodecBase()、ACodec::initiateAllocateComponent()、ACodec 的 OMX 节点分配、CCodec::initiateAllocateComponent()。

音频策略决定输出,AudioFlinger 组织执行

音频播放需要回答两个问题:选哪个输出设备,以及怎样把音频数据送到这个设备。

AudioPolicyManager 根据音频属性、设备和策略配置等信息选择输出;AudioFlinger 创建 Track,并在相应输出线程中处理音频数据、对接音频 HAL。在 createTrack 路径中,AudioFlinger 会获取策略选择的输出,所以两者是协作关系。

常规混音路径处理 PCM。Direct(直出)和 Offload(卸载)使用不同的输出路径,不能把所有音频播放都画成"先解码成 PCM,再进入普通混音线程"。实际走哪条路径,要结合格式、请求参数和设备能力判断。

源码位置:frameworks/av/services/audioflinger/AudioFlinger.cpp。

cpp 复制代码
lStatus = AudioSystem::getOutputForAttr(&localAttr, &output.outputId, sessionId, &streamType,
                                        adjAttributionSource, &input.config, input.flags,
                                        &selectedDeviceIds, &portId, &secondaryOutputs,
                                        &isSpatialized, &isBitPerfect);
output.selectedDeviceIds = selectedDeviceIds;

if (lStatus != NO_ERROR || output.outputId == AUDIO_IO_HANDLE_NONE) {
    ALOGE("createTrack() getOutputForAttr() return error %d or invalid output handle", lStatus);
    goto Exit;
}

// ...省略与当前结论无关的代码...

{
    audio_utils::lock_guard _l(mutex());
    IAfPlaybackThread* thread = checkPlaybackThread_l(output.outputId);
    if (thread == NULL) {
        ALOGE("no playback thread found for output handle %d", output.outputId);
        lStatus = BAD_VALUE;
        goto Exit;
    }

getOutputForAttr() 返回所选输出的句柄 output.outputId,随后 checkPlaybackThread_l() 按这个句柄查找播放线程。策略没有提供有效输出,或者找不到对应线程,都会进入失败路径。这是"选择输出"和"组织执行"之间的具体衔接点,后续 Track 创建和数据处理仍有其他步骤。

源码:AudioPolicyManager::getOutputForAttr、createTrack 中获取策略输出、audioserver 的本地初始化与注册。

解码完成与画面显示之间还有输出同步

NuPlayer 的 Renderer 根据媒体时间安排视频输出。解码得到视频帧,只能说明解码阶段有了结果;该帧还需要按输出时机交给 Surface 链路,随后参与 SurfaceFlinger 的合成和显示。

因此,黑屏问题除了检查轨道和解码器,还要检查输出 Surface 是否有效、窗口是否可见、Buffer 是否持续交付。WMS 和 SurfaceFlinger 在这里承担不同职责,不能只用"显示模块"四个字把它们的行为混在一起。

源码位置:frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp。

cpp 复制代码
int64_t nowUs = ALooper::GetNowUs();
int64_t realTimeUs;
int64_t mediaTimeUs = -1;
if (mFlags & FLAG_REAL_TIME) {
    CHECK(entry->mBuffer->meta()->findInt64("timeUs", &realTimeUs));
} else {
    CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));

    realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
}
realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;

普通媒体时间路径从 Buffer 中取出 timeUs,通过 getRealTimeUs() 换算为输出时刻,再交给视频调度器安排。这里体现的是"何时输出"的职责;解码器提供一帧和这一帧进入显示链路之间,仍隔着时序处理。

源码:Renderer 的视频输出调度、WMS 执行窗口 Surface 布局、SurfaceFlinger 合成入口。

版权保护和资源管理为何放在侧栏

Media DRM:会话、密钥交互和内容解密

这里的 Media DRM 是 Digital Rights Management(数字版权管理),与 Linux 图形显示子系统中的 Direct Rendering Manager(DRM)是两个概念。

DrmHal 对接 DRM 插件,管理会话,并处理密钥请求和响应;CryptoHal 提供加密媒体数据解密接口。它们与受保护内容的播放相配合,不能画成所有视频解码之后才执行的一道步骤。

对受保护内容来说,获得许可证也不等于设备一定能够播放。安全解码与输出保护要求还需要结合内容策略和设备能力确认。架构图因此将版权保护独立列出,提醒读者在配置和播放过程中检查这些条件。

源码位置:frameworks/av/drm/libmediadrm/DrmHal.cpp。

cpp 复制代码
DrmStatus DrmHal::openSession(DrmPlugin::SecurityLevel securityLevel, Vector<uint8_t>& sessionId) {
    if (mDrmHalAidl->initCheck() == OK) return mDrmHalAidl->openSession(securityLevel, sessionId);
    return mDrmHalHidl->openSession(securityLevel, sessionId);
}

// ...省略与当前结论无关的代码...

DrmStatus DrmHal::provideKeyResponse(Vector<uint8_t> const& sessionId,
                                     Vector<uint8_t> const& response, Vector<uint8_t>& keySetId) {
    if (mDrmHalAidl->initCheck() == OK)
        return mDrmHalAidl->provideKeyResponse(sessionId, response, keySetId);
    return mDrmHalHidl->provideKeyResponse(sessionId, response, keySetId);
}

源码位置:frameworks/av/drm/libmediadrm/CryptoHal.cpp。

cpp 复制代码
ssize_t CryptoHal::decrypt(const uint8_t key[16], const uint8_t iv[16], CryptoPlugin::Mode mode,
                           const CryptoPlugin::Pattern& pattern, const ::SharedBuffer& source,
                           size_t offset, const CryptoPlugin::SubSample* subSamples,
                           size_t numSubSamples, const ::DestinationBuffer& destination,
                           AString* errorDetailMsg) {
    // This requires plugin to be created.
    if (mCryptoHalAidl->initCheck() == OK)
        return mCryptoHalAidl->decrypt(key, iv, mode, pattern, source, offset, subSamples,
                                       numSubSamples, destination, errorDetailMsg);
    return mCryptoHalHidl->decrypt(key, iv, mode, pattern, source, offset, subSamples,
                                   numSubSamples, destination, errorDetailMsg);
}

DrmHal 的片段展示会话创建和密钥响应处理,CryptoHal 的片段展示解密调用。两者根据 AIDL 实现的初始化状态选择对应实现;参数也体现了职责差异:前者处理 sessionId 和密钥响应,后者处理密钥标识、初始化向量 iv、源 Buffer 与目标 Buffer。源码中名为 key 的解密参数不能简单理解为应用拿到了明文密钥。

源码:DrmHal 会话与密钥交互、CryptoHal 解密接口。

ResourceManagerService:记录占用,协调资源回收

ResourceManagerService 记录客户端登记的资源占用。在编解码场景中,这些记录帮助系统判断哪些客户端持有相关资源。

收到回收请求后,服务按资源策略选择目标客户端,再调用对应客户端的 reclaimResource(),请求它释放资源。服务负责选择和协调,具体释放由客户端执行,回收也可能失败。

这比"关联组件生命周期"更具体:组件使用资源时需要更新占用记录,发生回收时则由服务和客户端协作。分析第二路视频启动失败时,应把组件返回错误、资源请求和回收结果放在同一条时间线上查看。

源码位置:frameworks/av/services/mediaresourcemanager/ResourceManagerService.cpp。

cpp 复制代码
std::vector<ClientInfo> targetClients;
if (getTargetClients(clientInfo, resources, targetClients)) {
    // Reclaim all the target clients.
    *_aidl_return = reclaimUnconditionallyFrom(targetClients);
} else {
    // No clients to reclaim from.
    ALOGI("%s: There aren't any clients to reclaim from", __func__);
    // We need to log this failed reclaim as "no clients to reclaim from".
    targetClients.clear();
}

// ...省略与当前结论无关的代码...

for (const ClientInfo& targetClient : targetClients) {
    std::shared_ptr<IResourceManagerClient> client = nullptr;
    {
        std::scoped_lock lock{mLock};
        client = getClient_l(targetClient.mPid, targetClient.mClientId);
    }
    if (client == nullptr) {
        // skip already released clients.
        continue;
    }
    String8 log = String8::format("reclaimResource from client %p", client.get());
    mServiceLog->add(log);
    bool success;
    Status status = client->reclaimResource(&success);
    if (!status.isOk() || !success) {
        failedClientId = targetClient.mClientId;
        failedClientPid = targetClient.mPid;

第一段选择回收目标,第二段来自 reclaimUnconditionallyFrom():找到客户端后调用 client->reclaimResource(&success),再检查调用状态和释放结果。因此,发起回收、选出目标和客户端成功释放是三个观察点;服务完成一次请求处理,并不保证回收已经成功。

源码:登记资源、移除资源记录、选择回收对象、回调客户端请求释放。

录制与播放共享能力,但数据方向不同

典型文件播放从容器读取轨道数据,再解码并输出;录制从相机、音频采集或输入 Surface 获取数据,编码后封装到文件。两条路径都可以使用 MediaCodec 及资源管理能力,但播放与录制分别由各自的实现组织。

图 2:上方是录制控制请求,标明应用进程与 mediaserver 中的对象;下方是采集、编码、封装和文件写入的数据通路。蓝色虚线表示控制请求,绿色实线表示媒体数据传递与文件写入。下方按职责分组,具体输入由录制配置决定。

录制控制对象在哪里运行

MediaRecorder 是应用侧录制接口,MediaRecorderClient 和 StagefrightRecorder 则位于 mediaserver。应用的控制请求通过 Binder 到达 MediaRecorderClient,再由它在进程内调用 StagefrightRecorder。

先看 Binder 代理的 start()。源码位置:frameworks/av/media/libmedia/IMediaRecorder.cpp,BpMediaRecorder::start()。

cpp 复制代码
status_t start()
{
    ALOGV("start");
    Parcel data, reply;
    data.writeInterfaceToken(IMediaRecorder::getInterfaceDescriptor());
    remote()->transact(START, data, &reply);
    return reply.readInt32();
}

remote()->transact(START, ...) 将启动请求发给服务端,并从回复中读取结果。这条连线传递的是启动请求,不是采集到的音视频帧。

服务端的 MediaRecorderClient 在构造时创建 StagefrightRecorder。源码位置:frameworks/av/media/libmediaplayerservice/MediaRecorderClient.cpp。

cpp 复制代码
MediaRecorderClient::MediaRecorderClient(const sp<MediaPlayerService>& service,
        const AttributionSourceState& attributionSource)
{
    ALOGV("Client constructor");
    // attribution source already validated in createMediaRecorder
    mAttributionSource = attributionSource;
    mRecorder = new StagefrightRecorder(attributionSource);
    mMediaPlayerService = service;
}

同一个文件中的 start() 再调用这个录制对象:

cpp 复制代码
status_t MediaRecorderClient::start()
{
    ALOGV("start");
    Mutex::Autolock lock(mLock);
    if (mRecorder == NULL) {
        ALOGE("recorder is not initialized");
        return NO_INIT;
    }
    return mRecorder->start();

}

new StagefrightRecorder(...) 在 MediaRecorderClient 所在进程创建对象,mRecorder->start() 是进程内调用。这就是图中两个 mediaserver 节点之间的关系。

StagefrightRecorder 位于 mediaserver,负责组织采集源、编码器和 Writer;相机采集、音频采集及具体编解码处理仍可能涉及其他服务或 HAL。图中的"配置采集、编码和 Writer"表示协调职责,不能由此判断整条录制链路都在 mediaserver 内执行。

源码:服务端创建录制会话、Binder 启动请求、创建 StagefrightRecorder、服务端转交启动请求。

编码输出怎样交给 Writer

以 StagefrightRecorder 的 MP4/WebM 录制配置为例,系统根据输出格式创建 Writer;配置了视频输入时,建立媒体源和视频编码器,再将编码器输出加入 Writer;配置了音频输入且满足相应条件时,再加入音频编码轨道。只有音频或只有视频的录制,不必包含另一条分支。

录制结束时,StagefrightRecorder 还需要停止编码源和 Writer,完成写入与封装收尾。应用发出停止请求后,应继续关注调用结果和后续文件是否有效。

源码位置:frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp。

cpp 复制代码
sp<MediaWriter> writer;
sp<MPEG4Writer> mp4writer;
if (mOutputFormat == OUTPUT_FORMAT_WEBM) {
    writer = new WebmWriter(mOutputFd);
} else {
    writer = mp4writer = new MPEG4Writer(mOutputFd);
}

if (mVideoSource < VIDEO_SOURCE_LIST_END) {
    setDefaultVideoEncoderIfNecessary();

    sp<MediaSource> mediaSource;
    err = setupMediaSource(&mediaSource);
    if (err != OK) {
        return err;
    }

    sp<MediaCodecSource> encoder;
    err = setupVideoEncoder(mediaSource, &encoder);
    if (err != OK) {
        return err;
    }

    writer->addSource(encoder);
    mVideoEncoderSource = encoder;
    mTotalBitRate += mVideoBitRate;
}

// ...省略与当前结论无关的代码...

// disable audio for time lapse recording
const bool disableAudio = mCaptureFpsEnable && mCaptureFps < mFrameRate;
if (!disableAudio && mAudioSource != AUDIO_SOURCE_CNT) {
    err = setupAudioEncoder(writer);
    if (err != OK) return err;
    mTotalBitRate += mAudioBitRate;
}

停止录制包含哪些收尾工作

源码位置:frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp,stop()。

cpp 复制代码
int64_t stopTimeUs = systemTime() / 1000;
for (const auto &source : { mAudioEncoderSource, mVideoEncoderSource }) {
    if (source != nullptr && OK != source->setStopTimeUs(stopTimeUs)) {
        ALOGW("Failed to set stopTime %lld us for %s",
                (long long)stopTimeUs, source->isVideo() ? "Video" : "Audio");
    }
}

if (mWriter != NULL) {
    err = mWriter->stop();
    mLastSeqNo = mWriter->getSequenceNum();
    mWriter.clear();
}

配置片段中,writer->addSource(encoder) 把视频编码器输出加入容器写入流程;音频是否加入由输入和延时摄影相关条件决定。停止片段先设置编码源的停止时间,再调用 mWriter->stop()。mWriter->stop() 的结果保存到 err,随后清理 Writer 引用。判断收尾是否成功,应检查返回状态;仅看到对象被清理还不够。

源码:配置 MP4/WebM 录制、启动录制、停止录制与收尾。

用流程和状态区分请求与完成

播放与录制分别看

下面分别看文件播放与录制的数据方向:

graph LR P0[播放数据源] --> P1[解封装与轨道读取] P1 --> P2[解码或适用的音频直出路径] P2 --> P3[输出同步] P3 --> P4[音频设备或视频显示]
graph LR R0[相机或音频采集] --> R1[音视频编码] R1 --> R2[容器复用与封装] R2 --> R3[写入录制文件]

图 3、图 4:播放读取已有媒体,录制生成媒体文件。两条图表示数据方向,不表示所有节点严格按串行时序启动。

播放状态:以 NuPlayerDriver 为例

prepareAsync() 返回成功,只能说明准备请求被接受。在正常异步准备路径中,NuPlayerDriver 从 STATE_UNPREPARED 进入 STATE_PREPARING;准备完成回调成功后,才进入 STATE_PREPARED。随后调用 start(),进入 STATE_RUNNING。

graph LR U[STATE_UNPREPARED 尚未准备] -->|prepareAsync| P[STATE_PREPARING 准备中] P -->|准备完成且成功| D[STATE_PREPARED 准备完成] D -->|start| R[STATE_RUNNING 运行中] R -->|pause| A[STATE_PAUSED 已暂停] A -->|start| R

图 5:NuPlayerDriver 正常准备、播放和暂停路径的状态子集。数据源设置、停止、重置和错误分支未在此展开。状态名称来自该对象的实现,不等同于应用 API 或其他组件的状态。

这个状态图描述播放对象,与 MediaRecorder 的录制状态分别管理。即使进入 STATE_RUNNING,也不能仅凭状态值证明用户已经看到首帧或听到声音。播放器状态、数据输出和设备呈现是不同的观察点,需要结合回调和输出记录判断。

源码位置:frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDriver.cpp。

prepareAsync() 接受请求:

cpp 复制代码
case STATE_UNPREPARED:
    mState = STATE_PREPARING;
    mIsAsyncPrepare = true;
    mPlayer->prepareAsync();
    return OK;

返回 OK 时,状态已经设为 STATE_PREPARING,准备工作仍在继续。

notifyPrepareCompleted() 处理准备结果:

cpp 复制代码
mAsyncResult = err;

if (err == OK) {
    // update state before notifying client, so that if client calls back into NuPlayerDriver
    // in response, NuPlayerDriver has the right state
    mState = STATE_PREPARED;
    if (mIsAsyncPrepare) {
        notifyListener_l(MEDIA_PREPARED);
    }
} else {
    mState = STATE_UNPREPARED;
    if (mIsAsyncPrepare) {
        notifyListener_l(MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, err);
    }
}

成功时进入 STATE_PREPARED 并发送准备完成通知;失败则回到 STATE_UNPREPARED,发送错误通知。

start_l() 启动播放:

cpp 复制代码
case STATE_PAUSED:
case STATE_STOPPED_AND_PREPARED:
case STATE_PREPARED:
{
    mPlayer->start();

    FALLTHROUGH_INTENDED;
}
// ...
mState = STATE_RUNNING;

return OK;

这些状态分别对应请求接受、准备完成和启动处理。首帧与音频输出还要结合各自的输出记录观察。

源码:异步准备、准备完成回调、启动与运行状态、暂停。

从架构分工落到开发排查

从架构角度看,多媒体框架把容器兼容性、编解码组件、设备路由、显示以及资源约束分开处理。分析协作关系时,除了找到调用者和被调用者,还要确认数据由谁提供、资源由谁持有、完成结果由谁通知。

从应用开发角度看,应记录自己能够观察到的时间点:数据源设置、准备回调、启动请求、播放错误和停止请求。自建 MediaCodec 播放器还可以记录轨道发现、组件配置、输入输出 Buffer 等细节。平台排查则继续关联音频 Track、Surface 输出和资源回收记录,根据所使用的 API 选择能够获取的记录。

在车载系统中,还应记录用户、显示屏和音频设备上下文。例如视频已经解码,但显示在另一个目标上;音频已经播放,却路由到了另一设备。这样的现象需要沿输出路径检查。

现象 优先检查 需要区分的结果
能读取时长,但播放黑屏 视频轨道参数、组件配置错误、输出 Surface 和窗口可见性 容器信息读取、解码输出和实际显示
视频正常,但没有声音 是否有音频轨道、输出设备选择、Track 是否持续处理数据 路由选择和音频数据输出
第二路视频启动失败 组件并发能力、配置错误、资源占用和回收结果 格式支持和资源可用性
录制启动失败 采集源配置、权限错误、编码器配置与资源请求结果 采集输入可用性和编码资源可用性
录制停止后文件异常 停止返回值、Writer 错误、编码输出和文件写入记录 已写入数据与容器收尾成功
受保护内容播放失败 DRM 会话、密钥响应、解密错误、安全解码和输出保护条件 内容授权与后续播放条件

这些检查项提供的是定位顺序。最终判断仍要把同一次操作的参数、时间点、错误码和组件记录对应起来。

音频焦点、媒体会话与原生音频接口

音频执行链路之外,还有应用间播放协调和系统媒体控制。AudioService 提供音频焦点等入口,MediaSession 连接系统媒体控制与应用回调;AAudio 则是另一种原生音频流接口,不能把三者都当成 NuPlayer 处理数据的必经环节。

例如,MediaSession 中的播放状态由应用上报,适合驱动系统控制界面,但不能直接证明 AudioFlinger 已经输出了声音。AAudio 的 MMAP 或 Legacy 路径选择,还受到流参数、系统策略和设备支持等条件影响。

源码位置:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java,setPlaybackState()。

java 复制代码
synchronized (mLock) {
    mPlaybackState = state;
    updateUserEngagedStateIfNeededLocked(
            /* isTimeoutExpired= */ false, isGlobalPrioritySessionActive);
}
// ...省略调用身份处理等逻辑...
mService.onSessionPlaybackStateChanged(
        MediaSessionRecord.this, shouldUpdatePriority, mPlaybackState);
// ...省略调用身份恢复...
mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);

这里保存传入的 state,通知媒体会话服务,再安排状态更新消息。该方法并没有读取 AudioFlinger 的设备输出结果,所以媒体控制界面的"播放中"应与实际音频输出分别观察。

源码:音频焦点请求、应用上报媒体会话状态、AAudio 的 MMAP 条件与 Legacy 路径。

思绎沉淀

同样处理音视频,解决的问题却不同

把 Android 多媒体框架与 FFmpeg、GStreamer 放在一起看,能更清楚地理解前面的模块分工。三者都能参与音视频处理,但提供能力的范围不同:FFmpeg 提供格式、编解码与滤镜等库,GStreamer 提供可组合的媒体管线框架,Android 则需要把媒体处理接入一个多应用共享设备的操作系统。

这里比较的是 FFmpeg 的库、GStreamer 框架与 Android 平台媒体栈。ffmpeg 命令行程序是这些库的一种使用方式,不等同于应用集成 FFmpeg 后的全部运行结构。

对比维度 FFmpeg GStreamer Android 平台媒体栈
核心组织方式 libavformat、libavcodec、libavfilter 等库分工,应用组织调用,滤镜内部可形成处理图 Element 通过 Pad 连接成 Pipeline,按插件组合处理能力 应用 API、播放器或录制实现、系统服务与 HAL 分工协作
数据处理接口 Packet、Frame 等数据结构,以及编解码输入输出接口 Buffer 在 Element 之间流动,通过 Caps 描述和协商媒体格式 Source、MediaCodec Buffer、音频 Track、Surface 等接口共同构成通路
控制与反馈 应用管理库调用、错误返回及自身的调度逻辑 Pipeline 状态、事件与 Bus 消息参与控制和反馈 API 请求、Binder 调用、组件状态与异步通知分别反映处理进度
格式扩展 编解码器、封装格式及协议等模块,可按构建配置选择 插件提供源、处理、编码及输出等 Element 提取插件、OMX/Codec2 组件及设备 HAL 分别承载扩展
与系统的关系 可接入平台硬件加速和输出能力;多应用设备策略通常由宿主系统承担 可通过平台插件接入设备和硬件;系统级策略由集成环境提供 音频策略、窗口显示、权限及媒体资源管理本身就是平台协作的一部分

FFmpeg 的库接口体现了按处理能力拆分的思路。例如 libavcodec 的 avcodec_send_packet() 与 avcodec_receive_frame() 将输入提交和输出获取分开:输入一个 Packet,不应假定立即得到一个对应 Frame。这个接口约束与 Android 的异步处理虽不是同一种实现,却提醒开发者关注同一个问题------输入被接受与输出已经产生,是两个不同的结果。

GStreamer 更强调媒体组件的组合。Element 通过 Pad 连接,Caps 表达格式要求,Pipeline 组织运行状态,Bus 将消息交给应用。这种设计让应用可以通过调整管线组合来改变处理过程,同时也要求关注格式协商、状态转换和各 Element 的行为。

Android 的 MediaPlayer/NuPlayer 路径则为应用组织了常见播放过程,应用通常不需要逐个连接提取器、解码器和输出组件。直接使用 MediaCodec 时,数据读取、Buffer 提交与获取、时间戳和输出协调等工作,又需要由应用承担。平台提供不同抽象层次,让开发者在使用便利性和处理控制权之间选择。

参考:FFmpeg 库的职责划分、libavcodec 输入输出接口、GStreamer 基本概念、GStreamer Caps 与格式协商。这里的设计思想是根据职责划分与接口约束作出的归纳,不是对设计者个人动机的引用。

Android 的复杂性来自哪些系统约束

如果只考虑一个应用处理一个媒体文件,格式解析、编解码与输出可以被组织得比较集中。Android 还必须面对多个应用同时运行、共享硬件、应用退出,以及输出设备和窗口状态变化等情况。这些约束解释了为什么框架不能只有一条"读取、解码、播放"的管线。

资源可用性取决于整个系统。 一个组件支持某种编码格式,不代表当前一定还有可用实例。前文的 ResourceManagerService 记录占用、选择回收目标并请求客户端释放资源,处理的就是跨客户端的资源竞争。FFmpeg 或 GStreamer 接入硬件组件时同样可能遇到资源不足,但操作系统层面的回收规则不能只由某个应用的处理管线决定。

输出设备由系统策略共同决定。 应用提供音频属性,AudioPolicy 参与输出选择,AudioFlinger 再组织执行;视频还需要与 Surface、窗口和显示系统协作。数据处理成功只是其中一步,用户听到什么、在哪块屏幕看到什么,还受到设备路由和显示状态影响。

受保护内容要求的不只是解码能力。 Media DRM 的会话和密钥交互、内容解密,以及相应的安全解码与输出保护要求,需要与设备实现配合。接入一个支持目标格式的处理库,不能据此推断设备已满足受保护内容的播放要求。

服务公开与内部初始化也有先后约束。 audioserver 先创建并关联 AudioFlinger 和 AudioPolicyService,再向 ServiceManager 注册,体现了系统服务的另一项责任:对外可调用时,内部依赖需要处于可用状态。这个问题超出了单个编解码函数的正确性,也不是画出数据流就能说明的。

这些约束都能在前文的服务初始化、输出选择、DRM 接口与资源回收代码中找到对应入口。进程部署、组件职责和媒体数据流因此需要放在不同维度分析,再建立彼此之间的关系。

对 Android 开发的启发

引入 FFmpeg 或 GStreamer,可以改变应用如何读取媒体、组织处理和组合编解码能力,但与 Android 的设备、显示、权限和资源管理如何衔接,仍取决于具体集成方式。采用哪套媒体框架,应先明确需要解决的是格式兼容、管线组合、处理控制,还是系统设备协作。

回看 Android 的设计,它把常见媒体处理组织成易于调用的接口,同时将共享设备和系统策略交给相应服务。这减少了普通应用需要承担的协调工作,也使故障可能出现在多个组件之间。开发中值得留下的认识是:既要检查媒体处理本身,也要检查它运行时所依赖的系统条件;理解一套框架的能力,还需要理解它承担了哪些责任、把哪些责任留给了调用者。

相关推荐
阿杰m28 分钟前
Android源码第三章:Handler、Looper 与 MessageQueue——一条消息如何入队、唤醒并执行
android
海雅达手持终端PDA4 小时前
巡线、深井、爆管抢修与极寒水网的硬件矩阵怎么配?海雅达水务巡检终端PDA选型指南
android·物联网·5g·集成测试·能源·政务
恋猫de小郭6 小时前
Flutter 开始支持 Jetpack Compose Platform View
android·前端·flutter
三少爷的鞋18 小时前
Android 架构七条铁律:架构师真正设计的不是代码,而是秩序
android
xiangxiongfly91520 小时前
Android GsonFactory总结
android·gson·gsonfactory
WythePlus1 天前
Android 14 SELinux策略深度解析
android·安全策略·selinux策略
俩个逗号。。1 天前
Kotlin flow 详解
android·开发语言·kotlin
橙光成长1 天前
android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送
android·1024程序员节
深圳雨林凯AI1 天前
图案裂变的底色衰减:alpha通道、亮度对比与白墨分色的量化拆解
android