using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Common.Net.Handlers; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.DTO; using MediaBrowser.Model.Entities; using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.Streaming { /// /// Represents a common base class for both progressive and hls streaming /// /// The type of the T base item type. public abstract class BaseStreamingHandler : BaseHandler where TBaseItemType : BaseItem, IHasMediaStreams, new() { /// /// Gets the command line arguments. /// /// The output path. /// The iso mount. /// System.String. protected abstract string GetCommandLineArguments(string outputPath, IIsoMount isoMount); /// /// Gets or sets the log file stream. /// /// The log file stream. protected Stream LogFileStream { get; set; } /// /// Gets the type of the transcoding job. /// /// The type of the transcoding job. protected abstract TranscodingJobType TranscodingJobType { get; } /// /// Gets the output file extension. /// /// The output file extension. protected string OutputFileExtension { get { return Path.GetExtension(HttpListenerContext.Request.Url.LocalPath); } } /// /// Gets the output file path. /// /// The output file path. protected string OutputFilePath { get { return Path.Combine(Kernel.ApplicationPaths.FFMpegStreamCachePath, GetCommandLineArguments("dummy\\dummy", null).GetMD5() + OutputFileExtension.ToLower()); } } /// /// Gets the audio codec to endoce to. /// /// The audio encoding format. protected virtual AudioCodecs? AudioCodec { get { if (string.IsNullOrEmpty(QueryString["audioCodec"])) { return null; } return (AudioCodecs)Enum.Parse(typeof(AudioCodecs), QueryString["audioCodec"], true); } } /// /// Gets the video encoding codec. /// /// The video codec. protected VideoCodecs? VideoCodec { get { if (string.IsNullOrEmpty(QueryString["videoCodec"])) { return null; } return (VideoCodecs)Enum.Parse(typeof(VideoCodecs), QueryString["videoCodec"], true); } } /// /// Gets the time, in ticks, in which playback should start /// /// The start time ticks. protected long? StartTimeTicks { get { string val = QueryString["StartTimeTicks"]; if (string.IsNullOrEmpty(val)) { return null; } return long.Parse(val); } } /// /// The fast seek offset seconds /// private const int FastSeekOffsetSeconds = 1; /// /// Gets the fast seek command line parameter. /// /// The fast seek command line parameter. protected string FastSeekCommandLineParameter { get { var time = StartTimeTicks; if (time.HasValue) { var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds; if (seconds > 0) { return string.Format("-ss {0}", seconds); } } return string.Empty; } } /// /// Gets the slow seek command line parameter. /// /// The slow seek command line parameter. protected string SlowSeekCommandLineParameter { get { var time = StartTimeTicks; if (time.HasValue) { if (TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds > 0) { return string.Format(" -ss {0}", FastSeekOffsetSeconds); } } return string.Empty; } } /// /// Gets the map args. /// /// The map args. protected virtual string MapArgs { get { var args = string.Empty; if (VideoStream != null) { args += string.Format("-map 0:{0}", VideoStream.Index); } else { args += "-map -0:v"; } if (AudioStream != null) { args += string.Format(" -map 0:{0}", AudioStream.Index); } else { args += " -map -0:a"; } if (SubtitleStream == null) { args += " -map -0:s"; } return args; } } /// /// The _library item /// private TBaseItemType _libraryItem; /// /// Gets the library item that will be played, if any /// /// The library item. protected TBaseItemType LibraryItem { get { return _libraryItem ?? (_libraryItem = (TBaseItemType)DtoBuilder.GetItemByClientId(QueryString["id"])); } } /// /// Gets or sets the iso mount. /// /// The iso mount. private IIsoMount IsoMount { get; set; } /// /// The _audio stream /// private MediaStream _audioStream; /// /// Gets the audio stream. /// /// The audio stream. protected MediaStream AudioStream { get { return _audioStream ?? (_audioStream = GetMediaStream(AudioStreamIndex, MediaStreamType.Audio)); } } /// /// The _video stream /// private MediaStream _videoStream; /// /// Gets the video stream. /// /// The video stream. protected MediaStream VideoStream { get { // No video streams here // Need to make this check to make sure we don't pickup embedded image streams (which are listed in the file as type video) if (LibraryItem is Audio) { return null; } return _videoStream ?? (_videoStream = GetMediaStream(VideoStreamIndex, MediaStreamType.Video)); } } /// /// The subtitle stream /// private MediaStream _subtitleStream; /// /// Gets the subtitle stream. /// /// The subtitle stream. protected MediaStream SubtitleStream { get { // No subtitle streams here if (LibraryItem is Audio) { return null; } return _subtitleStream ?? (_subtitleStream = GetMediaStream(SubtitleStreamIndex, MediaStreamType.Subtitle, false)); } } /// /// Determines which stream will be used for playback /// /// Index of the desired. /// The type. /// if set to true [return first if no index]. /// MediaStream. private MediaStream GetMediaStream(int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true) { var streams = LibraryItem.MediaStreams.Where(s => s.Type == type).ToList(); if (desiredIndex.HasValue) { var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value); if (stream != null) { return stream; } } // Just return the first one return returnFirstIfNoIndex ? streams.FirstOrDefault() : null; } /// /// Gets the response info. /// /// Task{ResponseInfo}. protected override Task GetResponseInfo() { var info = new ResponseInfo { ContentType = MimeTypes.GetMimeType(OutputFilePath), CompressResponse = false }; return Task.FromResult(info); } /// /// Gets the client's desired audio bitrate /// /// The audio bit rate. protected int? AudioBitRate { get { var val = QueryString["AudioBitRate"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the client's desired video bitrate /// /// The video bit rate. protected int? VideoBitRate { get { var val = QueryString["VideoBitRate"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the desired audio stream index /// /// The index of the audio stream. private int? AudioStreamIndex { get { var val = QueryString["AudioStreamIndex"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the desired video stream index /// /// The index of the video stream. private int? VideoStreamIndex { get { var val = QueryString["VideoStreamIndex"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the desired subtitle stream index /// /// The index of the subtitle stream. private int? SubtitleStreamIndex { get { var val = QueryString["SubtitleStreamIndex"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the audio channels. /// /// The audio channels. public int? AudioChannels { get { var val = QueryString["audiochannels"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the audio sample rate. /// /// The audio sample rate. public int? AudioSampleRate { get { var val = QueryString["audiosamplerate"]; if (string.IsNullOrEmpty(val)) { return 44100; } return int.Parse(val); } } /// /// If we're going to put a fixed size on the command line, this will calculate it /// /// The output video codec. /// System.String. protected string GetOutputSizeParam(string outputVideoCodec) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ var assSubtitleParam = string.Empty; if (SubtitleStream != null) { if (SubtitleStream.Codec.IndexOf("srt", StringComparison.OrdinalIgnoreCase) != -1 || SubtitleStream.Codec.IndexOf("subrip", StringComparison.OrdinalIgnoreCase) != -1) { assSubtitleParam = GetTextSubtitleParam(SubtitleStream); } } // If fixed dimensions were supplied if (Width.HasValue && Height.HasValue) { return string.Format(" -vf \"scale={0}:{1}{2}\"", Width.Value, Height.Value, assSubtitleParam); } var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase); // If a fixed width was requested if (Width.HasValue) { return isH264Output ? string.Format(" -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", Width.Value, assSubtitleParam) : string.Format(" -vf \"scale={0}:-1{1}\"", Width.Value, assSubtitleParam); } // If a max width was requested if (MaxWidth.HasValue && !MaxHeight.HasValue) { return isH264Output ? string.Format(" -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", MaxWidth.Value, assSubtitleParam) : string.Format(" -vf \"scale=min(iw\\,{0}):-1{1}\"", MaxWidth.Value, assSubtitleParam); } // Need to perform calculations manually // Try to account for bad media info var currentHeight = VideoStream.Height ?? MaxHeight ?? Height ?? 0; var currentWidth = VideoStream.Width ?? MaxWidth ?? Width ?? 0; var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, Width, Height, MaxWidth, MaxHeight); // If we're encoding with libx264, it can't handle odd numbered widths or heights, so we'll have to fix that if (isH264Output) { return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", outputSize.Width, outputSize.Height, assSubtitleParam); } // Otherwise use -vf scale since ffmpeg will ensure internally that the aspect ratio is preserved return string.Format(" -vf \"scale={0}:-1{1}\"", Convert.ToInt32(outputSize.Width), assSubtitleParam); } /// /// Gets the text subtitle param. /// /// The subtitle stream. /// System.String. protected string GetTextSubtitleParam(MediaStream subtitleStream) { var path = subtitleStream.IsExternal ? GetConvertedAssPath(subtitleStream) : GetExtractedAssPath(subtitleStream); if (string.IsNullOrEmpty(path)) { return string.Empty; } var param = string.Format(",ass={0}", path); var time = StartTimeTicks; if (time.HasValue) { var seconds = Convert.ToInt32(TimeSpan.FromTicks(time.Value).TotalSeconds); param += string.Format(",setpts=PTS-{0}/TB", seconds); } return param; } /// /// Gets the extracted ass path. /// /// The subtitle stream. /// System.String. private string GetExtractedAssPath(MediaStream subtitleStream) { var video = LibraryItem as Video; var path = Kernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass"); if (!File.Exists(path)) { var success = Kernel.FFMpegManager.ExtractTextSubtitle(video, subtitleStream.Index, path, CancellationToken.None).Result; if (!success) { return null; } } return path; } /// /// Gets the converted ass path. /// /// The subtitle stream. /// System.String. private string GetConvertedAssPath(MediaStream subtitleStream) { var video = LibraryItem as Video; var path = Kernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass"); if (!File.Exists(path)) { var success = Kernel.FFMpegManager.ConvertTextSubtitle(subtitleStream, path, CancellationToken.None).Result; if (!success) { return null; } } return path; } /// /// Gets the internal graphical subtitle param. /// /// The subtitle stream. /// The video codec. /// System.String. protected string GetInternalGraphicalSubtitleParam(MediaStream subtitleStream, string videoCodec) { var outputSizeParam = string.Empty; // Add resolution params, if specified if (Width.HasValue || Height.HasValue || MaxHeight.HasValue || MaxWidth.HasValue) { outputSizeParam = GetOutputSizeParam(videoCodec).TrimEnd('"'); outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase)); } return string.Format(" -filter_complex \"[0:{0}]format=yuva444p,lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:0] [sub] overlay{1}\"", subtitleStream.Index, outputSizeParam); } /// /// Gets the fixed output video height, in pixels /// /// The height. protected int? Height { get { string val = QueryString["height"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the fixed output video width, in pixels /// /// The width. protected int? Width { get { string val = QueryString["width"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the maximum output video height, in pixels /// /// The height of the max. protected int? MaxHeight { get { string val = QueryString["maxheight"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the maximum output video width, in pixels /// /// The width of the max. protected int? MaxWidth { get { string val = QueryString["maxwidth"]; if (string.IsNullOrEmpty(val)) { return null; } return int.Parse(val); } } /// /// Gets the output video framerate /// /// The max frame rate. protected float? FrameRate { get { string val = QueryString["framerate"]; if (string.IsNullOrEmpty(val)) { return null; } return float.Parse(val); } } /// /// Gets the number of audio channels to specify on the command line /// /// System.Nullable{System.Int32}. protected int? GetSampleRateParam() { // If the user requested a max value if (AudioSampleRate.HasValue) { return AudioSampleRate.Value; } return null; } /// /// Gets the number of audio channels to specify on the command line /// /// The audio codec. /// System.Nullable{System.Int32}. protected int? GetNumAudioChannelsParam(string audioCodec) { if (AudioStream.Channels > 2) { if (audioCodec.Equals("libvo_aacenc")) { // libvo_aacenc currently only supports two channel output return 2; } if (audioCodec.Equals("wmav2")) { // wmav2 currently only supports two channel output return 2; } } return GetNumAudioChannelsParam(); } /// /// Gets the number of audio channels to specify on the command line /// /// System.Nullable{System.Int32}. protected int? GetNumAudioChannelsParam() { // If the user requested a max number of channels if (AudioChannels.HasValue) { return AudioChannels.Value; } return null; } /// /// Determines whether the specified stream is H264. /// /// The stream. /// true if the specified stream is H264; otherwise, false. protected bool IsH264(MediaStream stream) { return stream.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || stream.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; } /// /// Gets the name of the output audio codec /// /// System.String. protected string GetAudioCodec() { if (AudioCodec.HasValue) { if (AudioCodec == AudioCodecs.Aac) { return "libvo_aacenc"; } if (AudioCodec == AudioCodecs.Mp3) { return "libmp3lame"; } if (AudioCodec == AudioCodecs.Vorbis) { return "libvorbis"; } if (AudioCodec == AudioCodecs.Wma) { return "wmav2"; } } return "copy"; } /// /// Gets the name of the output video codec /// /// System.String. protected string GetVideoCodec() { if (VideoCodec.HasValue) { if (VideoCodec == VideoCodecs.H264) { return "libx264"; } if (VideoCodec == VideoCodecs.Vpx) { return "libvpx"; } if (VideoCodec == VideoCodecs.Wmv) { return "wmv2"; } if (VideoCodec == VideoCodecs.Theora) { return "libtheora"; } } return "copy"; } /// /// Gets the input argument. /// /// The iso mount. /// System.String. protected string GetInputArgument(IIsoMount isoMount) { return isoMount == null ? Kernel.FFMpegManager.GetInputArgument(LibraryItem) : Kernel.FFMpegManager.GetInputArgument(LibraryItem as Video, IsoMount); } /// /// Starts the FFMPEG. /// /// The output path. /// Task. protected async Task StartFFMpeg(string outputPath) { var video = LibraryItem as Video; if (video != null && video.VideoType == VideoType.Iso && video.IsoType.HasValue && Kernel.IsoManager.CanMount(video.Path)) { IsoMount = await Kernel.IsoManager.Mount(video.Path, CancellationToken.None).ConfigureAwait(false); } var process = new Process { StartInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, // Must consume both stdout and stderr or deadlocks may occur RedirectStandardOutput = true, RedirectStandardError = true, FileName = Kernel.FFMpegManager.FFMpegPath, WorkingDirectory = Path.GetDirectoryName(Kernel.FFMpegManager.FFMpegPath), Arguments = GetCommandLineArguments(outputPath, IsoMount), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false }, EnableRaisingEvents = true }; Plugin.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process); Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); var logFilePath = Path.Combine(Kernel.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid() + ".txt"); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. LogFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); process.Exited += OnFFMpegProcessExited; try { process.Start(); } catch (Win32Exception ex) { Logger.ErrorException("Error starting ffmpeg", ex); Plugin.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType); process.Exited -= OnFFMpegProcessExited; LogFileStream.Dispose(); throw; } // MUST read both stdout and stderr asynchronously or a deadlock may occurr process.BeginOutputReadLine(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback process.StandardError.BaseStream.CopyToAsync(LogFileStream); // Wait for the file to exist before proceeeding while (!File.Exists(outputPath)) { await Task.Delay(100).ConfigureAwait(false); } } /// /// Processes the exited. /// /// The sender. /// The instance containing the event data. protected void OnFFMpegProcessExited(object sender, EventArgs e) { if (IsoMount != null) { IsoMount.Dispose(); IsoMount = null; } var outputFilePath = OutputFilePath; LogFileStream.Dispose(); var process = (Process)sender; process.Exited -= OnFFMpegProcessExited; int? exitCode = null; try { exitCode = process.ExitCode; Logger.Info("FFMpeg exited with code {0} for {1}", exitCode.Value, outputFilePath); } catch { Logger.Info("FFMpeg exited with an error for {0}", outputFilePath); } process.Dispose(); Plugin.Instance.OnTranscodingFinished(outputFilePath, TranscodingJobType); if (!exitCode.HasValue || exitCode.Value != 0) { Logger.Info("Deleting partial stream file(s) {0}", outputFilePath); try { DeletePartialStreamFiles(outputFilePath); } catch (IOException ex) { Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, outputFilePath); } } else { Logger.Info("FFMpeg completed and exited normally for {0}", outputFilePath); } } /// /// Deletes the partial stream files. /// /// The output file path. protected abstract void DeletePartialStreamFiles(string outputFilePath); } }