using System; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.Playback { public class TranscodingThrottler : IDisposable { private readonly TranscodingJob _job; private readonly ILogger _logger; private Timer _timer; private bool _isPaused; private readonly IConfigurationManager _config; private readonly IFileSystem _fileSystem; public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem) { _job = job; _logger = logger; _config = config; _fileSystem = fileSystem; } private EncodingOptions GetOptions() { return _config.GetConfiguration("encoding"); } public void Start() { _timer = new Timer(TimerCallback, null, 5000, 5000); } private async void TimerCallback(object state) { if (_job.HasExited) { DisposeTimer(); return; } var options = GetOptions(); if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds)) { await PauseTranscoding(); } else { await UnpauseTranscoding(); } } private async Task PauseTranscoding() { if (!_isPaused) { _logger.LogDebug("Sending pause command to ffmpeg"); try { await _job.Process.StandardInput.WriteAsync("c"); _isPaused = true; } catch (Exception ex) { _logger.LogError(ex, "Error pausing transcoding"); } } } public async Task UnpauseTranscoding() { if (_isPaused) { _logger.LogDebug("Sending resume command to ffmpeg"); try { await _job.Process.StandardInput.WriteLineAsync(); _isPaused = false; } catch (Exception ex) { _logger.LogError(ex, "Error resuming transcoding"); } } } private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds) { var bytesDownloaded = job.BytesDownloaded ?? 0; var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; var downloadPositionTicks = job.DownloadPositionTicks ?? 0; var path = job.Path; var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks; if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) { // HLS - time-based consideration var targetGap = gapLengthInTicks; var gap = transcodingPositionTicks - downloadPositionTicks; if (gap < targetGap) { _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); return false; } _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap); return true; } if (bytesDownloaded > 0 && transcodingPositionTicks > 0) { // Progressive Streaming - byte-based consideration try { var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length; // Estimate the bytes the transcoder should be ahead double gapFactor = gapLengthInTicks; gapFactor /= transcodingPositionTicks; var targetGap = bytesTranscoded * gapFactor; var gap = bytesTranscoded - bytesDownloaded; if (gap < targetGap) { _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); return false; } _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); return true; } catch (Exception ex) { _logger.LogError(ex, "Error getting output size"); return false; } } _logger.LogDebug("No throttle data for " + path); return false; } public async Task Stop() { DisposeTimer(); await UnpauseTranscoding(); } public void Dispose() { DisposeTimer(); } private void DisposeTimer() { if (_timer != null) { _timer.Dispose(); _timer = null; } } } }