Skip to content

Commit 9f8dfe0

Browse files
authored
Clip download progress (#782)
* Add progress reporting to clip downloader * Fix oversight with clip metadata FFmpeg args * Fix crash when not providing a specific FFmpeg path to the CLI clip downloader * Add Span/ReadOnlySpan Read/Write overrides to ThrottledStream.cs
1 parent 7c5f1ba commit 9f8dfe0

File tree

4 files changed

+85
-11
lines changed

4 files changed

+85
-11
lines changed

TwitchDownloaderCLI/Modes/DownloadClip.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23
using System.Text.RegularExpressions;
34
using System.Threading;
45
using TwitchDownloaderCLI.Modes.Arguments;
@@ -48,7 +49,7 @@ private static ClipDownloadOptions GetDownloadOptions(ClipDownloadArgs inputOpti
4849
Filename = inputOptions.OutputFile,
4950
Quality = inputOptions.Quality,
5051
ThrottleKib = inputOptions.ThrottleKib,
51-
FfmpegPath = inputOptions.FfmpegPath,
52+
FfmpegPath = string.IsNullOrWhiteSpace(inputOptions.FfmpegPath) ? FfmpegHandler.FfmpegExecutableName : Path.GetFullPath(inputOptions.FfmpegPath),
5253
EncodeMetadata = inputOptions.EncodeMetadata!.Value,
5354
TempFolder = inputOptions.TempFolder
5455
};

TwitchDownloaderCore/ClipDownloader.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading;
77
using System.Threading.Tasks;
88
using System.Web;
9+
using TwitchDownloaderCore.Extensions;
910
using TwitchDownloaderCore.Options;
1011
using TwitchDownloaderCore.Tools;
1112

@@ -40,12 +41,18 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
4041
TwitchHelper.CreateDirectory(clipDirectory.FullName);
4142
}
4243

43-
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Downloading Clip"));
44+
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Downloading Clip 0%"));
45+
46+
void DownloadProgressHandler(StreamCopyProgress streamProgress)
47+
{
48+
var percent = (int)(streamProgress.BytesCopied / (double)streamProgress.SourceLength * 100);
49+
_progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Downloading Clip {percent}%"));
50+
_progress.Report(new ProgressReport(percent));
51+
}
4452

4553
if (!downloadOptions.EncodeMetadata)
4654
{
47-
await DownloadFileTaskAsync(downloadUrl, downloadOptions.Filename, downloadOptions.ThrottleKib, cancellationToken);
48-
_progress.Report(new ProgressReport(100));
55+
await DownloadFileTaskAsync(downloadUrl, downloadOptions.Filename, downloadOptions.ThrottleKib, new Progress<StreamCopyProgress>(DownloadProgressHandler), cancellationToken);
4956
return;
5057
}
5158

@@ -57,12 +64,14 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
5764
var tempFile = Path.Combine(downloadOptions.TempFolder, $"clip_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}_{Path.GetRandomFileName()}");
5865
try
5966
{
60-
await DownloadFileTaskAsync(downloadUrl, tempFile, downloadOptions.ThrottleKib, cancellationToken);
67+
await DownloadFileTaskAsync(downloadUrl, tempFile, downloadOptions.ThrottleKib, new Progress<StreamCopyProgress>(DownloadProgressHandler), cancellationToken);
6168

62-
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Encoding Clip Metadata"));
69+
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Encoding Clip Metadata 0%"));
70+
_progress.Report(new ProgressReport(0));
6371

6472
await EncodeClipMetadata(tempFile, downloadOptions.Filename, cancellationToken);
6573

74+
_progress.Report(new ProgressReport(ReportType.SameLineStatus, "Encoding Clip Metadata 100%"));
6675
_progress.Report(new ProgressReport(100));
6776
}
6877
finally
@@ -103,22 +112,26 @@ private async Task<string> GetDownloadUrl()
103112
return downloadUrl + "?sig=" + listLinks[0].data.clip.playbackAccessToken.signature + "&token=" + HttpUtility.UrlEncode(listLinks[0].data.clip.playbackAccessToken.value);
104113
}
105114

106-
private static async Task DownloadFileTaskAsync(string url, string destinationFile, int throttleKib, CancellationToken cancellationToken)
115+
private static async Task DownloadFileTaskAsync(string url, string destinationFile, int throttleKib, IProgress<StreamCopyProgress> progress, CancellationToken cancellationToken)
107116
{
108117
var request = new HttpRequestMessage(HttpMethod.Get, url);
109118
using var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
110119
response.EnsureSuccessStatusCode();
111120

121+
var contentLength = response.Content.Headers.ContentLength;
122+
112123
if (throttleKib == -1)
113124
{
114125
await using var fs = new FileStream(destinationFile, FileMode.Create, FileAccess.Write, FileShare.Read);
115-
await response.Content.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
126+
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
127+
await contentStream.ProgressCopyToAsync(fs, contentLength, progress, cancellationToken).ConfigureAwait(false);
116128
}
117129
else
118130
{
119-
await using var throttledStream = new ThrottledStream(await response.Content.ReadAsStreamAsync(cancellationToken), throttleKib);
120131
await using var fs = new FileStream(destinationFile, FileMode.Create, FileAccess.Write, FileShare.Read);
121-
await throttledStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
132+
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
133+
await using var throttledStream = new ThrottledStream(contentStream, throttleKib);
134+
await throttledStream.ProgressCopyToAsync(fs, contentLength, progress, cancellationToken).ConfigureAwait(false);
122135
}
123136
}
124137

@@ -137,7 +150,7 @@ await FfmpegMetadata.SerializeAsync(metadataFile, clipInfo.data.clip.broadcaster
137150
StartInfo =
138151
{
139152
FileName = downloadOptions.FfmpegPath,
140-
Arguments = $"-i \"{inputFile}\" -i \"{metadataFile}\" -map_metadata 1 -c copy \"{destinationFile}\"",
153+
Arguments = $"-i \"{inputFile}\" -i \"{metadataFile}\" -map_metadata 1 -y -c copy \"{destinationFile}\"",
141154
UseShellExecute = false,
142155
CreateNoWindow = true,
143156
RedirectStandardInput = false,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Buffers;
3+
using System.IO;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace TwitchDownloaderCore.Extensions
8+
{
9+
public record struct StreamCopyProgress(long SourceLength, long BytesCopied);
10+
11+
public static class StreamExtensions
12+
{
13+
// The default size from Stream.GetCopyBufferSize() is 81_920.
14+
private const int STREAM_DEFAULT_BUFFER_LENGTH = 81_920;
15+
16+
public static async Task ProgressCopyToAsync(this Stream source, Stream destination, long? sourceLength, IProgress<StreamCopyProgress> progress = null,
17+
CancellationToken cancellationToken = default)
18+
{
19+
if (!sourceLength.HasValue)
20+
{
21+
await source.CopyToAsync(destination, cancellationToken);
22+
return;
23+
}
24+
25+
var rentedBuffer = ArrayPool<byte>.Shared.Rent(STREAM_DEFAULT_BUFFER_LENGTH);
26+
var buffer = rentedBuffer.AsMemory(0, STREAM_DEFAULT_BUFFER_LENGTH);
27+
var totalBytesRead = 0L;
28+
29+
try
30+
{
31+
var bytesRead = 0;
32+
do
33+
{
34+
bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
35+
if (bytesRead == 0)
36+
continue;
37+
38+
await destination.WriteAsync(buffer[..bytesRead], cancellationToken).ConfigureAwait(false);
39+
totalBytesRead += bytesRead;
40+
41+
progress?.Report(new StreamCopyProgress(sourceLength!.Value, totalBytesRead));
42+
} while (bytesRead != 0);
43+
}
44+
finally
45+
{
46+
ArrayPool<byte>.Shared.Return(rentedBuffer);
47+
}
48+
}
49+
}
50+
}

TwitchDownloaderCore/Tools/ThrottledStream.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ public override int Read(byte[] buffer, int offset, int count)
5050
return read;
5151
}
5252

53+
public override int Read(Span<byte> buffer)
54+
{
55+
var newCount = GetBytesToReturn(buffer.Length);
56+
var read = BaseStream.Read(buffer[..newCount]);
57+
Interlocked.Add(ref _totalBytesRead, read);
58+
return read;
59+
}
60+
5361
public override long Seek(long offset, SeekOrigin origin)
5462
{
5563
return BaseStream.Seek(offset, origin);
@@ -59,6 +67,8 @@ public override void SetLength(long value) { }
5967

6068
public override void Write(byte[] buffer, int offset, int count) { }
6169

70+
public override void Write(ReadOnlySpan<byte> buffer) { }
71+
6272
private int GetBytesToReturn(int count)
6373
{
6474
return GetBytesToReturnAsync(count).GetAwaiter().GetResult();

0 commit comments

Comments
 (0)