StreamingHelpers.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using Jellyfin.Api.Extensions;
  9. using Jellyfin.Data.Enums;
  10. using Jellyfin.Extensions;
  11. using MediaBrowser.Common.Configuration;
  12. using MediaBrowser.Common.Extensions;
  13. using MediaBrowser.Controller.Configuration;
  14. using MediaBrowser.Controller.Entities;
  15. using MediaBrowser.Controller.Library;
  16. using MediaBrowser.Controller.MediaEncoding;
  17. using MediaBrowser.Controller.Streaming;
  18. using MediaBrowser.Model.Dlna;
  19. using MediaBrowser.Model.Dto;
  20. using MediaBrowser.Model.Entities;
  21. using Microsoft.AspNetCore.Http;
  22. using Microsoft.AspNetCore.Http.HttpResults;
  23. using Microsoft.Net.Http.Headers;
  24. namespace Jellyfin.Api.Helpers;
  25. /// <summary>
  26. /// The streaming helpers.
  27. /// </summary>
  28. public static class StreamingHelpers
  29. {
  30. /// <summary>
  31. /// Gets the current streaming state.
  32. /// </summary>
  33. /// <param name="streamingRequest">The <see cref="StreamingRequestDto"/>.</param>
  34. /// <param name="httpContext">The <see cref="HttpContext"/>.</param>
  35. /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
  36. /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
  37. /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
  38. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  39. /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
  40. /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
  41. /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
  42. /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
  43. /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
  44. /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
  45. public static async Task<StreamState> GetStreamingState(
  46. StreamingRequestDto streamingRequest,
  47. HttpContext httpContext,
  48. IMediaSourceManager mediaSourceManager,
  49. IUserManager userManager,
  50. ILibraryManager libraryManager,
  51. IServerConfigurationManager serverConfigurationManager,
  52. IMediaEncoder mediaEncoder,
  53. EncodingHelper encodingHelper,
  54. ITranscodeManager transcodeManager,
  55. TranscodingJobType transcodingJobType,
  56. CancellationToken cancellationToken)
  57. {
  58. var httpRequest = httpContext.Request;
  59. if (!string.IsNullOrWhiteSpace(streamingRequest.Params))
  60. {
  61. ParseParams(streamingRequest);
  62. }
  63. streamingRequest.StreamOptions = ParseStreamOptions(httpRequest.Query);
  64. if (httpRequest.Path.Value is null)
  65. {
  66. throw new ResourceNotFoundException(nameof(httpRequest.Path));
  67. }
  68. var url = httpRequest.Path.Value.AsSpan().RightPart('.').ToString();
  69. if (string.IsNullOrEmpty(streamingRequest.AudioCodec))
  70. {
  71. streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
  72. }
  73. var state = new StreamState(mediaSourceManager, transcodingJobType, transcodeManager)
  74. {
  75. Request = streamingRequest,
  76. RequestedUrl = url,
  77. UserAgent = httpRequest.Headers[HeaderNames.UserAgent]
  78. };
  79. var userId = httpContext.User.GetUserId();
  80. if (!userId.IsEmpty())
  81. {
  82. state.User = userManager.GetUserById(userId);
  83. }
  84. if (state.IsVideoRequest && !string.IsNullOrWhiteSpace(state.Request.VideoCodec))
  85. {
  86. state.SupportedVideoCodecs = state.Request.VideoCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
  87. state.Request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
  88. }
  89. if (!string.IsNullOrWhiteSpace(streamingRequest.AudioCodec))
  90. {
  91. state.SupportedAudioCodecs = streamingRequest.AudioCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
  92. state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec)
  93. ?? state.SupportedAudioCodecs.FirstOrDefault();
  94. }
  95. if (!string.IsNullOrWhiteSpace(streamingRequest.SubtitleCodec))
  96. {
  97. state.SupportedSubtitleCodecs = streamingRequest.SubtitleCodec.Split(',', StringSplitOptions.RemoveEmptyEntries);
  98. state.Request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(mediaEncoder.CanEncodeToSubtitleCodec)
  99. ?? state.SupportedSubtitleCodecs.FirstOrDefault();
  100. }
  101. var item = libraryManager.GetItemById<BaseItem>(streamingRequest.Id)
  102. ?? throw new ResourceNotFoundException();
  103. state.IsInputVideo = item.MediaType == MediaType.Video;
  104. MediaSourceInfo? mediaSource = null;
  105. if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
  106. {
  107. var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
  108. ? transcodeManager.GetTranscodingJob(streamingRequest.PlaySessionId)
  109. : null;
  110. if (currentJob is not null)
  111. {
  112. mediaSource = currentJob.MediaSource;
  113. }
  114. if (mediaSource is null)
  115. {
  116. var mediaSources = await mediaSourceManager.GetPlaybackMediaSources(libraryManager.GetItemById<BaseItem>(streamingRequest.Id), null, false, false, cancellationToken).ConfigureAwait(false);
  117. mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId)
  118. ? mediaSources[0]
  119. : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal));
  120. if (mediaSource is null && Guid.Parse(streamingRequest.MediaSourceId).Equals(streamingRequest.Id))
  121. {
  122. mediaSource = mediaSources[0];
  123. }
  124. }
  125. }
  126. else
  127. {
  128. // Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
  129. // Cap the MaxStreamingBitrate to 30Mbps, because we are unable to reliably probe source bitrate,
  130. // which will cause the client to request extremely high bitrate that may fail the player/encoder
  131. streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
  132. if (streamingRequest.SegmentContainer is not null)
  133. {
  134. // Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
  135. // Notably: Some channels won't play on FireFox and LG webOS
  136. // Some channels from HDHomerun will experience A/V sync issues
  137. streamingRequest.SegmentContainer = "ts";
  138. streamingRequest.VideoCodec = "h264";
  139. }
  140. var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
  141. mediaSource = liveStreamInfo.Item1;
  142. state.DirectStreamProvider = liveStreamInfo.Item2;
  143. }
  144. var encodingOptions = serverConfigurationManager.GetEncodingOptions();
  145. encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
  146. string? containerInternal = Path.GetExtension(state.RequestedUrl);
  147. if (!string.IsNullOrEmpty(streamingRequest.Container))
  148. {
  149. containerInternal = streamingRequest.Container;
  150. }
  151. if (string.IsNullOrEmpty(containerInternal))
  152. {
  153. containerInternal = streamingRequest.Static ?
  154. StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(state.InputContainer, null, DlnaProfileType.Audio)
  155. : GetOutputFileExtension(state, mediaSource);
  156. }
  157. var outputAudioCodec = streamingRequest.AudioCodec;
  158. state.OutputAudioCodec = outputAudioCodec;
  159. state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
  160. state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
  161. if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
  162. {
  163. state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
  164. }
  165. else
  166. {
  167. state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
  168. }
  169. if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
  170. {
  171. containerInternal = ".pcm";
  172. }
  173. if (state.VideoRequest is not null)
  174. {
  175. state.OutputVideoCodec = state.Request.VideoCodec;
  176. state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
  177. encodingHelper.TryStreamCopy(state);
  178. if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
  179. {
  180. var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
  181. && !state.VideoRequest.Height.HasValue
  182. && !state.VideoRequest.MaxWidth.HasValue
  183. && !state.VideoRequest.MaxHeight.HasValue;
  184. if (isVideoResolutionNotRequested
  185. && state.VideoStream is not null
  186. && state.VideoRequest.VideoBitRate.HasValue
  187. && state.VideoStream.BitRate.HasValue
  188. && state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
  189. {
  190. // Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
  191. // and the requested video bitrate is higher than source video bitrate.
  192. if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
  193. {
  194. state.VideoRequest.MaxWidth = state.VideoStream?.Width;
  195. state.VideoRequest.MaxHeight = state.VideoStream?.Height;
  196. }
  197. }
  198. else
  199. {
  200. var resolution = ResolutionNormalizer.Normalize(
  201. state.VideoStream?.BitRate,
  202. state.OutputVideoBitrate.Value,
  203. state.VideoRequest.MaxWidth,
  204. state.VideoRequest.MaxHeight);
  205. state.VideoRequest.MaxWidth = resolution.MaxWidth;
  206. state.VideoRequest.MaxHeight = resolution.MaxHeight;
  207. }
  208. }
  209. }
  210. var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
  211. ? GetOutputFileExtension(state, mediaSource)
  212. : ("." + GetContainerFileExtension(state.OutputContainer));
  213. state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
  214. return state;
  215. }
  216. /// <summary>
  217. /// Parses query parameters as StreamOptions.
  218. /// </summary>
  219. /// <param name="queryString">The query string.</param>
  220. /// <returns>A <see cref="Dictionary{String,String}"/> containing the stream options.</returns>
  221. private static Dictionary<string, string?> ParseStreamOptions(IQueryCollection queryString)
  222. {
  223. Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
  224. foreach (var param in queryString)
  225. {
  226. if (char.IsLower(param.Key[0]))
  227. {
  228. // This was probably not parsed initially and should be a StreamOptions
  229. // or the generated URL should correctly serialize it
  230. // TODO: This should be incorporated either in the lower framework for parsing requests
  231. streamOptions[param.Key] = param.Value;
  232. }
  233. }
  234. return streamOptions;
  235. }
  236. /// <summary>
  237. /// Gets the output file extension.
  238. /// </summary>
  239. /// <param name="state">The state.</param>
  240. /// <param name="mediaSource">The mediaSource.</param>
  241. /// <returns>System.String.</returns>
  242. private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
  243. {
  244. var ext = Path.GetExtension(state.RequestedUrl);
  245. if (!string.IsNullOrEmpty(ext))
  246. {
  247. return ext;
  248. }
  249. // Try to infer based on the desired video codec
  250. if (state.IsVideoRequest)
  251. {
  252. var videoCodec = state.Request.VideoCodec;
  253. if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  254. {
  255. return ".ts";
  256. }
  257. if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
  258. || string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase))
  259. {
  260. return ".mp4";
  261. }
  262. if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
  263. {
  264. return ".ogv";
  265. }
  266. if (string.Equals(videoCodec, "vp8", StringComparison.OrdinalIgnoreCase)
  267. || string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
  268. || string.Equals(videoCodec, "vpx", StringComparison.OrdinalIgnoreCase))
  269. {
  270. return ".webm";
  271. }
  272. if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase))
  273. {
  274. return ".asf";
  275. }
  276. }
  277. else
  278. {
  279. // Try to infer based on the desired audio codec
  280. var audioCodec = state.Request.AudioCodec;
  281. if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
  282. {
  283. return ".aac";
  284. }
  285. if (string.Equals("mp3", audioCodec, StringComparison.OrdinalIgnoreCase))
  286. {
  287. return ".mp3";
  288. }
  289. if (string.Equals("vorbis", audioCodec, StringComparison.OrdinalIgnoreCase))
  290. {
  291. return ".ogg";
  292. }
  293. if (string.Equals("wma", audioCodec, StringComparison.OrdinalIgnoreCase))
  294. {
  295. return ".wma";
  296. }
  297. }
  298. // Fallback to the container of mediaSource
  299. if (!string.IsNullOrEmpty(mediaSource?.Container))
  300. {
  301. var idx = mediaSource.Container.IndexOf(',', StringComparison.OrdinalIgnoreCase);
  302. return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
  303. }
  304. throw new InvalidOperationException("Failed to find an appropriate file extension");
  305. }
  306. /// <summary>
  307. /// Gets the output file path for transcoding.
  308. /// </summary>
  309. /// <param name="state">The current <see cref="StreamState"/>.</param>
  310. /// <param name="outputFileExtension">The file extension of the output file.</param>
  311. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  312. /// <param name="deviceId">The device id.</param>
  313. /// <param name="playSessionId">The play session id.</param>
  314. /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
  315. private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
  316. {
  317. var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
  318. var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
  319. var ext = outputFileExtension.ToLowerInvariant();
  320. var folder = serverConfigurationManager.GetTranscodePath();
  321. return Path.Combine(folder, filename + ext);
  322. }
  323. /// <summary>
  324. /// Parses the parameters.
  325. /// </summary>
  326. /// <param name="request">The request.</param>
  327. private static void ParseParams(StreamingRequestDto request)
  328. {
  329. if (string.IsNullOrEmpty(request.Params))
  330. {
  331. return;
  332. }
  333. var vals = request.Params.Split(';');
  334. var videoRequest = request as VideoRequestDto;
  335. for (var i = 0; i < vals.Length; i++)
  336. {
  337. var val = vals[i];
  338. if (string.IsNullOrWhiteSpace(val))
  339. {
  340. continue;
  341. }
  342. switch (i)
  343. {
  344. case 0:
  345. // DeviceProfileId
  346. break;
  347. case 1:
  348. request.DeviceId = val;
  349. break;
  350. case 2:
  351. request.MediaSourceId = val;
  352. break;
  353. case 3:
  354. request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  355. break;
  356. case 4:
  357. if (videoRequest is not null)
  358. {
  359. videoRequest.VideoCodec = val;
  360. }
  361. break;
  362. case 5:
  363. request.AudioCodec = val;
  364. break;
  365. case 6:
  366. if (videoRequest is not null)
  367. {
  368. videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
  369. }
  370. break;
  371. case 7:
  372. if (videoRequest is not null)
  373. {
  374. videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
  375. }
  376. break;
  377. case 8:
  378. if (videoRequest is not null)
  379. {
  380. videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
  381. }
  382. break;
  383. case 9:
  384. request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
  385. break;
  386. case 10:
  387. request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
  388. break;
  389. case 11:
  390. if (videoRequest is not null)
  391. {
  392. videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
  393. }
  394. break;
  395. case 12:
  396. if (videoRequest is not null)
  397. {
  398. videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
  399. }
  400. break;
  401. case 13:
  402. if (videoRequest is not null)
  403. {
  404. videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
  405. }
  406. break;
  407. case 14:
  408. request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
  409. break;
  410. case 15:
  411. if (videoRequest is not null)
  412. {
  413. videoRequest.Level = val;
  414. }
  415. break;
  416. case 16:
  417. if (videoRequest is not null)
  418. {
  419. videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
  420. }
  421. break;
  422. case 17:
  423. if (videoRequest is not null)
  424. {
  425. videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
  426. }
  427. break;
  428. case 18:
  429. if (videoRequest is not null)
  430. {
  431. videoRequest.Profile = val;
  432. }
  433. break;
  434. case 19:
  435. // cabac no longer used
  436. break;
  437. case 20:
  438. request.PlaySessionId = val;
  439. break;
  440. case 21:
  441. // api_key
  442. break;
  443. case 22:
  444. request.LiveStreamId = val;
  445. break;
  446. case 23:
  447. // Duplicating ItemId because of MediaMonkey
  448. break;
  449. case 24:
  450. if (videoRequest is not null)
  451. {
  452. videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  453. }
  454. break;
  455. case 25:
  456. if (!string.IsNullOrWhiteSpace(val) && videoRequest is not null)
  457. {
  458. if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
  459. {
  460. videoRequest.SubtitleMethod = method;
  461. }
  462. }
  463. break;
  464. case 26:
  465. request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
  466. break;
  467. case 27:
  468. if (videoRequest is not null)
  469. {
  470. videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  471. }
  472. break;
  473. case 28:
  474. request.Tag = val;
  475. break;
  476. case 29:
  477. if (videoRequest is not null)
  478. {
  479. videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  480. }
  481. break;
  482. case 30:
  483. request.SubtitleCodec = val;
  484. break;
  485. case 31:
  486. if (videoRequest is not null)
  487. {
  488. videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  489. }
  490. break;
  491. case 32:
  492. if (videoRequest is not null)
  493. {
  494. videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
  495. }
  496. break;
  497. case 33:
  498. request.TranscodeReasons = val;
  499. break;
  500. }
  501. }
  502. }
  503. /// <summary>
  504. /// Parses the container into its file extension.
  505. /// </summary>
  506. /// <param name="container">The container.</param>
  507. private static string? GetContainerFileExtension(string? container)
  508. {
  509. if (string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase))
  510. {
  511. return "ts";
  512. }
  513. if (string.Equals(container, "matroska", StringComparison.OrdinalIgnoreCase))
  514. {
  515. return "mkv";
  516. }
  517. return container;
  518. }
  519. }