DynamicHlsService.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Controller.Channels;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Dlna;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.LiveTv;
  7. using MediaBrowser.Controller.MediaEncoding;
  8. using MediaBrowser.Model.IO;
  9. using ServiceStack;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Globalization;
  13. using System.IO;
  14. using System.Text;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. namespace MediaBrowser.Api.Playback.Hls
  18. {
  19. [Route("/Videos/{Id}/master.m3u8", "GET")]
  20. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  21. public class GetMasterHlsVideoStream : VideoStreamRequest
  22. {
  23. [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
  24. public int? BaselineStreamAudioBitRate { get; set; }
  25. [ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
  26. public bool AppendBaselineStream { get; set; }
  27. }
  28. [Route("/Videos/{Id}/main.m3u8", "GET")]
  29. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  30. public class GetMainHlsVideoStream : VideoStreamRequest
  31. {
  32. }
  33. [Route("/Videos/{Id}/baseline.m3u8", "GET")]
  34. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  35. public class GetBaselineHlsVideoStream : VideoStreamRequest
  36. {
  37. }
  38. /// <summary>
  39. /// Class GetHlsVideoSegment
  40. /// </summary>
  41. [Route("/Videos/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.ts", "GET")]
  42. [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
  43. public class GetDynamicHlsVideoSegment : VideoStreamRequest
  44. {
  45. public string PlaylistId { get; set; }
  46. /// <summary>
  47. /// Gets or sets the segment id.
  48. /// </summary>
  49. /// <value>The segment id.</value>
  50. public string SegmentId { get; set; }
  51. }
  52. public class DynamicHlsService : BaseHlsService
  53. {
  54. public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, ILiveTvManager liveTvManager, IDlnaManager dlnaManager, IChannelManager channelManager, ISubtitleEncoder subtitleEncoder) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, liveTvManager, dlnaManager, channelManager, subtitleEncoder)
  55. {
  56. }
  57. public object Get(GetMasterHlsVideoStream request)
  58. {
  59. var result = GetAsync(request).Result;
  60. return result;
  61. }
  62. public object Get(GetDynamicHlsVideoSegment request)
  63. {
  64. if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase))
  65. {
  66. return GetDynamicSegment(request, false).Result;
  67. }
  68. return GetDynamicSegment(request, true).Result;
  69. }
  70. private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
  71. private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
  72. {
  73. var cancellationTokenSource = new CancellationTokenSource();
  74. var cancellationToken = cancellationTokenSource.Token;
  75. var index = int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
  76. var state = await GetState(request, cancellationToken).ConfigureAwait(false);
  77. var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
  78. var segmentPath = GetSegmentPath(playlistPath, index);
  79. if (File.Exists(segmentPath))
  80. {
  81. ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
  82. return GetSegementResult(segmentPath);
  83. }
  84. await FfmpegStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
  85. try
  86. {
  87. if (File.Exists(segmentPath))
  88. {
  89. ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
  90. return GetSegementResult(segmentPath);
  91. }
  92. else
  93. {
  94. if (index == 0)
  95. {
  96. // If the playlist doesn't already exist, startup ffmpeg
  97. try
  98. {
  99. ApiEntryPoint.Instance.KillTranscodingJobs(state.Request.DeviceId, false);
  100. await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
  101. }
  102. catch
  103. {
  104. state.Dispose();
  105. throw;
  106. }
  107. await WaitForMinimumSegmentCount(playlistPath, 2, cancellationTokenSource.Token).ConfigureAwait(false);
  108. }
  109. }
  110. }
  111. finally
  112. {
  113. FfmpegStartLock.Release();
  114. }
  115. Logger.Info("waiting for {0}", segmentPath);
  116. while (!File.Exists(segmentPath))
  117. {
  118. await Task.Delay(50, cancellationToken).ConfigureAwait(false);
  119. }
  120. Logger.Info("returning {0}", segmentPath);
  121. return GetSegementResult(segmentPath);
  122. }
  123. protected override int GetStartNumber(StreamState state)
  124. {
  125. var request = (GetDynamicHlsVideoSegment) state.Request;
  126. return int.Parse(request.SegmentId, NumberStyles.Integer, UsCulture);
  127. }
  128. private string GetSegmentPath(string playlist, int index)
  129. {
  130. var folder = Path.GetDirectoryName(playlist);
  131. var filename = Path.GetFileNameWithoutExtension(playlist);
  132. return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts");
  133. }
  134. private object GetSegementResult(string path)
  135. {
  136. // TODO: Handle if it's currently being written to
  137. return ResultFactory.GetStaticFileResult(Request, path, FileShare.ReadWrite);
  138. }
  139. private async Task<object> GetAsync(GetMasterHlsVideoStream request)
  140. {
  141. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  142. int audioBitrate;
  143. int videoBitrate;
  144. GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
  145. var appendBaselineStream = false;
  146. var baselineStreamBitrate = 64000;
  147. var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream;
  148. if (hlsVideoRequest != null)
  149. {
  150. appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
  151. baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
  152. }
  153. var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
  154. return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  155. }
  156. private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
  157. {
  158. var builder = new StringBuilder();
  159. builder.AppendLine("#EXTM3U");
  160. // Pad a little to satisfy the apple hls validator
  161. var paddedBitrate = Convert.ToInt32(bitrate * 1.05);
  162. var queryStringIndex = Request.RawUrl.IndexOf('?');
  163. var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
  164. // Main stream
  165. builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
  166. var playlistUrl = "main.m3u8" + queryString;
  167. builder.AppendLine(playlistUrl);
  168. // Low bitrate stream
  169. if (includeBaselineStream)
  170. {
  171. builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture));
  172. playlistUrl = "baseline.m3u8" + queryString;
  173. builder.AppendLine(playlistUrl);
  174. }
  175. return builder.ToString();
  176. }
  177. public object Get(GetMainHlsVideoStream request)
  178. {
  179. var result = GetPlaylistAsync(request, "main").Result;
  180. return result;
  181. }
  182. public object Get(GetBaselineHlsVideoStream request)
  183. {
  184. var result = GetPlaylistAsync(request, "baseline").Result;
  185. return result;
  186. }
  187. private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
  188. {
  189. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  190. var builder = new StringBuilder();
  191. builder.AppendLine("#EXTM3U");
  192. builder.AppendLine("#EXT-X-VERSION:3");
  193. builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
  194. builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
  195. var queryStringIndex = Request.RawUrl.IndexOf('?');
  196. var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
  197. var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
  198. var index = 0;
  199. while (seconds > 0)
  200. {
  201. var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds;
  202. builder.AppendLine("#EXTINF:" + length.ToString(UsCulture));
  203. builder.AppendLine(string.Format("hlsdynamic/{0}/{1}.ts{2}",
  204. name,
  205. index.ToString(UsCulture),
  206. queryString));
  207. seconds -= state.SegmentLength;
  208. index++;
  209. }
  210. builder.AppendLine("#EXT-X-ENDLIST");
  211. var playlistText = builder.ToString();
  212. return ResultFactory.GetResult(playlistText, Common.Net.MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  213. }
  214. protected override string GetAudioArguments(StreamState state)
  215. {
  216. var codec = state.OutputAudioCodec;
  217. if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
  218. {
  219. return "-codec:a:0 copy";
  220. }
  221. var args = "-codec:a:0 " + codec;
  222. if (state.AudioStream != null)
  223. {
  224. var channels = state.OutputAudioChannels;
  225. if (channels.HasValue)
  226. {
  227. args += " -ac " + channels.Value;
  228. }
  229. var bitrate = state.OutputAudioBitrate;
  230. if (bitrate.HasValue)
  231. {
  232. args += " -ab " + bitrate.Value.ToString(UsCulture);
  233. }
  234. args += " " + GetAudioFilterParam(state, true);
  235. return args;
  236. }
  237. return args;
  238. }
  239. protected override string GetVideoArguments(StreamState state)
  240. {
  241. var codec = state.OutputVideoCodec;
  242. // See if we can save come cpu cycles by avoiding encoding
  243. if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
  244. {
  245. return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
  246. }
  247. var keyFrameArg = state.ReadInputAtNativeFramerate ?
  248. " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+1))" :
  249. " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
  250. var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
  251. var args = "-codec:v:0 " + codec + " " + GetVideoQualityParam(state, "libx264", true) + keyFrameArg;
  252. // Add resolution params, if specified
  253. if (!hasGraphicalSubs)
  254. {
  255. if (state.VideoRequest.Width.HasValue || state.VideoRequest.Height.HasValue || state.VideoRequest.MaxHeight.HasValue || state.VideoRequest.MaxWidth.HasValue)
  256. {
  257. args += GetOutputSizeParam(state, codec, CancellationToken.None);
  258. }
  259. }
  260. // This is for internal graphical subs
  261. if (hasGraphicalSubs)
  262. {
  263. args += GetInternalGraphicalSubtitleParam(state, codec);
  264. }
  265. return args;
  266. }
  267. /// <summary>
  268. /// Gets the segment file extension.
  269. /// </summary>
  270. /// <param name="state">The state.</param>
  271. /// <returns>System.String.</returns>
  272. protected override string GetSegmentFileExtension(StreamState state)
  273. {
  274. return ".ts";
  275. }
  276. }
  277. }