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

Данная статья демонстрирует, как написать простейший музыкальный плеер под 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);


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

Заключение


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

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

Similar posts

Comments 17

    0
    Жаль, что это реализация всего лишь GUI :(
      0
      Осильте VCS для хранения кода.
        0
        Спасибо за комментарий.
        Теперь можно сразу посмотреть код.
        0
        Да что ж такое, откуда опять пошла популярность недохостинга ipicture :( Повезёт, если загрузится хоть одна картинка. Из-за рубежа вообще ни одна не грузится.
        PS. Пользуйтесь, пожалуйста, по возможности habrastorage.org/
          +1
          Спасибо за комментарий. Ваше пожелание учтено, картинки перезалиты.
          0
          Этот подход не решает одной проблемы: Если пользователь переключает «Вперед/назад» не с помощью вашего плеера, а с помощью стандартного UVC (который вызывается кнопками регулировки звука), то как отобразить в нашем плеере, что что-то изменилось?
            0
            Когда вы находитесь в самом приложение, и переключаете с помощью UVC, то благодаря методу Instance_PlayStateChanged и секции PlayState.Playing при переключение треков, выделение в списке будет менять на нужный трэк.

            Но тут есть одна действительно не решенная задача, если мы переключили трэк когда приложение было в фоне.
            То, чтобы корректно отобразить текущий трэк при возобновление приложения, надо просто в метод
            OnNavigatedTo(который срабатывает при переходе на страницу) вставить такой же switch, что и в Instance_PlayStateChanged.
              0
              Я имел ввиду, что прямой связи между AudioPlaybackAgent и нашим приложением нет. Мы можем использовать только промежуточный BackgroundPlayer.

              В случае с событием PlayStateChanged мы не можем быть уверены, что изменилась песня (к примеру пользователь сначала приостановил песню Pause, потом снова воспроизвел или началась буферизация в случае со стримингом), нужно хранить её где-то в памяти и сравнивать Track. Более того, как чаще бывает в самом приложении мы не храним Track, он представлен в виде модели другой сущности и есть только три способа (которые знаю я) как передать данные из агента в наше приложение:
              1. Использовать свойство Tag у трека. (т.е. некие meta-данные, используя которые можно преобразовать в необходимые модели)
              2. Изолированное хранилище (используя серилизацию/дессириализацию хранить текущую песню)
              3. База данных

              У меня сейчас как раз встал вопрос «как используя mvvm-патерн обновить view из viewmodel когда пользователь перемещается вперед/назад используя UVC».

            0
            Еще одно странное решение:
            (playlist.IndexOf(BackgroundAudioPlayer.Instance.Track.Source.OriginalString) + 1)
            У нас BackgroundAudioPlayer.Instance.Track может быть равен null, соответственно это упадает. Да и вообще искать в массиве по строке, не самое удачное решение, если массив неизеняется, лучше держать индекс.

            Второе, сам по себе agent может быть убит системой, точнее он пересоздается бекграундом, когда это надо, я столкнулся с этим, когда держал также массив музыки в плеере.
              0
              Очень ценный комментарий.
              Действительно если начать работать с плеером не с выбора трэка, а с кнопок, то плеер падает.

              Для того, чтобы он корректно работал необходимо в классе AudioPlayer в методе OnUserAction в самом начале его добавить такую строчку:
              if (track.Source == null) player.Track = new AudioTrack(new Uri(playlist[0], UriKind.Relative), null, null, null, null);

              Она определит первый трек текущим, если еще не был выбран трэк.
              Данное решение добавлю в статью в ближайшее время.

              А по поводу того, как хранить список музыки, считать индекс или нет, зависит от конкретной задачи. В данном случае, я думаю, что это не принципиально.
                0
                Небольшое уточнение. вместо проверки if (track.Source == null) лучше if (player.track == null)

                  0
                  Спасибо, так на самом деле понятнее и правильнее.
            • UFO just landed and posted this here
                0
                Microsoft на данный момент запрещает использовать нативный код в приложениях под WP7. Нет, использовать его конечно можно, но тогда приложение в маркет не пропустят.
                • UFO just landed and posted this here
                0
                Вероятно используют XNA и управляемый код. NDK для WP7 не выпускали и пока что о ней никаких новостей.
                Так же вполне возможно, что для крупных студий (вроде Gameloft) Microsoft дает добро на использование нативных сборок.
                Например игра Tentacles — хорошая графика и производительность, довелось отрефлексить ее код — там все управляемое.
                  0
                  Капец, ну нафига так сложно то (претензия к авторам долбанных агентов и прочего API)?..
                  Отдельный проект с передачей данных в него через файловое хранилище… капец жопа…

                  Only users with full accounts can post comments. Log in, please.