我的新项目,インターネットラジオステーション<音泉> 的第三方客户端,开发代号 Onsen Tamako,已经到了收尾的时候了。
音泉有个特点就是屏蔽非日本 IP,日本国外用户虽然能登录官网并且查看各自信息,但是想要收听或者收看广播那是不行的,服务器会给你 403。
但是在一个月前开始这项目时进行的测试发现,音泉会被国产视频网站几百年前就玩烂了的 X-Forwarded-For
给迷惑。
X-Forwarded-For HTTP Header
简单的说,X-Forwarded-For
HTTP header 是告诉服务器,我是个透明代理,我是帮这个 IP 转发信息的;或者用在负载均衡器内部,用于标识外部 IP。这本来是个非标准 HTTP Header,但是这么久过去了也就变公认标准了。总之音泉不知道是 Amazon AWS 就用了这个,还是自己的 Apache 配置不到位,只要发请求时带上这个 header,给一个虚假的日本 IP,就可以顺利得到媒体文件了。
Onsen Tamako
回到 Onsen Tamako 的话题上来。这个 UWP 需要使用自定义 HTTP 请求,加上 X-Forwarded-For
header 来绕过 IP 限制,然而不管是 XAML 的 MediaElement
(MSDN) 还是用于后台播放的 MediaPlayer
(MSDN),本身都是不支持自定义 HTTP 请求的。
于是经历了以下几个阶段
最初的实现是依靠下载,依然不能在线看,那我就下载下来,但是这个方法太丑了,让它作为历史过去吧。
和 Soymilk 一样,依靠 FFMpeg 进行解码,制作自定义
MediaStreamSource
(MSDN) 进行播放,但是这需要带上 7MB+ 的 FFMpeg 库,所以它是历史。自己实现 MP3 和 MP4 分离。这个其实还是
MediaStreamSource
,MP3 的部分我参考 NAudio 的代码,很快就完成了,但是 MP4 格式比 FLV 还复杂的多,没敢尝试,暂时还是 FFMpeg 顶上。大吃一惊,原来
MediaElement
和BackgroundMediaPlayer
本身就支持播放一个 Stream!
播放自定义 Stream
废话这么多,最终需要实现的就只有一个 IRandomAccessStream
(MSDN),直接贴代码。
using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.Web.Http;
namespace OnsenTamako
{
public sealed class HttpStreamingStream : IRandomAccessStreamWithContentType
{
public bool CanRead => true;
public bool CanWrite => false;
public ulong Position { get; private set; }
private readonly ulong _size;
public ulong Size
{
get { return _size; }
set { throw new NotSupportedException(); }
}
public string ContentType { get; }
readonly HttpClient _client;
readonly Uri _uri;
private HttpStreamingStream(HttpClient client, Uri uri, ulong size, string contentType)
{
_client = client;
_uri = uri;
_size = size;
ContentType = contentType;
}
public IRandomAccessStream CloneStream() { throw new NotImplementedException(); }
public void Dispose() { }
public IAsyncOperation<bool> FlushAsync() { throw new NotSupportedException(); }
public IInputStream GetInputStreamAt(ulong position) { throw new NotImplementedException(); }
public IOutputStream GetOutputStreamAt(ulong position) { throw new NotSupportedException(); }
public IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
{
return AsyncInfo.Run<IBuffer, uint>(async (cancelToken, progress) =>
{
progress.Report(0);
using (var request = new HttpRequestMessage(HttpMethod.Get, _uri))
{
request.Headers.TryAppendWithoutValidation("Range", $"bytes={Position}-{Position + count}");
using (var response = await _client.SendRequestAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
using (var content = response.Content)
using (var stream = await content.ReadAsInputStreamAsync())
return await stream.ReadAsync(buffer, count, options).AsTask(cancelToken, progress);
}
}
});
}
public void Seek(ulong position)
{
Position = position;
}
public IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer) { throw new NotSupportedException(); }
static async Task<HttpStreamingStream> CreateAsyncInternal(HttpClient client, Uri uri)
{
using (var request = new HttpRequestMessage(HttpMethod.Head, uri))
using (var response = await client.SendRequestAsync(request))
{
response.EnsureSuccessStatusCode();
using (var content = response.Content)
{
var size = content.Headers.ContentLength;
if (size == null)
throw new NotSupportedException("The requested Uri doesn't support Content-Length");
string acceptRanges;
if (!response.Headers.TryGetValue("Accept-Ranges", out acceptRanges) || acceptRanges != "bytes")
throw new NotSupportedException("The requested Uri may not support ranged request.");
var contentType = response.Content.Headers.ContentType?.MediaType;
return new HttpStreamingStream(client, uri, size.Value, contentType);
}
}
}
public static IAsyncOperation<HttpStreamingStream> CreateAsync(HttpClient client, Uri uri)
{
return CreateAsyncInternal(client, uri).AsAsyncOperation();
}
}
}
(代码是 C# 6,我目测 code highlighing 又要死的很惨了这个不知名的 code highlighter 居然做得很漂亮嘛,基本能看。另外恭喜 Web Essentials 的 Markdown 高亮,不管是 Github 式 code block 还是每行前面加 tab,全都死了)
可以看到很多方法不是未实现就是不支持,但是这样就够了,系统类库很智能,用这些就能 buffer 和 seek 了。详细解释起来的话很复杂,涉及 HTTP 通讯;UWP/.NET 互操作;一些文档没说的内容 等方面,懒得写。
总之用这个代码就可以自定义一个 HttpClient
(MSDN),然后 CreateAsync()
出一个 Stream,然后 MediaElement.SetSource(IRandomAccessStream, string)
(MSDN) 或者 MediaPlayer.SetStreamSource(IRandomAccessStream)
(MSDN) 来播放了。
不过奇怪的是现在后台播放器有内存泄漏,大概 1MB/100s,因为后台是有很严格的 RAM 限制的,不知道哪天就会爆炸,Profiler 插进去也没发现什么。
其它的代码不多了,内存泄漏的可能性很小,但是以上代码我已经每个 IDisposable 都 using 了,不能确定问题是我的还是系统自己的。
或许下次可以给系统一个来自文件的 IRandomAccessStream 试试看,下次。
最后博客新增了 Google+1 按钮(下面)和 Disqus 评论(更下面)(当然我觉得你们不瞎的话自己应该看的见),喜欢的话可以用一下,评论是可以不登录 Disqus 直接发表的。