【Win 10 应用开发】在后台进行多媒体转码
前面,老周给大伙儿讲了如何运用 MediaTranscoder 类来完成多媒体。然而,你懂的,要是多媒体文件比较大,转码时间会更长,有可能用户不会一眭停在当前应用界面上,或许会切换到其他应用程序,甚至关闭应用程序。
所以,在特定情形下,是得考虑在后台任务上进行转码,这样哪怕前台应用退出了,也不会使转码中止,处理工作可以在后台任务中继续耍。
不过,多媒体转码这玩意儿毕竟很耗性能,也很耗电,强烈建议大家不要搞什么批量转码,不然,我估计平板/手机的电池续航能力支撑不了。
示例我已经做好了,因而完整的代码大伙儿可以下载来慢慢参考,bug比较多,因为老周的编程水平实在太差,比不上支付婊的高手们,故bug如林。
下面老周就重点扯一下实现过程。其实也没什么,也是用到万能的 MediaTranscoder 类,只不是过放在后台任务中进行了而已。
1、实现后台任务。咱们先做后台吧,在新建项目时,一定要选“Windows 运行时组件”,以前有朋友错选了类库项目,结果一运行就呵呵了。所以,选错老婆是很痛苦的,记好了,是 windows 运行时组件。
不要把后台类型声明在主启动项目中,不然激活后台时会把你的前台程序给挂掉,因为运行后台是要新的实例的,而RT应用为了防止人品不端正的开发者故意破坏人民群众的公共设施,RT一般同时只能有一个实例在运行,所以,后台类一定要扔到一个独立的 Windows 运行时组件中,并在主启动项目中引用之。
实现后台,我相信大家都会了,老周以前也写过相关的东东,而且老周的破书里面也有讲到。总的一句就是:实现IBackgroundTask接口,就完事了。
好了,上代码。
public sealed class MPBackTask : IBackgroundTask { public async void Run(IBackgroundTaskInstance taskInstance) { 。。。。。。。。。。。。。。。。。。 } private async Task TranscodeAsync(IBackgroundTaskInstance taskInst, MediaProcessingTriggerDetails d) { 。。。。。。。。。。。。。。。。。。。。。。。。。 } }
Run方法是必须实现的,因为接口中包含了该方法,而TranscodeAsync方法是我自定义的,负责完成转码工作。
先看Run方法,
public async void Run(IBackgroundTaskInstance taskInstance) { var deferral = taskInstance.GetDeferral(); var details = taskInstance.TriggerDetails as MediaProcessingTriggerDetails; try { // 向应用设置写入一个标记,表示后台正在处理转码 // 数据内容随意 ApplicationData.Current.LocalSettings.Values["running"] = "y"; await TranscodeAsync(taskInstance, details); ShowNotification("后台转码成功。"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"异常:{ex.Message}"); } finally { // 删除设置中的标记 var settingcont = ApplicationData.Current.LocalSettings; if (settingcont.Values.ContainsKey("running")) { settingcont.Values.Remove("running"); } deferral.Complete(); } }
从taskInstance.TriggerDetails属性中你可以得到一个MediaProcessingTriggerDetails实例,有啥用呢,它可以接收从前台应用程序发送来的参数。比如输入文件,输出文件等,因为选择文件的操作肯定在前台完成的,选择文件后,在触发后台任务时传递进来。
为了让前台应用能够确认这个后台任务是否正在执行转码,我这里巧用一下应用程序设置容器,这些数据是保存在注册表中的,当你的应用被卸载时,系统会自动清理注册表,所以RT应用是很干净的,别担心。
我的做法是,在执行转码时,向设置项中加一个叫running的项,value随便,因为我不是判断值的,而是看key的,在执行转码前向设置容器加上这个key,当转码完成后再把这个key干掉。这样一来,在前台应用程序中,如果发现有这个key就说明转码还在后台干活,如果没有这个key,表明转码完成了。
好,现在来看看 TranscodeAsync 方法,我让它返回Task,表示它可以异步等待,因为要接收从前台发来的参数,所以,用一个d参数来引用MediaProcessingTriggerDetails实例。那为什么还要一个参数来引用IBackgroundTaskInstance呢,这是为了报告处理进度,如果前台程序觉得有必要监视进度时,可以向前台应用报告进度。
代码如下。
private async Task TranscodeAsync(IBackgroundTaskInstance taskInst, MediaProcessingTriggerDetails d) { // 取出传进来的参数 string inputpath = d.Arguments["input_path"] as string; string outputpath = d.Arguments["output_path"] as string; if (inputpath == null || outputpath == null) { return; } StorageFile inputfile = await StorageFile.GetFileFromPathAsync(inputpath); StorageFile outputfile = await StorageFile.GetFileFromPathAsync(outputpath); MediaTranscoder transcoder = new MediaTranscoder(); MediaEncodingProfile profile = MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High); // 准备转码 PrepareTranscodeResult result = await transcoder.PrepareFileTranscodeAsync(inputfile, outputfile, profile); if (result.CanTranscode) { Progress<double> progress = new Progress<double>(p => { taskInst.Progress = Convert.ToUInt32(p); }); // 转码 await result.TranscodeAsync().AsTask(progress); } }
转码的过程我就不多说了,一样的,这里我把wav格式转为mp3格式,注意你输入的wav文件不能是DTS,因为有些DTS音频文件也是把后缀弄成wav,其实与wav不同,所以要用正常的wav文件,如果是DTS,转码后的mp3文件,你只能听到“沙沙沙”的声音。
关键还是介绍一下如何获取参数,MediaProcessingTriggerDetails有一个Arguments属性,其实是一个字典数据结构的东东,key是字符串,value是object类型,因为我在前台已经把输入和输出文件作为参数传过来,所以这里可以直接从参数中得到输入和输出的文件。
string inputpath = d.Arguments["input_path"] as string; string outputpath = d.Arguments["output_path"] as string;
然后用GetFileFromPathAsync静态方法从路径生成StorageFile实例,就可以读写了。
顺便说说,官方的SDK示例中,它是用应用设置容器来传递的,就是用户选好文件后,把文件路径存到设置项中,然后在后台中读取。而我这里用的是参数传递的方法,达到的效果一样。
后台完成后,在主启动项目中引用后台项目,记得了,别忘了引用。然后打开清单文件,在[声明]选项卡中添加一个后台任务,类型勾选“媒体处理”,别勾错了,常规和系统事件都会发生异常。XML代码如下:
<Extension Category="windows.backgroundTasks" EntryPoint="BTM.MPBackTask"> <BackgroundTasks> <uap:Task Type="mediaProcessing" /> </BackgroundTasks> </Extension>
最后,就是在前台代码中注册后台了。因为这个后台任务只是为了完成转码工作的,并不需要长期存在,我的想法是,最好在需要转码时注册,不用时直接取消注册。另外一个原因就是,用来触发这个后台的类只能使用一次。当开始新的转码任务时,也要重新实例化一个的。
后台转码用的是 MediaProcessingTrigger 触发器类,而且,触发后台时也是要用这个类的RequestAsync方法,只要这个方法被调用,后台任务就会触发执行。可是,你懂的,Trigger是在注册后台任务时,通过BackgroundTaskBuilder类的SetTrigger方法来设置的。正因为如此,当你每次实例化一个MediaProcessingTrigger时,都必须注册一次后台任务,否则无法与后台任务关联。但是,已经注册的后台任务是不能重复注册的,所以唯一的做法就是每用一次注册一次。
private MediaProcessingTrigger SetBackgroundTaskForTranscode() { BackgroundTaskBuilder tb = new BackgroundTaskBuilder(); tb.Name = "btc"; tb.TaskEntryPoint = $"{nameof(BTM)}.{nameof(BTM.MPBackTask)}"; MediaProcessingTrigger trigger = new MediaProcessingTrigger(); tb.SetTrigger(trigger); foreach (var bt in BackgroundTaskRegistration.AllTasks) { if (bt.Value.Name == "btc") { bt.Value.Unregister(true); break; } } var r = tb.Register(); r.Progress += onProgress; r.Completed += onCompleted; return trigger; }
以上方法就是用于注册后台任务的,注册后,并把关联的 MediaProcessingTrigger实例返回,以供其他代码来触发。
下面代码将触发后台任务,执行转码。
if (ApplicationData.Current.LocalSettings.Values.ContainsKey("running")) { tbmsg.Text = "有转码任务正在运行,请稍等。"; return; } btnStart.IsEnabled = false; MediaProcessingTrigger mdproctrigger = SetBackgroundTaskForTranscode(); MediaProcessingTriggerResult res = await mdproctrigger.RequestAsync(argstobtproc); switch (res) { case MediaProcessingTriggerResult.Allowed: tbmsg.Text = "已启动后台转码。"; break; case MediaProcessingTriggerResult.DisabledByPolicy: tbmsg.Text = "你的山寨机禁止后台运行。"; break; default: tbmsg.Text = "对不起,发生灵异事件。"; break; }
还记得吧,前面在实现后台任务时,我向设置容器中写了个running的项, 这里可以检查一下,如果running存在,说明后台转码还在进行,就不要重开启操作。
RequestAsync方法调用时,可以把要传给后台的参数放进去,即一个ValueSet实例,刚才说了,其实它是一个字典。
最后,还有一个很关键的,就是访问文件的权限。RT应用不像传统桌面应用那样你可以毫不顾忌地访问各种文件。RT库对文件的访问权限有严格的控制。用户通过picker选择了一个文件,但是,这个StorageFile无法直接传给后台,因此只能把这个文件的path传到后台,再在后台中用GetFileFromPathAsync方法再获取StorageFile实例。然而,问题来了,如何让后台任务代码具备访问权限呢?
没事,SDK既然想到严格的安全控制,当然就会有解决方案。在Windows.Storage.AccessCache命名空间下,有专门的类,可以用来授权。
用户通过操作选择了文件后,会得到一个StorageFile实例,我们只需要把这个StorageFile实例 Add 到StorageApplicationPermissions类的任何一个属性的集合中就可以了。
其实,StorageApplicationPermissions类就两个属性——FutureAccessList 和 MostRecentlyUsedList,这两个属性用法是一样的,只是意思不同罢了。MostRecentlyUsedList 是过去式,表示文件访问历史;FutureAccessList 是未来式,表示即将要用到的列表。
所以,无论你把文件添加到哪个属性中,应用程序就具备该文件的访问权了。
有人会说,这不是多此一举吗,还要用这个来授权。非也,表面上看好像是多余的,但你想想,文件是如何得到的?就是通过Picker让用户自愿选择的,是吧,这就对了,如果用户不想让你访问那个文件,他就不选;只有当用户自己选了,你的代码才能访问到那个文件,才能把这个文件加入到FutureAccessList列表中。所以嘛,这样的处理不流氓,很有人品,很有德行。
下面代码把输入文件加入授权列表,输出文件的原理一样。
FileOpenPicker picker = new FileOpenPicker(); picker.FileTypeFilter.Add(".wav"); StorageFile wavefile = await picker.PickSingleFileAsync(); if (wavefile != null) { // 显示路径 tbInputfile.Text = wavefile.Path; // 把该文件放入文件访问列表中,以便后台使用 // 授权,否则后台访问不了 StorageApplicationPermissions.FutureAccessList.Add(wavefile); // 写入参数列表 argstobtproc["input_path"] = wavefile.Path; }
好了,大事做完了,看看效果吧。
===============================