老司机学新平台 - 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事件,代表一次播放结束。
老司机学新平台 - Xamarin开发之我的第一个MvvmCross跨平台插件:SimpleAudioPlayer的更多相关文章
- 老司机学新平台 - Xamarin开发环境及开发框架初探
随着被微软收购,最近一年间,Xamarin的火爆程度与日俱增.免费.更好的VS2015集成.更好的模拟器,甚至,在windows上运行和调试iOS平台程序,让我这样接触了十几年.NET平台的老司机,即 ...
- 老司机学新平台 - Xamarin Forms开发框架二探 (Prism vs MvvmCross)
在上一篇Xamarin开发环境及开发框架初探中,曾简单提到MvvmCross这个Xamarin下的开发框架.最近又评估了一些别的,发现老牌Mvvm框架Prism现在也支持Xamarin Forms了, ...
- 老司机学新平台 - Xamarin Forms开发框架之MvvmCross插件精选
在前两篇老司机学Xamarin系列中,简单介绍了Xamarin开发环境的搭建以及Prism和MvvmCross这两个开发框架.不同的框架,往往不仅仅使用不同的架构风格,同时社区活跃度不同,各种功能模块 ...
- 老司机学Xamarin系列总目录
Xamarin开发环境及开发框架初探 Xamarin Forms开发框架二探 (Prism vs MvvmCross) Xamarin Forms开发框架之MvvmCross插件精选 Xamarin开 ...
- C语言老司机学Python (五)
今天看的是标准库概览. 操作系统接口: 用os模块实现. 针对文件和目录管理,还有个shutil模块可以用. 例句: import os os.getcwd() # 返回当前的工作目录 os.chdi ...
- C语言老司机学Python (一)
Python 版本:3.6.4 参考网上教程:http://www.runoob.com/python3/python3-basic-syntax.html 开始了啊. 干咱们这行的老规矩,学新语言的 ...
- C语言老司机学Python (六)- 多线程
前面的1-5都是比较基础的东西,能做的事情也有限. 从本节起,随着更多进阶技术的掌握,渐渐就可以用Python开始浪了. Python3使用threading模块来实现线程操作. 根据在其他语言处学来 ...
- C语言老司机学Python (三)
条件语句: 注意1) condition后面的冒号 2) elif if condition_1: statement_block_1elif condition_2: statement_block ...
- C语言老司机学Python (二)
标准数据类型: 共6种:Number(数字),String(字符串),List(列表),Tuple(元组),Sets(集合),Dictionary(字典) 本次学习主要是和数据类型混个脸熟,知道每样东 ...
随机推荐
- SqlLite 基本操作
1.数据类型 ● SQLite将数据划分为以下⼏几种存储类型: ● integer : 整型值 ● real : 浮点值 ● text : ⽂文本字符串 ● blob : ⼆二进制数据(⽐比 ...
- Android获取ImageView上的图片,和一个有可能遇到的问题!
1.在获取图片前先调用setDrawingCacheEnabled(true)这个方法: 举例:mImageView.setDrawingCacheEnabled(true); 2.之后可以通过get ...
- 最大流-最小割 MAXFLOW-MINCUT ISAP
简单的叙述就不必了. 对于一个图,我们要找最大流,对于基于增广路径的算法,首先必须要建立反向边. 反向边的正确性: 我努力查找了许多资料,都没有找到理论上关于反向边正确性的证明. 但事实上,我们不难理 ...
- 【BZOJ1700】[Usaco2007 Jan]Problem Solving 解题 动态规划
[BZOJ1700][Usaco2007 Jan]Problem Solving 解题 Description 过去的日子里,农夫John的牛没有任何题目. 可是现在他们有题目,有很多的题目. 精确地 ...
- clientX .offsetX .screenX x 的区别
clientX 设置或获取鼠标指针位置相对于当前窗口的 x 坐标,其中客户区域不包括窗口自身的控件和滚动条. clientY 设置或获取鼠标指针位置相对于当前窗口的 y 坐标,其中客户区域不包括窗口自 ...
- PHP用户注册与登录【1】
需求分析 主要功能分为 用户注册.用户登录.用户退出.用户中心 四个部分. 用户注册 用户注册主要功能有: 注册信息表单填写界面 javascript 脚本初步检测用户输入的注册信息. 注册处理模块检 ...
- Shader实例:NGUI图集中的UISprite正确使用Shader的方法
效果: 变灰,过滤,流光 都是UI上常用效果. 比如: 1.按钮禁用时,变灰. 2.一张Icon要应付圆形背景框,又要应付矩形背景框.就要使用过滤的方式来裁剪. 避免了美术提供两张icon的麻烦,又节 ...
- 回流(reflow)与重绘(repaint)
最近项目排期不紧,于是看了一下之前看了好久也没看明白的chrome调试工具的timeline.但是很遗憾,虽然大概懂了每一项是做什么的,但是用起来并不能得心应手.所以今天的重点不是timeline,而 ...
- JavaScript-Function基础知识
function 1. 定义:一段预先设置的代码块,可以反复调用,根据输入参数的不同,返回不同的值: 2. 函数的声明方法: (1)function 命令声明函数 functio ...
- javascript基础知识
1.javascript 表单验证,减轻服务器压力 制作网页特效 动态改变页面内容 基于对象和事件驱动的,具有安全性能的脚本语言 交互,脚本语言.解释性语言,边执行边解释 2.script标签 添加位 ...