using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using MediaBrowser.Common.Logging; using MediaBrowser.Common.Net; using MediaBrowser.Common.Net.Handlers; using MediaBrowser.Controller; using MediaBrowser.Model.Entities; namespace MediaBrowser.Api.HttpHandlers { public abstract class BaseMediaHandler : BaseHandler where T : BaseItem, new() { /// /// Supported values: mp3,flac,ogg,wav,asf,wma,aac /// protected virtual IEnumerable OutputFormats { get { return QueryString["outputformats"].Split(','); } } /// /// These formats can be outputted directly but cannot be encoded to /// protected virtual IEnumerable UnsupportedOutputEncodingFormats { get { return new string[] { }; } } private T _LibraryItem; /// /// Gets the library item that will be played, if any /// protected T LibraryItem { get { if (_LibraryItem == null) { string id = QueryString["id"]; if (!string.IsNullOrEmpty(id)) { _LibraryItem = Kernel.Instance.GetItemById(Guid.Parse(id)) as T; } } return _LibraryItem; } } public int? AudioChannels { get { string val = QueryString["audiochannels"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } public int? AudioSampleRate { get { string val = QueryString["audiosamplerate"]; if (string.IsNullOrEmpty(val)) { return 44100; } return int.Parse(val); } } public override Task GetContentType() { return Task.FromResult(MimeTypes.GetMimeType("." + GetConversionOutputFormat())); } public override bool ShouldCompressResponse(string contentType) { return false; } public override Task ProcessRequest(HttpListenerContext ctx) { HttpListenerContext = ctx; if (!RequiresConversion()) { return new StaticFileHandler() { Path = LibraryItem.Path }.ProcessRequest(ctx); } else { return base.ProcessRequest(ctx); } } protected abstract string GetCommandLineArguments(); /// /// Gets the format we'll be converting to /// protected virtual string GetConversionOutputFormat() { return OutputFormats.First(f => !UnsupportedOutputEncodingFormats.Any(s => s.Equals(f, StringComparison.OrdinalIgnoreCase))); } protected virtual bool RequiresConversion() { string currentFormat = Path.GetExtension(LibraryItem.Path).Replace(".", string.Empty); if (OutputFormats.Any(f => currentFormat.EndsWith(f, StringComparison.OrdinalIgnoreCase))) { // We can output these files directly, but we can't encode them if (UnsupportedOutputEncodingFormats.Any(f => currentFormat.EndsWith(f, StringComparison.OrdinalIgnoreCase))) { return false; } } else { // If it's not in a format the consumer accepts, return true return true; } return false; } protected async override Task WriteResponseToOutputStream(Stream stream) { ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.CreateNoWindow = true; startInfo.UseShellExecute = false; // Must consume both or ffmpeg may hang due to deadlocks. See comments below. startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; startInfo.FileName = Kernel.Instance.ApplicationPaths.FFMpegPath; startInfo.WorkingDirectory = Kernel.Instance.ApplicationPaths.FFMpegDirectory; startInfo.Arguments = GetCommandLineArguments(); Logger.LogInfo(startInfo.FileName + " " + startInfo.Arguments); Process process = new Process(); process.StartInfo = startInfo; // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. FileStream logStream = new FileStream(Path.Combine(Kernel.Instance.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid().ToString() + ".txt"), FileMode.Create); try { process.Start(); // MUST read both stdout and stderr asynchronously or a deadlock may occurr // If we ever decide to disable the ffmpeg log then you must uncomment the below line. //process.BeginErrorReadLine(); Task debugLogTask = process.StandardError.BaseStream.CopyToAsync(logStream); await process.StandardOutput.BaseStream.CopyToAsync(stream).ConfigureAwait(false); process.WaitForExit(); Logger.LogInfo("FFMpeg exited with code " + process.ExitCode); await debugLogTask.ConfigureAwait(false); } catch (Exception ex) { Logger.LogException(ex); // Hate having to do this try { process.Kill(); } catch { } } finally { logStream.Dispose(); process.Dispose(); } } /// /// Gets the number of audio channels to specify on the command line /// protected int? GetNumAudioChannelsParam(int libraryItemChannels) { // If the user requested a max number of channels if (AudioChannels.HasValue) { // Only specify the param if we're going to downmix if (AudioChannels.Value < libraryItemChannels) { return AudioChannels.Value; } } return null; } /// /// Gets the number of audio channels to specify on the command line /// protected int? GetSampleRateParam(int libraryItemSampleRate) { // If the user requested a max value if (AudioSampleRate.HasValue) { // Only specify the param if we're going to downmix if (AudioSampleRate.Value < libraryItemSampleRate) { return AudioSampleRate.Value; } } return null; } } }