private void CleanTempData(bool keepCommand) { _logger.LogDebug($"Cleaning up the TEMP data{(keepCommand ? ", keeping the command file." : ".")}"); if (File.Exists(_tempFile)) { File.Delete(_tempFile); } if (!keepCommand && File.Exists(_shFile)) { File.Delete(_shFile); } if (!keepCommand && File.Exists(_batFile)) { File.Delete(_batFile); } _tempFile = null; TranscodingStarted = null; _lastTempFileSize = null; _stalledFileCandidate = null; _transcodingJob = null; // This kills the .OnError attached to transcoding job }
public async Task StartTranscodingAsync(ITranscodingJob transcodingJob, CancellationToken cancellationToken = default) { if (!File.Exists(transcodingJob.SourceFile)) { throw new EncoderException($"Cannot start transcoding. File {transcodingJob.SourceFile} does not exist."); } if (Busy) { _logger.LogWarning($"Cannot start another transcoding, encoder busy with {CurrentFile}. {transcodingJob.SourceFile} rejected."); return; } cancellationToken.Register(() => { _logger.LogDebug("Encoder cancelled from the outside"); Kill(); }); _transcodingJob = transcodingJob; if (_transcodingJob.Action == EncodingTargetFlags.None) { throw new Exception("No transcoding action selected"); } _busy = true; _logger.LogDebug("Getting FFMPEG File Model"); var fm = await GetFileModelAsync(CurrentFile); if (fm == null) { throw new Exception($"Could not get FFMPEG file model: {nameof(fm)} was null."); } var mainAudioStream = fm.MainAudioStream(_sofakingConfiguration.AudioLanguages); var ffmpeg = new Engine(_configuration.FFMPEGBinary); _logger.LogDebug("Preparing files"); _tempFile = Path.Combine(_sofakingConfiguration.TempFolder, Path.GetFileNameWithoutExtension(CurrentFile) + ".mkv"); _shFile = Path.Combine(_sofakingConfiguration.TempFolder, Path.GetFileNameWithoutExtension(CurrentFile) + ".sh"); _batFile = Path.Combine(_sofakingConfiguration.TempFolder, Path.GetFileNameWithoutExtension(CurrentFile) + ".bat"); // Discard all streams except those in _configuration.AudioLanguages // Reencode the english stream only, and add it as primary stream. Copy all other desirable audio languages from the list. _logger.LogDebug("Constructing the encoder command"); var a = new StringBuilder(); // Overwrite existing file a.Append("-y "); if (transcodingJob.UseCuda) { a.Append("-hwaccel cuda -hwaccel_output_format cuda "); } a.Append($"-i \"{CurrentFile}\" "); // Subtitles - input files if (transcodingJob.Subtitles.Count > 0) { foreach (var sub in transcodingJob.Subtitles) { // a.Append("-sub_charenc UTF_8 -i french.srt ") a.Append($"-i {sub.Value} "); } } a.Append("-c copy "); // Copy metadata a.Append("-map_metadata 0 "); // Video a.Append($"-map 0:v:0 -c:v "); if (_transcodingJob.Action.HasFlag(EncodingTargetFlags.NeedsNewVideo)) { // Resize? a.Append(_configuration.OutputVideoCodec + " " + (fm.MainVideoStream.HorizontalResolution > _sofakingConfiguration.MaxHorizontalVideoResolution ? $"-vf scale={_sofakingConfiguration.MaxHorizontalVideoResolution}:-2:flags=lanczos+accurate_rnd " : string.Empty)); // Make sure we have the right bitrate, important for PS4 compatibility a.Append($"-b:v {_configuration.OutputVideoBitrateMbits}M "); a.Append($"-maxrate {_configuration.OutputVideoBitrateMbits}M "); a.Append($"-bufsize {(int)Math.Ceiling(_configuration.OutputVideoBitrateMbits/2)}M "); // These arguments are here for improved compatibility. Level 4.0 is the lowest bandwidth required for FullHD - good for better compatibility and network streaming // http://blog.mediacoderhq.com/h264-profiles-and-levels/ a.Append("-profile:v high -level:v 4.2 -pix_fmt yuv420p "); // Get the highest quality possible a.Append("-preset veryslow "); } else { a.Append("copy "); } a.Append($"-tune {(_transcodingJob.Action.HasFlag(EncodingTargetFlags.VideoIsAnimation) ? "animation" : "film")} "); // Audio a.Append("-map -0:a "); // Drop all audio tracks, and then add only those we want below. var audioTrackCounter = 0; // Main track, transcode if needed a.Append($"-map {mainAudioStream.StreamId}:{mainAudioStream.StreamIndex} -c:a:{audioTrackCounter} {(_transcodingJob.Action.HasFlag(EncodingTargetFlags.NeedsNewAudio) ? $"{_configuration.OutputAudioCodec} -b:a:{audioTrackCounter} {_configuration.OutputAudioBitrateMbits}M" : "copy")} "); if (_transcodingJob.Action.HasFlag(EncodingTargetFlags.NeedsNewAudio)) { a.Append($"-metadata:s:a:{audioTrackCounter} title=\"{(!mainAudioStream.Metadata.ContainsKey("title") ? string.Empty : mainAudioStream.Metadata?["title"] + " ")}(PS4 Compatible)\" "); } a.Append($"-disposition:a:{audioTrackCounter} default "); audioTrackCounter++; // Copy the remaining audio tracks, as long as they are in the configured array of desired languages. foreach (var audioStream in fm.Streams.Where(x => x.StreamType == StreamTypeEnum.Audio && _sofakingConfiguration.AudioLanguages.Contains(x.Language))) { if (audioStream == mainAudioStream) { continue; } // TODO: Refactor this var hasAcceptableCodec = audioStream.StreamCodec.ToLower() == "ac3" || audioStream.StreamCodec.ToLower() == "aac" || audioStream.StreamCodec.ToLower().Contains("pcm"); a.Append($"-map {audioStream.StreamId}:{audioStream.StreamIndex} -c:a:{audioTrackCounter} {(!hasAcceptableCodec ? $"{_configuration.OutputAudioCodec} -b:a:{audioTrackCounter} {_configuration.OutputAudioBitrateMbits}M" : "copy")} "); if (!hasAcceptableCodec) { a.Append($"-metadata:s:a:{audioTrackCounter} title=\"{(!audioStream.Metadata.ContainsKey("title") ? string.Empty : audioStream.Metadata["title"] + " ")}(PS4 Compatible)\" "); } audioTrackCounter++; } // Subtitles - lang settings if (transcodingJob.Subtitles.Count > 0) { a.Append($"-map -0:s{optionalFlag} "); for (var i = 1; i < transcodingJob.Subtitles.Count + 1; i++) { a.Append($"-map {i} "); } var subtitleIndex = 0; foreach (var sub in transcodingJob.Subtitles) { a.Append($"-metadata:s:s:{subtitleIndex} language={sub.Key} "); // Could be three letters subtitleIndex++; } } else { // Keep the embeded subs only if no new subs had been provided. a.Append($"-map 0:s{optionalFlag} "); } // Prevents some errors. a.Append("-max_muxing_queue_size 9999 "); // Metadata // See: https://matroska.org/technical/specs/tagging/index.html a.Append($"-metadata COMMENT=\"Original file name: {Path.GetFileName(CurrentFile)}\" "); a.Append($"-metadata ENCODER_SETTINGS=\"V: {_configuration.OutputVideoCodec + (_transcodingJob.Action.HasFlag(EncodingTargetFlags.NeedsNewVideo) ? $"@{_configuration.OutputVideoBitrateMbits}M" : " (copy)")}, A:{_configuration.OutputAudioCodec}@{_configuration.OutputAudioBitrateMbits}M\" "); // Cover image if (!string.IsNullOrWhiteSpace(_transcodingJob.CoverImageJpg)) { a.Append($"-attach \"{_transcodingJob.CoverImageJpg}\" -metadata:s:t mimetype=image/jpeg "); } if (_transcodingJob.Metadata != null && _transcodingJob.Metadata.Count > 0) { foreach (var m in _transcodingJob.Metadata) { if (string.IsNullOrWhiteSpace(m.Value)) { continue; } if (m.Key == FFMPEGMetadataEnum.year) { if (int.TryParse(m.Value, out int year)) { a.Append($"-metadata DATE_RELEASED=\"{year}\" "); } continue; } if (m.Key == FFMPEGMetadataEnum.description) { if (int.TryParse(m.Value, out int year)) { a.Append($"-metadata SUMMARY=\"{year}\" "); } continue; } if (m.Key == FFMPEGMetadataEnum.IMDBRating) { if (double.TryParse(m.Value, out double imdbRating)) { // convert 10-based rating to a 5-based rated double ratingBaseFive = Math.Floor(((imdbRating / 10) * 5) * 10) / 10; a.Append($"-metadata RATING=\"{ratingBaseFive}\" "); } continue; } a.Append($"-metadata {Enum.GetName(typeof(FFMPEGMetadataEnum), m.Key).ToUpper()}=\"{m.Value}\" "); } } a.Append($"\"{_tempFile}\""); _logger.LogDebug("Setting up events"); ffmpeg.Progress += (object sender, ConversionProgressEventArgs e) => { PercentDone = ((double)e.ProcessedDuration.Ticks / (double)transcodingJob.Duration.Ticks) * 100d; OnProgress?.Invoke(this, new EncodingProgressEventArgs(PercentDone, CurrentFile, e.SizeKb, e.ProcessedDuration, e.Fps)); }; ffmpeg.Error += (object sender, ConversionErrorEventArgs e) => { _logger.LogError($"Encoding error {e.Exception.Message}", e.Exception); Kill(); OnError?.Invoke(this, new EncodingErrorEventArgs(e.Exception.Message)); }; ffmpeg.Complete += (object sender, ConversionCompleteEventArgs e) => { _logger.LogWarning($"Encoding complete! {CurrentFile}"); //if (_onSuccessInternal == null) //{ // _logger.LogError("No success action defined!"); // throw new EncoderException("No success action defined!"); //} //// DONT CHANGE THE ORDER OF CALLS BELOW!!! //_logger.LogWarning("A"); //_transcodingJob.OnComplete(); // Deletes the original source file //_logger.LogWarning("B"); //_onDoneInternal(); // WORKS //_logger.LogWarning("C"); //_onSuccessInternal(_tempFile); // Move the finished file, remove transcoding job from queue //_logger.LogWarning("D"); //CleanTempData(); //_logger.LogWarning("E"); //_busy = false; //_logger.LogWarning("F"); OnSuccess?.Invoke(this, new EncodingSuccessEventArgs(_tempFile)); _busy = false; }; _logger.LogDebug($"Adding a {_shFile} sh file for debugging."); // Make an .sh file with the same command for eventual debugging await File.WriteAllTextAsync(_shFile, _configuration.FFMPEGBinary + " " + a.ToString()); // For Windows... // TODO: Refactor paths _logger.LogDebug($"Adding a {_batFile} bat file for debugging."); var cmdWin = "ffmpeg -t 00:01:00 " + a.ToString().Replace("h264_omx", "h264").Replace("/mnt/hd1/", "Z:\\").Replace("/", "\\") + "\r\npause"; cmdWin = Regex.Replace(cmdWin, @"Z:\\TEMP\\Transcoding\\(?<FileName>[^""]+?\.mkv)", "C:\\Users\\ohmy\\Desktop\\FFMPEG\\${FileName}", RegexOptions.Singleline | RegexOptions.IgnoreCase); await File.WriteAllTextAsync(_batFile, cmdWin); _logger.LogDebug("Starting ffmpeg"); try { _ = ffmpeg.ExecuteAsync(a.ToString(), _cancellationTokenSource.Token); } catch (Exception ex) { _logger.LogError($"Starting FFMPEG failed! {ex.Message}", ex); Kill(); return; } TranscodingStarted = DateTime.Now; _logger.LogDebug("Ffmpeg started"); _stalledFileCandidate = null; StallMonitor(); OnStart?.Invoke(this, new EventArgs()); }