using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using ServiceStack.Web; using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Progressive { /// /// Class BaseProgressiveStreamingService /// public abstract class BaseProgressiveStreamingService : BaseStreamingService { protected readonly IImageProcessor ImageProcessor; protected readonly IHttpClient HttpClient; protected BaseProgressiveStreamingService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder, IImageProcessor imageProcessor, IHttpClient httpClient) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder) { ImageProcessor = imageProcessor; HttpClient = httpClient; } /// /// Gets the output file extension. /// /// The state. /// System.String. protected override string GetOutputFileExtension(StreamState state) { var ext = base.GetOutputFileExtension(state); if (!string.IsNullOrEmpty(ext)) { return ext; } var isVideoRequest = state.VideoRequest != null; // Try to infer based on the desired video codec if (isVideoRequest) { var videoCodec = state.VideoRequest.VideoCodec; if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { return ".ts"; } if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase)) { return ".ogv"; } if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase)) { return ".webm"; } if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase)) { return ".asf"; } } // Try to infer based on the desired audio codec if (!isVideoRequest) { var audioCodec = state.Request.AudioCodec; if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) { return ".aac"; } if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase)) { return ".mp3"; } if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase)) { return ".ogg"; } if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase)) { return ".wma"; } } return null; } /// /// Gets the type of the transcoding job. /// /// The type of the transcoding job. protected override TranscodingJobType TranscodingJobType { get { return TranscodingJobType.Progressive; } } /// /// Processes the request. /// /// The request. /// if set to true [is head request]. /// Task. protected object ProcessRequest(StreamRequest request, bool isHeadRequest) { var state = GetState(request, CancellationToken.None).Result; var responseHeaders = new Dictionary(); // Static remote stream if (request.Static && state.InputProtocol == MediaProtocol.Http) { AddDlnaHeaders(state, responseHeaders, true); using (state) { return GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest).Result; } } if (request.Static && state.InputProtocol != MediaProtocol.File) { throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol)); } var outputPath = state.OutputFilePath; var outputPathExists = File.Exists(outputPath); var isStatic = request.Static || (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)); AddDlnaHeaders(state, responseHeaders, isStatic); // Static stream if (request.Static) { var contentType = state.GetMimeType(state.MediaPath); using (state) { return ResultFactory.GetStaticFileResult(Request, state.MediaPath, contentType, null, FileShare.Read, responseHeaders, isHeadRequest); } } // Not static but transcode cache file exists if (outputPathExists && !ApiEntryPoint.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive)) { var contentType = state.GetMimeType(outputPath); try { return ResultFactory.GetStaticFileResult(Request, outputPath, contentType, null, FileShare.Read, responseHeaders, isHeadRequest); } finally { state.Dispose(); } } // Need to start ffmpeg try { return GetStreamResult(state, responseHeaders, isHeadRequest).Result; } catch { state.Dispose(); throw; } } /// /// Gets the static remote stream result. /// /// The state. /// The response headers. /// if set to true [is head request]. /// Task{System.Object}. private async Task GetStaticRemoteStreamResult(StreamState state, Dictionary responseHeaders, bool isHeadRequest) { string useragent = null; state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); var options = new HttpRequestOptions { Url = state.MediaPath, UserAgent = useragent, BufferContent = false }; var response = await HttpClient.GetResponse(options).ConfigureAwait(false); responseHeaders["Accept-Ranges"] = "none"; var length = response.Headers["Content-Length"]; if (!string.IsNullOrEmpty(length)) { responseHeaders["Content-Length"] = length; } if (isHeadRequest) { using (response.Content) { return ResultFactory.GetResult(new byte[] { }, response.ContentType, responseHeaders); } } var result = new StaticRemoteStreamWriter(response); result.Options["Content-Type"] = response.ContentType; // Add the response headers to the result object foreach (var header in responseHeaders) { result.Options[header.Key] = header.Value; } return result; } /// /// Gets the stream result. /// /// The state. /// The response headers. /// if set to true [is head request]. /// Task{System.Object}. private async Task GetStreamResult(StreamState state, IDictionary responseHeaders, bool isHeadRequest) { // Use the command line args with a dummy playlist path var outputPath = state.OutputFilePath; responseHeaders["Accept-Ranges"] = "none"; var contentType = state.GetMimeType(outputPath); var contentLength = state.EstimateContentLength ? GetEstimatedContentLength(state) : null; if (contentLength.HasValue) { responseHeaders["Content-Length"] = contentLength.Value.ToString(UsCulture); } // Headers only if (isHeadRequest) { var streamResult = ResultFactory.GetResult(new byte[] { }, contentType, responseHeaders); if (!contentLength.HasValue) { var hasOptions = streamResult as IHasOptions; if (hasOptions != null) { if (hasOptions.Options.ContainsKey("Content-Length")) { hasOptions.Options.Remove("Content-Length"); } } } return streamResult; } if (!File.Exists(outputPath)) { await StartFfMpeg(state, outputPath, new CancellationTokenSource()).ConfigureAwait(false); } else { ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); state.Dispose(); } var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive); var result = new ProgressiveStreamWriter(outputPath, Logger, FileSystem, job); result.Options["Content-Type"] = contentType; // Add the response headers to the result object foreach (var item in responseHeaders) { result.Options[item.Key] = item.Value; } return result; } /// /// Gets the length of the estimated content. /// /// The state. /// System.Nullable{System.Int64}. private long? GetEstimatedContentLength(StreamState state) { var totalBitrate = state.TotalOutputBitrate ?? 0; if (totalBitrate > 0 && state.RunTimeTicks.HasValue) { return Convert.ToInt64(totalBitrate * TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds); } return null; } } }