【发布时间】:2016-06-06 17:12:16
【问题描述】:
我正在编写一个基本的 Http Live Stream (HLS) 下载器,我正在以“#EXT-X-TARGETDURATION”指定的时间间隔重新下载 m3u8 媒体播放列表,然后将 *.ts 片段下载为它们变得可用。
这是 m3u8 媒体播放列表首次下载时的样子。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:12
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:7.975,
http://website.com/segment_1.ts
#EXTINF:7.941,
http://website.com/segment_2.ts
#EXTINF:7.975,
http://website.com/segment_3.ts
我想使用 HttpClient async/await 同时下载这些 *.ts 段。这些段的大小不同,因此即使“segment_1.ts”的下载首先开始,它也可能在其他两个段之后完成。
这些片段都是一个大视频的一部分,因此下载片段的数据必须按照它们开始的顺序写入,而不是按照它们完成的顺序。
如果分段一个接一个地下载,我下面的代码可以正常工作,但同时下载多个分段时就不行了,因为有时它们不会按照开始的顺序完成。
我考虑过使用 Task.WhenAll,它可以保证正确的顺序,但我不想将下载的段不必要地保留在内存中,因为它们的大小可能只有几兆字节。如果“segment_1.ts”的下载确实首先完成,那么它应该立即写入磁盘,而不必等待其他段完成。将所有 *.ts 段写入单独的文件并在最后加入它们也不是一种选择,因为它需要双倍的磁盘空间,并且整个视频的大小可能是几 GB。
我不知道该怎么做,我想知道是否有人可以帮助我。我正在寻找一种不需要我手动创建线程或长时间阻塞 ThreadPool 线程的方法。
一些代码和异常处理已被删除,以便更容易看到发生了什么。
// Async BlockingCollection from the AsyncEx library
private AsyncCollection<byte[]> segmentDataQueue = new AsyncCollection<byte[]>();
public void Start()
{
RunConsumer();
RunProducer();
}
private async void RunProducer()
{
while (!_isCancelled)
{
var response = await _client.GetAsync(_playlistBaseUri + _playlistFilename, _cts.Token).ConfigureAwait(false);
var data = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
string[] lines = data.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
if (!lines.Any() || lines[0] != "#EXTM3U")
throw new Exception("Invalid m3u8 media playlist.");
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
if (line.StartsWith("#EXT-X-TARGETDURATION"))
{
ParseTargetDuration(line);
}
else if (line.StartsWith("#EXT-X-MEDIA-SEQUENCE"))
{
ParseMediaSequence(line);
}
else if (!line.StartsWith("#"))
{
if (_isNewSegment)
{
// Fire and forget
DownloadTsSegment(line);
}
}
}
// Wait until it's time to reload the m3u8 media playlist again
await Task.Delay(_targetDuration * 1000, _cts.Token).ConfigureAwait(false);
}
}
// async void. We never await this method, so we can download multiple segments at once
private async void DownloadTsSegment(string tsUrl)
{
var response = await _client.GetAsync(tsUrl, _cts.Token).ConfigureAwait(false);
var data = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
// Add the downloaded segment data to the AsyncCollection
await segmentDataQueue.AddAsync(data, _cts.Token).ConfigureAwait(false);
}
private async void RunConsumer()
{
using (FileStream fs = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
while (!_isCancelled)
{
// Wait until new segment data is added to the AsyncCollection and write it to disk
var data = await segmentDataQueue.TakeAsync(_cts.Token).ConfigureAwait(false);
await fs.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
}
}
}
【问题讨论】:
-
我认为除了您已经知道的方法之外,没有其他方法了。 1)将段保存到磁盘并在所有段完成后加入。 2) 使用
Task.WhenAll保证订单 3) 不使用Fire and Forget 保证订单,但对每个段下载使用await。 3 个选项中的每一个都有自己的优点,但也有缺点。这里没有灵丹妙药,您必须选择最适合您能够接受/符合要求的解决方案。
标签: c# async-await httpclient http-live-streaming m3u8