DynamicHlsHelper.cs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Net;
  6. using System.Security.Claims;
  7. using System.Text;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. using Jellyfin.Api.Extensions;
  11. using Jellyfin.Api.Models.StreamingDtos;
  12. using Jellyfin.Extensions;
  13. using Jellyfin.Data.Entities;
  14. using MediaBrowser.Common.Configuration;
  15. using MediaBrowser.Common.Extensions;
  16. using MediaBrowser.Common.Net;
  17. using MediaBrowser.Controller.Configuration;
  18. using MediaBrowser.Controller.Devices;
  19. using MediaBrowser.Controller.Dlna;
  20. using MediaBrowser.Controller.Library;
  21. using MediaBrowser.Controller.MediaEncoding;
  22. using MediaBrowser.Controller.Trickplay;
  23. using MediaBrowser.Model.Dlna;
  24. using MediaBrowser.Model.Entities;
  25. using MediaBrowser.Model.Net;
  26. using Microsoft.AspNetCore.Http;
  27. using Microsoft.AspNetCore.Mvc;
  28. using Microsoft.Extensions.Logging;
  29. using Microsoft.Net.Http.Headers;
  30. namespace Jellyfin.Api.Helpers;
  31. /// <summary>
  32. /// Dynamic hls helper.
  33. /// </summary>
  34. public class DynamicHlsHelper
  35. {
  36. private readonly ILibraryManager _libraryManager;
  37. private readonly IUserManager _userManager;
  38. private readonly IDlnaManager _dlnaManager;
  39. private readonly IMediaSourceManager _mediaSourceManager;
  40. private readonly IServerConfigurationManager _serverConfigurationManager;
  41. private readonly IMediaEncoder _mediaEncoder;
  42. private readonly IDeviceManager _deviceManager;
  43. private readonly TranscodingJobHelper _transcodingJobHelper;
  44. private readonly INetworkManager _networkManager;
  45. private readonly ILogger<DynamicHlsHelper> _logger;
  46. private readonly IHttpContextAccessor _httpContextAccessor;
  47. private readonly EncodingHelper _encodingHelper;
  48. private readonly ITrickplayManager _trickplayManager;
  49. /// <summary>
  50. /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
  51. /// </summary>
  52. /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
  53. /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
  54. /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
  55. /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
  56. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  57. /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
  58. /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
  59. /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param>
  60. /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
  61. /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
  62. /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
  63. /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
  64. /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
  65. public DynamicHlsHelper(
  66. ILibraryManager libraryManager,
  67. IUserManager userManager,
  68. IDlnaManager dlnaManager,
  69. IMediaSourceManager mediaSourceManager,
  70. IServerConfigurationManager serverConfigurationManager,
  71. IMediaEncoder mediaEncoder,
  72. IDeviceManager deviceManager,
  73. TranscodingJobHelper transcodingJobHelper,
  74. INetworkManager networkManager,
  75. ILogger<DynamicHlsHelper> logger,
  76. IHttpContextAccessor httpContextAccessor,
  77. EncodingHelper encodingHelper,
  78. ITrickplayManager trickplayManager)
  79. {
  80. _libraryManager = libraryManager;
  81. _userManager = userManager;
  82. _dlnaManager = dlnaManager;
  83. _mediaSourceManager = mediaSourceManager;
  84. _serverConfigurationManager = serverConfigurationManager;
  85. _mediaEncoder = mediaEncoder;
  86. _deviceManager = deviceManager;
  87. _transcodingJobHelper = transcodingJobHelper;
  88. _networkManager = networkManager;
  89. _logger = logger;
  90. _httpContextAccessor = httpContextAccessor;
  91. _encodingHelper = encodingHelper;
  92. _trickplayManager = trickplayManager;
  93. }
  94. /// <summary>
  95. /// Get master hls playlist.
  96. /// </summary>
  97. /// <param name="transcodingJobType">Transcoding job type.</param>
  98. /// <param name="streamingRequest">Streaming request dto.</param>
  99. /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
  100. /// <returns>A <see cref="Task"/> containing the resulting <see cref="ActionResult"/>.</returns>
  101. public async Task<ActionResult> GetMasterHlsPlaylist(
  102. TranscodingJobType transcodingJobType,
  103. StreamingRequestDto streamingRequest,
  104. bool enableAdaptiveBitrateStreaming)
  105. {
  106. var isHeadRequest = _httpContextAccessor.HttpContext?.Request.Method == WebRequestMethods.Http.Head;
  107. // CTS lifecycle is managed internally.
  108. var cancellationTokenSource = new CancellationTokenSource();
  109. return await GetMasterPlaylistInternal(
  110. streamingRequest,
  111. isHeadRequest,
  112. enableAdaptiveBitrateStreaming,
  113. transcodingJobType,
  114. cancellationTokenSource).ConfigureAwait(false);
  115. }
  116. /// <summary>
  117. /// Get trickplay tiles hls playlist.
  118. /// </summary>
  119. /// <param name="width">The width of a single tile.</param>
  120. /// <param name="mediaSourceId">The media version id.</param>
  121. /// <returns>The resulting <see cref="ActionResult"/>.</returns>
  122. public ActionResult GetTilesHlsPlaylist(int width, string mediaSourceId)
  123. {
  124. if (_httpContextAccessor.HttpContext is null)
  125. {
  126. throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
  127. }
  128. var tilesResolutions = _trickplayManager.GetTilesResolutions(Guid.Parse(mediaSourceId));
  129. if (tilesResolutions is not null && tilesResolutions.ContainsKey(width))
  130. {
  131. var builder = new StringBuilder(128);
  132. var tilesInfo = tilesResolutions[width];
  133. if (tilesInfo.TileCount > 0)
  134. {
  135. const string urlFormat = "{0}/Trickplay/{1}/{2}.jpg?&api_key={3}";
  136. const string decimalFormat = "{0:0.###}";
  137. var resolution = tilesInfo.Width.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.Height.ToString(CultureInfo.InvariantCulture);
  138. var layout = tilesInfo.TileWidth.ToString(CultureInfo.InvariantCulture) + "x" + tilesInfo.TileHeight.ToString(CultureInfo.InvariantCulture);
  139. var tilesPerGrid = tilesInfo.TileWidth * tilesInfo.TileHeight;
  140. var tileDuration = (decimal)tilesInfo.Interval / 1000;
  141. var tileGridCount = (int)Math.Ceiling((decimal)tilesInfo.TileCount / tilesPerGrid);
  142. builder.AppendLine("#EXTM3U");
  143. builder.Append("#EXT-X-TARGETDURATION:").AppendLine(tileGridCount.ToString(CultureInfo.InvariantCulture));
  144. builder.AppendLine("#EXT-X-VERSION:7");
  145. builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1");
  146. builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
  147. builder.AppendLine("#EXT-X-IMAGES-ONLY");
  148. for (int i = 0; i < tileGridCount; i++)
  149. {
  150. // All tile grids before the last one must contain full amount of tiles.
  151. // The final grid will be 0 < count <= maxTiles
  152. if (i == tileGridCount - 1)
  153. {
  154. tilesPerGrid = tilesInfo.TileCount - (i * tilesPerGrid);
  155. }
  156. var infDuration = tileDuration * tilesPerGrid;
  157. var url = string.Format(
  158. CultureInfo.InvariantCulture,
  159. urlFormat,
  160. mediaSourceId,
  161. width.ToString(CultureInfo.InvariantCulture),
  162. i.ToString(CultureInfo.InvariantCulture),
  163. _httpContextAccessor.HttpContext.User.GetToken());
  164. // EXTINF
  165. builder.Append("#EXTINF:").Append(string.Format(CultureInfo.InvariantCulture, decimalFormat, infDuration))
  166. .AppendLine(",");
  167. // EXT-X-TILES
  168. builder.Append("#EXT-X-TILES:RESOLUTION=").Append(resolution).Append(",LAYOUT=").Append(layout).Append(",DURATION=")
  169. .AppendLine(string.Format(CultureInfo.InvariantCulture, decimalFormat, tileDuration));
  170. // URL
  171. builder.AppendLine(url);
  172. }
  173. builder.AppendLine("#EXT-X-ENDLIST");
  174. return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
  175. }
  176. }
  177. return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
  178. }
  179. private async Task<ActionResult> GetMasterPlaylistInternal(
  180. StreamingRequestDto streamingRequest,
  181. bool isHeadRequest,
  182. bool enableAdaptiveBitrateStreaming,
  183. TranscodingJobType transcodingJobType,
  184. CancellationTokenSource cancellationTokenSource)
  185. {
  186. if (_httpContextAccessor.HttpContext is null)
  187. {
  188. throw new ResourceNotFoundException(nameof(_httpContextAccessor.HttpContext));
  189. }
  190. using var state = await StreamingHelpers.GetStreamingState(
  191. streamingRequest,
  192. _httpContextAccessor.HttpContext,
  193. _mediaSourceManager,
  194. _userManager,
  195. _libraryManager,
  196. _serverConfigurationManager,
  197. _mediaEncoder,
  198. _encodingHelper,
  199. _dlnaManager,
  200. _deviceManager,
  201. _transcodingJobHelper,
  202. transcodingJobType,
  203. cancellationTokenSource.Token)
  204. .ConfigureAwait(false);
  205. _httpContextAccessor.HttpContext.Response.Headers.Add(HeaderNames.Expires, "0");
  206. if (isHeadRequest)
  207. {
  208. return new FileContentResult(Array.Empty<byte>(), MimeTypes.GetMimeType("playlist.m3u8"));
  209. }
  210. var totalBitrate = (state.OutputAudioBitrate ?? 0) + (state.OutputVideoBitrate ?? 0);
  211. var builder = new StringBuilder();
  212. builder.AppendLine("#EXTM3U");
  213. var isLiveStream = state.IsSegmentedLiveStream;
  214. var queryString = _httpContextAccessor.HttpContext.Request.QueryString.ToString();
  215. // from universal audio service
  216. if (!string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
  217. && !queryString.Contains("SegmentContainer", StringComparison.OrdinalIgnoreCase))
  218. {
  219. queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
  220. }
  221. // from universal audio service
  222. if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons)
  223. && !queryString.Contains("TranscodeReasons=", StringComparison.OrdinalIgnoreCase))
  224. {
  225. queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
  226. }
  227. // Main stream
  228. var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
  229. playlistUrl += queryString;
  230. var subtitleStreams = state.MediaSource
  231. .MediaStreams
  232. .Where(i => i.IsTextSubtitleStream)
  233. .ToList();
  234. var subtitleGroup = subtitleStreams.Count > 0 && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest!.EnableSubtitlesInManifest)
  235. ? "subs"
  236. : null;
  237. // If we're burning in subtitles then don't add additional subs to the manifest
  238. if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
  239. {
  240. subtitleGroup = null;
  241. }
  242. if (!string.IsNullOrWhiteSpace(subtitleGroup))
  243. {
  244. AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
  245. }
  246. var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
  247. if (state.VideoStream is not null && state.VideoRequest is not null)
  248. {
  249. // Provide a workaround for the case issue between flac and fLaC.
  250. var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
  251. if (!string.IsNullOrEmpty(flacWaPlaylist))
  252. {
  253. builder.Append(flacWaPlaylist);
  254. }
  255. var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
  256. // Provide SDR HEVC entrance for backward compatibility.
  257. if (encodingOptions.AllowHevcEncoding
  258. && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
  259. && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
  260. && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
  261. && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
  262. {
  263. var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
  264. if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
  265. {
  266. // Force HEVC Main Profile and disable video stream copy.
  267. state.OutputVideoCodec = "hevc";
  268. var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
  269. sdrVideoUrl += "&AllowVideoStreamCopy=false";
  270. var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
  271. var sdrOutputAudioBitrate = 0;
  272. if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
  273. {
  274. sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
  275. }
  276. else
  277. {
  278. sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
  279. }
  280. var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
  281. var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
  282. // Provide a workaround for the case issue between flac and fLaC.
  283. flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
  284. if (!string.IsNullOrEmpty(flacWaPlaylist))
  285. {
  286. builder.Append(flacWaPlaylist);
  287. }
  288. // Restore the video codec
  289. state.OutputVideoCodec = "copy";
  290. }
  291. }
  292. // Provide Level 5.0 entrance for backward compatibility.
  293. // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
  294. // but in fact it is capable of playing videos up to Level 6.1.
  295. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
  296. && state.VideoStream.Level.HasValue
  297. && state.VideoStream.Level > 150
  298. && !string.IsNullOrEmpty(state.VideoStream.VideoRange)
  299. && string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
  300. && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
  301. {
  302. var playlistCodecsField = new StringBuilder();
  303. AppendPlaylistCodecsField(playlistCodecsField, state);
  304. // Force the video level to 5.0.
  305. var originalLevel = state.VideoStream.Level;
  306. state.VideoStream.Level = 150;
  307. var newPlaylistCodecsField = new StringBuilder();
  308. AppendPlaylistCodecsField(newPlaylistCodecsField, state);
  309. // Restore the video level.
  310. state.VideoStream.Level = originalLevel;
  311. var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
  312. builder.Append(newPlaylist);
  313. // Provide a workaround for the case issue between flac and fLaC.
  314. flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
  315. if (!string.IsNullOrEmpty(flacWaPlaylist))
  316. {
  317. builder.Append(flacWaPlaylist);
  318. }
  319. }
  320. }
  321. if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
  322. {
  323. var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
  324. // By default, vary by just 200k
  325. var variation = GetBitrateVariation(totalBitrate);
  326. var newBitrate = totalBitrate - variation;
  327. var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
  328. AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
  329. variation *= 2;
  330. newBitrate = totalBitrate - variation;
  331. variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
  332. AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
  333. }
  334. if (!isLiveStream && (state.VideoRequest?.EnableTrickplay).GetValueOrDefault(false))
  335. {
  336. var sourceId = Guid.Parse(state.Request.MediaSourceId);
  337. var tilesResolutions = _trickplayManager.GetTilesResolutions(sourceId);
  338. AddTrickplay(state, tilesResolutions, builder, _httpContextAccessor.HttpContext.User);
  339. }
  340. return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
  341. }
  342. private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
  343. {
  344. var playlistBuilder = new StringBuilder();
  345. playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
  346. .Append(bitrate.ToString(CultureInfo.InvariantCulture))
  347. .Append(",AVERAGE-BANDWIDTH=")
  348. .Append(bitrate.ToString(CultureInfo.InvariantCulture));
  349. AppendPlaylistVideoRangeField(playlistBuilder, state);
  350. AppendPlaylistCodecsField(playlistBuilder, state);
  351. AppendPlaylistResolutionField(playlistBuilder, state);
  352. AppendPlaylistFramerateField(playlistBuilder, state);
  353. if (!string.IsNullOrWhiteSpace(subtitleGroup))
  354. {
  355. playlistBuilder.Append(",SUBTITLES=\"")
  356. .Append(subtitleGroup)
  357. .Append('"');
  358. }
  359. playlistBuilder.Append(Environment.NewLine);
  360. playlistBuilder.AppendLine(url);
  361. builder.Append(playlistBuilder);
  362. return playlistBuilder;
  363. }
  364. /// <summary>
  365. /// Appends a VIDEO-RANGE field containing the range of the output video stream.
  366. /// </summary>
  367. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  368. /// <param name="builder">StringBuilder to append the field to.</param>
  369. /// <param name="state">StreamState of the current stream.</param>
  370. private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
  371. {
  372. if (state.VideoStream is not null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
  373. {
  374. var videoRange = state.VideoStream.VideoRange;
  375. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  376. {
  377. if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
  378. {
  379. builder.Append(",VIDEO-RANGE=SDR");
  380. }
  381. if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
  382. {
  383. builder.Append(",VIDEO-RANGE=PQ");
  384. }
  385. }
  386. else
  387. {
  388. // Currently we only encode to SDR.
  389. builder.Append(",VIDEO-RANGE=SDR");
  390. }
  391. }
  392. }
  393. /// <summary>
  394. /// Appends a CODECS field containing formatted strings of
  395. /// the active streams output video and audio codecs.
  396. /// </summary>
  397. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  398. /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
  399. /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
  400. /// <param name="builder">StringBuilder to append the field to.</param>
  401. /// <param name="state">StreamState of the current stream.</param>
  402. private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
  403. {
  404. // Video
  405. string videoCodecs = string.Empty;
  406. int? videoCodecLevel = GetOutputVideoCodecLevel(state);
  407. if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
  408. {
  409. videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
  410. }
  411. // Audio
  412. string audioCodecs = string.Empty;
  413. if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
  414. {
  415. audioCodecs = GetPlaylistAudioCodecs(state);
  416. }
  417. StringBuilder codecs = new StringBuilder();
  418. codecs.Append(videoCodecs);
  419. if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
  420. {
  421. codecs.Append(',');
  422. }
  423. codecs.Append(audioCodecs);
  424. if (codecs.Length > 1)
  425. {
  426. builder.Append(",CODECS=\"")
  427. .Append(codecs)
  428. .Append('"');
  429. }
  430. }
  431. /// <summary>
  432. /// Appends a RESOLUTION field containing the resolution of the output stream.
  433. /// </summary>
  434. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  435. /// <param name="builder">StringBuilder to append the field to.</param>
  436. /// <param name="state">StreamState of the current stream.</param>
  437. private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
  438. {
  439. if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
  440. {
  441. builder.Append(",RESOLUTION=")
  442. .Append(state.OutputWidth.GetValueOrDefault())
  443. .Append('x')
  444. .Append(state.OutputHeight.GetValueOrDefault());
  445. }
  446. }
  447. /// <summary>
  448. /// Appends a FRAME-RATE field containing the framerate of the output stream.
  449. /// </summary>
  450. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  451. /// <param name="builder">StringBuilder to append the field to.</param>
  452. /// <param name="state">StreamState of the current stream.</param>
  453. private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
  454. {
  455. double? framerate = null;
  456. if (state.TargetFramerate.HasValue)
  457. {
  458. framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
  459. }
  460. else if (state.VideoStream?.RealFrameRate is not null)
  461. {
  462. framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
  463. }
  464. if (framerate.HasValue)
  465. {
  466. builder.Append(",FRAME-RATE=")
  467. .Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
  468. }
  469. }
  470. private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress)
  471. {
  472. // Within the local network this will likely do more harm than good.
  473. if (_networkManager.IsInLocalNetwork(ipAddress))
  474. {
  475. return false;
  476. }
  477. if (!enableAdaptiveBitrateStreaming)
  478. {
  479. return false;
  480. }
  481. if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
  482. {
  483. // Opening live streams is so slow it's not even worth it
  484. return false;
  485. }
  486. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  487. {
  488. return false;
  489. }
  490. if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
  491. {
  492. return false;
  493. }
  494. if (!state.IsOutputVideo)
  495. {
  496. return false;
  497. }
  498. // Having problems in android
  499. return false;
  500. // return state.VideoRequest.VideoBitRate.HasValue;
  501. }
  502. private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder, ClaimsPrincipal user)
  503. {
  504. if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Drop)
  505. {
  506. return;
  507. }
  508. var selectedIndex = state.SubtitleStream is null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
  509. const string Format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
  510. foreach (var stream in subtitles)
  511. {
  512. var name = stream.DisplayTitle;
  513. var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
  514. var isForced = stream.IsForced;
  515. var url = string.Format(
  516. CultureInfo.InvariantCulture,
  517. "{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
  518. state.Request.MediaSourceId,
  519. stream.Index.ToString(CultureInfo.InvariantCulture),
  520. 30.ToString(CultureInfo.InvariantCulture),
  521. user.GetToken());
  522. var line = string.Format(
  523. CultureInfo.InvariantCulture,
  524. Format,
  525. name,
  526. isDefault ? "YES" : "NO",
  527. isForced ? "YES" : "NO",
  528. url,
  529. stream.Language ?? "Unknown");
  530. builder.AppendLine(line);
  531. }
  532. }
  533. /// <summary>
  534. /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
  535. /// </summary>
  536. /// <param name="state">StreamState of the current stream.</param>
  537. /// <param name="tilesResolutions">Dictionary of widths to corresponding tiles info.</param>
  538. /// <param name="builder">StringBuilder to append the field to.</param>
  539. /// <param name="user">Http user context.</param>
  540. private void AddTrickplay(StreamState state, Dictionary<int, TrickplayTilesInfo> tilesResolutions, StringBuilder builder, ClaimsPrincipal user)
  541. {
  542. const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
  543. foreach (var resolution in tilesResolutions)
  544. {
  545. var width = resolution.Key;
  546. var tilesInfo = resolution.Value;
  547. var url = string.Format(
  548. CultureInfo.InvariantCulture,
  549. "tiles.m3u8?Width={0}&MediaSourceId={1}&api_key={2}",
  550. width.ToString(CultureInfo.InvariantCulture),
  551. state.Request.MediaSourceId,
  552. user.GetToken());
  553. var line = string.Format(
  554. CultureInfo.InvariantCulture,
  555. playlistFormat,
  556. tilesInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
  557. tilesInfo.Width.ToString(CultureInfo.InvariantCulture),
  558. tilesInfo.Height.ToString(CultureInfo.InvariantCulture),
  559. url);
  560. builder.AppendLine(line);
  561. }
  562. }
  563. /// <summary>
  564. /// Get the H.26X level of the output video stream.
  565. /// </summary>
  566. /// <param name="state">StreamState of the current stream.</param>
  567. /// <returns>H.26X level of the output video stream.</returns>
  568. private int? GetOutputVideoCodecLevel(StreamState state)
  569. {
  570. string levelString = string.Empty;
  571. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
  572. && state.VideoStream is not null
  573. && state.VideoStream.Level.HasValue)
  574. {
  575. levelString = state.VideoStream.Level.ToString() ?? string.Empty;
  576. }
  577. else
  578. {
  579. if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  580. {
  581. levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
  582. levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
  583. }
  584. if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
  585. || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
  586. {
  587. levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
  588. levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
  589. }
  590. }
  591. if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
  592. {
  593. return parsedLevel;
  594. }
  595. return null;
  596. }
  597. /// <summary>
  598. /// Get the H.26X profile of the output video stream.
  599. /// </summary>
  600. /// <param name="state">StreamState of the current stream.</param>
  601. /// <param name="codec">Video codec.</param>
  602. /// <returns>H.26X profile of the output video stream.</returns>
  603. private string GetOutputVideoCodecProfile(StreamState state, string codec)
  604. {
  605. string profileString = string.Empty;
  606. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
  607. && !string.IsNullOrEmpty(state.VideoStream.Profile))
  608. {
  609. profileString = state.VideoStream.Profile;
  610. }
  611. else if (!string.IsNullOrEmpty(codec))
  612. {
  613. profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
  614. if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
  615. {
  616. profileString ??= "high";
  617. }
  618. if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
  619. || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
  620. {
  621. profileString ??= "main";
  622. }
  623. }
  624. return profileString;
  625. }
  626. /// <summary>
  627. /// Gets a formatted string of the output audio codec, for use in the CODECS field.
  628. /// </summary>
  629. /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
  630. /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
  631. /// <param name="state">StreamState of the current stream.</param>
  632. /// <returns>Formatted audio codec string.</returns>
  633. private string GetPlaylistAudioCodecs(StreamState state)
  634. {
  635. if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
  636. {
  637. string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
  638. return HlsCodecStringHelpers.GetAACString(profile);
  639. }
  640. if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
  641. {
  642. return HlsCodecStringHelpers.GetMP3String();
  643. }
  644. if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
  645. {
  646. return HlsCodecStringHelpers.GetAC3String();
  647. }
  648. if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
  649. {
  650. return HlsCodecStringHelpers.GetEAC3String();
  651. }
  652. if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
  653. {
  654. return HlsCodecStringHelpers.GetFLACString();
  655. }
  656. if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
  657. {
  658. return HlsCodecStringHelpers.GetALACString();
  659. }
  660. if (string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
  661. {
  662. return HlsCodecStringHelpers.GetOPUSString();
  663. }
  664. return string.Empty;
  665. }
  666. /// <summary>
  667. /// Gets a formatted string of the output video codec, for use in the CODECS field.
  668. /// </summary>
  669. /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
  670. /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
  671. /// <param name="state">StreamState of the current stream.</param>
  672. /// <param name="codec">Video codec.</param>
  673. /// <param name="level">Video level.</param>
  674. /// <returns>Formatted video codec string.</returns>
  675. private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
  676. {
  677. if (level == 0)
  678. {
  679. // This is 0 when there's no requested H.26X level in the device profile
  680. // and the source is not encoded in H.26X
  681. _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
  682. return string.Empty;
  683. }
  684. if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
  685. {
  686. string profile = GetOutputVideoCodecProfile(state, "h264");
  687. return HlsCodecStringHelpers.GetH264String(profile, level);
  688. }
  689. if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
  690. || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
  691. {
  692. string profile = GetOutputVideoCodecProfile(state, "hevc");
  693. return HlsCodecStringHelpers.GetH265String(profile, level);
  694. }
  695. return string.Empty;
  696. }
  697. private int GetBitrateVariation(int bitrate)
  698. {
  699. // By default, vary by just 50k
  700. var variation = 50000;
  701. if (bitrate >= 10000000)
  702. {
  703. variation = 2000000;
  704. }
  705. else if (bitrate >= 5000000)
  706. {
  707. variation = 1500000;
  708. }
  709. else if (bitrate >= 3000000)
  710. {
  711. variation = 1000000;
  712. }
  713. else if (bitrate >= 2000000)
  714. {
  715. variation = 500000;
  716. }
  717. else if (bitrate >= 1000000)
  718. {
  719. variation = 300000;
  720. }
  721. else if (bitrate >= 600000)
  722. {
  723. variation = 200000;
  724. }
  725. else if (bitrate >= 400000)
  726. {
  727. variation = 100000;
  728. }
  729. return variation;
  730. }
  731. private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
  732. {
  733. return url.Replace(
  734. "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
  735. "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
  736. StringComparison.OrdinalIgnoreCase);
  737. }
  738. private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
  739. {
  740. string profileStr = codec + "-profile=";
  741. return url.Replace(
  742. profileStr + oldValue,
  743. profileStr + newValue,
  744. StringComparison.OrdinalIgnoreCase);
  745. }
  746. private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
  747. {
  748. var oldPlaylist = playlist.ToString();
  749. return oldPlaylist.Replace(
  750. oldValue.ToString(),
  751. newValue.ToString(),
  752. StringComparison.Ordinal);
  753. }
  754. private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
  755. {
  756. if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
  757. {
  758. return string.Empty;
  759. }
  760. var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
  761. return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
  762. }
  763. }