Данная статья продемонстрирует, как написать простейший музыкальный плеер под 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();.
Теперь наше приложение обладает всей базовой функциональностью, которая необходима каждому плееру.
Заключение
Реализация плеера довольно типичная задача, и я надеюсь статья дала понять, как реализовать простой плеер.
Исходный код: скачать
Автор: StrangeAndr