DynamicHlsService.cs 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using MediaBrowser.Common.Net;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Controller.Devices;
  12. using MediaBrowser.Controller.Dlna;
  13. using MediaBrowser.Controller.Library;
  14. using MediaBrowser.Controller.MediaEncoding;
  15. using MediaBrowser.Controller.Net;
  16. using MediaBrowser.Model.Configuration;
  17. using MediaBrowser.Model.Dlna;
  18. using MediaBrowser.Model.Entities;
  19. using MediaBrowser.Model.IO;
  20. using MediaBrowser.Model.Serialization;
  21. using MediaBrowser.Model.Services;
  22. using Microsoft.Extensions.Logging;
  23. using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
  24. namespace MediaBrowser.Api.Playback.Hls
  25. {
  26. /// <summary>
  27. /// Options is needed for chromecast. Threw Head in there since it's related
  28. /// </summary>
  29. [Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
  30. [Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
  31. public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
  32. {
  33. public bool EnableAdaptiveBitrateStreaming { get; set; }
  34. public GetMasterHlsVideoPlaylist()
  35. {
  36. EnableAdaptiveBitrateStreaming = true;
  37. }
  38. }
  39. [Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
  40. [Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
  41. public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
  42. {
  43. public bool EnableAdaptiveBitrateStreaming { get; set; }
  44. public GetMasterHlsAudioPlaylist()
  45. {
  46. EnableAdaptiveBitrateStreaming = true;
  47. }
  48. }
  49. public interface IMasterHlsRequest
  50. {
  51. bool EnableAdaptiveBitrateStreaming { get; set; }
  52. }
  53. [Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
  54. public class GetVariantHlsVideoPlaylist : VideoStreamRequest
  55. {
  56. }
  57. [Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
  58. public class GetVariantHlsAudioPlaylist : StreamRequest
  59. {
  60. }
  61. [Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
  62. public class GetHlsVideoSegment : VideoStreamRequest
  63. {
  64. public string PlaylistId { get; set; }
  65. /// <summary>
  66. /// Gets or sets the segment id.
  67. /// </summary>
  68. /// <value>The segment id.</value>
  69. public string SegmentId { get; set; }
  70. }
  71. [Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
  72. public class GetHlsAudioSegment : StreamRequest
  73. {
  74. public string PlaylistId { get; set; }
  75. /// <summary>
  76. /// Gets or sets the segment id.
  77. /// </summary>
  78. /// <value>The segment id.</value>
  79. public string SegmentId { get; set; }
  80. }
  81. [Authenticated]
  82. public class DynamicHlsService : BaseHlsService
  83. {
  84. public DynamicHlsService(
  85. ILogger<DynamicHlsService> logger,
  86. IServerConfigurationManager serverConfigurationManager,
  87. IHttpResultFactory httpResultFactory,
  88. IUserManager userManager,
  89. ILibraryManager libraryManager,
  90. IIsoManager isoManager,
  91. IMediaEncoder mediaEncoder,
  92. IFileSystem fileSystem,
  93. IDlnaManager dlnaManager,
  94. IDeviceManager deviceManager,
  95. IMediaSourceManager mediaSourceManager,
  96. IJsonSerializer jsonSerializer,
  97. IAuthorizationContext authorizationContext,
  98. INetworkManager networkManager,
  99. EncodingHelper encodingHelper)
  100. : base(
  101. logger,
  102. serverConfigurationManager,
  103. httpResultFactory,
  104. userManager,
  105. libraryManager,
  106. isoManager,
  107. mediaEncoder,
  108. fileSystem,
  109. dlnaManager,
  110. deviceManager,
  111. mediaSourceManager,
  112. jsonSerializer,
  113. authorizationContext,
  114. encodingHelper)
  115. {
  116. NetworkManager = networkManager;
  117. }
  118. protected INetworkManager NetworkManager { get; private set; }
  119. public Task<object> Get(GetMasterHlsVideoPlaylist request)
  120. {
  121. return GetMasterPlaylistInternal(request, "GET");
  122. }
  123. public Task<object> Head(GetMasterHlsVideoPlaylist request)
  124. {
  125. return GetMasterPlaylistInternal(request, "HEAD");
  126. }
  127. public Task<object> Get(GetMasterHlsAudioPlaylist request)
  128. {
  129. return GetMasterPlaylistInternal(request, "GET");
  130. }
  131. public Task<object> Head(GetMasterHlsAudioPlaylist request)
  132. {
  133. return GetMasterPlaylistInternal(request, "HEAD");
  134. }
  135. public Task<object> Get(GetVariantHlsVideoPlaylist request)
  136. {
  137. return GetVariantPlaylistInternal(request, true, "main");
  138. }
  139. public Task<object> Get(GetVariantHlsAudioPlaylist request)
  140. {
  141. return GetVariantPlaylistInternal(request, false, "main");
  142. }
  143. public Task<object> Get(GetHlsVideoSegment request)
  144. {
  145. return GetDynamicSegment(request, request.SegmentId);
  146. }
  147. public Task<object> Get(GetHlsAudioSegment request)
  148. {
  149. return GetDynamicSegment(request, request.SegmentId);
  150. }
  151. private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
  152. {
  153. if ((request.StartTimeTicks ?? 0) > 0)
  154. {
  155. throw new ArgumentException("StartTimeTicks is not allowed.");
  156. }
  157. var cancellationTokenSource = new CancellationTokenSource();
  158. var cancellationToken = cancellationTokenSource.Token;
  159. var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
  160. var state = await GetState(request, cancellationToken).ConfigureAwait(false);
  161. var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
  162. var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
  163. var segmentExtension = GetSegmentFileExtension(state.Request);
  164. TranscodingJob job = null;
  165. if (File.Exists(segmentPath))
  166. {
  167. job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
  168. Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
  169. return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
  170. }
  171. var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
  172. await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  173. var released = false;
  174. var startTranscoding = false;
  175. try
  176. {
  177. if (File.Exists(segmentPath))
  178. {
  179. job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
  180. transcodingLock.Release();
  181. released = true;
  182. Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
  183. return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
  184. }
  185. else
  186. {
  187. var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
  188. var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
  189. if (currentTranscodingIndex == null)
  190. {
  191. Logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
  192. startTranscoding = true;
  193. }
  194. else if (requestedIndex < currentTranscodingIndex.Value)
  195. {
  196. Logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
  197. startTranscoding = true;
  198. }
  199. else if (requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
  200. {
  201. Logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", requestedIndex - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, requestedIndex);
  202. startTranscoding = true;
  203. }
  204. if (startTranscoding)
  205. {
  206. // If the playlist doesn't already exist, startup ffmpeg
  207. try
  208. {
  209. await ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
  210. if (currentTranscodingIndex.HasValue)
  211. {
  212. DeleteLastFile(playlistPath, segmentExtension, 0);
  213. }
  214. request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
  215. state.WaitForPath = segmentPath;
  216. job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
  217. }
  218. catch
  219. {
  220. state.Dispose();
  221. throw;
  222. }
  223. // await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
  224. }
  225. else
  226. {
  227. job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
  228. if (job.TranscodingThrottler != null)
  229. {
  230. await job.TranscodingThrottler.UnpauseTranscoding();
  231. }
  232. }
  233. }
  234. }
  235. finally
  236. {
  237. if (!released)
  238. {
  239. transcodingLock.Release();
  240. }
  241. }
  242. // Logger.LogInformation("waiting for {0}", segmentPath);
  243. // while (!File.Exists(segmentPath))
  244. //{
  245. // await Task.Delay(50, cancellationToken).ConfigureAwait(false);
  246. //}
  247. Logger.LogDebug("returning {0} [general case]", segmentPath);
  248. job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
  249. return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
  250. }
  251. private const int BufferSize = 81920;
  252. private long GetStartPositionTicks(StreamState state, int requestedIndex)
  253. {
  254. double startSeconds = 0;
  255. var lengths = GetSegmentLengths(state);
  256. if (requestedIndex >= lengths.Length)
  257. {
  258. var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
  259. throw new ArgumentException(msg);
  260. }
  261. for (var i = 0; i < requestedIndex; i++)
  262. {
  263. startSeconds += lengths[i];
  264. }
  265. var position = TimeSpan.FromSeconds(startSeconds).Ticks;
  266. return position;
  267. }
  268. private long GetEndPositionTicks(StreamState state, int requestedIndex)
  269. {
  270. double startSeconds = 0;
  271. var lengths = GetSegmentLengths(state);
  272. if (requestedIndex >= lengths.Length)
  273. {
  274. var msg = string.Format("Invalid segment index requested: {0} - Segment count: {1}", requestedIndex, lengths.Length);
  275. throw new ArgumentException(msg);
  276. }
  277. for (var i = 0; i <= requestedIndex; i++)
  278. {
  279. startSeconds += lengths[i];
  280. }
  281. var position = TimeSpan.FromSeconds(startSeconds).Ticks;
  282. return position;
  283. }
  284. private double[] GetSegmentLengths(StreamState state)
  285. {
  286. var result = new List<double>();
  287. var ticks = state.RunTimeTicks ?? 0;
  288. var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
  289. while (ticks > 0)
  290. {
  291. var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
  292. result.Add(TimeSpan.FromTicks(length).TotalSeconds);
  293. ticks -= length;
  294. }
  295. return result.ToArray();
  296. }
  297. public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
  298. {
  299. var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
  300. if (job == null || job.HasExited)
  301. {
  302. return null;
  303. }
  304. var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
  305. if (file == null)
  306. {
  307. return null;
  308. }
  309. var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
  310. var indexString = Path.GetFileNameWithoutExtension(file.Name).AsSpan().Slice(playlistFilename.Length);
  311. return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
  312. }
  313. private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
  314. {
  315. var file = GetLastTranscodingFile(playlistPath, segmentExtension, FileSystem);
  316. if (file != null)
  317. {
  318. DeleteFile(file.FullName, retryCount);
  319. }
  320. }
  321. private void DeleteFile(string path, int retryCount)
  322. {
  323. if (retryCount >= 5)
  324. {
  325. return;
  326. }
  327. Logger.LogDebug("Deleting partial HLS file {path}", path);
  328. try
  329. {
  330. FileSystem.DeleteFile(path);
  331. }
  332. catch (IOException ex)
  333. {
  334. Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
  335. var task = Task.Delay(100);
  336. Task.WaitAll(task);
  337. DeleteFile(path, retryCount + 1);
  338. }
  339. catch (Exception ex)
  340. {
  341. Logger.LogError(ex, "Error deleting partial stream file(s) {path}", path);
  342. }
  343. }
  344. private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
  345. {
  346. var folder = Path.GetDirectoryName(playlist);
  347. var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
  348. try
  349. {
  350. return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
  351. .Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
  352. .OrderByDescending(fileSystem.GetLastWriteTimeUtc)
  353. .FirstOrDefault();
  354. }
  355. catch (IOException)
  356. {
  357. return null;
  358. }
  359. }
  360. protected override int GetStartNumber(StreamState state)
  361. {
  362. return GetStartNumber(state.VideoRequest);
  363. }
  364. private int GetStartNumber(VideoStreamRequest request)
  365. {
  366. var segmentId = "0";
  367. if (request is GetHlsVideoSegment segmentRequest)
  368. {
  369. segmentId = segmentRequest.SegmentId;
  370. }
  371. return int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
  372. }
  373. private string GetSegmentPath(StreamState state, string playlist, int index)
  374. {
  375. var folder = Path.GetDirectoryName(playlist);
  376. var filename = Path.GetFileNameWithoutExtension(playlist);
  377. return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request));
  378. }
  379. private async Task<object> GetSegmentResult(StreamState state,
  380. string playlistPath,
  381. string segmentPath,
  382. string segmentExtension,
  383. int segmentIndex,
  384. TranscodingJob transcodingJob,
  385. CancellationToken cancellationToken)
  386. {
  387. var segmentExists = File.Exists(segmentPath);
  388. if (segmentExists)
  389. {
  390. if (transcodingJob != null && transcodingJob.HasExited)
  391. {
  392. // Transcoding job is over, so assume all existing files are ready
  393. Logger.LogDebug("serving up {0} as transcode is over", segmentPath);
  394. return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
  395. }
  396. var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
  397. // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
  398. if (segmentIndex < currentTranscodingIndex)
  399. {
  400. Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
  401. return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
  402. }
  403. }
  404. var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
  405. if (transcodingJob != null)
  406. {
  407. while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
  408. {
  409. // To be considered ready, the segment file has to exist AND
  410. // either the transcoding job should be done or next segment should also exist
  411. if (segmentExists)
  412. {
  413. if (transcodingJob.HasExited || File.Exists(nextSegmentPath))
  414. {
  415. Logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
  416. return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
  417. }
  418. }
  419. else
  420. {
  421. segmentExists = File.Exists(segmentPath);
  422. if (segmentExists)
  423. {
  424. continue; // avoid unnecessary waiting if segment just became available
  425. }
  426. }
  427. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  428. }
  429. if (!File.Exists(segmentPath))
  430. {
  431. Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
  432. }
  433. else
  434. {
  435. Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
  436. }
  437. cancellationToken.ThrowIfCancellationRequested();
  438. }
  439. else
  440. {
  441. Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
  442. }
  443. return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
  444. }
  445. private Task<object> GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJob transcodingJob)
  446. {
  447. var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
  448. return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
  449. {
  450. Path = segmentPath,
  451. FileShare = FileShare.ReadWrite,
  452. OnComplete = () =>
  453. {
  454. Logger.LogDebug("finished serving {0}", segmentPath);
  455. if (transcodingJob != null)
  456. {
  457. transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
  458. ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
  459. }
  460. }
  461. });
  462. }
  463. private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
  464. {
  465. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  466. if (string.IsNullOrEmpty(request.MediaSourceId))
  467. {
  468. throw new ArgumentException("MediaSourceId is required");
  469. }
  470. var playlistText = string.Empty;
  471. if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
  472. {
  473. var audioBitrate = state.OutputAudioBitrate ?? 0;
  474. var videoBitrate = state.OutputVideoBitrate ?? 0;
  475. playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
  476. }
  477. return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  478. }
  479. private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
  480. {
  481. var builder = new StringBuilder();
  482. builder.AppendLine("#EXTM3U");
  483. var isLiveStream = state.IsSegmentedLiveStream;
  484. var queryStringIndex = Request.RawUrl.IndexOf('?');
  485. var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
  486. // from universal audio service
  487. if (queryString.IndexOf("SegmentContainer", StringComparison.OrdinalIgnoreCase) == -1 && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer))
  488. {
  489. queryString += "&SegmentContainer=" + state.Request.SegmentContainer;
  490. }
  491. // from universal audio service
  492. if (!string.IsNullOrWhiteSpace(state.Request.TranscodeReasons) && queryString.IndexOf("TranscodeReasons=", StringComparison.OrdinalIgnoreCase) == -1)
  493. {
  494. queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
  495. }
  496. // Main stream
  497. var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
  498. playlistUrl += queryString;
  499. var request = state.Request;
  500. var subtitleStreams = state.MediaSource
  501. .MediaStreams
  502. .Where(i => i.IsTextSubtitleStream)
  503. .ToList();
  504. var subtitleGroup = subtitleStreams.Count > 0 &&
  505. request is GetMasterHlsVideoPlaylist &&
  506. (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest.EnableSubtitlesInManifest) ?
  507. "subs" :
  508. null;
  509. // If we're burning in subtitles then don't add additional subs to the manifest
  510. if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
  511. {
  512. subtitleGroup = null;
  513. }
  514. if (!string.IsNullOrWhiteSpace(subtitleGroup))
  515. {
  516. AddSubtitles(state, subtitleStreams, builder);
  517. }
  518. AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
  519. if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
  520. {
  521. var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
  522. // By default, vary by just 200k
  523. var variation = GetBitrateVariation(totalBitrate);
  524. var newBitrate = totalBitrate - variation;
  525. var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
  526. AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
  527. variation *= 2;
  528. newBitrate = totalBitrate - variation;
  529. variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
  530. AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
  531. }
  532. return builder.ToString();
  533. }
  534. private string ReplaceBitrate(string url, int oldValue, int newValue)
  535. {
  536. return url.Replace(
  537. "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
  538. "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
  539. StringComparison.OrdinalIgnoreCase);
  540. }
  541. private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
  542. {
  543. var selectedIndex = state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
  544. foreach (var stream in subtitles)
  545. {
  546. const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
  547. var name = stream.DisplayTitle;
  548. var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
  549. var isForced = stream.IsForced;
  550. var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
  551. state.Request.MediaSourceId,
  552. stream.Index.ToString(CultureInfo.InvariantCulture),
  553. 30.ToString(CultureInfo.InvariantCulture),
  554. AuthorizationContext.GetAuthorizationInfo(Request).Token);
  555. var line = string.Format(format,
  556. name,
  557. isDefault ? "YES" : "NO",
  558. isForced ? "YES" : "NO",
  559. url,
  560. stream.Language ?? "Unknown");
  561. builder.AppendLine(line);
  562. }
  563. }
  564. private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream)
  565. {
  566. // Within the local network this will likely do more harm than good.
  567. if (Request.IsLocal || NetworkManager.IsInLocalNetwork(Request.RemoteIp))
  568. {
  569. return false;
  570. }
  571. if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming)
  572. {
  573. return false;
  574. }
  575. if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
  576. {
  577. // Opening live streams is so slow it's not even worth it
  578. return false;
  579. }
  580. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  581. {
  582. return false;
  583. }
  584. if (EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
  585. {
  586. return false;
  587. }
  588. if (!state.IsOutputVideo)
  589. {
  590. return false;
  591. }
  592. // Having problems in android
  593. return false;
  594. // return state.VideoRequest.VideoBitRate.HasValue;
  595. }
  596. /// <summary>
  597. /// Get the H.26X level of the output video stream.
  598. /// </summary>
  599. /// <param name="state">StreamState of the current stream.</param>
  600. /// <returns>H.26X level of the output video stream.</returns>
  601. private int? GetOutputVideoCodecLevel(StreamState state)
  602. {
  603. string levelString;
  604. if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
  605. && state.VideoStream.Level.HasValue)
  606. {
  607. levelString = state.VideoStream?.Level.ToString();
  608. }
  609. else
  610. {
  611. levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
  612. }
  613. if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
  614. {
  615. return parsedLevel;
  616. }
  617. return null;
  618. }
  619. /// <summary>
  620. /// Gets a formatted string of the output audio codec, for use in the CODECS field.
  621. /// </summary>
  622. /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
  623. /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
  624. /// <param name="state">StreamState of the current stream.</param>
  625. /// <returns>Formatted audio codec string.</returns>
  626. private string GetPlaylistAudioCodecs(StreamState state)
  627. {
  628. if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
  629. {
  630. string profile = state.GetRequestedProfiles("aac").FirstOrDefault();
  631. return HlsCodecStringFactory.GetAACString(profile);
  632. }
  633. else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
  634. {
  635. return HlsCodecStringFactory.GetMP3String();
  636. }
  637. else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
  638. {
  639. return HlsCodecStringFactory.GetAC3String();
  640. }
  641. else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
  642. {
  643. return HlsCodecStringFactory.GetEAC3String();
  644. }
  645. return string.Empty;
  646. }
  647. /// <summary>
  648. /// Gets a formatted string of the output video codec, for use in the CODECS field.
  649. /// </summary>
  650. /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
  651. /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
  652. /// <param name="state">StreamState of the current stream.</param>
  653. /// <returns>Formatted video codec string.</returns>
  654. private string GetPlaylistVideoCodecs(StreamState state, string codec, int level)
  655. {
  656. if (level == 0)
  657. {
  658. // This is 0 when there's no requested H.26X level in the device profile
  659. // and the source is not encoded in H.26X
  660. Logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
  661. return string.Empty;
  662. }
  663. if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
  664. {
  665. string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
  666. return HlsCodecStringFactory.GetH264String(profile, level);
  667. }
  668. else if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
  669. || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
  670. {
  671. string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
  672. return HlsCodecStringFactory.GetH265String(profile, level);
  673. }
  674. return string.Empty;
  675. }
  676. /// <summary>
  677. /// Appends a CODECS field containing formatted strings of
  678. /// the active streams output video and audio codecs.
  679. /// </summary>
  680. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  681. /// <seealso cref="GetPlaylistVideoCodecs(StreamState, string, int)"/>
  682. /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
  683. /// <param name="builder">StringBuilder to append the field to.</param>
  684. /// <param name="state">StreamState of the current stream.</param>
  685. private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
  686. {
  687. // Video
  688. string videoCodecs = string.Empty;
  689. int? videoCodecLevel = GetOutputVideoCodecLevel(state);
  690. if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec) && videoCodecLevel.HasValue)
  691. {
  692. videoCodecs = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value);
  693. }
  694. // Audio
  695. string audioCodecs = string.Empty;
  696. if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
  697. {
  698. audioCodecs = GetPlaylistAudioCodecs(state);
  699. }
  700. StringBuilder codecs = new StringBuilder();
  701. codecs.Append(videoCodecs)
  702. .Append(',')
  703. .Append(audioCodecs);
  704. if (codecs.Length > 1)
  705. {
  706. builder.Append(",CODECS=\"")
  707. .Append(codecs)
  708. .Append('"');
  709. }
  710. }
  711. /// <summary>
  712. /// Appends a FRAME-RATE field containing the framerate of the output stream.
  713. /// </summary>
  714. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  715. /// <param name="builder">StringBuilder to append the field to.</param>
  716. /// <param name="state">StreamState of the current stream.</param>
  717. private void AppendPlaylistFramerateField(StringBuilder builder, StreamState state)
  718. {
  719. double? framerate = null;
  720. if (state.TargetFramerate.HasValue)
  721. {
  722. framerate = Math.Round(state.TargetFramerate.GetValueOrDefault(), 3);
  723. }
  724. else if (state.VideoStream?.RealFrameRate != null)
  725. {
  726. framerate = Math.Round(state.VideoStream.RealFrameRate.GetValueOrDefault(), 3);
  727. }
  728. if (framerate.HasValue)
  729. {
  730. builder.Append(",FRAME-RATE=")
  731. .Append(framerate.Value);
  732. }
  733. }
  734. /// <summary>
  735. /// Appends a RESOLUTION field containing the resolution of the output stream.
  736. /// </summary>
  737. /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
  738. /// <param name="builder">StringBuilder to append the field to.</param>
  739. /// <param name="state">StreamState of the current stream.</param>
  740. private void AppendPlaylistResolutionField(StringBuilder builder, StreamState state)
  741. {
  742. if (state.OutputWidth.HasValue && state.OutputHeight.HasValue)
  743. {
  744. builder.Append(",RESOLUTION=")
  745. .Append(state.OutputWidth.GetValueOrDefault())
  746. .Append('x')
  747. .Append(state.OutputHeight.GetValueOrDefault());
  748. }
  749. }
  750. private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
  751. {
  752. builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
  753. .Append(bitrate.ToString(CultureInfo.InvariantCulture))
  754. .Append(",AVERAGE-BANDWIDTH=")
  755. .Append(bitrate.ToString(CultureInfo.InvariantCulture));
  756. AppendPlaylistCodecsField(builder, state);
  757. AppendPlaylistResolutionField(builder, state);
  758. AppendPlaylistFramerateField(builder, state);
  759. if (!string.IsNullOrWhiteSpace(subtitleGroup))
  760. {
  761. builder.Append(",SUBTITLES=\"")
  762. .Append(subtitleGroup)
  763. .Append('"');
  764. }
  765. builder.Append(Environment.NewLine);
  766. builder.AppendLine(url);
  767. }
  768. private int GetBitrateVariation(int bitrate)
  769. {
  770. // By default, vary by just 50k
  771. var variation = 50000;
  772. if (bitrate >= 10000000)
  773. {
  774. variation = 2000000;
  775. }
  776. else if (bitrate >= 5000000)
  777. {
  778. variation = 1500000;
  779. }
  780. else if (bitrate >= 3000000)
  781. {
  782. variation = 1000000;
  783. }
  784. else if (bitrate >= 2000000)
  785. {
  786. variation = 500000;
  787. }
  788. else if (bitrate >= 1000000)
  789. {
  790. variation = 300000;
  791. }
  792. else if (bitrate >= 600000)
  793. {
  794. variation = 200000;
  795. }
  796. else if (bitrate >= 400000)
  797. {
  798. variation = 100000;
  799. }
  800. return variation;
  801. }
  802. private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
  803. {
  804. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  805. var segmentLengths = GetSegmentLengths(state);
  806. var builder = new StringBuilder();
  807. builder.AppendLine("#EXTM3U");
  808. builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
  809. builder.AppendLine("#EXT-X-VERSION:3");
  810. builder.Append("#EXT-X-TARGETDURATION:")
  811. .AppendLine(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
  812. builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
  813. var queryStringIndex = Request.RawUrl.IndexOf('?');
  814. var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
  815. // if ((Request.UserAgent ?? string.Empty).IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1)
  816. //{
  817. // queryString = string.Empty;
  818. //}
  819. var index = 0;
  820. foreach (var length in segmentLengths)
  821. {
  822. builder.Append("#EXTINF:")
  823. .Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
  824. .AppendLine(", nodesc");
  825. builder.AppendFormat(
  826. CultureInfo.InvariantCulture,
  827. "hls1/{0}/{1}{2}{3}",
  828. name,
  829. index.ToString(CultureInfo.InvariantCulture),
  830. GetSegmentFileExtension(request),
  831. queryString).AppendLine();
  832. index++;
  833. }
  834. builder.AppendLine("#EXT-X-ENDLIST");
  835. var playlistText = builder.ToString();
  836. return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  837. }
  838. protected override string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
  839. {
  840. var audioCodec = EncodingHelper.GetAudioEncoder(state);
  841. if (!state.IsOutputVideo)
  842. {
  843. if (EncodingHelper.IsCopyCodec(audioCodec))
  844. {
  845. return "-acodec copy";
  846. }
  847. var audioTranscodeParams = new List<string>();
  848. audioTranscodeParams.Add("-acodec " + audioCodec);
  849. if (state.OutputAudioBitrate.HasValue)
  850. {
  851. audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
  852. }
  853. if (state.OutputAudioChannels.HasValue)
  854. {
  855. audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
  856. }
  857. if (state.OutputAudioSampleRate.HasValue)
  858. {
  859. audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
  860. }
  861. audioTranscodeParams.Add("-vn");
  862. return string.Join(" ", audioTranscodeParams.ToArray());
  863. }
  864. if (EncodingHelper.IsCopyCodec(audioCodec))
  865. {
  866. var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
  867. if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
  868. {
  869. return "-codec:a:0 copy -copypriorss:a:0 0";
  870. }
  871. return "-codec:a:0 copy";
  872. }
  873. var args = "-codec:a:0 " + audioCodec;
  874. var channels = state.OutputAudioChannels;
  875. if (channels.HasValue)
  876. {
  877. args += " -ac " + channels.Value;
  878. }
  879. var bitrate = state.OutputAudioBitrate;
  880. if (bitrate.HasValue)
  881. {
  882. args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
  883. }
  884. if (state.OutputAudioSampleRate.HasValue)
  885. {
  886. args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
  887. }
  888. args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
  889. return args;
  890. }
  891. protected override string GetVideoArguments(StreamState state, EncodingOptions encodingOptions)
  892. {
  893. if (!state.IsOutputVideo)
  894. {
  895. return string.Empty;
  896. }
  897. var codec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
  898. var args = "-codec:v:0 " + codec;
  899. // if (state.EnableMpegtsM2TsMode)
  900. // {
  901. // args += " -mpegts_m2ts_mode 1";
  902. // }
  903. // See if we can save come cpu cycles by avoiding encoding
  904. if (EncodingHelper.IsCopyCodec(codec))
  905. {
  906. if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
  907. {
  908. string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
  909. if (!string.IsNullOrEmpty(bitStreamArgs))
  910. {
  911. args += " " + bitStreamArgs;
  912. }
  913. }
  914. // args += " -flags -global_header";
  915. }
  916. else
  917. {
  918. var gopArg = string.Empty;
  919. var keyFrameArg = string.Format(
  920. CultureInfo.InvariantCulture,
  921. " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
  922. GetStartNumber(state) * state.SegmentLength,
  923. state.SegmentLength);
  924. var framerate = state.VideoStream?.RealFrameRate;
  925. if (framerate.HasValue)
  926. {
  927. // This is to make sure keyframe interval is limited to our segment,
  928. // as forcing keyframes is not enough.
  929. // Example: we encoded half of desired length, then codec detected
  930. // scene cut and inserted a keyframe; next forced keyframe would
  931. // be created outside of segment, which breaks seeking
  932. // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
  933. gopArg = string.Format(
  934. CultureInfo.InvariantCulture,
  935. " -g {0} -keyint_min {0} -sc_threshold 0",
  936. Math.Ceiling(state.SegmentLength * framerate.Value)
  937. );
  938. }
  939. args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset());
  940. // Unable to force key frames using these hw encoders, set key frames by GOP
  941. if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
  942. || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
  943. || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
  944. {
  945. args += " " + gopArg;
  946. }
  947. else
  948. {
  949. args += " " + keyFrameArg + gopArg;
  950. }
  951. // args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
  952. var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
  953. // This is for graphical subs
  954. if (hasGraphicalSubs)
  955. {
  956. args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
  957. }
  958. // Add resolution params, if specified
  959. else
  960. {
  961. args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
  962. }
  963. // -start_at_zero is necessary to use with -ss when seeking,
  964. // otherwise the target position cannot be determined.
  965. if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream))
  966. {
  967. args += " -start_at_zero";
  968. }
  969. // args += " -flags -global_header";
  970. }
  971. if (!string.IsNullOrEmpty(state.OutputVideoSync))
  972. {
  973. args += " -vsync " + state.OutputVideoSync;
  974. }
  975. args += EncodingHelper.GetOutputFFlags(state);
  976. return args;
  977. }
  978. protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
  979. {
  980. var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
  981. var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
  982. if (state.BaseRequest.BreakOnNonKeyFrames)
  983. {
  984. // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
  985. // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
  986. // to produce a missing part of video stream before first keyframe is encountered, which may lead to
  987. // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
  988. Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
  989. state.BaseRequest.BreakOnNonKeyFrames = false;
  990. }
  991. var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
  992. // If isEncoding is true we're actually starting ffmpeg
  993. var startNumber = GetStartNumber(state);
  994. var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
  995. var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
  996. var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
  997. var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
  998. if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
  999. {
  1000. segmentFormat = "mpegts";
  1001. }
  1002. return string.Format(
  1003. "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
  1004. inputModifier,
  1005. EncodingHelper.GetInputArgument(state, encodingOptions),
  1006. threads,
  1007. mapArgs,
  1008. GetVideoArguments(state, encodingOptions),
  1009. GetAudioArguments(state, encodingOptions),
  1010. state.SegmentLength.ToString(CultureInfo.InvariantCulture),
  1011. segmentFormat,
  1012. startNumberParam,
  1013. outputTsArg,
  1014. outputPath
  1015. ).Trim();
  1016. }
  1017. }
  1018. }