2024-05-04 15:35:32 +00:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using System.Threading.Tasks ;
using CRD.Utils.Structs ;
namespace CRD.Utils.Muxing ;
public class Merger {
private MergerOptions options ;
public Merger ( MergerOptions options ) {
this . options = options ;
if ( this . options . SkipSubMux ! = null & & this . options . SkipSubMux = = true ) {
this . options . Subtitles = new List < SubtitleInput > ( ) ;
}
if ( this . options . VideoTitle ! = null & & this . options . VideoTitle . Length > 0 ) {
this . options . VideoTitle = this . options . VideoTitle . Replace ( "\"" , "'" ) ;
}
}
public string FFmpeg ( ) {
List < string > args = new List < string > ( ) ;
List < string > metaData = new List < string > ( ) ;
var index = 0 ;
var audioIndex = 0 ;
var hasVideo = false ;
if ( ! options . mp3 ) {
foreach ( var vid in options . VideoAndAudio ) {
2024-05-22 21:22:02 +00:00
if ( vid . Delay ! = null & & hasVideo ) {
args . Add ( $"-itsoffset -{Math.Ceiling((double)vid.Delay * 1000)}ms" ) ;
}
2024-05-04 15:35:32 +00:00
2024-05-22 21:22:02 +00:00
args . Add ( $"-i \" { vid . Path } \ "" ) ;
if ( ! hasVideo | | options . KeepAllVideos = = true ) {
metaData . Add ( $"-map {index}:a -map {index}:v" ) ;
metaData . Add ( $"-metadata:s:a:{audioIndex} language={vid.Language.Code}" ) ;
metaData . Add ( $"-metadata:s:v:{index} title=\" { options . VideoTitle } \ "" ) ;
hasVideo = true ;
} else {
metaData . Add ( $"-map {index}:a" ) ;
metaData . Add ( $"-metadata:s:a:{audioIndex} language={vid.Language.Code}" ) ;
}
audioIndex + + ;
index + + ;
2024-05-04 15:35:32 +00:00
}
2024-05-22 21:22:02 +00:00
foreach ( var vid in options . OnlyVid ) {
if ( ! hasVideo | | options . KeepAllVideos = = true ) {
args . Add ( $"-i \" { vid . Path } \ "" ) ;
metaData . Add ( $"-map {index} -map -{index}:a" ) ;
metaData . Add ( $"-metadata:s:v:{index} title=\" { options . VideoTitle } \ "" ) ;
hasVideo = true ;
index + + ;
}
}
2024-05-04 15:35:32 +00:00
2024-05-22 21:22:02 +00:00
foreach ( var aud in options . OnlyAudio ) {
args . Add ( $"-i \" { aud . Path } \ "" ) ;
metaData . Add ( $"-map {index}" ) ;
metaData . Add ( $"-metadata:s:a:{audioIndex} language={aud.Language.Code}" ) ;
2024-05-04 15:35:32 +00:00
index + + ;
2024-05-22 21:22:02 +00:00
audioIndex + + ;
2024-05-04 15:35:32 +00:00
}
2024-05-22 21:22:02 +00:00
foreach ( var sub in options . Subtitles . Select ( ( value , i ) = > new { value , i } ) ) {
if ( sub . value . Delay ! = null ) {
args . Add ( $"-itsoffset -{Math.Ceiling((double)sub.value.Delay * 1000)}ms" ) ;
}
2024-05-04 15:35:32 +00:00
2024-05-22 21:22:02 +00:00
args . Add ( $"-i \" { sub . value . File } \ "" ) ;
2024-05-04 15:35:32 +00:00
}
2024-05-22 21:22:02 +00:00
if ( options . Output . EndsWith ( ".mkv" , StringComparison . OrdinalIgnoreCase ) ) {
if ( options . Fonts ! = null ) {
int fontIndex = 0 ;
foreach ( var font in options . Fonts ) {
args . Add ( $"-attach {font.Path} -metadata:s:t:{fontIndex} mimetype={font.Mime}" ) ;
fontIndex + + ;
}
2024-05-04 15:35:32 +00:00
}
}
2024-05-22 21:22:02 +00:00
args . AddRange ( metaData ) ;
args . AddRange ( options . Subtitles . Select ( ( sub , subIndex ) = > $"-map {subIndex + index}" ) ) ;
args . Add ( "-c:v copy" ) ;
args . Add ( "-c:a copy" ) ;
args . Add ( options . Output . EndsWith ( ".mp4" , StringComparison . OrdinalIgnoreCase ) ? "-c:s mov_text" : "-c:s ass" ) ;
args . AddRange ( options . Subtitles . Select ( ( sub , subindex ) = >
$"-metadata:s:s:{subindex} title=\" { sub . Language . Language ? ? sub . Language . Name } { ( sub . ClosedCaption = = true ? $" {options.CcTag}" : "" ) } { ( sub . Signs = = true ? " Signs" : "" ) } \ " -metadata:s:s:{subindex} language={sub.Language.Code}" ) ) ;
if ( options . Options . ffmpeg ? . Count > 0 ) {
args . AddRange ( options . Options . ffmpeg ) ;
}
args . Add ( $"\" { options . Output } \ "" ) ;
return string . Join ( " " , args ) ;
2024-05-04 15:35:32 +00:00
}
2024-05-22 21:22:02 +00:00
foreach ( var aud in options . OnlyAudio ) {
args . Add ( $"-i \" { aud . Path } \ "" ) ;
metaData . Add ( $"-map {index}" ) ;
metaData . Add ( $"-metadata:s:a:{audioIndex} language={aud.Language.Code}" ) ;
index + + ;
audioIndex + + ;
}
2024-05-04 15:35:32 +00:00
args . Add ( "-acodec libmp3lame" ) ;
args . Add ( "-ab 192k" ) ;
args . Add ( $"\" { options . Output } \ "" ) ;
return string . Join ( " " , args ) ;
}
public string MkvMerge ( ) {
List < string > args = new List < string > ( ) ;
bool hasVideo = false ;
args . Add ( $"-o \" { options . Output } \ "" ) ;
if ( options . Options . mkvmerge ! = null ) {
args . AddRange ( options . Options . mkvmerge ) ;
}
foreach ( var vid in options . OnlyVid ) {
if ( ! hasVideo | | options . KeepAllVideos = = true ) {
args . Add ( "--video-tracks 0" ) ;
args . Add ( "--no-audio" ) ;
string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}{(options.Simul == true ? " [ Simulcast ] " : " [ Uncut ] ")}" ;
args . Add ( $"--track-name 0:\" { trackName } \ "" ) ;
args . Add ( $"--language 0:{vid.Language.Code}" ) ;
hasVideo = true ;
args . Add ( $"\" { vid . Path } \ "" ) ;
}
}
foreach ( var vid in options . VideoAndAudio ) {
string audioTrackNum = options . InverseTrackOrder = = true ? "0" : "1" ;
string videoTrackNum = options . InverseTrackOrder = = true ? "1" : "0" ;
if ( vid . Delay . HasValue ) {
double delay = vid . Delay ? ? 0 ;
args . Add ( $"--sync {audioTrackNum}:-{Math.Ceiling(delay * 1000)}" ) ;
}
if ( ! hasVideo | | options . KeepAllVideos = = true ) {
args . Add ( $"--video-tracks {videoTrackNum}" ) ;
args . Add ( $"--audio-tracks {audioTrackNum}" ) ;
string trackName = $"{(options.VideoTitle ?? vid.Language.Name)}{(options.Simul == true ? " [ Simulcast ] " : " [ Uncut ] ")}" ;
args . Add ( $"--track-name 0:\" { trackName } \ "" ) ; // Assuming trackName applies to video if present
args . Add ( $"--language {audioTrackNum}:{vid.Language.Code}" ) ;
if ( options . Defaults . Audio . Code = = vid . Language . Code ) {
args . Add ( $"--default-track {audioTrackNum}" ) ;
} else {
args . Add ( $"--default-track {audioTrackNum}:0" ) ;
}
hasVideo = true ;
} else {
args . Add ( "--no-video" ) ;
args . Add ( $"--audio-tracks {audioTrackNum}" ) ;
if ( options . Defaults . Audio . Code = = vid . Language . Code ) {
args . Add ( $"--default-track {audioTrackNum}" ) ;
} else {
args . Add ( $"--default-track {audioTrackNum}:0" ) ;
}
args . Add ( $"--track-name {audioTrackNum}:\" { vid . Language . Name } \ "" ) ;
args . Add ( $"--language {audioTrackNum}:{vid.Language.Code}" ) ;
}
args . Add ( $"\" { vid . Path } \ "" ) ;
}
foreach ( var aud in options . OnlyAudio ) {
string trackName = aud . Language . Name ;
args . Add ( $"--track-name 0:\" { trackName } \ "" ) ;
args . Add ( $"--language 0:{aud.Language.Code}" ) ;
args . Add ( "--no-video" ) ;
args . Add ( "--audio-tracks 0" ) ;
if ( options . Defaults . Audio . Code = = aud . Language . Code ) {
args . Add ( "--default-track 0" ) ;
} else {
args . Add ( "--default-track 0:0" ) ;
}
args . Add ( $"\" { aud . Path } \ "" ) ;
}
if ( options . Subtitles . Count > 0 ) {
foreach ( var subObj in options . Subtitles ) {
if ( subObj . Delay . HasValue ) {
double delay = subObj . Delay ? ? 0 ;
args . Add ( $"--sync 0:-{Math.Ceiling(delay * 1000)}" ) ;
}
string trackNameExtra = subObj . ClosedCaption = = true ? $" {options.CcTag}" : "" ;
trackNameExtra + = subObj . Signs = = true ? " Signs" : "" ;
string trackName = $"0:\" { ( subObj . Language . Language ? ? subObj . Language . Name ) + trackNameExtra } \ "" ;
args . Add ( $"--track-name {trackName}" ) ;
args . Add ( $"--language 0:\" { subObj . Language . Code } \ "" ) ;
if ( options . Defaults . Sub . Code = = subObj . Language . Code & & subObj . ClosedCaption = = false ) {
args . Add ( "--default-track 0" ) ;
} else {
args . Add ( "--default-track 0:0" ) ;
}
args . Add ( $"\" { subObj . File } \ "" ) ;
}
} else {
args . Add ( "--no-subtitles" ) ;
}
if ( options . Fonts ! = null & & options . Fonts . Count > 0 ) {
foreach ( var font in options . Fonts ) {
args . Add ( $"--attachment-name \" { font . Name } \ "" ) ;
args . Add ( $"--attachment-mime-type \" { font . Mime } \ "" ) ;
args . Add ( $"--attach-file \" { font . Path } \ "" ) ;
}
} else {
args . Add ( "--no-attachments" ) ;
}
if ( options . Chapters ! = null & & options . Chapters . Count > 0 ) {
args . Add ( $"--chapters \" { options . Chapters [ 0 ] . Path } \ "" ) ;
}
return string . Join ( " " , args ) ;
}
// public async Task CreateDelays(){
// // Don't bother scanning if there is only 1 vna stream
// if (options.VideoAndAudio.Count > 1){
// var bin = await YamlCfg.LoadBinCfg();
// var vnas = this.options.VideoAndAudio;
/ /
// // Get and set durations on each videoAndAudio Stream
// foreach (var vna in vnas){
// var streamInfo = await FFProbe(vna.Path, bin.FFProbe);
// var videoInfo = streamInfo.Streams.Where(stream => stream.CodecType == "video").FirstOrDefault();
// vna.Duration = int.Parse(videoInfo.Duration);
// }
/ /
// // Sort videoAndAudio streams by duration (shortest first)
// vnas.Sort((a, b) => {
// if (a.Duration == 0 || b.Duration == 0) return -1;
// return a.Duration.CompareTo(b.Duration);
// });
/ /
// // Set Delays
// var shortestDuration = vnas[0].Duration;
// foreach (var (vna, index) in vnas.Select((vna, index) => (vna, index))){
// // Don't calculate the shortestDuration track
// if (index == 0){
// if (!vna.IsPrimary)
// Console.WriteLine("Shortest video isn't primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.");
// continue;
// }
/ /
// if (vna.Duration > 0 && shortestDuration > 0){
// // Calculate the tracks delay
// vna.Delay = Math.Ceiling((vna.Duration - shortestDuration) * 1000) / 1000;
/ /
// var subtitles = this.options.Subtitles.Where(sub => sub.Language.Code == vna.Lang.Code).ToList();
// foreach (var (sub, subIndex) in subtitles.Select((sub, subIndex) => (sub, subIndex))){
// if (vna.IsPrimary)
// subtitles[subIndex].Delay = vna.Delay;
// else if (sub.ClosedCaption)
// subtitles[subIndex].Delay = vna.Delay;
// }
// }
// }
// }
// }
public async Task Merge ( string type , string bin ) {
string command = type switch {
"ffmpeg" = > FFmpeg ( ) ,
"mkvmerge" = > MkvMerge ( ) ,
_ = > ""
} ;
if ( string . IsNullOrEmpty ( command ) ) {
Console . WriteLine ( "Unable to merge files." ) ;
return ;
}
Console . WriteLine ( $"[{type}] Started merging" ) ;
var result = await Helpers . ExecuteCommandAsync ( type , bin , command ) ;
if ( ! result . IsOk & & type = = "mkvmerge" & & result . ErrorCode = = 1 ) {
Console . WriteLine ( $"[{type}] Mkvmerge finished with at least one warning" ) ;
} else if ( ! result . IsOk ) {
Console . WriteLine ( $"[{type}] Merging failed with exit code {result.ErrorCode}" ) ;
} else {
Console . WriteLine ( $"[{type} Done]" ) ;
}
}
public void CleanUp ( ) {
// Combine all media file lists and iterate through them
var allMediaFiles = options . OnlyAudio . Concat ( options . OnlyVid )
. Concat ( options . VideoAndAudio ) . ToList ( ) ;
allMediaFiles . ForEach ( file = > DeleteFile ( file . Path ) ) ;
allMediaFiles . ForEach ( file = > DeleteFile ( file . Path + ".resume" ) ) ;
// Delete chapter files if any
options . Chapters ? . ForEach ( chapter = > DeleteFile ( chapter . Path ) ) ;
// Delete subtitle files
options . Subtitles . ForEach ( subtitle = > DeleteFile ( subtitle . File ) ) ;
}
private void DeleteFile ( string filePath ) {
try {
if ( File . Exists ( filePath ) ) {
File . Delete ( filePath ) ;
}
} catch ( Exception ex ) {
Console . WriteLine ( $"Failed to delete file {filePath}. Error: {ex.Message}" ) ;
// Handle exceptions if you need to log them or throw
}
}
}
public class MergerInput {
public string Path { get ; set ; }
public LanguageItem Language { get ; set ; }
public int? Duration { get ; set ; }
public int? Delay { get ; set ; }
public bool? IsPrimary { get ; set ; }
}
public class SubtitleInput {
public LanguageItem Language { get ; set ; }
public string File { get ; set ; }
public bool? ClosedCaption { get ; set ; }
public bool? Signs { get ; set ; }
public int? Delay { get ; set ; }
}
public class ParsedFont {
public string Name { get ; set ; }
public string Path { get ; set ; }
public string Mime { get ; set ; }
}
public class CrunchyMuxOptions {
public string Output { get ; set ; }
public bool? SkipSubMux { get ; set ; }
public bool? KeepAllVideos { get ; set ; }
public bool? Novids { get ; set ; }
public bool Mp4 { get ; set ; }
public string ForceMuxer { get ; set ; }
public bool? NoCleanup { get ; set ; }
public string VideoTitle { get ; set ; }
public List < string > FfmpegOptions { get ; set ; } = new List < string > ( ) ;
public List < string > MkvmergeOptions { get ; set ; } = new List < string > ( ) ;
public LanguageItem DefaultSub { get ; set ; }
public LanguageItem DefaultAudio { get ; set ; }
public string CcTag { get ; set ; }
public bool SyncTiming { get ; set ; }
}
public class MergerOptions {
public List < MergerInput > VideoAndAudio { get ; set ; } = new List < MergerInput > ( ) ;
public List < MergerInput > OnlyVid { get ; set ; } = new List < MergerInput > ( ) ;
public List < MergerInput > OnlyAudio { get ; set ; } = new List < MergerInput > ( ) ;
public List < SubtitleInput > Subtitles { get ; set ; } = new List < SubtitleInput > ( ) ;
public List < MergerInput > Chapters { get ; set ; } = new List < MergerInput > ( ) ;
public string CcTag { get ; set ; }
public string Output { get ; set ; }
public string VideoTitle { get ; set ; }
public bool? Simul { get ; set ; }
public bool? InverseTrackOrder { get ; set ; }
public bool? KeepAllVideos { get ; set ; }
public List < ParsedFont > Fonts { get ; set ; } = new List < ParsedFont > ( ) ;
public bool? SkipSubMux { get ; set ; }
public MuxOptions Options { get ; set ; }
public Defaults Defaults { get ; set ; }
2024-05-22 21:22:02 +00:00
2024-05-04 15:35:32 +00:00
public bool mp3 { get ; set ; }
}
public class MuxOptions {
public List < string > ? ffmpeg { get ; set ; }
public List < string > ? mkvmerge { get ; set ; }
}
public class Defaults {
public LanguageItem Audio { get ; set ; }
public LanguageItem Sub { get ; set ; }
}