< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Transcoding.TranscodeManager
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
Line coverage
9%
Covered lines: 32
Uncovered lines: 311
Coverable lines: 343
Total lines: 757
Line coverage: 9.3%
Branch coverage
1%
Covered branches: 3
Total branches: 164
Branch coverage: 1.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 16.3% (32/196) Branch coverage: 3.9% (3/76) Total lines: 7574/19/2026 - 12:14:27 AM Line coverage: 9.3% (32/343) Branch coverage: 1.8% (3/164) Total lines: 757 1/23/2026 - 12:11:06 AM Line coverage: 16.3% (32/196) Branch coverage: 3.9% (3/76) Total lines: 7574/19/2026 - 12:14:27 AM Line coverage: 9.3% (32/343) Branch coverage: 1.8% (3/164) Total lines: 757

Coverage delta

Coverage delta 7 -7

Metrics

File(s)

/srv/git/jellyfin/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Runtime.CompilerServices;
 8using System.Text;
 9using System.Text.Json;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using AsyncKeyedLock;
 13using Jellyfin.Data;
 14using Jellyfin.Database.Implementations.Enums;
 15using Jellyfin.Extensions;
 16using MediaBrowser.Common;
 17using MediaBrowser.Common.Configuration;
 18using MediaBrowser.Common.Extensions;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Library;
 21using MediaBrowser.Controller.MediaEncoding;
 22using MediaBrowser.Controller.Session;
 23using MediaBrowser.Controller.Streaming;
 24using MediaBrowser.Model.Dlna;
 25using MediaBrowser.Model.Entities;
 26using MediaBrowser.Model.IO;
 27using MediaBrowser.Model.MediaInfo;
 28using MediaBrowser.Model.Session;
 29using Microsoft.Extensions.Logging;
 30
 31namespace MediaBrowser.MediaEncoding.Transcoding;
 32
 33/// <inheritdoc cref="ITranscodeManager"/>
 34public sealed class TranscodeManager : ITranscodeManager, IDisposable
 35{
 36    private readonly ILoggerFactory _loggerFactory;
 37    private readonly ILogger<TranscodeManager> _logger;
 38    private readonly IFileSystem _fileSystem;
 39    private readonly IApplicationPaths _appPaths;
 40    private readonly IServerConfigurationManager _serverConfigurationManager;
 41    private readonly IUserManager _userManager;
 42    private readonly ISessionManager _sessionManager;
 43    private readonly EncodingHelper _encodingHelper;
 44    private readonly IMediaEncoder _mediaEncoder;
 45    private readonly IMediaSourceManager _mediaSourceManager;
 46    private readonly IAttachmentExtractor _attachmentExtractor;
 47
 248    private readonly List<TranscodingJob> _activeTranscodingJobs = new();
 249    private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
 250    {
 251        o.PoolSize = 20;
 252        o.PoolInitialFill = 1;
 253    });
 54
 255    private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
 56
 57    /// <summary>
 58    /// Initializes a new instance of the <see cref="TranscodeManager"/> class.
 59    /// </summary>
 60    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
 61    /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
 62    /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
 63    /// <param name="serverConfigurationManager">The <see cref="IServerConfigurationManager"/>.</param>
 64    /// <param name="userManager">The <see cref="IUserManager"/>.</param>
 65    /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
 66    /// <param name="encodingHelper">The <see cref="EncodingHelper"/>.</param>
 67    /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
 68    /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
 69    /// <param name="attachmentExtractor">The <see cref="IAttachmentExtractor"/>.</param>
 70    public TranscodeManager(
 71        ILoggerFactory loggerFactory,
 72        IFileSystem fileSystem,
 73        IApplicationPaths appPaths,
 74        IServerConfigurationManager serverConfigurationManager,
 75        IUserManager userManager,
 76        ISessionManager sessionManager,
 77        EncodingHelper encodingHelper,
 78        IMediaEncoder mediaEncoder,
 79        IMediaSourceManager mediaSourceManager,
 80        IAttachmentExtractor attachmentExtractor)
 81    {
 282        _loggerFactory = loggerFactory;
 283        _fileSystem = fileSystem;
 284        _appPaths = appPaths;
 285        _serverConfigurationManager = serverConfigurationManager;
 286        _userManager = userManager;
 287        _sessionManager = sessionManager;
 288        _encodingHelper = encodingHelper;
 289        _mediaEncoder = mediaEncoder;
 290        _mediaSourceManager = mediaSourceManager;
 291        _attachmentExtractor = attachmentExtractor;
 92
 293        _logger = loggerFactory.CreateLogger<TranscodeManager>();
 294        DeleteEncodedMediaCache();
 295        _sessionManager.PlaybackProgress += OnPlaybackProgress;
 296        _sessionManager.PlaybackStart += OnPlaybackProgress;
 297    }
 98
 99    /// <inheritdoc />
 100    public TranscodingJob? GetTranscodingJob(string playSessionId)
 101    {
 0102        lock (_activeTranscodingJobs)
 103        {
 0104            return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringCompar
 105        }
 0106    }
 107
 108    /// <inheritdoc />
 109    public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type)
 110    {
 0111        lock (_activeTranscodingJobs)
 112        {
 0113            return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringCompar
 114        }
 0115    }
 116
 117    /// <inheritdoc />
 118    public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
 119    {
 0120        ArgumentException.ThrowIfNullOrEmpty(playSessionId);
 121
 0122        _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
 123
 124        List<TranscodingJob> jobs;
 125
 0126        lock (_activeTranscodingJobs)
 127        {
 128            // This is really only needed for HLS.
 129            // Progressive streams can stop on their own reliably.
 0130            jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.Ordi
 0131        }
 132
 0133        foreach (var job in jobs)
 134        {
 0135            if (isUserPaused.HasValue)
 136            {
 0137                _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
 0138                job.IsUserPaused = isUserPaused.Value;
 139            }
 140
 0141            PingTimer(job, true);
 142        }
 0143    }
 144
 145    private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
 146    {
 0147        if (job.HasExited)
 148        {
 0149            job.StopKillTimer();
 0150            return;
 151        }
 152
 0153        var timerDuration = 10000;
 154
 0155        if (job.Type != TranscodingJobType.Progressive)
 156        {
 0157            timerDuration = 60000;
 158        }
 159
 0160        job.PingTimeout = timerDuration;
 0161        job.LastPingDate = DateTime.UtcNow;
 162
 163        // Don't start the timer for playback checkins with progressive streaming
 0164        if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
 165        {
 0166            job.StartKillTimer(OnTranscodeKillTimerStopped);
 167        }
 168        else
 169        {
 0170            job.ChangeKillTimerIfStarted();
 171        }
 0172    }
 173
 174    private async void OnTranscodeKillTimerStopped(object? state)
 175    {
 0176        var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(Transc
 0177        if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
 178        {
 0179            var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
 180
 0181            if (timeSinceLastPing < job.PingTimeout)
 182            {
 0183                job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
 0184                return;
 185            }
 186        }
 187
 0188        _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", jo
 189
 0190        await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
 0191    }
 192
 193    /// <inheritdoc />
 194    public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
 195    {
 0196        var jobs = new List<TranscodingJob>();
 197
 0198        lock (_activeTranscodingJobs)
 199        {
 200            // This is really only needed for HLS.
 201            // Progressive streams can stop on their own reliably.
 0202            jobs.AddRange(_activeTranscodingJobs.Where(j => string.IsNullOrWhiteSpace(playSessionId)
 0203                ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
 0204                : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)));
 0205        }
 206
 0207        return Task.WhenAll(GetKillJobs());
 208
 209        IEnumerable<Task> GetKillJobs()
 210        {
 211            foreach (var job in jobs)
 212            {
 213                yield return KillTranscodingJob(job, false, deleteFiles);
 214            }
 215        }
 216    }
 217
 218    private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
 219    {
 0220        job.DisposeKillTimer();
 221
 0222        _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessio
 223
 0224        lock (_activeTranscodingJobs)
 225        {
 0226            _activeTranscodingJobs.Remove(job);
 227
 0228            if (job.CancellationTokenSource?.IsCancellationRequested == false)
 229            {
 230#pragma warning disable CA1849 // Can't await in lock block
 0231                job.CancellationTokenSource.Cancel();
 232#pragma warning restore CA1849
 233            }
 0234        }
 235
 0236        job.Stop();
 237
 0238        if (delete(job.Path!))
 239        {
 0240            await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
 241        }
 242
 0243        if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
 244        {
 0245            await _sessionManager.CloseLiveStreamIfNeededAsync(job.LiveStreamId, job.PlaySessionId).ConfigureAwait(false
 246        }
 0247    }
 248
 249    private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
 250    {
 0251        if (retryCount >= 10)
 252        {
 0253            return;
 254        }
 255
 0256        _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
 257
 0258        await Task.Delay(delayMs).ConfigureAwait(false);
 259
 260        try
 261        {
 0262            if (jobType == TranscodingJobType.Progressive)
 263            {
 0264                DeleteProgressivePartialStreamFiles(path);
 265            }
 266            else
 267            {
 0268                DeleteHlsPartialStreamFiles(path);
 269            }
 0270        }
 271        catch (IOException ex)
 272        {
 0273            _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
 274
 0275            await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
 276        }
 0277        catch (Exception ex)
 278        {
 0279            _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
 0280        }
 0281    }
 282
 283    private void DeleteProgressivePartialStreamFiles(string outputFilePath)
 284    {
 0285        if (File.Exists(outputFilePath))
 286        {
 0287            _fileSystem.DeleteFile(outputFilePath);
 288        }
 0289    }
 290
 291    private void DeleteHlsPartialStreamFiles(string outputFilePath)
 292    {
 0293        var directory = Path.GetDirectoryName(outputFilePath)
 0294                        ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
 295
 0296        var name = Path.GetFileNameWithoutExtension(outputFilePath);
 297
 0298        var filesToDelete = _fileSystem.GetFilePaths(directory)
 0299            .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase));
 300
 0301        List<Exception>? exs = null;
 0302        foreach (var file in filesToDelete)
 303        {
 304            try
 305            {
 0306                _logger.LogDebug("Deleting HLS file {0}", file);
 0307                _fileSystem.DeleteFile(file);
 0308            }
 0309            catch (IOException ex)
 310            {
 0311                (exs ??= new List<Exception>()).Add(ex);
 0312                _logger.LogError(ex, "Error deleting HLS file {Path}", file);
 0313            }
 314        }
 315
 0316        if (exs is not null)
 317        {
 0318            throw new AggregateException("Error deleting HLS files", exs);
 319        }
 0320    }
 321
 322    /// <inheritdoc />
 323    public void ReportTranscodingProgress(
 324        TranscodingJob job,
 325        StreamState state,
 326        TimeSpan? transcodingPosition,
 327        float? framerate,
 328        double? percentComplete,
 329        long? bytesTranscoded,
 330        int? bitRate)
 331    {
 0332        var ticks = transcodingPosition?.Ticks;
 333
 0334        if (job is not null)
 335        {
 0336            job.Framerate = framerate;
 0337            job.CompletionPercentage = percentComplete;
 0338            job.TranscodingPositionTicks = ticks;
 0339            job.BytesTranscoded = bytesTranscoded;
 0340            job.BitRate = bitRate;
 341        }
 342
 0343        var deviceId = state.Request.DeviceId;
 344
 0345        if (!string.IsNullOrWhiteSpace(deviceId))
 346        {
 0347            var audioCodec = state.ActualOutputAudioCodec;
 0348            var videoCodec = state.ActualOutputVideoCodec;
 0349            var hardwareAccelerationType = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
 350
 0351            _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
 0352            {
 0353                Bitrate = bitRate ?? state.TotalOutputBitrate,
 0354                AudioCodec = audioCodec,
 0355                VideoCodec = videoCodec,
 0356                Container = state.OutputContainer,
 0357                Framerate = framerate,
 0358                CompletionPercentage = percentComplete,
 0359                Width = state.OutputWidth,
 0360                Height = state.OutputHeight,
 0361                AudioChannels = state.OutputAudioChannels,
 0362                IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
 0363                IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
 0364                HardwareAccelerationType = hardwareAccelerationType,
 0365                TranscodeReasons = state.TranscodeReasons
 0366            });
 367        }
 0368    }
 369
 370    /// <inheritdoc />
 371    public async Task<TranscodingJob> StartFfMpeg(
 372        StreamState state,
 373        string outputPath,
 374        string commandLineArguments,
 375        Guid userId,
 376        TranscodingJobType transcodingJobType,
 377        CancellationTokenSource cancellationTokenSource,
 378        string? workingDirectory = null)
 379    {
 0380        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 0381        Directory.CreateDirectory(directory);
 382
 0383        await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 384
 0385        if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 386        {
 0387            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 0388            if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
 389            {
 0390                OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 391
 0392                throw new ArgumentException("User does not have access to video transcoding.");
 393            }
 394        }
 395
 0396        ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
 397
 398        // If subtitles get burned in fonts may need to be extracted from the media file
 0399        if (state.SubtitleStream is not null && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode || state.
 400        {
 0401            if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
 402            {
 0403                var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
 0404                await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, cancellationTokenSource.
 405            }
 406            else
 407            {
 0408                await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, cancellationTokenSo
 409            }
 410
 0411            if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", 
 412            {
 0413                await _attachmentExtractor.ExtractAllAttachments(state.SubtitleStream.Path, state.MediaSource, cancellat
 414            }
 415        }
 416
 0417        var process = new Process
 0418        {
 0419            StartInfo = new ProcessStartInfo
 0420            {
 0421                WindowStyle = ProcessWindowStyle.Hidden,
 0422                CreateNoWindow = true,
 0423                UseShellExecute = false,
 0424
 0425                // Must consume both stdout and stderr or deadlocks may occur
 0426                // RedirectStandardOutput = true,
 0427                RedirectStandardError = true,
 0428                RedirectStandardInput = true,
 0429                FileName = _mediaEncoder.EncoderPath,
 0430                Arguments = commandLineArguments,
 0431                WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
 0432                ErrorDialog = false
 0433            },
 0434            EnableRaisingEvents = true
 0435        };
 436
 0437        var transcodingJob = OnTranscodeBeginning(
 0438            outputPath,
 0439            state.Request.PlaySessionId,
 0440            state.MediaSource.LiveStreamId,
 0441            Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
 0442            transcodingJobType,
 0443            process,
 0444            state.Request.DeviceId,
 0445            state,
 0446            cancellationTokenSource);
 447
 0448        _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 449
 0450        var logFilePrefix = "FFmpeg.Transcode-";
 0451        if (state.VideoRequest is not null
 0452            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 453        {
 0454            logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 0455                ? "FFmpeg.Remux-"
 0456                : "FFmpeg.DirectStream-";
 457        }
 458
 0459        if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 460        {
 0461            logFilePrefix = "FFmpeg.Remux-";
 462        }
 463
 0464        var logFilePath = Path.Combine(
 0465            _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
 0466            $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()
 467
 468        // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
 0469        Stream logStream = new FileStream(
 0470            logFilePath,
 0471            FileMode.Create,
 0472            FileAccess.Write,
 0473            FileShare.Read,
 0474            IODefaults.FileStreamBufferSize,
 0475            FileOptions.Asynchronous);
 476
 0477        await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Tok
 0478        var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
 0479            Environment.NewLine
 0480            + Environment.NewLine
 0481            + process.StartInfo.FileName + " " + process.StartInfo.Arguments
 0482            + Environment.NewLine
 0483            + Environment.NewLine);
 484
 0485        await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
 486
 0487        process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state);
 488
 489        try
 490        {
 0491            process.Start();
 0492        }
 0493        catch (Exception ex)
 494        {
 0495            _logger.LogError(ex, "Error starting FFmpeg");
 0496            OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 497
 0498            throw;
 499        }
 500
 0501        _logger.LogDebug("Launched FFmpeg process");
 0502        state.TranscodingJob = transcodingJob;
 503
 504        // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
 0505        _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
 506
 507        // Wait for the file to exist before proceeding
 0508        var ffmpegTargetFile = state.WaitForPath ?? outputPath;
 0509        _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
 0510        while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
 511        {
 0512            await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
 513        }
 514
 0515        _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
 516
 0517        if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
 518        {
 0519            await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
 520
 0521            if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
 522            {
 0523                await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
 524            }
 525        }
 526
 0527        if (!transcodingJob.HasExited)
 528        {
 0529            StartThrottler(state, transcodingJob);
 0530            StartSegmentCleaner(state, transcodingJob);
 531        }
 0532        else if (transcodingJob.ExitCode != 0)
 533        {
 0534            throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transco
 535        }
 536
 0537        _logger.LogDebug("StartFfMpeg() finished successfully");
 538
 0539        return transcodingJob;
 0540    }
 541
 542    private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
 543    {
 0544        if (EnableThrottling(state)
 0545            && (_mediaEncoder.IsPkeyPauseSupported
 0546                || _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
 547        {
 0548            transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<T
 0549            transcodingJob.TranscodingThrottler.Start();
 550        }
 0551    }
 552
 553    private static bool EnableThrottling(StreamState state)
 0554        => state.InputProtocol == MediaProtocol.File
 0555           && state.RunTimeTicks.HasValue
 0556           && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks
 0557           && state.IsInputVideo
 0558           && state.VideoType == VideoType.VideoFile;
 559
 560    private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
 561    {
 0562        if (EnableSegmentCleaning(state))
 563        {
 0564            transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.Crea
 0565            transcodingJob.TranscodingSegmentCleaner.Start();
 566        }
 0567    }
 568
 569    private static bool EnableSegmentCleaning(StreamState state)
 0570        => state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
 0571           && state.IsInputVideo
 0572           && state.TranscodingType == TranscodingJobType.Hls
 0573           && state.RunTimeTicks.HasValue
 0574           && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
 575
 576    private TranscodingJob OnTranscodeBeginning(
 577        string path,
 578        string? playSessionId,
 579        string? liveStreamId,
 580        string transcodingJobId,
 581        TranscodingJobType type,
 582        Process process,
 583        string? deviceId,
 584        StreamState state,
 585        CancellationTokenSource cancellationTokenSource)
 586    {
 0587        lock (_activeTranscodingJobs)
 588        {
 0589            var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>())
 0590            {
 0591                Type = type,
 0592                Path = path,
 0593                Process = process,
 0594                ActiveRequestCount = 1,
 0595                DeviceId = deviceId,
 0596                CancellationTokenSource = cancellationTokenSource,
 0597                Id = transcodingJobId,
 0598                PlaySessionId = playSessionId,
 0599                LiveStreamId = liveStreamId,
 0600                MediaSource = state.MediaSource
 0601            };
 602
 0603            _activeTranscodingJobs.Add(job);
 604
 0605            ReportTranscodingProgress(job, state, null, null, null, null, null);
 606
 0607            return job;
 608        }
 0609    }
 610
 611    /// <inheritdoc />
 612    public void OnTranscodeEndRequest(TranscodingJob job)
 613    {
 0614        job.ActiveRequestCount--;
 0615        _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
 0616        if (job.ActiveRequestCount <= 0)
 617        {
 0618            PingTimer(job, false);
 619        }
 0620    }
 621
 622    private void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
 623    {
 0624        lock (_activeTranscodingJobs)
 625        {
 0626            var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringCom
 627
 0628            if (job is not null)
 629            {
 0630                _activeTranscodingJobs.Remove(job);
 631            }
 0632        }
 633
 0634        if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
 635        {
 0636            _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
 637        }
 0638    }
 639
 640    private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
 641    {
 0642        job.HasExited = true;
 0643        job.ExitCode = process.ExitCode;
 644
 0645        ReportTranscodingProgress(job, state, null, null, null, null, null);
 646
 0647        _logger.LogDebug("Disposing stream resources");
 0648        state.Dispose();
 649
 0650        if (process.ExitCode == 0)
 651        {
 0652            _logger.LogInformation("FFmpeg exited with code 0");
 653        }
 654        else
 655        {
 0656            _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
 657        }
 658
 0659        job.Dispose();
 0660    }
 661
 662    private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
 663    {
 0664        if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
 665        {
 0666            var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
 0667                    new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
 0668                    cancellationTokenSource.Token)
 0669                .ConfigureAwait(false);
 0670            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 671
 0672            _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.Requeste
 673
 0674            if (state.VideoRequest is not null)
 675            {
 0676                _encodingHelper.TryStreamCopy(state, encodingOptions);
 677            }
 678        }
 679
 0680        if (state.MediaSource.BufferMs.HasValue)
 681        {
 0682            await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
 683        }
 0684    }
 685
 686    /// <inheritdoc />
 687    public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type)
 688    {
 0689        lock (_activeTranscodingJobs)
 690        {
 0691            var job = _activeTranscodingJobs
 0692                .FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
 693
 0694            if (job is null)
 695            {
 0696                return null;
 697            }
 698
 0699            job.ActiveRequestCount++;
 0700            if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
 701            {
 0702                job.StopKillTimer();
 703            }
 704
 0705            return job;
 706        }
 0707    }
 708
 709    private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
 710    {
 0711        if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
 712        {
 0713            PingTranscodingJob(e.PlaySessionId, e.IsPaused);
 714        }
 0715    }
 716
 717    private void DeleteEncodedMediaCache()
 718    {
 2719        var path = _serverConfigurationManager.GetTranscodePath();
 2720        if (!Directory.Exists(path))
 721        {
 0722            return;
 723        }
 724
 8725        foreach (var file in _fileSystem.GetFilePaths(path, true))
 726        {
 727            try
 728            {
 2729                _fileSystem.DeleteFile(file);
 2730            }
 0731            catch (Exception ex)
 732            {
 0733                _logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
 0734            }
 735        }
 2736    }
 737
 738    /// <summary>
 739    /// Transcoding lock.
 740    /// </summary>
 741    /// <param name="outputPath">The output path of the transcoded file.</param>
 742    /// <param name="cancellationToken">The cancellation token.</param>
 743    /// <returns>An <see cref="IDisposable"/>.</returns>
 744    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 745    public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
 746    {
 0747        return _transcodingLocks.LockAsync(outputPath, cancellationToken);
 748    }
 749
 750    /// <inheritdoc />
 751    public void Dispose()
 752    {
 2753        _sessionManager.PlaybackProgress -= OnPlaybackProgress;
 2754        _sessionManager.PlaybackStart -= OnPlaybackProgress;
 2755        _transcodingLocks.Dispose();
 2756    }
 757}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILoggerFactory,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Common.Configuration.IApplicationPaths,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Session.ISessionManager,MediaBrowser.Controller.MediaEncoding.EncodingHelper,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,MediaBrowser.Controller.Library.IMediaSourceManager,MediaBrowser.Controller.MediaEncoding.IAttachmentExtractor)
GetTranscodingJob(System.String)
GetTranscodingJob(System.String,MediaBrowser.Controller.MediaEncoding.TranscodingJobType)
PingTranscodingJob(System.String,System.Nullable`1<System.Boolean>)
PingTimer(MediaBrowser.Controller.MediaEncoding.TranscodingJob,System.Boolean)
OnTranscodeKillTimerStopped()
KillTranscodingJobs(System.String,System.String,System.Func`2<System.String,System.Boolean>)
KillTranscodingJob()
DeletePartialStreamFiles()
DeleteProgressivePartialStreamFiles(System.String)
DeleteHlsPartialStreamFiles(System.String)
ReportTranscodingProgress(MediaBrowser.Controller.MediaEncoding.TranscodingJob,MediaBrowser.Controller.Streaming.StreamState,System.Nullable`1<System.TimeSpan>,System.Nullable`1<System.Single>,System.Nullable`1<System.Double>,System.Nullable`1<System.Int64>,System.Nullable`1<System.Int32>)
StartFfMpeg()
StartThrottler(MediaBrowser.Controller.Streaming.StreamState,MediaBrowser.Controller.MediaEncoding.TranscodingJob)
EnableThrottling(MediaBrowser.Controller.Streaming.StreamState)
StartSegmentCleaner(MediaBrowser.Controller.Streaming.StreamState,MediaBrowser.Controller.MediaEncoding.TranscodingJob)
EnableSegmentCleaning(MediaBrowser.Controller.Streaming.StreamState)
OnTranscodeBeginning(System.String,System.String,System.String,System.String,MediaBrowser.Controller.MediaEncoding.TranscodingJobType,System.Diagnostics.Process,System.String,MediaBrowser.Controller.Streaming.StreamState,System.Threading.CancellationTokenSource)
OnTranscodeEndRequest(MediaBrowser.Controller.MediaEncoding.TranscodingJob)
OnTranscodeFailedToStart(System.String,MediaBrowser.Controller.MediaEncoding.TranscodingJobType,MediaBrowser.Controller.Streaming.StreamState)
OnFfMpegProcessExited(System.Diagnostics.Process,MediaBrowser.Controller.MediaEncoding.TranscodingJob,MediaBrowser.Controller.Streaming.StreamState)
AcquireResources()
OnTranscodeBeginRequest(System.String,MediaBrowser.Controller.MediaEncoding.TranscodingJobType)
OnPlaybackProgress(System.Object,MediaBrowser.Controller.Library.PlaybackProgressEventArgs)
DeleteEncodedMediaCache()
LockAsync(System.String,System.Threading.CancellationToken)
Dispose()