Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #848 add CLI mode to manually concatenate ts parts #888

Merged
merged 55 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c603097
#tsmerge Update README.md
superbonaci Oct 28, 2023
baf2271
#tsmerge Update Program.cs
superbonaci Oct 28, 2023
9fc7c74
#tsmerge Add MergeTS.cs
superbonaci Oct 28, 2023
fefe961
#tsmerge Add TsMergeArgs.cs
superbonaci Oct 28, 2023
0a00e81
#tsmerge Add TsMerger.cs
superbonaci Oct 28, 2023
550c265
#tsmerge Add TsMergeOptions.cs
superbonaci Oct 28, 2023
0946fcc
Update Program.cs
superbonaci Oct 28, 2023
638d5b1
#tsmerge Update README.md
superbonaci Oct 28, 2023
36cc340
Program.cs Remove newline at the end
superbonaci Oct 28, 2023
8fb6a4d
Delete TwitchDownloaderCLI/Program.cs
superbonaci Oct 28, 2023
68d686b
Try to upload again Program.cs
superbonaci Oct 28, 2023
b1d48b1
Merge pull request #1 from lay295/master
superbonaci Nov 5, 2023
a69c641
Merge pull request #2 from lay295/master
superbonaci Nov 9, 2023
37a2e14
Merge pull request #3 from lay295/master
superbonaci Nov 12, 2023
32e6b0d
Delete TwitchDownloaderCLI/README.md
superbonaci Nov 12, 2023
a192207
Add files via upload
superbonaci Nov 12, 2023
7a41051
Delete TwitchDownloaderCLI/Program.cs
superbonaci Nov 12, 2023
b71b6b8
Add files via upload
superbonaci Nov 12, 2023
b9bfa63
Delete TwitchDownloaderCore/TsMerger.cs
superbonaci Nov 12, 2023
84172c6
Add files via upload
superbonaci Nov 12, 2023
286f71c
Delete TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
superbonaci Nov 12, 2023
e155335
Add files via upload
superbonaci Nov 12, 2023
8288281
Delete TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
superbonaci Nov 12, 2023
f8fbf31
Add files via upload
superbonaci Nov 12, 2023
ee02787
Delete TwitchDownloaderCore/TsMerger.cs
superbonaci Nov 12, 2023
393e3c0
Add files via upload
superbonaci Nov 12, 2023
00a7d4d
Delete TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
superbonaci Nov 12, 2023
81ecdbe
Add files via upload
superbonaci Nov 12, 2023
abe5b40
Delete TwitchDownloaderCore/TsMergeArgs.cs
superbonaci Nov 12, 2023
4c72064
Add files via upload
superbonaci Nov 12, 2023
60b3491
Delete TwitchDownloaderCLI/README.md
superbonaci Nov 12, 2023
263dff8
Add files via upload
superbonaci Nov 12, 2023
b1d95af
Merge pull request #4 from lay295/master
superbonaci Nov 16, 2023
b3f7bea
Merge pull request #5 from lay295/master
superbonaci Nov 18, 2023
92dcb03
Merge pull request #6 from lay295/master
superbonaci Nov 21, 2023
47022d7
Merge pull request #7 from lay295/master
superbonaci Nov 23, 2023
4d9f801
Merge pull request #8 from lay295/master
superbonaci Nov 28, 2023
10fd997
Merge pull request #9 from lay295/master
superbonaci Nov 29, 2023
2dc7026
Merge pull request #10 from lay295/master
superbonaci Nov 30, 2023
b7fe2a7
Merge pull request #11 from lay295/master
superbonaci Dec 1, 2023
4aa7309
Merge pull request #12 from lay295/master
superbonaci Dec 2, 2023
2e1a47d
Merge pull request #13 from lay295/master
superbonaci Dec 9, 2023
3dbebb9
Merge pull request #14 from lay295/master
superbonaci Dec 14, 2023
ba343bd
Merge pull request #15 from lay295/master
superbonaci Dec 15, 2023
a6ef5ee
Merge pull request #16 from lay295/master
superbonaci Dec 19, 2023
14b0728
Merge pull request #17 from lay295/master
superbonaci Dec 31, 2023
5f40948
Merge pull request #18 from lay295/master
superbonaci Jan 22, 2024
a3134c9
Cleanup code and fix Readme entry
ScrubN Jan 23, 2024
592f28f
Merge pull request #19 from lay295/master
superbonaci Jan 23, 2024
a4b4b5c
Merge pull request #20 from lay295/master
superbonaci Jan 30, 2024
5a7fb47
Merge pull request #21 from lay295/master
superbonaci Feb 2, 2024
4d607b3
Merge pull request #22 from lay295/master
superbonaci Feb 5, 2024
a11aa5b
Merge pull request #23 from lay295/master
superbonaci Feb 7, 2024
719e3e4
Merge pull request #24 from lay295/master
superbonaci Feb 13, 2024
98a3771
Cleanup
ScrubN Feb 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions TwitchDownloaderCLI/Modes/Arguments/TsMergeArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using CommandLine;

namespace TwitchDownloaderCLI.Modes.Arguments
{
[Verb("tsmerge", HelpText = "Concatenates multiple .ts/.tsv/.tsa/.m2t/.m2ts (MPEG Transport Stream) files into a single file")]
public class TsMergeArgs : ITwitchDownloaderArgs
{
[Option('i', "input", Required = true, HelpText = "Path a text file containing the absolute paths of the files to concatenate, separated by newlines. M3U/M3U8 is also supported.")]
public string InputList { get; set; }

[Option('o', "output", Required = true, HelpText = "Path to output file.")]
public string OutputFile { get; set; }

[Option("banner", Default = true, HelpText = "Displays a banner containing version and copyright information.")]
public bool? ShowBanner { get; set; }
}
}
33 changes: 33 additions & 0 deletions TwitchDownloaderCLI/Modes/MergeTs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Threading;
using TwitchDownloaderCLI.Modes.Arguments;
using TwitchDownloaderCLI.Tools;
using TwitchDownloaderCore;
using TwitchDownloaderCore.Options;

namespace TwitchDownloaderCLI.Modes
{
internal static class MergeTs
{
internal static void Merge(TsMergeArgs inputOptions)
{
Progress<ProgressReport> progress = new();
progress.ProgressChanged += ProgressHandler.Progress_ProgressChanged;

var mergeOptions = GetMergeOptions(inputOptions);
TsMerger tsMerger = new(mergeOptions, progress);
tsMerger.MergeAsync(new CancellationToken()).Wait();
}

private static TsMergeOptions GetMergeOptions(TsMergeArgs inputOptions)
{
TsMergeOptions mergeOptions = new()
{
OutputFile = inputOptions.OutputFile,
InputFile = inputOptions.InputList
};

return mergeOptions;
}
}
}
5 changes: 3 additions & 2 deletions TwitchDownloaderCLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private static void Main(string[] args)
config.HelpWriter = null; // Use null instead of TextWriter.Null due to how CommandLine works internally
});

var parserResult = parser.ParseArguments<VideoDownloadArgs, ClipDownloadArgs, ChatDownloadArgs, ChatUpdateArgs, ChatRenderArgs, FfmpegArgs, CacheArgs>(preParsedArgs);
var parserResult = parser.ParseArguments<VideoDownloadArgs, ClipDownloadArgs, ChatDownloadArgs, ChatUpdateArgs, ChatRenderArgs, FfmpegArgs, CacheArgs, TsMergeArgs>(preParsedArgs);
parserResult.WithNotParsed(errors => WriteHelpText(errors, parserResult, parser.Settings));

CoreLicensor.EnsureFilesExist(AppContext.BaseDirectory);
Expand All @@ -37,7 +37,8 @@ private static void Main(string[] args)
.WithParsed<ChatUpdateArgs>(UpdateChat.Update)
.WithParsed<ChatRenderArgs>(RenderChat.Render)
.WithParsed<FfmpegArgs>(FfmpegHandler.ParseArgs)
.WithParsed<CacheArgs>(CacheHandler.ParseArgs);
.WithParsed<CacheArgs>(CacheHandler.ParseArgs)
.WithParsed<TsMergeArgs>(MergeTs.Merge);
}

private static void WriteHelpText(IEnumerable<Error> errors, ParserResult<object> parserResult, ParserSettings parserSettings)
Expand Down
24 changes: 24 additions & 0 deletions TwitchDownloaderCLI/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# TwitchDownloaderCLI
A cross platform command line tool that can do the main functions of the GUI program, which can download VODs/Clips/Chats and render chats.
Also can concatenate/combine/merge Transport Stream files, either those parts downloaded with the CLI itself or from another source.

- [TwitchDownloaderCLI](#twitchdownloadercli)
- [Arguments for mode videodownload](#arguments-for-mode-videodownload)
Expand All @@ -9,6 +10,7 @@ A cross platform command line tool that can do the main functions of the GUI pro
- [Arguments for mode chatrender](#arguments-for-mode-chatrender)
- [Arguments for mode ffmpeg](#arguments-for-mode-ffmpeg)
- [Arguments for mode cache](#arguments-for-mode-cache)
- [Arguments for mode tsmerge](#arguments-for-mode-tsmerge)
- [Example Commands](#example-commands)
- [Additional Notes](#additional-notes)

Expand Down Expand Up @@ -343,6 +345,18 @@ Other = `1`, Broadcaster = `2`, Moderator = `4`, VIP = `8`, Subscriber = `16`, P
**--banner**
(Default: `true`) Displays a banner containing version and copyright information.

## Arguments for mode tsmerge
#### Concatenates multiple .ts/.tsv/.tsa/.m2t/.m2ts (MPEG Transport Stream) files into a single file

**-i / --input (REQUIRED)**
Path a text file containing the absolute paths of the files to concatenate, separated by newlines. M3U/M3U8 is also supported.

**-o / --output (REQUIRED)**
File the program will output to.

**--banner**
(Default: `true`) Displays a banner containing version and copyright information.

---

## Example Commands
Expand Down Expand Up @@ -394,6 +408,10 @@ Clear the default TwitchDownloader cache folder

./TwitchDownloaderCLI cache --clear

Concatenate several ts files into a single output file

TwitchDownloaderCLI tsmerge -i list.txt -o output.ts

Print the available operations

./TwitchDownloaderCLI help
Expand All @@ -415,3 +433,9 @@ Default true boolean flags must be assigned: `--default-true-flag=false`. Defaul
For Linux users, ensure both `fontconfig` and `libfontconfig1` are installed. `apt-get install fontconfig libfontconfig1` on Ubuntu.

Some distros, like Linux Alpine, lack fonts for some languages (Arabic, Persian, Thai, etc.) If this is the case for you, install additional fonts families such as [Noto](https://fonts.google.com/noto/specimen/Noto+Sans) or check your distro's wiki page on fonts as it may have an install command for this specific scenario, such as the [Linux Alpine](https://wiki.alpinelinux.org/wiki/Fonts) font page.

The list file for tsmerge can contain relative or absolute paths, but the path format differs from Windows to Linux/UNIX/MacOS (like volume paths or "/" instead of "\" to separate directories).
The list must be a text file and describe one ts path per line.

The concatenation made by tsmerge is not a raw (binary) concatenation, like `dd`, `cat` or `pv` would do on Linux, or `copy /b` on Windows.
Instead, the streams have to be partially recoded, to keep its structure valid and playable. Using other software may not achieve this result.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should be functionally equivalent to cat/copy /b, no? It quite literally opens a file, copies the bytes to a destination, then closes the file:

 await using (var fs = File.Open(partFile, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    await fs.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
}

And I'm not sure why this says the ts files are partially recoded? We rely solely on FFmpeg for video encoding, and this function doesn't ever call FFmpeg.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not a raw concatenation, the resulting file has different size than the sum of its parts.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using cat *.ts > cat.ts under WSL I get the exact same result as I do from CombineVideoParts.
image
image

In order to achieve this result with cat, however, I had to pad the filenames with zeroes so they were enumerated in the correct order.

Copy link
Contributor Author

@superbonaci superbonaci Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've downloaded all the parts and both match too. It's a corner case, I don't have anymore the test stream which caused the issue, I think it was for paid subscribers only and maybe it's removed because it's more than 2 months old.

Anyway I've fixed it already, I'm sure the issue will happen again eventually with some other video, maybe it's the older ones only, no idea about that. What I'm most worried now, is that ffmpeg converts ts files to mp4 with random stream errors, this is how I came about to test the parts concatenation in the first place, and I've found that the mp4/mkv can be fault for those 2 reasons:

  • bad ts concatenation
  • bad transcoding from output.ts to mp4

I've already asked you to implement the feature to keep ts parts for manual concatenation (apart from automatic one): #844 which would help me a lot to make some batch testing. please take this as high priority.

If you want me to implement the feature I could try and submit another PR.

If I find another stream which does not concatenate ts parts correctly in raw mode I'll tell about it don't worry.

8 changes: 8 additions & 0 deletions TwitchDownloaderCore/Options/TsMergeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TwitchDownloaderCore.Options
{
public class TsMergeOptions
{
public string OutputFile { get; set; }
public string InputFile { get; set; }
}
}
136 changes: 136 additions & 0 deletions TwitchDownloaderCore/TsMerger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using TwitchDownloaderCore.Options;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderCore
{
public sealed class TsMerger
{
private readonly TsMergeOptions mergeOptions;
private readonly IProgress<ProgressReport> _progress;

public TsMerger(TsMergeOptions tsMergeOptions, IProgress<ProgressReport> progress)
{
mergeOptions = tsMergeOptions;
_progress = progress;
}

public async Task MergeAsync(CancellationToken cancellationToken)
{
if (!File.Exists(mergeOptions.InputFile))
{
throw new FileNotFoundException("Input file does not exist");
}

var fileList = new List<string>();
await using (var fs = File.Open(mergeOptions.InputFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using var sr = new StreamReader(fs);
while (await sr.ReadLineAsync() is { } line)
{
if (string.IsNullOrWhiteSpace(line))
continue;

if (line.StartsWith('#'))
continue;

fileList.Add(line);
}
}

_progress.Report(new ProgressReport(ReportType.SameLineStatus, "Verifying Parts 0% [1/2]"));

await VerifyVideoParts(fileList, cancellationToken);

_progress.Report(new ProgressReport() { ReportType = ReportType.NewLineStatus, Data = "Combining Parts 0% [2/2]" });

await CombineVideoParts(fileList, cancellationToken);

_progress.Report(new ProgressReport(100));
}

private async Task VerifyVideoParts(IReadOnlyCollection<string> fileList, CancellationToken cancellationToken)
{
var failedParts = new List<string>();
var partCount = fileList.Count;
var doneCount = 0;

foreach (var part in fileList)
{
var isValidTs = await VerifyVideoPart(part);
if (!isValidTs)
{
failedParts.Add(part);
}

doneCount++;
var percent = (int)(doneCount / (double)partCount * 100);
_progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Verifying Parts {percent}% [1/2]"));
_progress.Report(new ProgressReport(percent));

cancellationToken.ThrowIfCancellationRequested();
}

if (failedParts.Count != 0)
{
if (failedParts.Count == fileList.Count)
{
// Every video part returned corrupted, probably a false positive.
return;
}

_progress.Report(new ProgressReport(ReportType.Log, $"The following TS files appear to be invalid or corrupted: {string.Join(", ", failedParts)}"));
}
}

private static async Task<bool> VerifyVideoPart(string filePath)
{
const int TS_PACKET_LENGTH = 188; // MPEG TS packets are made of a header and a body: [ 4B ][ 184B ] - https://tsduck.io/download/docs/mpegts-introduction.pdf

if (!File.Exists(filePath))
{
return false;
}

await using var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var fileLength = fs.Length;
if (fileLength == 0 || fileLength % TS_PACKET_LENGTH != 0)
{
return false;
}

return true;
}

private async Task CombineVideoParts(IReadOnlyCollection<string> fileList, CancellationToken cancellationToken)
{
DriveInfo outputDrive = DriveHelper.GetOutputDrive(mergeOptions.OutputFile);
string outputFile = mergeOptions.OutputFile;

int partCount = fileList.Count;
int doneCount = 0;

await using var outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write, FileShare.Read);
foreach (var partFile in fileList)
{
await DriveHelper.WaitForDrive(outputDrive, _progress, cancellationToken);

await using (var fs = File.Open(partFile, FileMode.Open, FileAccess.Read, FileShare.Read))
{
await fs.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
}

doneCount++;
int percent = (int)(doneCount / (double)partCount * 100);
_progress.Report(new ProgressReport(ReportType.SameLineStatus, $"Combining Parts {percent}% [2/2]"));
_progress.Report(new ProgressReport(percent));

cancellationToken.ThrowIfCancellationRequested();
}
}
}
}