DynamicHlsHelper.cs 32 KB

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