• 老司机学新平台


    大家好,老司机学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事件,代表一次播放结束。
  • 相关阅读:
    什么是操作系统
    去除按钮点击的边框
    unsupported time zone specified undefined
    w3c JS测试
    视频播放器
    document.write
    HTML4到HTML5
    MPU6050
    NRF24L01模块配置
    4-Four-Seeing hands
  • 原文地址:https://www.cnblogs.com/teddyma/p/xamarin_mvvmcross_plugin_simpleaudioplayer.html
Copyright © 2020-2023  润新知