< 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: 768
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.Enums;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Common;
 16using MediaBrowser.Common.Configuration;
 17using MediaBrowser.Common.Extensions;
 18using MediaBrowser.Controller.Configuration;
 19using MediaBrowser.Controller.Library;
 20using MediaBrowser.Controller.MediaEncoding;
 21using MediaBrowser.Controller.Session;
 22using MediaBrowser.Controller.Streaming;
 23using MediaBrowser.Model.Dlna;
 24using MediaBrowser.Model.Entities;
 25using MediaBrowser.Model.IO;
 26using MediaBrowser.Model.MediaInfo;
 27using MediaBrowser.Model.Session;
 28using Microsoft.Extensions.Logging;
 29
 30namespace MediaBrowser.MediaEncoding.Transcoding;
 31
 32/// <inheritdoc cref="ITranscodeManager"/>
 33public sealed class TranscodeManager : ITranscodeManager, IDisposable
 34{
 35    private readonly ILoggerFactory _loggerFactory;
 36    private readonly ILogger<TranscodeManager> _logger;
 37    private readonly IFileSystem _fileSystem;
 38    private readonly IApplicationPaths _appPaths;
 39    private readonly IServerConfigurationManager _serverConfigurationManager;
 40    private readonly IUserManager _userManager;
 41    private readonly ISessionManager _sessionManager;
 42    private readonly EncodingHelper _encodingHelper;
 43    private readonly IMediaEncoder _mediaEncoder;
 44    private readonly IMediaSourceManager _mediaSourceManager;
 45    private readonly IAttachmentExtractor _attachmentExtractor;
 46
 247    private readonly List<TranscodingJob> _activeTranscodingJobs = new();
 248    private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
 249    {
 250        o.PoolSize = 20;
 251        o.PoolInitialFill = 1;
 252    });
 53
 254    private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
 55
 56    /// <summary>
 57    /// Initializes a new instance of the <see cref="TranscodeManager"/> class.
 58    /// </summary>
 59    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
 60    /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
 61    /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
 62    /// <param name="serverConfigurationManager">The <see cref="IServerConfigurationManager"/>.</param>
 63    /// <param name="userManager">The <see cref="IUserManager"/>.</param>
 64    /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
 65    /// <param name="encodingHelper">The <see cref="EncodingHelper"/>.</param>
 66    /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
 67    /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
 68    /// <param name="attachmentExtractor">The <see cref="IAttachmentExtractor"/>.</param>
 69    public TranscodeManager(
 70        ILoggerFactory loggerFactory,
 71        IFileSystem fileSystem,
 72        IApplicationPaths appPaths,
 73        IServerConfigurationManager serverConfigurationManager,
 74        IUserManager userManager,
 75        ISessionManager sessionManager,
 76        EncodingHelper encodingHelper,
 77        IMediaEncoder mediaEncoder,
 78        IMediaSourceManager mediaSourceManager,
 79        IAttachmentExtractor attachmentExtractor)
 80    {
 281        _loggerFactory = loggerFactory;
 282        _fileSystem = fileSystem;
 283        _appPaths = appPaths;
 284        _serverConfigurationManager = serverConfigurationManager;
 285        _userManager = userManager;
 286        _sessionManager = sessionManager;
 287        _encodingHelper = encodingHelper;
 288        _mediaEncoder = mediaEncoder;
 289        _mediaSourceManager = mediaSourceManager;
 290        _attachmentExtractor = attachmentExtractor;
 91
 292        _logger = loggerFactory.CreateLogger<TranscodeManager>();
 293        DeleteEncodedMediaCache();
 294        _sessionManager.PlaybackProgress += OnPlaybackProgress;
 295        _sessionManager.PlaybackStart += OnPlaybackProgress;
 296    }
 97
 98    /// <inheritdoc />
 99    public TranscodingJob? GetTranscodingJob(string playSessionId)
 100    {
 0101        lock (_activeTranscodingJobs)
 102        {
 0103            return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringCompar
 104        }
 0105    }
 106
 107    /// <inheritdoc />
 108    public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type)
 109    {
 0110        lock (_activeTranscodingJobs)
 111        {
 0112            return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringCompar
 113        }
 0114    }
 115
 116    /// <inheritdoc />
 117    public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
 118    {
 0119        ArgumentException.ThrowIfNullOrEmpty(playSessionId);
 120
 0121        _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
 122
 123        List<TranscodingJob> jobs;
 124
 0125        lock (_activeTranscodingJobs)
 126        {
 127            // This is really only needed for HLS.
 128            // Progressive streams can stop on their own reliably.
 0129            jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.Ordi
 0130        }
 131
 0132        foreach (var job in jobs)
 133        {
 0134            if (isUserPaused.HasValue)
 135            {
 0136                _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
 0137                job.IsUserPaused = isUserPaused.Value;
 138            }
 139
 0140            PingTimer(job, true);
 141        }
 0142    }
 143
 144    private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
 145    {
 0146        if (job.HasExited)
 147        {
 0148            job.StopKillTimer();
 0149            return;
 150        }
 151
 0152        var timerDuration = 10000;
 153
 0154        if (job.Type != TranscodingJobType.Progressive)
 155        {
 0156            timerDuration = 60000;
 157        }
 158
 0159        job.PingTimeout = timerDuration;
 0160        job.LastPingDate = DateTime.UtcNow;
 161
 162        // Don't start the timer for playback checkins with progressive streaming
 0163        if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
 164        {
 0165            job.StartKillTimer(OnTranscodeKillTimerStopped);
 166        }
 167        else
 168        {
 0169            job.ChangeKillTimerIfStarted();
 170        }
 0171    }
 172
 173    private async void OnTranscodeKillTimerStopped(object? state)
 174    {
 175        var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(Transc
 176        if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
 177        {
 178            var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
 179
 180            if (timeSinceLastPing < job.PingTimeout)
 181            {
 182                job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
 183                return;
 184            }
 185        }
 186
 187        _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", jo
 188
 189        await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
 190    }
 191
 192    /// <inheritdoc />
 193    public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
 194    {
 0195        var jobs = new List<TranscodingJob>();
 196
 0197        lock (_activeTranscodingJobs)
 198        {
 199            // This is really only needed for HLS.
 200            // Progressive streams can stop on their own reliably.
 0201            jobs.AddRange(_activeTranscodingJobs.Where(j => string.IsNullOrWhiteSpace(playSessionId)
 0202                ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
 0203                : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)));
 0204        }
 205
 0206        return Task.WhenAll(GetKillJobs());
 207
 208        IEnumerable<Task> GetKillJobs()
 209        {
 210            foreach (var job in jobs)
 211            {
 212                yield return KillTranscodingJob(job, false, deleteFiles);
 213            }
 214        }
 215    }
 216
 217    private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
 218    {
 219        job.DisposeKillTimer();
 220
 221        _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessio
 222
 223        lock (_activeTranscodingJobs)
 224        {
 225            _activeTranscodingJobs.Remove(job);
 226
 227            if (job.CancellationTokenSource?.IsCancellationRequested == false)
 228            {
 229#pragma warning disable CA1849 // Can't await in lock block
 230                job.CancellationTokenSource.Cancel();
 231#pragma warning restore CA1849
 232            }
 233        }
 234
 235        job.Stop();
 236
 237        if (delete(job.Path!))
 238        {
 239            await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
 240        }
 241
 242        if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
 243        {
 244            try
 245            {
 246                await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
 247            }
 248            catch (Exception ex)
 249            {
 250                _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
 251            }
 252        }
 253    }
 254
 255    private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
 256    {
 257        if (retryCount >= 10)
 258        {
 259            return;
 260        }
 261
 262        _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
 263
 264        await Task.Delay(delayMs).ConfigureAwait(false);
 265
 266        try
 267        {
 268            if (jobType == TranscodingJobType.Progressive)
 269            {
 270                DeleteProgressivePartialStreamFiles(path);
 271            }
 272            else
 273            {
 274                DeleteHlsPartialStreamFiles(path);
 275            }
 276        }
 277        catch (IOException ex)
 278        {
 279            _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
 280
 281            await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
 282        }
 283        catch (Exception ex)
 284        {
 285            _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
 286        }
 287    }
 288
 289    private void DeleteProgressivePartialStreamFiles(string outputFilePath)
 290    {
 0291        if (File.Exists(outputFilePath))
 292        {
 0293            _fileSystem.DeleteFile(outputFilePath);
 294        }
 0295    }
 296
 297    private void DeleteHlsPartialStreamFiles(string outputFilePath)
 298    {
 0299        var directory = Path.GetDirectoryName(outputFilePath)
 0300                        ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
 301
 0302        var name = Path.GetFileNameWithoutExtension(outputFilePath);
 303
 0304        var filesToDelete = _fileSystem.GetFilePaths(directory)
 0305            .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase));
 306
 0307        List<Exception>? exs = null;
 0308        foreach (var file in filesToDelete)
 309        {
 310            try
 311            {
 0312                _logger.LogDebug("Deleting HLS file {0}", file);
 0313                _fileSystem.DeleteFile(file);
 0314            }
 0315            catch (IOException ex)
 316            {
 0317                (exs ??= new List<Exception>()).Add(ex);
 0318                _logger.LogError(ex, "Error deleting HLS file {Path}", file);
 0319            }
 320        }
 321
 0322        if (exs is not null)
 323        {
 0324            throw new AggregateException("Error deleting HLS files", exs);
 325        }
 0326    }
 327
 328    /// <inheritdoc />
 329    public void ReportTranscodingProgress(
 330        TranscodingJob job,
 331        StreamState state,
 332        TimeSpan? transcodingPosition,
 333        float? framerate,
 334        double? percentComplete,
 335        long? bytesTranscoded,
 336        int? bitRate)
 337    {
 0338        var ticks = transcodingPosition?.Ticks;
 339
 0340        if (job is not null)
 341        {
 0342            job.Framerate = framerate;
 0343            job.CompletionPercentage = percentComplete;
 0344            job.TranscodingPositionTicks = ticks;
 0345            job.BytesTranscoded = bytesTranscoded;
 0346            job.BitRate = bitRate;
 347        }
 348
 0349        var deviceId = state.Request.DeviceId;
 350
 0351        if (!string.IsNullOrWhiteSpace(deviceId))
 352        {
 0353            var audioCodec = state.ActualOutputAudioCodec;
 0354            var videoCodec = state.ActualOutputVideoCodec;
 0355            var hardwareAccelerationType = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
 356
 0357            _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
 0358            {
 0359                Bitrate = bitRate ?? state.TotalOutputBitrate,
 0360                AudioCodec = audioCodec,
 0361                VideoCodec = videoCodec,
 0362                Container = state.OutputContainer,
 0363                Framerate = framerate,
 0364                CompletionPercentage = percentComplete,
 0365                Width = state.OutputWidth,
 0366                Height = state.OutputHeight,
 0367                AudioChannels = state.OutputAudioChannels,
 0368                IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
 0369                IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
 0370                HardwareAccelerationType = hardwareAccelerationType,
 0371                TranscodeReasons = state.TranscodeReasons
 0372            });
 373        }
 0374    }
 375
 376    /// <inheritdoc />
 377    public async Task<TranscodingJob> StartFfMpeg(
 378        StreamState state,
 379        string outputPath,
 380        string commandLineArguments,
 381        Guid userId,
 382        TranscodingJobType transcodingJobType,
 383        CancellationTokenSource cancellationTokenSource,
 384        string? workingDirectory = null)
 385    {
 386        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) 
 387        Directory.CreateDirectory(directory);
 388
 389        await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
 390
 391        if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 392        {
 393            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 394            if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
 395            {
 396                OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 397
 398                throw new ArgumentException("User does not have access to video transcoding.");
 399            }
 400        }
 401
 402        ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
 403
 404        // If subtitles get burned in fonts may need to be extracted from the media file
 405        if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
 406        {
 407            var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
 408            if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
 409            {
 410                var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
 411                await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancella
 412            }
 413            else
 414            {
 415                await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, can
 416            }
 417
 418            if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", 
 419            {
 420                string subtitlePath = state.SubtitleStream.Path;
 421                string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.R
 422                string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 423
 424                await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPat
 425            }
 426        }
 427
 428        var process = new Process
 429        {
 430            StartInfo = new ProcessStartInfo
 431            {
 432                WindowStyle = ProcessWindowStyle.Hidden,
 433                CreateNoWindow = true,
 434                UseShellExecute = false,
 435
 436                // Must consume both stdout and stderr or deadlocks may occur
 437                // RedirectStandardOutput = true,
 438                RedirectStandardError = true,
 439                RedirectStandardInput = true,
 440                FileName = _mediaEncoder.EncoderPath,
 441                Arguments = commandLineArguments,
 442                WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
 443                ErrorDialog = false
 444            },
 445            EnableRaisingEvents = true
 446        };
 447
 448        var transcodingJob = OnTranscodeBeginning(
 449            outputPath,
 450            state.Request.PlaySessionId,
 451            state.MediaSource.LiveStreamId,
 452            Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
 453            transcodingJobType,
 454            process,
 455            state.Request.DeviceId,
 456            state,
 457            cancellationTokenSource);
 458
 459        _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 460
 461        var logFilePrefix = "FFmpeg.Transcode-";
 462        if (state.VideoRequest is not null
 463            && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
 464        {
 465            logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
 466                ? "FFmpeg.Remux-"
 467                : "FFmpeg.DirectStream-";
 468        }
 469
 470        if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
 471        {
 472            logFilePrefix = "FFmpeg.Remux-";
 473        }
 474
 475        var logFilePath = Path.Combine(
 476            _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
 477            $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()
 478
 479        // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
 480        Stream logStream = new FileStream(
 481            logFilePath,
 482            FileMode.Create,
 483            FileAccess.Write,
 484            FileShare.Read,
 485            IODefaults.FileStreamBufferSize,
 486            FileOptions.Asynchronous);
 487
 488        await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Tok
 489        var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
 490            Environment.NewLine
 491            + Environment.NewLine
 492            + process.StartInfo.FileName + " " + process.StartInfo.Arguments
 493            + Environment.NewLine
 494            + Environment.NewLine);
 495
 496        await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
 497
 498        process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state);
 499
 500        try
 501        {
 502            process.Start();
 503        }
 504        catch (Exception ex)
 505        {
 506            _logger.LogError(ex, "Error starting FFmpeg");
 507            OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
 508
 509            throw;
 510        }
 511
 512        _logger.LogDebug("Launched FFmpeg process");
 513        state.TranscodingJob = transcodingJob;
 514
 515        // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
 516        _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
 517
 518        // Wait for the file to exist before proceeding
 519        var ffmpegTargetFile = state.WaitForPath ?? outputPath;
 520        _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
 521        while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
 522        {
 523            await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
 524        }
 525
 526        _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
 527
 528        if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
 529        {
 530            await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
 531
 532            if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
 533            {
 534                await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
 535            }
 536        }
 537
 538        if (!transcodingJob.HasExited)
 539        {
 540            StartThrottler(state, transcodingJob);
 541            StartSegmentCleaner(state, transcodingJob);
 542        }
 543        else if (transcodingJob.ExitCode != 0)
 544        {
 545            throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transco
 546        }
 547
 548        _logger.LogDebug("StartFfMpeg() finished successfully");
 549
 550        return transcodingJob;
 551    }
 552
 553    private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
 554    {
 0555        if (EnableThrottling(state)
 0556            && (_mediaEncoder.IsPkeyPauseSupported
 0557                || _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
 558        {
 0559            transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<T
 0560            transcodingJob.TranscodingThrottler.Start();
 561        }
 0562    }
 563
 564    private static bool EnableThrottling(StreamState state)
 0565        => state.InputProtocol == MediaProtocol.File
 0566           && state.RunTimeTicks.HasValue
 0567           && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks
 0568           && state.IsInputVideo
 0569           && state.VideoType == VideoType.VideoFile;
 570
 571    private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
 572    {
 0573        if (EnableSegmentCleaning(state))
 574        {
 0575            transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.Crea
 0576            transcodingJob.TranscodingSegmentCleaner.Start();
 577        }
 0578    }
 579
 580    private static bool EnableSegmentCleaning(StreamState state)
 0581        => state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
 0582           && state.IsInputVideo
 0583           && state.TranscodingType == TranscodingJobType.Hls
 0584           && state.RunTimeTicks.HasValue
 0585           && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
 586
 587    private TranscodingJob OnTranscodeBeginning(
 588        string path,
 589        string? playSessionId,
 590        string? liveStreamId,
 591        string transcodingJobId,
 592        TranscodingJobType type,
 593        Process process,
 594        string? deviceId,
 595        StreamState state,
 596        CancellationTokenSource cancellationTokenSource)
 597    {
 0598        lock (_activeTranscodingJobs)
 599        {
 0600            var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>())
 0601            {
 0602                Type = type,
 0603                Path = path,
 0604                Process = process,
 0605                ActiveRequestCount = 1,
 0606                DeviceId = deviceId,
 0607                CancellationTokenSource = cancellationTokenSource,
 0608                Id = transcodingJobId,
 0609                PlaySessionId = playSessionId,
 0610                LiveStreamId = liveStreamId,
 0611                MediaSource = state.MediaSource
 0612            };
 613
 0614            _activeTranscodingJobs.Add(job);
 615
 0616            ReportTranscodingProgress(job, state, null, null, null, null, null);
 617
 0618            return job;
 619        }
 0620    }
 621
 622    /// <inheritdoc />
 623    public void OnTranscodeEndRequest(TranscodingJob job)
 624    {
 0625        job.ActiveRequestCount--;
 0626        _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
 0627        if (job.ActiveRequestCount <= 0)
 628        {
 0629            PingTimer(job, false);
 630        }
 0631    }
 632
 633    private void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
 634    {
 0635        lock (_activeTranscodingJobs)
 636        {
 0637            var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringCom
 638
 0639            if (job is not null)
 640            {
 0641                _activeTranscodingJobs.Remove(job);
 642            }
 0643        }
 644
 0645        if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
 646        {
 0647            _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
 648        }
 0649    }
 650
 651    private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
 652    {
 0653        job.HasExited = true;
 0654        job.ExitCode = process.ExitCode;
 655
 0656        ReportTranscodingProgress(job, state, null, null, null, null, null);
 657
 0658        _logger.LogDebug("Disposing stream resources");
 0659        state.Dispose();
 660
 0661        if (process.ExitCode == 0)
 662        {
 0663            _logger.LogInformation("FFmpeg exited with code 0");
 664        }
 665        else
 666        {
 0667            _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
 668        }
 669
 0670        job.Dispose();
 0671    }
 672
 673    private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
 674    {
 675        if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
 676        {
 677            var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
 678                    new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
 679                    cancellationTokenSource.Token)
 680                .ConfigureAwait(false);
 681            var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 682
 683            _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.Requeste
 684
 685            if (state.VideoRequest is not null)
 686            {
 687                _encodingHelper.TryStreamCopy(state);
 688            }
 689        }
 690
 691        if (state.MediaSource.BufferMs.HasValue)
 692        {
 693            await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
 694        }
 695    }
 696
 697    /// <inheritdoc />
 698    public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type)
 699    {
 0700        lock (_activeTranscodingJobs)
 701        {
 0702            var job = _activeTranscodingJobs
 0703                .FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
 704
 0705            if (job is null)
 706            {
 0707                return null;
 708            }
 709
 0710            job.ActiveRequestCount++;
 0711            if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
 712            {
 0713                job.StopKillTimer();
 714            }
 715
 0716            return job;
 717        }
 0718    }
 719
 720    private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
 721    {
 0722        if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
 723        {
 0724            PingTranscodingJob(e.PlaySessionId, e.IsPaused);
 725        }
 0726    }
 727
 728    private void DeleteEncodedMediaCache()
 729    {
 2730        var path = _serverConfigurationManager.GetTranscodePath();
 2731        if (!Directory.Exists(path))
 732        {
 0733            return;
 734        }
 735
 4736        foreach (var file in _fileSystem.GetFilePaths(path, true))
 737        {
 738            try
 739            {
 0740                _fileSystem.DeleteFile(file);
 0741            }
 0742            catch (Exception ex)
 743            {
 0744                _logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
 0745            }
 746        }
 2747    }
 748
 749    /// <summary>
 750    /// Transcoding lock.
 751    /// </summary>
 752    /// <param name="outputPath">The output path of the transcoded file.</param>
 753    /// <param name="cancellationToken">The cancellation token.</param>
 754    /// <returns>An <see cref="IDisposable"/>.</returns>
 755    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 756    public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
 757    {
 0758        return _transcodingLocks.LockAsync(outputPath, cancellationToken);
 759    }
 760
 761    /// <inheritdoc />
 762    public void Dispose()
 763    {
 2764        _sessionManager.PlaybackProgress -= OnPlaybackProgress;
 2765        _sessionManager.PlaybackStart -= OnPlaybackProgress;
 2766        _transcodingLocks.Dispose();
 2767    }
 768}

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