using MediaBrowser.Api.Playback; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api { /// /// Class ServerEntryPoint /// public class ApiEntryPoint : IServerEntryPoint { /// /// The instance /// public static ApiEntryPoint Instance; /// /// Gets or sets the logger. /// /// The logger. private ILogger Logger { get; set; } /// /// The application paths /// private readonly IServerApplicationPaths _appPaths; private readonly ISessionManager _sessionManager; public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1); /// /// Initializes a new instance of the class. /// /// The logger. /// The application paths. /// The session manager. public ApiEntryPoint(ILogger logger, IServerApplicationPaths appPaths, ISessionManager sessionManager) { Logger = logger; _appPaths = appPaths; _sessionManager = sessionManager; Instance = this; } /// /// Runs this instance. /// public void Run() { try { DeleteEncodedMediaCache(); } catch (DirectoryNotFoundException) { // Don't clutter the log } catch (IOException ex) { Logger.ErrorException("Error deleting encoded media cache", ex); } } /// /// Deletes the encoded media cache. /// private void DeleteEncodedMediaCache() { foreach (var file in Directory.EnumerateFiles(_appPaths.TranscodingTempPath, "*", SearchOption.AllDirectories) .ToList()) { File.Delete(file); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { var jobCount = _activeTranscodingJobs.Count; Parallel.ForEach(_activeTranscodingJobs.ToList(), j => KillTranscodingJob(j, path => true)); // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files if (jobCount > 0) { Thread.Sleep(1000); } } /// /// The active transcoding jobs /// private readonly List _activeTranscodingJobs = new List(); /// /// Called when [transcode beginning]. /// /// The path. /// The transcoding job identifier. /// The type. /// The process. /// The device id. /// The state. /// The cancellation token source. /// TranscodingJob. public TranscodingJob OnTranscodeBeginning(string path, string transcodingJobId, TranscodingJobType type, Process process, string deviceId, StreamState state, CancellationTokenSource cancellationTokenSource) { lock (_activeTranscodingJobs) { var job = new TranscodingJob { Type = type, Path = path, Process = process, ActiveRequestCount = 1, DeviceId = deviceId, CancellationTokenSource = cancellationTokenSource, Id = transcodingJobId }; _activeTranscodingJobs.Add(job); ReportTranscodingProgress(job, state, null, null, null, null); return job; } } public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded) { var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null; if (job != null) { job.Framerate = framerate; job.CompletionPercentage = percentComplete; job.TranscodingPositionTicks = ticks; job.BytesTranscoded = bytesTranscoded; } var deviceId = state.Request.DeviceId; if (!string.IsNullOrWhiteSpace(deviceId)) { var audioCodec = state.ActualOutputVideoCodec; var videoCodec = state.ActualOutputVideoCodec; _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo { Bitrate = state.TotalOutputBitrate, AudioCodec = audioCodec, VideoCodec = videoCodec, Container = state.OutputContainer, Framerate = framerate, CompletionPercentage = percentComplete, Width = state.OutputWidth, Height = state.OutputHeight, AudioChannels = state.OutputAudioChannels, IsAudioDirect = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase), IsVideoDirect = string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) }); } } /// /// /// The progressive /// /// Called when [transcode failed to start]. /// /// The path. /// The type. /// The state. public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state) { lock (_activeTranscodingJobs) { var job = _activeTranscodingJobs.First(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); _activeTranscodingJobs.Remove(job); } if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) { _sessionManager.ClearTranscodingInfo(state.Request.DeviceId); } } /// /// Determines whether [has active transcoding job] [the specified path]. /// /// The path. /// The type. /// true if [has active transcoding job] [the specified path]; otherwise, false. public bool HasActiveTranscodingJob(string path, TranscodingJobType type) { return GetTranscodingJob(path, type) != null; } public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); } } public TranscodingJob GetTranscodingJob(string id) { lock (_activeTranscodingJobs) { return _activeTranscodingJobs.FirstOrDefault(j => j.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); } } /// /// Called when [transcode begin request]. /// /// The path. /// The type. public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); if (job == null) { return null; } job.ActiveRequestCount++; job.DisposeKillTimer(); return job; } } public void OnTranscodeEndRequest(TranscodingJob job) { job.ActiveRequestCount--; if (job.ActiveRequestCount == 0) { if (job.Type == TranscodingJobType.Progressive) { const int timerDuration = 1000; if (job.KillTimer == null) { job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite); } else { job.KillTimer.Change(timerDuration, Timeout.Infinite); } } } } /// /// Called when [transcode kill timer stopped]. /// /// The state. private void OnTranscodeKillTimerStopped(object state) { var job = (TranscodingJob)state; KillTranscodingJob(job, path => true); } /// /// Kills the single transcoding job. /// /// The device id. /// The delete files. /// Task. /// deviceId internal void KillTranscodingJobs(string deviceId, Func deleteFiles) { if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException("deviceId"); } KillTranscodingJobs(j => string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase), deleteFiles); } /// /// Kills the transcoding jobs. /// /// The kill job. /// The delete files. /// Task. internal void KillTranscodingJobs(Func killJob, Func deleteFiles) { var jobs = new List(); lock (_activeTranscodingJobs) { // This is really only needed for HLS. // Progressive streams can stop on their own reliably jobs.AddRange(_activeTranscodingJobs.Where(killJob)); } if (jobs.Count == 0) { return; } foreach (var job in jobs) { KillTranscodingJob(job, deleteFiles); } } /// /// Kills the transcoding job. /// /// The job. /// The delete. private void KillTranscodingJob(TranscodingJob job, Func delete) { lock (_activeTranscodingJobs) { _activeTranscodingJobs.Remove(job); if (!job.CancellationTokenSource.IsCancellationRequested) { job.CancellationTokenSource.Cancel(); } job.DisposeKillTimer(); } lock (job.ProcessLock) { var process = job.Process; var hasExited = true; try { hasExited = process.HasExited; } catch (Exception ex) { Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path); } if (!hasExited) { try { Logger.Info("Killing ffmpeg process for {0}", job.Path); //process.Kill(); process.StandardInput.WriteLine("q"); // Need to wait because killing is asynchronous process.WaitForExit(5000); } catch (Exception ex) { Logger.ErrorException("Error killing transcoding job for {0}", ex, job.Path); } } } if (delete(job.Path)) { DeletePartialStreamFiles(job.Path, job.Type, 0, 1500); } } private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs) { if (retryCount >= 10) { return; } Logger.Info("Deleting partial stream file(s) {0}", path); await Task.Delay(delayMs).ConfigureAwait(false); try { if (jobType == TranscodingJobType.Progressive) { DeleteProgressivePartialStreamFiles(path); } else { DeleteHlsPartialStreamFiles(path); } } catch (DirectoryNotFoundException) { } catch (FileNotFoundException) { } catch (IOException ex) { Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path); DeletePartialStreamFiles(path, jobType, retryCount + 1, 500); } catch (Exception ex) { Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path); } } /// /// Deletes the progressive partial stream files. /// /// The output file path. private void DeleteProgressivePartialStreamFiles(string outputFilePath) { File.Delete(outputFilePath); } /// /// Deletes the HLS partial stream files. /// /// The output file path. private void DeleteHlsPartialStreamFiles(string outputFilePath) { var directory = Path.GetDirectoryName(outputFilePath); var name = Path.GetFileNameWithoutExtension(outputFilePath); var filesToDelete = Directory.EnumerateFiles(directory) .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1) .ToList(); Exception e = null; foreach (var file in filesToDelete) { try { Logger.Info("Deleting HLS file {0}", file); File.Delete(file); } catch (DirectoryNotFoundException) { } catch (FileNotFoundException) { } catch (IOException ex) { e = ex; Logger.ErrorException("Error deleting HLS file {0}", ex, file); } } if (e != null) { throw e; } } } /// /// Class TranscodingJob /// public class TranscodingJob { /// /// Gets or sets the path. /// /// The path. public string Path { get; set; } /// /// Gets or sets the type. /// /// The type. public TranscodingJobType Type { get; set; } /// /// Gets or sets the process. /// /// The process. public Process Process { get; set; } /// /// Gets or sets the active request count. /// /// The active request count. public int ActiveRequestCount { get; set; } /// /// Gets or sets the kill timer. /// /// The kill timer. public Timer KillTimer { get; set; } public string DeviceId { get; set; } public CancellationTokenSource CancellationTokenSource { get; set; } public object ProcessLock = new object(); public bool HasExited { get; set; } public string Id { get; set; } public float? Framerate { get; set; } public double? CompletionPercentage { get; set; } public long? BytesDownloaded { get; set; } public long? BytesTranscoded { get; set; } public long? TranscodingPositionTicks { get; set; } public long? DownloadPositionTicks { get; set; } public void DisposeKillTimer() { if (KillTimer != null) { KillTimer.Dispose(); KillTimer = null; } } } /// /// Enum TranscodingJobType /// public enum TranscodingJobType { /// /// The progressive /// Progressive, /// /// The HLS /// Hls, /// /// The dash /// Dash } }