private Rectangle[] CalculateJSFrames(LayoutInput[] inputs, LayoutOutput output, string javascriptFile) { var engine = new Engine((options) => { options.AllowDebuggerStatement(false); options.LimitRecursion(64); options.LocalTimeZone(TimeZoneInfo.Utc); options.MaxStatements(16384); }); engine.Execute(File.ReadAllText(javascriptFile)); var result = engine.Invoke("layout", inputs.Select(x => x.ToDynamic()).ToArray(), output.ToDynamic()); if (result == null) { throw new Exception("Missing return value from JS 'layout' function."); } var dynamicFrames = result.ToObject() as dynamic[]; if (dynamicFrames == null) { throw new Exception("Unexpected return value from JS 'layout' function."); } if (dynamicFrames.Length != inputs.Length) { throw new Exception($"Unexpected array length ({dynamicFrames.Length}, expected {inputs.Length}) in return value from JS 'calculate' function."); } var frames = new Rectangle[dynamicFrames.Length]; for (var i = 0; i < dynamicFrames.Length; i++) { var frame = Rectangle.FromDynamic(dynamicFrames[i]) as Rectangle?; if (frame == null) { throw new Exception($"Unexpected array value at index {i} in return value from JS 'layout' function."); } frames[i] = frame.Value; } return(frames); }
public static Layout Calculate(LayoutType type, LayoutInput[] inputs, LayoutOutput output, string javascriptFile) { var layout = new Layout { Margin = output.Margin, Size = output.Size }; // get frames var frames = layout.CalculateFrames(type, inputs, output, javascriptFile); // set bounds var views = new Dictionary <string, LayoutView>(); for (var i = 0; i < frames.Length; i++) { views[inputs[i].ConnectionId] = new LayoutView(frames[i], new Rectangle(Point.Zero, inputs[i].Size)); } layout.Views = views; return(layout); }
private Rectangle[] CalculateFrames(LayoutType type, LayoutInput[] inputs, LayoutOutput output, string javascriptFile) { switch (type) { case LayoutType.HStack: return(CalculateStackFrames(true, inputs)); case LayoutType.VStack: return(CalculateStackFrames(false, inputs)); case LayoutType.HGrid: return(CalculateGridFrames(true, inputs)); case LayoutType.VGrid: return(CalculateGridFrames(false, inputs)); case LayoutType.JS: return(CalculateJSFrames(inputs, output, javascriptFile)); default: throw new Exception("Unrecognized layout."); } }
private Rectangle[] CalculateFrames(LayoutType type, int cameraWeight, int screenWeight, LayoutInput[] inputs, LayoutOutput output, string javascriptFile) { if (type == LayoutType.JS) { return(CalculateJSFrames(inputs, output, javascriptFile)); } // separate screen vs. camera content var screenInputs = inputs.Where(input => input.VideoContent == VideoContent.Screen).ToArray(); var cameraInputs = inputs.Except(screenInputs).ToArray(); // single-content case if (inputs.Length == screenInputs.Length || inputs.Length == cameraInputs.Length) { return(CalculateFrames(type, inputs)); } // sanity check if (cameraWeight < 1 || screenWeight < 1) { throw new ArgumentException("Camera weight and/or screen weight must be positive integers."); } Rectangle[] cameraFrames; Rectangle[] screenFrames; if (type == LayoutType.HStack || type == LayoutType.HGrid) { var usableWidth = Size.Width - Margin; var usableHeight = Size.Height; var cameraWidth = (int)Math.Floor((double)cameraWeight * usableWidth / (cameraWeight + screenWeight)); cameraFrames = new Layout { Margin = Margin, Size = new Size(cameraWidth, usableHeight) }.CalculateFrames(type, cameraInputs); var screenWidth = usableWidth - cameraWidth; screenFrames = new Layout { Margin = Margin, Size = new Size(screenWidth, usableHeight) }.CalculateFrames(type, screenInputs); // camera content to the right for (var i = 0; i < cameraFrames.Length; i++) { var cameraFrame = cameraFrames[i]; var cameraFrameOrigin = cameraFrame.Origin; cameraFrames[i] = new Rectangle(new Point(screenWidth + Margin + cameraFrameOrigin.X, cameraFrameOrigin.Y), cameraFrame.Size); } } else { var usableWidth = Size.Width; var usableHeight = Size.Height - Margin; var cameraHeight = (int)Math.Floor((double)cameraWeight * usableHeight / (cameraWeight + screenWeight)); cameraFrames = new Layout { Margin = Margin, Size = new Size(usableWidth, cameraHeight) }.CalculateFrames(type, cameraInputs); var screenHeight = usableHeight - cameraHeight; screenFrames = new Layout { Margin = Margin, Size = new Size(usableWidth, screenHeight) }.CalculateFrames(type, screenInputs); // camera content to the bottom for (var i = 0; i < cameraFrames.Length; i++) { var cameraFrame = cameraFrames[i]; var cameraFrameOrigin = cameraFrame.Origin; cameraFrames[i] = new Rectangle(new Point(cameraFrameOrigin.X, screenHeight + Margin + cameraFrameOrigin.Y), cameraFrame.Size); } } // maintain ordering var cameraIndex = 0; var screenIndex = 0; var frames = new Rectangle[inputs.Length]; for (var i = 0; i < frames.Length; i++) { if (inputs[i].VideoContent == VideoContent.Screen) { frames[i] = screenFrames[screenIndex++]; } else { frames[i] = cameraFrames[cameraIndex++]; } } return(frames); }
private async Task <bool> MuxVideo(MuxOptions options) { if (options.TrimFirst || options.TrimLast) { _Logger.LogError("--trim-first and --trim-last are not supported for video."); return(false); } // set output file name VideoFileName = $"{ProcessOutputFileName(options.OutputFileName)}_video.{options.VideoContainer}"; // get output file path VideoFile = Path.Combine(GetOutputPath(options), VideoFileName); // initialize recordings var recordingIndex = 0; var recordings = CompletedRecordings.Where(x => x.VideoFileExists && x.VideoStartTimestamp != null && x.VideoStopTimestamp != null).ToArray(); foreach (var recording in recordings) { if (options.DryRun) { recording.SetVideoSegments(); } else { recording.VideoCodec = await GetVideoCodec(recording).ConfigureAwait(false); recording.SetVideoSegments(await ParseVideoSegments(recording).ConfigureAwait(false)); } } recordings = recordings.Where(x => x.VideoSegments.Length > 0).ToArray(); if (recordings.Length == 0) { _Logger.LogInformation("Session has no video segments."); return(true); } foreach (var recording in recordings) { recording.VideoIndex = recordingIndex++; recording.VideoTag = $"[{recording.VideoIndex}:v]"; } // initialize layout output var layoutOutput = new LayoutOutput { ApplicationId = ApplicationId, ChannelId = ChannelId, Margin = options.Margin, Size = new Size(options.Width, options.Height) }; // initialize unique connections var connections = new HashSet <Connection>(); foreach (var recording in recordings) { connections.Add(recording.Connection); } // initialize static layout inputs var staticLayoutInputs = new List <LayoutInput>(); foreach (var connection in connections) { staticLayoutInputs.Add(connection.GetLayoutInput()); } // convert recordings into event timeline var events = new List <VideoEvent>(); foreach (var recording in recordings) { events.AddRange(recording.VideoEvents); } // sort event timeline events = events.OrderBy(x => x.Timestamp).ThenBy(x => (int)x.Type).ToList(); // convert event timeline into chunks var chunks = new List <VideoChunk>(); var lastChunk = (VideoChunk)null; foreach (var @event in events) { // blank chunk mid-session if (chunks.Count > 0 && lastChunk == null) { chunks.Add(new VideoChunk { StartTimestamp = chunks.Last().StopTimestamp, StopTimestamp = @event.Timestamp, Layout = Layout.Calculate(options.Layout, options.CameraWeight, options.ScreenWeight, new LayoutInput[0], layoutOutput, options.JavaScriptFile), Segments = new VideoSegment[0] }); } VideoChunk chunk; if (chunks.Count == 0) { chunk = VideoChunk.First(@event); } else { chunk = chunks.Last().Next(@event); } lastChunk = chunk; if (chunk != null) { // keep the segments sorted by their time of first join chunk.Segments = chunk.Segments.OrderBy(x => x.Recording.VideoIndex).ToArray(); // calculate the layout if (options.Dynamic) { chunk.Layout = Layout.Calculate(options.Layout, options.CameraWeight, options.ScreenWeight, chunk.Segments.Select(x => x.GetLayoutInput()).ToArray(), layoutOutput, options.JavaScriptFile); } else { chunk.Layout = Layout.Calculate(options.Layout, options.CameraWeight, options.ScreenWeight, staticLayoutInputs.ToArray(), layoutOutput, options.JavaScriptFile); } chunks.Add(chunk); } } var ONE_MS = new TimeSpan(10000); // 1 Tick = 100ns, 10000 Ticks = 1ms // insert blank chunk if needed if (chunks.Count > 0 && (chunks[0].StartTimestamp - StartTimestamp).Duration() >= ONE_MS) { chunks.Insert(0, new VideoChunk { StartTimestamp = StartTimestamp, StopTimestamp = chunks[0].StartTimestamp, Layout = Layout.Calculate(options.Layout, options.CameraWeight, options.ScreenWeight, new LayoutInput[0], layoutOutput, options.JavaScriptFile), Segments = new VideoSegment[0] }); } chunks.RemoveAll(chunk => chunk.Duration < ONE_MS); // build filter chains var filterChainsAndTags = GetVideoFilterChainsAndTags(chunks.ToArray(), options); // each filter chain represents a single chunk var chunkFiles = new List <string>(); for (var i = 0; i < filterChainsAndTags.Length; i++) { var filterChainAndTag = filterChainsAndTags[i]; var filterChain = filterChainAndTag.Item1; var filterTag = filterChainAndTag.Item2; var chunkFilterChainFileName = $"{ProcessOutputFileName(options.OutputFileName)}_video_chunk_{i}.filter"; var chunkFilterChainFile = Path.Combine(GetTempPath(options), chunkFilterChainFileName); var chunkFileName = $"{ProcessOutputFileName(options.OutputFileName)}_video_chunk_{i}.mkv"; var chunkFile = Path.Combine(GetTempPath(options), chunkFileName); chunkFiles.Add(chunkFile); try { // construct argument list var arguments = new List <string> { "-y" // overwrite output files without asking }; arguments.AddRange(recordings.Select(recording => { return($"-i {recording.VideoFile}"); })); try { if (options.NoFilterFiles) { arguments.Add($@"-filter_complex ""{filterChain}"""); } else { var chunkFilterChainFilePath = Path.GetDirectoryName(chunkFilterChainFile); if (!Directory.Exists(chunkFilterChainFilePath)) { Directory.CreateDirectory(chunkFilterChainFilePath); } System.IO.File.WriteAllText(chunkFilterChainFile, filterChain); arguments.Add($@"-filter_complex_script {chunkFilterChainFile}"); } } catch (Exception ex) { _Logger.LogError(ex, "Could not create temporary chunk filter chain file '{ChunkFilterChainFileName}'.", chunkFilterChainFileName); _Logger.LogWarning($"Chunk filter chain will be passed as command-line argument."); arguments.Add($@"-filter_complex ""{filterChain}"""); } arguments.Add($@"-map ""{filterTag}"""); if (options.VideoCodec != null) { arguments.Add($"-codec:v {options.VideoCodec}"); } arguments.Add(chunkFile); if (options.DryRun) { return(true); } var outputPath = Path.GetDirectoryName(chunkFile); if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } // run it await _FfmpegUtility.FFmpeg(string.Join(" ", arguments)).ConfigureAwait(false); } finally { try { if (System.IO.File.Exists(chunkFilterChainFile) && !options.SaveTempFiles) { System.IO.File.Delete(chunkFilterChainFile); } } catch (Exception ex) { _Logger.LogError(ex, "Could not delete temporary chunk filter chain file '{ChunkFilterChainFileName}'.", chunkFilterChainFileName); } } } if (chunkFiles.Count == 0) { return(true); } var chunkListFileName = $"{ProcessOutputFileName(options.OutputFileName)}_video_chunks.list"; var chunkListFile = Path.Combine(GetTempPath(options), chunkListFileName); var chunkListFilePath = Path.GetDirectoryName(chunkListFile); if (!Directory.Exists(chunkListFilePath)) { Directory.CreateDirectory(chunkListFilePath); } System.IO.File.WriteAllText(chunkListFile, string.Join(Environment.NewLine, chunkFiles.Select(chunkFile => $"file '{chunkFile}'"))); try { // construct argument list var arguments = new List <string> { "-y", // overwrite output files without asking "-safe 0", "-f concat", }; arguments.Add($"-i {chunkListFile}"); arguments.Add("-c copy"); arguments.Add(VideoFile); if (options.DryRun) { return(true); } var outputPath = Path.GetDirectoryName(VideoFile); if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } // run it await _FfmpegUtility.FFmpeg(string.Join(" ", arguments)).ConfigureAwait(false); return(VideoFileExists); } finally { try { if (System.IO.File.Exists(chunkListFile) && !options.SaveTempFiles) { System.IO.File.Delete(chunkListFile); } } catch (Exception ex) { _Logger.LogError(ex, "Could not delete temporary chunk list file '{ChunkListFile}'.", chunkListFile); } foreach (var chunkFile in chunkFiles) { try { if (System.IO.File.Exists(chunkFile) && !options.SaveTempFiles) { System.IO.File.Delete(chunkFile); } } catch (Exception ex) { _Logger.LogError(ex, "Could not delete temporary chunk file '{ChunkFile}'.", chunkFile); } } } }