VideoHlsService.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Dto;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.LiveTv;
  6. using MediaBrowser.Controller.MediaInfo;
  7. using MediaBrowser.Controller.Persistence;
  8. using MediaBrowser.Model.Dto;
  9. using MediaBrowser.Model.IO;
  10. using ServiceStack;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.Text;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. namespace MediaBrowser.Api.Playback.Hls
  17. {
  18. /// <summary>
  19. /// Class GetHlsVideoStream
  20. /// </summary>
  21. [Route("/Videos/{Id}/stream.m3u8", "GET")]
  22. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  23. public class GetHlsVideoStream : VideoStreamRequest
  24. {
  25. [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
  26. public int? BaselineStreamAudioBitRate { get; set; }
  27. [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")]
  28. public bool AppendBaselineStream { get; set; }
  29. [ApiMember(Name = "TimeStampOffsetMs", Description = "Optional. Alter the timestamps in the playlist by a given amount, in ms. Default is 1000.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
  30. public int TimeStampOffsetMs { get; set; }
  31. }
  32. [Route("/Videos/{Id}/master.m3u8", "GET")]
  33. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  34. public class GetMasterHlsVideoStream : VideoStreamRequest
  35. {
  36. [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
  37. public int? BaselineStreamAudioBitRate { get; set; }
  38. [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")]
  39. public bool AppendBaselineStream { get; set; }
  40. }
  41. [Route("/Videos/{Id}/main.m3u8", "GET")]
  42. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  43. public class GetMainHlsVideoStream : VideoStreamRequest
  44. {
  45. }
  46. [Route("/Videos/{Id}/baseline.m3u8", "GET")]
  47. [Api(Description = "Gets a video stream using HTTP live streaming.")]
  48. public class GetBaselineHlsVideoStream : VideoStreamRequest
  49. {
  50. }
  51. /// <summary>
  52. /// Class VideoHlsService
  53. /// </summary>
  54. public class VideoHlsService : BaseHlsService
  55. {
  56. public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager)
  57. : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager)
  58. {
  59. }
  60. public object Get(GetMasterHlsVideoStream request)
  61. {
  62. var result = GetAsync(request).Result;
  63. return result;
  64. }
  65. public object Get(GetMainHlsVideoStream request)
  66. {
  67. var result = GetPlaylistAsync(request, "main").Result;
  68. return result;
  69. }
  70. public object Get(GetBaselineHlsVideoStream request)
  71. {
  72. var result = GetPlaylistAsync(request, "baseline").Result;
  73. return result;
  74. }
  75. private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
  76. {
  77. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  78. var builder = new StringBuilder();
  79. builder.AppendLine("#EXTM3U");
  80. builder.AppendLine("#EXT-X-VERSION:3");
  81. builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
  82. builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
  83. var queryStringIndex = Request.RawUrl.IndexOf('?');
  84. var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
  85. var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
  86. var index = 0;
  87. while (seconds > 0)
  88. {
  89. var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds;
  90. builder.AppendLine("#EXTINF:" + length.ToString(UsCulture));
  91. builder.AppendLine(string.Format("hls/{0}/{1}.ts{2}" ,
  92. name,
  93. index.ToString(UsCulture),
  94. queryString));
  95. seconds -= state.SegmentLength;
  96. index++;
  97. }
  98. builder.AppendLine("#EXT-X-ENDLIST");
  99. var playlistText = builder.ToString();
  100. return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  101. }
  102. private async Task<object> GetAsync(GetMasterHlsVideoStream request)
  103. {
  104. var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
  105. if (!state.VideoRequest.VideoBitRate.HasValue && (!state.VideoRequest.VideoCodec.HasValue || state.VideoRequest.VideoCodec.Value != VideoCodecs.Copy))
  106. {
  107. throw new ArgumentException("A video bitrate is required");
  108. }
  109. if (!state.Request.AudioBitRate.HasValue && (!state.Request.AudioCodec.HasValue || state.Request.AudioCodec.Value != AudioCodecs.Copy))
  110. {
  111. throw new ArgumentException("An audio bitrate is required");
  112. }
  113. int audioBitrate;
  114. int videoBitrate;
  115. GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
  116. var appendBaselineStream = false;
  117. var baselineStreamBitrate = 64000;
  118. var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream;
  119. if (hlsVideoRequest != null)
  120. {
  121. appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
  122. baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
  123. }
  124. var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
  125. return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
  126. }
  127. private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
  128. {
  129. var builder = new StringBuilder();
  130. builder.AppendLine("#EXTM3U");
  131. // Pad a little to satisfy the apple hls validator
  132. var paddedBitrate = Convert.ToInt32(bitrate * 1.05);
  133. var queryStringIndex = Request.RawUrl.IndexOf('?');
  134. var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
  135. // Main stream
  136. builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
  137. var playlistUrl = "main.m3u8" + queryString;
  138. builder.AppendLine(playlistUrl);
  139. // Low bitrate stream
  140. if (includeBaselineStream)
  141. {
  142. builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture));
  143. playlistUrl = "baseline.m3u8" + queryString;
  144. builder.AppendLine(playlistUrl);
  145. }
  146. return builder.ToString();
  147. }
  148. /// <summary>
  149. /// Gets the specified request.
  150. /// </summary>
  151. /// <param name="request">The request.</param>
  152. /// <returns>System.Object.</returns>
  153. public object Get(GetHlsVideoStream request)
  154. {
  155. return ProcessRequest(request);
  156. }
  157. /// <summary>
  158. /// Gets the audio arguments.
  159. /// </summary>
  160. /// <param name="state">The state.</param>
  161. /// <returns>System.String.</returns>
  162. protected override string GetAudioArguments(StreamState state)
  163. {
  164. var codec = GetAudioCodec(state.Request);
  165. if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
  166. {
  167. return "-codec:a:0 copy";
  168. }
  169. var args = "-codec:a:0 " + codec;
  170. if (state.AudioStream != null)
  171. {
  172. var channels = GetNumAudioChannelsParam(state.Request, state.AudioStream);
  173. if (channels.HasValue)
  174. {
  175. args += " -ac " + channels.Value;
  176. }
  177. var bitrate = GetAudioBitrateParam(state);
  178. if (bitrate.HasValue)
  179. {
  180. args += " -ab " + bitrate.Value.ToString(UsCulture);
  181. }
  182. args += " " + GetAudioFilterParam(state, true);
  183. return args;
  184. }
  185. return args;
  186. }
  187. /// <summary>
  188. /// Gets the video arguments.
  189. /// </summary>
  190. /// <param name="state">The state.</param>
  191. /// <param name="performSubtitleConversion">if set to <c>true</c> [perform subtitle conversion].</param>
  192. /// <returns>System.String.</returns>
  193. protected override string GetVideoArguments(StreamState state, bool performSubtitleConversion)
  194. {
  195. var codec = GetVideoCodec(state.VideoRequest);
  196. // See if we can save come cpu cycles by avoiding encoding
  197. if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
  198. {
  199. return IsH264(state.VideoStream) ? "-codec:v:0 copy -bsf h264_mp4toannexb" : "-codec:v:0 copy";
  200. }
  201. const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
  202. var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsExternal &&
  203. (state.SubtitleStream.Codec.IndexOf("pgs", StringComparison.OrdinalIgnoreCase) != -1 ||
  204. state.SubtitleStream.Codec.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1);
  205. var args = "-codec:v:0 " + codec + " " + GetVideoQualityParam(state, "libx264") + keyFrameArg;
  206. var bitrate = GetVideoBitrateParam(state);
  207. if (bitrate.HasValue)
  208. {
  209. args += string.Format(" -b:v {0} -maxrate ({0}*.80) -bufsize {0}", bitrate.Value.ToString(UsCulture));
  210. }
  211. // Add resolution params, if specified
  212. if (!hasGraphicalSubs)
  213. {
  214. if (state.VideoRequest.Width.HasValue || state.VideoRequest.Height.HasValue || state.VideoRequest.MaxHeight.HasValue || state.VideoRequest.MaxWidth.HasValue)
  215. {
  216. args += GetOutputSizeParam(state, codec, performSubtitleConversion);
  217. }
  218. }
  219. if (state.VideoRequest.Framerate.HasValue)
  220. {
  221. args += string.Format(" -r {0}", state.VideoRequest.Framerate.Value);
  222. }
  223. args += " -vsync vfr";
  224. if (!string.IsNullOrEmpty(state.VideoRequest.Profile))
  225. {
  226. args += " -profile:v " + state.VideoRequest.Profile;
  227. }
  228. if (!string.IsNullOrEmpty(state.VideoRequest.Level))
  229. {
  230. args += " -level " + state.VideoRequest.Level;
  231. }
  232. // This is for internal graphical subs
  233. if (hasGraphicalSubs)
  234. {
  235. args += GetInternalGraphicalSubtitleParam(state, codec);
  236. }
  237. return args;
  238. }
  239. /// <summary>
  240. /// Gets the segment file extension.
  241. /// </summary>
  242. /// <param name="state">The state.</param>
  243. /// <returns>System.String.</returns>
  244. protected override string GetSegmentFileExtension(StreamState state)
  245. {
  246. return ".ts";
  247. }
  248. }
  249. }