Example #1
0
        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
        }
Example #2
0
        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());
        }