PAYATHON 2026

如何在 .NET MAUI App 中实现原生音乐播放和耳机切歌控制?

技术老齐

可以用 .NET MAUI 实现:MAUI 负责界面和共享业务逻辑,Android、iOS 平台层分别接入各自的播放器、媒体控制和后台音频机制。

耳机切歌通常由操作系统通过媒体命令通知应用,无需自行解析蓝牙协议。应用需要向系统登记播放会话、提供当前曲目信息,并处理播放、暂停、上一首和下一首等命令。

先分清播放、媒体控制和音频中断

功能AndroidiOS
播放音频Media3 ExoPlayerAVPlayer 或 AVQueuePlayer
接收耳机和系统播放命令Media3 MediaSessionMPRemoteCommandCenter
展示系统播放信息媒体会话元数据、媒体通知MPNowPlayingInfoCenter
后台继续播放MediaSessionService、前台服务AVAudioSession、后台音频配置
处理其他应用抢占音频Audio FocusAudio 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 服务名称需要替换为项目自己的名称。服务生命周期、权限和媒体通知的配置可参考 后台播放指南。

加载队列并接入耳机命令

  1. 用指向该服务的 SessionToken 创建 MediaController,等待异步连接完成后再发送命令。
  2. 将歌曲转换为 MediaItem,设置可访问的音频 URI,以及标题、歌手和封面等元数据。
  3. 设置播放队列,调用 Prepare(),再根据用户操作调用 Play()。
  4. 通过控制器监听实际播放状态和曲目变化,更新 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 管理连续播放队列。接入系统媒体功能还需要完成以下配置:

  1. 将 AVAudioSession 类别设为 Playback,在开始播放时激活音频会话。
  2. 在 Info.plist 中启用后台音频。
  3. 向 MPNowPlayingInfoCenter 写入当前曲目、时长、进度及播放速率。
  4. 向 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 音频会话不意味着所有其他应用都会收到统一的“歌曲开始播放”广播。应用应接入系统规定的控制、焦点和中断接口。
  • 本地曲库权限与后台播放权限需要分别处理。读取共享音频、读取用户选择的文件和播放网络音频,也各有对应的处理要求。

备注:内容仅供参考。