Windows Phone Mango开发实践——后台音频
后台音频
后台播放音频的应用程序,顾名思义,即使用户已通过按"返回"按键或"开始"按键离开应用程序也可以继续播放音频。
后台音频体系结构
后台音频应用程序利用后台代理(Windows Phone OS 7.1 中的新增内容)。Windows Phone上的所有媒体都是通过Zune媒体队列播放的。后台音频应用程序向Zune媒体队列发指令控制当前堆栈、播放音乐、暂停音乐,以及快进和后退操作。那么这些是如何实现的呢?其实是调用BackgroundAudioPlayer类中方法来完成控制操作,Instance对象与Zune媒体队列通信以操作音频的播放。
有两种类型的后台音频应用程序。一种类型实现简单的播放列表并将一个包含媒体文件地址的URI传递给Zune媒体队列以设置当前曲目。URI可以是手机的本地或远程地址,在任何一种情况下,URI指向的音频必须是Windows Phone支持的类型才能播放。另一种类型的后台音频应用程序使用MediaStreamSource实现音频流的播放,此音频流的格式则可以是任何格式。这是因为由MediaStreamSource派生的类实现了对音频流的处理和解码。
1) 后台音频播放列表应用程序
若要创建后台音频播放列表需实现两个部分:提供用于控制播放的用户界面和实现从AudioPlayerAgent 派生的类,即下图中以绿色显示的部分。
图后台音频播放列表应用程序
应用程序UI提供控制音频播放的用户界面。通过UI的交互调用Instance对象设置Zune媒体队列中的当前曲目。AudioPlayerAgent由操作系统进行实例化,并应用程序的用户界面或UVC处理交互操作。
AudioPlayerAgent在后台运行并调用BackgroundAudioPlayer的实例,该实例调用 Zune 媒体队列以实际播放音频。
您可使用Visual Studio中的"Windows Phone Playback Agent"模板创建新的AudioPlayerAgent项目并将该项目添加到您的解决方案中。
图创建新的AudioPlayerAgent项目
从主应用程序项目中添加对新AudioPlayerAgent的引用。即右键单击应用程序项目中的"引用"节点,然后选择"添加引用...",如下图所示。
图 添加引用
2) 音频流代理的应用程序
对于音频流的应用程序,需实现从AudioStreamingAgent派生的类和MediaStreamSource派生的类。您即实现下图中以绿色显示的部分。音频流代理AudioStreamingAgent 负责创建MediaStreamSource并将Zune媒体队列指向该实例。
图 后台音频流应用程序
使用Visual Studio中的"Windows Phone Audio Streaming Agent"模板创建新的AudioStreamingAgent项目并将该项目添加到解决方案中。
图 创建新的AudioStreamingAgent项目
3) 后台音频生命周期
AudioPlayerAgent和AudioStreamingAgent虽然都是由BackgroundAudioPlayer创建,但是其各自的生命周期却有所不同。
AudioPlayerAgent是由BackgroundAudioPlayer在需要时创建的,用于处理来自应用程序的用户界面或通用音量控制(UVC)的用户操作请求。
AudioStreamingAgent是由BackgroundAudioPlayer在需要新的音频流时创建的。当新曲目由于要开始播放而需要音频流时,BackgroundAudioPlayer在AudioStreamingAgent 中调用OnBeginStreaming方法。
当后台代理调用 Abort()()()()或NotifyComplete()()()()时释放后台代理。
4) 实现音频播放的类
BackgroundAudioPlayer
BackgroundAudioPlayer类与Zune媒体队列通信,通过Instance控制正在播放的音频。调用BackgroundAudioPlayer类的Instance属性返回BackgroundAudioPlayer的实例。如果此应用程序已分配后台音频播放资源,则返回的BackgroundAudioPlayer将包含对这些资源的引用。
注意:
每个应用程序只能有一个BackgroundAudioPlayer处于活动状态。
AudioTrack
AudioTrack类表示有关某个曲目的元数据,包括标题、艺术家、专辑以及曲目的URI。如果URI设置为null,则系统则默认曲目设置为MediaStreamSource。在这种情况下,可使用Tag属性将AudioPlayerAgent中的信息传递给AudioStreamingAgent。
AudioStreamer
AudioStreamer的实例传递给OnBeginStreaming(AudioTrack, AudioStreamer)。在OnBeginStreaming的实现中,调用SetSource(MediaStreamSource)指向从MediaStreamSource派生的类。
后台音频的程序的认证要求
后台音频的程序属于特定应用程序类型,除了一般应用程序的要求需满足外,还需符合如下的要求。
1) 该应用程序不得启动后台音频播放,除非用户激活该应用程序提供的可发现 UI 元素。
验证方法:
- 启动应用程序;
- 验证该应用程序中是否存在可发现的UI元素,并允许后台音频播放;
- 激活UI元素以开始后台音频播放;
- 验证后台音频播放是否开始。
2) 当该应用程序位于前台中时,它必须向用户提供可发现的UI元素,允许用户停止音频播放。
验证方法:
- 启动应用程序;
- 开始音频播放;
- 验证应用程序内是否可发现停止音频播放的UI元素;
- 激活UI元素以停止音频播放;
- 验证音频播放是否停止。
3) 当用户点按统一音量控制上的"停止"按键时,应用程序必须停止后台中播放的音频。如果播放服务支持暂停操作,则通过统一音量控制暂停后台音频必须根据用户的操作暂停或重新启动音频。
验证方法:
- 启动应用程序;
- 开始音频播放;
- 关闭该应用程序;
- 验证该音频是否继续在后台中播放;
- 查看统一音量控制;
- 如果播放服务支持暂停命令,则可以通过统一音量控制暂停音频,验证播放是否暂停;通过统一音量控制重新启动音频,验证播放是否重新启动;
- 通过统一音量控制停止音频播放;
- 验证播放是否停止;
4) 当从应用程序中播放后台音频时,该应用程序发送的要显示在统一音量控制中的元数据必须对该音频进行描述(例如歌曲、曲目、艺术家、播放状态或应用程序名称)。应用程序错误代码不得显示在统一音量控制中。禁止在统一音量控制中插入广告和其他不相关的内容。
验证方法:
- 启动应用程序;
- 开始音频播放;
- 关闭该应用程序;
- 验证该音频是否继续在后台中播放;
- 查看统一音量控制;
- 验证该音频播放的元数据是否显示以及是否与音频内容相关。
5) 使用AudioStreamingAgent API的应用程序只能用于执行与音频播放及关联元数据管理相关的任务。
验证方法:
- 启动应用程序;
- 关闭该应用程序;
- 验证后台音频流代理是否仅用于传输指定音频内容及进行相关的元数据管理。
后台音频的最佳实践
1) 调试
在"编码—调试—编辑代码—重新启动调试"的过程中,调试器可能会在您重新启动调试时将自身连接到后台音频代理,而不是您的应用程序。若要避免出现这种情况,您需要确保后台代理在应用程序启动时处于关闭状态。要实现这一点,有一种简便的方法是将对Close()()()()的调用放到App类的构造函数中。如果您是通过Visual Studio模板创建的应用,则App类构造函数中会包含一项检查,以了解该应用程序是否正在调试器下运行。可将对BackgroundAudioPlayer.Instance.Close的调用放在此if语句中。
// Show graphics profiling information while debugging.
if (System.Diagnostics.Debugger.IsAttached)
{
// Close the background audio player in case it
// was running from a previous debugging session.
BackgroundAudioPlayer.Instance.Close();
// ...
}
2) 处理用户操作
BackgroundAudioPlayer未对用户操作(UserAction)请求的数量施加任何限制。通过UI(应用界面或通用音量控制即UVC)调用的操作排成在队列中处理。AudioPlayerAgent没有确定是否存在连续调用的方法。需要网络请求的SkipNext、SkipPrevious或Play调用可能需要几秒钟的时间。
- 无论调用花费的时间长短,都将按顺序处理对SkipNext、SkipPrevious或Play的连续调用。
- 每次调用允许30秒的时间。
- 由于Play和Pause操作未获得优先权,因此代理可能花费几秒钟(甚至几分钟)的时间才能响应用户操作。
因此限制排队的SkipNext和SkipPrevious请求的数量并给予Play和Pause较高的优先级。后台音频播放器示例只是在处理请求之前禁用"下一个"和"上一个"按钮。从 Windows Phone 的代码示例页面中获得后台音频播放器示例的代码。
MediaElement和BackgroundAudioPlayer
混合BackgroundAudioPlayer和MediaElement要注意:
- 必须在切换到MediaElement 播放之前调用Close()()()()。
- 只有一个媒体队列。您的应用程序不能暂停后台音频,不能使用 MediaElement 播放某些内容,而且不能继续播放后台音频流。
内存和运行时约束
- AudioPlayerAgent 的实现必须在30秒内调用NotifyComplete()()()()或Abort()()()()。
- AudioStreamingAgent 的实现则允许无限时运行。
- 两种类型的后台音频代理都托管在同一进程中,并且共用最大限制为 15 MB 的内存。
- 在调试器下运行时,Windows Phone 操作系统会忽略内存和运行时约束。
动手实践——音乐播放器
本节的重点内容是实现音乐播放器的后台音乐播放功能,即在应用程序处于非激活状态时持续的播放音乐。本节的涉及的技术如何实现使用音频播放代理播放本地媒体的 Windows Phone 应用程序。
任务1——创建Windows Phone后台音频应用程序
- 通过选择"文件|新项目..."菜单命令创建一个新项目。
- 将显示"新建项目"对话框。展开"Visual C#"模板,然后选择"Silverlight for Windows Phone"模板。
- 选择"Windows Phone应用程序"模板。根据需要填写"MusicPlayer",然后单击"确定"。
- 在"解决方案资源管理器"中,右键单击"解决方案"节点,然后选择"添加 | 新项目..."。
- 在"添加新项目"对话框中,单击"Windows Phone音频播放代理"。
- 在名称中填写"AudioPlaybackAgent",然后单击"确定"。至此,解决方案应该具有两个项目,即应用程序项目和后台代理项目。
任务2——添加UI和事件处理函数
在此任务中添加MainPage的UI和后台音频Microsoft.Phone.BackgroundAudio.BackgroundAudioPlayer的事件处理函数。
打开 MainPage.xaml 文件
在文件首部的phone:PhoneApplicationPage中绑定DataContext的相关资源属性。
DataContext="{Binding RelativeSource={RelativeSource Self}}"
定位到ContentPanel的标签,在其中添加HorizontalAlignment属性,设置其为"Stretch"。
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" HorizontalAlignment="Stretch">
在Grid中增加对于行属性的定义和TextBlock,TextBlock绑定播放列表的名称。
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding ActivePlaylist.Name}" FontSize="{StaticResource PhoneFontSizeExtraLarge}"/>
</Grid>
增加显示播放曲目的ListBox,代码如下。
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding ActivePlaylist.Name}" FontSize="{StaticResource PhoneFontSizeExtraLarge}"/>
<ListBox ItemsSource="{Binding ActivePlaylist.Tracks}" Grid.Row="1" x:Name="lstTracks" HorizontalContentAlignment="Stretch" Loaded="lstTracks_Loaded">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Margin="10,0" Source="{Binding Tile, TargetNullValue={StaticResource NoArt}}" Grid.RowSpan="3" Width="60" Stretch="Uniform" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Title}" Grid.Column="1" FontSize="{StaticResource PhoneFontSizeLarge}"/>
<TextBlock Text="{Binding Artist}" Grid.Row="1" Grid.Column="1"/>
<TextBlock Text="{Binding Album}" Grid.Row="2" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
打开MainPage.xaml.cs,添加命名为lstTracks_Loaded的函数。
publicpartialclassMainPage : PhoneApplicationPage
{
...
private void lstTracks_Loaded(object sender, RoutedEventArgs e)
{
}
...
}
编译并在模拟器中运行的效果如下所示。
图 运行效果
定义播放列表类Playlist的依赖属性,将其命名为ActivePlaylist。
publicpartialclassMainPage : PhoneApplicationPage
{
public Playlist ActivePlaylist
{
get { return (Playlist)GetValue(ActivePlaylistProperty); }
set { SetValue(ActivePlaylistProperty, value); }
}
public static readonly DependencyProperty ActivePlaylistProperty =
DependencyProperty.Register("ActivePlaylist", typeof(Playlist), typeof(MainPage), newPropertyMetadata(null));
...
}
重载OnNavigatedTo方法,读取独立存储空间中的播放列表。
publicpartialclassMainPage : PhoneApplicationPage
{
...
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
Stream playlistStream = Application.GetResourceStream(new Uri("Misc/Playlist.xml", UriKind.Relative)).Stream;
System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(Playlist));
ActivePlaylist = (Playlist)serializer.Deserialize(playlistStream);
using ( IsolatedStorageFile isoStorage = IsolatedStorageFile.GetUserStoreForApplication() )
{
using ( IsolatedStorageFileStream file = isoStorage.OpenFile("playlist.xml", FileMode.OpenOrCreate) )
{
var writer = new StreamWriter(file);
serializer.Serialize(writer, ActivePlaylist);
}
}
base.OnNavigatedTo(e);
}
...
}
在MainPage的构造函数中,添加PlayStateChanged事件订阅的代码。
publicpartialclassMainPage : PhoneApplicationPage
{
...
public MainPage()
{
InitializeComponent();
BackgroundAudioPlayer.Instance.PlayStateChanged += new EventHandler(Instance_PlayStateChanged);
}
...
}
添加事件处理函数的响应代码,实现当后台音频播放器运行时,更新播放曲目和播放状态。
publicpartialclassMainPage : PhoneApplicationPage
{
...
privatevoid Instance_PlayStateChanged(object sender, EventArgs e)
{
UpdateSelection();
}
privatevoid UpdateSelection()
{
int activeTrackNumber = GetActiveTrackIndex();
if ( activeTrackNumber != -1 )
{
lstTracks.SelectedIndex = activeTrackNumber;
}
}
privateint GetActiveTrackIndex()
{
int track = -1;
if ( null != BackgroundAudioPlayer.Instance.Track )
{
track = int.Parse(BackgroundAudioPlayer.Instance.Track.Tag);
}
return track;
}
...
}
在应用程序处于不活动状态时,后台音频播放器也允许用户更改播放的曲目。若要实现此功能,需要更新的lstTracks_Loaded方法,在该方法中插入以下代码。
publicpartialclassMainPage : PhoneApplicationPage
{
...
private void lstTracks_Loaded(object sender, RoutedEventArgs e)
{
UpdateSelection();
}
...
}
编译并运行此程序,首页呈现的内容如下。
-
图 播放列表
任务3——添加控制按钮
本节中将添加ApplicationBar的按钮,控制音乐播放器播放、停止、前一个音轨和后一个音轨。
为MainPage.xaml添加PhoneApplicationPage.ApplicationBar,即在LayoutRoot结尾部分添加如下的代码。
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="False">
<shell:ApplicationBarIconButton IconUri="/Images/prev.png" Text="prev" Click="appbar_prev"/>
<shell:ApplicationBarIconButton IconUri="/Images/play.png" Text="play" Click="appbar_playpause"/>
<shell:ApplicationBarIconButton IconUri="/Images/stop.png" Text="stop" Click="appbar_stop"/>
<shell:ApplicationBarIconButton IconUri="/Images/next.png" Text="next" Click="appbar_next"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
在MainPage.xaml.cs添加应用程序栏(ApplicationBar)的Click事件处理函数。
public partial class MainPage : PhoneApplicationPage
{
...
#region ApplicationBar Buttons Events
private void appbar_prev(object sender, EventArgs e)
{
BackgroundAudioPlayer.Instance.SkipPrevious();
}
private void appbar_playpause(object sender, EventArgs e)
{
if ( BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing )
BackgroundAudioPlayer.Instance.Pause();
else
BackgroundAudioPlayer.Instance.Play();
}
private void appbar_stop(object sender, EventArgs e)
{
BackgroundAudioPlayer.Instance.Stop();
}
private void appbar_next(object sender, EventArgs e)
{
BackgroundAudioPlayer.Instance.SkipNext();
}
#endregion
...
}
在Instance_PlayStateChanged方法中更新应用程序栏的按钮状态,为此调用UpdateAppBarStatus方法。
publicpartialclassMainPage : PhoneApplicationPage
{
...
privatevoid Instance_PlayStateChanged(object sender, EventArgs e)
{
UpdateAppBarStatus();
UpdateSelection();
}
...
}
实现UpdateAppBarStatus 方法。判断BackgroundAudioPlayer的状态是处于播放、暂停和停止的状态,然后分别设置"前一个"、"播放/暂停"、"停止"和"下一个"按钮的状态。
publicpartialclassMainPage : PhoneApplicationPage
{
...
privatevoid UpdateAppBarStatus()
{
switch ( BackgroundAudioPlayer.Instance.PlayerState )
{
case PlayState.Playing:
//Prev Button
if ( GetActiveTrackIndex() > 0 )
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;
//Play/Pause Button
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true;
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "pause";
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/pause.png", UriKind.Relative);
//Stop Button
( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = true;
//Next button
if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 )
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false;
break;
case PlayState.Paused:
//Prev Button
if ( GetActiveTrackIndex() > 0 )
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;
//Play/Pause Button
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true;
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "play";
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/play.png", UriKind.Relative);
//Stop Button
( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = true;
//Next button
if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 )
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false;
break;
case PlayState.Stopped:
//Prev Button
if ( GetActiveTrackIndex() > 0 )
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;
//Play/Pause Button
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true;
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "play";
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/play.png", UriKind.Relative);
//Stop Button
( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = false;
//Next button
if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 )
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false;
break;
case PlayState.Unknown:
//Prev Button
( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;
//Play/Pause Button
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true;
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "play";
( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/play.png", UriKind.Relative);
//Stop Button
( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = false;
//Next button
if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 )
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true;
else
( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false;
break;
default:
break;
}
}
...
}
编译和运行应用程序,可以看到播放列表下方新增了应用程序栏。
图 具有应用程序栏的播放列表
- 任务4——在独立存储空间保存音乐和图像文件
您可能会疑惑为什么要将曲目保存至独立存储空间,答案很简单,就是为了能够被播放器播放。
在App.xaml.cs的文件头部添加如下的引用
using System.IO.IsolatedStorage;
using System.Windows.Resources;
-
在App类的构造函数中,添加调用CopyToIsolatedStorage方法的代码,并实现CopyToIsolatedStorage的代码:创建IsolatedStorageFile对象引用独立存储空间的类,将音频文件写入独立存储空间的"Music/"文件夹中,将专辑图片写入独立存储空间的"Shared/Media/"文件夹中。
public App()
{
...
CopyToIsolatedStorage();
}
private void CopyToIsolatedStorage()
{
using ( IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication() )
{
string[] files = new string[] { "file1.mp3", "file2.mp3", "file3.mp3" };
foreach ( var _fileName in files )
{
if ( !storage.FileExists(_fileName) )
{
string _filePath = "Music/" + _fileName;
StreamResourceInfo resource = Application.GetResourceStream(new Uri(_filePath, UriKind.Relative));
using ( IsolatedStorageFileStream file = storage.CreateFile(_fileName) )
{
int chunkSize = 4096;
byte[] bytes = new byte[chunkSize];
int byteCount;
while ( ( byteCount = resource.Stream.Read(bytes, 0, chunkSize) ) > 0 )
{
file.Write(bytes, 0, byteCount);
}
}
}
}
files = new string[] { "Image1.jpg", "Image3.jpg", "no-art.jpg" };
foreach ( var _fileName in files )
{
string _destFilePath = "Shared/Media/" + _fileName;
;
if ( !storage.FileExists(_destFilePath) )
{
string _filePath = "Images/" + _fileName;
StreamResourceInfo resource = Application.GetResourceStream(new Uri(_filePath, UriKind.Relative));
using ( IsolatedStorageFileStream file = storage.CreateFile(_destFilePath) )
{
int chunkSize = 4096;
byte[] bytes = new byte[chunkSize];
int byteCount;
while ( ( byteCount = resource.Stream.Read(bytes, 0, chunkSize) ) > 0 )
{
file.Write(bytes, 0, byteCount);
}
}
}
}
}
}
-
- 任务5——音频播放的后台代理
后台音频播放的代理运行在后台,是由操作系统实例化的对象,用来处理用户的后台运行请求。AudioPlayerAgent在后台运行,调用BackgroundAudioPlayer播放Zune媒体队列。
在解决方案中添加新的工程,工程模板为Windows Phone Audio Playback Agent。Windows Phone Audio Playback Agent模板位于Silverlight for Windows Phone 类别中,将新工程命名为AudioPlaybackAgent。
在此工程中需要用到model的定义和XML的序列化,所以在工程的引用中添加Models和System.Xml.Serialization的引用。
在AudioPlayer.cs中重构AudioPlayer类。删除原有的代码,在文件的头部添加引用,代码如下。
using System.IO.IsolatedStorage;
using System.Xml.Serialization;
using System.IO;
using Models;
在AudioPlayer类中添加静态变量currentTrackNumber和playlist。currentTrackNumber保存当前播放的音轨,playlist保存当前的播放列表。
static int currentTrackNumber = 0;
static Playlist playlist;
AudioPlayer的构造函数,从独立存储空间读取XML格式的文件列表。
public AudioPlayer()
: base()
{
//Load from IsoStore & deserialize
using ( IsolatedStorageFile isoStorage = IsolatedStorageFile.GetUserStoreForApplication() )
{
using ( IsolatedStorageFileStream file = isoStorage.OpenFile("playlist.xml", FileMode.Open) )
{
XmlSerializer serializer = new XmlSerializer(typeof(Playlist));
var reader = new StreamReader(file);
playlist = (Playlist)serializer.Deserialize(reader);
}
}
}
重载OnPlayStateChanged 方法,实现处理播放器的状态变化。其中调用NotifyComplete方法通知后台音频播放器状态的变化。
protected override void OnPlayStateChanged(BackgroundAudioPlayer player, AudioTrack track, PlayState playState)
{
switch ( playState )
{
case PlayState.TrackEnded:
PlayNext(player);
break;
case PlayState.TrackReady:
player.Play();
break;
default:
break;
}
NotifyComplete();
}
添加PlayNext和PlayPrev方法,实现上一个和下一个音轨的播放。
private void PlayNext(BackgroundAudioPlayer player)
{
var songsCount = playlist.Tracks.Count;
if ( ++currentTrackNumber >= songsCount )
{
currentTrackNumber = 0;
}
Play(player);
}
private void PlayPrev(BackgroundAudioPlayer player)
{
var songsCount = playlist.Tracks.Count;
if ( --currentTrackNumber < 0 )
{
currentTrackNumber = songsCount - 1;
}
Play(player);
}
重载OnUserAction方法,处理后台音频播放器与用户触控的交互。
protected override void OnUserAction(BackgroundAudioPlayer player, AudioTrack track, UserAction action, object param)
{
switch ( action )
{
case UserAction.FastForward:
player.FastForward();
break;
case UserAction.Pause:
player.Pause();
break;
case UserAction.Play:
if ( player.PlayerState == PlayState.Paused )
{
player.Play();
}
else
{
Play(player);
}
break;
case UserAction.Rewind:
player.Rewind();
break;
case UserAction.Seek:
player.Position = (TimeSpan)param;
break;
case UserAction.SkipNext:
PlayNext(player);
break;
case UserAction.SkipPrevious:
PlayPrev(player);
break;
case UserAction.Stop:
player.Stop();
break;
default:
break;
}
NotifyComplete();
}
实现初始化播放当前音轨的方法:从播放列表中获取当前音轨的源、标题、艺术家和专辑等信息,以此信息实例化AudioTrack类并播放。
private void Play(BackgroundAudioPlayer player)
{
var currentTrack = playlist.Tracks[currentTrackNumber];
Uri tileUri = ( currentTrack.Tile == null ? new Uri("Shared/Media/no-art.jpg", UriKind.Relative) :
( currentTrack.Tile.IsAbsoluteUri ? new Uri("Shared/Media/no-art.jpg", UriKind.Relative) :
new Uri(currentTrack.TileString.Replace("/Images", "Shared/Media"), UriKind.Relative) ) );
var audioTrack = new AudioTrack(currentTrack.Source,
currentTrack.Title,
currentTrack.Artist,
currentTrack.Album,
tileUri,
currentTrackNumber.ToString(),
EnabledPlayerControls.All);
player.Track = audioTrack;
}
最后一步,在MusicPlayer项目中添加AudioPlaybackAgent项目的引用。
任务6——测试应用程序
运行应用程序,单击播放按钮
。确认在键盘上按F9键可增加模拟器的音量,按F10键可降低模拟器的音量。
图4 在模拟器中运行
按下启动键
,确认音乐可以继续播放。
图5 后台音频播放控制器
在键盘上按下F9键显示后台音频播放器的控制界面,通过触控按钮控制当前播放的曲目。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构