< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Transcoding.TranscodeManager
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
Line coverage
15%
Covered lines: 30
Uncovered lines: 166
Coverable lines: 196
Total lines: 757
Line coverage: 15.3%
Branch coverage
2%
Covered branches: 2
Total branches: 76
Branch coverage: 2.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

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    {
 176        var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(Transc
 177        if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
 178        {
 179            var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
 180
 181            if (timeSinceLastPing < job.PingTimeout)
 182            {
 183                job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
 184                return;
 185            }
 186        }
 187
 188        _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", jo
 189
 190        await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
 191    }
 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    {
 220        job.DisposeKillTimer();
 221
 222        _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessio
 223
 224        lock (_activeTranscodingJobs)
 225        {
 226            _activeTranscodingJobs.Remove(job);
 227
 228            if (job.CancellationTokenSource?.IsCancellationRequested == false)
 229            {
 230#pragma warning disable CA1849 // Can't await in lock block
 231                job.CancellationTokenSource.Cancel();
 232#pragma warning restore CA1849
 233            }
 234        }
 235
 236        job.Stop();
 237
 238        if (delete(job.Path!))
 239        {
 240            await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
 241        }
 242
 243        if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
 244        {
 245            await _sessionManager.CloseLiveStreamIfNeededAsync(job.LiveStreamId, job.PlaySessionId).ConfigureAwait(false
 246        }
 247    }
 248
 249    private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
 250    {
 251        if (retryCount >= 10)
 252        {
 253            return;
 254        }
 255
 256        _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
 257
 258        await Task.Delay(delayMs).ConfigureAwait(false);
 259
 260        try
 261        {
 262            if (jobType == TranscodingJobType.Progressive)
 263            {
 264                DeleteProgressivePartialStreamFiles(path);
 265            }
 266            else
 267            {
 268                DeleteHlsPartialStreamFiles(path);
 269            }
 270        }
 271        catch (IOException ex)
 272        {
 273            _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
 274
 275            await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
 276        }
 277        catch (Exception ex)
 278        {
 279            _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
 280        }
 281    }
 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    {
 380        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 381        Directory.CreateDirectory(directory);
 382
 383        await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 384
 385        if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 386        {
 387            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 388            if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
 389            {
 390                OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 391
 392                throw new ArgumentException("User does not have access to video transcoding.");
 393            }
 394        }
 395
 396        ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
 397
 398        // If subtitles get burned in fonts may need to be extracted from the media file
 399        if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
 400        {
 401            if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
 402            {
 403                var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
 404                await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, cancellationTokenSource.
 405            }
 406            else
 407            {
 408                await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, cancellationTokenSo
 409            }
 410
 411            if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", 
 412            {
 413                await _attachmentExtractor.ExtractAllAttachments(state.SubtitleStream.Path, state.MediaSource, cancellat
 414            }
 415        }
 416
 417        var process = new Process
 418        {
 419            StartInfo = new ProcessStartInfo
 420            {
 421                WindowStyle = ProcessWindowStyle.Hidden,
 422                CreateNoWindow = true,
 423                UseShellExecute = false,
 424
 425                // Must consume both stdout and stderr or deadlocks may occur
 426                // RedirectStandardOutput = true,
 427                RedirectStandardError = true,
 428                RedirectStandardInput = true,
 429                FileName = _mediaEncoder.EncoderPath,
 430                Arguments = commandLineArguments,
 431                WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
 432                ErrorDialog = false
 433            },
 434            EnableRaisingEvents = true
 435        };
 436
 437        var transcodingJob = OnTranscodeBeginning(
 438            outputPath,
 439            state.Request.PlaySessionId,
 440            state.MediaSource.LiveStreamId,
 441            Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
 442            transcodingJobType,
 443            process,
 444            state.Request.DeviceId,
 445            state,
 446            cancellationTokenSource);
 447
 448        _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 449
 450        var logFilePrefix = "FFmpeg.Transcode-";
 451        if (state.VideoRequest is not null
 452            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 453        {
 454            logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 455                ? "FFmpeg.Remux-"
 456                : "FFmpeg.DirectStream-";
 457        }
 458
 459        if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 460        {
 461            logFilePrefix = "FFmpeg.Remux-";
 462        }
 463
 464        var logFilePath = Path.Combine(
 465            _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
 466            $"{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.
 469        Stream logStream = new FileStream(
 470            logFilePath,
 471            FileMode.Create,
 472            FileAccess.Write,
 473            FileShare.Read,
 474            IODefaults.FileStreamBufferSize,
 475            FileOptions.Asynchronous);
 476
 477        await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Tok
 478        var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
 479            Environment.NewLine
 480            + Environment.NewLine
 481            + process.StartInfo.FileName + " " + process.StartInfo.Arguments
 482            + Environment.NewLine
 483            + Environment.NewLine);
 484
 485        await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
 486
 487        process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state);
 488
 489        try
 490        {
 491            process.Start();
 492        }
 493        catch (Exception ex)
 494        {
 495            _logger.LogError(ex, "Error starting FFmpeg");
 496            OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 497
 498            throw;
 499        }
 500
 501        _logger.LogDebug("Launched FFmpeg process");
 502        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
 505        _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
 506
 507        // Wait for the file to exist before proceeding
 508        var ffmpegTargetFile = state.WaitForPath ?? outputPath;
 509        _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
 510        while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
 511        {
 512            await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
 513        }
 514
 515        _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
 516
 517        if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
 518        {
 519            await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
 520
 521            if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
 522            {
 523                await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
 524            }
 525        }
 526
 527        if (!transcodingJob.HasExited)
 528        {
 529            StartThrottler(state, transcodingJob);
 530            StartSegmentCleaner(state, transcodingJob);
 531        }
 532        else if (transcodingJob.ExitCode != 0)
 533        {
 534            throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transco
 535        }
 536
 537        _logger.LogDebug("StartFfMpeg() finished successfully");
 538
 539        return transcodingJob;
 540    }
 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    {
 664        if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
 665        {
 666            var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
 667                    new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
 668                    cancellationTokenSource.Token)
 669                .ConfigureAwait(false);
 670            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 671
 672            _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.Requeste
 673
 674            if (state.VideoRequest is not null)
 675            {
 676                _encodingHelper.TryStreamCopy(state);
 677            }
 678        }
 679
 680        if (state.MediaSource.BufferMs.HasValue)
 681        {
 682            await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
 683        }
 684    }
 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
 4725        foreach (var file in _fileSystem.GetFilePaths(path, true))
 726        {
 727            try
 728            {
 0729                _fileSystem.DeleteFile(file);
 0730            }
 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)
KillTranscodingJobs(System.String,System.String,System.Func`2<System.String,System.Boolean>)
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>)
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)
OnTranscodeBeginRequest(System.String,MediaBrowser.Controller.MediaEncoding.TranscodingJobType)
OnPlaybackProgress(System.Object,MediaBrowser.Controller.Library.PlaybackProgressEventArgs)
DeleteEncodedMediaCache()
LockAsync(System.String,System.Threading.CancellationToken)
Dispose()