1)文章由移远通信技术股份有限公司提供
2)以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改/删除

一、环境与版本信息

1.1 硬件平台

  • 开发板:RK3576
  • CPU架构:ARM64

1.2 软件环境

  • 操作系统:OpenHarmony 6.1
  • 内核版本:Linux 6.6
  • SDK版本:6.1.0.31
  • API版本:23
  • 构建类型:Release

1.3 版本确认

通过构建配置文件确认版本信息:

  • 硬件平台:RK3576
  • 操作系统:OpenHarmony 6.1
  • 内核版本:Linux 6.6
  • 软件版本:见下方版本信息
"build/version.gni"

# Copyright (c) 2021 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# OHOS version
declare_args() {
  sdk_version = "6.1.0.31"
  api_version = "23"

  # Release type, optional values: Betax, RCx...
  release_type = "Release"
  meta_version = "3.0.0"
  platform_version = "4.0.0"
}

# ohos SDK version
declare_args() {
  current_sdk_version = sdk_version
}

# ohos NDK version
declare_args() {
  current_ndk_version = current_sdk_version
}

1.4 本文目标

本文旨在深度解析OpenHarmony音频播放链路中,从应用层点击Play按钮到media_service服务启动音频pipeline的完整调用路径。
通过源码分析结合GDB调试,我们将:

  1. 追踪跨进程调用:分析应用进程如何通过IPC与media_service通信
  2. 理解状态机转换:解析播放器状态机的状态转换逻辑
  3. 验证技术实现:通过GDB调试验证理论分析与实际执行路径的一致性
  4. 建立完整认知:构建从应用层到服务层的完整技术栈理解

本文聚焦于分析 MiniMusic app 调用 play() 后,Start 请求如何从应用进程传递到 media_service,并在 media_service 中启动播放器 pipeline 的音频 sink。通过源码分析和GDB调试,我们将完整追踪这一跨进程调用链路。

1.5 核心流程架构图

后续链路

media_service进程

应用进程

🎵 MiniMusic.hap
应用层

📱 ArkTS UI点击Play

🔧 @ohos.multimedia.media.AVPlayer

🌉 AVPlayerNapi
JS/Native桥接

⚙️ PlayerImpl
Native播放器实现

📡 PlayerService IPC
跨进程通信

🏗️ PlayerServer
服务端状态机

🚀 HiPlayerImpl
播放引擎

🔌 pipeline_->Start()
启动媒体管道

🎧 AudioSinkFilter
音频输出过滤器

🔊 AudioRenderer
音频渲染器

1.5.1 核心调用链

// 应用进程侧
MiniMusic.hap → ArkTS UI点击Play → @ohos.multimedia.media.AVPlayer
→ AVPlayerNapi → PlayerImpl → PlayerService IPC

// media_service进程侧
PlayerServiceStub::Play → PlayerServer::Play → PlayerServer::OnPlay
→ PlayerServer::HandlePlay → HiPlayerImpl::Play → pipeline_->Start()
→ Filter::Start() → AudioSinkFilter::DoStart()

1.5.2 技术要点

  1. 跨进程架构:应用与media_service分离,通过IPC通信
  2. 异步任务模型:播放操作通过任务队列异步执行
  3. 状态机管理:PlayerServer维护播放状态,确保状态转换正确性
  4. 插件化设计:AudioSinkFilter作为插件接入pipeline架构
MiniMusic.hap
  → ArkTS 页面点击 Play
  → @ohos.multimedia.media.AVPlayer
  → AVPlayerNapi
  → PlayerImpl
  → PlayerService IPC
  → PlayerServer
  → HiPlayerImpl
  → pipeline_->Start()

核心目标:将"app中点击Play"这一用户操作,真正落地到 media_service 的播放器服务线程中执行。

MiniMusic.hap
  -> ArkTS 页面点击 Play
  -> @ohos.multimedia.media.AVPlayer
  -> AVPlayerNapi
  -> PlayerImpl
  -> PlayerService IPC
  -> PlayerServer
  -> HiPlayerImpl
  -> pipeline_->Start()

这里的核心是把“app 里点了 Play”这件事,真正落到 media_service 的播放器服务线程里。

二、MiniMusic.hap:最小化播放器Demo

MiniMusic.hap 是我自己写的一个最小音乐播放器 Demo,主要用于验证播放功能和梳理播放链路。
它的特点是代码量小、路径固定、干扰少,适合拿来观察 play()appframework 再到 service 的完整过程。

2.1 目录与构成

Demo 位于:

applications/standard/hap/minimusic

核心文件主要包括:

applications/standard/hap/minimusic/AppScope/app.json
applications/standard/hap/minimusic/entry/src/main/module.json
applications/standard/hap/minimusic/entry/src/main/ets/mainAbility/MainAbility.ts
applications/standard/hap/minimusic/entry/src/main/ets/pages/Index.ets
applications/standard/hap/minimusic/signature/minimusic_release.p7b
applications/standard/hap/minimusic/signature/minimusic_release_profile.json

2.2 播放实现

播放逻辑主要在 Index.ets,固定播放系统文件:

/system/etc/dynamic.wav

基本流程是:

打开音频文件
  -> createAVPlayer()
  -> 设置 fdSrc
  -> 等待 initialized
  -> prepare()
  -> 等待 prepared
  -> play()

对应的核心代码是:

this.audioFile = fs.openSync(MUSIC_PATH, fs.OpenMode.READ_ONLY);
this.player = await media.createAVPlayer();

const fileStat = fs.statSync(MUSIC_PATH);
this.player.fdSrc = {
  fd: this.audioFile.fd,
  offset: 0,
  length: fileStat.size,
};

await this.waitForState(this.player, 'initialized');
await this.player.prepare();
await this.waitForState(this.player, 'prepared');
await this.player.play();

2.3 为什么使用 fdSrc

这个 Demo 不直接把路径字符串丢给播放器,而是用 fdSrc 传文件描述符和长度。这样做的原因很直接:

  • 避免路径权限、沙箱和 URI 解析差异。
  • 让播放器走 AVFileDescriptor 这条更稳定的输入路径。
  • 对应到 native 层时,NAPI 会把它转成 SetSource(fd, offset, length)

所以本文后面的 GDB 和源码分析,起点都是这个最小 Demo 发起的 play(),不是一个复杂播放器工程。

2.4 为什么选 MiniMusic.hap

因为系统自带的音乐播放器代码对于我来说还是太多了
我只是想搞清楚流程不关心应用的复杂逻辑
所以我让AI帮写了一个核心逻辑只有100多行的demo用来配合我梳理播放的一些流程

2.5 实现效果

在这里插入图片描述

三、ArkTS 入口

入口文件:

applications/standard/hap/minimusic/entry/src/main/ets/pages/Index.ets

关键播放逻辑:

const MUSIC_PATH: string = '/system/etc/dynamic.wav';

this.audioFile = fs.openSync(MUSIC_PATH, fs.OpenMode.READ_ONLY);
this.player = await media.createAVPlayer();

const fileStat = fs.statSync(MUSIC_PATH);
this.player.fdSrc = {
  fd: this.audioFile.fd,
  offset: 0,
  length: fileStat.size,
};

await this.waitForState(this.player, 'initialized');
await this.player.prepare();
await this.waitForState(this.player, 'prepared');
await this.player.play();

这段代码做了三件事:

1. 打开系统音频文件。
2. 创建 AVPlayer。
3. 调用 prepare() 和 play()。

这里 fdSrcurl 更稳,因为当前版本对 fd://... 的 URL 形式校验比较严格,
fdSrc 能直接走文件描述符路径。

四、createAVPlayer() 到 AVPlayerNapi

media.createAVPlayer() 只是 ArkTS 层入口,真正会进入 NAPI

foundation/multimedia/player_framework/frameworks/js/avplayer/avplayer_napi.cpp

关键位置:

napi_value AVPlayerNapi::JsPlay(napi_env env, napi_callback_info info)

它的职责是:

1. 取出 JS 对象对应的 AVPlayerNapi 实例。
2. 做状态检查。
3. 创建异步播放任务。

play() 并不是同步把播放做完,而是进入一个任务对象:

promiseCtx->asyncTask = jsPlayer->PlayTask();

五、play() 进入 native AVPlayer

AVPlayerNapi::PlayTask() 的关键调用是:

int32_t ret = player_->Play();

这里的 player_native player 对象,继续进入播放器实现层。

Index.ets
  -> media.createAVPlayer()
  -> player.prepare()
  -> player.play()
  -> AVPlayerNapi::JsPlay()
  -> AVPlayerNapi::PlayTask()
  -> player_->Play()

六、native Player 到 PlayerService

native player 并不直接控制媒体线程,它会通过 PlayerService 继续进入 media_service

关键链路可以整理为:

PlayerImpl::Play()
  -> PlayerClient::Play()
  -> PlayerServiceProxy::Play()
  -> IPC PLAY
  -> PlayerServiceStub::Play()
  -> PlayerServer::Play()

这意味着:

应用进程里的 AVPlayer 只是客户端;
真正的播放器服务在 media_service。

6.1 PlayerImpl::Play()

PlayerImpl::Play() 的核心动作是调用服务端代理:

ret = playerService_->Play();

6.2 PlayerClient::Play()

PlayerClient 继续通过 IPC proxy 发送请求。

6.3 PlayerServiceProxy::Play()

这里会写 interface token,并通过 remote->SendRequest(PLAY, ...) 把请求发送给服务端。

6.4 PlayerServiceStub::Play()

服务端收到 IPC 后,进入:

playerServer_->Play()

七、PlayerServer 状态机

PlayerServer::Play() 并不是直接播放,它会先检查状态。

典型逻辑是:

if (lastOpStatus_ == PLAYER_PREPARED ||
    lastOpStatus_ == PLAYER_PLAYBACK_COMPLETE ||
    lastOpStatus_ == PLAYER_PAUSED) {
    return OnPlay();
}

含义:

只有准备好、播完、暂停等状态才允许进入播放。

7.1 PlayerServer::OnPlay()

OnPlay() 会投递一个任务,而不是把所有逻辑都同步执行完:

auto playingTask = std::make_shared<TaskHandler<void>>([this]() {
    auto currState = std::static_pointer_cast<BaseState>(GetCurrState());
    (void)currState->Play();
});
int ret = taskMgr_.LaunchTask(playingTask, PlayerServerTaskType::STATE_CHANGE, "play");
lastOpStatus_ = PLAYER_STARTED;

7.2 PlayerServer::HandlePlay()

真正进入播放器引擎的关键调用:

int32_t ret = playerEngine_->Play();

到这里,控制链路已经完全进入 media_service 的播放器引擎层。

八、PlayerEngine 到 HiPlayerImpl

仓库里有两套 HiPlayerImpl 路径,当前文档只需要记住它们都会通向同一个结果:

HiPlayerImpl::Play()
  -> pipeline_->Start()

8.1 histreamer 路径

关键调用:

syncManager_->Resume();
ret = TransStatus(pipeline_->Start());

8.2 media_foundation standard player 路径

关键调用:

syncManager_->Resume();
auto ret = pipeline_->Start();

不管是哪个 engine 实现,最终都要启动媒体 pipeline。

九、pipeline_->Start() 进入音频 sink

当前实测走的是 histreamerPipeline::Start()。它的模式很清楚:
先通过 SubmitJobOnce 提交启动任务,然后在任务里遍历 pipeline 中的 filters_

Status Pipeline::Start()
{
    Status ret = Status::OK;
    SubmitJobOnce([&] {
        AutoLock lock(mutex_);
        for (auto it = filters_.begin(); it != filters_.end(); ++it) {
            ret = (*it)->Start();
            if (ret != Status::OK) {
                return;
            }
        }
        ...
    });
    return ret;
}

也就是说,pipeline 启动时会启动每个 filter。音频输出相关 filte是:

AudioSinkFilter

Filter::Start() 会把具体 filter 的启动动作投递到 pipeline 线程里执行,
最终进入 AudioSinkFilter::DoStart()。这一步之后,控制权才交给具体的音频输出插件。

十、AudioServerSinkPlugin 创建 AudioRenderer

AudioServerSinkPluginmedia_service 中连接播放器和 AudioRenderer 的关键插件。

它会创建:

audioRenderer_ = AudioStandard::AudioRenderer::Create(rendererOptions_, appInfo);

然后在 Prepare() 里设置参数:

audioRenderer_->SetParams(rendererParams_);

最后在 Start() 里调用:

ret = audioRenderer_->Start();

这一步之后,就进入下一篇要分析的 AudioRendererPrivate::Start()
RendererInClientInner::StartAudioStream()

十一、GDB 实证:从 app 进程到 media_service

这一段建议分两个进程看。

11.1 attach MiniMusic / HAP 进程

先找到 HAP 进程:

ps -A | grep -E "MiniMusic|minimusic|com.example.minimusic"

然后 attach 到应用进程。

在 app 进程里,先抓 ArkTS 进入 NAPI 的点:

handle SIG38 nostop noprint pass
b OHOS::Media::AVPlayerNapi::JsPlay
b OHOS::Media::AVPlayerNapi::PlayTask
b OHOS::Media::PlayerImpl::Play
b OHOS::Media::PlayerServiceProxy::Play

在这里插入图片描述

这一步能证明:

1. `play()` 先进入 AVPlayerNapi。
2. AVPlayerNapi 会创建异步 PlayTask。
3. PlayTask 继续进入 native PlayerImpl。
4. PlayerImpl 通过 PlayerServiceProxy 发送 IPC。

11.1.1 证明 PlayerServiceProxy 的远端对象属于 media_service

继续在 PlayerServiceProxy::Play() 里看 Remote() 返回的对象:

在这里插入图片描述
在这里插入图片描述

关键结果是:

handle_ = 21
remoteDescriptor_ = "IStandardPlayerService"

这说明 PlayerServiceProxy 持有的是一个 Binder 远端代理对象,而且接口名就是播放器服务的标准接口。
在这里插入图片描述
再结合内核 Binder 状态查同一个句柄。

cat /sys/kernel/debug/binder/proc/5927 | grep "desc 21"

实测:

ref 165107: desc 21 node 165106 s 1 w 0 d 0000000000000000

这表示 MiniMusic app 进程 5927 中的 desc 21 指向 Binder node 165106

再到 media_servicebinder proc 中查同一个 node

cat /sys/kernel/debug/binder/proc/$(pidof media_service) | grep 165106

实测:

node 165106: u0000007f0b3af490 c0000007f0b3b09a0 hs 1 hw 1 ls 0 lw 0 is 1 iw 1 tr 1 proc 5927

这条 node 165106 出现在 media_servicebinder proc 文件中,
尾部的 proc 5927 表示当前引用者包含 MiniMusic app 进程。
再确认 app pid

ps -A | grep minimusic

实测:

5927 ? 00:00:02 ample.minimusic

所以这条证据链是:

ample.minimusic(pid 5927) 中的 IPCObjectProxy(handle=21)
  -> binder desc 21
  -> binder node 165106
  -> node 165106 位于 media_service
  -> 引用者 proc 5927 正是 ample.minimusic

这就证明 PlayerServiceProxy::Play()Binder
请求对端是 media_service 中的 IStandardPlayerService 服务端对象。

11.2 attach media_service

attachmedia_service,抓 PlayerService stubPlayerServer

handle SIG38 nostop noprint pass
b OHOS::Media::PlayerServiceStub::Play
b OHOS::Media::PlayerServer::Play
b OHOS::Media::PlayerServer::OnPlay
b OHOS::Media::PlayerServer::HandlePlay
b OHOS::Media::HiPlayerImpl::Play
b OHOS::Media::Pipeline::Pipeline::Start
b OHOS::Media::Pipeline::Filter::Start
b OHOS::Media::Pipeline::AudioSinkFilter::DoStart

media_service 侧不是一条同步栈跑到底,而是分成几个阶段。

11.2.1 第一阶段

PlayerRequest 线程收到 app 发来的 Play IPC。命中 PlayerServiceStub::Play 后,栈如下:

#0  OHOS::Media::PlayerServiceStub::Play(data, reply)@plt
#1  OHOS::Media::PlayerServiceStub::FillPlayerFuncPart1()::$_4::operator()
    at player_service_stub.cpp:141
#6  std::__h::function<int ()>::operator()()
#7  OHOS::Media::TaskHandler<int>::Execute
    at task_queue.h:137
#8  OHOS::Media::TaskQueue::TaskProcessor
    at task_queue.cpp:196

继续后命中真正的 PlayerServiceStub::Play(this)

OHOS::Media::PlayerServiceStub::Play
    at player_service_stub.cpp:443

再继续命中 PlayerServer::Play

#0  OHOS::Media::PlayerServer::Play
    at player_server.cpp:544
#1  OHOS::Media::PlayerServerMem::Play
    at player_server_mem.cpp:310
#2  OHOS::Media::PlayerServiceStub::Play
    at player_service_stub.cpp:449
#3  OHOS::Media::PlayerServiceStub::Play(data, reply)
    at player_service_stub.cpp:889

在这里插入图片描述
这一步证明 appPlay IPC 已经进入 media_servicePlayerService 服务端对象,
并转到 PlayerServer

11.2.2 第二阶段

PlayerServer::Play() 把真正的播放动作投递到 PlayerEngine 线程。
命中 HandlePlayHiPlayerImpl::Play 后,栈如下:

在这里插入图片描述

#0  OHOS::Media::HiPlayerImpl::Play
    at hiplayer_impl.cpp:913
#1  OHOS::Media::PlayerServer::HandlePlay
    at player_server.cpp:587
#2  OHOS::Media::PlayerServer::OnPlay()::$_6::operator()()
    at player_server.cpp:573
#8  OHOS::Media::TaskHandler<void>::Execute
    at task_queue.h:132
#9  OHOS::Media::TaskQueue::TaskProcessor
    at task_queue.cpp:196

在这里插入图片描述

源码里 HiPlayerImpl::Play() 在 942 行启动 pipeline

syncManager_->Resume();
ret = TransStatus(pipeline_->Start());

在 942 行下断点后,GDB 实测:

在这里插入图片描述

11.2.3 第三阶段

单步进入 pipeline_->Start(),确认目标是 Pipeline::Start()

在这里插入图片描述
Pipeline::Start() 的关键源码:

Status Pipeline::Start()
{
    Status ret = Status::OK;
    SubmitJobOnce([&] {
        AutoLock lock(mutex_);
        for (auto it = filters_.begin(); it != filters_.end(); ++it) {
            ret = (*it)->Start();
            if (ret != Status::OK) {
                return;
            }
        }
        ...
    });
    return ret;
}

继续在 Filter::Start 下断点,命中时的栈是:

在这里插入图片描述
这一步证明 HiPlayerImpl::Play() 不是直接调用某个固定的 audio sink
而是通过 pipeline_->Start() 遍历 pipeline 中的 filters_,逐个调用 Filter::Start()

11.2.4 第四阶段

Filter::Start() 内部再把具体 filter 的启动动作投递到 pipeline 线程。
随后命中 AudioSinkFilter::DoStart

在这里插入图片描述

#0  OHOS::Media::Pipeline::AudioSinkFilter::DoStart
    at audio_sink_filter.cpp:151
#1  OHOS::Media::Pipeline::Filter::StartDone
    at filter.cpp:165
#2  OHOS::Media::Pipeline::Filter::Start()::$_2::operator()()
    at filter.cpp:143
#9  OHOS::Media::TaskInner::HandleJob
    at taskInner.cpp:331
#10 OHOS::Media::PipeLineThread::Run
    at pipeline_threadpool.cpp:207

这一步能把 app 侧和 media_service 侧合起来:

app 进程:
AVPlayerNapi::JsPlay
  -> AVPlayerNapi::PlayTask
  -> PlayerImpl::Play
  -> PlayerServiceProxy::Play
  -> IPC PLAY

media_service:
PlayerServiceStub::Play
  -> PlayerServer::Play
  -> PlayerServer::OnPlay
  -> PlayerServer::HandlePlay
  -> HiPlayerImpl::Play
  -> pipeline_->Start()
  -> Pipeline::Start()
  -> Filter::Start()
  -> AudioSinkFilter::DoStart()

十二、从 app 到 media_service 的完整链路

把这一篇的链路整理成一句话,就是:

MiniMusic.hap
  -> Index.ets 点击 Play
  -> media.createAVPlayer()
  -> AVPlayerNapi::JsPlay()
  -> AVPlayerNapi::PlayTask()
  -> PlayerImpl::Play()
  -> PlayerClient::Play()
  -> PlayerServiceProxy::Play()
  -> PlayerServiceStub::Play()
  -> PlayerServer::Play()
  -> PlayerServer::OnPlay()
  -> PlayerServer::HandlePlay()
  -> HiPlayerImpl::Play()
  -> pipeline_->Start()
  -> Pipeline::Start()
  -> Filter::Start()
  -> AudioSinkFilter::DoStart()

十三、这一篇的定位

这一篇只负责把 app -> media_service 的入口打通,
并把播放器引擎如何进入 pipeline 的起点说明清楚。

后面三篇继续往下走:

02: media_service -> audio_server
03: audio_server -> audio_host
04: audio_host -> ALSA

这样四篇拼起来,就是一条完整的播放 Start 链路。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐