예제 #1
0
        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);
        }
예제 #2
0
        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);
        }
예제 #3
0
        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.");
            }
        }
예제 #4
0
        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);
        }
예제 #5
0
        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);
                    }
                }
            }
        }