private void ScanWithComskip() { string parameters = ""; if (_convOptions.comskipIni != "") // Check for custom Ini file { if (File.Exists(_convOptions.comskipIni)) { parameters += "--ini=" + Util.FilePaths.FixSpaces(_convOptions.comskipIni); } else { _jobLog.WriteEntry(this, Localise.GetPhrase("Custom Comskip INI file does not exist, Skipping custom INI"), Log.LogEntryType.Warning); } } parameters += " --output=" + Util.FilePaths.FixSpaces(_convOptions.workingPath); // Redirect all files to working folder (save issues with skipping copying original files and simulatenous scanning on same file) parameters += " " + Util.FilePaths.FixSpaces(_videoFileName); float Duration = 0; Duration = VideoParams.VideoDuration(_videoFileName); if (Duration <= 0) { FFmpegMediaInfo mediaInfo = new FFmpegMediaInfo(_videoFileName, _jobStatus, _jobLog); if (!mediaInfo.Success || mediaInfo.ParseError) { _jobLog.WriteEntry(this, Localise.GetPhrase("Cannot read video duration"), Log.LogEntryType.Error); } if (mediaInfo.MediaInfo.VideoInfo.Duration == 0) { _jobLog.WriteEntry(this, Localise.GetPhrase("Video duration 0"), Log.LogEntryType.Warning); } else { Duration = mediaInfo.MediaInfo.VideoInfo.Duration; } } // Check for custom version of Comskip path Comskip comskip = new Comskip(_customComskipPath, parameters, Duration, _jobStatus, _jobLog); // Use custom comskip or fallback to default comskip.Run(); if (!comskip.Success || !(File.Exists(WorkingEDLFilePath) || File.Exists(WorkingEDLPFilePath))) // Check if the EDL/EDLP file exists (here it is in working path), % does not always work { _jobLog.WriteEntry(this, "Comskip failed or no output EDL/EDLP file found", Log.LogEntryType.Warning); _jobStatus.PercentageComplete = 0; } }
public bool Trim(string sourceVideo, string workingPath, int startTrim, int endTrim) { _jobLog.WriteEntry(this, "Start Trim : " + startTrim.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Stop Trim : " + endTrim.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Video File : " + sourceVideo, Log.LogEntryType.Debug); if ((startTrim == 0) && (endTrim == 0)) { _trimmedVideo = sourceVideo; // It's the same file return(true); // nothing to do here } string tempFile = Path.Combine(workingPath, Path.GetFileNameWithoutExtension(sourceVideo) + "-temp" + Util.FilePaths.CleanExt(sourceVideo)); // Get the length of the video, needed to calculate end point float Duration; Duration = VideoParams.VideoDuration(sourceVideo); FFmpegMediaInfo ffmpegStreamInfo = new FFmpegMediaInfo(sourceVideo, _jobStatus, _jobLog); if (!ffmpegStreamInfo.Success || ffmpegStreamInfo.ParseError) { _jobLog.WriteEntry(this, "Cannot read video info", Log.LogEntryType.Error); return(false); } if (Duration <= 0) { // Converted file should contain only 1 audio stream Duration = ffmpegStreamInfo.MediaInfo.VideoInfo.Duration; _jobLog.WriteEntry(this, "Video duration : " + Duration.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Information); if (Duration == 0) { _jobLog.WriteEntry(this, "Video duration 0", Log.LogEntryType.Error); return(false); } } // dont' use threads here since we are copying video to improve stability string ffmpegParams = "-y"; ffmpegParams += " -i " + Util.FilePaths.FixSpaces(sourceVideo); // While setting the start trim before the input file (FAST seek) can speed up seeking and (http://ffmpeg.org/trac/ffmpeg/wiki/Seeking%20with%20FFmpeg) FAST seek (since accurate seek cuts on a NON KeyFrame which causes Audio Sync Issues) // Due to ffmpeg ticket #3252 we need to use ACCURATE seek (trim after input file) to avoid PTS<DTS error if (startTrim != 0) { if (startTrim < Duration) { ffmpegParams += " -ss " + startTrim.ToString(System.Globalization.CultureInfo.InvariantCulture); } else { _jobLog.WriteEntry(this, "Start trim (" + startTrim.ToString() + ") greater than file duration (" + Duration.ToString(System.Globalization.CultureInfo.InvariantCulture) + "). Skipping start trimming.", Log.LogEntryType.Warning); startTrim = 0; // Skip it } } // Set the end trim (calculate from reducing from video length) if (endTrim != 0) { // FFMPEG can specify duration of encoding, i.e. encoding_duration = stopTime - startTime // startTime = startTrim, stopTime = video_duration - endTrim int encDuration = (((int)Duration) - endTrim) - (startTrim); // by default _startTrim is 0 if (encDuration > 0) { ffmpegParams += " -t " + encDuration.ToString(System.Globalization.CultureInfo.InvariantCulture); } else { _jobLog.WriteEntry(this, "End trim (" + endTrim.ToString() + ") + Start trim (" + startTrim.ToString() + ") greater than file duration (" + Duration.ToString(System.Globalization.CultureInfo.InvariantCulture) + "). Skipping end trimming.", Log.LogEntryType.Warning); endTrim = 0; } } // Sanity check once more if ((startTrim == 0) && (endTrim == 0)) { _jobLog.WriteEntry(this, "Start trim and end trim skipped. Skipping trimming.", Log.LogEntryType.Warning); _trimmedVideo = sourceVideo; // It's the same file return(true); // nothing to do here } // Check for audio channels if (ffmpegStreamInfo.AudioTracks > 0) { ffmpegParams += " -map 0:a -acodec copy"; } else { ffmpegParams += " -an"; } // Check for video stream if (ffmpegStreamInfo.MediaInfo.VideoInfo.Stream != -1) { ffmpegParams += " -map 0:" + ffmpegStreamInfo.MediaInfo.VideoInfo.Stream.ToString() + " -vcodec copy"; // Fix for FFMPEG WTV MJPEG ticket #2227 } else { ffmpegParams += " -vn"; } //Output file ffmpegParams += " " + Util.FilePaths.FixSpaces(tempFile); if (!FFmpeg.FFMpegExecuteAndHandleErrors(ffmpegParams, _jobStatus, _jobLog, Util.FilePaths.FixSpaces(tempFile))) //check of file is created, outputhandler reports success (Percentage not requires since Success is more accurate) { _jobLog.WriteEntry("Failed to trim video at " + _jobStatus.PercentageComplete.ToString(System.Globalization.CultureInfo.InvariantCulture) + "%", Log.LogEntryType.Error); return(false); } // Replace the file if (File.Exists(tempFile)) { _jobLog.WriteEntry(this, "TrimVideo trying to replace file Source : " + sourceVideo + " Temp : " + tempFile, Log.LogEntryType.Debug); // If the original uncut file is in the working temp directory, then just replace it if (Path.GetDirectoryName(sourceVideo).ToLower() == workingPath.ToLower()) { Util.FileIO.TryFileReplace(sourceVideo, tempFile); } else // If the original uncut video is not in the working temp directory, then just rename the tempFile with the original name and keep in the temp working directory (don't mangle original video file) { FileIO.TryFileDelete(Path.Combine(workingPath, Path.GetFileName(sourceVideo))); // Just in case it exists FileIO.MoveAndInheritPermissions(tempFile, Path.Combine(workingPath, Path.GetFileName(sourceVideo))); } _trimmedVideo = Path.Combine(workingPath, Path.GetFileName(sourceVideo)); // The final cut video always lies in the working directory } else { _jobLog.WriteEntry(this, "TrimVideo cannot find temp file " + tempFile, Log.LogEntryType.Error); _jobStatus.PercentageComplete = 0; return(false); } return(true); // All good here }
/// <summary> /// Extract closed captions from the source file into the temp path with same filename and SRT format /// </summary> /// <param name="sourceFile">Original file</param> /// <param name="workingPath">Temp folder</param> /// <param name="ccOptions">CC extraction options</param> /// <param name="startTrim">Trim initial</param> /// <param name="endTrim">Trim end</param> /// <param name="ccOffset">Offset initial</param> /// <returns>True if successful</returns> public bool ExtractCC(string sourceFile, string workingPath, string ccOptions, int startTrim, int endTrim, double ccOffset) { // TODO: Do we need process WTV files separately with -wtvmpeg2 option and also Teletext and DVB with -teletext and -codec dvb options? _jobLog.WriteEntry(this, ("Extracting Closed Captions as SRT file"), Log.LogEntryType.Information); _jobLog.WriteEntry(this, "Source File : " + sourceFile, Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Working Path " + workingPath, Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "CC Options : " + ccOptions, Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Start Trim : " + startTrim.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Stop Trim : " + endTrim.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Offset : " + ccOffset.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (String.IsNullOrWhiteSpace(ccOptions)) { return(true); // nothing to do, accidentally called } // Output SRT file has to be working directory, will be copied to output afterwards string tmpExtractedSRTFile = Path.Combine(workingPath, Path.GetFileNameWithoutExtension(sourceFile)) + ".srt"; string ccExtractorParams = ""; string field = ""; string channel = ""; if (ccOptions.ToLower() != "default") // let ccextrator choose defaults if specified { // CCOptions are encoded as field,channel field = ccOptions.Split(',')[0]; channel = ccOptions.Split(',')[1]; } if (field == "1" || field == "2") { ccExtractorParams += " -" + field; // Field is -1 or -2 (we don't support -12 since it creates 2 SRT files and there's no way to handle that) } if (channel == "2") { ccExtractorParams += " -cc2"; // By default is Channel 1, there is no parameter for it } // NOTE: delay does not work reliably do not use // Adjust for any offset required during extraction (opposite direction, so -ve) if (ccOffset != 0) { ccExtractorParams += " -delay " + (-ccOffset * 1000).ToString(CultureInfo.InvariantCulture); // ccOffset is in seconds while -delay required milliseconds } // Get the length of the video, needed to calculate end point float Duration = 0; Duration = VideoParams.VideoDuration(sourceFile); if (Duration <= 0) { FFmpegMediaInfo ffmpegStreamInfo = new FFmpegMediaInfo(sourceFile, _jobStatus, _jobLog); if (ffmpegStreamInfo.Success && !ffmpegStreamInfo.ParseError) { // Converted file should contain only 1 audio stream Duration = ffmpegStreamInfo.MediaInfo.VideoInfo.Duration; _jobLog.WriteEntry(this, ("Video duration") + " : " + Duration.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Information); if (Duration == 0) { _jobLog.WriteEntry(this, ("Video duration 0"), Log.LogEntryType.Error); return(false); } } else { _jobLog.WriteEntry(this, ("Cannot read video duration"), Log.LogEntryType.Error); return(false); } } // Set the start trim time (it only accepts +ve) if (startTrim > 0) { ccExtractorParams += " -startat " + TimeSpan.FromSeconds((double)startTrim).ToString(); } else if (startTrim < 0) { _jobLog.WriteEntry(this, "Skipping start trim since it's negative", Log.LogEntryType.Warning); } // Set the end trim time if (endTrim > 0) { // startTime = startTrim, stopTime = video_duration - endTrim int encDuration = (((int)Duration) - endTrim) - (startTrim); // by default _startTrim is 0 ccExtractorParams += " -endat " + TimeSpan.FromSeconds((double)encDuration).ToString(); } else if (endTrim < 0) { _jobLog.WriteEntry(this, "Skipping end trim since it's negative", Log.LogEntryType.Warning); } // Set the input file ccExtractorParams += " " + Util.FilePaths.FixSpaces(sourceFile); // set output file ccExtractorParams += " -o " + Util.FilePaths.FixSpaces(tmpExtractedSRTFile); // Run the command CCExtractor ccExtractor = new CCExtractor(ccExtractorParams, _jobStatus, _jobLog); ccExtractor.Run(); if (!ccExtractor.Success) // check for termination/success { _jobLog.WriteEntry("CCExtractor failed. Disabling detection , retrying using TS format", Log.LogEntryType.Warning); // TODO: Right format to pick (TS/ES/PS etc) - doing TS for now // Sometimes it doesn't detect the format correctly so try to force it (TS) ccExtractorParams += " -ts"; ccExtractor = new CCExtractor(ccExtractorParams, _jobStatus, _jobLog); ccExtractor.Run(); if (!ccExtractor.Success) // check for termination/success { _jobLog.WriteEntry("CCExtractor failed to extract closed captions", Log.LogEntryType.Error); return(false); } } // Check if we have a format identification error (sometimes ccextractor misidentifies files) if ((Util.FileIO.FileSize(tmpExtractedSRTFile) <= 0) && ccExtractor.FormatReadingError) { FileIO.TryFileDelete(tmpExtractedSRTFile); // Delete the empty file _jobLog.WriteEntry(this, "No SRT file found and format error detected. CCExtractor may not have identified the format correctly, forcing TS format and retrying extraction.", Log.LogEntryType.Warning); ccExtractorParams += " -in=ts"; // force TS format and retry ccExtractor = new CCExtractor(ccExtractorParams, _jobStatus, _jobLog); ccExtractor.Run(); if (!ccExtractor.Success) // check for termination/success { _jobLog.WriteEntry(("Retrying: CCExtractor failed to extract closed captions"), Log.LogEntryType.Error); return(false); } } // Check for empty file if (Util.FileIO.FileSize(tmpExtractedSRTFile) <= 0) { FileIO.TryFileDelete(tmpExtractedSRTFile); // Delete the empty file _jobLog.WriteEntry(this, ("No valid SRT file found"), Log.LogEntryType.Warning); return(true); // no error } _extractedSRTFile = tmpExtractedSRTFile; // We got the file _jobLog.WriteEntry(this, ("Extracted closed captions to") + " " + _extractedSRTFile, Log.LogEntryType.Information); return(true); }
/// <summary> /// Updates the Video file properties structure, check for crop, audio and video information /// </summary> /// <param name="skipCropDetect">True to skip detecting cropping parameters</param> /// <param name="detectInterlace">Extracts the video intelacing type by analyzing it in depth</param> /// <param name="videoFileName">Path to Original Source Video</param> /// <param name="remuxedFileName">Path to Remuxed video, else null or empty string</param> /// <param name="edlFile">Path to EDL file else null or empty string</param> /// <param name="audioLanguage">Audio Language</param> /// <param name="jobStatus">JobStatus</param> /// <param name="jobLog">JobLog</param> public void UpdateVideoInfo(bool skipCropDetect, bool detectInterlace, string videoFileName, string remuxedFileName, string edlFile, string audioLanguage, JobStatus jobStatus, Log jobLog) { ResetParameters(); // Reset VideoInfo parameters _jobStatus = jobStatus; _jobLog = jobLog; _EDLFile = edlFile; _requestedAudioLanguage = audioLanguage; _originalFileName = videoFileName; _remuxedFileName = remuxedFileName; _skipCropDetect = skipCropDetect; _detectInterlacing = detectInterlace; _jobLog.WriteEntry(this, "Reading MediaInfo from " + SourceVideo, Log.LogEntryType.Information); _videoCodec = VideoParams.VideoFormat(SourceVideo); jobLog.WriteEntry(this, "Video Codec : " + _videoCodec, Log.LogEntryType.Debug); if (String.IsNullOrWhiteSpace(_videoCodec)) { _error = true; } _audioCodec = VideoParams.AudioFormat(SourceVideo); jobLog.WriteEntry(this, "Audio Codec : " + _audioCodec, Log.LogEntryType.Debug); if (String.IsNullOrWhiteSpace(_audioCodec)) { _error = true; } _fps = VideoParams.FPS(SourceVideo); jobLog.WriteEntry(this, "Video FPS : " + _fps.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (_fps <= 0) { _error = true; } _width = VideoParams.VideoWidth(SourceVideo); jobLog.WriteEntry(this, "Video Width : " + _width.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (_width <= 0) { _error = true; } _height = VideoParams.VideoHeight(SourceVideo); jobLog.WriteEntry(this, "Video Height : " + _height.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (_height <= 0) { _error = true; } _duration = VideoParams.VideoDuration(SourceVideo); jobLog.WriteEntry(this, "Video Duration : " + _duration.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (_duration <= 0) { _error = true; } _audioDelay = VideoParams.AudioDelay(SourceVideo); jobLog.WriteEntry(this, "Audio Delay : " + _audioDelay.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (_detectInterlacing) // Get Interlacing from FFMPEG more reliable then MediaInfo - avoid unnecessary cycles if not required { jobLog.WriteEntry(this, "Scan type unknown, trying with FFMPEGMediaInfo", Log.LogEntryType.Debug); _ffmpegStreamInfo = new FFmpegMediaInfo(SourceVideo, _jobStatus, _jobLog, 0, 0, _ignoreSuspend); // Run interlace detection with defaults if (_ffmpegStreamInfo.Success && !_ffmpegStreamInfo.ParseError) { // Now calcuate whether it's Interlaced or Progressive based on the Multi Frame Interlaced Detection Results long totalInterlaced = _ffmpegStreamInfo.MFInterlaceDetectionResults.BFF + _ffmpegStreamInfo.MFInterlaceDetectionResults.TFF; long totalProgressive = _ffmpegStreamInfo.MFInterlaceDetectionResults.Progressive; long totalUndetermined = _ffmpegStreamInfo.MFInterlaceDetectionResults.Undetermined; // TODO: What to do with this? if (totalInterlaced == 0 && totalProgressive == 0) // Boundary conditions { _scanType = ScanType.Unknown; } else if (totalInterlaced == 0) // Avoid divide by zero exception { _scanType = ScanType.Progressive; } else { double PtoIRatio = totalProgressive / totalInterlaced; // See below comment // Refer to this, how to tell if the video is interlaced or telecine // http://forum.videohelp.com/threads/295007-Interlaced-or-telecined-how-to-tell?p=1797771&viewfull=1#post1797771 // It is a statistical ratio, telecine has approx split of 3 progressive and 2 interlaced (i.e. ratio of about 1.5 progressive to interlaced) // TODO: We need to revisit the logic below for telecine, interlaced or progressive detection (check idet filter for updates ffmpeg ticket #3073) if ((totalProgressive == 0) && (totalProgressive == 0)) // Unknown - could not find { _scanType = ScanType.Unknown; } else if ((PtoIRatio > TELECINE_LOW_P_TO_I_RATIO) && (PtoIRatio < TELECINE_HIGH_P_TO_I_RATIO)) // Let us keep a band to measure telecine ratio, see comment above { _scanType = ScanType.Telecine; } else if (PtoIRatio <= TELECINE_LOW_P_TO_I_RATIO) // We play safe, more interlaced than progressive { _scanType = ScanType.Interlaced; } else if (PtoIRatio >= TELECINE_HIGH_P_TO_I_RATIO) // Progressive has the clear lead { _scanType = ScanType.Progressive; } else { _scanType = ScanType.Unknown; // No idea where we are } } jobLog.WriteEntry(this, "FFMPEG Video Scan Type : " + _scanType.ToString(), Log.LogEntryType.Debug); } else { jobLog.WriteEntry(this, "Error reading scan type from FFMPEGMediaInfo", Log.LogEntryType.Warning); } if (_scanType == ScanType.Unknown) // If we couldn't get it from FFMPEG lets try MediaInfo as a backup { _scanType = VideoParams.VideoScanType(SourceVideo); jobLog.WriteEntry(this, " MediaInfo Video Scan Type : " + _scanType.ToString(), Log.LogEntryType.Debug); } } // We don't get AudioChannel information here as it interfers with FFMPEG /*mi.Option("Inform", "Audio; %Channels%"); * int.TryParse(mi.Inform(), out _audioChannels); * jobLog.WriteEntry(this, "Audio Channels : " + _audioChannels.ToString(System.Globalization.CultureInfo.InvariantCulture), Log.LogEntryType.Debug);*/ // Supplement with extracting Video and Audio information (sometimes MediaInfo fails) using FFMPEG and selected the Audio Language specified _jobLog.WriteEntry(this, "Supplementing Media information using FFMPEG", Log.LogEntryType.Information); _ffmpegStreamInfo = new FFmpegMediaInfo(SourceVideo, _jobStatus, _jobLog, _ignoreSuspend); // this may be called from the UI request if (_ffmpegStreamInfo.Success && !_ffmpegStreamInfo.ParseError) { // Store the video information (there's only 1 video per file) _width = _ffmpegStreamInfo.MediaInfo.VideoInfo.Width; _height = _ffmpegStreamInfo.MediaInfo.VideoInfo.Height; if ((_fps <= 0) || ((_fps > _ffmpegStreamInfo.MediaInfo.VideoInfo.FPS) && (_ffmpegStreamInfo.MediaInfo.VideoInfo.FPS > 0))) // Check _fps, sometimes MediaInfo get it below 0 or too high (most times it's reliable) { _fps = _ffmpegStreamInfo.MediaInfo.VideoInfo.FPS; } else { _ffmpegStreamInfo.MediaInfo.VideoInfo.FPS = _fps; // Store the value from MediaInfo, more reliable } _duration = _ffmpegStreamInfo.MediaInfo.VideoInfo.Duration; _videoCodec = _ffmpegStreamInfo.MediaInfo.VideoInfo.VideoCodec; _videoStream = _ffmpegStreamInfo.MediaInfo.VideoInfo.Stream; _videoPID = _ffmpegStreamInfo.MediaInfo.VideoInfo.PID; // video PID // Default Check if all audio streams have same codec, if so populate the field for later use during reading profiles for (int i = 0; i < _ffmpegStreamInfo.AudioTracks; i++) { if (i == 0) { _audioCodec = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].AudioCodec; // baseline the codec name } else if (_audioCodec != _ffmpegStreamInfo.MediaInfo.AudioInfo[i].AudioCodec) { _audioCodec = ""; // All codecs are not the same, reset it and let the encoder figure it out break; // we're done here } } // Default check if all audio streams have same channels, if so populate the field for later use during reading profiles for (int i = 0; i < _ffmpegStreamInfo.AudioTracks; i++) { if (i == 0) { _audioChannels = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels; // baseline the channels } else if (_audioChannels != _ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels) { _audioChannels = 0; // All channels are not the same, reset it and let the encoder figure it out break; // we're done here } } // Audio parameters - find the best Audio channel for the selected language or best audio track if there are imparired tracks otherwise by default the encoder will select the best audio channel (encoders do not do a good job of ignoring imparired tracks) bool selectedTrack = false; if ((!String.IsNullOrEmpty(_requestedAudioLanguage) || (_ffmpegStreamInfo.ImpariedAudioTrackCount > 0)) && (_ffmpegStreamInfo.AudioTracks > 1)) // More than 1 audio track to choose from and either we have a language match request or a presence of an imparied channel (likely no audio) { for (int i = 0; i < _ffmpegStreamInfo.AudioTracks; i++) { bool processTrack = false; // By default we don't need to process // Language selection check, if the user has picked a specific language code, look for it // If we find a match, we look the one with the highest number of channels in it if (!String.IsNullOrEmpty(_requestedAudioLanguage)) { if ((_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Language.ToLower() == _requestedAudioLanguage) && (_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels > 0)) { if (selectedTrack) { if (!( // take into account impaired tracks (since impaired tracks typically have no audio) ((_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels > _audioChannels) && !_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Impaired) || // PREFERENCE to non-imparied Audio tracks with the most channels ((_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels > _audioChannels) && _selectedAudioImpaired) || // PREFERENCE to Audio tracks with most channels if currently selected track is impaired (!_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Impaired && _selectedAudioImpaired) // PREFER non impaired audio over currently selected impaired )) { continue; // we have found a lang match, now we are looking for more channels only now } } processTrack = true; // All conditions met, we need to process this track } } else if (_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels > 0)// we have a imparied audio track, select the non impaired track with the highest number of tracks or bitrate or frequency { if (selectedTrack) { if (!( // take into account impaired tracks (since impaired tracks typically have no audio) ((_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels > _audioChannels) && !_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Impaired) || // PREFERENCE to non-imparied Audio tracks with the most channels ((_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels > _audioChannels) && _selectedAudioImpaired) || // PREFERENCE to Audio tracks with most channels if currently selected track is impaired (!_ffmpegStreamInfo.MediaInfo.AudioInfo[i].Impaired && _selectedAudioImpaired) // PREFER non impaired audio over currently selected impaired )) { continue; // we have found a lang match, now we are looking for more channels only now } } processTrack = true; // All conditions met, we need to process this track } if (processTrack) // We need to process this track { _audioChannels = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].Channels; _audioStream = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].Stream; // store the stream number for the selected audio channel _audioCodec = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].AudioCodec; _audioPID = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].PID; // Audio PID _audioTrack = i; // Store the audio track number we selected _audioLanguage = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].Language.ToLower(); // this is what we selected _selectedAudioImpaired = _ffmpegStreamInfo.MediaInfo.AudioInfo[i].Impaired; // Is this an imparied audio track? selectedTrack = true; // We found a suitable track if (!String.IsNullOrEmpty(_requestedAudioLanguage)) { _jobLog.WriteEntry(this, "Found Audio Language match for language" + " " + _requestedAudioLanguage.ToUpper() + ", " + "Audio Stream" + " " + _audioStream.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Audio Track") + " " + _audioTrack.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Channels") + " " + _audioChannels.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Codec") + " " + _audioCodec + " Impaired " + _selectedAudioImpaired.ToString(), Log.LogEntryType.Debug); } else { _jobLog.WriteEntry(this, "Compensating for audio impaired tracks, selected track with language" + " " + _requestedAudioLanguage.ToUpper() + ", " + "Audio Stream" + " " + _audioStream.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Audio Track") + " " + _audioTrack.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Channels") + " " + _audioChannels.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Codec") + " " + _audioCodec + " Impaired " + _selectedAudioImpaired.ToString(), Log.LogEntryType.Debug); } } } if (!selectedTrack) { _jobLog.WriteEntry(this, ("Could not find a match for selected Audio Language Code") + " " + _requestedAudioLanguage + ", letting encoder choose best audio language", Log.LogEntryType.Warning); } else { _jobLog.WriteEntry(this, ("Selected Audio Stream") + " " + _audioStream.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Audio Track") + " " + _audioTrack.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Channels") + " " + _audioChannels.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Codec") + " " + _audioCodec + ", Impaired " + _selectedAudioImpaired.ToString(), Log.LogEntryType.Debug); } } else if (_ffmpegStreamInfo.AudioTracks == 1) // We have just one audio track, then populate the information otherwise the encoding operations will have a hard time determining audio information { if (_ffmpegStreamInfo.MediaInfo.AudioInfo[0].Channels > 0) { _audioChannels = _ffmpegStreamInfo.MediaInfo.AudioInfo[0].Channels; _audioStream = _ffmpegStreamInfo.MediaInfo.AudioInfo[0].Stream; // store the stream number for the selected audio channel _audioCodec = _ffmpegStreamInfo.MediaInfo.AudioInfo[0].AudioCodec; _audioPID = _ffmpegStreamInfo.MediaInfo.AudioInfo[0].PID; // Audio PID _audioTrack = 0; // Store the audio track number we selected _audioLanguage = _ffmpegStreamInfo.MediaInfo.AudioInfo[0].Language.ToLower(); // this is what we selected _selectedAudioImpaired = _ffmpegStreamInfo.MediaInfo.AudioInfo[0].Impaired; // Is this an imparied audio track? _jobLog.WriteEntry(this, "Only one audio track present, " + ("Audio Stream") + " " + _audioStream.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Audio Track") + " " + _audioTrack.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Channels") + " " + _audioChannels.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Codec") + " " + _audioCodec + " Impaired " + _selectedAudioImpaired.ToString(), Log.LogEntryType.Debug); } } else { _jobLog.WriteEntry(this, ("Audio Stream") + " " + _audioStream.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Audio Track") + " " + _audioTrack.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Channels") + " " + _audioChannels.ToString(System.Globalization.CultureInfo.InvariantCulture) + ", " + ("Codec") + " " + _audioCodec + ", Impaired " + _selectedAudioImpaired.ToString(), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "No audio language selected, letting encoder choose best audio language", Log.LogEntryType.Warning); } _error = false; // all good now } else { _error = true; } if (_error) { _jobLog.WriteEntry(this, ("Unable to read media information using FFMPEG or MediaInfo"), Log.LogEntryType.Error); return; } // Get the video properties for the original video _jobLog.WriteEntry(this, "Reading Original File Media information", Log.LogEntryType.Information); _originalFileFFmpegStreamInfo = new FFmpegMediaInfo(_originalFileName, _jobStatus, _jobLog, _ignoreSuspend); if (!_originalFileFFmpegStreamInfo.Success || _originalFileFFmpegStreamInfo.ParseError) { _jobLog.WriteEntry(this, ("Unable to read media information using FFMPEG"), Log.LogEntryType.Warning); } if (_skipCropDetect) { _jobLog.WriteEntry(this, "Skipping crop information", Log.LogEntryType.Information); } else { UpdateCropInfo(jobLog); } }
/// <summary> /// Extracts subtitles from a video file into a SRT format (with same name) and cleans it up. /// It will overwrite any existing SRT files /// If there are multiple subtitles it extracts them into multiple files with incremental names. /// </summary> /// <param name="sourceFile">Path to video file</param> /// <param name="offset">Offset of the subtitles during extraction</param> /// <param name="overWrite">True to overwrite existing SRT files, false to create new ones with incremental names</param> /// <param name="languageExtractList">List of 3 digit language codes to extract (blank to extract all, unnamed languages will always be extracted)</param> /// <returns>True if successful</returns> public bool ExtractSubtitles(string sourceFile, string workingPath, int startTrim, int endTrim, double offset, bool overWrite, List <string> languageExtractList) { _jobLog.WriteEntry(this, ("Extracting Subtitles from " + sourceFile + " into SRT file"), Log.LogEntryType.Information); _jobLog.WriteEntry(this, "Source File : " + sourceFile, Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Working Path " + workingPath, Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Start Trim : " + startTrim.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Stop Trim : " + endTrim.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Debug); _jobLog.WriteEntry(this, "Offset : " + offset.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Debug); if (String.IsNullOrEmpty(sourceFile)) { return(true); // nothing to do } if (!File.Exists(sourceFile)) { _jobLog.WriteEntry(this, ("File does not exist " + sourceFile), Log.LogEntryType.Warning); return(true); //nothing to process } FFmpegMediaInfo mediaInfo = new FFmpegMediaInfo(sourceFile, _jobStatus, _jobLog); if (!mediaInfo.Success || mediaInfo.ParseError) { _jobLog.WriteEntry(this, ("Error reading subtitle info from file"), Log.LogEntryType.Error); return(false); } _jobLog.WriteEntry(this, "Found " + mediaInfo.SubtitleTracks.ToString() + " Subtitle tracks, extract only the first matching track", Log.LogEntryType.Debug); bool extractedSubtitle = false; for (int i = 0; i < mediaInfo.SubtitleTracks; i++) { if (extractedSubtitle) // Only extract and use one subtitle (sometimes chapter tracks are misidentified as subtitle tracks) { continue; } // Build the command line string parameters = ""; string outputSRTFile = ""; // Using Serviio subtitle filename format (filename.srt or filename_language.srt or filename_uniquenumber.srt) // Check for language comparison if required if (languageExtractList != null) { if (languageExtractList.Count > 0) // If list is empty, we extract all { _jobLog.WriteEntry(this, "Subtitle language extraction list -> " + String.Join(",", languageExtractList.ToArray()), Log.LogEntryType.Debug); if (!String.IsNullOrWhiteSpace(mediaInfo.MediaInfo.SubtitleInfo[i].Language)) // check if we have a language defined for this track { if (!languageExtractList.Contains(mediaInfo.MediaInfo.SubtitleInfo[i].Language)) // This language is not in the list of extraction { _jobLog.WriteEntry(this, "Skipping subtitle extraction since subtitle language >" + mediaInfo.MediaInfo.SubtitleInfo[i].Language + "< is NOT in the subtitle language list", Log.LogEntryType.Warning); continue; // Skip this subtitle track } } else { _jobLog.WriteEntry(this, "Extracting subtitle since there is no language defined for track", Log.LogEntryType.Debug); } } } // Check for existing SRT files if (overWrite) { outputSRTFile = Path.Combine(workingPath, Path.GetFileNameWithoutExtension(sourceFile)) + (i > 0 ? (String.IsNullOrWhiteSpace(mediaInfo.MediaInfo.SubtitleInfo[i].Language) ? "_" + i.ToString() : "_" + mediaInfo.MediaInfo.SubtitleInfo[i].Language) : "") + ".srt"; // First file user default name, then try to name with language first, if not give a unique number parameters += " -y"; } else // Create a unique SRT file name { int existingSRTCount = 0; outputSRTFile = Path.Combine(workingPath, Path.GetFileNameWithoutExtension(sourceFile)) + ".srt"; // Try default name while (File.Exists(outputSRTFile)) { _jobLog.WriteEntry(this, "Subtitle file " + outputSRTFile + " exists, creating a new unique SRT filename", Log.LogEntryType.Debug); outputSRTFile = Path.Combine(workingPath, Path.GetFileNameWithoutExtension(sourceFile)) + (String.IsNullOrWhiteSpace(mediaInfo.MediaInfo.SubtitleInfo[i].Language) ? "_" + existingSRTCount.ToString() : "_" + mediaInfo.MediaInfo.SubtitleInfo[i].Language + (existingSRTCount > 0 ? existingSRTCount.ToString() : "")) + ".srt"; // Create a unique SRT filename, try with language first, if not give a unique identifier, avoid a loop existingSRTCount++; } } // Build ffmpeg command line parameters += " -i " + FilePaths.FixSpaces(sourceFile); parameters += " -an -vn"; parameters += " -map 0:" + mediaInfo.MediaInfo.SubtitleInfo[i].Stream.ToString(); // Subtitle steam no parameters += " -scodec copy -copyinkf -f srt " + FilePaths.FixSpaces(outputSRTFile); // Now extract it _jobLog.WriteEntry(this, "Extracting Subtitle " + (i + 1).ToString() + " with language >" + mediaInfo.MediaInfo.SubtitleInfo[i].Language + "< to " + outputSRTFile, Log.LogEntryType.Debug); FFmpeg ffmpeg = new FFmpeg(parameters, _jobStatus, _jobLog); ffmpeg.Run(); if (!ffmpeg.Success) { FileIO.TryFileDelete(outputSRTFile); // Delete partial file // Backup, try using MP4Box instead to extract it _jobLog.WriteEntry(this, ("FFMPEG failed to extract subtitles into SRT file, retrying using MP4Box"), Log.LogEntryType.Warning); parameters = "-srt " + (mediaInfo.MediaInfo.SubtitleInfo[i].Stream + 1).ToString() + " " + FilePaths.FixSpaces(sourceFile); // MP4Box create an output file called <input>_<track>_text.srt // Check if the output srt exists and then rename it if it does string tempSrtOutput = FilePaths.GetFullPathWithoutExtension(sourceFile) + "_" + (mediaInfo.MediaInfo.SubtitleInfo[i].Stream + 1).ToString() + "_text.srt"; bool tempSrtExists = false; if (File.Exists(tempSrtOutput)) // Save the output srt filename if it exists { try { FileIO.MoveAndInheritPermissions(tempSrtOutput, tempSrtOutput + ".tmp"); } catch (Exception e) { _jobLog.WriteEntry(this, ("Error extracting subtitles into SRT file.\r\n" + e.ToString()), Log.LogEntryType.Error); return(false); } tempSrtExists = true; } // Extract the subtitle MP4Box mp4Box = new MP4Box(parameters, _jobStatus, _jobLog); mp4Box.Run(); if (!mp4Box.Success) { _jobLog.WriteEntry(this, ("Error extracting subtitles into SRT file"), Log.LogEntryType.Error); FileIO.TryFileDelete(tempSrtOutput); // Delete partial file if (tempSrtExists) { RestoreSavedSrt(tempSrtOutput); } return(false); } if (FileIO.FileSize(tempSrtOutput) <= 0) // MP4Box always return success even if nothing is extracted, so check if has been extracted { _jobLog.WriteEntry(this, "No or empty Subtitle file " + tempSrtOutput + " extracted by MP4Box, skipping", Log.LogEntryType.Debug); FileIO.TryFileDelete(tempSrtOutput); // Delete empty file } else { // Rename the temp output SRT to the expected name try { FileIO.MoveAndInheritPermissions(tempSrtOutput, outputSRTFile); } catch (Exception e) { _jobLog.WriteEntry(this, ("Error extracting subtitles into SRT file.\r\n" + e.ToString()), Log.LogEntryType.Error); FileIO.TryFileDelete(tempSrtOutput); // Delete partial file if (tempSrtExists) { RestoreSavedSrt(tempSrtOutput); } return(false); } } // Restore temp SRT file if it exists if (tempSrtExists) { RestoreSavedSrt(tempSrtOutput); } } if (FileIO.FileSize(outputSRTFile) <= 0) // Check for empty files { _jobLog.WriteEntry(this, "Empty Subtitle file " + outputSRTFile + " extracted, deleting it", Log.LogEntryType.Warning); FileIO.TryFileDelete(outputSRTFile); // Delete empty file } else { // Trim the SRT file if required if (startTrim > 0 || endTrim > 0) { // Get the length of the video, needed to calculate end point float Duration = 0; Duration = VideoParams.VideoDuration(sourceFile); if (Duration <= 0) { FFmpegMediaInfo ffmpegStreamInfo = new FFmpegMediaInfo(sourceFile, _jobStatus, _jobLog); if (ffmpegStreamInfo.Success && !ffmpegStreamInfo.ParseError) { // Converted file should contain only 1 audio stream Duration = ffmpegStreamInfo.MediaInfo.VideoInfo.Duration; _jobLog.WriteEntry(this, ("Video duration") + " : " + Duration.ToString(CultureInfo.InvariantCulture), Log.LogEntryType.Information); if (Duration == 0) { _jobLog.WriteEntry(this, ("Video duration 0"), Log.LogEntryType.Error); return(false); } } else { _jobLog.WriteEntry(this, ("Cannot read video duration"), Log.LogEntryType.Error); return(false); } } // Trim the subtitle if (!TrimSubtitle(outputSRTFile, workingPath, startTrim, endTrim, Duration, 0)) { _jobLog.WriteEntry(this, ("Error trimming SRT file"), Log.LogEntryType.Error); return(false); } } // Clean it up and offset the SRT if required if (!SRTValidateAndClean(outputSRTFile, offset)) { _jobLog.WriteEntry(this, ("Cannot clean and set offset for SRT file"), Log.LogEntryType.Error); return(false); } // Check for empty file if (Util.FileIO.FileSize(outputSRTFile) <= 0) { FileIO.TryFileDelete(outputSRTFile); // Delete the empty file _jobLog.WriteEntry(this, ("No valid SRT file found"), Log.LogEntryType.Warning); continue; // check for the next subtitle track } _extractedSRTFile = outputSRTFile; // Save it _jobLog.WriteEntry(this, "Extracted Subtitle file " + outputSRTFile + ", size [KB] " + (FileIO.FileSize(outputSRTFile) / 1024).ToString("N", CultureInfo.InvariantCulture), Log.LogEntryType.Debug); extractedSubtitle = true; // We have success } } return(true); }