Skip to content

Commit

Permalink
Add Safe video trim mode to circumvent rare encoding issues (#1112)
Browse files Browse the repository at this point in the history
* Make videoLength calculation not dependent on Twitch API response

* Implement "safe" trimming to disable fractional trimming

* Implement in CLI

* Implement in WPF

* Update Translations
  • Loading branch information
ScrubN authored Jun 27, 2024
1 parent 979422e commit cdb247b
Show file tree
Hide file tree
Showing 24 changed files with 258 additions and 16 deletions.
4 changes: 4 additions & 0 deletions TwitchDownloaderCLI/Modes/Arguments/VideoDownloadArgs.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CommandLine;
using TwitchDownloaderCLI.Models;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderCLI.Modes.Arguments
{
Expand Down Expand Up @@ -27,6 +28,9 @@ internal sealed class VideoDownloadArgs : IFileCollisionArgs, ITwitchDownloaderA
[Option("bandwidth", Default = -1, HelpText = "The maximum bandwidth a thread will be allowed to use in kibibytes per second (KiB/s), or -1 for no maximum.")]
public int ThrottleKib { get; set; }

[Option("trim-mode", Default = VideoTrimMode.Exact, HelpText = "Sets the trim handling. Videos trimmed with exact trim may rarely experience video/audio stuttering within the first/last few seconds. Safe trimming is guaranteed to not stutter but may result in a slightly longer video. Valid values are: Safe, Exact")]
public VideoTrimMode TrimMode { get; set; }

[Option("oauth", HelpText = "OAuth access token to download subscriber only VODs. DO NOT SHARE THIS WITH ANYONE.")]
public string Oauth { get; set; }

Expand Down
1 change: 1 addition & 0 deletions TwitchDownloaderCLI/Modes/DownloadVideo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private static VideoDownloadOptions GetDownloadOptions(VideoDownloadArgs inputOp
TrimBeginningTime = inputOptions.TrimBeginningTime,
TrimEnding = inputOptions.TrimEndingTime > TimeSpan.Zero,
TrimEndingTime = inputOptions.TrimEndingTime,
TrimMode = inputOptions.TrimMode,
FfmpegPath = string.IsNullOrWhiteSpace(inputOptions.FfmpegPath) ? FfmpegHandler.FfmpegExecutableName : Path.GetFullPath(inputOptions.FfmpegPath),
TempFolder = inputOptions.TempFolder,
CacheCleanerCallback = directoryInfos =>
Expand Down
3 changes: 3 additions & 0 deletions TwitchDownloaderCLI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Time to trim ending. See [Time durations](#time-durations) for a more detailed e
**--bandwidth**
(Default: `-1`) The maximum bandwidth a thread will be allowed to use in kibibytes per second (KiB/s), or `-1` for no maximum.

**--trim-mode**
(Default: `Exact`) Sets the video trim handling. Videos trimmed with exact trim may rarely experience video/audio stuttering within the first/last few seconds. Safe trimming is guaranteed to not stutter but may result in a slightly longer video. Valid values are: `Safe`, `Exact`.

**--oauth**
OAuth access token to download subscriber only VODs. <ins>**DO NOT SHARE YOUR OAUTH TOKEN WITH ANYONE.**</ins>

Expand Down
2 changes: 2 additions & 0 deletions TwitchDownloaderCore/Options/VideoDownloadOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderCore.Options
{
Expand All @@ -19,5 +20,6 @@ public class VideoDownloadOptions
public string TempFolder { get; set; }
public Func<DirectoryInfo[], DirectoryInfo[]> CacheCleanerCallback { get; set; }
public Func<FileInfo, FileInfo> FileCollisionCallback { get; set; } = info => info;
public VideoTrimMode TrimMode { get; set; }
}
}
6 changes: 6 additions & 0 deletions TwitchDownloaderCore/Tools/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ public enum TimestampFormat
None,
UtcFull
}

public enum VideoTrimMode
{
Safe,
Exact
}
}
30 changes: 17 additions & 13 deletions TwitchDownloaderCore/VideoDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF
var videoInfo = videoInfoResponse.data.video;
var (playlist, airDate) = await GetVideoPlaylist(playlistUrl, cancellationToken);

var videoListCrop = GetStreamListTrim(playlist.Streams, videoInfo, out var videoLength, out var startOffset, out var endOffset);
var videoListCrop = GetStreamListTrim(playlist.Streams, out var videoLength, out var startOffset, out var endDuration);

CheckAvailableStorageSpace(qualityPlaylist.StreamInfo.Bandwidth, videoLength);

Expand Down Expand Up @@ -128,7 +128,7 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfo.owner.displayName, d
var ffmpegRetries = 0;
do
{
ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, outputFileInfo, concatListPath, metadataPath, startOffset, endOffset, videoLength), cancellationToken);
ffmpegExitCode = await Task.Run(() => RunFfmpegVideoCopy(downloadFolder, outputFileInfo, concatListPath, metadataPath, startOffset, endDuration, videoLength), cancellationToken);
if (ffmpegExitCode != 0)
{
_progress.LogError($"Failed to finalize video (code {ffmpegExitCode}), retrying in 10 seconds...");
Expand Down Expand Up @@ -326,7 +326,7 @@ private async Task VerifyDownloadedParts(ICollection<M3U8.Stream> playlist, Rang
}
}

private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string concatListPath, string metadataPath, decimal startOffset, decimal endOffset, TimeSpan videoLength)
private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string concatListPath, string metadataPath, decimal startOffset, decimal endDuration, TimeSpan videoLength)
{
using var process = new Process
{
Expand Down Expand Up @@ -358,7 +358,7 @@ private int RunFfmpegVideoCopy(string tempFolder, FileInfo outputFile, string co
};

// TODO: Make this optional - "Safe" and "Exact" trimming methods
if (endOffset > 0)
if (endDuration > 0)
{
args.Insert(0, "-t");
args.Insert(1, videoLength.TotalSeconds.ToString(CultureInfo.InvariantCulture));
Expand Down Expand Up @@ -438,15 +438,15 @@ private void HandleFfmpegOutput(string output, Regex encodingTimeRegex, TimeSpan
return (playlist, airDate);
}

private Range GetStreamListTrim(IList<M3U8.Stream> streamList, VideoInfo videoInfo, out TimeSpan videoLength, out decimal startOffset, out decimal endOffset)
private Range GetStreamListTrim(IList<M3U8.Stream> streamList, out TimeSpan videoLength, out decimal startOffset, out decimal endDuration)
{
startOffset = 0;
endOffset = 0;
endDuration = 0;

var startIndex = 0;
var startTime = 0m;
if (downloadOptions.TrimBeginning)
{
var startTime = 0m;
var trimTotalSeconds = (decimal)downloadOptions.TrimBeginningTime.TotalSeconds;
foreach (var videoPart in streamList)
{
Expand All @@ -462,17 +462,18 @@ private Range GetStreamListTrim(IList<M3U8.Stream> streamList, VideoInfo videoIn
}

var endIndex = streamList.Count;
var endTime = streamList.Sum(x => x.PartInfo.Duration);
var endOffset = 0m;
if (downloadOptions.TrimEnding)
{
var endTime = streamList.Sum(x => x.PartInfo.Duration);
var trimTotalSeconds = (decimal)downloadOptions.TrimEndingTime.TotalSeconds;
for (var i = streamList.Count - 1; i >= 0; i--)
{
var videoPart = streamList[i];
if (endTime - videoPart.PartInfo.Duration < trimTotalSeconds)
{
var offset = endTime - trimTotalSeconds;
if (offset > 0) endOffset = videoPart.PartInfo.Duration - offset;
endOffset = endTime - trimTotalSeconds;
if (endOffset > 0) endDuration = videoPart.PartInfo.Duration - endOffset;

break;
}
Expand All @@ -482,9 +483,12 @@ private Range GetStreamListTrim(IList<M3U8.Stream> streamList, VideoInfo videoIn
}
}

videoLength =
(downloadOptions.TrimEnding ? downloadOptions.TrimEndingTime : TimeSpan.FromSeconds(videoInfo.lengthSeconds))
- (downloadOptions.TrimBeginning ? downloadOptions.TrimBeginningTime : TimeSpan.Zero);
if (downloadOptions.TrimMode == VideoTrimMode.Safe)
{
startOffset = endOffset = endDuration = 0;
}

videoLength = TimeSpan.FromSeconds((double)((endTime - endOffset) - (startTime + startOffset)));

return new Range(startIndex, endIndex);
}
Expand Down
3 changes: 3 additions & 0 deletions TwitchDownloaderWPF/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@
<setting name="AdjustUsernameVisibility" serializeAs="String">
<value>False</value>
</setting>
<setting name="VodTrimMode" serializeAs="String">
<value>1</value>
</setting>
</TwitchDownloaderWPF.Properties.Settings>
</userSettings>
</configuration>
9 changes: 7 additions & 2 deletions TwitchDownloaderWPF/PageVodDownload.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,20 @@
<StackPanel HorizontalAlignment="Left">
<TextBlock Text="{lex:Loc Length}" HorizontalAlignment="Right" Foreground="{DynamicResource AppText}" />
<TextBlock Text="{lex:Loc Quality}" HorizontalAlignment="Right" Margin="0,15,0,0" Foreground="{DynamicResource AppText}" />
<TextBlock Text="{lex:Loc TrimVideo}" HorizontalAlignment="Right" Margin="0,17,0,0" Foreground="{DynamicResource AppText}" />
<TextBlock HorizontalAlignment="Right" Margin="0,15,0,0" Foreground="{DynamicResource AppText}"><Run Text="{lex:Loc VideoTrimMode}"/><Hyperlink ToolTipService.ShowDuration="30000" Foreground="{DynamicResource AppHyperlink}"><Hyperlink.ToolTip><Run Text="{lex:Loc VideoTrimModeTooltip}"/></Hyperlink.ToolTip>(?)</Hyperlink>:</TextBlock>
<TextBlock Text="{lex:Loc TrimVideo}" HorizontalAlignment="Right" Margin="0,11,0,0" Foreground="{DynamicResource AppText}" />
<TextBlock Text="{lex:Loc VideoDownloadThreads}" HorizontalAlignment="Right" Margin="0,46,0,0" Foreground="{DynamicResource AppText}" />
<TextBlock HorizontalAlignment="Right" Margin="0,21,0,0" Foreground="{DynamicResource AppText}"><Run Text="{lex:Loc Oauth}"/><Hyperlink NavigateUri="https://www.youtube.com/watch?v=1MBsUoFGuls" RequestNavigate="Hyperlink_RequestNavigate" ToolTipService.ShowDuration="30000" Foreground="{DynamicResource AppHyperlink}"><Hyperlink.ToolTip><Run Text="{lex:Loc OauthTooltip}"/></Hyperlink.ToolTip>(?)</Hyperlink>:</TextBlock>
</StackPanel>
<StackPanel>
<TextBlock x:Name="labelLength" Text="0:0:0" Margin="5,0,0,0" Foreground="{DynamicResource AppText}" />
<ComboBox x:Name="comboQuality" Margin="5,10,0,0" Background="{DynamicResource AppElementBackground}" BorderBrush="{DynamicResource AppElementBorder}" Foreground="{DynamicResource AppText}"/>
<StackPanel Margin="5,5,0,0">
<StackPanel Margin="5,8,0,0">
<StackPanel Orientation="Horizontal">
<RadioButton x:Name="RadioTrimSafe" Content="{lex:Loc VideoTrimModeSafe}" Margin="3,0,0,0" Checked="RadioTrimSafe_OnCheckedStateChanged" Unchecked="RadioTrimSafe_OnCheckedStateChanged" BorderBrush="{DynamicResource AppElementBorder}" Foreground="{DynamicResource AppText}" />
<RadioButton x:Name="RadioTrimExact" Content="{lex:Loc VideoTrimModeExact}" IsChecked="True" Margin="3,0,0,0" Checked="RadioTrimExact_OnCheckedStateChanged" Unchecked="RadioTrimExact_OnCheckedStateChanged" BorderBrush="{DynamicResource AppElementBorder}" Foreground="{DynamicResource AppText}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<CheckBox x:Name="checkStart" VerticalAlignment="Center" HorizontalAlignment="Left" Checked="checkStart_OnCheckStateChanged" Unchecked="checkStart_OnCheckStateChanged" BorderBrush="{DynamicResource AppElementBorder}"/>
<TextBlock Margin="2,0,0,0" Text="{lex:Loc TrimStart}" VerticalAlignment="Center" Foreground="{DynamicResource AppText}" />
<hc:NumericUpDown Margin="3,-1,0,0" Minimum="0" Maximum="48" Value="0" x:Name="numStartHour" ValueChanged="numStartHour_ValueChanged" Background="{DynamicResource AppElementBackground}" BorderBrush="{DynamicResource AppElementBorder}" Foreground="{DynamicResource AppText}" />
Expand Down
29 changes: 29 additions & 0 deletions TwitchDownloaderWPF/PageVodDownload.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ public VideoDownloadOptions GetOptions(string filename, string folder)
FfmpegPath = "ffmpeg",
TempFolder = Settings.Default.TempPath
};

if (RadioTrimSafe.IsChecked == true)
options.TrimMode = VideoTrimMode.Safe;
else if (RadioTrimExact.IsChecked == true)
options.TrimMode = VideoTrimMode.Exact;

return options;
}

Expand Down Expand Up @@ -338,6 +344,11 @@ private void Page_Initialized(object sender, EventArgs e)
WebRequest.DefaultWebProxy = null;
numDownloadThreads.Value = Settings.Default.VodDownloadThreads;
TextOauth.Text = Settings.Default.OAuth;
_ = (VideoTrimMode)Settings.Default.VodTrimMode switch
{
VideoTrimMode.Exact => RadioTrimExact.IsChecked = true,
_ => RadioTrimSafe.IsChecked = true,
};
}

private void numDownloadThreads_ValueChanged(object sender, HandyControl.Data.FunctionEventArgs<double> e)
Expand Down Expand Up @@ -547,5 +558,23 @@ private async void TextUrl_OnKeyDown(object sender, KeyEventArgs e)
e.Handled = true;
}
}

private void RadioTrimSafe_OnCheckedStateChanged(object sender, RoutedEventArgs e)
{
if (IsInitialized)
{
Settings.Default.VodTrimMode = (int)VideoTrimMode.Safe;
Settings.Default.Save();
}
}

private void RadioTrimExact_OnCheckedStateChanged(object sender, RoutedEventArgs e)
{
if (IsInitialized)
{
Settings.Default.VodTrimMode = (int)VideoTrimMode.Exact;
Settings.Default.Save();
}
}
}
}
12 changes: 12 additions & 0 deletions TwitchDownloaderWPF/Properties/Settings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions TwitchDownloaderWPF/Properties/Settings.settings
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@
<Setting Name="AdjustUsernameVisibility" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="VodTrimMode" Type="System.Int32" Scope="User">
<Value Profile="(Default)">1</Value>
</Setting>
</Settings>
</SettingsFile>

2 changes: 2 additions & 0 deletions TwitchDownloaderWPF/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ To get started, input a valid link or ID to a VOD or highlight. If the VOD or hi

**Quality**: Selects the quality of the download and provides an estimated file size. Occasionally Twitch calls the highest quality as 'Source' instead of the typical resolution formatting (1080p60 in the case of figure 1.1).

**Trim Mode**: Sets the video trim handling. Videos trimmed with exact trim may rarely experience video/audio stuttering within the first/last few seconds. Safe trimming is guaranteed to not stutter but may result in a slightly longer video.

**Trim**: Sets the start and end time to trim the video download in the format \[hours\] \[minutes\] \[seconds\]. Trimming the video will result in a smaller total download.

**Download Threads**: The amount of parallel download threads to be dispatched.
Expand Down
38 changes: 37 additions & 1 deletion TwitchDownloaderWPF/Translations/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions TwitchDownloaderWPF/Translations/Strings.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -928,4 +928,16 @@
<data name="FileAlreadyExistsRememberMyChoice" xml:space="preserve">
<value>Remember my choice for this session</value>
</data>
<data name="VideoTrimMode" xml:space="preserve">
<value>Trim Mode </value><comment>Leave a trailing space</comment>
</data>
<data name="VideoTrimModeSafe" xml:space="preserve">
<value>Safe</value>
</data>
<data name="VideoTrimModeExact" xml:space="preserve">
<value>Exact</value>
</data>
<data name="VideoTrimModeTooltip" xml:space="preserve">
<value>Videos trimmed with exact trim may rarely experience video/audio stuttering within the first/last few seconds. Safe trimming is guaranteed to not stutter but may result in a slightly longer video.</value>
</data>
</root>
12 changes: 12 additions & 0 deletions TwitchDownloaderWPF/Translations/Strings.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,16 @@
<data name="FileAlreadyExistsRememberMyChoice" xml:space="preserve">
<value>Remember my choice for this session</value>
</data>
<data name="VideoTrimMode" xml:space="preserve">
<value>Trim Mode </value><comment>Leave a trailing space</comment>
</data>
<data name="VideoTrimModeSafe" xml:space="preserve">
<value>Safe</value>
</data>
<data name="VideoTrimModeExact" xml:space="preserve">
<value>Exact</value>
</data>
<data name="VideoTrimModeTooltip" xml:space="preserve">
<value>Videos trimmed with exact trim may rarely experience video/audio stuttering within the first/last few seconds. Safe trimming is guaranteed to not stutter but may result in a slightly longer video.</value>
</data>
</root>
12 changes: 12 additions & 0 deletions TwitchDownloaderWPF/Translations/Strings.it.resx
Original file line number Diff line number Diff line change
Expand Up @@ -928,4 +928,16 @@
<data name="FileAlreadyExistsRememberMyChoice" xml:space="preserve">
<value>Remember my choice for this session</value>
</data>
<data name="VideoTrimMode" xml:space="preserve">
<value>Trim Mode </value><comment>Leave a trailing space</comment>
</data>
<data name="VideoTrimModeSafe" xml:space="preserve">
<value>Safe</value>
</data>
<data name="VideoTrimModeExact" xml:space="preserve">
<value>Exact</value>
</data>
<data name="VideoTrimModeTooltip" xml:space="preserve">
<value>Videos trimmed with exact trim may rarely experience video/audio stuttering within the first/last few seconds. Safe trimming is guaranteed to not stutter but may result in a slightly longer video.</value>
</data>
</root>
Loading

0 comments on commit cdb247b

Please sign in to comment.