Pull to refresh

Пишем простой плеер под Windows Phone

Reading time7 min
Views13K
Данная статья демонстрирует, как написать простейший музыкальный плеер под Windows Phone.

Основные возможности: плеер работает с плейлистом, можно управлять музыкой (переключать мелодии), плеер может играть в фоне.

Автор в первую очередь руководствовался статьей, в которой не были освещены некоторые элементарные, но не всегда очевидные, аспекты написания плеера. Они и разобраны в данной статье.


Создание проекта




За основу берем обычное Silverlight for Windows Phone решение, назовем его SimplePlayer.



К уже существующему решению добавим еще один проект вида Windows Phone Audio Playback Agent, назовем его AudioPlaybackAgent.



В проекте SimplePlayer добавим ссылку на AudioPlaybackAgent.


SimplePlayer – основной проект, он содержит и обслуживает графический интерфейс и запускается первым, а AudioPlaybackAgent – это оболочка над внутренним плеером Windows Phone, которая запускается из основного проекта при запросе на воспроизведение трэка.
Общение между двумя проектами, во время выполнения приложения, довольно ограничено и накладывает некоторые ограничения, мы рассмотрим, как их можно преодолеть.

Дизайн интерфейса


Разберемся с дизайном: он довольно прост и сразу дает понять, каким функционалом должно обладать приложение.
Для этого откройте MainPage.xaml, где содержится разметка интерфейса.
1. Надо заменить на StackPanel с именем TitlePanel на данный код:
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
	<TextBlock x:Name="ApplicationTitle" Text="SIMPLE PLAYER" 
				Style="{StaticResource PhoneTextNormalStyle}"/>
	<TextBlock x:Name="PageTitle" Text="playlist" Margin="9,-7,0,0" 
				Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>

2. Надо заменить Grid с именем ContentPanel на код:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
	<StackPanel Orientation="Vertical">
		<ListBox x:Name="PlayListBox" SelectionChanged="PlayListBox_SelectionChanged"/>
		<StackPanel Orientation="Horizontal">
                	<Button x:Name="PrevButton" Content="prev" Height="140" Width="140"
						Click="PrevButton_Click"/>                   
			<Button x:Name="PlayButton" Content="play" Height="140" Width="140"
						Click="PlayButton_Click"/>
                	<Button x:Name="NextButton" Content="next" Height="140" Width="140"
						Click="NextButton_Click"/>
		</StackPanel>
	</StackPanel>
</Grid>

После этого интерфейс в дизайнере будет выглядеть так:


На этом мы с дизайном покончили.

Плэйлист


Пришло время поговорить о том, как мы будем хранить плэйлист с трэками и откуда будем брать эти трэки.

Добавим в проект SipmlePlayer несколько трэков формата mp3 или wma. (В данном примере: 1.mp3, 2.mp3, 3.mp3)



Теперь откроем файл MainPage.xaml.cs, где представлен код, обслуживающий наш интерфейс. Добавим в класс MainPage поле:
private List<string> playlist; 

Такое же поле нужно добавить в проект AudioPlaybackAgent в класс AudioPlayer.

В этом поле мы будем хранить наш список файлов, в пользу простоты сразу отказываемся от хранения и отображения названия трэков, исполнителей и альбомов.

Хранить плэйлист мы будем не статически в коде, а в XML файле, который будет десериализорвываться в поле playlist. Добавим в основной проект xml файл, назовем его playlist.xml. Содержимое будет таким:
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
			xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<string>1.mp3</string>
	<string>2.mp3</string>
	<string>3.mp3</string>
</ArrayOfString>

Для другого списка файлов надо просто добавлять, изменять, удалять тэги .



Внимание: Не забудьте у всех файлов музыки и плэйлиста выставить Build Action в Content, a Copy to Output Directory в Copy always.

Пришло время сделать playlist.xml доступным для обоих проектов. Для этого необходимо скопировать его в IsolatedStorage, доступ к которому есть у обоих проектов. Хранилище, на данный момент, — единственный способ сообщать в фоновый агент информацию помимо событий и текущего трэка.

Функция сохранения файла из проекта в хранилище:
private void CopyToIsolatedStorage(string fullFilePath, string storeFileName)
{
	using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
	{
		if (!storage.FileExists(storeFileName))
		{
			StreamResourceInfo resource = 
				Application.GetResourceStream(new Uri(fullFilePath, 
					UriKind.Relative));
			
			using (IsolatedStorageFileStream file = storage.CreateFile(storeFileName))
			{
				const int chunkSize = 4096;
				byte[] bytes = new byte[chunkSize];
				int byteCount;

				while ((byteCount = resource.Stream.Read(bytes, 0, chunkSize)) > 0)
				{
					file.Write(bytes, 0, byteCount);
				}
			}
		}
	}
}

Её нужно добавить в класс MainPage, подробно рассматривать ее в данной статье нет смысла. После чего вызываем ее из конструктора:
CopyToIsolatedStorage("playlist.xml", "playlist.xml"); 

А вот теперь мы ее и десериализуем для этого напишем функцию, которую тоже нужно вызвать в конструкторе сразу после предыдущей.
private void LoadPlaylist()
{
	using (IsolatedStorageFile myIsolatedStorage = 
			IsolatedStorageFile.GetUserStoreForApplication())
	{
		using (IsolatedStorageFileStream stream = 
			myIsolatedStorage.OpenFile("playlist.xml", FileMode.Open))
		{
			XmlSerializer serializer = new XmlSerializer(typeof(List<string>));
			playlist = (List<string>)serializer.Deserialize(stream);
		}
	}
}

Эту же функцию надо скопировать в проект AudioPlaybackAgent и добавить ее вызов в конструкторе класса AudioPlayer.


Внимание: Для этой функции необходимо добавить ссылку на System.Xml.Serialization.

Позаботимся теперь о загрузке музыкальных файлов, написав функцию, которую надо вызвать в конструкторе класс MainPage сразу после LoadPlaylist().
private void LoadMusicFiles()
{
	foreach (var filepath in playlist)
	{
		CopyToIsolatedStorage(filepath, filepath);
	}
}

Внимание: Данный способ является не оптимальным и критично задерживает загрузку приложения, советую для реальных приложений прибегать к BackgroundWorker.

Чтобы наш плэйлист отображался, и пользователь мог с ним работать, в классе MainPage в конструкторе после вызова InitializeComponent() добавим строку:
PlayListBox.ItemsSource = playlist;


Обработчики событий


Когда мы отредактировали XAML код, то объявили обработчики событий, но не описали их. Рассмотрим код:
private void PlayListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
	string filepath = (string)e.AddedItems[0];
	BackgroundAudioPlayer.Instance.Track =
		new AudioTrack(new Uri(filepath,UriKind.Relative),null,null,null,null);
	BackgroundAudioPlayer.Instance.Play();
}

private void PrevButton_Click(object sender, RoutedEventArgs e)
{
	BackgroundAudioPlayer.Instance.SkipPrevious();
}

private void PlayButton_Click(object sender, RoutedEventArgs e)
{
	if (BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing)
		BackgroundAudioPlayer.Instance.Pause();
	else BackgroundAudioPlayer.Instance.Play();
}

private void NextButton_Click(object sender, RoutedEventArgs e)
{
	BackgroundAudioPlayer.Instance.SkipNext();
}

Может возникнуть вопрос, что же такое BackgroundAudioPlayer?

Это, на самом деле, и есть единственная прямая связь с фоновым агентом.
В этих обработчиках мы задаем поведение нашего агента, но мы также можем и получать от него события (например остановка трэка, начало проигрывания и т.д.), для этого в класс MainPage добавим метод:
void Instance_PlayStateChanged(object sender, EventArgs e)
{
	switch (BackgroundAudioPlayer.Instance.PlayerState)
	{
		case PlayState.Playing:
			PlayButton.Content = "pause";
			//при переключение трэка, делаем его выделение
			PlayListBox.SelectedItem =
				BackgroundAudioPlayer.Instance.Track.Source.OriginalString; 
			break;
		case PlayState.Paused:
		case PlayState.Stopped:
			PlayButton.Content = "play";
			break;
	}
}


А в конструкторе класса MainPage свяжем его с нашим агентом строкой:
BackgroundAudioPlayer.Instance.PlayStateChanged += Instance_PlayStateChanged;

Стоит заметить, что поле BackgroundAudioPlayer.Instance.PlayerState содержит текущие состояние фонового агента, т.е. музыка играет, на паузе, закончилась и так далее. Еще есть поле BackgroundAudioPlayer.Instance.Position, которое отражает текущее место проигрывания в файле, что также может пригодиться, мы же опустим эту возможность.

На данном этапе наш проект почти закончен, но не работает переключение трэков, а также не происходит переход на следующий трэк, если текущий закончил проигрываться.

В классе AuidioPlayer, можно увидеть две интересные функции OnPlayStateChanged и OnUserAction. Первая отвечает за обрабатывание изменений состояния агента, в вторая — за обработку воздействия основного проекта. В обеих вызываются методы заглушки, которые подают следующий и предыдущий трэк.

Реализуем:
private AudioTrack GetNextTrack()
{
	int next =
		(playlist.IndexOf(BackgroundAudioPlayer.Instance.Track.Source.OriginalString) + 1)
			% (playlist.Count);

	return new AudioTrack(new Uri(playlist[next], UriKind.Relative), null, null, null, null); ;
}

private AudioTrack GetPreviousTrack()
{
	int prev = 
		(playlist.IndexOf(BackgroundAudioPlayer.Instance.Track.Source.OriginalString) -1 
			+ playlist.Count) % (playlist.Count);

	return new AudioTrack(new Uri(playlist[prev], UriKind.Relative), null, null, null, null); ;
}

По непонятным автору причинам, разработчики шаблона решили, что при окончание трэка(PlayState.TrackEnded), следующим будет проигрываться предыдущий трэк, для решения этого недоразумения нужно в обработчике OnPlayStateChanged заменить player.Track = GetPreviousTrack(); на player.Track = GetNextTrack();.

Если пользователь не выбрал трэк, и он нажимает одну из кнопок (play/next/prev), тогда в классе AuidioPlayer также не выбран текущий трэк, и поэтому могут возникнуть критические исключения.
При первом событие пользователя, определим текущим трэк первым, если он еще не выбран, добавив первую строчку в метод OnUserAction:
if (player.Track == null) 
	player.Track = new AudioTrack(new Uri(playlist[0], UriKind.Relative), null, null, null, null);


Теперь наше приложение обладает всей заявленной базовой функциональностью, которая необходима каждому плееру.

Заключение


Реализация плеера — довольно типичная задача, и я надеюсь, что статья дала понять, как сделать простой плеер.

Исходный код: скачать/посмотреть
Пример приложения на основе статьи: Музыка Нового Года
Tags:
Hubs:
+15
Comments17

Articles

Change theme settings