Example #1
0
        public Muxer(MuxOptions options, ILoggerFactory loggerFactory)
        {
            Options       = options;
            LoggerFactory = loggerFactory;

            _Logger = LoggerFactory.CreateLogger(nameof(Muxer));
        }
Example #2
0
        private bool ConfirmDelete(MuxOptions options, string path)
        {
            if (options.NoPrompt)
            {
                return(true);
            }

            while (true)
            {
                Console.Error.Write($"Delete {path} ([y]es/[n]o/[a]ll)? ");
                var key = Console.ReadKey().Key;
                Console.Error.WriteLine();
                switch (key)
                {
                case ConsoleKey.A:
                    options.NoPrompt = true;
                    return(true);

                case ConsoleKey.Y:
                    return(true);

                case ConsoleKey.N:
                    return(false);
                }
            }
        }
Example #3
0
        public bool WriteMetadata(MuxOptions options)
        {
            MetadataFile = Path.Combine(Path.GetDirectoryName(File), $"{Path.GetFileNameWithoutExtension(File)}.json");

            var file      = File;
            var audioFile = AudioFile;
            var videoFile = VideoFile;

            try
            {
                if (options.DryRun)
                {
                    File      = null;
                    AudioFile = null;
                    VideoFile = null;
                }

                var metadataFilePath = Path.GetDirectoryName(MetadataFile);
                if (!Directory.Exists(metadataFilePath))
                {
                    Directory.CreateDirectory(metadataFilePath);
                }

                System.IO.File.WriteAllText(MetadataFile, JsonConvert.SerializeObject(ToModel()));

                return(MetadataFileExists);
            }
            finally
            {
                File      = file;
                AudioFile = audioFile;
                VideoFile = videoFile;
            }
        }
Example #4
0
        private string GetTempPath(MuxOptions options)
        {
            switch (options.Strategy)
            {
            case StrategyType.Hierarchical:
                return(Path.Combine(options.TempPath, ApplicationId, ChannelId));

            case StrategyType.Flat:
                return(options.TempPath);

            default:
                throw new InvalidOperationException($"Unexpected strategy type '{options.Strategy}'.");
            }
        }
Example #5
0
        private string GetTempPath(MuxOptions options)
        {
            switch (options.Strategy)
            {
            case StrategyType.Hierarchical:
                return(Path.Combine(options.TempPath, ApplicationId, ChannelId));

            case StrategyType.Flat:
                return(options.TempPath);

            default:
                throw new Exception("Unrecognized strategy.");
            }
        }
Example #6
0
 private bool Delete(string file, MuxOptions options)
 {
     try
     {
         if (ConfirmDelete(options, file))
         {
             File.Delete(file);
             return(true);
         }
     }
     catch (Exception ex)
     {
         Console.Error.WriteLine($"Could not delete {file}. {ex}");
     }
     return(false);
 }
Example #7
0
        public bool ProcessLogEntry(LogEntry logEntry, MuxOptions options)
        {
            var applicationId = logEntry.ApplicationId;

            if (applicationId == null)
            {
                return(false);
            }

            if (!_Applications.TryGetValue(applicationId, out var application))
            {
                _Applications[applicationId] = application = new Application(applicationId);
            }

            return(application.ProcessLogEntry(logEntry, options));
        }
Example #8
0
 private bool Delete(string file, MuxOptions options)
 {
     try
     {
         if (ConfirmDelete(options, file))
         {
             File.Delete(file);
             return(true);
         }
     }
     catch (Exception ex)
     {
         _Logger.LogError(ex, "Could not delete {File}.", file);
     }
     return(false);
 }
Example #9
0
        public bool ProcessLogEntry(LogEntry logEntry, MuxOptions options, ILoggerFactory loggerFactory)
        {
            var channelId = logEntry.ChannelId;

            if (channelId == null)
            {
                return(false);
            }

            if (!_Channels.TryGetValue(channelId, out var channel))
            {
                _Channels[channelId] = channel = new Channel(channelId, Id, ExternalId);
            }

            return(channel.ProcessLogEntry(logEntry, options, loggerFactory));
        }
Example #10
0
        public bool ProcessLogEntry(LogEntry logEntry, MuxOptions options)
        {
            var connectionId = logEntry.ConnectionId;

            if (connectionId == null)
            {
                return(false);
            }

            if (!_Connections.TryGetValue(connectionId, out var connection))
            {
                _Connections[connectionId] = connection = new Connection(connectionId, Id, DeviceId, UserId, ChannelId, ApplicationId, ExternalId)
                {
                    Client = this
                };
            }

            return(connection.ProcessLogEntry(logEntry, options));
        }
Example #11
0
        private string Move(string file, MuxOptions options)
        {
            var newFile = file.Replace(options.InputPath, options.OutputPath, StringComparison.InvariantCultureIgnoreCase);

            var outputPath = Path.GetDirectoryName(newFile);

            if (!Directory.Exists(outputPath))
            {
                Directory.CreateDirectory(outputPath);
            }

            try
            {
                File.Move(file, newFile);
                return(newFile);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"Could not move {file} to {newFile}. {ex}");
            }
            return(file);
        }
Example #12
0
        private string Move(string file, MuxOptions options)
        {
            var newFile = file.Replace(options.InputPath, options.MovePath);

            var movePath = Path.GetDirectoryName(newFile);

            if (!Directory.Exists(movePath))
            {
                Directory.CreateDirectory(movePath);
            }

            try
            {
                File.Move(file, newFile);
                return(newFile);
            }
            catch (Exception ex)
            {
                _Logger.LogError(ex, "Could not move {File} to {NewFile}.", file, newFile);
            }
            return(file);
        }
Example #13
0
        public bool ProcessLogEntry(LogEntry logEntry, MuxOptions options)
        {
            var clientId = logEntry.ClientId;

            if (clientId == null)
            {
                return(false);
            }

            if (!_Clients.TryGetValue(clientId, out var client))
            {
                _Clients[clientId] = client = new Client(clientId, logEntry.DeviceId, logEntry.UserId, Id, ApplicationId);
            }

            var result = client.ProcessLogEntry(logEntry, options);

            if (Completed)
            {
                _CompletedSessions.Add(new Session(Id, ApplicationId, CompletedClients));
                _Clients = new Dictionary <string, Client>();
            }

            return(result);
        }
Example #14
0
        private async Task <LogEntry[]> GetLogEntries(MuxOptions options)
        {
            switch (options.Strategy)
            {
            case StrategyType.AutoDetect:
            {
                var logFilePath = Path.Combine(options.InputPath, HierarchicalLogFileName);
                if (File.Exists(logFilePath))
                {
                    options.Strategy = StrategyType.Hierarchical;
                }
                else
                {
                    options.Strategy = StrategyType.Flat;
                }
                return(await GetLogEntries(options));
            }

            case StrategyType.Hierarchical:
            {
                var logFilePath = Path.Combine(options.InputPath, HierarchicalLogFileName);
                if (!File.Exists(logFilePath))
                {
                    return(null);
                }

                return(await LogUtility.GetEntries(logFilePath));
            }

            case StrategyType.Flat:
            {
                var logEntries = new List <LogEntry>();
                IEnumerable <string> filePaths;

                if (Options.InputFileNames.Count() == 0)
                {
                    filePaths = Directory.EnumerateFiles(Options.InputPath, "*.*", SearchOption.TopDirectoryOnly);
                }
                else
                {
                    filePaths = Options.InputFileNames.Select(inputFileName => Path.Combine(Options.InputPath, inputFileName));
                }

                foreach (var filePath in filePaths)
                {
                    // filter the input files if a filter is provided.
                    if (Options.InputFilter != null && !Regex.Match(Path.GetFileName(filePath), Options.InputFilter).Success)
                    {
                        continue;
                    }

                    if (filePath.EndsWith(".json") || filePath.EndsWith(".json.rec"))
                    {
                        try
                        {
                            logEntries.AddRange(await LogUtility.GetEntries(filePath));
                        }
                        catch (FileNotFoundException)
                        {
                            Console.Error.WriteLine($"Could not read from {filePath} as it no longer exists. Is another process running that could have removed it?");
                        }
                        catch (IOException ex) when(ex.Message.Contains("Stale file handle"))          // for Linux
                        {
                            Console.Error.WriteLine($"Could not read from {filePath} as the file handle is stale. Is another process running that could have removed it?");
                        }
                    }
                }
                return(logEntries.ToArray());
            }

            default:
                throw new Exception("Unrecognized strategy.");
            }
        }
Example #15
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);
                }
            }
        }
Example #16
0
 public Muxer(MuxOptions options)
 {
     Options = options;
 }
Example #17
0
        public bool ProcessLogEntry(LogEntry logEntry, MuxOptions options)
        {
            if (logEntry.Type == LogEntry.TypeStartRecording)
            {
                if (ActiveRecording != null)
                {
                    // already recording, shouldn't happen
                    return(false);
                }

                ActiveRecording = new Recording
                {
                    Connection     = this,
                    StartTimestamp = logEntry.Timestamp,
                    LogFile        = logEntry.FilePath
                };

                if (StartTimestamp == null)
                {
                    StartTimestamp = logEntry.Timestamp;
                }

                ActiveRecording.Update(logEntry);
            }
            else if (logEntry.Type == LogEntry.TypeUpdateRecording)
            {
                if (ActiveRecording == null)
                {
                    // not recording, shouldn't happen
                    return(false);
                }

                ActiveRecording.Update(logEntry);
            }
            else if (logEntry.Type == LogEntry.TypeStopRecording)
            {
                if (ActiveRecording == null)
                {
                    // not recording, shouldn't happen
                    return(false);
                }

                ActiveRecording.Update(logEntry, true);

                ActiveRecording.StopTimestamp = logEntry.Timestamp;
                ActiveRecording.AudioFile     = logEntry.Data?.AudioFile;
                ActiveRecording.VideoFile     = logEntry.Data?.VideoFile;

                if (ActiveRecording.AudioFile != null)
                {
                    ActiveRecording.AudioStartTimestamp = logEntry.Data?.AudioFirstFrameTimestamp ?? ActiveRecording.StartTimestamp;
                    ActiveRecording.AudioStopTimestamp  = logEntry.Data?.AudioLastFrameTimestamp ?? ActiveRecording.StopTimestamp;

                    if (!Path.IsPathRooted(ActiveRecording.AudioFile))
                    {
                        ActiveRecording.AudioFile = Path.Combine(options.InputPath, ActiveRecording.AudioFile);
                    }
                }

                if (ActiveRecording.VideoFile != null)
                {
                    ActiveRecording.VideoStartTimestamp = logEntry.Data?.VideoFirstFrameTimestamp ?? ActiveRecording.StartTimestamp;
                    ActiveRecording.VideoStopTimestamp  = logEntry.Data?.VideoLastFrameTimestamp ?? ActiveRecording.StopTimestamp;

                    if (!Path.IsPathRooted(ActiveRecording.VideoFile))
                    {
                        ActiveRecording.VideoFile = Path.Combine(options.InputPath, ActiveRecording.VideoFile);
                    }
                }

                var videoDelay = logEntry.Data?.VideoDelay ?? 0D;
                if (videoDelay != 0 && ActiveRecording.AudioFile != null && ActiveRecording.VideoStartTimestamp.HasValue && ActiveRecording.VideoStopTimestamp.HasValue)
                {
                    ActiveRecording.VideoStartTimestamp = ActiveRecording.VideoStartTimestamp.Value.AddSeconds(videoDelay);
                    ActiveRecording.VideoStopTimestamp  = ActiveRecording.VideoStopTimestamp.Value.AddSeconds(videoDelay);
                }

                // ensure consistency on start/stop timestamps
                var audioStartTimestampTicks = long.MaxValue;
                var audioStopTimestampTicks  = long.MinValue;
                if (ActiveRecording.AudioFile != null && ActiveRecording.AudioStartTimestamp.HasValue && ActiveRecording.AudioStopTimestamp.HasValue)
                {
                    audioStartTimestampTicks = ActiveRecording.AudioStartTimestamp.Value.Ticks;
                    audioStopTimestampTicks  = ActiveRecording.AudioStopTimestamp.Value.Ticks;
                }

                var videoStartTimestampTicks = long.MaxValue;
                var videoStopTimestampTicks  = long.MinValue;
                if (ActiveRecording.VideoFile != null && ActiveRecording.VideoStartTimestamp.HasValue && ActiveRecording.VideoStopTimestamp.HasValue)
                {
                    videoStartTimestampTicks = ActiveRecording.VideoStartTimestamp.Value.Ticks;
                    videoStopTimestampTicks  = ActiveRecording.VideoStopTimestamp.Value.Ticks;
                }

                StartTimestamp = ActiveRecording.StartTimestamp = new DateTime(Math.Min(audioStartTimestampTicks, videoStartTimestampTicks));
                StopTimestamp  = ActiveRecording.StopTimestamp = new DateTime(Math.Max(audioStopTimestampTicks, videoStopTimestampTicks));

                _Recordings.Add(ActiveRecording);
                ActiveRecording = null;
            }
            return(true);
        }
Example #18
0
        public async Task <bool> Mux(MuxOptions options)
        {
            // set output file name
            FileName = $"{ProcessOutputFileName(options.OutputFileName)}.{(options.NoVideo ? options.AudioContainer : options.VideoContainer)}";

            // get output file path
            File = Path.Combine(GetOutputPath(options), FileName);

            var inputArguments = new List <string>();

            // process audio
            if (HasAudio && !options.NoAudio)
            {
                if (!await MuxAudio(options).ConfigureAwait(false))
                {
                    return(false);
                }
                inputArguments.Add($"-i {AudioFile}");
            }

            // process video
            if (HasVideo && !options.NoVideo)
            {
                if (!await MuxVideo(options).ConfigureAwait(false))
                {
                    return(false);
                }
                inputArguments.Add($"-i {VideoFile}");
            }

            if (inputArguments.Count == 0)
            {
                _Logger.LogInformation("No media files found.");
                return(false);
            }

            // pull together the final arguments list
            var arguments = new List <string>
            {
                "-y" // overwrite output files without asking
            };

            arguments.AddRange(inputArguments);
            if (!options.NoAudio)
            {
                arguments.Add($"-codec:a copy");
            }
            if (!options.NoVideo)
            {
                arguments.Add($"-codec:v copy");
            }
            arguments.Add(File);

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

            var outputPath = Path.GetDirectoryName(File);

            if (!Directory.Exists(outputPath))
            {
                Directory.CreateDirectory(outputPath);
            }

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

            return(FileExists);
        }
Example #19
0
        private string[] GetAudioFilterChains(Recording[] recordings, MuxOptions options)
        {
            // build filter chains
            var filterChains  = new List <string>();
            var recordingTags = new List <string>();

            var startDelays = new List <double>();
            var endDelays   = new List <double>();
            var trimFirst   = 0D;
            var trimLast    = 0D;

            //--trim-first --trim-last
            if (options.TrimFirst || options.TrimLast)
            {
                foreach (var recording in recordings)
                {
                    startDelays.Add((recording.StartTimestamp - StartTimestamp).TotalSeconds);
                    endDelays.Add((StopTimestamp - recording.StopTimestamp).TotalSeconds);
                }

                //Find the second lowest delay. This is the offset we will use for audio recordings.
                startDelays.Sort();
                endDelays.Sort();

                if (recordings.Length > 1)
                {
                    if (options.TrimFirst)
                    {
                        trimFirst = startDelays[1];
                    }
                    if (options.TrimLast)
                    {
                        trimLast = endDelays[1];
                    }
                }
            }

            foreach (var recording in recordings)
            {
                // initialize tag
                var recordingTag = recording.AudioTag;
                var resampleTag  = $"[aresample_{recording.AudioIndex}]";
                var delayTag     = $"[adelay_{recording.AudioIndex}]";
                var trimFirstTag = $"[atrimfirst_{recording.AudioIndex}]";
                var trimLastTag  = $"[atrimlast_{recording.AudioIndex}]";

                // resample
                filterChains.Add(recording.GetAudioResampleFilterChain(recordingTag, resampleTag));
                recordingTag = resampleTag;

                // delay
                filterChains.Add(recording.GetAudioDelayFilterChain(StartTimestamp, recordingTag, delayTag));
                recordingTag = delayTag;

                if (trimFirst > 0)
                {
                    // atrim start - removes the beginning of the audio of the first track.
                    filterChains.Add(recording.GetAudioStartTrimFilterChain(recordingTag, trimFirstTag, trimFirst));
                    recordingTag = trimFirstTag;
                }

                if (trimLast > 0 && recording.StopTimestamp == StopTimestamp)
                {
                    // atrim end - removes the ending of the audio for the last track.
                    filterChains.Add(recording.GetAudioEndTrimFilterChain(recordingTag, trimLastTag, trimLast));
                    recordingTag = trimLastTag;
                }

                // keep track of tags
                recordingTags.Add(recordingTag);
            }

            // mix tags
            filterChains.AddRange(GetAudioMixFilterChains(recordingTags, out var outputTag));

            // null out
            filterChains.Add($"{outputTag}anull[aout]");

            return(filterChains.ToArray());
        }
Example #20
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 #21
0
        private ValueTuple <string, string>[] GetVideoFilterChainsAndTags(VideoChunk[] chunks, MuxOptions options)
        {
            // process each chunk
            var filterChainsAndTags = new List <ValueTuple <string, string> >();

            for (var chunkIndex = 0; chunkIndex < chunks.Length; chunkIndex++)
            {
                var chunk             = chunks[chunkIndex];
                var chunkFilterChains = new List <string>();

                // initialize tag
                var colorTag = $"[vcolor_{chunkIndex}]";
                var chunkTag = colorTag;

                // color
                chunkFilterChains.Add(chunk.GetColorFilterChain(options.BackgroundColor, colorTag));

                // process each chunk segment
                var segments = chunk.Segments;
                var views    = chunk.Layout.Views;
                for (var segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
                {
                    var segment   = segments[segmentIndex];
                    var view      = views[segment.Recording.Connection.Id];
                    var recording = segment.Recording;

                    // scale bounds to segment
                    view.Bounds = new Rectangle(Point.Zero, segment.Size);

                    if (segment.Size != Size.Empty)
                    {
                        view.ScaleBounds(options.Crop);
                    }

                    // initialize tags
                    var segmentTag = recording.VideoTag;
                    var fpsTag     = $"[vfps_{chunkIndex}_{segmentIndex}]";
                    var trimTag    = $"[vtrim_{chunkIndex}_{segmentIndex}]";
                    var scaleTag   = $"[vscale_{chunkIndex}_{segmentIndex}]";
                    var cropTag    = $"[vcrop_{chunkIndex}_{segmentIndex}]";
                    var overlayTag = $"[voverlay_{chunkIndex}_{segmentIndex}]";

                    // fps
                    chunkFilterChains.Add(chunk.GetFpsFilterChain(options.FrameRate, segmentTag, fpsTag));
                    segmentTag = fpsTag;

                    // trim
                    chunkFilterChains.Add(chunk.GetTrimFilterChain(recording, segmentTag, trimTag));
                    segmentTag = trimTag;

                    // then scale
                    chunkFilterChains.Add(view.GetSizeFilterChain(segmentTag, scaleTag));
                    segmentTag = scaleTag;

                    // then crop (optional)
                    if (options.Crop)
                    {
                        chunkFilterChains.Add(view.GetCropFilterChain(segmentTag, cropTag));
                        segmentTag = cropTag;
                    }

                    // then overlay
                    chunkFilterChains.Add(view.GetOverlayChain(options.Crop, chunkTag, segmentTag, overlayTag));
                    chunkTag = overlayTag;
                }

                filterChainsAndTags.Add((string.Join(";", chunkFilterChains), chunkTag));
            }

            return(filterChainsAndTags.ToArray());
        }