< 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: 98
Coverable lines: 98
Total lines: 338
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 34
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%210140%
GetAudioArgs(...)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            if (!File.Exists(targetFile))
 77            {
 78                FileHelper.CreateEmpty(targetFile);
 79            }
 80
 81            var processStartInfo = new ProcessStartInfo
 82            {
 83                CreateNoWindow = true,
 84                UseShellExecute = false,
 85
 86                RedirectStandardError = true,
 87                RedirectStandardInput = true,
 88
 89                FileName = _mediaEncoder.EncoderPath,
 90                Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile),
 91
 92                WindowStyle = ProcessWindowStyle.Hidden,
 93                ErrorDialog = false
 94            };
 95
 96            _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments);
 97
 98            var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
 99            Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
 100
 101            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log direct
 102            _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefault
 103
 104            await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureA
 105            await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processSt
 106
 107            _process = new Process
 108            {
 109                StartInfo = processStartInfo,
 110                EnableRaisingEvents = true
 111            };
 112            _process.Exited += (_, _) => OnFfMpegProcessExited(_process);
 113
 114            _process.Start();
 115
 116            cancellationToken.Register(Stop);
 117
 118            onStarted();
 119
 120            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
 121            _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
 122
 123            _logger.LogInformation("ffmpeg recording process started for {Path}", _targetPath);
 124
 125            // Block until ffmpeg exits
 126            await _taskCompletionSource.Task.ConfigureAwait(false);
 127        }
 128
 129        private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
 130        {
 0131            string videoArgs = "-codec:v:0 copy -fflags +genpts";
 132
 0133            var flags = new List<string>();
 0134            if (mediaSource.IgnoreDts)
 135            {
 0136                flags.Add("+igndts");
 137            }
 138
 0139            if (mediaSource.IgnoreIndex)
 140            {
 0141                flags.Add("+ignidx");
 142            }
 143
 0144            if (mediaSource.GenPtsInput)
 145            {
 0146                flags.Add("+genpts");
 147            }
 148
 0149            var inputModifier = "-async 1";
 150
 0151            if (flags.Count > 0)
 152            {
 0153                inputModifier += " -fflags " + string.Join(string.Empty, flags);
 154            }
 155
 0156            if (mediaSource.ReadAtNativeFramerate)
 157            {
 0158                inputModifier += " -re";
 159            }
 160
 0161            if (mediaSource.RequiresLooping)
 162            {
 0163                inputModifier += " -stream_loop -1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2";
 164            }
 165
 0166            var analyzeDurationSeconds = 5;
 0167            var analyzeDuration = " -analyzeduration " +
 0168                  (analyzeDurationSeconds * 1000000).ToString(CultureInfo.InvariantCulture);
 0169            inputModifier += analyzeDuration;
 170
 0171            var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
 172
 173            // var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase
 174            //    " -f mp4 -movflags frag_keyframe+empty_moov" :
 175            //    string.Empty;
 176
 0177            var outputParam = string.Empty;
 178
 0179            var threads = EncodingHelper.GetNumberOfThreads(null, _serverConfigurationManager.GetEncodingOptions(), null
 0180            var commandLineArgs = string.Format(
 0181                CultureInfo.InvariantCulture,
 0182                "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
 0183                inputTempFile,
 0184                targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
 0185                videoArgs,
 0186                GetAudioArgs(mediaSource),
 0187                subtitleArgs,
 0188                outputParam,
 0189                threads);
 190
 0191            return inputModifier + " " + commandLineArgs;
 192        }
 193
 194        private static string GetAudioArgs(MediaSourceInfo mediaSource)
 195        {
 0196            return "-codec:a:0 copy";
 197        }
 198
 199        protected string GetOutputSizeParam()
 0200            => "-vf \"yadif=0:-1:0\"";
 201
 202        private void Stop()
 203        {
 0204            if (!_hasExited)
 205            {
 206                try
 207                {
 0208                    _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath);
 209
 0210                    _process.StandardInput.WriteLine("q");
 0211                }
 0212                catch (Exception ex)
 213                {
 0214                    _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath);
 0215                }
 216
 0217                if (_hasExited)
 218                {
 0219                    return;
 220                }
 221
 222                try
 223                {
 0224                    _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath);
 225
 0226                    if (_process.WaitForExit(10000))
 227                    {
 0228                        return;
 229                    }
 0230                }
 0231                catch (Exception ex)
 232                {
 0233                    _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath);
 0234                }
 235
 0236                if (_hasExited)
 237                {
 0238                    return;
 239                }
 240
 241                try
 242                {
 0243                    _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath);
 244
 0245                    _process.Kill();
 0246                }
 0247                catch (Exception ex)
 248                {
 0249                    _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath);
 0250                }
 251            }
 0252        }
 253
 254        /// <summary>
 255        /// Processes the exited.
 256        /// </summary>
 257        private void OnFfMpegProcessExited(Process process)
 258        {
 0259            using (process)
 260            {
 0261                _hasExited = true;
 262
 0263                _logFileStream?.Dispose();
 0264                _logFileStream = null;
 265
 0266                var exitCode = process.ExitCode;
 267
 0268                _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath)
 269
 0270                if (exitCode == 0)
 271                {
 0272                    _taskCompletionSource.TrySetResult(true);
 273                }
 274                else
 275                {
 0276                    _taskCompletionSource.TrySetException(
 0277                        new FfmpegException(
 0278                            string.Format(
 0279                                CultureInfo.InvariantCulture,
 0280                                "Recording for {0} failed. Exit code {1}",
 0281                                _targetPath,
 0282                                exitCode)));
 283                }
 0284            }
 0285        }
 286
 287        private async Task StartStreamingLog(Stream source, FileStream target)
 288        {
 289            try
 290            {
 291                using (var reader = new StreamReader(source))
 292                {
 293                    await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
 294                    {
 295                        var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
 296
 297                        await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false);
 298                        await target.FlushAsync().ConfigureAwait(false);
 299                    }
 300                }
 301            }
 302            catch (Exception ex)
 303            {
 304                _logger.LogError(ex, "Error reading ffmpeg recording log");
 305            }
 306        }
 307
 308        /// <inheritdoc />
 309        public void Dispose()
 310        {
 0311            Dispose(true);
 0312            GC.SuppressFinalize(this);
 0313        }
 314
 315        /// <summary>
 316        /// Releases unmanaged and optionally managed resources.
 317        /// </summary>
 318        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 319        protected virtual void Dispose(bool disposing)
 320        {
 0321            if (_disposed)
 322            {
 0323                return;
 324            }
 325
 0326            if (disposing)
 327            {
 0328                _logFileStream?.Dispose();
 0329                _process?.Dispose();
 330            }
 331
 0332            _logFileStream = null;
 0333            _process = null;
 334
 0335            _disposed = true;
 0336        }
 337    }
 338}