/// <summary> /// Returns the version information from FFmpeg. /// </summary> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>A IFFmpegProcess object containing the version information.</returns> public string GetVersion(ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); Worker.OutputType = ProcessOutput.Output; Worker.RunEncoder("-version", EncoderApp.FFmpeg); return(Worker.Output); }
/// <summary> /// Creates a new process worker to run an encoder with specified options. /// </summary> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The newly created encoder process manager.</returns> public virtual IProcessWorkerEncoder CreateEncoder(ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { var Result = new ProcessWorkerEncoder(Config, ProcessFactory, FileSystemService, ParserFactory, options); if (callback != null) { Result.ProcessStarted += callback; } return(Result); }
/// <summary> /// Parses the output of X264 or X265 to return the info of all input streams. /// </summary> /// <param name="outputText">The text containing the file information to parse.</param> public void ParseFileInfo(string outputText, ProcessOptionsEncoder options) { IsParsed = true; if (options?.FrameCount > 0) { FrameCount = options.FrameCount; } else { FrameCount = ParseFrameCount(outputText); } }
/// <summary> /// Returns the exact frame count of specified video file. /// </summary> /// <param name="source">The file to get information about.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The number of frames in the video.</returns> public long GetFrameCount(string source, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(source)) { throw new ArgumentException("Source cannot be null or empty.", nameof(source)); } long Result = 0; IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); Worker.ProgressReceived += (sender, e) => { // Read all status lines and keep the last one. Result = (e.Progress as ProgressStatusFFmpeg).Frame; }; Worker.RunEncoder($@"-i ""{source}"" -f null /dev/null", EncoderApp.FFmpeg); return(Result); }
/// <summary> /// Runs vspipe with specified source file. The output will be discarded. /// </summary> /// <param name="path">The path to the script to run.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus RunVapourSynth(string path, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path cannot be null or empty.", nameof(path)); } if (!fileSystem.Exists(factory.Config.VsPipePath)) { throw new System.IO.FileNotFoundException($@"File ""{factory.Config.VsPipePath}"" specified by Config.VsPipePath is not found."); } string Args = $@"""{path}"" ."; IProcessWorker Manager = factory.Create(options, callback); CompletionStatus Result = Manager.Run(factory.Config.VsPipePath, Args); return(Result); }
/// <summary> /// Gets file streams information of specified file via FFmpeg. /// </summary> /// <param name="source">The file to get information about.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>A IFFmpegProcess object containing the file information.</returns> public IFileInfoFFmpeg GetFileInfo(string source, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(source)) { throw new ArgumentException("Source cannot be null or empty.", nameof(source)); } IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); Worker.ProcessCompleted += (s, e) => { if (e.Status == CompletionStatus.Failed && (Worker.FileInfo as IFileInfoFFmpeg)?.FileStreams != null) { e.Status = CompletionStatus.Success; } }; Worker.RunEncoder($@"-i ""{source}""", EncoderApp.FFmpeg); return(Worker.FileInfo as IFileInfoFFmpeg); }
/// <summary> /// Runs avs2pipemod with specified source file. The output will be discarded. /// </summary> /// <param name="path">The path to the script to run.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus RunAvisynth(string path, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path cannot be null or empty.", nameof(path)); } if (!fileSystem.Exists(factory.Config.Avs2PipeMod)) { throw new System.IO.FileNotFoundException($@"File ""{factory.Config.Avs2PipeMod}"" specified by Config.Avs2PipeModPath is not found."); } string Args = $@"""{path}"" -rawvideo > NUL"; IProcessWorker Manager = factory.Create(options, callback); Manager.OutputType = ProcessOutput.Error; string Cmd = $@"""{factory.Config.Avs2PipeMod}"" {Args}"; CompletionStatus Result = Manager.RunAsCommand(Cmd); return(Result); }
/// <summary> /// Returns stream information as FFmpegStream about specified media file that can be used to call a muxing operation. /// </summary> /// <param name="path">The path of the media file to query.</param> /// <param name="streamType">The type of media stream to query.</param> /// <param name="options">The options for starting the process.</param> /// <returns>A FFmpegStream object.</returns> private MediaStream GetStreamInfo(string path, FFmpegStreamType streamType, ProcessOptionsEncoder options) { IFileInfoFFmpeg FileInfo = infoReader.GetFileInfo(path, options); MediaStreamInfo StreamInfo = FileInfo.FileStreams?.FirstOrDefault(x => x.StreamType == streamType); if (StreamInfo != null) { return(new MediaStream() { Path = path, Index = StreamInfo.Index, Format = StreamInfo.Format, Type = streamType }); } else { return(null); } }
/// <summary> /// Converts specified file into AVI UT Video format. /// </summary> /// <param name="source">The file to convert.</param> /// <param name="destination">The destination file, ending with .AVI</param> /// <param name="audio">Whether to encode audio.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus ConvertToAviUtVideo(string source, string destination, bool audio, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { // -vcodec huffyuv or utvideo, -acodec pcm_s16le return(EncodeFFmpeg(source, destination, "utvideo", audio ? "pcm_s16le" : null, null, options, callback)); }
/// <summary> /// Merges specified audio and video files. /// </summary> /// <param name="videoFile">The file containing the video.</param> /// <param name="audioFile">The file containing the audio.</param> /// <param name="destination">The destination file.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus Muxe(string videoFile, string audioFile, string destination, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(videoFile) && string.IsNullOrEmpty(audioFile)) { throw new ArgumentException("You must specifiy either videoFile or audioFile.", nameof(videoFile)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); } List <MediaStream> InputStreamList = new List <MediaStream>(); MediaStream InputStream; if (!string.IsNullOrEmpty(videoFile)) { InputStream = GetStreamInfo(videoFile, FFmpegStreamType.Video, options); if (InputStream != null) { InputStreamList.Add(InputStream); } } if (!string.IsNullOrEmpty(audioFile)) { InputStream = GetStreamInfo(audioFile, FFmpegStreamType.Audio, options); if (InputStream != null) { InputStreamList.Add(InputStream); } } if (InputStreamList.Any()) { return(Muxe(InputStreamList, destination, options, callback)); } else { return(CompletionStatus.Failed); } }
/// <summary> /// Merges the specified list of file streams. /// </summary> /// <param name="fileStreams">The list of file streams to include in the output.</param> /// <param name="destination">The destination file.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus Muxe(IEnumerable <MediaStream> fileStreams, string destination, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (fileStreams == null || !fileStreams.Any()) { throw new ArgumentException("FileStreams cannot be null or empty.", nameof(fileStreams)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); } CompletionStatus Result = CompletionStatus.Success; List <string> TempFiles = new List <string>(); fileSystem.Delete(destination); // FFMPEG fails to muxe H264 into MKV container. Converting to MP4 and then muxing with the audio, however, works. foreach (MediaStream item in fileStreams) { if (string.IsNullOrEmpty(item.Path) && item.Type != FFmpegStreamType.None) { throw new ArgumentException("FFmpegStream.Path cannot be null or empty.", nameof(item.Path)); } if (item.Type == FFmpegStreamType.Video && (item.Format == "h264" || item.Format == "h265") && destination.EndsWith(".mkv")) { string NewFile = item.Path.Substring(0, item.Path.LastIndexOf('.')) + ".mp4"; Result = Muxe(new List <MediaStream>() { item }, NewFile, options); TempFiles.Add(NewFile); if (Result != CompletionStatus.Success) { break; } } } if (Result == CompletionStatus.Success) { // Join audio and video files. StringBuilder Query = new StringBuilder(); StringBuilder Map = new StringBuilder(); Query.Append("-y "); int StreamIndex = 0; bool HasVideo = false, HasAudio = false, HasPcmDvdAudio = false; StringBuilder AacFix = new StringBuilder(); var FileStreamsOrdered = fileStreams.OrderBy(f => f.Type); foreach (MediaStream item in FileStreamsOrdered) { if (item.Type == FFmpegStreamType.Video) { HasVideo = true; } if (item.Type == FFmpegStreamType.Audio) { HasAudio = true; if (item.Format == "aac") { AacFix.AppendFormat("-bsf:{0} aac_adtstoasc ", StreamIndex); } if (item.Format == "pcm_dvd") { HasPcmDvdAudio = true; } } Query.Append("-i \""); Query.Append(item.Path); Query.Append("\" "); Map.Append("-map "); Map.Append(StreamIndex++); Map.Append(":"); Map.Append(item.Index); Map.Append(" "); } if (!HasVideo && !HasAudio) { throw new ArgumentException("FileStreams cannot be empty.", nameof(fileStreams)); } if (HasVideo) { Query.Append("-vcodec copy "); } if (HasAudio) { Query.Append(HasPcmDvdAudio ? "-acodec pcm_s16le " : "-acodec copy "); } Query.Append(Map); // FFMPEG-encoded AAC streams are invalid and require an extra flag to join. if (AacFix.Length > 0 && HasVideo) { Query.Append(AacFix); } Query.Append("\""); Query.Append(destination); Query.Append("\""); IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); Result = Worker.RunEncoder(Query.ToString(), EncoderApp.FFmpeg); } // Delete temp file. foreach (string item in TempFiles) { fileSystem.Delete(item); } return(Result); }
/// <summary> /// Parses the output of FFmpeg to return the info of all input streams. /// </summary> /// <param name="outputText">The text containing the file information to parse.</param> public void ParseFileInfo(string outputText, ProcessOptionsEncoder options) { IsParsed = true; FileDuration = new TimeSpan(); FileStreams = new List <MediaStreamInfo>(); if (string.IsNullOrEmpty(outputText)) { return; } string[] OutLines = outputText.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); // Find duration line. int DurationIndex = -1; for (int i = 0; i < OutLines.Length; i++) { if (OutLines[i].StartsWith(" Duration: ")) { DurationIndex = i; // Parse duration line. string[] DurationInfo = OutLines[i].Trim().Split(new string[] { ", " }, StringSplitOptions.None); string DurationString = DurationInfo[0].Split(' ')[1]; if (DurationString == "N/A") { FileDuration = new TimeSpan(0); } else { try { FileDuration = TimeSpan.Parse(DurationString, CultureInfo.InvariantCulture); } catch { } } break; } } // Find input streams. MediaStreamInfo ItemInfo; for (int i = DurationIndex + 1; i < OutLines.Length; i++) { if (OutLines[i].StartsWith(" Stream #0:")) { // Parse input stream. ItemInfo = ParseStreamInfo(OutLines[i]); if (ItemInfo != null) { FileStreams.Add(ItemInfo); } } else if (OutLines[i].StartsWith("Output ")) { break; } } // Calculate FrameCount. if (options?.FrameCount > 0) { FrameCount = options.FrameCount; } else if (VideoStream != null) { FrameCount = (long)(FileDuration.TotalSeconds * VideoStream.FrameRate); } }
private CompletionStatus EncodeX264Internal(SourceType sourceType, EncoderApp encoderApp, string source, string destination, string encodeArgs, ProcessOptionsEncoder options, ProcessStartedEventHandler callback) { if (string.IsNullOrEmpty(source)) { throw new ArgumentException("Source cannot be null or empty.", nameof(source)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); } File.Delete(destination); StringBuilder Query = new StringBuilder(); if (sourceType != SourceType.Direct) { Query.AppendFormat("--{0}y4m ", encoderApp == EncoderApp.x264 ? "demuxer " : ""); } if (!string.IsNullOrEmpty(encodeArgs)) { Query.Append($"{encodeArgs} "); } Query.Append($@"-o ""{destination}"" "); if (sourceType == SourceType.Direct) { Query.Append($@"""{source}"""); } else { Query.Append("-"); } // Run X264 or X265 with query. IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); CompletionStatus Result = RunEncoderInternal(source, Query.ToString(), Worker, sourceType, encoderApp); return(Result); }
public ProcessWorkerEncoder(IMediaConfig config, IProcessFactory processFactory, IFileSystemService fileSystemService, IFileInfoParserFactory parserFactory, ProcessOptionsEncoder options = null) : base(config, processFactory, fileSystemService, options ?? new ProcessOptionsEncoder()) { this.parserFactory = parserFactory ?? throw new ArgumentNullException(nameof(parserFactory)); OutputType = ProcessOutput.Error; }
/// <summary> /// Encodes a VapourSynth script file using X265 with specified arguments. /// </summary> /// <param name="source">The file to convert.</param> /// <param name="destination">The destination file.</param> /// <param name="encodeArgs">Additional arguments to pass to X265.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus EncodeVapourSynthToX265(string source, string destination, string encodeArgs, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { return(EncodeX264Internal(SourceType.VapourSynth, EncoderApp.x265, source, destination, encodeArgs, options, callback)); }
private CompletionStatus EncodeFFmpegInternal(SourceType sourceType, string source, string destination, string videoCodec, string audioCodec, string encodeArgs, ProcessOptionsEncoder options, ProcessStartedEventHandler callback) { if (string.IsNullOrEmpty(source)) { throw new ArgumentException("Source cannot be null or empty.", nameof(source)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); } if (string.IsNullOrEmpty(videoCodec) && string.IsNullOrEmpty(audioCodec)) { throw new ArgumentException("You must specify at least one video or audio codec."); } File.Delete(destination); StringBuilder Query = new StringBuilder(); Query.Append("-y -i "); if (sourceType == SourceType.Direct) { Query.Append("\""); Query.Append(source); Query.Append("\""); } else { Query.Append("-"); // Pipe source } // Add video codec. if (string.IsNullOrEmpty(videoCodec)) { Query.Append(" -vn"); } else { Query.Append($" -vcodec {videoCodec}"); } // Add audio codec. if (string.IsNullOrEmpty(audioCodec)) { Query.Append(" -an"); } else { Query.Append($" -acodec {audioCodec}"); } if (!string.IsNullOrEmpty(encodeArgs)) { Query.Append(" "); Query.Append(encodeArgs); } Query.Append(" \""); Query.Append(destination); Query.Append("\""); // Run FFmpeg with query. IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); CompletionStatus Result = RunEncoderInternal(source, Query.ToString(), Worker, sourceType, EncoderApp.FFmpeg); return(Result); }
/// <summary> /// Encodes a VapourSynth script file using FFmpeg with specified arguments. /// </summary> /// <param name="source">The file to convert.</param> /// <param name="destination">The destination file.</param> /// <param name="videoCodec">The codec to use to encode the video stream.</param> /// <param name="audioCodec">The codec( to use to encode the audio stream.</param> /// <param name="encodeArgs">Additional arguments to pass to FFmpeg.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus EncodeVapourSynthToFFmpeg(string source, string destination, string videoCodec, string audioCodec, string encodeArgs, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { return(EncodeFFmpegInternal(SourceType.VapourSynth, source, destination, videoCodec, audioCodec, encodeArgs, options, callback)); }
/// <summary> /// Extracts the audio stream from specified file. /// </summary> /// <param name="source">The media file to extract from.</param> /// <param name="destination">The destination file.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus ExtractAudio(string source, string destination, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { return(ExtractStream(@"-y -i ""{0}"" -vn -acodec copy ""{1}""", source, destination, options, callback)); }
/// <summary> /// Truncates a media file from specified start position with specified duration. This can result in data loss or corruption if not splitting exactly on a framekey. /// </summary> /// <param name="source">The source file to truncate.</param> /// <param name="destination">The output file to write.</param> /// <param name="startPos">The position where to start copying. Anything before this position will be ignored. TimeSpan.Zero or null to start from beginning.</param> /// <param name="duration">The duration after which to stop copying. Anything after this duration will be ignored. TimeSpan.Zero or null to copy until the end.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus Truncate(string source, string destination, TimeSpan?startPos, TimeSpan?duration = null, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(source)) { throw new ArgumentException("Source cannot be empty.", nameof(source)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be empty.", nameof(destination)); } fileSystem.Delete(destination); IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); string Args = string.Format(@"-i ""{0}"" -vcodec copy -acodec copy {1}{2}""{3}""", source, startPos.HasValue && startPos > TimeSpan.Zero ? $"-ss {startPos:c} " : "", duration.HasValue && duration > TimeSpan.Zero ? $"-t {duration:c} " : "", destination); return(Worker.RunEncoder(Args, EncoderApp.FFmpeg)); }
/// <summary> /// Concatenates (merges) all specified files. /// </summary> /// <param name="files">The files to merge.</param> /// <param name="destination">The destination file.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> public CompletionStatus Concatenate(IEnumerable <string> files, string destination, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (files == null || !files.Any()) { throw new ArgumentException("Files cannot be null or empty.", nameof(files)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); } CompletionStatus Result = CompletionStatus.None; // Write temp file. string TempFile = fileSystem.GetTempFile(); StringBuilder TempContent = new StringBuilder(); foreach (string item in files) { TempContent.AppendFormat("file '{0}'", item).AppendLine(); } fileSystem.WriteAllText(TempFile, TempContent.ToString()); string Query = $@"-y -f concat -fflags +genpts -async 1 -safe 0 -i ""{TempFile}"" -c copy ""{destination}"""; IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); Result = Worker.RunEncoder(Query.ToString(), EncoderApp.FFmpeg); fileSystem.Delete(TempFile); return(Result); }
/// <summary> /// Extracts an audio or video stream from specified file. /// </summary> /// <param name="args">The arguments string that will be passed to FFmpeg.</param> /// <param name="source">The media file to extract from.</param> /// <param name="destination">The destination file.</param> /// <param name="options">The options for starting the process.</param> /// <param name="callback">A method that will be called after the process has been started.</param> /// <returns>The process completion status.</returns> private CompletionStatus ExtractStream(string args, string source, string destination, ProcessOptionsEncoder options = null, ProcessStartedEventHandler callback = null) { if (string.IsNullOrEmpty(source)) { throw new ArgumentException("Source cannot be empty.", nameof(source)); } if (string.IsNullOrEmpty(destination)) { throw new ArgumentException("Destination cannot be empty.", nameof(destination)); } fileSystem.Delete(destination); IProcessWorkerEncoder Worker = factory.CreateEncoder(options, callback); return(Worker.RunEncoder(string.Format(args, source, destination), EncoderApp.FFmpeg)); }