BaseHlsService.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Controller.Configuration;
  9. using MediaBrowser.Controller.Devices;
  10. using MediaBrowser.Controller.Dlna;
  11. using MediaBrowser.Controller.Library;
  12. using MediaBrowser.Controller.MediaEncoding;
  13. using MediaBrowser.Controller.Net;
  14. using MediaBrowser.Model.Configuration;
  15. using MediaBrowser.Model.IO;
  16. using MediaBrowser.Model.Net;
  17. using MediaBrowser.Model.Serialization;
  18. using Microsoft.Extensions.Logging;
  19. namespace MediaBrowser.Api.Playback.Hls
  20. {
  21. /// <summary>
  22. /// Class BaseHlsService
  23. /// </summary>
  24. public abstract class BaseHlsService : BaseStreamingService
  25. {
  26. public BaseHlsService(
  27. ILogger logger,
  28. IServerConfigurationManager serverConfigurationManager,
  29. IHttpResultFactory httpResultFactory,
  30. IUserManager userManager,
  31. ILibraryManager libraryManager,
  32. IIsoManager isoManager,
  33. IMediaEncoder mediaEncoder,
  34. IFileSystem fileSystem,
  35. IDlnaManager dlnaManager,
  36. IDeviceManager deviceManager,
  37. IMediaSourceManager mediaSourceManager,
  38. IJsonSerializer jsonSerializer,
  39. IAuthorizationContext authorizationContext,
  40. EncodingHelper encodingHelper)
  41. : base(
  42. logger,
  43. serverConfigurationManager,
  44. httpResultFactory,
  45. userManager,
  46. libraryManager,
  47. isoManager,
  48. mediaEncoder,
  49. fileSystem,
  50. dlnaManager,
  51. deviceManager,
  52. mediaSourceManager,
  53. jsonSerializer,
  54. authorizationContext,
  55. encodingHelper)
  56. {
  57. }
  58. /// <summary>
  59. /// Gets the audio arguments.
  60. /// </summary>
  61. protected abstract string GetAudioArguments(StreamState state, EncodingOptions encodingOptions);
  62. /// <summary>
  63. /// Gets the video arguments.
  64. /// </summary>
  65. protected abstract string GetVideoArguments(StreamState state, EncodingOptions encodingOptions);
  66. /// <summary>
  67. /// Gets the segment file extension.
  68. /// </summary>
  69. protected string GetSegmentFileExtension(StreamRequest request)
  70. {
  71. var segmentContainer = request.SegmentContainer;
  72. if (!string.IsNullOrWhiteSpace(segmentContainer))
  73. {
  74. return "." + segmentContainer;
  75. }
  76. return ".ts";
  77. }
  78. /// <summary>
  79. /// Gets the type of the transcoding job.
  80. /// </summary>
  81. /// <value>The type of the transcoding job.</value>
  82. protected override TranscodingJobType TranscodingJobType => TranscodingJobType.Hls;
  83. /// <summary>
  84. /// Processes the request async.
  85. /// </summary>
  86. /// <param name="request">The request.</param>
  87. /// <param name="isLive">if set to <c>true</c> [is live].</param>
  88. /// <returns>Task{System.Object}.</returns>
  89. /// <exception cref="ArgumentException">A video bitrate is required
  90. /// or
  91. /// An audio bitrate is required</exception>
  92. protected async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
  93. {
  94. var cancellationTokenSource = new CancellationTokenSource();
  95. var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false);
  96. TranscodingJob job = null;
  97. var playlist = state.OutputFilePath;
  98. if (!File.Exists(playlist))
  99. {
  100. var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
  101. await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  102. try
  103. {
  104. if (!File.Exists(playlist))
  105. {
  106. // If the playlist doesn't already exist, startup ffmpeg
  107. try
  108. {
  109. job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
  110. job.IsLiveOutput = isLive;
  111. }
  112. catch
  113. {
  114. state.Dispose();
  115. throw;
  116. }
  117. var minSegments = state.MinSegments;
  118. if (minSegments > 0)
  119. {
  120. await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
  121. }
  122. }
  123. }
  124. finally
  125. {
  126. transcodingLock.Release();
  127. }
  128. }
  129. if (isLive)
  130. {
  131. job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
  132. if (job != null)
  133. {
  134. ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
  135. }
  136. return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  137. }
  138. var audioBitrate = state.OutputAudioBitrate ?? 0;
  139. var videoBitrate = state.OutputVideoBitrate ?? 0;
  140. var baselineStreamBitrate = 64000;
  141. var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
  142. job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
  143. if (job != null)
  144. {
  145. ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
  146. }
  147. return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  148. }
  149. private string GetLivePlaylistText(string path, int segmentLength)
  150. {
  151. using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
  152. using var reader = new StreamReader(stream);
  153. var text = reader.ReadToEnd();
  154. text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
  155. var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
  156. text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
  157. //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
  158. return text;
  159. }
  160. private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
  161. {
  162. var builder = new StringBuilder();
  163. builder.AppendLine("#EXTM3U");
  164. // Pad a little to satisfy the apple hls validator
  165. var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
  166. // Main stream
  167. builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture));
  168. var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
  169. builder.AppendLine(playlistUrl);
  170. return builder.ToString();
  171. }
  172. protected virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
  173. {
  174. Logger.LogDebug("Waiting for {0} segments in {1}", segmentCount, playlist);
  175. while (!cancellationToken.IsCancellationRequested)
  176. {
  177. try
  178. {
  179. // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
  180. var fileStream = GetPlaylistFileStream(playlist);
  181. await using (fileStream.ConfigureAwait(false))
  182. {
  183. using var reader = new StreamReader(fileStream);
  184. var count = 0;
  185. while (!reader.EndOfStream)
  186. {
  187. var line = await reader.ReadLineAsync().ConfigureAwait(false);
  188. if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
  189. {
  190. count++;
  191. if (count >= segmentCount)
  192. {
  193. Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
  194. return;
  195. }
  196. }
  197. }
  198. }
  199. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  200. }
  201. catch (IOException)
  202. {
  203. // May get an error if the file is locked
  204. }
  205. await Task.Delay(50, cancellationToken).ConfigureAwait(false);
  206. }
  207. }
  208. protected Stream GetPlaylistFileStream(string path)
  209. {
  210. return new FileStream(
  211. path,
  212. FileMode.Open,
  213. FileAccess.Read,
  214. FileShare.ReadWrite,
  215. IODefaults.FileStreamBufferSize,
  216. FileOptions.SequentialScan);
  217. }
  218. protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
  219. {
  220. var itsOffsetMs = 0;
  221. var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture));
  222. var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
  223. var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
  224. var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
  225. // If isEncoding is true we're actually starting ffmpeg
  226. var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0";
  227. var baseUrlParam = string.Empty;
  228. if (state.Request is GetLiveHlsStream)
  229. {
  230. baseUrlParam = string.Format(" -hls_base_url \"{0}/\"",
  231. "hls/" + Path.GetFileNameWithoutExtension(outputPath));
  232. }
  233. var useGenericSegmenter = true;
  234. if (useGenericSegmenter)
  235. {
  236. var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
  237. var timeDeltaParam = string.Empty;
  238. var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
  239. if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
  240. {
  241. segmentFormat = "mpegts";
  242. }
  243. baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
  244. return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
  245. inputModifier,
  246. EncodingHelper.GetInputArgument(state, encodingOptions),
  247. threads,
  248. EncodingHelper.GetMapArgs(state),
  249. GetVideoArguments(state, encodingOptions),
  250. GetAudioArguments(state, encodingOptions),
  251. state.SegmentLength.ToString(CultureInfo.InvariantCulture),
  252. startNumberParam,
  253. outputPath,
  254. outputTsArg,
  255. timeDeltaParam,
  256. segmentFormat,
  257. baseUrlParam
  258. ).Trim();
  259. }
  260. // add when stream copying?
  261. // -avoid_negative_ts make_zero -fflags +genpts
  262. var args = string.Format("{0} {1} {2} -map_metadata -1 -map_chapters -1 -threads {3} {4} {5} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero {6} -hls_time {7} -individual_header_trailer 0 -start_number {8} -hls_list_size {9}{10} -y \"{11}\"",
  263. itsOffset,
  264. inputModifier,
  265. EncodingHelper.GetInputArgument(state, encodingOptions),
  266. threads,
  267. EncodingHelper.GetMapArgs(state),
  268. GetVideoArguments(state, encodingOptions),
  269. GetAudioArguments(state, encodingOptions),
  270. state.SegmentLength.ToString(CultureInfo.InvariantCulture),
  271. startNumberParam,
  272. state.HlsListSize.ToString(CultureInfo.InvariantCulture),
  273. baseUrlParam,
  274. outputPath
  275. ).Trim();
  276. return args;
  277. }
  278. protected override string GetDefaultEncoderPreset()
  279. {
  280. return "veryfast";
  281. }
  282. protected virtual int GetStartNumber(StreamState state)
  283. {
  284. return 0;
  285. }
  286. }
  287. }