private static void QueueMergeJob(BaseJob job, IDbConnection connection, string outputFolder, string fileNameWithoutExtension, string fileExtension, JobRequest jobRequest) { ICollection <FfmpegPart> chunks = connection.Query <FfmpegPart>( "SELECT Filename, Number, Target, (SELECT VideoSourceFilename FROM FfmpegVideoRequest WHERE JobCorrelationId = @Id) AS VideoSourceFilename, Width, Height, Bitrate FROM FfmpegVideoParts WHERE JobCorrelationId = @Id ORDER BY Target, Number;", new { Id = job.JobCorrelationId }) .ToList(); foreach (IEnumerable <FfmpegPart> chunk in chunks.GroupBy(x => x.Target, x => x, (key, values) => values)) { var FfmpegVideoParts = chunk as IList <FfmpegPart> ?? chunk.ToList(); int targetNumber = FfmpegVideoParts.First().Target; DestinationFormat target = jobRequest.Targets[targetNumber]; string targetFilename = $@"{outputFolder}{Path.DirectorySeparatorChar}{fileNameWithoutExtension}_{target.Width}x{target.Height}_{target.VideoBitrate}_{target.AudioBitrate}{fileExtension}"; // TODO Implement proper detection if files are already merged if (File.Exists(targetFilename)) { continue; } string path = $"{outputFolder}{Path.DirectorySeparatorChar}{job.JobCorrelationId.ToString("N")}{Path.DirectorySeparatorChar}{fileNameWithoutExtension}_{targetNumber}.list"; using (TextWriter tw = new StreamWriter(path)) { foreach (FfmpegPart part in FfmpegVideoParts.Where(x => x.IsAudio == false)) { tw.WriteLine($"file '{part.Filename}'"); } } string audioSource = chunks.Single(x => x.IsAudio && x.Bitrate == target.AudioBitrate).Filename; string arguments = $@"-y -f concat -safe 0 -i ""{path}"" -i ""{audioSource}"" -c copy {targetFilename}"; connection.Execute( "INSERT INTO FfmpegMergeJobs (JobCorrelationId, Arguments, Needed, State, Target) VALUES(@JobCorrelationId, @Arguments, @Needed, @State, @Target);", new { JobCorrelationId = job.JobCorrelationId, Arguments = arguments, Needed = jobRequest.Needed, State = TranscodingJobState.Queued, Target = targetNumber }); } }
private static void QueueMpegDashMergeJob(BaseJob job, string destinationFilename, IDbConnection connection, JobRequest jobRequest, string fileNameWithoutExtension, string outputFolder, string fileExtension) { string destinationFolder = Path.GetDirectoryName(destinationFilename); ICollection <TranscodingJobState> totalJobs = connection.Query <TranscodingJobState>("SELECT State FROM Mp4boxJobs WHERE JobCorrelationId = @Id;", new { Id = jobRequest.JobCorrelationId }) .ToList(); // One MPEG DASH merge job is already queued. Do nothing if (totalJobs.Any()) { return; } string arguments = $@"-dash 4000 -rap -frag-rap -profile onDemand -out {destinationFolder}{Path.DirectorySeparatorChar}{fileNameWithoutExtension}.mpd"; var chunks = connection.Query <FfmpegPart>( "SELECT Filename, Number, Target, (SELECT VideoSourceFilename FROM FfmpegVideoRequest WHERE JobCorrelationId = @Id) AS VideoSourceFilename FROM FfmpegVideoParts WHERE JobCorrelationId = @Id ORDER BY Target, Number;", new { Id = job.JobCorrelationId }); foreach (var chunk in chunks.GroupBy(x => x.Target, x => x, (key, values) => values)) { int targetNumber = chunk.First().Target; DestinationFormat target = jobRequest.Targets[targetNumber]; string targetFilename = $@"{outputFolder}{Path.DirectorySeparatorChar}{fileNameWithoutExtension}_{target.Width}x{target .Height}_{target.VideoBitrate}_{target.AudioBitrate}{fileExtension}"; arguments += $@" {targetFilename}"; } connection.Execute( "INSERT INTO Mp4boxJobs (JobCorrelationId, Arguments, Needed, State) VALUES(@JobCorrelationId, @Arguments, @Needed, @State);", new { JobCorrelationId = jobRequest.JobCorrelationId, Arguments = arguments, Needed = jobRequest.Needed, State = TranscodingJobState.Queued }); }
public void SaveJobs(JobRequest job, ICollection <VideoTranscodingJob> jobs, IDbConnection connection, Guid jobCorrelationId, int chunkDuration) { if (jobs.Any(x => x.State == TranscodingJobState.Unknown)) { throw new ArgumentException( "One or more jobs have state TranscodingJobState.Unknown. A valid state must be set before saving to database"); } connection.Execute( "INSERT INTO FfmpegVideoRequest (JobCorrelationId, VideoSourceFilename, AudioSourceFilename, DestinationFilename, Needed, Created, EnableDash, EnableTwoPass, EnablePsnr) VALUES(@JobCorrelationId, @VideoSourceFilename, @AudioSourceFilename, @DestinationFilename, @Needed, @Created, @EnableDash, @EnableTwoPass, @EnablePsnr);", new { JobCorrelationId = jobCorrelationId, job.VideoSourceFilename, job.AudioSourceFilename, job.DestinationFilename, job.Needed, Created = DateTime.UtcNow, job.EnableDash, job.EnableTwoPass, job.EnablePsnr }); foreach (DestinationFormat target in job.Targets) { connection.Execute( "INSERT INTO FfmpegVideoRequestTargets (JobCorrelationId, Width, Height, VideoBitrate, AudioBitrate, H264Level, H264Profile) VALUES(@JobCorrelationId, @Width, @Height, @VideoBitrate, @AudioBitrate, @Level, @Profile);", new { JobCorrelationId = jobCorrelationId, Width = target.Width, Height = target.Height, VideoBitrate = target.VideoBitrate, AudioBitrate = target.AudioBitrate, Level = target.Level, Profile = target.Profile }); } foreach (VideoTranscodingJob transcodingJob in jobs) { connection.Execute( "INSERT INTO FfmpegVideoJobs (JobCorrelationId, Arguments, Needed, VideoSourceFilename, ChunkDuration, State) VALUES(@JobCorrelationId, @Arguments, @Needed, @VideoSourceFilename, @ChunkDuration, @State);", new { JobCorrelationId = jobCorrelationId, Arguments = String.Join("|", transcodingJob.Arguments), Needed = transcodingJob.Needed, VideoSourceFilename = transcodingJob.SourceFilename, ChunkDuration = chunkDuration, State = transcodingJob.State }); int jobId = connection.Query <int>("SELECT @@IDENTITY;") .Single(); foreach (FfmpegPart part in transcodingJob.Chunks) { DestinationFormat format = job.Targets[part.Target]; connection.Execute( "INSERT INTO FfmpegVideoParts (JobCorrelationId, Target, Filename, Number, FfmpegVideoJobs_Id, Width, Height, Bitrate) VALUES(@JobCorrelationId, @Target, @Filename, @Number, @FfmpegVideoJobsId, @Width, @Height, @Bitrate);", new { JobCorrelationId = jobCorrelationId, Target = part.Target, Filename = part.Filename, Number = part.Number, FfmpegVideoJobsId = jobId, Width = format.Width, Height = format.Height, Bitrate = part.IsAudio ? format.AudioBitrate : format.VideoBitrate }); } } }
/// <summary> /// Validate the order instance, may throw OrderException if the order cannot be procesed. /// </summary> /// <exception cref="OrderException">If the order is unacceptable (:lemon:)</exception> public void Validate(IMediaInfoFacade mediaInfoFacade, ITimeProvider timeProvider) { if (!File.Exists(FilePath)) { throw new OrderException("Filepath does not exist."); } //Call Media Info for source format and valid source file check var arForced = ForceAspectRatio.HasValue && ForceAspectRatio != AspectRatio.unknown; var rForced = ForceResolution.HasValue && ForceResolution != Resolution.unknown; var infoResult = MediaInfoValidation(mediaInfoFacade, FilePath, arForced, rForced); if (!arForced) { AspectRatio = infoResult.AspectRatio; } CustomFormat = infoResult.CustomFormat; Duration = infoResult.Duration; Format = infoResult.Format; if (!rForced) { Resolution = infoResult.Resolution; } if (BurnInLogo.GetValueOrDefault(false)) { if (!string.IsNullOrEmpty(LogoPath)) { if (!File.Exists(LogoPath)) { throw new OrderException("LogoPath does not exist."); } if (!(LogoPath.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase) || LogoPath.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase) || LogoPath.EndsWith(".jpeg", StringComparison.InvariantCultureIgnoreCase))) { throw new OrderException("LogoPath is not a supported file type. Must be png/jpg/jpeg"); } } } if (!string.IsNullOrEmpty(AlternateAudioPath)) { if (!File.Exists(AlternateAudioPath)) { throw new OrderException("AlternateAudioPath does not exist."); } if (!(AlternateAudioPath.EndsWith(".wav", StringComparison.InvariantCultureIgnoreCase))) { throw new OrderException("AlternateAudioPath is not a supported file type. Must be wav format"); } var audioInfo = MediaInfoValidation(mediaInfoFacade, AlternateAudioPath); if (audioInfo.Format == StateFormat.custom) { throw new OrderException("AlternateAudioPath is not a supported encoding."); } ValidateAlternateAudioToVideo(mediaInfoFacade, AlternateAudioPath); if (!FFMpeg.SupportedAudioMuxingVideoSourceFormats.Contains(Format) || !FFMpeg.SupportedAudioSourceFormats.Contains(audioInfo.Format)) { throw new OrderException($"Alternative audio format {audioInfo.Format} mux to {Format} is invalid."); } } if (BurnInSubtitles.GetValueOrDefault(false)) { if (!File.Exists(SubtitlesPath)) { throw new OrderException("SubtitlesPath does not exist."); } if (!FFMpeg.SupportedSubtitleExtensions.Any(e => SubtitlesPath.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) { throw new OrderException($"SubtitlesPath is not a supported file type. Must be one of {FFMpeg.SupportedSubtitleExtensions.Aggregate((c,n)=>$"{c}, {n}")}"); } } if ((FFMpeg.SupportedAudioDestinationFormats.Contains(DestinationFormat) && !FFMpeg.SupportedAudioSourceFormats.Contains(Format)) || (Wfs.SupportedDestinationFormats.Contains(DestinationFormat) && !Wfs.SupportedSourceFormats.Contains(Format))) { throw new OrderException($"DestinationFormat {DestinationFormat} is invalid for source format {Format}."); } if (DestinationFormat == StateFormat.unknown) { throw new OrderException("DestinationFormat is invalid."); } if (DestinationFormat.GetType().GetField(DestinationFormat.ToString()).CustomAttributes.ToList() .Find(a => a.AttributeType == typeof(ObsoleteAttribute)) != null) { throw new OrderException($"DestinationFormat is obsolete. {DestinationFormat}"); } if (!Directory.Exists(DestinationPath)) { throw new OrderException("DestinationPath does not exist."); } IntroDuration = ValidateStichAttachments(IntroFilePath, nameof(IntroFilePath), mediaInfoFacade); OutroDuration = ValidateStichAttachments(OutroFilePath, nameof(OutroFilePath), mediaInfoFacade); //TODO: Disabled until further notice. This check fails as it doens't check "Everyone". We might need to rethink this entirely. :bomb: ////Check if we have write permission on destination //var acc = new NTAccount(Environment.UserName); //var accessGranted = false; //string rights = $"Rights found on {acc.Value}: "; //var secId = acc.Translate(typeof(SecurityIdentifier)) as SecurityIdentifier; //if (secId == null) // throw new OrderException($"Unable to read SecurityIdentifier from {Environment.UserName}"); //var dInfo = new DirectoryInfo(DestinationPath); //var dSecurity = dInfo.GetAccessControl(); //var rules = dSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); //foreach (FileSystemAccessRule ar in rules) //{ // if (secId.CompareTo(ar.IdentityReference as SecurityIdentifier) == 0) // { // Debug.WriteLine($"We found the rights for the user: {ar.FileSystemRights}"); // rights += ar.FileSystemRights + " "; // if ((ar.FileSystemRights & FileSystemRights.Write) == FileSystemRights.Write) // { // Debug.WriteLine("ACCESS GRANTED!"); // accessGranted = true; // } // } //} //if (!accessGranted) throw new OrderException("Can't write to destination. Only found following: "+ rights); if (!Priority.HasValue) { Priority = Marvin.Model.Priority.low; } if (!string.IsNullOrEmpty(CallbackUrl) && !Uri.IsWellFormedUriString(CallbackUrl, UriKind.Absolute)) { throw new OrderException("Callback URL is not valid"); } if (!string.IsNullOrEmpty(DestinationFilename)) { var invalidChars = Path.GetInvalidFileNameChars(); foreach (var c in DestinationFilename.Where(c => invalidChars.Contains(c))) { throw new OrderException($"{DestinationFilename} contains illigal character: {c}"); } } Issued = timeProvider.GetUtcNow(); if (!DueDate.HasValue || DueDate.Value < Issued) { DueDate = Issued; } if (string.IsNullOrEmpty(Name)) { Name = new FileInfo(FilePath).Name; } if (string.IsNullOrEmpty(SourceUrn)) { SourceUrn = null; } else if (!_urnValidation.IsMatch(SourceUrn)) { throw new OrderException($"Invalid source urn {SourceUrn} supplied"); } Validated = true; }