DynamicHlsHelper.cs 30 KB

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