×
Namespaces

Variants
Actions

Windows Phone上的声音录制与编码(二):将wav上传至服务器

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon Multimedia.png
SignpostIcon WP7 70px.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 23 Feb 2012 创建
最后由 hamishwillee 在 16 Jul 2013 编辑

Contents

简介

之前的一篇wiki介绍如何在Windows Phone上录制声音,并且编码成wav格式,存储到isolated storage中。但是有很多场景都需要将用户录制的声音发送到其它地方。例如,在语音聊天中,用户需要听到其它手机上录制的声音;在一个说故事或者唱歌的程序中,用户也很可能希望将自己的故事和歌曲分享给其他人。这时候就需要将文件传送到服务器上了。这也是本文所要描述的内容。

你可以自这里下载本文的示例代码 File:SimpleAudio2.zip

创建一个文件上传的REST service

为了将文件上传至服务器,我们可以创建一个REST service。

From WCF Web API to ASP.NET Web API

曾几何时,在“使用OAuth保护供Windows Phone使用的REST service”这篇wiki中我们介绍了如何使用WCF Web API创建一个RESTful service。在那篇wiki发布后的这一个月中,WCF Web API出现了一个新版本,并且改名为ASP.NET Web API,正式进入了beta阶段。

REST service

REST service的核心思想还是一样的,重点依然如下:

  • REST service是面向资源的,不是面向操作的,这和SOAP是不同的
  • REST service使用HTTP拓扑,因此你需要定义客户端如何发送HTTP请求来访问资源,这包括URI和HTTP method

大多数WCF Web API的服务都可以轻松升级到ASP.NET Web API。事实上,只要你掌握了REST service的机理,就算你不使用微软的平台,也能轻松编写REST service,因为只是写法不同而已,思想方法是完全一致的。现在就让我们看看如何使用ASP.NET Web API。

使用ASP.NET Web API

为了使用ASP.NET Web API,首先必须自这里下载并安装ASP.NET MVC 4 Beta,今后该组件可能被直接集成到.NET 4.5中,但目前必须单独下载。

接下来,若是你按照我们在“使用OAuth保护供Windows Phone使用的REST service”这篇wiki中介绍的方法使用WCF Web API编写了REST service,请先将相关的assembly reference移除。或者你也可以从一个空的ASP.NET项目开始。此外,ASP.NET MVC也提供了Web API相关的现成模板,不过那个模板会帮助你建一堆和本文需求无关的内容,例如MVC的view以及很多JavaScript引用,因此本文并不会使用这个模板。无论如何,不妨看看这个模板的截图:

WebAPIProjectTemplate.png

若是你没有使用Web API的现成模板,就需要手工添加引用至ASP.NET Web API的assembly:System.Web.Http.dll,System.Web.Http.Common,System.Web.Http.WebHost,以及System.Net.Http.dll。注意这里的System.Net.Http.dll不同于WCF Web API中的System.Net.Http.dll,你必须重新添加引用。还有,不要忘记将这些引用的Copy to local设置成true,否则当你部署到其它服务器时可能会有问题。

接下来你需要修改一些namespace的引用,你会发现一些Microsoft.Web开头的namespace不存在了,因为它们的名字改掉了。不过,Visual Studio会自动帮你检测正确的namespace,所以没必要刻意去记到底需要哪些namespace。

下一步是修改Global.asax。之前你可能拥有这样的代码,其中ServiceRoute的作用是使用ASP.NET URL Routing确定service的base URL,从而去除WCF中的.svc扩展名:

            routes.Add(new ServiceRoute(
"files",
new HttpServiceHostFactory(),
typeof(FileUploadService)));

在ASP.NET Web API中,URL Routing不仅仅用于指定base URL,也用于指定完整的URL。也就是说以前需要URL Routing和UriTemplate结合起来的事,现在只需要URL Routing就可以实现了。

        public static void RegisterRoutes(RouteCollection routes)
{
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{filename}",
defaults: new { filename = RouteParameter.Optional }
);
}

以上代码指定你的请求发送到http://[server name]/files/myfile.wav,然后Web API会寻找一个名为FilesController的类(对应于{controller},大小写不敏感),并调用里面对应的方法,再将myfile.wav作为finename这个参数传入(对应于{filename})。这个参数并不是必须的。

所以你必须有一个类名为FilesController,而不能用诸如FileUploadService这样的方法。这可能会让真正的service开发人员感觉不爽,似乎必须引入MVC,但是目前就是这样设计的。注意你在其它地方可能会看到所有的controller都放在一个Controllers文件夹下,但是这不是强制的,你可以放在根目录下,或者放在services文件夹下,或者任意其它地方。再有要注意这个类必须继承自ApiController,而不像以前在WCF时代什么都不用继承。

除了URI,对于一个 REST service而言,HTTP method也是很重要的。在ASP.NET Web API中,你不用WebGet/WebInvoke这样的属性,而使用HttpGet和AcceptVerbs这样的属性,当然这也是换汤不换药的。如果你的方法名字以Get/Post/Put/Delete开头,那么这些属性甚至可以省略。例如:

       public HttpResponseMessage Post([FromUri]string filename)

这里我们并没有为这个方法加任何属性,Web API在看到POST请求时就会自动调用它了。

还请注意filename这个参数前有一个FromUri属性,这是因为ASP.NET Web API和MVC可以协同工作。默认情况下,最后一个参数会被认为来自request body,并且尝试被反序列化成一个model类。在很多场合下这很有用,但是我们现在的场景并没有model,默认的行为会出错的。所以我们需要显示设置FromUri属性,告诉MVC这个参数来自URI,请不要尝试将它反序列化成一个model。这对GET而言并不需要,因为GET并不存在request body,但是加上这个属性也没关系。

有关URL Routing的更多信息,可以参考这里

文件上传的实现

现在我们可以实现文件上传功能了。实现的逻辑可以是多种多样的,为了简单起见,本文只是将上传的文件保存在网站当前目录下。请注意在很多情况下这并不是一个可行的方案,例如在Windows Azure中,默认情况下你对网站当前目录没有权限,必须将文件存放到local storage中,而且考虑到多个instance可能需要共享文件,推荐存放到blob storage中。不过如何处理Windows Azure不在本文的讨论范围之内。

以下是文件上传的实现:

        public HttpResponseMessage Post([FromUri]string filename)
{
var task = this.Request.Content.ReadAsStreamAsync();
task.Wait();
Stream requestStream = task.Result;
 
try
{
Stream fileStream = File.Create(HttpContext.Current.Server.MapPath("~/" + filename));
requestStream.CopyTo(fileStream);
fileStream.Close();
requestStream.Close();
}
catch (IOException)
{
throw new HttpResponseException("A generic error occured. Please try again later.", HttpStatusCode.InternalServerError);
}
 
HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = HttpStatusCode.Created;
return response;
}

注意到这一次我们并没有使用HttpRequestMessage做为方法的一个参数,而是通过this.Request获取request相关的信息,这也是新版本的Web API的一个不同。其它地方则和以前版本的Web API差不多,例如,想要读取request body,还是必须使用异步方式(.NET的Task Parallel Library让编写异步代码变得很简单,到了.NET 4.5,代码还可以进一步简化。)再有,在创建response时,还是要注意status code,通常创建了一个资源成功后会返回201 Created。在错误发生时,你依然可以throw一个HttpResponseException,Web API会自动创建对应的status code和response body。

文件下载的实现和Range header

虽说文件下载对于我们的场景并不是必须的,但是不妨也实现一下看看。在这个过程中我们也会看看如何处理Range header。

Range header是HTTP标准的一部分,通常只针对GET有效。如果一个GET request中有Range header,意味着客户端并不需要下载完整的资源,而只需要其中的一部分。这对于断点续传是很有用的。你可以在这篇文章上搜索14.35,里面详细介绍了这个header。

在Windows Phone中,background file transfer可能会用到Range header,从而实现下载时的断点续传。所以今天我们要看看这个header。

Range header的格式通常是:

bytes=10-20,30-40

这意味着客户端希望服务器返回资源的第10到20个字节,以及第30到40个字节,而不是整个资源。

Range header也可能以这样的形式出现:

bytes=-100 bytes=300-

前一种情况意味着客户端希望获得资源的0到100个字节,而后一种情况意味着客户端希望跳过前300个字节,获得该资源剩下的全部字节。

在第一个例子中我们看到Range header可能是分段的,客户端可以要求10-20以及30-40这两段数据。但是在实际操作中,通常很少这样做。尤其在文件下载这个场景下,很少会需要获取多段离散的数据。因此很多服务都只支持一段数据。我们今天的例子也只会支持一段数据,若是你的场景需要支持多段数据,你可以在这个例子的基础上进一步修改。

以下代码实现了文件下载的功能,它支持Range header中的第一段range,而无视剩下的range。

        public HttpResponseMessage Get([FromUri]string filename)
{
string path = HttpContext.Current.Server.MapPath("~/" + filename);
if (!File.Exists(path))
{
throw new HttpResponseException("The file does not exist.", HttpStatusCode.NotFound);
}
 
try
{
MemoryStream responseStream = new MemoryStream();
Stream fileStream = File.Open(path, FileMode.Open);
bool fullContent = true;
if (this.Request.Headers.Range != null)
{
fullContent = false;
 
// Currently we only support a single range.
RangeItemHeaderValue range = this.Request.Headers.Range.Ranges.First();
 
 
// From specified, so seek to the requested position.
if (range.From != null)
{
fileStream.Seek(range.From.Value, SeekOrigin.Begin);
 
// In this case, actually the complete file will be returned.
if (range.From == 0 && (range.To == null || range.To >= fileStream.Length))
{
fileStream.CopyTo(responseStream);
fullContent = true;
}
}
if (range.To != null)
{
// 10-20, return the range.
if (range.From != null)
{
long? rangeLength = range.To - range.From;
int length = (int)Math.Min(rangeLength.Value, fileStream.Length - range.From.Value);
byte[] buffer = new byte[length];
fileStream.Read(buffer, 0, length);
responseStream.Write(buffer, 0, length);
}
// -20, return the bytes from beginning to the specified value.
else
{
int length = (int)Math.Min(range.To.Value, fileStream.Length);
byte[] buffer = new byte[length];
fileStream.Read(buffer, 0, length);
responseStream.Write(buffer, 0, length);
}
}
// No Range.To
else
{
// 10-, return from the specified value to the end of file.
if (range.From != null)
{
if (range.From < fileStream.Length)
{
int length = (int)(fileStream.Length - range.From.Value);
byte[] buffer = new byte[length];
fileStream.Read(buffer, 0, length);
responseStream.Write(buffer, 0, length);
}
}
}
}
// No Range header. Return the complete file.
else
{
fileStream.CopyTo(responseStream);
}
fileStream.Close();
responseStream.Position = 0;
 
HttpResponseMessage response = new HttpResponseMessage();
response.StatusCode = fullContent ? HttpStatusCode.OK : HttpStatusCode.PartialContent;
response.Content = new StreamContent(responseStream);
return response;
}
catch (IOException)
{
throw new HttpResponseException("A generic error occured. Please try again later.", HttpStatusCode.InternalServerError);
}
}

注意到在使用Web API时,并不需要手工解析Range header的字符串。Web API会自动帮我们进行解析,并且针对每一段range,给了我们From和To这两个属性,它们的类型是Nullable<long>,毕竟像刚才举的第二第三个例子那样(-100以及300-),有可能一个range并没有from或者to。对于这些场景的处理一定要多加小心。

当然还有一个必须小心处理的是to的值超过资源大小的情况。这时候我们可以认为客户端就是要访问到资源结尾。

通常如果一整个资源都被返回了,我们会将status code设置成200 OK,而若是只返回了一部分数据,就设置成206 PartialContent。

测试服务

为了测试一个REST service,就像其它服务一样,我们需要一个客户端。不过通常我们不需要自己写什么客户端。对于GET请求,往往浏览器就足够了,对于其它请求,Fiddler是一个很好的工具。Fiddler也可以帮助我们测试一些高级的GET请求,例如使用了Range header的那种。

本文不会再详细介绍如何使用Fiddler,毕竟在他们的网站上已经有了很多的介绍。下面是一些截图给你一个印象如何使用Fiddler测试文件下载,请注意其中的Range header:

Fiddler1.png

Fiddler2.png

注意到虽然请求的范围是1424040-1500000,但事实上我们的文件本身只有1424044这么大,所以最终结果只返回了文件的最后4个字节。

你需要多测试几次,尝试各种情况,以确保程序逻辑无误。此外,也可以考虑自行写一个客户端程序做unit test,那样的好处是你可以事先写好很多测试案例,在程序逻辑发生修改时,测试案例并不需要修改,可以再次自动运行全部案例。而如果用Fiddler,你将必须手工重新测试所有案例。本文就不再多讨论unit test了。

Background File Transfer

至此,我们的服务就完成了。接下来我们要在Windows Phone上将编码后的wav文件上传至服务。为此我们可以使用普通的HttpWebRequest,或者background file transfer

什么时候使用background file transfer

通常文件较小时我们会选择直接使用HttpWebRequest,因为使用background file transfer毕竟还是稍微要多用一些资源的。但是如果文件比较大,通常就推荐使用background file transfer(但是考虑到手机上网通常需要额外的费用,也不允许传太大的文件),因为它可以让你的程序在非运行状态下时继续在后台上传/下载文件,而且还实现了很多诸如下载断点续传(前提是服务要支持Range header),发生错误时重试(前提是服务要返回标准的代表错误的status code)等功能。

在我们的场景下,我们决定如果文件小于1MB,就直接上传,否则使用background file transfer。具体怎样判断要视你的场景而定。

            if (wavStream.Length < 1048576)
{
this.UploadDirectly(wavStream);
}
else
{
this.UploadInBackground(wavStream);
}

使用background file transfer

具体background file transfer的用法在文档上有很详细的说明,所以我们就不再详细介绍了。以下只是给大家看代码,并且指出几个注意点。

首先,一个上传的请求最大只能有5M(5242880),这就是为什么我们之前在录制声音时限制大小为5242000的一个原因(但是下载最大可达100M)。

                if (this._stream.Length + offset > 5242000)
{
this._microphone.Stop();
MessageBox.Show("The recording has been stopped as it is too long.");
return;
}

再有,为了使用background file transfer,文件必须放在isolated storage中的/shared/transfers目录下。

此外,一个程序同一时间最多只能要求在后台上传/下载5个文件,若是已经到达了极限,你可以考虑通知用户现在不能上传/下载,或者自己写一个queue,等有空了再上传/下载。

下面的代码在已经有5个请求的情况下简单提示用户目前不能下载。若是可以下载,就将文件存储到isolated storage中规定的目录下,并且创建一个BackgroundTransferRequest上传文件。注意为了简单,我们的例子永远将文件名定位test.wav,你真正的程序中可能会希望让用户输入文件名。

        private void UploadInBackground(Stream wavStream)
{
// Check if there're already 5 requests.
if (BackgroundTransferService.Requests.Count() >= 5)
{
MessageBox.Show("Please wait until other records have been uploaded.");
return;
}
 
// Store the file in isolated storage.
var iso = IsolatedStorageFile.GetUserStoreForApplication();
if (!iso.DirectoryExists("/shared/transfers"))
{
iso.CreateDirectory("/shared/transfers");
}
using (var fileStream = iso.CreateFile("/shared/transfers/test.wav"))
{
wavStream.CopyTo(fileStream);
}
 
// Transfer the file.
try
{
BackgroundTransferRequest request = new BackgroundTransferRequest(new Uri("http://localhost:4349/files/test.wav"));
request.Method = "POST";
request.UploadLocation = new Uri("shared/transfers/test.wav", UriKind.Relative);
request.TransferPreferences = TransferPreferences.AllowCellularAndBattery;
request.TransferStatusChanged += new EventHandler<BackgroundTransferEventArgs>(Request_TransferStatusChanged);
BackgroundTransferService.Add(request);
}
catch
{
MessageBox.Show("Unable to upload the file at the moment. Please try again later.");
}
}

你可能注意到了我们将TransferPreferences属性设置成了AllowCellularAndBattery,这样一来就算没有wifi只能用手机网络,或者电池低下,还是可以上传文件。在实际使用时,请考虑你上传文件这件事是不是会造成太多的网络费用,以及会不会太消耗电池,再决定是否这样设置。在这边我们认为上传不到5MB的数据并不会造成太多的网络费用,也不会太影响电池,不过实际情况下还是要多做测试才能决定。

当一个文件上传/下载完毕后,对应的background file transfer request并不会被自动移除,你必须手工移除。考虑到可能在很多地方都会要做这件事,你可以在一个全局的地方(例如App类)写一个静态方法:

        internal static void OnBackgroundTransferStatusChanged(BackgroundTransferRequest request)
{
if (request.TransferStatus == TransferStatus.Completed)
{
BackgroundTransferService.Remove(request);
if (request.StatusCode == 201)
{
MessageBox.Show("Upload completed.");
}
else
{
MessageBox.Show("An error occured during uploading. Please try again later.");
}
}
}

这里在TransferStatus变为Completed字后,我们将request移除,并且检查服务返回的值是不是预期的201,以判断上传是否成功。

之后就是处理TransferStatusChanged事件了。我们不仅仅在创建一个request时需要处理这个事件,在程序启动或者进入墓碑状态并回来时也需要,这是因为就算你的程序没有在运行,background file transfer还是可能在继续执行(但是若是程序只是被扔到了后台再回到前台,而没有进入墓碑状态,就不需要了,因为内存中的数据会全部保留):

        // Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
this.HandleBackgroundTransfer();
}
 
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
if (!e.IsApplicationInstancePreserved)
{
this.HandleBackgroundTransfer();
}
}
 
private void HandleBackgroundTransfer()
{
foreach (var request in BackgroundTransferService.Requests)
{
if (request.TransferStatus == TransferStatus.Completed)
{
BackgroundTransferService.Remove(request);
}
else
{
request.TransferStatusChanged += new EventHandler<BackgroundTransferEventArgs>(Request_TransferStatusChanged);
}
}
}
 
private void Request_TransferStatusChanged(object sender, BackgroundTransferEventArgs e)
{
App.OnBackgroundTransferStatusChanged((BackgroundTransferRequest)sender);
}

最后注意,在一个完整的程序中,你应该提供界面让用户查看/取消所有正在运行的background file transfer。不过本文就不再专门描述这一块了。

直接上传

为了完整,这里也给出直接使用HttpWebRequest将文件上传至服务的情况。

        private void UploadDirectly(Stream wavStream)
{
string serviceUri = "http://localhost:4349/files/test.wav";
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(serviceUri);
request.Method = "POST";
request.BeginGetRequestStream(result =>
{
Stream requestStream = request.EndGetRequestStream(result);
wavStream.CopyTo(requestStream);
requestStream.Close();
request.BeginGetResponse(result2 =>
{
try
{
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result2);
if (response.StatusCode == HttpStatusCode.Created)
{
this.Dispatcher.BeginInvoke(() =>
{
MessageBox.Show("Upload completed.");
});
}
else
{
this.Dispatcher.BeginInvoke(() =>
{
MessageBox.Show("An error occured during uploading. Please try again later.");
});
}
}
catch
{
this.Dispatcher.BeginInvoke(() =>
{
MessageBox.Show("An error occured during uploading. Please try again later.");
});
}
wavStream.Close();
}, null);
}, null);
}

这里唯一要注意的是HttpWebRequest的回调方法在另外一个线程上执行,要通知UI,必须使用Dispatcher.BeginInvoke。

最后注意,如果你在真实手机设备上测试文件上传,不能使用localhost。请确保你的手机能通过wifi连接到电脑网络,然后使用电脑的名字。或者你也可以将服务部署到internet上(例如Windows Azure之上)。

总结

本文介绍了如何使用ASP.NET Web API构建一个REST service用于上传/下载文件,以及如何在Windows Phone中使用background file transfer上传/下载文件。关于service部分,如果你不使用ASP.NET Web API,也可以用类似的逻辑。请尤其注意Range header的使用。

今后的一篇wiki会介绍在服务器上如何进一步将wav文件编码成其它更常见的格式,例如mp4。

This page was last modified on 16 July 2013, at 10:41.
111 page views in the last 30 days.
×