在 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 | 都不满足 | 返回空对象 |
组件名称前缀的匹配不区分大小写。这张表说明的是创建时的选择规则,不能据此推导出解码失败后的自动重试策略。
源码位置: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 录制、启动录制、停止录制与收尾。
用流程和状态区分请求与完成
播放与录制分别看
下面分别看文件播放与录制的数据方向:
图 3、图 4:播放读取已有媒体,录制生成媒体文件。两条图表示数据方向,不表示所有节点严格按串行时序启动。
播放状态:以 NuPlayerDriver 为例
prepareAsync() 返回成功,只能说明准备请求被接受。在正常异步准备路径中,NuPlayerDriver 从 STATE_UNPREPARED 进入 STATE_PREPARING;准备完成回调成功后,才进入 STATE_PREPARED。随后调用 start(),进入 STATE_RUNNING。
图 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 的设计,它把常见媒体处理组织成易于调用的接口,同时将共享设备和系统策略交给相应服务。这减少了普通应用需要承担的协调工作,也使故障可能出现在多个组件之间。开发中值得留下的认识是:既要检查媒体处理本身,也要检查它运行时所依赖的系统条件;理解一套框架的能力,还需要理解它承担了哪些责任、把哪些责任留给了调用者。