Xamarin開發之我的第一個MvvmCross跨平臺外掛:SimpleAudioPlayer
大家好,老司機學Xamarin系列又來啦!上一篇MvvmCross外掛精選文末提到,Xamarin平臺下,一直沒找到一個可用的跨平臺AudioPlayer外掛。那就自力更生,讓我們就自己來寫一個吧!
原始碼和Nuget包
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();
}
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));
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)));
好了,不同平臺的實現就介紹到這裡。下面來看看示例程式。
示例程式
本專案的原始碼同時包含了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事件,代表一次播放結束。
相關推薦
Xamarin開發之我的第一個MvvmCross跨平臺外掛:SimpleAudioPlayer
大家好,老司機學Xamarin系列又來啦!上一篇MvvmCross外掛精選文末提到,Xamarin平臺下,一直沒找到一個可用的跨平臺AudioPlayer外掛。那就自力更生,讓我們就自己來寫一個吧! 原始碼和Nuget包 MvvmCross的PCL+Native外掛架構簡介 在開始寫一個MvvmCross
我的第一個Chrome小外掛-基於vue開發的flexbox佈局CSS拷貝工具
概述 之前介紹過 移動Web開發基礎-flex彈性佈局(相容寫法) 裡面有提到過想做一個Chrome外掛,來生成flexbox佈局的css程式碼直接拷貝出來用。最近把這個想法實現了,給大家分享下。 play-flexbox外掛介紹 play-flexb
【Mac系統 + Python + Django】之搭建第一個Demo
versions 打開 配置 onf demo -s 進入 127.0.0.1 seq 一、首先,用pip安裝Django # 安裝命令 pip install django==1.10.3 安裝路徑為: /Users/zhan/.pyenv/versi
結對開發之《返回一個整數陣列中最大子陣列的和》
一、題目要求 題目:返回一個整數陣列中最大子陣列的和。 要求: 輸入一個整形陣列,數組裡有正數也有負數。 陣列中連續的一個或多個整陣列成一個子陣列,每個子陣列都有一個和。
使用Phaser開發你的第一個H5遊戲(一)
本文來自網易雲社群 作者:王鴿 不知你是否還記得當年風靡一時的2048這個遊戲,一個簡單而又不簡單的遊戲,總會讓你在空閒時間玩上一會兒。 在這篇文章裡,我們將使用開源的H5框架——Phaser來重現這個遊戲。這裡你可以瞭解到遊戲內的狀態管理、Sprite元件物件等,以及如何使用Preload、Create
APP開發之我遇到的那點事兒-1
客戶端結構圖:通用型(N-Tab)結構。 開發工具:xcode 9.4.1 語言:OC 1、建立一個Single View App。 2、新增一個.pch檔案,在Prefix Header新增.pch檔案的路徑。 3、匯入常用的第三方庫( cocopads管理),cocopads環境搭建
後臺開發閱讀筆記——第一個C++程式
#include <>與#include ""的區別: 前者常用來包含系統提供的標頭檔案,編譯器會到儲存系統標準標頭檔案的位置查詢標頭檔案;後者常用於包括程式設計師自己編號的標頭檔案,用這種格式時,編譯器先查詢當前目錄是否有指定名稱的標頭檔案,然後從標準頭目錄中進行查詢。
Servlet基礎之實現第一個servlet程式
實現第一個Servlet程式 1. 建立servlet檔案 在某個盤下建立一個新的資料夾,(資料夾名字自己開心就好,也可以是原來命名好的資料夾)在此目錄下建立一個XXXX.java檔案,內容如下 packagejava_web;//這是包名,也就
「 Android開發 」開啟第一個App應用
每天進步一丟丟,連線夢與想 無論什麼時候,永遠不要以為自己知道一切 —巴普洛夫 最近玩了下Android,但遇到了一些坑,浪費了很多的時間,在此記錄一下,你若是遇到了就知道怎麼解決了 PS:建議使用電腦網頁開啟,圖片較多 開發環境 1.A
Scrum敏捷開發之我的總結
Team剛剛完成了一個敏捷專案,做一下專案總結,以備以後借鑑和提高。 需求 - 溝通 – 人 - 過程 - 工具 專案要成功的最關鍵因素是什麼?軟體要快速高效又高質量的提交靠的是什麼?有人說最關鍵是專案經理,關鍵是溝通,有人說是技術設計,有人說是對需求的把握… … 從
作業系統之----我是一個程序話劇
視訊展示 視訊地址 場景準備 運算器,控制器,儲存器,輸入裝置,輸出裝置 人員準備 一個程式A--------進化成一個程序A--------程序控制塊A 一個程式B--------進化成一個程序B--------程序控制塊B cpu處理人員 程序A分出來的執行
Android開發之愛奇藝Flutter跨平臺Hybrid實踐
愛奇藝開播助手 愛奇藝開播助手專案,又稱"直播機",該專案目標是通過一個移動平臺為主播提供多樣化的直播內容。現階段所涵蓋的直播內容包括:遊戲直播,美女攝像直播,小劇場直播,其中游戲直播相對主播數量最多,3種推流模式所涉及的推流SDK基本一致,推流邏輯存在部分差異。 該專案的Android端和
python開發之路---第三次筆記
部分字串用法 1 s.startswith() # 以xxxx開頭 2 s.endswith() # 以xxxx結尾 3 s.split() #以某個字元分割字串,並以列表的形式儲存 4 isdigit
開發你的第一個React + Ant Design網頁(一、配置+編寫主頁)
前言 React是Facebook推出的一個前端框架,之前被用於著名的社交媒體Instagram中,後來由於取得了不錯的反響,於是Facebook決定將其開源。出身名門的React也不負眾望,成功成為當前最火熱的三大前端框架之一。相比於Angular,Reac
python開發之路---第四次筆記--解碼和編碼
utf-8 ------> decode 解碼 ---》 Unicode Unicode ---> encode 編碼 ---》 GBK/UTF-8 舉個栗子 s =
JAVA之旅——第一個Java程式
JAVA之旅——第一個Java程式 1、開始入坑JAVA,首先得配置環境,安裝IDE(eclipse)。 JDK配置是一件很重要的事,需要細心謹慎(安裝jdk,配置環境變數)。安裝eclipse就相對來說容易(解壓開啟應用程式即可)。 2、建立第一個JAVA程式,第一次啟動ec
SharePoint 2013 開發——開發並部署第一個APP
本篇我們開始對開發APP應用程式進行了解。本篇基於本地SharePoint環境(如果是Office 365的話會方便許多),需要配置一下APP的環境,具體參照霖雨大神的Blog。開發APP的第一步,建立
嵌入式linux開發環境熟悉---第一個hello word!
1.前言:我對linux環境的一點認知 初學嵌入式linux,對於整個環境的認知,以及整個環境的操作非常重要。平時程式設計都是在整合開發環境下進行,比如VC6.0,寫完程式碼後,直接按鈕單擊“編譯”,點選“執行”,均是介面化操作。但各位開發程式猿們是否想過這整
JavaEE學習之路|第一個jsp
為了更加鞏固java的基礎,學習JavaEE的知識,從這一篇文章開始一步一步地進階,學習JavaEE的開發。 首先,開發所用的伺服器為tomcat,以下為部署tomcat的過程: 1.到tomcat官
1、OpenGL之旅+第一個OpenGL視窗
第一種方法:使用glut 工具包建立第一OpenGL視窗程式 首先,需要包含標頭檔案#include <GL/glut.h>,這是GLUT的標頭檔案。 本來OpenGL程式一般還要包含<GL/gl.h>和<GL/glu.h>,但GLUT的