You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

341 lines
8.1 KiB

Fmp4 support (#829) * Media init segment support Resolve EXT-X-MAP URI information in the playlist loader. Add support for requesting and appending initialization segments to the segment loader. * Basic support for fragmented MP4 playback Re-arrange source updater and track support to fit our design goals more closely. Make adjustments so that the correct source buffer types are created when a fragmented mp4 is encountered. This version will play Apple's fMp4 bipbop stream but you have to seek the player to 10 seconds after starting because the first fragment starts at 10, not 0. * Finish consolidating audio loaders Manage a single pair of audio playlist and segment loaders, instead of one per track. Update track logic to work with the new flow. * Detect and set the correct starting timestamp offset Probe the init and first MP4 segment to correctly set timestamp offset so that the stream begins at time zero. After this change, Apple's fragmented MP4 HLS example stream plays without additional modification. * Guard against media playlists without bandwidth information If a media playlist is loaded directly or bandwidth info is unavailable, make sure the lowest bitrate check doesn't error. Add some unnecessary request shifting to tests to avoid extraneous requests caused by the current behavior of segment loader when abort()-ing THEN pause()-ing. * Add stub prog_index.m3u8 for tests Some of the tests point to master playlists that reference prog_index.m3u8. Sinon caught most of the exceptions related to this but the tests weren't really exercising realistic scenarios. Add a stub prog_index to the test fixtures so that requests for prog_index don't unintentionally error. * Abort init segment XHR alongside other segment XHRs If the segment loader XHRs are aborted, stop the init segment one as well. Make sure to check the right property for the init segment XHR before continuing the loading process. Make sure falsey values do not cause a playlist to be blacklisted in FF for audio info changes. * Fix audio track management after reorganization Delay segment loader initialization steps until all starting configuration is ready. This allowed source updater MIME types to be specified early without triggering the main updater to have its audio disabled on startup. Tweak the mime type identifier function to signal alternate audio earlier. Move `this` references in segment loader's checkBuffer_ out to stateful functions to align with the original design goals. Removed a segment loader test that seemed duplicative after the checkBuffer_ change. * Fix D3 on stats page Update URL for D3. Remove audio switcher since it's included by default now. * Only override codec defaults if an actual value was parsed When converting codec strings into MIME type configurations for source buffers, make sure to use default values if the codec string didn't supply particular fields. Export the codec to MIME helper function so it can be unit-tested. * IE fixes Array.prototype.find() isn't available in IE so use .filter()[0] instead. * Blacklist unsupported codecs If MediaSource.isTypeSupported fails in a generic MP4 container, swapping to a variant with those codecs is unlikely to be successful. For instance, the fragmented bip-bop stream includes AC-3 and EC-3 audio which is not supported on Chrome or Firefox today. Exclude variants with codecs that don't pass the isTypeSupported test.
9 years ago
  1. import document from 'global/document';
  2. import sinon from 'sinon';
  3. import videojs from 'video.js';
  4. import QUnit from 'qunit';
  5. /* eslint-disable no-unused-vars */
  6. // needed so MediaSource can be registered with videojs
  7. import MediaSource from 'videojs-contrib-media-sources';
  8. /* eslint-enable */
  9. import testDataManifests from './test-manifests.js';
  10. import xhrFactory from '../src/xhr';
  11. import window from 'global/window';
  12. // a SourceBuffer that tracks updates but otherwise is a noop
  13. class MockSourceBuffer extends videojs.EventTarget {
  14. constructor() {
  15. super();
  16. this.updates_ = [];
  17. this.updating = false;
  18. this.on('updateend', function() {
  19. this.updating = false;
  20. });
  21. this.buffered = videojs.createTimeRanges();
  22. this.duration_ = NaN;
  23. Object.defineProperty(this, 'duration', {
  24. get() {
  25. return this.duration_;
  26. },
  27. set(duration) {
  28. this.updates_.push({
  29. duration
  30. });
  31. this.duration_ = duration;
  32. }
  33. });
  34. }
  35. abort() {
  36. this.updates_.push({
  37. abort: true
  38. });
  39. }
  40. appendBuffer(bytes) {
  41. this.updates_.push({
  42. append: bytes
  43. });
  44. this.updating = true;
  45. }
  46. remove(start, end) {
  47. this.updates_.push({
  48. remove: [start, end]
  49. });
  50. }
  51. }
  52. class MockMediaSource extends videojs.EventTarget {
  53. constructor() {
  54. super();
  55. this.readyState = 'closed';
  56. this.on('sourceopen', function() {
  57. this.readyState = 'open';
  58. });
  59. this.sourceBuffers = [];
  60. this.duration = NaN;
  61. this.seekable = videojs.createTimeRange();
  62. }
  63. addSeekableRange_(start, end) {
  64. this.seekable = videojs.createTimeRange(start, end);
  65. }
  66. addSourceBuffer(mime) {
  67. let sourceBuffer = new MockSourceBuffer();
  68. sourceBuffer.mimeType_ = mime;
  69. this.sourceBuffers.push(sourceBuffer);
  70. return sourceBuffer;
  71. }
  72. endOfStream(error) {
  73. this.readyState = 'closed';
  74. this.error_ = error;
  75. }
  76. }
  77. export const useFakeMediaSource = function() {
  78. let RealMediaSource = videojs.MediaSource;
  79. let realCreateObjectURL = window.URL.createObjectURL;
  80. let id = 0;
  81. videojs.MediaSource = MockMediaSource;
  82. videojs.MediaSource.supportsNativeMediaSources =
  83. RealMediaSource.supportsNativeMediaSources;
  84. videojs.URL.createObjectURL = function() {
  85. id++;
  86. return 'blob:videojs-contrib-hls-mock-url' + id;
  87. };
  88. return {
  89. restore() {
  90. videojs.MediaSource = RealMediaSource;
  91. videojs.URL.createObjectURL = realCreateObjectURL;
  92. }
  93. };
  94. };
  95. let fakeEnvironment = {
  96. requests: [],
  97. restore() {
  98. this.clock.restore();
  99. videojs.xhr.XMLHttpRequest = window.XMLHttpRequest;
  100. this.xhr.restore();
  101. ['warn', 'error'].forEach((level) => {
  102. if (this.log && this.log[level] && this.log[level].restore) {
  103. if (QUnit) {
  104. let calls = this.log[level].args.map((args) => {
  105. return args.join(', ');
  106. }).join('\n ');
  107. QUnit.equal(this.log[level].callCount,
  108. 0,
  109. 'no unexpected logs at level "' + level + '":\n ' + calls);
  110. }
  111. this.log[level].restore();
  112. }
  113. });
  114. }
  115. };
  116. export const useFakeEnvironment = function() {
  117. fakeEnvironment.log = {};
  118. ['warn', 'error'].forEach((level) => {
  119. // you can use .log[level].args to get args
  120. sinon.stub(videojs.log, level);
  121. fakeEnvironment.log[level] = videojs.log[level];
  122. Object.defineProperty(videojs.log[level], 'calls', {
  123. get() {
  124. // reset callCount to 0 so they don't have to
  125. let callCount = this.callCount;
  126. this.callCount = 0;
  127. return callCount;
  128. }
  129. });
  130. });
  131. fakeEnvironment.clock = sinon.useFakeTimers();
  132. fakeEnvironment.xhr = sinon.useFakeXMLHttpRequest();
  133. fakeEnvironment.requests.length = 0;
  134. fakeEnvironment.xhr.onCreate = function(xhr) {
  135. fakeEnvironment.requests.push(xhr);
  136. };
  137. videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
  138. return fakeEnvironment;
  139. };
  140. // patch over some methods of the provided tech so it can be tested
  141. // synchronously with sinon's fake timers
  142. export const mockTech = function(tech) {
  143. if (tech.isMocked_) {
  144. // make this function idempotent because HTML and Flash based
  145. // playback have very different lifecycles. For HTML, the tech
  146. // is available on player creation. For Flash, the tech isn't
  147. // ready until the source has been loaded and one tick has
  148. // expired.
  149. return;
  150. }
  151. tech.isMocked_ = true;
  152. tech.src_ = null;
  153. tech.time_ = null;
  154. tech.paused_ = !tech.autoplay();
  155. tech.paused = function() {
  156. return tech.paused_;
  157. };
  158. if (!tech.currentTime_) {
  159. tech.currentTime_ = tech.currentTime;
  160. }
  161. tech.currentTime = function() {
  162. return tech.time_ === null ? tech.currentTime_() : tech.time_;
  163. };
  164. tech.setSrc = function(src) {
  165. tech.src_ = src;
  166. };
  167. tech.src = function(src) {
  168. if (src !== null) {
  169. return tech.setSrc(src);
  170. }
  171. return tech.src_ === null ? tech.src : tech.src_;
  172. };
  173. tech.currentSrc_ = tech.currentSrc;
  174. tech.currentSrc = function() {
  175. return tech.src_ === null ? tech.currentSrc_() : tech.src_;
  176. };
  177. tech.play_ = tech.play;
  178. tech.play = function() {
  179. tech.play_();
  180. tech.paused_ = false;
  181. tech.trigger('play');
  182. };
  183. tech.pause_ = tech.pause;
  184. tech.pause = function() {
  185. tech.pause_();
  186. tech.paused_ = true;
  187. tech.trigger('pause');
  188. };
  189. tech.setCurrentTime = function(time) {
  190. tech.time_ = time;
  191. setTimeout(function() {
  192. tech.trigger('seeking');
  193. setTimeout(function() {
  194. tech.trigger('seeked');
  195. }, 1);
  196. }, 1);
  197. };
  198. };
  199. export const createPlayer = function(options) {
  200. let video;
  201. let player;
  202. video = document.createElement('video');
  203. video.className = 'video-js';
  204. document.querySelector('#qunit-fixture').appendChild(video);
  205. player = videojs(video, options || {
  206. flash: {
  207. swf: ''
  208. }
  209. });
  210. player.buffered = function() {
  211. return videojs.createTimeRange(0, 0);
  212. };
  213. mockTech(player.tech_);
  214. return player;
  215. };
  216. export const openMediaSource = function(player, clock) {
  217. // ensure the Flash tech is ready
  218. player.tech_.triggerReady();
  219. clock.tick(1);
  220. // mock the tech *after* it has finished loading so that we don't
  221. // mock a tech that will be unloaded on the next tick
  222. mockTech(player.tech_);
  223. player.tech_.hls.xhr = xhrFactory();
  224. // simulate the sourceopen event
  225. player.tech_.hls.mediaSource.readyState = 'open';
  226. player.tech_.hls.mediaSource.dispatchEvent({
  227. type: 'sourceopen',
  228. swfId: player.tech_.el().id
  229. });
  230. };
  231. export const standardXHRResponse = function(request, data) {
  232. if (!request.url) {
  233. return;
  234. }
  235. let contentType = 'application/json';
  236. // contents off the global object
  237. let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
  238. if (manifestName) {
  239. manifestName = manifestName[1];
  240. } else {
  241. manifestName = request.url;
  242. }
  243. if (/\.m3u8?/.test(request.url)) {
  244. contentType = 'application/vnd.apple.mpegurl';
  245. } else if (/\.ts/.test(request.url)) {
  246. contentType = 'video/MP2T';
  247. }
  248. if (!data) {
  249. data = testDataManifests[manifestName];
  250. }
  251. request.response = new Uint8Array(1024).buffer;
  252. request.respond(200, {'Content-Type': contentType}, data);
  253. };
  254. // return an absolute version of a page-relative URL
  255. export const absoluteUrl = function(relativeUrl) {
  256. return window.location.protocol + '//' +
  257. window.location.host +
  258. (window.location.pathname
  259. .split('/')
  260. .slice(0, -1)
  261. .concat(relativeUrl)
  262. .join('/')
  263. );
  264. };
  265. export const playlistWithDuration = function(time, conf) {
  266. let result = {
  267. targetDuration: 10,
  268. mediaSequence: conf && conf.mediaSequence ? conf.mediaSequence : 0,
  269. discontinuityStarts: [],
  270. segments: [],
  271. endList: true
  272. };
  273. let count = Math.floor(time / 10);
  274. let remainder = time % 10;
  275. let i;
  276. let isEncrypted = conf && conf.isEncrypted;
  277. for (i = 0; i < count; i++) {
  278. result.segments.push({
  279. uri: i + '.ts',
  280. resolvedUri: i + '.ts',
  281. duration: 10
  282. });
  283. if (isEncrypted) {
  284. result.segments[i].key = {
  285. uri: i + '-key.php',
  286. resolvedUri: i + '-key.php'
  287. };
  288. }
  289. }
  290. if (remainder) {
  291. result.segments.push({
  292. uri: i + '.ts',
  293. duration: remainder
  294. });
  295. }
  296. return result;
  297. };