ManifestBuilder.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. using System;
  2. using System.Globalization;
  3. using System.Security;
  4. using System.Text;
  5. namespace MediaBrowser.Api.Playback.Dash
  6. {
  7. public class ManifestBuilder
  8. {
  9. protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
  10. public string GetManifestText(StreamState state, string playlistUrl)
  11. {
  12. var builder = new StringBuilder();
  13. var time = TimeSpan.FromTicks(state.RunTimeTicks.Value);
  14. var duration = "PT" + time.Hours.ToString("00", UsCulture) + "H" + time.Minutes.ToString("00", UsCulture) + "M" + time.Seconds.ToString("00", UsCulture) + ".00S";
  15. builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
  16. builder.AppendFormat(
  17. "<MPD xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"urn:mpeg:dash:schema:mpd:2011\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" type=\"static\" mediaPresentationDuration=\"{0}\" minBufferTime=\"PT5.0S\">",
  18. duration);
  19. builder.Append("<ProgramInformation>");
  20. builder.Append("</ProgramInformation>");
  21. builder.Append("<Period start=\"PT0S\">");
  22. builder.Append(GetVideoAdaptationSet(state, playlistUrl));
  23. builder.Append(GetAudioAdaptationSet(state, playlistUrl));
  24. builder.Append("</Period>");
  25. builder.Append("</MPD>");
  26. return builder.ToString();
  27. }
  28. private string GetVideoAdaptationSet(StreamState state, string playlistUrl)
  29. {
  30. var builder = new StringBuilder();
  31. builder.Append("<AdaptationSet id=\"video\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">");
  32. builder.Append(GetVideoRepresentationOpenElement(state));
  33. AppendSegmentList(state, builder, "0", playlistUrl);
  34. builder.Append("</Representation>");
  35. builder.Append("</AdaptationSet>");
  36. return builder.ToString();
  37. }
  38. private string GetAudioAdaptationSet(StreamState state, string playlistUrl)
  39. {
  40. var builder = new StringBuilder();
  41. builder.Append("<AdaptationSet id=\"audio\" segmentAlignment=\"true\" bitstreamSwitching=\"true\">");
  42. builder.Append(GetAudioRepresentationOpenElement(state));
  43. builder.Append("<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"6\" />");
  44. AppendSegmentList(state, builder, "1", playlistUrl);
  45. builder.Append("</Representation>");
  46. builder.Append("</AdaptationSet>");
  47. return builder.ToString();
  48. }
  49. private string GetVideoRepresentationOpenElement(StreamState state)
  50. {
  51. var codecs = GetVideoCodecDescriptor(state);
  52. var mime = "video/mp4";
  53. var xml = "<Representation id=\"0\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\"";
  54. if (state.OutputWidth.HasValue)
  55. {
  56. xml += " width=\"" + state.OutputWidth.Value.ToString(UsCulture) + "\"";
  57. }
  58. if (state.OutputHeight.HasValue)
  59. {
  60. xml += " height=\"" + state.OutputHeight.Value.ToString(UsCulture) + "\"";
  61. }
  62. if (state.OutputVideoBitrate.HasValue)
  63. {
  64. xml += " bandwidth=\"" + state.OutputVideoBitrate.Value.ToString(UsCulture) + "\"";
  65. }
  66. xml += ">";
  67. return xml;
  68. }
  69. private string GetAudioRepresentationOpenElement(StreamState state)
  70. {
  71. var codecs = GetAudioCodecDescriptor(state);
  72. var mime = "audio/mp4";
  73. var xml = "<Representation id=\"1\" mimeType=\"" + mime + "\" codecs=\"" + codecs + "\"";
  74. if (state.OutputAudioSampleRate.HasValue)
  75. {
  76. xml += " audioSamplingRate=\"" + state.OutputAudioSampleRate.Value.ToString(UsCulture) + "\"";
  77. }
  78. if (state.OutputAudioBitrate.HasValue)
  79. {
  80. xml += " bandwidth=\"" + state.OutputAudioBitrate.Value.ToString(UsCulture) + "\"";
  81. }
  82. xml += ">";
  83. return xml;
  84. }
  85. private string GetVideoCodecDescriptor(StreamState state)
  86. {
  87. // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
  88. // http://www.chipwreck.de/blog/2010/02/25/html-5-video-tag-and-attributes/
  89. var level = state.TargetVideoLevel ?? 0;
  90. var profile = state.TargetVideoProfile ?? string.Empty;
  91. if (profile.IndexOf("high", StringComparison.OrdinalIgnoreCase) != -1)
  92. {
  93. if (level >= 4.1)
  94. {
  95. return "avc1.640028";
  96. }
  97. if (level >= 4)
  98. {
  99. return "avc1.640028";
  100. }
  101. return "avc1.64001f";
  102. }
  103. if (profile.IndexOf("main", StringComparison.OrdinalIgnoreCase) != -1)
  104. {
  105. if (level >= 4)
  106. {
  107. return "avc1.4d0028";
  108. }
  109. if (level >= 3.1)
  110. {
  111. return "avc1.4d001f";
  112. }
  113. return "avc1.4d001e";
  114. }
  115. if (level >= 3.1)
  116. {
  117. return "avc1.42001f";
  118. }
  119. return "avc1.42E01E";
  120. }
  121. private string GetAudioCodecDescriptor(StreamState state)
  122. {
  123. // https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
  124. if (string.Equals(state.OutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
  125. {
  126. return "mp4a.40.34";
  127. }
  128. // AAC 5ch
  129. if (state.OutputAudioChannels.HasValue && state.OutputAudioChannels.Value >= 5)
  130. {
  131. return "mp4a.40.5";
  132. }
  133. // AAC 2ch
  134. return "mp4a.40.2";
  135. }
  136. private void AppendSegmentList(StreamState state, StringBuilder builder, string type, string playlistUrl)
  137. {
  138. var extension = ".m4s";
  139. var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
  140. var queryStringIndex = playlistUrl.IndexOf('?');
  141. var queryString = queryStringIndex == -1 ? string.Empty : playlistUrl.Substring(queryStringIndex);
  142. var index = 0;
  143. var duration = 1000000 * state.SegmentLength;
  144. builder.AppendFormat("<SegmentList timescale=\"1000000\" duration=\"{0}\" startNumber=\"1\">", duration.ToString(CultureInfo.InvariantCulture));
  145. while (seconds > 0)
  146. {
  147. var filename = index == 0
  148. ? "init"
  149. : (index - 1).ToString(UsCulture);
  150. var segmentUrl = string.Format("dash/{3}/{0}{1}{2}",
  151. filename,
  152. extension,
  153. SecurityElement.Escape(queryString),
  154. type);
  155. if (index == 0)
  156. {
  157. builder.AppendFormat("<Initialization sourceURL=\"{0}\"/>", segmentUrl);
  158. }
  159. else
  160. {
  161. builder.AppendFormat("<SegmentURL media=\"{0}\"/>", segmentUrl);
  162. }
  163. seconds -= state.SegmentLength;
  164. index++;
  165. }
  166. builder.Append("</SegmentList>");
  167. }
  168. }
  169. }