BaseHlsService.cs 14 KB

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