簡體   English   中英

如何確保多個異步下載的數據按啟動順序保存?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM