老司機學新平台 - Xamarin開發之我的第一個MvvmCross跨平台插件:SimpleAudioPlayer


大家好,老司機學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事件,代表一次播放結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM