BaseHlsService.cs 14 KB

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