×
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 07:41.
105 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×