1. 程式人生 > >Xamarin開發之我的第一個MvvmCross跨平臺外掛:SimpleAudioPlayer

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的