BaseProgressiveStreamingService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using MediaBrowser.Common.Net;
  8. using MediaBrowser.Controller.Configuration;
  9. using MediaBrowser.Controller.Devices;
  10. using MediaBrowser.Controller.Dlna;
  11. using MediaBrowser.Controller.Drawing;
  12. using MediaBrowser.Controller.Library;
  13. using MediaBrowser.Controller.MediaEncoding;
  14. using MediaBrowser.Controller.Net;
  15. using MediaBrowser.Model.IO;
  16. using MediaBrowser.Model.MediaInfo;
  17. using MediaBrowser.Model.Serialization;
  18. using MediaBrowser.Model.Services;
  19. using MediaBrowser.Model.System;
  20. using Microsoft.Net.Http.Headers;
  21. namespace MediaBrowser.Api.Playback.Progressive
  22. {
  23. /// <summary>
  24. /// Class BaseProgressiveStreamingService
  25. /// </summary>
  26. public abstract class BaseProgressiveStreamingService : BaseStreamingService
  27. {
  28. protected readonly IEnvironmentInfo EnvironmentInfo;
  29. protected IHttpClient HttpClient { get; private set; }
  30. public BaseProgressiveStreamingService(
  31. IHttpClient httpClient,
  32. IServerConfigurationManager serverConfig,
  33. IUserManager userManager,
  34. ILibraryManager libraryManager,
  35. IIsoManager isoManager,
  36. IMediaEncoder mediaEncoder,
  37. IFileSystem fileSystem,
  38. IDlnaManager dlnaManager,
  39. ISubtitleEncoder subtitleEncoder,
  40. IDeviceManager deviceManager,
  41. IMediaSourceManager mediaSourceManager,
  42. IJsonSerializer jsonSerializer,
  43. IAuthorizationContext authorizationContext,
  44. IEnvironmentInfo environmentInfo)
  45. : base(serverConfig,
  46. userManager,
  47. libraryManager,
  48. isoManager,
  49. mediaEncoder,
  50. fileSystem,
  51. dlnaManager,
  52. subtitleEncoder,
  53. deviceManager,
  54. mediaSourceManager,
  55. jsonSerializer,
  56. authorizationContext)
  57. {
  58. EnvironmentInfo = environmentInfo;
  59. HttpClient = httpClient;
  60. }
  61. /// <summary>
  62. /// Gets the output file extension.
  63. /// </summary>
  64. /// <param name="state">The state.</param>
  65. /// <returns>System.String.</returns>
  66. protected override string GetOutputFileExtension(StreamState state)
  67. {
  68. var ext = base.GetOutputFileExtension(state);
  69. if (!string.IsNullOrEmpty(ext))
  70. {
  71. return ext;
  72. }
  73. var isVideoRequest = state.VideoRequest != null;
  74. // Try to infer based on the desired video codec
  75. if (isVideoRequest)
  76. {
  77. var videoCodec = state.VideoRequest.VideoCodec;
  78. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  79. {
  80. return ".ts";
  81. }
  82. if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
  83. {
  84. return ".ogv";
  85. }
  86. if (string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
  87. {
  88. return ".webm";
  89. }
  90. if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
  91. {
  92. return ".asf";
  93. }
  94. }
  95. // Try to infer based on the desired audio codec
  96. if (!isVideoRequest)
  97. {
  98. var audioCodec = state.Request.AudioCodec;
  99. if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
  100. {
  101. return ".aac";
  102. }
  103. if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
  104. {
  105. return ".mp3";
  106. }
  107. if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
  108. {
  109. return ".ogg";
  110. }
  111. if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
  112. {
  113. return ".wma";
  114. }
  115. }
  116. return null;
  117. }
  118. /// <summary>
  119. /// Gets the type of the transcoding job.
  120. /// </summary>
  121. /// <value>The type of the transcoding job.</value>
  122. protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Progressive;
  123. /// <summary>
  124. /// Processes the request.
  125. /// </summary>
  126. /// <param name="request">The request.</param>
  127. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  128. /// <returns>Task.</returns>
  129. protected async Task<object> ProcessRequest(StreamRequest request, bool isHeadRequest)
  130. {
  131. var cancellationTokenSource = new CancellationTokenSource();
  132. var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
  133. var responseHeaders = new Dictionary<string, string>();
  134. if (request.Static && state.DirectStreamProvider != null)
  135. {
  136. AddDlnaHeaders(state, responseHeaders, true);
  137. using (state)
  138. {
  139. var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  140. // TODO: Don't hardcode this
  141. outputHeaders[HeaderNames.ContentType] = Model.Net.MimeTypes.GetMimeType("file.ts");
  142. return new ProgressiveFileCopier(state.DirectStreamProvider, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None)
  143. {
  144. AllowEndOfFile = false
  145. };
  146. }
  147. }
  148. // Static remote stream
  149. if (request.Static && state.InputProtocol == MediaProtocol.Http)
  150. {
  151. AddDlnaHeaders(state, responseHeaders, true);
  152. using (state)
  153. {
  154. return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
  155. }
  156. }
  157. if (request.Static && state.InputProtocol != MediaProtocol.File)
  158. {
  159. throw new ArgumentException(string.Format("Input protocol {0} cannot be streamed statically.", state.InputProtocol));
  160. }
  161. var outputPath = state.OutputFilePath;
  162. var outputPathExists = File.Exists(outputPath);
  163. var transcodingJob = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
  164. var isTranscodeCached = outputPathExists && transcodingJob != null;
  165. AddDlnaHeaders(state, responseHeaders, request.Static || isTranscodeCached);
  166. // Static stream
  167. if (request.Static)
  168. {
  169. var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
  170. using (state)
  171. {
  172. if (state.MediaSource.IsInfiniteStream)
  173. {
  174. var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
  175. {
  176. [HeaderNames.ContentType] = contentType
  177. };
  178. return new ProgressiveFileCopier(FileSystem, state.MediaPath, outputHeaders, null, Logger, EnvironmentInfo, CancellationToken.None)
  179. {
  180. AllowEndOfFile = false
  181. };
  182. }
  183. TimeSpan? cacheDuration = null;
  184. if (!string.IsNullOrEmpty(request.Tag))
  185. {
  186. cacheDuration = TimeSpan.FromDays(365);
  187. }
  188. return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  189. {
  190. ResponseHeaders = responseHeaders,
  191. ContentType = contentType,
  192. IsHeadRequest = isHeadRequest,
  193. Path = state.MediaPath,
  194. CacheDuration = cacheDuration
  195. }).ConfigureAwait(false);
  196. }
  197. }
  198. //// Not static but transcode cache file exists
  199. //if (isTranscodeCached && state.VideoRequest == null)
  200. //{
  201. // var contentType = state.GetMimeType(outputPath);
  202. // try
  203. // {
  204. // if (transcodingJob != null)
  205. // {
  206. // ApiEntryPoint.Instance.OnTranscodeBeginRequest(transcodingJob);
  207. // }
  208. // return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  209. // {
  210. // ResponseHeaders = responseHeaders,
  211. // ContentType = contentType,
  212. // IsHeadRequest = isHeadRequest,
  213. // Path = outputPath,
  214. // FileShare = FileShareMode.ReadWrite,
  215. // OnComplete = () =>
  216. // {
  217. // if (transcodingJob != null)
  218. // {
  219. // ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
  220. // }
  221. // }
  222. // }).ConfigureAwait(false);
  223. // }
  224. // finally
  225. // {
  226. // state.Dispose();
  227. // }
  228. //}
  229. // Need to start ffmpeg
  230. try
  231. {
  232. return await GetStreamResult(request, state, responseHeaders, isHeadRequest, cancellationTokenSource).ConfigureAwait(false);
  233. }
  234. catch
  235. {
  236. state.Dispose();
  237. throw;
  238. }
  239. }
  240. /// <summary>
  241. /// Gets the static remote stream result.
  242. /// </summary>
  243. /// <param name="state">The state.</param>
  244. /// <param name="responseHeaders">The response headers.</param>
  245. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  246. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  247. /// <returns>Task{System.Object}.</returns>
  248. private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
  249. {
  250. string useragent = null;
  251. state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent);
  252. var trySupportSeek = false;
  253. var options = new HttpRequestOptions
  254. {
  255. Url = state.MediaPath,
  256. UserAgent = useragent,
  257. BufferContent = false,
  258. CancellationToken = cancellationTokenSource.Token
  259. };
  260. if (trySupportSeek)
  261. {
  262. if (!string.IsNullOrWhiteSpace(Request.QueryString[HeaderNames.Range]))
  263. {
  264. options.RequestHeaders[HeaderNames.Range] = Request.QueryString[HeaderNames.Range];
  265. }
  266. }
  267. var response = await HttpClient.GetResponse(options).ConfigureAwait(false);
  268. if (trySupportSeek)
  269. {
  270. foreach (var name in new[] { HeaderNames.ContentRange, HeaderNames.AcceptRanges })
  271. {
  272. var val = response.Headers[name];
  273. if (!string.IsNullOrWhiteSpace(val))
  274. {
  275. responseHeaders[name] = val;
  276. }
  277. }
  278. }
  279. else
  280. {
  281. responseHeaders[HeaderNames.AcceptRanges] = "none";
  282. }
  283. if (isHeadRequest)
  284. {
  285. using (response)
  286. {
  287. return ResultFactory.GetResult(null, new byte[] { }, response.ContentType, responseHeaders);
  288. }
  289. }
  290. var result = new StaticRemoteStreamWriter(response);
  291. result.Headers[HeaderNames.ContentType] = response.ContentType;
  292. // Add the response headers to the result object
  293. foreach (var header in responseHeaders)
  294. {
  295. result.Headers[header.Key] = header.Value;
  296. }
  297. return result;
  298. }
  299. /// <summary>
  300. /// Gets the stream result.
  301. /// </summary>
  302. /// <param name="state">The state.</param>
  303. /// <param name="responseHeaders">The response headers.</param>
  304. /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
  305. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  306. /// <returns>Task{System.Object}.</returns>
  307. private async Task<object> GetStreamResult(StreamRequest request, StreamState state, IDictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource)
  308. {
  309. // Use the command line args with a dummy playlist path
  310. var outputPath = state.OutputFilePath;
  311. responseHeaders[HeaderNames.AcceptRanges] = "none";
  312. var contentType = state.GetMimeType(outputPath);
  313. // TODO: The isHeadRequest is only here because ServiceStack will add Content-Length=0 to the response
  314. // Headers only
  315. if (isHeadRequest)
  316. {
  317. return ResultFactory.GetResult(null, Array.Empty<byte>(), contentType, responseHeaders);
  318. }
  319. var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
  320. await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  321. try
  322. {
  323. TranscodingJob job;
  324. if (!File.Exists(outputPath))
  325. {
  326. job = await StartFfMpeg(state, outputPath, cancellationTokenSource).ConfigureAwait(false);
  327. }
  328. else
  329. {
  330. job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
  331. state.Dispose();
  332. }
  333. var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
  334. {
  335. [HeaderNames.ContentType] = contentType
  336. };
  337. // Add the response headers to the result object
  338. foreach (var item in responseHeaders)
  339. {
  340. outputHeaders[item.Key] = item.Value;
  341. }
  342. return new ProgressiveFileCopier(FileSystem, outputPath, outputHeaders, job, Logger, EnvironmentInfo, CancellationToken.None);
  343. }
  344. finally
  345. {
  346. transcodingLock.Release();
  347. }
  348. }
  349. }
  350. }