public Models.Connection ToModel() { return(new Models.Connection { Id = Id, StartTimestamp = StartTimestamp, StopTimestamp = StopTimestamp, Recordings = CompletedRecordings.Select(recording => recording.ToModel()).ToArray() }); }
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)) }); }
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(); }
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); } } } }
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); } } }