< 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: 312
Coverable lines: 344
Total lines: 758
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 5/1/2026 - 12:13:05 AM Line coverage: 9.3% (32/343) Branch coverage: 1.8% (3/164) Total lines: 7577/18/2026 - 12:15:19 AM Line coverage: 9.3% (32/344) Branch coverage: 1.8% (3/164) Total lines: 758 5/1/2026 - 12:13:05 AM Line coverage: 9.3% (32/343) Branch coverage: 1.8% (3/164) Total lines: 7577/18/2026 - 12:15:19 AM Line coverage: 9.3% (32/344) Branch coverage: 1.8% (3/164) Total lines: 758

Coverage delta

Coverage delta 1 -1

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                StandardErrorEncoding = Encoding.UTF8,
 0428                RedirectStandardError = true,
 0429                RedirectStandardInput = true,
 0430                FileName = _mediaEncoder.EncoderPath,
 0431                Arguments = commandLineArguments,
 0432                WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
 0433                ErrorDialog = false
 0434            },
 0435            EnableRaisingEvents = true
 0436        };
 437
 0438        var transcodingJob = OnTranscodeBeginning(
 0439            outputPath,
 0440            state.Request.PlaySessionId,
 0441            state.MediaSource.LiveStreamId,
 0442            Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
 0443            transcodingJobType,
 0444            process,
 0445            state.Request.DeviceId,
 0446            state,
 0447            cancellationTokenSource);
 448
 0449        _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 450
 0451        var logFilePrefix = "FFmpeg.Transcode-";
 0452        if (state.VideoRequest is not null
 0453            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 454        {
 0455            logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 0456                ? "FFmpeg.Remux-"
 0457                : "FFmpeg.DirectStream-";
 458        }
 459
 0460        if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 461        {
 0462            logFilePrefix = "FFmpeg.Remux-";
 463        }
 464
 0465        var logFilePath = Path.Combine(
 0466            _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
 0467            $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()
 468
 469        // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
 0470        Stream logStream = new FileStream(
 0471            logFilePath,
 0472            FileMode.Create,
 0473            FileAccess.Write,
 0474            FileShare.Read,
 0475            IODefaults.FileStreamBufferSize,
 0476            FileOptions.Asynchronous);
 477
 0478        await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Tok
 0479        var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
 0480            Environment.NewLine
 0481            + Environment.NewLine
 0482            + process.StartInfo.FileName + " " + process.StartInfo.Arguments
 0483            + Environment.NewLine
 0484            + Environment.NewLine);
 485
 0486        await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
 487
 0488        process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state);
 489
 490        try
 491        {
 0492            process.Start();
 0493        }
 0494        catch (Exception ex)
 495        {
 0496            _logger.LogError(ex, "Error starting FFmpeg");
 0497            OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 498
 0499            throw;
 500        }
 501
 0502        _logger.LogDebug("Launched FFmpeg process");
 0503        state.TranscodingJob = transcodingJob;
 504
 505        // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
 0506        _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
 507
 508        // Wait for the file to exist before proceeding
 0509        var ffmpegTargetFile = state.WaitForPath ?? outputPath;
 0510        _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
 0511        while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
 512        {
 0513            await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
 514        }
 515
 0516        _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
 517
 0518        if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
 519        {
 0520            await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
 521
 0522            if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
 523            {
 0524                await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
 525            }
 526        }
 527
 0528        if (!transcodingJob.HasExited)
 529        {
 0530            StartThrottler(state, transcodingJob);
 0531            StartSegmentCleaner(state, transcodingJob);
 532        }
 0533        else if (transcodingJob.ExitCode != 0)
 534        {
 0535            throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transco
 536        }
 537
 0538        _logger.LogDebug("StartFfMpeg() finished successfully");
 539
 0540        return transcodingJob;
 0541    }
 542
 543    private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
 544    {
 0545        if (EnableThrottling(state)
 0546            && (_mediaEncoder.IsPkeyPauseSupported
 0547                || _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
 548        {
 0549            transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<T
 0550            transcodingJob.TranscodingThrottler.Start();
 551        }
 0552    }
 553
 554    private static bool EnableThrottling(StreamState state)
 0555        => state.InputProtocol == MediaProtocol.File
 0556           && state.RunTimeTicks.HasValue
 0557           && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks
 0558           && state.IsInputVideo
 0559           && state.VideoType == VideoType.VideoFile;
 560
 561    private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
 562    {
 0563        if (EnableSegmentCleaning(state))
 564        {
 0565            transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.Crea
 0566            transcodingJob.TranscodingSegmentCleaner.Start();
 567        }
 0568    }
 569
 570    private static bool EnableSegmentCleaning(StreamState state)
 0571        => state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
 0572           && state.IsInputVideo
 0573           && state.TranscodingType == TranscodingJobType.Hls
 0574           && state.RunTimeTicks.HasValue
 0575           && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
 576
 577    private TranscodingJob OnTranscodeBeginning(
 578        string path,
 579        string? playSessionId,
 580        string? liveStreamId,
 581        string transcodingJobId,
 582        TranscodingJobType type,
 583        Process process,
 584        string? deviceId,
 585        StreamState state,
 586        CancellationTokenSource cancellationTokenSource)
 587    {
 0588        lock (_activeTranscodingJobs)
 589        {
 0590            var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>())
 0591            {
 0592                Type = type,
 0593                Path = path,
 0594                Process = process,
 0595                ActiveRequestCount = 1,
 0596                DeviceId = deviceId,
 0597                CancellationTokenSource = cancellationTokenSource,
 0598                Id = transcodingJobId,
 0599                PlaySessionId = playSessionId,
 0600                LiveStreamId = liveStreamId,
 0601                MediaSource = state.MediaSource
 0602            };
 603
 0604            _activeTranscodingJobs.Add(job);
 605
 0606            ReportTranscodingProgress(job, state, null, null, null, null, null);
 607
 0608            return job;
 609        }
 0610    }
 611
 612    /// <inheritdoc />
 613    public void OnTranscodeEndRequest(TranscodingJob job)
 614    {
 0615        var activeRequestCount = job.DecrementActiveRequestCount();
 0616        _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", activeRequestCount);
 0617        if (activeRequestCount <= 0)
 618        {
 0619            PingTimer(job, false);
 620        }
 0621    }
 622
 623    private void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
 624    {
 0625        lock (_activeTranscodingJobs)
 626        {
 0627            var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringCom
 628
 0629            if (job is not null)
 630            {
 0631                _activeTranscodingJobs.Remove(job);
 632            }
 0633        }
 634
 0635        if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
 636        {
 0637            _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
 638        }
 0639    }
 640
 641    private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
 642    {
 0643        job.HasExited = true;
 0644        job.ExitCode = process.ExitCode;
 645
 0646        ReportTranscodingProgress(job, state, null, null, null, null, null);
 647
 0648        _logger.LogDebug("Disposing stream resources");
 0649        state.Dispose();
 650
 0651        if (process.ExitCode == 0)
 652        {
 0653            _logger.LogInformation("FFmpeg exited with code 0");
 654        }
 655        else
 656        {
 0657            _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
 658        }
 659
 0660        job.Dispose();
 0661    }
 662
 663    private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
 664    {
 0665        if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
 666        {
 0667            var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
 0668                    new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
 0669                    cancellationTokenSource.Token)
 0670                .ConfigureAwait(false);
 0671            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 672
 0673            _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.Requeste
 674
 0675            if (state.VideoRequest is not null)
 676            {
 0677                _encodingHelper.TryStreamCopy(state, encodingOptions);
 678            }
 679        }
 680
 0681        if (state.MediaSource.BufferMs.HasValue)
 682        {
 0683            await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
 684        }
 0685    }
 686
 687    /// <inheritdoc />
 688    public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type)
 689    {
 0690        lock (_activeTranscodingJobs)
 691        {
 0692            var job = _activeTranscodingJobs
 0693                .FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
 694
 0695            if (job is null)
 696            {
 0697                return null;
 698            }
 699
 0700            job.IncrementActiveRequestCount();
 0701            if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
 702            {
 0703                job.StopKillTimer();
 704            }
 705
 0706            return job;
 707        }
 0708    }
 709
 710    private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
 711    {
 0712        if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
 713        {
 0714            PingTranscodingJob(e.PlaySessionId, e.IsPaused);
 715        }
 0716    }
 717
 718    private void DeleteEncodedMediaCache()
 719    {
 2720        var path = _serverConfigurationManager.GetTranscodePath();
 2721        if (!Directory.Exists(path))
 722        {
 0723            return;
 724        }
 725
 8726        foreach (var file in _fileSystem.GetFilePaths(path, true))
 727        {
 728            try
 729            {
 2730                _fileSystem.DeleteFile(file);
 2731            }
 0732            catch (Exception ex)
 733            {
 0734                _logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
 0735            }
 736        }
 2737    }
 738
 739    /// <summary>
 740    /// Transcoding lock.
 741    /// </summary>
 742    /// <param name="outputPath">The output path of the transcoded file.</param>
 743    /// <param name="cancellationToken">The cancellation token.</param>
 744    /// <returns>An <see cref="IDisposable"/>.</returns>
 745    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 746    public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
 747    {
 0748        return _transcodingLocks.LockAsync(outputPath, cancellationToken);
 749    }
 750
 751    /// <inheritdoc />
 752    public void Dispose()
 753    {
 2754        _sessionManager.PlaybackProgress -= OnPlaybackProgress;
 2755        _sessionManager.PlaybackStart -= OnPlaybackProgress;
 2756        _transcodingLocks.Dispose();
 2757    }
 758}

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()