大家好,老司機學Xamarin系列又來啦!上一篇MvvmCross插件精選文末提到,Xamarin平台下,一直沒找到一個可用的跨平台AudioPlayer插件。那就自力更生,讓我們就自己來寫一個吧!
源碼和Nuget包
源碼:https://github.com/teddymacn/Teddy-MvvmCross-Plugins
Nuget包:https://www.nuget.org/packages/Teddy.MvvmCross.Plugin.SimpleAudioPlayer/
MvvmCross的PCL+Native插件架構簡介
在開始寫一個MvvmCross插件之前,先簡單介紹一下MvvmCross的插件架構。MvvmCross的插件,一般有三種類型:純PCL,PCL+Native和Configurable插件。本文介紹的是,最典型最常用的一種插件類型,即PCL+Native,簡單的說,就是一個PCL的Portable項目包含服務的接口,各個Platform特定的Xamarin Native項目包含不同平台的接口實現。
PCL項目除了需要包含一個服務接口外,還會包含一個PluginLoader類,這個類有一個標准實現,和我們要實現的自定義功能沒關系,只是調用的MvvmCross框架的相關類,它的代碼一般固定是這樣的:
public class PluginLoader
: IMvxPluginLoader
{
public static readonly PluginLoader Instance = new PluginLoader();
public void EnsureLoaded()
{
var manager = Mvx.Resolve<IMvxPluginManager>();
manager.EnsurePlatformAdaptionLoaded<PluginLoader>();
}
}
在一個MvvmCross項目啟動時,PluginLoader.Instance.EnsureLoaded()會被自動調用,通過反射裝載項目中定義的真正的插件。
在每個平台特定的Xamarin項目中,則通常要包含一個Plugin類,Plugin類只有一個Load()方法需要實現,用來在項目啟動時,自動向MvvmCross的IoC容器中注冊插件的接口實現。比如,本文要實現的SimpleAudioPlayer插件,它的Plugin類,它的Droid版本是這樣的:
namespace Teddy.MvvmCross.Plugins.SimpleAudioPlayer.Droid
{
public class Plugin
: IMvxPlugin
{
public void Load()
{
Mvx.RegisterType<IMvxSimpleAudioPlayer, MvxSimpleAudioPlayer>();
}
}
}
在使用這個插件的具體的Xamarin App的Bootstrop目錄中,一般當我們添加一個MvvmCross插件的nuget package時,package會自動為每個插件創建一各PluginBootstrap類,只有App包含了PluginBootstrap類,對應的插件才會被MvvmCross框架自動裝載。比如,我們的SimpleAudioPlayer插件的package,如果在一個Droid App里面被引用,它會向Bootstrap目錄里自動添加一個SimpleAudioPlayerPluginBootstrap類如下:
public class SimpleAudioPlayerPluginBootstrap
: MvxPluginBootstrapAction<Teddy.MvvmCross.Plugins.SimpleAudioPlayer.PluginLoader>
{ }
上面就是一個PCL+Native插件包含的所有元素。一旦根據這些命名規范,裝載了一個插件,我們就可以在ViewModel里面,通過構造函數注入,或者通過調用Mvx.Resolve()獲取我們的接口的實例了。比如,在我們的Demo項目中,通過構造函數注入,得到了插件接口的實例:
public class MainViewModel : BaseViewModel
{
private readonly IMvxSimpleAudioPlayer _player;
private readonly IMvxFileStore _fileStore;
public MainViewModel(IMvxSimpleAudioPlayer player
, IMvxFileStore fileStore
)
{
_player = player;
_fileStore = fileStore;
}
...
關於其他類型的MvvmCross插件的介紹,請參見官方文檔。
需求定義
我們來列一下我們要實現的插件的需求:
- 實現一個跨平台(Droid,iOS,UWP)支持在線(by URL)和本地(打包到App)文件的常見audio文件(至少支持mp3)播放;
- 支持MvvmCross的插件架構
項目結構
定義Portable接口
首先,我們需要新建一個跨平台的Portable項目Teddy.MvvmCross.Plugins.SimpleAudioPlayer,包含這個播放器的基本接口:
public interface IMvxSimpleAudioPlayer : IDisposable
{
/// <summary>
/// Gets the current audio path.
/// </summary>
string Path { get;}
/// <summary>
/// Gets the duration of the audio in milliseconds.
/// </summary>
double Duration { get; }
/// <summary>
/// Gets the current position in milliseconds.
/// </summary>
double Position { get; }
/// <summary>
/// Whether or not it is playing.
/// </summary>
bool IsPlaying { get; }
/// <summary>
/// Gets or sets the current volume.
/// </summary>
double Volume { get; set; }
/// <summary>
/// Opens a specified audio path.
///
/// The following formats of path are supported:
/// - Absolute URL,
/// e.g. http://abc.com/test.mp3
///
/// - Assets Deployed with App, relative path assumed to be in the device specific assets folder
/// Android and UWP relative to the Assets folder while iOS relative to the App root folder
/// e.g. test.mp3
///
/// - Local File System, arbitry local absolute file path the app has access
/// e.g. /sdcard/test.mp3
/// </summary>
/// <param name="path">
/// The audio path.
/// </param>
bool Open(string path);
/// <summary>
/// Plays the opened audio.
/// </summary>
void Play();
/// <summary>
/// Stops playing.
/// </summary>
void Stop();
/// <summary>
/// Pauses the playing.
/// </summary>
void Pause();
/// <summary>
/// Seeks to specified position in milliseconds.
/// </summary>
/// <param name="pos">The position to seek to.</param>
void Seek(double pos);
/// <summary>
/// Callback at the end of playing.
/// </summary>
event EventHandler Completion;
}
注釋已經自描述了,就不多解釋了。簡單的說,我們的播放器支持Open一個audio文件,然后可以Play,Stop,Pause等等。離全功能的音樂播放器還差得遠,不過,用來實現app中各種簡單的在線和本地mp3播放控制應該足夠了。
Droid實現
Droid的實現是Teddy.MvvmCross.Plugins.SimpleAudioPlayer.Droid項目中的MvxSimpleAudioPlayer類。安卓的媒體播放一般都基於安卓SDK的MediaPlayer類,代碼並不復雜,但是,有一些坑。
坑一:
首先是播放不同來源(URL,本地或Assets中的)的文件,Load文件的方式有差異:
_player = new MediaPlayer();
if (Path.StartsWith(Root) || Uri.IsWellFormedUriString(Path, UriKind.Absolute))
{
// for URL or local file path, simply set data source
_player.SetDataSource(Path);
}
else
{
// search for files with relative path in Assets folder
// files in the Assets folder requires to be opened with a FileDescriptor
var descriptor = Application.Context.Assets.OpenFd(Path);
long start = descriptor.StartOffset;
long end = descriptor.Length;
_player.SetDataSource(descriptor.FileDescriptor, start, end);
}
對於在線的URL和絕對路徑的本地文件,只需要設置MediPlayer的SetDataSource()就可以了;但是對於Assets目錄中,和App一起打包發布的資源,必須通過Assets.OpenFd()打開,才能設置SetDataSource()。
坑二:
MediaPlayer調用Stop()之后,重新播放之前必須重新Prepare(),否則會報錯:
public void Stop()
{
if (_player == null) return;
if (_player.IsPlaying)
{
_player.Stop();
// after _player.Stop(), re-prepare the audio, otherwise, re-play will fail
_player.Prepare();
_player.SeekTo(0);
}
}
坑三:
銷毀一個MediaPlayer的實例之前,必須先調用Reset()方法,否則,Xamarin主程序不會報錯,但是,Debug日志會顯示內部有exception,可能會導致內存泄漏:
private void ReleasePlayer()
{
// stop
if (_player.IsPlaying) _player.Stop();
// for android, thr call to Reset() is required before calling Release()
// otherwise, an exception will be thrown when Release() is called
_player.Reset();
// release the player, after what the player could not be reused anymore
_player.Release();
}
完整的源代碼可以看這里:MvxSimpleAudioPlayer.cs
iOS實現
iOS實現在Teddy.MvvmCross.Plugins.SimpleAudioPlayer.iOS項目的MvxSimpleAudioPlayer類。iOS下的音頻播放一般通過SDK的AVPlayer或者AVAudioPlayer類,我也不是iOS的專家,不太清楚兩個有啥淵源,最開始嘗試使用AVAudioPlayer,但是,播放本地文件沒問題,播放URL遇到了各種問題,最后也沒有解決。換成使用AVPlayer以后,順暢了很多。如果有知道什么時候應該使用AVAudioPlayer而不是AVPlayer的,望不吝告知。
使用AVPlayer播放mp3的整個過程,要比安卓下的MediaPlayer順暢很多。有兩點需要注意的:
注意一:
Load不同來源的文件,注意使用不同的格式的URL:
AVAsset audioAsset;
if (Uri.IsWellFormedUriString(Path, UriKind.Absolute))
audioAsset = AVAsset.FromUrl(NSUrl.FromString(Path));
else if (Path.StartsWith(Root))
audioAsset = AVAsset.FromUrl(NSUrl.FromString("file://" + Path));
else
audioAsset = AVAsset.FromUrl(NSUrl.FromFilename(Path));
_timeScale = audioAsset.Duration.TimeScale;
var audioItem = AVPlayerItem.FromAsset(audioAsset);
_player = AVPlayer.FromPlayerItem(audioItem);
上面的代碼組要注意的是,當Path是相對路徑時,NSUrl.FromFilename(Path)生成的絕對路徑是相對於App主程序目錄的。
注意二:
和Droid下MediaPlayer直接包含Completion事件回掉,能夠知道一次播放已經完成不同,AVPlayer上面沒有這類通知包裝成.NET事件,而且也沒有專門的Play Completion這樣的事件,不過,AVPlayer包含一個AddBoundaryTimeObserver()方法,可以在音頻播放到指定的進度時,回調指定的方法,所以,也可以實現類似Completion事件的通知:
_player.AddBoundaryTimeObserver(
times: new[] { NSValue.FromCMTime(audioAsset.Duration) }, // callback when reach end of duration
queue: null,
handler: () => Seek(0));
完整的源代碼可以看這里:MvxSimpleAudioPlayer.cs
UWP實現
這里的UWP實現,目前只支持uap10.0這個target。編譯的程序在Win10上運行是沒問題的,別的UWP支持的環境沒測過,對WinPhone也不是很了解,如果對這方面有需要的朋友,自己做一下擴展吧。
UWP的實現在是Teddy.MvvmCross.Plugins.SimpleAudioPlayer.UWP項目的MvxSimpleAudioPlayer類。這里並沒有像Droid和iOS那樣每次實例化一個內部的player實例,而是調用了BackgroundMediaPlayer.Current這個默認MediaPlayer實例。
微軟自己的Player還是封裝的非常好的,使用非常簡單,唯一值得一提的是,Load Assets目錄中的文件時,需要指定一個特別的protocol:
if (Uri.IsWellFormedUriString(Path, UriKind.Absolute) || Path.Contains(Drive))
_player.Source = MediaSource.CreateFromUri(new Uri(path, UriKind.Absolute));
else
_player.Source = MediaSource.CreateFromUri(new Uri(string.Format("ms-appx:///Assets/" + path, UriKind.Absolute)));
完整的源代碼可以看這里:MvxSimpleAudioPlayer.cs
好了,不同平台的實現就介紹到這里。下面來看看示例程序。
示例程序
本項目的源碼同時包含了Droid,iOS和UWP各平台的Demo程序,可以直接運行體驗。示例程序包含了一個簡單的UI,演示了播放Assets里的mp3文件,mp3 URL和從遠程URL下載到本地的mp3。
調用IMvxSimpleAudioPlayer接口播放的代碼,主要在MainViewModel中,播放不同來源文件的示例在OpenAudio()方法中:
private void OpenAudio()
{
// for testing with remote audio, you need to setup a web server to serve the test.mp3 file
// and please change the server address below
// according to your local machine, device or emulator's network settings
string server = (Device.OS == TargetPlatform.Android) ?
"http://169.254.80.80" // default host address for Android emulator
:
"http://192.168.2.104"; // my local machine's intranet ip, change to your server's instead
// by default, testing playing audio from Assets
_player.Open("test.mp3");
_player.Volume = 1;
_player.Play();
// comment the code above and uncomment the code below
// if you want to test playing a remote audio by URL
//_player.Open(server + "/test.mp3");
//_player.Play();
// comment the code above and uncomment the code below
// if you want to test playing a downloaded audio
//var request = new MvxFileDownloadRequest(server + "/test.mp3", "test.mp3");
//request.DownloadComplete += (sender, e) =>
//{
// _player.Open(_fileStore.NativePath("test.mp3"));
// _player.Play();
//};
//request.Start();
}
上面的OpenAudio()方法中,默認播放的是,打包到App的Assets里的mp3文件,兩外兩個被注釋掉的版本,則分別是播放URL,和下載URL到本地mp3再播放。下載文件的部分,使用了MvvmCross官方的DownloadCache插件和File插件。
URL地址可能需要根據你的本地情況自己設置了,可以將Droid Demo的Assets目錄里的test.mp3放到比本機的某個web server下面。注意,安卓模擬器訪問的ip只能是對應安卓模擬器的虛擬網卡的ip。在我本機上安卓SDK模擬器的虛擬網卡ip是169.254.80.80,Android Emulator for Visual Studio的虛擬網卡ip是192.168.17.1。這個不確定每個機器上是不是一樣,具體的可以在cmd里面執行ipconfig /all看到,你也可以先在模擬器里的browser里面訪問試試。
安卓的運行效果如下:
iOS運行效果如下:
UWP在Win10下運行如下:
其他注意事項:
在Droid下,從URL播放音頻需要設置INTERNET權限:
在iOS下,從非https的URL播放音頻需要在項目根目錄的info.plist文件中配置NSAppTransportSecurity參數,否則無法播放:
...
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
在UWP下,可能因為UWP App的項目是.Net Core格式的項目類型,nuget package的自動往Bootstrap目錄自動添加PluginBoorstrap類的功能,貌似不work,這個感覺算是VS 2015的Package Manager的bug。anyway,如果它沒有自動添加,用戶可以參考UWP的Demo手動添加。
就是這么多了,enjoy!
PS:雖然是‘老’司機,不過對Xamarin和安卓、iOS和UWP開發都是剛接觸不久,如有任何疏漏或者錯誤,請不吝指正,共同學習,謝謝!
2016-10-23 Update:
- 將SimpleAudioPlayer升級到了1.0.5,新增了Position,IsPlaying和Volume屬性。
- 另外,在Xamarin-Forms-Labs這個開源項目里,終於發現一個ISoundService,同樣實現了Xamarin下的Droid,iOS和UWP下的mp3播放,不過它只支持本地Assets中的文件播放,並不支持本地絕對路徑和在線URL的播放。功能上被SimpleAudioPlayer完全壓倒!不過,咱的新版本新增了Position,IsPlaying和Volume屬性是受它啟發,這幾個確實是必須的屬性參數,所以,還是要感謝人家的!
- 22:30, 再次將SimpleAudioPlayer升級到了1.0.6,新增了Completion事件,代表一次播放結束。