如何在 .NET MAUI App 中实现原生音乐播放和耳机切歌控制?
可以用 .NET MAUI 实现:MAUI 负责界面和共享业务逻辑,Android、iOS 平台层分别接入各自的播放器、媒体控制和后台音频机制。
耳机切歌通常由操作系统通过媒体命令通知应用,无需自行解析蓝牙协议。应用需要向系统登记播放会话、提供当前曲目信息,并处理播放、暂停、上一首和下一首等命令。
先分清播放、媒体控制和音频中断
| 功能 | Android | iOS |
|---|---|---|
| 播放音频 | Media3 ExoPlayer | AVPlayer 或 AVQueuePlayer |
| 接收耳机和系统播放命令 | Media3 MediaSession | MPRemoteCommandCenter |
| 展示系统播放信息 | 媒体会话元数据、媒体通知 | MPNowPlayingInfoCenter |
| 后台继续播放 | MediaSessionService、前台服务 | AVAudioSession、后台音频配置 |
| 处理其他应用抢占音频 | Audio Focus | Audio Session interruption |
“应用收到播放通知”可能指两种情况:耳机发来的“下一首”是控制命令;来电或其他应用开始播放导致的暂停,则属于音频焦点变化或音频中断。应用需要分别处理。
Android 的媒体会话可以向系统提供播放状态,并将耳机等外部来源的命令交给播放器处理,具体可参考官方的 MediaSession 指南。
在 MAUI 中划分共享层和平台层
共享层适合保存曲目模型、播放列表规则和界面需要的播放状态。平台层负责音频输出、后台生命周期和系统媒体接口。
可以先定义一个简单的接口:
public interface IPlaybackService
{
Task LoadAsync(
IReadOnlyList<Track> tracks,
int startIndex);
Task PlayAsync();
Task PauseAsync();
Task NextAsync();
Task PreviousAsync();
Task SeekAsync(TimeSpan position);
}
public sealed record Track(
string Id,
Uri Source,
string Title,
string Artist);
这个接口需要在两个平台分别实现:Android 实现放在 Platforms/Android,iOS 实现放在 Platforms/iOS,再通过依赖注入供 ViewModel 使用。MAUI 平台代码的组织方式可参考 调用平台代码。
界面上的播放状态应以实际播放器为准。例如,加载失败后,界面不能因为调用过 PlayAsync() 就一直显示“正在播放”。后续可以添加状态变化事件,传递缓冲、暂停、播放结束和错误等信息。
播放队列也需要明确由哪一层管理。如果由原生播放器管理队列,共享层可以保留业务模型,但需要同步曲目增删、当前索引和播放顺序。
Android:用 Media3 建立后台播放服务
需要后台播放的音乐播放器可以采用以下结构:
MAUI 页面 / ViewModel
↓
MediaController
↓
MediaSessionService
├── MediaSession
└── ExoPlayer
由服务持有播放器,页面关闭或 Activity 重建时,播放对象仍可继续工作。MAUI 界面通过 MediaController 控制同一个会话。
.NET 项目可以使用 Xamarin.AndroidX.Media3.ExoPlayer 和 Xamarin.AndroidX.Media3.Session 等绑定包。包名保留了 Xamarin 前缀,其中的 Media3.Session 包提供 .NET for Android 和 MAUI 绑定。选择版本时要核对目标框架和依赖关系,并且只为 Android 目标引用这些包。
创建服务和媒体会话
将以下 C# 服务骨架放在 Android 平台目录中。它包含对象的创建和释放,还需要补上队列加载、控制器连接和错误处理,才能构成完整播放器。
using Android.App;
using Android.Content;
using Android.Content.PM;
using AndroidX.Media3.ExoPlayer;
using AndroidX.Media3.Session;
namespace MusicApp.Platforms.Android;
[Service(
Name = "com.example.music.PlaybackService",
Exported = true,
ForegroundServiceType = ForegroundService.TypeMediaPlayback)]
[IntentFilter(new[]
{
"androidx.media3.session.MediaSessionService"
})]
public sealed class PlaybackService : MediaSessionService
{
private IExoPlayer? _player;
private MediaSession? _session;
public override void OnCreate()
{
base.OnCreate();
_player = new ExoPlayerBuilder(this).Build()
?? throw new InvalidOperationException(
"Unable to create ExoPlayer.");
_session = new MediaSession.Builder(this, _player).Build();
}
public override MediaSession? OnGetSession(
MediaSession.ControllerInfo controllerInfo)
{
return _session;
}
public override void OnDestroy()
{
_player?.Release();
_session?.Release();
_player = null;
_session = null;
base.OnDestroy();
}
}
C# 绑定中的名称是 ExoPlayerBuilder,Android Java/Kotlin 文档中则是 ExoPlayer.Builder。阅读官方示例时,要留意这类绑定差异。
在 Platforms/Android/AndroidManifest.xml 的 <manifest> 下声明前台服务权限:
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
[Service] 和 [IntentFilter] 用于生成服务声明,示例中的 Java 服务名称需要替换为项目自己的名称。服务生命周期、权限和媒体通知的配置可参考 后台播放指南。
加载队列并接入耳机命令
- 用指向该服务的
SessionToken创建MediaController,等待异步连接完成后再发送命令。 - 将歌曲转换为
MediaItem,设置可访问的音频 URI,以及标题、歌手和封面等元数据。 - 设置播放队列,调用
Prepare(),再根据用户操作调用Play()。 - 通过控制器监听实际播放状态和曲目变化,更新 MAUI 界面。
如果通过控制器添加曲目,还需要按所选 Media3 版本的要求实现 MediaSession.Callback.onAddMediaItems(),将曲目 ID 解析为可播放的媒体项,或校验并接纳传入的 URI。
MediaSession 可以把标准的播放、暂停和切歌命令转交给播放器。如果播放器只加载了一首歌,通常就没有“下一首”可切。如果业务上还有下一首,需要提供相应队列,或自行实现队列处理。
Media3 能根据播放器状态更新会话,MediaSessionService 默认也提供媒体通知。按钮行为和控制器访问限制的更多配置,可参考 MediaSessionService API。
补齐音频焦点和耳机断开处理
创建 ExoPlayer 时,需要配置适合音乐播放的 AudioAttributes,并启用音频焦点管理。官方的 Java/Kotlin 配置入口为:
builder.setAudioAttributes(audioAttributes, true);
builder.setHandleAudioBecomingNoisy(true);
第一项让播放器管理音频焦点;第二项用于在输出设备断开等情况下暂停,避免耳机断开后音乐突然从扬声器播放。C# 中需要使用对应的绑定方法。
应用以 Android 15(API 35)或更高版本为目标时,只有在前台或正在运行前台服务,才能请求音频焦点。具体规则见 音频焦点指南。还要根据项目的目标 SDK 检查后台启动限制,优先在用户主动点击播放时启动播放流程。
iOS:配置音频会话和远程命令
iOS 可以用 AVPlayer 播放单个媒体项,用 AVQueuePlayer 管理连续播放队列。接入系统媒体功能还需要完成以下配置:
- 将
AVAudioSession类别设为Playback,在开始播放时激活音频会话。 - 在
Info.plist中启用后台音频。 - 向
MPNowPlayingInfoCenter写入当前曲目、时长、进度及播放速率。 - 向
MPRemoteCommandCenter注册播放、暂停、上一首和下一首的处理程序。
后台音频配置为:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
将这段配置合并到现有的 Info.plist 中,保留项目已有的后台模式。Playback 类别与后台音频模式的关系,可参考 AVAudioSession 文档。
在 .NET iOS 中,远程控制入口为:
var commands = MediaPlayer.MPRemoteCommandCenter.Shared;
commands.PlayCommand.Enabled = true;
commands.PauseCommand.Enabled = true;
commands.NextTrackCommand.Enabled = true;
commands.PreviousTrackCommand.Enabled = true;
这段代码只启用了命令,还需要通过各命令的 AddTarget(...) 注册处理程序,调用实际的播放逻辑。相关 C# 属性见 MPRemoteCommandCenter API。
注册时要保存 AddTarget 返回的对象,释放播放器时通过 RemoveTarget(...) 移除,避免重复注册。处理程序应根据实际结果返回状态。例如,队列末尾无法切歌时,不应无条件报告成功。命令分发和处理程序的注册方式见 Apple 的 MPRemoteCommand 文档。
播放、暂停、切换曲目或拖动进度后,都需要同步更新 Now Playing 信息,否则锁屏显示可能与实际播放状态不一致。实现方式可参考 Apple 的远程控制与播放信息指南。
从 Unity 转过来,优先掌握哪些知识
有游戏开发经验,播放列表和状态机通常容易上手。主要需要补充的是移动系统对应用生命周期和音频资源的管理:
- Android 的 Activity、Service、前台服务和媒体会话生命周期。
- iOS 的 Audio Session、后台音频、音频中断和输出路由变化。
- C# 异步编程、线程切换,以及原生对象和回调的释放。
- 本地文件 URI、网络媒体地址,以及对应的访问权限。
- 系统命令与界面操作并发发生时,如何保持队列和播放状态一致。
可以先用两首可访问的音频文件,在一个平台上实现播放、暂停和切歌,再接入系统控制,最后测试锁屏与后台播放。耳机相关功能要用真机验证,至少覆盖耳机断开、来电打断、其他应用开始播放和快速连续切歌。
实现时还需要注意以下限制:
- 耳机发出的具体命令取决于硬件和用户设置,不能假定双击一定表示“下一首”。
- 后台音频配置不保证进程永久存活。服务或进程结束后的恢复播放需要另行设计,用户强制停止应用也有不同限制。
- Android 媒体会话和 iOS 音频会话不意味着所有其他应用都会收到统一的“歌曲开始播放”广播。应用应接入系统规定的控制、焦点和中断接口。
- 本地曲库权限与后台播放权限需要分别处理。读取共享音频、读取用户选择的文件和播放网络音频,也各有对应的处理要求。
备注:内容仅供参考。
