Most apps employing background audio are going to want to provide an enhanced interface within the foreground app for controlling playback and displaying track information. This section looks at the MainPage
and associated classes of the WPUnleashed.BackgroundAudio project. The page allows the user to play audio via the BackgroundAudioPlayer
class. It provides application bar buttons for play and pause, stop, rewind, and forward, and several fields for displaying the current track information.
Note
It is a Windows Phone Marketplace certification requirement that when the user is already playing background music on the phone when your app is launched, your app must ask the user for consent to stop playing or to adjust the background music. This prompt must occur each time the app launches, unless there is an opt-in setting provided to the user, and the user has used this setting to opt in.
The BackgroundAudioPlayer
class is inherently difficult to use in unit tests because of the cross-process interaction with the AudioPlayerAgent
and its dependence on native phone components. For this reason I have provided a wrapper called BackgroundAudioPlayerProxy
, which implements a custom IBackgroundAudioPlayer
interface. In addition, a second implementation, MockBackgroundAudioPlayer
, allows you to test whether your code called the background audio player as expected; it allows you to test, for example, whether your code attempted to play a track.
The code for IBackgroundAudioPlayer
and its associated class implementations is not shown in this chapter because it is lengthy, and the methods, properties, and events of the IBackgroundAudioPlayer
coincide with those of the built-in BackgroundAudioPlayer
. You can, however, find the code in the downloadable sample code for the book.
The viewmodel is declared as a field of the MainPage, as shown:
readonly MainPageViewModel viewModel = new MainPageViewModel(
new BackgroundAudioPlayerProxy(),
new IsolatedStorageUtility());
The MainPageViewModel
class constructor accepts an IBackgroundAudioPlayer
as well as an IIsolatedStorageUtility
instance (see Listing 34.1). A subscription to the PlayStateChanged
event of the background audio player allows the viewmodel to update various properties whenever the play state changes.
The viewmodel contains five commands for controlling the BackgroundAudioPlayer
while the app is in the foreground, which provide for playing, pausing, stopping, and skipping between tracks.
The viewmodel’s Refresh
method is called to update the viewmodel, which relies on the state of the BackgroundAudioPlayer
. A Timer
is used to monitor the progress of the track while it is being played.
public MainPageViewModel(
IBackgroundAudioPlayer backgroundAudioPlayer,
IIsolatedStorageUtility isolatedStorageUtility)
{
player = ArgumentValidator.AssertNotNull(
backgroundAudioPlayer, "backgroundAudioPlayer");
ArgumentValidator.AssertNotNull(
isolatedStorageUtility, "isolatedStorageUtility");
player.PlayStateChanged += HandlePlayStateChanged;
BackgroundAudioPlayerAgent.CopyAudioToIsolatedStorage(isolatedStorageUtility);
playCommand = new DelegateCommand(obj => player.Play());
pauseCommand = new DelegateCommand(obj => player.Pause());
stopCommand = new DelegateCommand(obj => player.Stop());
previousTrackCommand = new DelegateCommand(obj => player.SkipPrevious());
nextTrackCommand = new DelegateCommand(obj => player.SkipNext());
Refresh(false);
timer = new Timer(HandleTimerTick, null, 3000, 1000);
}
The viewmodel’s TrackArtist
and TrackTitle
properties reflect the Artist
and Title
properties of the current AudioTrack
.
The viewmodel’s VisualState
property, which is of type string
, determines the visual state of the view, and the visibility of various application bar buttons. The VisualState
value is set to the PlayerState
property of the BackgroundAudioPlayer
, as shown:
void Refresh(bool setVisualState = true)
{
switch (player.PlayerState)
{
case PlayState.Playing:
CanPause = true;
TrackArtist = player.Track.Artist;
TrackTitle = player.Track.Title;
break;
case PlayState.Paused:
CanPause = false;
break;
}
BufferingProgress = player.BufferingProgress;
if (setVisualState)
{
VisualState = player.PlayerState.ToString("G");
}
if (player.Error != null)
{
MessageService.ShowError("A problem occured:" + player.Error);
}
}
When the viewmodel’s VisualState
property changes, the view responds by refreshing the VisualStateManager
state. This is performed from the MainPage
constructor, as shown:
public MainPage()
{
InitializeComponent();
DataContext = viewModel;
viewModel.PropertyChanged
+= (sender, args) =>
{
if (args.PropertyName == "VisualState")
{
SetVisualState();
}
};
}
The view’s custom AppBar
includes icon buttons bound to the viewmodel’s commands, as shown:
<u:AppBar>
<u:AppBarIconButton
Command="{Binding PreviousTrackCommand}"
Text="Previous"
IconUri="/Images/ApplicationBarIcons/Previous.png" />
<!-- A toggle button is used rather than visual state to avoid
repopulating the app bar when the user taps play or pause. -->
<u:AppBarToggleButton
x:Name="Button_Play"
Command1="{Binding PlayCommand}"
Text1="Play"
Icon1Uri="/Images/ApplicationBarIcons/Play.png"
Command2="{Binding PauseCommand}"
Text2="Pause"
Icon2Uri="/Images/ApplicationBarIcons/Pause.png"
Toggled="{Binding CanPause}"/>
<u:AppBarIconButton
x:Name="Button_Stop"
Command="{Binding StopCommand}"
Text="Stop"
IconUri="/Images/ApplicationBarIcons/Stop.png" />
<u:AppBarIconButton
Command="{Binding NextTrackCommand}"
Text="Previous"
IconUri="/Images/ApplicationBarIcons/Next.png" />
</u:AppBar>
The viewmodel’s ViewState
property determines the visibility of each button. The XAML that defines the VisualStateGroups
is not shown, but if you are interested, see the downloadable sample code.
Two TextBlock
controls are used to display the viewmodel’s TrackArtist
and TrackTitle
properties, like so:
<TextBlock Text="{Binding TrackArtist}"
Style="{StaticResource PhoneTextTitle3Style}"
TextWrapping="Wrap" />
<TextBlock Text="{Binding TrackTitle}"
Style="{StaticResource PhoneTextTitle2Style}"
TextWrapping="Wrap" />
Whenever the track changes, these fields are updated according to the new track.
The viewmodel contains a Position
property that reflects the progress of the current AudioTrack
. Because the BackgroundAudioPlayer
does not have facility to monitor the progress of a track directly, the viewmodel uses a Timer
to periodically raise a property changed event for the Position
property. The tick handler is shown in the following excerpt:
void HandleTimerTick(object state)
{
if (player.PlayerState == PlayState.Playing)
{
OnPropertyChanged(() => Position);
}
}
When a Position
property change is detected in the view, it prompts a Slider
control to reread the property. The Position
get accessor calculates the position value, which is returned as a value between 0 and 1, as shown:
public double Position
{
get
{
if (player.Track == null || player.Track.Duration.TotalSeconds < 1)
{
return 0;
}
double result = player.Position.TotalSeconds
/ player.Track.Duration.TotalSeconds;
return result;
}
set
{
if (player.Track != null)
{
double newSeconds = player.Track.Duration.TotalSeconds * value;
TimeSpan newPosition = TimeSpan.FromSeconds(newSeconds);
player.Position = newPosition;
}
OnPropertyChanged(() => Position);
}
}
Raising a property changed event for the Position
property causes the Slider
, which is bound to the property, to be updated. The Slider
is defined like so:
<Slider Value="{Binding Position, Mode=TwoWay}" Minimum="0" Maximum="1" />
Figure 34.3 shows the MainPage
with the slider indicating the progress of the current track and the application bar icon buttons for controlling playback.
Numerous possibilities exist for extending an app such as this. For example, it could be extended to include a view for the playlist or an image for the album art—the sky’s the limit!
18.118.119.229