diff options
| author | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-26 23:19:05 -0500 |
|---|---|---|
| committer | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-26 23:19:05 -0500 |
| commit | 3751e14eb1b7a0815b6ab7c2164c262e4723c52e (patch) | |
| tree | 4acac5381745de703ff0ea65bee049cf6e0e4f5b /MediaBrowser.Api/Playback/BaseStreamingService.cs | |
| parent | a6596042a67e2d846f74542d72e81b87d1521a5d (diff) | |
restored audio
Diffstat (limited to 'MediaBrowser.Api/Playback/BaseStreamingService.cs')
| -rw-r--r-- | MediaBrowser.Api/Playback/BaseStreamingService.cs | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs new file mode 100644 index 000000000..159106437 --- /dev/null +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -0,0 +1,624 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Implementations.HttpServer; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.Playback +{ + /// <summary> + /// Class BaseStreamingService + /// </summary> + public abstract class BaseStreamingService : BaseRestService + { + /// <summary> + /// Gets or sets the application paths. + /// </summary> + /// <value>The application paths.</value> + protected IServerApplicationPaths ApplicationPaths { get; set; } + + /// <summary> + /// Gets the server kernel. + /// </summary> + /// <value>The server kernel.</value> + protected Kernel ServerKernel + { + get { return Kernel as Kernel; } + } + + /// <summary> + /// Initializes a new instance of the <see cref="BaseStreamingService" /> class. + /// </summary> + /// <param name="appPaths">The app paths.</param> + protected BaseStreamingService(IServerApplicationPaths appPaths) + { + ApplicationPaths = appPaths; + } + + /// <summary> + /// Gets the command line arguments. + /// </summary> + /// <param name="outputPath">The output path.</param> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + protected abstract string GetCommandLineArguments(string outputPath, StreamState state); + + /// <summary> + /// Gets the type of the transcoding job. + /// </summary> + /// <value>The type of the transcoding job.</value> + protected abstract TranscodingJobType TranscodingJobType { get; } + + /// <summary> + /// Gets the output file extension. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + protected virtual string GetOutputFileExtension(StreamState state) + { + return Path.GetExtension(state.Url); + } + + /// <summary> + /// Gets the output file path. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + protected string GetOutputFilePath(StreamState state) + { + var folder = ApplicationPaths.FFMpegStreamCachePath; + return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state).GetMD5() + GetOutputFileExtension(state).ToLower()); + } + + /// <summary> + /// The fast seek offset seconds + /// </summary> + private const int FastSeekOffsetSeconds = 1; + + /// <summary> + /// Gets the fast seek command line parameter. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.String.</returns> + /// <value>The fast seek command line parameter.</value> + protected string GetFastSeekCommandLineParameter(StreamRequest request) + { + var time = request.StartTimeTicks; + + if (time.HasValue) + { + var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds; + + if (seconds > 0) + { + return string.Format("-ss {0}", seconds); + } + } + + return string.Empty; + } + + /// <summary> + /// Gets the slow seek command line parameter. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.String.</returns> + /// <value>The slow seek command line parameter.</value> + protected string GetSlowSeekCommandLineParameter(StreamRequest request) + { + var time = request.StartTimeTicks; + + if (time.HasValue) + { + if (TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds > 0) + { + return string.Format(" -ss {0}", FastSeekOffsetSeconds); + } + } + + return string.Empty; + } + + /// <summary> + /// Gets the map args. + /// </summary> + /// <param name="state">The state.</param> + /// <returns>System.String.</returns> + protected string GetMapArgs(StreamState state) + { + var args = string.Empty; + + if (state.VideoStream != null) + { + args += string.Format("-map 0:{0}", state.VideoStream.Index); + } + else + { + args += "-map -0:v"; + } + + if (state.AudioStream != null) + { + args += string.Format(" -map 0:{0}", state.AudioStream.Index); + } + else + { + args += " -map -0:a"; + } + + if (state.SubtitleStream == null) + { + args += " -map -0:s"; + } + + return args; + } + + /// <summary> + /// Determines which stream will be used for playback + /// </summary> + /// <param name="allStream">All stream.</param> + /// <param name="desiredIndex">Index of the desired.</param> + /// <param name="type">The type.</param> + /// <param name="returnFirstIfNoIndex">if set to <c>true</c> [return first if no index].</param> + /// <returns>MediaStream.</returns> + private MediaStream GetMediaStream(IEnumerable<MediaStream> allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true) + { + var streams = allStream.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; + } + + /// <summary> + /// If we're going to put a fixed size on the command line, this will calculate it + /// </summary> + /// <param name="state">The state.</param> + /// <param name="outputVideoCodec">The output video codec.</param> + /// <returns>System.String.</returns> + protected string GetOutputSizeParam(StreamState state, string outputVideoCodec) + { + // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ + + var assSubtitleParam = string.Empty; + + var request = state.Request; + + if (state.SubtitleStream != null) + { + if (state.SubtitleStream.Codec.IndexOf("srt", StringComparison.OrdinalIgnoreCase) != -1 || state.SubtitleStream.Codec.IndexOf("subrip", StringComparison.OrdinalIgnoreCase) != -1) + { + assSubtitleParam = GetTextSubtitleParam((Video)state.Item, state.SubtitleStream, request.StartTimeTicks); + } + } + + // If fixed dimensions were supplied + if (request.Width.HasValue && request.Height.HasValue) + { + return string.Format(" -vf \"scale={0}:{1}{2}\"", request.Width.Value, request.Height.Value, assSubtitleParam); + } + + var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase); + + // If a fixed width was requested + if (request.Width.HasValue) + { + return isH264Output ? + string.Format(" -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", request.Width.Value, assSubtitleParam) : + string.Format(" -vf \"scale={0}:-1{1}\"", request.Width.Value, assSubtitleParam); + } + + // If a max width was requested + if (request.MaxWidth.HasValue && !request.MaxHeight.HasValue) + { + return isH264Output ? + string.Format(" -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", request.MaxWidth.Value, assSubtitleParam) : + string.Format(" -vf \"scale=min(iw\\,{0}):-1{1}\"", request.MaxWidth.Value, assSubtitleParam); + } + + // Need to perform calculations manually + + // Try to account for bad media info + var currentHeight = state.VideoStream.Height ?? request.MaxHeight ?? request.Height ?? 0; + var currentWidth = state.VideoStream.Width ?? request.MaxWidth ?? request.Width ?? 0; + + var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, request.Width, request.Height, request.MaxWidth, request.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); + } + + /// <summary> + /// Gets the text subtitle param. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="subtitleStream">The subtitle stream.</param> + /// <param name="startTimeTicks">The start time ticks.</param> + /// <returns>System.String.</returns> + protected string GetTextSubtitleParam(Video video, MediaStream subtitleStream, long? startTimeTicks) + { + var path = subtitleStream.IsExternal ? GetConvertedAssPath(video, subtitleStream) : GetExtractedAssPath(video, subtitleStream); + + if (string.IsNullOrEmpty(path)) + { + return string.Empty; + } + + var param = string.Format(",ass={0}", path); + + if (startTimeTicks.HasValue) + { + var seconds = Convert.ToInt32(TimeSpan.FromTicks(startTimeTicks.Value).TotalSeconds); + param += string.Format(",setpts=PTS-{0}/TB", seconds); + } + + return param; + } + + /// <summary> + /// Gets the extracted ass path. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="subtitleStream">The subtitle stream.</param> + /// <returns>System.String.</returns> + private string GetExtractedAssPath(Video video, MediaStream subtitleStream) + { + var path = ServerKernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass"); + + if (!File.Exists(path)) + { + var success = ServerKernel.FFMpegManager.ExtractTextSubtitle(video, subtitleStream.Index, path, CancellationToken.None).Result; + + if (!success) + { + return null; + } + } + + return path; + } + + /// <summary> + /// Gets the converted ass path. + /// </summary> + /// <param name="video">The video.</param> + /// <param name="subtitleStream">The subtitle stream.</param> + /// <returns>System.String.</returns> + private string GetConvertedAssPath(Video video, MediaStream subtitleStream) + { + var path = ServerKernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass"); + + if (!File.Exists(path)) + { + var success = ServerKernel.FFMpegManager.ConvertTextSubtitle(subtitleStream, path, CancellationToken.None).Result; + + if (!success) + { + return null; + } + } + + return path; + } + + /// <summary> + /// Gets the internal graphical subtitle param. + /// </summary> + /// <param name="state">The state.</param> + /// <param name="outputVideoCodec">The output video codec.</param> + /// <returns>System.String.</returns> + protected string GetInternalGraphicalSubtitleParam(StreamState state, string outputVideoCodec) + { + var outputSizeParam = string.Empty; + + var request = state.Request; + + // Add resolution params, if specified + if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) + { + outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).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}\"", state.SubtitleStream.Index, outputSizeParam); + } + + /// <summary> + /// Gets the number of audio channels to specify on the command line + /// </summary> + /// <param name="request">The request.</param> + /// <param name="audioStream">The audio stream.</param> + /// <returns>System.Nullable{System.Int32}.</returns> + protected int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream) + { + if (audioStream.Channels > 2 && request.AudioCodec.HasValue) + { + if (request.AudioCodec.Value == AudioCodecs.Aac) + { + // libvo_aacenc currently only supports two channel output + return 2; + } + if (request.AudioCodec.Value == AudioCodecs.Wma) + { + // wmav2 currently only supports two channel output + return 2; + } + } + + return request.AudioChannels; + } + + /// <summary> + /// Determines whether the specified stream is H264. + /// </summary> + /// <param name="stream">The stream.</param> + /// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns> + protected bool IsH264(MediaStream stream) + { + return stream.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || + stream.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; + } + + /// <summary> + /// Gets the name of the output audio codec + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.String.</returns> + protected string GetAudioCodec(StreamRequest request) + { + var codec = request.AudioCodec; + + if (codec.HasValue) + { + if (codec == AudioCodecs.Aac) + { + return "libvo_aacenc"; + } + if (codec == AudioCodecs.Mp3) + { + return "libmp3lame"; + } + if (codec == AudioCodecs.Vorbis) + { + return "libvorbis"; + } + if (codec == AudioCodecs.Wma) + { + return "wmav2"; + } + } + + return "copy"; + } + + /// <summary> + /// Gets the name of the output video codec + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.String.</returns> + protected string GetVideoCodec(StreamRequest request) + { + var codec = request.VideoCodec; + + if (codec.HasValue) + { + if (codec == VideoCodecs.H264) + { + return "libx264"; + } + if (codec == VideoCodecs.Vpx) + { + return "libvpx"; + } + if (codec == VideoCodecs.Wmv) + { + return "wmv2"; + } + if (codec == VideoCodecs.Theora) + { + return "libtheora"; + } + } + + return "copy"; + } + + /// <summary> + /// Gets the input argument. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="isoMount">The iso mount.</param> + /// <returns>System.String.</returns> + protected string GetInputArgument(BaseItem item, IIsoMount isoMount) + { + return isoMount == null ? + ServerKernel.FFMpegManager.GetInputArgument(item) : + ServerKernel.FFMpegManager.GetInputArgument(item as Video, isoMount); + } + + /// <summary> + /// Starts the FFMPEG. + /// </summary> + /// <param name="state">The state.</param> + /// <param name="outputPath">The output path.</param> + /// <returns>Task.</returns> + protected async Task StartFFMpeg(StreamState state, string outputPath) + { + var video = state.Item 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 = ServerKernel.FFMpegManager.FFMpegPath, + WorkingDirectory = Path.GetDirectoryName(ServerKernel.FFMpegManager.FFMpegPath), + Arguments = GetCommandLineArguments(outputPath, state), + + 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. + state.LogFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); + + process.Exited += (sender, args) => OnFFMpegProcessExited(process, state); + + try + { + process.Start(); + } + catch (Win32Exception ex) + { + Logger.ErrorException("Error starting ffmpeg", ex); + + Plugin.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType); + + state.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(state.LogFileStream); + + // Wait for the file to exist before proceeeding + while (!File.Exists(outputPath)) + { + await Task.Delay(100).ConfigureAwait(false); + } + } + + /// <summary> + /// Processes the exited. + /// </summary> + /// <param name="process">The process.</param> + /// <param name="state">The state.</param> + protected void OnFFMpegProcessExited(Process process, StreamState state) + { + if (state.IsoMount != null) + { + state.IsoMount.Dispose(); + state.IsoMount = null; + } + + var outputFilePath = GetOutputFilePath(state); + + state.LogFileStream.Dispose(); + + 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); + } + } + + /// <summary> + /// Deletes the partial stream files. + /// </summary> + /// <param name="outputFilePath">The output file path.</param> + protected abstract void DeletePartialStreamFiles(string outputFilePath); + + /// <summary> + /// Gets the state. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>StreamState.</returns> + protected StreamState GetState(StreamRequest request) + { + var item = DtoBuilder.GetItemByClientId(request.Id); + + var media = (IHasMediaStreams)item; + + return new StreamState + { + Item = item, + Request = request, + AudioStream = GetMediaStream(media.MediaStreams, request.AudioStreamIndex, MediaStreamType.Audio, true), + VideoStream = GetMediaStream(media.MediaStreams, request.VideoStreamIndex, MediaStreamType.Video, true), + SubtitleStream = GetMediaStream(media.MediaStreams, request.SubtitleStreamIndex, MediaStreamType.Subtitle, false), + Url = Request.PathInfo + }; + } + } +} |
