[英]How to make sure that the data of multiple Async downloads are saved in the order they were started?
我正在編寫一個基本的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);
}
}
}
我認為您根本不需要生產者/消費者隊列。 但是,我確實認為您應該避免“生而死”。
您可以同時啟動它們,並在完成時對其進行處理。
首先,定義如何下載單個段:
private async Task<byte[]> DownloadTsSegmentAsync(string tsUrl)
{
var response = await _client.GetAsync(tsUrl, _cts.Token).ConfigureAwait(false);
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
然后添加對播放列表的解析,從而產生分段下載列表 (所有下載正在進行中):
private List<Task<byte[]>> DownloadTasks(string data)
{
var result = new List<Task<byte[]>>();
string[] lines = data.Split(new string[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
if (!lines.Any() || lines[0] != "#EXTM3U")
throw new Exception("Invalid m3u8 media playlist.");
...
if (_isNewSegment)
{
result.Add(DownloadTsSegmentAsync(line));
}
...
return result;
}
通過寫入文件來一次(按順序)消耗一個列表:
private async Task RunConsumerAsync(List<Task<byte[]>> downloads)
{
using (FileStream fs = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
for (var task in downloads)
{
var data = await task.ConfigureAwait(false);
await fs.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
}
}
}
並與制作人一起開始一切:
public async Task RunAsync()
{
// TODO: consider CancellationToken instead of a boolean.
while (!_isCancelled)
{
var response = await _client.GetAsync(_playlistBaseUri + _playlistFilename, _cts.Token).ConfigureAwait(false);
var data = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var tasks = DownloadTasks(data);
await RunConsumerAsync(tasks);
await Task.Delay(_targetDuration * 1000, _cts.Token).ConfigureAwait(false);
}
}
請注意,此解決方案確實會同時運行所有下載,這可能會導致內存不足。 如果這是一個問題,我建議您進行重組以使用TPL Dataflow,它內置了節流支持。
為每個下載分配一個序列號。 將結果放入Dictionary<int, byte[]>
。 每次下載完成時,都會添加自己的結果。
然后,它檢查是否有要寫入磁盤的段:
while (dict.ContainsKey(lowestWrittenSegmentNumber + 1)) {
WriteSegment(dict[lowestWrittenSegmentNumber + 1]);
lowestWrittenSegmentNumber++;
}
這樣,所有段都按順序並帶有緩沖地存儲在磁盤上。
RunConsumer();
RunProducer();
確保使用async Task
以便可以使用等待async Task
來等待完成await Task.WhenAll(RunConsumer(), RunProducer());
。 但是您不再需要RunConsumer
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.