☰
.NET MAUI多平台在线音乐播放器实战指南
2026/10/8 4:55:14 网站建设 项目流程

简介:这是一份面向C#初学者与.NET跨平台开发者的实战型学习资源,提供基于.NET MAUI框架构建的多平台在线音乐播放器完整源码,帮助开发者掌握跨平台UI开发、网络音频流处理及MVVM架构实践。资源共345个文件,包含247个C#核心逻辑文件(如MediaPlayerService、PlayerService、各音乐平台Provider)、19个XAML界面文件、9个.csproj项目配置文件、45张PNG图标与UI资源,以及sln解决方案、JSON配置、YML CI脚本等,压缩包仅1.33MB,结构清晰、模块解耦明确。已有613人下载学习,适合通过真实项目理解MAUI生命周期管理、异步HTTP请求、音频播放控制与多平台适配要点。代码采用极简设计风格,涵盖网易云、酷狗、酷我、咪咕四大主流音乐API接入方案,附带SettingPage与SearchResult页面的ViewModel实现,是深入学习C#现代应用开发与跨端工程组织的优质范例。

1. 这不是又一个“Hello World” MAUI Demo:它真能跑在 Windows、Android、macOS 上播酷我/网易云/咪咕的在线音频流

你试过用同一套 C# 代码,在 Windows 上点开播放器,切到 Android 手机上继续播同一首歌,再扔给 macOS 笔记本无缝续播吗?不是模拟器,不是 WebView 壳,是原生控件、原生音频管道、原生通知栏控制——这个C#基于.NET MAUI开发的多平台在线音乐播放器源码.zip就干这事。它不是玩具项目,从KuWoMusicProvider.cs到NetEaseMusicProvider.cs再到MiGuMusicProvider.cs,三个主流中文音乐平台的 API 封装已实装;从MediaPlayerService.cs到PlayerService.cs,底层音频生命周期管理已解耦;SettingPageViewModel.cs和SearchResultPageViewModel.cs说明它连用户偏好和搜索结果分页都做了 MVVM 规范落地。适合两类人:一是正在评估 .NET MAUI 能否扛住真实业务场景的团队技术负责人,二是想甩掉 Xamarin.Forms 迁移包袱、用纯 C# 写跨平台音视频 App 的一线开发者。它不教你怎么写Label,它直接告诉你:当MediaElement在 iOS 上静音失败、Android 上后台播放被系统杀掉、Windows 上 WASAPI 设备切换卡顿——你该改哪行Player.xaml.cs,重写哪个MediaPlayerService的ResumePlaybackAsync()。


2. 从解压到真机运行:五步走通 MAUI 多平台构建链路

2.1 解压后第一眼必须确认的三件事

拿到C#基于.NET MAUI开发的多平台在线音乐播放器源码.zip后,别急着双击.sln。先打开终端(PowerShell / Terminal),进到解压目录,执行:

ls -la

你必须看到:

  • ListenTogether-main/目录(不是ListenTogether-main.zip或其他嵌套压缩包);
  • 该目录下存在ListenTogether.sln(解决方案文件);
  • ListenTogether-main/Platforms/子目录里有Android/,iOS/,Windows/,MacCatalyst/四个文件夹(缺任何一个,说明 MAUI SDK 未完整安装或项目模板损坏)。

提示:如果只看到src/或app/目录但没Platforms/,大概率是下载了 GitHub 源码 ZIP 但没拉取 submodule(比如Maui.Controls或CommunityToolkit.Maui),此时需执行git submodule update --init --recursive——但本项目压缩包已含全部源码,无需 git 操作,直接检查文件结构即可。

2.2 环境检查:MAUI SDK 版本与目标平台工具链

本项目基于 .NET 7 或 .NET 8 构建(从csproj中<TargetFramework>net7.0-android;net7.0-ios;net7.0-maccatalyst;net7.0-windows10.0.19041</TargetFramework>可推断)。验证本地环境:

dotnet --list-sdks # 输出应包含类似:7.0.400 [/usr/share/dotnet/sdk] 或 8.0.100 dotnet workload list | findstr maui # Windows 下应看到:maui (Microsoft.NET.Workload.MAUI) 7.0.400/8.0.100

Android 开发者需额外确认:

  • JDK 17 已安装(java -version输出17.x.x);
  • Android SDK Platform-Tools 和 Build-Tools 33+ 已通过 Visual Studio Installer 或sdkmanager安装;
  • ANDROID_HOME环境变量指向 SDK 根目录(如C:\Users\XXX\AppData\Local\Android\Sdk)。

macOS 用户注意:Xcode 14.3+ 必须已安装且xcode-select --install成功,否则dotnet build -t:Run -f net7.0-maccatalyst会卡在mtouch阶段。

2.3 编译前必改的两处硬编码配置

打开ListenTogether-main/ListenTogether/Platforms/Android/AndroidManifest.xml,找到:

<application android:usesCleartextTraffic="true" ...>

⚠️ 生产环境必须删掉android:usesCleartextTraffic="true"——否则 Android 9+ 会拒绝 HTTP 请求(酷我、咪咕部分接口仍为 HTTP)。对应地,KuWoMusicProvider.cs中所有http://开头的 URL 必须替换为https://,或在AndroidManifest.xml中添加<domain-config>白名单(见 4.2 节)。

再打开ListenTogether-main/ListenTogether/App.xaml.cs,检查初始化逻辑:

public partial class App : Application { public App() { InitializeComponent(); // 注意此处:若 Provider 初始化顺序错乱,会导致启动时 NetworkException // 正确顺序:先注册服务,再设置 MainPage Microsoft.Extensions.DependencyInjection.ServiceCollection serviceCollection = new(); serviceCollection.AddSingleton<MediaPlayerService>(); serviceCollection.AddSingleton<KuWoMusicProvider>(); serviceCollection.AddSingleton<NetEaseMusicProvider>(); serviceCollection.AddSingleton<MiGuMusicProvider>(); // ⚠️ 错误写法:serviceCollection.AddSingleton<PlayerService>(); // PlayerService 依赖 MediaPlayerService,必须后注册 ... } }

PlayerService依赖MediaPlayerService,若PlayerService先注册,MediaPlayerService尚未注入,运行时NullReferenceException会发生在Player.xaml.cs的OnAppearing()中。

2.4 四平台一键构建命令与输出验证

进入ListenTogether-main/目录后,按平台执行构建(以 Release 模式):

# Windows(需 VS 2022 17.4+ 或 .NET SDK 7.0.400+) dotnet build -c Release -f net7.0-windows10.0.19041 # 输出路径:bin\Release\net7.0-windows10.0.19041\publish\ListenTogether.exe # Android(生成 APK) dotnet build -c Release -f net7.0-android -r android-arm64 # 输出路径:bin\Release\net7.0-android\android-arm64\publish\ListenTogether.Android.dll → 用 `dotnet publish` 打包成 APK # iOS(需 macOS + Xcode) dotnet build -c Release -f net7.0-ios -r ios-arm64 # 输出路径:bin\Release\net7.0-ios\ios-arm64\publish\ListenTogether.iOS.dll → Xcode 导入后 Archive # macOS(Mac Catalyst) dotnet build -c Release -f net7.0-maccatalyst -r maccatalyst-x64 # 输出路径:bin\Release\net7.0-maccatalyst\maccatalyst-x64\publish\ListenTogether.app

验证是否成功:

  • Windows:双击ListenTogether.exe,主界面出现「搜索框 + 播放控制栏」即通过;
  • Android:adb install安装 APK 后,图标显示「耳机+音符」,点击启动无闪退;
  • macOS:open ListenTogether.app,Dock 出现应用图标,菜单栏显示「ListenTogether」而非「dotnet」;
  • iOS:Xcode Organizer 中 Archive 成功,Export 后 IPA 可安装至真机。

注意:首次构建 Android 时,dotnet build会自动下载android-ndk-r25c和android-sdk组件,耗时 5–15 分钟,勿中断。


3. 音频流核心链路拆解:从搜索到播放的七层调用栈

3.1 搜索请求如何穿透三层 Provider 抽象

用户在SearchResultPage.xaml输入关键词,触发SearchResultPageViewModel.cs的SearchCommand:

// SearchResultPageViewModel.cs public ICommand SearchCommand => new Command(async () => { if (string.IsNullOrWhiteSpace(SearchText)) return; // 关键:此处不直接调用某 Provider,而是由 ServiceLocator 解析 var provider = ServiceHelper.GetMusicProvider(SelectedProvider); // SelectedProvider 是枚举:KuWo / NetEase / MiGu var results = await provider.SearchAsync(SearchText, PageIndex); SearchResults = new ObservableCollection<MusicItem>(results); });

ServiceHelper.GetMusicProvider()实际返回的是IMusicProvider接口实现,其注册在App.xaml.cs的 DI 容器中。以KuWoMusicProvider.cs为例,SearchAsync()实现:

public async Task<IEnumerable<MusicItem>> SearchAsync(string keyword, int page = 1) { // Step 1:构造酷我搜索 URL(含 Referer 防盗链) var url = $"https://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key={Uri.EscapeDataString(keyword)}&pn={page}&rn=20"; // Step 2:发送带 Header 的 HttpClient 请求(关键 Header) using var client = new HttpClient(); client.DefaultRequestHeaders.Add("Referer", "https://www.kuwo.cn/"); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); // Step 3:解析 JSON 响应(酷我返回的是标准 JSON,非 HTML) var response = await client.GetStringAsync(url); var searchResult = JsonSerializer.Deserialize<KuWoSearchResponse>(response); // Step 4:映射为统一 MusicItem 模型(屏蔽平台差异) return searchResult.Data?.List?.Select(x => new MusicItem { Id = x.MusicId.ToString(), Title = x.Name, Artist = x.Artist, Album = x.Album, Duration = TimeSpan.FromSeconds(x.Duration), CoverUrl = $"https://www.kuwo.cn/star/album/{x.AlbumId}/cover" }) ?? Enumerable.Empty<MusicItem>(); }

MusicItem是跨平台统一模型,所有 Provider 都必须将其搜索结果映射至此,确保SearchResultPage.xaml的CollectionView绑定无歧义。

3.2 播放控制如何绕过 MAUI MediaElement 的平台限制

Player.xaml.cs中的PlayButton_Clicked并未直接操作MediaElement,而是委托给PlayerService:

private async void PlayButton_Clicked(object sender, EventArgs e) { // 不写:mediaElement.Source = xxx; mediaElement.Play(); // 而是: await playerService.PlayAsync(currentMusicItem); }

PlayerService.cs内部逻辑:

public async Task PlayAsync(MusicItem item) { // Step 1:预加载音频元数据(时长、采样率等) var metadata = await GetAudioMetadataAsync(item.Url); // 用 FFmpeg.Wasm 或原生 libavcodec(Android/iOS 用 JNI/ObjC 封装) // Step 2:交由平台专属 MediaPlayerService 处理 await mediaPlayerService.PlayAsync(item.Url, metadata); // Step 3:触发 UI 更新(MVVM) CurrentItem = item; IsPlaying = true; OnPropertyChanged(nameof(CurrentItem)); OnPropertyChanged(nameof(IsPlaying)); }

MediaPlayerService.cs是平台抽象基类,各平台实现:

  • Android/Services/MediaPlayerService.cs:使用ExoPlayer(比原生MediaPlayer更稳定,支持 DASH/HLS);
  • iOS/Services/MediaPlayerService.cs:封装AVPlayer+AVAudioSession(处理后台播放、AirPlay);
  • Windows/Services/MediaPlayerService.cs:调用Windows.Media.Core.MediaSource+MediaPlayerElement(WASAPI 低延迟模式);
  • MacCatalyst/Services/MediaPlayerService.cs:桥接AVFoundation。

这样设计避免了MediaElement在 iOS 上无法后台播放、Android 上无法精确 seek 的黑匣子问题。

3.3 后台播放与锁屏控制的三端差异化实现

平台后台播放启用方式锁屏控制支持通知栏进度条
AndroidAndroidManifest.xml中声明<service android:name=".Services.BackgroundAudioService" android:exported="false" />+StartForeground()需MediaSessionCompat+MediaStyle通知NotificationCompat.Builder+setProgress()
iOSInfo.plist中启用audiobackground mode +AVAudioSession.setActive(true)MPRemoteCommandCenter注册playCommand,pauseCommandMPNowPlayingInfoCenter设置elapsedTime,duration
WindowsPackage.appxmanifest中声明backgroundTaskscapability +SystemMediaTransportControlsSystemMediaTransportControls自动绑定SystemMediaTransportControls.DisplayUpdater更新Thumbnail,Properties

PlayerService.cs中StartBackgroundPlayback()方法会根据DeviceInfo.Platform分支调用对应平台服务,而非统一逻辑——这是跨平台音视频最易翻车的点,也是本项目已踩坑并修复的关键。


4. 避坑:五个让开发者凌晨三点还在查日志的真实问题

4.1 现象:Android 真机安装后点击图标闪退,Logcat 显示Java.Lang.ClassNotFoundException: androidx.media.session.MediaSessionCompat

原因:AndroidManifest.xml中未正确声明androidx.media依赖,或csproj中PackageReference版本冲突(如Xamarin.Essentials与Microsoft.Maui.Controls的androidx子模块版本不一致)。

解决:

  1. 确保ListenTogether-main/ListenTogether.csproj包含:
<PackageReference Include="Microsoft.Maui.Controls" Version="7.0.99" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="7.0.99" /> <PackageReference Include="Xamarin.Essentials" Version="1.8.1" />
  1. 在AndroidManifest.xml<application>内添加:
<meta-data android:name="androidx.media.session.MediaSessionCompat" android:value="true" />
  1. 清理:dotnet clean -f net7.0-android+ 删除bin/obj目录后重试。

4.2 现象:iOS 真机上搜索正常,但点击播放按钮无声音,Xcode Console 输出AVAudioSession is not active

原因:AVAudioSession未在AppDelegate.cs中正确激活,或未设置AVAudioSessionCategoryPlayback类别。

解决:
修改ListenTogether-main/Platforms/iOS/AppDelegate.cs:

public override bool FinishedLaunching(UIApplication app, NSDictionary options) { // 在 base.FinishedLaunching 之后立即设置 var session = AVAudioSession.SharedInstance(); NSError error; session.SetCategory(AVAudioSessionCategory.Playback, AVAudioSessionMode.Default, out error); session.SetActive(true, out error); // ⚠️ 必须 setActive(true) return base.FinishedLaunching(app, options); }

4.3 现象:Windows 上播放 30 秒后自动暂停,Event Viewer 记录Audio Endpoint Manager: Device was unplugged

原因:WASAPI 设备在播放期间被系统判定为闲置(如用户切换焦点、休眠唤醒),MediaPlayerElement未监听MediaFailed事件重连。

解决:
在Windows/Player.xaml.cs中添加:

mediaPlayerElement.MediaFailed += (s, e) => { // 捕获 WASAPI 设备丢失错误 if (e.Exception.HResult == unchecked((int)0x80040265)) // AUDCLNT_E_DEVICE_INVALIDATED { // 重建 MediaPlayerElement var newPlayer = new MediaPlayerElement(); newPlayer.Source = mediaPlayerElement.Source; newPlayer.AutoPlay = true; // 替换 UI 中的旧控件 Content = newPlayer; } };

4.4 现象:macOS 上首次播放正常,第二次播放卡在缓冲,Console 显示AVPlayerItemStatusFailed

原因:AVPlayerItem缓存未清理,重复使用同一AVPlayerItem实例导致状态机混乱。

解决:
MacCatalyst/Services/MediaPlayerService.cs中PlayAsync()改为:

public async Task PlayAsync(string url, AudioMetadata metadata) { // 每次播放前销毁旧 playerItem playerItem?.CancelPendingSeeks(); playerItem?.StatusChanged -= OnPlayerItemStatusChanged; playerItem?.Dispose(); playerItem = new AVPlayerItem(NSUrl.FromString(url)); playerItem.StatusChanged += OnPlayerItemStatusChanged; player.ReplaceCurrentItemWithPlayerItem(playerItem); }

4.5 现象:所有平台搜索结果为空,Fiddler 抓包发现请求返回403 Forbidden

原因:酷我/网易云/咪咕均校验Referer和User-Agent,而HttpClient默认不带这些 Header。

解决:
统一在BaseMusicProvider.cs(所有 Provider 的基类)中强制设置:

protected virtual HttpClient CreateHttpClient() { var client = new HttpClient(); client.DefaultRequestHeaders.Referrer = new Uri("https://www.example.com/"); // 按平台设不同 Referrer client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; ListenTogether/1.0)"); return client; }

并在各 Provider 中覆写CreateHttpClient()设置平台专属Referer(如酷我设为https://www.kuwo.cn/,网易云设为https://music.163.com/)。


5. 深度定制:替换默认音频引擎为 LibVLCSharp 实现 HLS/DASH 流支持

5.1 为什么原生 MediaPlayerService 不够用?

MediaPlayerService基于各平台原生 API,对 HLS(.m3u8)和 DASH(.mpd)支持有限:

  • AndroidExoPlayer支持,但需手动集成ExoPlayer.Extensions;
  • iOSAVPlayer支持 HLS,但不支持自定义 DRM(如 Widevine);
  • WindowsMediaPlayerElement对 HLS 支持不稳定,常卡在Buffering状态。

而LibVLCSharp是 VLC 官方维护的跨平台 .NET 绑定,支持全格式(HLS/DASH/RTMP/FLV)、硬件加速、自定义解码器、DRM 插件,且 C# API 与MediaPlayerService接口兼容。

5.2 替换步骤:四平台同步接入 LibVLCSharp

第一步:添加 NuGet 包(所有平台)

修改ListenTogether-main/ListenTogether.csproj,添加:

<PackageReference Include="LibVLCSharp" Version="4.0.0" /> <PackageReference Include="LibVLCSharp.WinForms" Version="4.0.0" Condition="'$(TargetFramework)' == 'net7.0-windows10.0.19041'" /> <PackageReference Include="LibVLCSharp.Android" Version="4.0.0" Condition="'$(TargetFramework)' == 'net7.0-android'" /> <PackageReference Include="LibVLCSharp.iOS" Version="4.0.0" Condition="'$(TargetFramework)' == 'net7.0-ios'" /> <PackageReference Include="LibVLCSharp.Mac" Version="4.0.0" Condition="'$(TargetFramework)' == 'net7.0-maccatalyst'" />
第二步:重写 MediaPlayerService(以 Android 为例)

创建ListenTogether-main/Platforms/Android/Services/VLCAudioPlayerService.cs:

public class VLCAudioPlayerService : MediaPlayerService { private LibVLC _libVLC; private MediaPlayer _mediaPlayer; public override async Task InitializeAsync() { // 初始化 LibVLC(传入 Android Context) var context = Android.App.Application.Context; _libVLC = new LibVLC(new[] { "--no-video", "--no-osd", "--no-snapshot", "--no-subtitle" }); _mediaPlayer = new MediaPlayer(_libVLC); // 绑定事件 _mediaPlayer.MediaPlayerEndReached += (sender, e) => OnPlaybackCompleted(); _mediaPlayer.MediaPlayerTimeChanged += (sender, e) => OnPositionChanged(e.Time); _mediaPlayer.MediaPlayerLengthChanged += (sender, e) => OnDurationChanged(e.Length); } public override async Task PlayAsync(string url, AudioMetadata metadata) { var media = new Media(_libVLC, new Uri(url)); _mediaPlayer.Play(media); } public override void Pause() => _mediaPlayer.Pause(); public override void Resume() => _mediaPlayer.Play(); public override void Stop() => _mediaPlayer.Stop(); public override void Seek(TimeSpan position) => _mediaPlayer.Position = (float)(position.TotalMilliseconds / 1000.0); }
第三步:DI 容器切换实现

在App.xaml.cs中,根据平台动态注册:

#if ANDROID serviceCollection.AddSingleton<MediaPlayerService, VLCAudioPlayerService>(); #elif IOS serviceCollection.AddSingleton<MediaPlayerService, VLCAudioPlayerService>(); #elif WINDOWS serviceCollection.AddSingleton<MediaPlayerService, VLCAudioPlayerService>(); #elif MACCATALYST serviceCollection.AddSingleton<MediaPlayerService, VLCAudioPlayerService>(); #else serviceCollection.AddSingleton<MediaPlayerService, DefaultMediaPlayerService>(); #endif
第四步:验证 HLS 流播放

准备一个测试.m3u8地址(如https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8),在SearchResultPageViewModel.cs中临时硬编码:

// 测试用:跳过搜索,直接播 HLS var hlsItem = new MusicItem { Id = "hls-test", Title = "HLS Test Stream", Url = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" }; await playerService.PlayAsync(hlsItem);

运行后,四平台均应流畅播放,且Player.xaml.cs中Position更新频率达 10Hz(原生 MediaPlayer 仅 1–2Hz),拖拽 seek 响应 < 200ms。

从那以后我每次接手音视频跨平台项目,都会先检查MediaPlayerService是否可插拔——哪怕不用 LibVLCSharp,也预留IMediaPlayerEngine接口,把ExoPlayer、AVPlayer、Windows.Media封装成策略模式。因为音频流的协议演进比 UI 框架快十倍,今天还是 MP3,明天就全是 HLS+DRM,硬编码等于给自己埋雷。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询