Skip to content

Commit

Permalink
New video finalization method (#1103)
Browse files Browse the repository at this point in the history
* Concat video parts with FFmpeg

* Fix too long chapters causing some video players to hallucinate a longer video duration

* Fix race conditions

* Implement unused CancellationToken

* Log failure to clean up

* Forgot to specify concat arg. May or may not be necessary

* Fix video trimming

* Make sure to dispose of FFmpeg

* Don't append trim args unless absolutely necessary

* Cleanup

* Fix
  • Loading branch information
ScrubN authored Jun 20, 2024
1 parent a49daec commit 7cda6be
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 88 deletions.
8 changes: 7 additions & 1 deletion TwitchDownloaderCore/ChatDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,16 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
}
catch
{
await Task.Delay(100, CancellationToken.None);

outputFileInfo.Refresh();
if (outputFileInfo.Exists && outputFileInfo.Length == 0)
{
outputFileInfo.Delete();
try
{
outputFileInfo.Delete();
}
catch { }
}

throw;
Expand Down
14 changes: 12 additions & 2 deletions TwitchDownloaderCore/ChatRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,28 @@ public async Task RenderVideoAsync(CancellationToken cancellationToken)
}
catch
{
await Task.Delay(100, CancellationToken.None);

outputFileInfo.Refresh();
if (outputFileInfo.Exists && outputFileInfo.Length == 0)
{
outputFileInfo.Delete();
try
{
outputFileInfo.Delete();
}
catch { }
}

if (maskFileInfo is not null)
{
maskFileInfo.Refresh();
if (maskFileInfo.Exists && maskFileInfo.Length == 0)
{
maskFileInfo.Delete();
try
{
maskFileInfo.Delete();
}
catch { }
}
}

Expand Down
8 changes: 7 additions & 1 deletion TwitchDownloaderCore/ChatUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,16 @@ public async Task UpdateAsync(CancellationToken cancellationToken)
}
catch
{
await Task.Delay(100, CancellationToken.None);

outputFileInfo.Refresh();
if (outputFileInfo.Exists && outputFileInfo.Length == 0)
{
outputFileInfo.Delete();
try
{
outputFileInfo.Delete();
}
catch { }
}

throw;
Expand Down
10 changes: 9 additions & 1 deletion TwitchDownloaderCore/ClipDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
}
catch
{
await Task.Delay(100, CancellationToken.None);

outputFileInfo.Refresh();
if (outputFileInfo.Exists && outputFileInfo.Length == 0)
{
outputFileInfo.Delete();
try
{
outputFileInfo.Delete();
}
catch { }
}

throw;
Expand Down Expand Up @@ -101,6 +107,8 @@ private async Task DownloadAsyncImpl(FileInfo outputFileInfo, FileStream outputF
}
finally
{
await Task.Delay(100, CancellationToken.None);

File.Delete(tempFile);
}

Expand Down
35 changes: 35 additions & 0 deletions TwitchDownloaderCore/Tools/FfmpegConcatList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace TwitchDownloaderCore.Tools
{
// https://www.ffmpeg.org/ffmpeg-formats.html#toc-concat-1
public static class FfmpegConcatList
{
private const string LINE_FEED = "\u000A";

public static async Task SerializeAsync(string filePath, M3U8 playlist, Range videoListCrop, CancellationToken cancellationToken = default)
{
await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED };

await sw.WriteLineAsync("ffconcat version 1.0");

foreach (var stream in playlist.Streams.Take(videoListCrop))
{
cancellationToken.ThrowIfCancellationRequested();

await sw.WriteAsync("file '");
await sw.WriteAsync(stream.Path);
await sw.WriteLineAsync('\'');

await sw.WriteAsync("duration ");
await sw.WriteLineAsync(stream.PartInfo.Duration.ToString(CultureInfo.InvariantCulture));
}
}
}
}
23 changes: 19 additions & 4 deletions TwitchDownloaderCore/Tools/FfmpegMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ public static class FfmpegMetadata
private const string LINE_FEED = "\u000A";

public static async Task SerializeAsync(string filePath, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, string videoDescription = null,
TimeSpan startOffset = default, IEnumerable<VideoMomentEdge> videoMomentEdges = null, CancellationToken cancellationToken = default)
TimeSpan startOffset = default, TimeSpan videoLength = default, IEnumerable<VideoMomentEdge> videoMomentEdges = null, CancellationToken cancellationToken = default)
{
await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED };

await SerializeGlobalMetadata(sw, streamerName, videoId, videoTitle, videoCreation, viewCount, videoDescription);
await fs.FlushAsync(cancellationToken);

await SerializeChapters(sw, videoMomentEdges, startOffset);
await SerializeChapters(sw, videoMomentEdges, startOffset, videoLength);
await fs.FlushAsync(cancellationToken);
}

Expand All @@ -44,14 +44,13 @@ private static async Task SerializeGlobalMetadata(StreamWriter sw, string stream
await sw.WriteLineAsync(@$"Views: {viewCount}");
}

private static async Task SerializeChapters(StreamWriter sw, IEnumerable<VideoMomentEdge> videoMomentEdges, TimeSpan startOffset)
private static async Task SerializeChapters(StreamWriter sw, IEnumerable<VideoMomentEdge> videoMomentEdges, TimeSpan startOffset, TimeSpan videoLength)
{
if (videoMomentEdges is null)
{
return;
}

// Note: FFmpeg automatically handles out of range chapters for us
var startOffsetMillis = (int)startOffset.TotalMilliseconds;
foreach (var momentEdge in videoMomentEdges)
{
Expand All @@ -64,6 +63,22 @@ private static async Task SerializeChapters(StreamWriter sw, IEnumerable<VideoMo
var lengthMillis = momentEdge.node.durationMilliseconds;
var gameName = momentEdge.node.details.game?.displayName ?? momentEdge.node.description;

// videoLength may be 0 if it is not passed as an arg
if (videoLength > TimeSpan.Zero)
{
var chapterStart = TimeSpan.FromMilliseconds(startMillis);
if (chapterStart >= videoLength)
{
continue;
}

var chapterEnd = chapterStart + TimeSpan.FromMilliseconds(lengthMillis);
if (chapterEnd > videoLength)
{
lengthMillis = (int)(videoLength - chapterStart).TotalMilliseconds;
}
}

await sw.WriteLineAsync("[CHAPTER]");
await sw.WriteLineAsync("TIMEBASE=1/1000");
await sw.WriteLineAsync($"START={startMillis}");
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderCore/Tools/M3U8.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public enum PlaylistType
// Twitch specific
public uint TwitchLiveSequence { get; private set; }
public decimal TwitchElapsedSeconds { get; private set; }
public decimal TwitchTotalSeconds { get; private set; }
public decimal TwitchTotalSeconds { get; internal set; }

// Other headers that we don't have dedicated properties for. Useful for debugging.
private readonly List<KeyValuePair<string, string>> _unparsedValues = new();
Expand Down
8 changes: 7 additions & 1 deletion TwitchDownloaderCore/TsMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,16 @@ public async Task MergeAsync(CancellationToken cancellationToken)
}
catch
{
await Task.Delay(100, CancellationToken.None);

outputFileInfo.Refresh();
if (outputFileInfo.Exists && outputFileInfo.Length == 0)
{
outputFileInfo.Delete();
try
{
outputFileInfo.Delete();
}
catch { }
}

throw;
Expand Down
Loading

0 comments on commit 7cda6be

Please sign in to comment.