Example #1
0
 public Models.Connection ToModel()
 {
     return(new Models.Connection
     {
         Id = Id,
         StartTimestamp = StartTimestamp,
         StopTimestamp = StopTimestamp,
         Recordings = CompletedRecordings.Select(recording => recording.ToModel()).ToArray()
     });
 }
Example #2
0
 public LayoutInput GetLayoutInput()
 {
     return(new LayoutInput
     {
         ConnectionId = Id,
         ClientId = ClientId,
         DeviceId = DeviceId,
         UserId = UserId,
         Size = new Size(CompletedRecordings.SelectMany(x => x.VideoSegments).Max(x => x.Size.Width), CompletedRecordings.SelectMany(x => x.VideoSegments).Max(x => x.Size.Height))
     });
 }
Example #3
0
        public SessionsControl(IAdaptersControl adapters, SessionRecorderFactoryLocator recorders)
        {
            _adapters  = adapters;
            _recorders = recorders;


            CompletedRecordings = _recordings.SelectMany(r => recorders.CreateForRecording(r))
                                  .Do(r => r.Record())
                                  .SelectMany(r => Observable.FromEventPattern <ISessionRecordingResult>(h => r.Closed += h, h => r.Closed -= h)
                                              .FirstOrDefaultAsync()
                                              .Where(e => e != null)
                                              .Select(e => e.EventArgs))
                                  .Publish()
                                  .RefCount();

            _recordersSubscription = CompletedRecordings.Subscribe();
        }
Example #4
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);
                    }
                }
            }
        }
Example #5
0
        private async Task <bool> MuxAudio(MuxOptions options)
        {
            // set output file name
            AudioFileName = $"{ProcessOutputFileName(options.OutputFileName)}_audio.{options.AudioContainer}";

            // get output file path
            AudioFile = Path.Combine(GetOutputPath(options), AudioFileName);

            // initialize recordings
            var recordingIndex = 0;
            var recordings     = CompletedRecordings.Where(x => x.AudioFileExists && x.AudioStartTimestamp != null && x.AudioStopTimestamp != null).ToArray();

            foreach (var recording in recordings)
            {
                recording.AudioIndex = recordingIndex++;
                recording.AudioTag   = $"[{recording.AudioIndex}:a]";

                if (!options.DryRun)
                {
                    recording.AudioCodec = await GetAudioCodec(recording).ConfigureAwait(false);
                }
            }

            // build filter chains
            var filterChains        = GetAudioFilterChains(recordings, options);
            var filterChainFileName = $"{ProcessOutputFileName(options.OutputFileName)}_audio.filter";
            var filterChainFile     = Path.Combine(GetTempPath(options), filterChainFileName);

            try
            {
                // pull together the final arguments list
                var arguments = new List <string>
                {
                    "-y" // overwrite output files without asking
                };
                arguments.AddRange(recordings.Select(recording =>
                {
                    if (recording.AudioCodec == "opus")
                    {
                        // 'opus' doesn't support SILK, but 'libopus' does,
                        // so prefer that when decoding to avoid audio loss
                        return($"-codec:a libopus -i {recording.AudioFile}");
                    }
                    return($"-i {recording.AudioFile}");
                }));
                if (filterChains.Length > 0)
                {
                    try
                    {
                        if (options.NoFilterFiles)
                        {
                            arguments.Add($@"-filter_complex ""{string.Join(";", filterChains)}""");
                        }
                        else
                        {
                            var filterChainFilePath = Path.GetDirectoryName(filterChainFile);
                            if (!Directory.Exists(filterChainFilePath))
                            {
                                Directory.CreateDirectory(filterChainFilePath);
                            }

                            System.IO.File.WriteAllText(filterChainFile, string.Join(";", filterChains));
                            arguments.Add($@"-filter_complex_script {filterChainFile}");
                        }
                    }
                    catch (Exception ex)
                    {
                        _Logger.LogError(ex, "Could not create temporary filter chain file '{FilterChainFileName}'.", filterChainFileName);
                        _Logger.LogWarning($"Filter chain will be passed as command-line argument.");
                        arguments.Add($@"-filter_complex ""{string.Join(";", filterChains)}""");
                    }
                }
                arguments.Add($@"-map ""[aout]""");
                if (options.AudioCodec != null)
                {
                    arguments.Add($"-codec:a {options.AudioCodec}");
                }
                arguments.Add(AudioFile);

                if (options.DryRun)
                {
                    return(true);
                }

                var outputPath = Path.GetDirectoryName(AudioFile);
                if (!Directory.Exists(outputPath))
                {
                    Directory.CreateDirectory(outputPath);
                }

                // run it
                await _FfmpegUtility.FFmpeg(string.Join(" ", arguments)).ConfigureAwait(false);

                return(AudioFileExists);
            }
            finally
            {
                try
                {
                    if (System.IO.File.Exists(filterChainFile) && !options.SaveTempFiles)
                    {
                        System.IO.File.Delete(filterChainFile);
                    }
                }
                catch (Exception ex)
                {
                    _Logger.LogError(ex, "Could not delete temporary filter chain file '{FilterChainFileName}'.", filterChainFileName);
                }
            }
        }