< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.IO.EncodedRecorder
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 106
Coverable lines: 106
Total lines: 362
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 36
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_CopySubtitles()100%210%
GetOutputPath(...)100%210%
GetCommandLineArgs(...)0%272160%
GetAudioArgs(...)100%210%
EncodeVideo(...)100%210%
GetOutputSizeParam()100%210%
Stop()0%7280%
OnFfMpegProcessExited(...)0%2040%
Dispose()100%210%
Dispose(...)0%7280%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Diagnostics;
 8using System.Globalization;
 9using System.IO;
 10using System.Text;
 11using System.Text.Json;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using Jellyfin.Extensions;
 15using Jellyfin.Extensions.Json;
 16using MediaBrowser.Common;
 17using MediaBrowser.Common.Configuration;
 18using MediaBrowser.Controller;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Library;
 21using MediaBrowser.Controller.MediaEncoding;
 22using MediaBrowser.Model.Dto;
 23using MediaBrowser.Model.IO;
 24using Microsoft.Extensions.Logging;
 25
 26namespace Jellyfin.LiveTv.IO
 27{
 28    public class EncodedRecorder : IRecorder
 29    {
 30        private readonly ILogger _logger;
 31        private readonly IMediaEncoder _mediaEncoder;
 32        private readonly IServerApplicationPaths _appPaths;
 033        private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationO
 34        private readonly IServerConfigurationManager _serverConfigurationManager;
 035        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 36        private bool _hasExited;
 37        private FileStream _logFileStream;
 38        private string _targetPath;
 39        private Process _process;
 40        private bool _disposed;
 41
 42        public EncodedRecorder(
 43            ILogger logger,
 44            IMediaEncoder mediaEncoder,
 45            IServerApplicationPaths appPaths,
 46            IServerConfigurationManager serverConfigurationManager)
 47        {
 048            _logger = logger;
 049            _mediaEncoder = mediaEncoder;
 050            _appPaths = appPaths;
 051            _serverConfigurationManager = serverConfigurationManager;
 052        }
 53
 054        private static bool CopySubtitles => false;
 55
 56        public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile)
 57        {
 058            return Path.ChangeExtension(targetFile, ".ts");
 59        }
 60
 61        public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetF
 62        {
 63            // The media source is infinite so we need to handle stopping ourselves
 64            using var durationToken = new CancellationTokenSource(duration);
 65            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durat
 66
 67            await RecordFromFile(mediaSource, mediaSource.Path, targetFile, onStarted, cancellationTokenSource.Token).Co
 68
 69            _logger.LogInformation("Recording completed to file {Path}", targetFile);
 70        }
 71
 72        private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, Action onSta
 73        {
 74            _targetPath = targetFile;
 75            Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
 76
 77            var processStartInfo = new ProcessStartInfo
 78            {
 79                CreateNoWindow = true,
 80                UseShellExecute = false,
 81
 82                RedirectStandardError = true,
 83                RedirectStandardInput = true,
 84
 85                FileName = _mediaEncoder.EncoderPath,
 86                Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile),
 87
 88                WindowStyle = ProcessWindowStyle.Hidden,
 89                ErrorDialog = false
 90            };
 91
 92            _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments);
 93
 94            var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
 95            Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
 96
 97            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log direct
 98            _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefault
 99
 100            await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureA
 101            await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processSt
 102
 103            _process = new Process
 104            {
 105                StartInfo = processStartInfo,
 106                EnableRaisingEvents = true
 107            };
 108            _process.Exited += (_, _) => OnFfMpegProcessExited(_process);
 109
 110            _process.Start();
 111
 112            cancellationToken.Register(Stop);
 113
 114            onStarted();
 115
 116            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
 117            _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
 118
 119            _logger.LogInformation("ffmpeg recording process started for {Path}", _targetPath);
 120
 121            // Block until ffmpeg exits
 122            await _taskCompletionSource.Task.ConfigureAwait(false);
 123        }
 124
 125        private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
 126        {
 127            string videoArgs;
 0128            if (EncodeVideo(mediaSource))
 129            {
 130                const int MaxBitrate = 25000000;
 0131                videoArgs = string.Format(
 0132                    CultureInfo.InvariantCulture,
 0133                    "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superf
 0134                    GetOutputSizeParam(),
 0135                    MaxBitrate);
 136            }
 137            else
 138            {
 0139                videoArgs = "-codec:v:0 copy";
 140            }
 141
 0142            videoArgs += " -fflags +genpts";
 143
 0144            var flags = new List<string>();
 0145            if (mediaSource.IgnoreDts)
 146            {
 0147                flags.Add("+igndts");
 148            }
 149
 0150            if (mediaSource.IgnoreIndex)
 151            {
 0152                flags.Add("+ignidx");
 153            }
 154
 0155            if (mediaSource.GenPtsInput)
 156            {
 0157                flags.Add("+genpts");
 158            }
 159
 0160            var inputModifier = "-async 1 -vsync -1";
 161
 0162            if (flags.Count > 0)
 163            {
 0164                inputModifier += " -fflags " + string.Join(string.Empty, flags);
 165            }
 166
 0167            if (mediaSource.ReadAtNativeFramerate)
 168            {
 0169                inputModifier += " -re";
 170            }
 171
 0172            if (mediaSource.RequiresLooping)
 173            {
 0174                inputModifier += " -stream_loop -1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2";
 175            }
 176
 0177            var analyzeDurationSeconds = 5;
 0178            var analyzeDuration = " -analyzeduration " +
 0179                  (analyzeDurationSeconds * 1000000).ToString(CultureInfo.InvariantCulture);
 0180            inputModifier += analyzeDuration;
 181
 0182            var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
 183
 184            // var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase
 185            //    " -f mp4 -movflags frag_keyframe+empty_moov" :
 186            //    string.Empty;
 187
 0188            var outputParam = string.Empty;
 189
 0190            var threads = EncodingHelper.GetNumberOfThreads(null, _serverConfigurationManager.GetEncodingOptions(), null
 0191            var commandLineArgs = string.Format(
 0192                CultureInfo.InvariantCulture,
 0193                "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
 0194                inputTempFile,
 0195                targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
 0196                videoArgs,
 0197                GetAudioArgs(mediaSource),
 0198                subtitleArgs,
 0199                outputParam,
 0200                threads);
 201
 0202            return inputModifier + " " + commandLineArgs;
 203        }
 204
 205        private static string GetAudioArgs(MediaSourceInfo mediaSource)
 206        {
 0207            return "-codec:a:0 copy";
 208
 209            // var audioChannels = 2;
 210            // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
 211            // if (audioStream is not null)
 212            // {
 213            //    audioChannels = audioStream.Channels ?? audioChannels;
 214            // }
 215            // return "-codec:a:0 aac -strict experimental -ab 320000";
 216        }
 217
 218        private static bool EncodeVideo(MediaSourceInfo mediaSource)
 219        {
 0220            return false;
 221        }
 222
 223        protected string GetOutputSizeParam()
 0224            => "-vf \"yadif=0:-1:0\"";
 225
 226        private void Stop()
 227        {
 0228            if (!_hasExited)
 229            {
 230                try
 231                {
 0232                    _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath);
 233
 0234                    _process.StandardInput.WriteLine("q");
 0235                }
 0236                catch (Exception ex)
 237                {
 0238                    _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath);
 0239                }
 240
 0241                if (_hasExited)
 242                {
 0243                    return;
 244                }
 245
 246                try
 247                {
 0248                    _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath);
 249
 0250                    if (_process.WaitForExit(10000))
 251                    {
 0252                        return;
 253                    }
 0254                }
 0255                catch (Exception ex)
 256                {
 0257                    _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath);
 0258                }
 259
 0260                if (_hasExited)
 261                {
 0262                    return;
 263                }
 264
 265                try
 266                {
 0267                    _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath);
 268
 0269                    _process.Kill();
 0270                }
 0271                catch (Exception ex)
 272                {
 0273                    _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath);
 0274                }
 275            }
 0276        }
 277
 278        /// <summary>
 279        /// Processes the exited.
 280        /// </summary>
 281        private void OnFfMpegProcessExited(Process process)
 282        {
 0283            using (process)
 284            {
 0285                _hasExited = true;
 286
 0287                _logFileStream?.Dispose();
 0288                _logFileStream = null;
 289
 0290                var exitCode = process.ExitCode;
 291
 0292                _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath)
 293
 0294                if (exitCode == 0)
 295                {
 0296                    _taskCompletionSource.TrySetResult(true);
 297                }
 298                else
 299                {
 0300                    _taskCompletionSource.TrySetException(
 0301                        new FfmpegException(
 0302                            string.Format(
 0303                                CultureInfo.InvariantCulture,
 0304                                "Recording for {0} failed. Exit code {1}",
 0305                                _targetPath,
 0306                                exitCode)));
 307                }
 0308            }
 0309        }
 310
 311        private async Task StartStreamingLog(Stream source, FileStream target)
 312        {
 313            try
 314            {
 315                using (var reader = new StreamReader(source))
 316                {
 317                    await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
 318                    {
 319                        var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
 320
 321                        await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false);
 322                        await target.FlushAsync().ConfigureAwait(false);
 323                    }
 324                }
 325            }
 326            catch (Exception ex)
 327            {
 328                _logger.LogError(ex, "Error reading ffmpeg recording log");
 329            }
 330        }
 331
 332        /// <inheritdoc />
 333        public void Dispose()
 334        {
 0335            Dispose(true);
 0336            GC.SuppressFinalize(this);
 0337        }
 338
 339        /// <summary>
 340        /// Releases unmanaged and optionally managed resources.
 341        /// </summary>
 342        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 343        protected virtual void Dispose(bool disposing)
 344        {
 0345            if (_disposed)
 346            {
 0347                return;
 348            }
 349
 0350            if (disposing)
 351            {
 0352                _logFileStream?.Dispose();
 0353                _process?.Dispose();
 354            }
 355
 0356            _logFileStream = null;
 0357            _process = null;
 358
 0359            _disposed = true;
 0360        }
 361    }
 362}