C# 如何确保多个异步下载的数据按启动顺序保存?
我正在编写一个基本的Http Live Stream(HLS)下载程序,其中我正在以“#EXT-X-TARGETDURATION”指定的间隔重新下载一个m3u8媒体播放列表,然后下载可用的*.ts段 这是m3u8媒体播放列表首次下载时的样子C# 如何确保多个异步下载的数据按启动顺序保存?,c#,async-await,httpclient,http-live-streaming,m3u8,C#,Async Await,Httpclient,Http Live Streaming,M3u8,我正在编写一个基本的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:
#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
我不知道怎么做,我想知道是否有人能帮我。我正在寻找一种不需要我手动创建线程或长时间阻止线程池线程的方法
一些代码和异常处理已被删除,以便更容易查看正在发生的情况
//来自AsyncEx库的Async BlockingCollection
private AsyncCollection segmentDataQueue=new AsyncCollection();
公开作废开始()
{
RunConsumer();
RunProducer();
}
专用异步void RunProducer()
{
而(!\u被取消)
{
var response=await\u client.GetAsync(\u playlaybaseuri+\u playlayfilename,\u cts.Token)。ConfigureAwait(false);
var data=await response.Content.ReadAsStringAsync().ConfigureAwait(false);
string[]lines=data.Split(新字符串[]{“\n”},StringSplitOptions.RemoveEmptyEntries);
如果(!lines.Any()| | lines[0]!=“#EXTM3U”)
抛出新异常(“无效的m3u8媒体播放列表”);
对于(变量i=1;i
为每个下载分配一个序列号。将结果放入词典
。每次下载完成后,它都会添加自己的结果
然后检查是否有要写入磁盘的段:
while (dict.ContainsKey(lowestWrittenSegmentNumber + 1)) {
WriteSegment(dict[lowestWrittenSegmentNumber + 1]);
lowestWrittenSegmentNumber++;
}
这样,所有段都会以顺序和缓冲方式结束在磁盘上
确保使用
async Task
,以便您可以使用wait Task.WhenAll(RunConsumer(),RunProducer())等待完成代码>。但是您不应该再需要运行consumer
。我认为您根本不需要生产者/消费者队列。然而,我确实认为你应该避免“火与忘”
您可以同时启动它们,并在它们完成时进行处理
首先,定义如何下载单个片段:
private async Task<byte[]> DownloadTsSegmentAsync(string tsUrl)
{
var response = await _client.GetAsync(tsUrl, _cts.Token).ConfigureAwait(false);
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
}
请注意,此解决方案确实会同时运行所有下载,这可能会造成内存压力。如果这是一个问题,我建议您重新构造以使用TPL Dataflow,它内置了对节流的支持。我认为没有其他方法可以替代您已经知道的方法。1) 将段保存到磁盘,并在所有段完成后连接。2) 使用Task来保证订单。当所有3)保证订单时,不要使用Fire和Forget,而是对每个片段下载使用等待。这三种方案各有优缺点。这里没有灵丹妙药,你必须选择最适合你生活的解决方案。内存压力
OP已经指出他的文件大小可以达到GB。太大了,他甚至想用临时文件。非常感谢,很好用。唯一的小问题是RunAsync方法中的Task.Delay运行
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);
}
}