flash.test.js 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. import document from 'global/document';
  2. import window from 'global/window';
  3. import QUnit from 'qunit';
  4. import sinon from 'sinon';
  5. import videojs from 'video.js';
  6. import muxjs from 'mux.js';
  7. import FlashSourceBuffer from '../src/flash-source-buffer';
  8. import FlashConstants from '../src/flash-constants';
  9. // we disable this because browserify needs to include these files
  10. // but the exports are not important
  11. /* eslint-disable no-unused-vars */
  12. import {MediaSource, URL} from '../src/videojs-contrib-media-sources.js';
  13. /* eslint-disable no-unused-vars */
  14. // return the sequence of calls to append to the SWF
  15. const appendCalls = function(calls) {
  16. return calls.filter(function(call) {
  17. return call.callee && call.callee === 'vjs_appendChunkReady';
  18. });
  19. };
  20. const getFlvHeader = function() {
  21. return new Uint8Array([1, 2, 3]);
  22. };
  23. const makeFlvTag = function(pts, data) {
  24. return {
  25. pts,
  26. dts: pts,
  27. bytes: data
  28. };
  29. };
  30. let timers;
  31. let oldSTO;
  32. const fakeSTO = function() {
  33. oldSTO = window.setTimeout;
  34. timers = [];
  35. timers.run = function(num) {
  36. let timer;
  37. while (num--) {
  38. timer = this.pop();
  39. if (timer) {
  40. timer();
  41. }
  42. }
  43. };
  44. timers.runAll = function() {
  45. while (this.length) {
  46. this.pop()();
  47. }
  48. };
  49. window.setTimeout = function(callback) {
  50. timers.push(callback);
  51. };
  52. window.setTimeout.fake = true;
  53. };
  54. const unfakeSTO = function() {
  55. timers = [];
  56. window.setTimeout = oldSTO;
  57. };
  58. // Create a WebWorker-style message that signals the transmuxer is done
  59. const createDataMessage = function(data, audioData, metadata, captions) {
  60. let captionStreams = {};
  61. if (captions) {
  62. captions.forEach((caption) => {
  63. captionStreams[caption.stream] = true;
  64. });
  65. }
  66. return {
  67. data: {
  68. action: 'data',
  69. segment: {
  70. tags: {
  71. videoTags: data.map((tag) => {
  72. return makeFlvTag(tag.pts, tag.bytes);
  73. }),
  74. audioTags: audioData ? audioData.map((tag) => {
  75. return makeFlvTag(tag.pts, tag.bytes);
  76. }) : []
  77. },
  78. metadata,
  79. captions,
  80. captionStreams
  81. }
  82. }
  83. };
  84. };
  85. const doneMessage = {
  86. data: {
  87. action: 'done'
  88. }
  89. };
  90. const postMessage_ = function(msg) {
  91. if (msg.action === 'push') {
  92. window.setTimeout(()=> {
  93. this.onmessage(createDataMessage([{
  94. bytes: new Uint8Array(msg.data, msg.byteOffset, msg.byteLength),
  95. pts: 0
  96. }]));
  97. }, 1);
  98. } else if (msg.action === 'flush') {
  99. window.setTimeout(() => {
  100. this.onmessage(doneMessage);
  101. }, 1);
  102. }
  103. };
  104. QUnit.module('Flash MediaSource', {
  105. beforeEach(assert) {
  106. let swfObj;
  107. // Mock the environment's timers because certain things - particularly
  108. // player readiness - are asynchronous in video.js 5.
  109. this.clock = sinon.useFakeTimers();
  110. this.fixture = document.getElementById('qunit-fixture');
  111. this.video = document.createElement('video');
  112. this.fixture.appendChild(this.video);
  113. this.player = videojs(this.video);
  114. this.oldMediaSource = window.MediaSource || window.WebKitMediaSource;
  115. window.MediaSource = null;
  116. window.WebKitMediaSource = null;
  117. this.Flash = videojs.getTech('Flash');
  118. this.oldFlashSupport = this.Flash.isSupported;
  119. this.oldCanPlay = this.Flash.canPlaySource;
  120. this.Flash.canPlaySource = this.Flash.isSupported = function() {
  121. return true;
  122. };
  123. this.oldFlashTransmuxerPostMessage = muxjs.flv.Transmuxer.postMessage;
  124. this.oldGetFlvHeader = muxjs.flv.getFlvHeader;
  125. muxjs.flv.getFlvHeader = getFlvHeader;
  126. this.swfCalls = [];
  127. this.mediaSource = new videojs.MediaSource();
  128. this.player.src({
  129. src: videojs.URL.createObjectURL(this.mediaSource),
  130. type: 'video/mp2t'
  131. });
  132. // vjs6 takes 1 tick to set source async
  133. this.clock.tick(1);
  134. swfObj = document.createElement('fake-object');
  135. swfObj.id = 'fake-swf-' + assert.test.testId;
  136. this.player.el().replaceChild(swfObj, this.player.tech_.el());
  137. this.player.tech_.hls = new videojs.EventTarget();
  138. this.player.tech_.el_ = swfObj;
  139. swfObj.tech = this.player.tech_;
  140. /* eslint-disable camelcase */
  141. swfObj.vjs_abort = () => {
  142. this.swfCalls.push('abort');
  143. };
  144. swfObj.vjs_getProperty = (attr) => {
  145. if (attr === 'buffered') {
  146. return [];
  147. } else if (attr === 'currentTime') {
  148. return 0;
  149. // ignored for vjs6
  150. } else if (attr === 'videoWidth') {
  151. return 0;
  152. }
  153. this.swfCalls.push({ attr });
  154. };
  155. swfObj.vjs_load = () => {
  156. this.swfCalls.push('load');
  157. };
  158. swfObj.vjs_setProperty = (attr, value) => {
  159. this.swfCalls.push({ attr, value });
  160. };
  161. swfObj.vjs_discontinuity = (attr, value) => {
  162. this.swfCalls.push({ attr, value });
  163. };
  164. swfObj.vjs_appendChunkReady = (method) => {
  165. window.setTimeout(() => {
  166. let chunk = window[method]();
  167. // only care about the segment data, not the flv header
  168. if (method.substr(0, 21) === 'vjs_flashEncodedData_') {
  169. let call = {
  170. callee: 'vjs_appendChunkReady',
  171. arguments: [window.atob(chunk).split('').map((c) => c.charCodeAt(0))]
  172. };
  173. this.swfCalls.push(call);
  174. }
  175. }, 1);
  176. };
  177. swfObj.vjs_adjustCurrentTime = (value) => {
  178. this.swfCalls.push({ call: 'adjustCurrentTime', value });
  179. };
  180. /* eslint-enable camelcase */
  181. this.mediaSource.trigger({
  182. type: 'sourceopen',
  183. swfId: swfObj.id
  184. });
  185. fakeSTO();
  186. },
  187. afterEach() {
  188. window.MediaSource = this.oldMediaSource;
  189. window.WebKitMediaSource = window.MediaSource;
  190. this.Flash.isSupported = this.oldFlashSupport;
  191. this.Flash.canPlaySource = this.oldCanPlay;
  192. muxjs.flv.Transmuxer.postMessage = this.oldFlashTransmuxerPostMessage;
  193. muxjs.flv.getFlvHeader = this.oldGetFlvHeader;
  194. this.player.dispose();
  195. this.clock.restore();
  196. this.swfCalls = [];
  197. unfakeSTO();
  198. }
  199. });
  200. QUnit.test('raises an exception for unrecognized MIME types', function() {
  201. try {
  202. this.mediaSource.addSourceBuffer('video/garbage');
  203. } catch (e) {
  204. QUnit.ok(e, 'an error was thrown');
  205. return;
  206. }
  207. QUnit.ok(false, 'no error was thrown');
  208. });
  209. QUnit.test('creates FlashSourceBuffers for video/mp2t', function() {
  210. QUnit.ok(this.mediaSource.addSourceBuffer('video/mp2t') instanceof FlashSourceBuffer,
  211. 'create source buffer');
  212. });
  213. QUnit.test('creates FlashSourceBuffers for audio/mp2t', function() {
  214. QUnit.ok(this.mediaSource.addSourceBuffer('audio/mp2t') instanceof FlashSourceBuffer,
  215. 'create source buffer');
  216. });
  217. QUnit.test('waits for the next tick to append', function() {
  218. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  219. sourceBuffer.transmuxer_.postMessage = postMessage_;
  220. QUnit.equal(this.swfCalls.length, 1, 'made one call on init');
  221. QUnit.equal(this.swfCalls[0], 'load', 'called load');
  222. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  223. this.swfCalls = appendCalls(this.swfCalls);
  224. QUnit.strictEqual(this.swfCalls.length, 0, 'no appends were made');
  225. });
  226. QUnit.test('passes bytes to Flash', function() {
  227. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  228. sourceBuffer.transmuxer_.postMessage = postMessage_;
  229. this.swfCalls.length = 0;
  230. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  231. timers.runAll();
  232. timers.runAll();
  233. QUnit.ok(this.swfCalls.length, 'the SWF was called');
  234. this.swfCalls = appendCalls(this.swfCalls);
  235. QUnit.strictEqual(this.swfCalls[0].callee, 'vjs_appendChunkReady', 'called vjs_appendChunkReady');
  236. QUnit.deepEqual(this.swfCalls[0].arguments[0],
  237. [0, 1],
  238. 'passed the base64 encoded data');
  239. });
  240. QUnit.test('passes chunked bytes to Flash', function() {
  241. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  242. let oldChunkSize = FlashConstants.BYTES_PER_CHUNK;
  243. sourceBuffer.transmuxer_.postMessage = postMessage_;
  244. FlashConstants.BYTES_PER_CHUNK = 2;
  245. this.swfCalls.length = 0;
  246. sourceBuffer.appendBuffer(new Uint8Array([0, 1, 2, 3, 4]));
  247. timers.runAll();
  248. QUnit.ok(this.swfCalls.length, 'the SWF was called');
  249. this.swfCalls = appendCalls(this.swfCalls);
  250. QUnit.equal(this.swfCalls.length, 3, 'the SWF received 3 chunks');
  251. QUnit.strictEqual(this.swfCalls[0].callee, 'vjs_appendChunkReady', 'called vjs_appendChunkReady');
  252. QUnit.deepEqual(this.swfCalls[0].arguments[0],
  253. [0, 1],
  254. 'passed the base64 encoded data');
  255. QUnit.deepEqual(this.swfCalls[1].arguments[0],
  256. [2, 3],
  257. 'passed the base64 encoded data');
  258. QUnit.deepEqual(this.swfCalls[2].arguments[0],
  259. [4],
  260. 'passed the base64 encoded data');
  261. FlashConstants.BYTES_PER_CHUNK = oldChunkSize;
  262. });
  263. QUnit.test('clears the SWF on seeking', function() {
  264. let aborts = 0;
  265. this.mediaSource.addSourceBuffer('video/mp2t');
  266. // track calls to abort()
  267. /* eslint-disable camelcase */
  268. this.mediaSource.swfObj.vjs_abort = function() {
  269. aborts++;
  270. };
  271. /* eslint-enable camelcase */
  272. this.mediaSource.tech_.trigger('seeking');
  273. QUnit.strictEqual(1, aborts, 'aborted pending buffer');
  274. });
  275. QUnit.test('drops tags before currentTime when seeking', function() {
  276. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  277. let i = 10;
  278. let currentTime;
  279. let tags_ = [];
  280. sourceBuffer.transmuxer_.postMessage = postMessage_;
  281. this.mediaSource.tech_.currentTime = function() {
  282. return currentTime;
  283. };
  284. // push a tag into the buffer to establish the starting PTS value
  285. currentTime = 0;
  286. sourceBuffer.transmuxer_.onmessage(createDataMessage([{
  287. pts: 19 * 1000,
  288. bytes: new Uint8Array(1)
  289. }]));
  290. timers.runAll();
  291. sourceBuffer.appendBuffer(new Uint8Array(10));
  292. timers.runAll();
  293. // mock out a new segment of FLV tags, starting 10s after the
  294. // starting PTS value
  295. while (i--) {
  296. tags_.unshift(
  297. {
  298. pts: (i * 1000) + (29 * 1000),
  299. bytes: new Uint8Array([i])
  300. }
  301. );
  302. }
  303. let dataMessage = createDataMessage(tags_);
  304. // mock gop start at seek point
  305. dataMessage.data.segment.tags.videoTags[7].keyFrame = true;
  306. sourceBuffer.transmuxer_.onmessage(dataMessage);
  307. // seek to 7 seconds into the new swegment
  308. this.mediaSource.tech_.seeking = function() {
  309. return true;
  310. };
  311. currentTime = 10 + 7;
  312. this.mediaSource.tech_.trigger('seeking');
  313. sourceBuffer.appendBuffer(new Uint8Array(10));
  314. this.swfCalls.length = 0;
  315. timers.runAll();
  316. QUnit.deepEqual(this.swfCalls[0].arguments[0], [7, 8, 9],
  317. 'three tags are appended');
  318. });
  319. QUnit.test('drops audio and video (complete gops) tags before the buffered end always', function() {
  320. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  321. let endTime;
  322. let videoTags_ = [];
  323. let audioTags_ = [];
  324. sourceBuffer.transmuxer_.postMessage = postMessage_;
  325. this.mediaSource.tech_.buffered = function() {
  326. return videojs.createTimeRange([[0, endTime]]);
  327. };
  328. // push a tag into the buffer to establish the starting PTS value
  329. endTime = 0;
  330. // mock buffering 17 seconds of data so flash source buffer internal end of buffer
  331. // tracking is accurate
  332. let i = 17;
  333. while (i--) {
  334. videoTags_.unshift({
  335. pts: (i * 1000) + (19 * 1000),
  336. bytes: new Uint8Array(1)
  337. });
  338. }
  339. i = 17;
  340. while (i--) {
  341. audioTags_.unshift({
  342. pts: (i * 1000) + (19 * 1000),
  343. bytes: new Uint8Array(1)
  344. });
  345. }
  346. let dataMessage = createDataMessage(videoTags_, audioTags_);
  347. sourceBuffer.transmuxer_.onmessage(dataMessage);
  348. timers.runAll();
  349. sourceBuffer.appendBuffer(new Uint8Array(10));
  350. timers.runAll();
  351. i = 10;
  352. videoTags_ = [];
  353. audioTags_ = [];
  354. // mock out a new segment of FLV tags, starting 10s after the
  355. // starting PTS value
  356. while (i--) {
  357. videoTags_.unshift({
  358. pts: (i * 1000) + (29 * 1000),
  359. bytes: new Uint8Array([i])
  360. });
  361. }
  362. i = 10;
  363. while (i--) {
  364. audioTags_.unshift({
  365. pts: (i * 1000) + (29 * 1000),
  366. bytes: new Uint8Array([i + 100])
  367. });
  368. }
  369. dataMessage = createDataMessage(videoTags_, audioTags_);
  370. dataMessage.data.segment.tags.videoTags[0].keyFrame = true;
  371. dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  372. dataMessage.data.segment.tags.videoTags[6].keyFrame = true;
  373. dataMessage.data.segment.tags.videoTags[8].keyFrame = true;
  374. sourceBuffer.transmuxer_.onmessage(dataMessage);
  375. endTime = 10 + 7;
  376. sourceBuffer.appendBuffer(new Uint8Array(10));
  377. this.swfCalls.length = 0;
  378. timers.runAll();
  379. // end of buffer is 17 seconds
  380. // frames 0-6 for video have pts values less than 17 seconds
  381. // since frame 6 is a key frame, it should still be appended to preserve the entire gop
  382. // so we should have appeneded frames 6 - 9
  383. // frames 100-106 for audio have pts values less than 17 seconds
  384. // but since we appended an extra video frame, we should also append audio frames
  385. // to fill in the gap in audio. This means we should be appending audio frames
  386. // 106, 107, 108, 109
  387. // Append order is 6, 7, 107, 8, 108, 9, 109 since we order tags based on dts value
  388. QUnit.deepEqual(this.swfCalls[0].arguments[0], [6, 106, 7, 107, 8, 108, 9, 109],
  389. 'audio and video tags properly dropped');
  390. });
  391. QUnit.test('seeking into the middle of a GOP adjusts currentTime to the start of the GOP', function() {
  392. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  393. let i = 10;
  394. let currentTime;
  395. let tags_ = [];
  396. sourceBuffer.transmuxer_.postMessage = postMessage_;
  397. this.mediaSource.tech_.currentTime = function() {
  398. return currentTime;
  399. };
  400. // push a tag into the buffer to establish the starting PTS value
  401. currentTime = 0;
  402. let dataMessage = createDataMessage([{
  403. pts: 19 * 1000,
  404. bytes: new Uint8Array(1)
  405. }]);
  406. sourceBuffer.transmuxer_.onmessage(dataMessage);
  407. timers.runAll();
  408. sourceBuffer.appendBuffer(new Uint8Array(10));
  409. timers.runAll();
  410. // mock out a new segment of FLV tags, starting 10s after the
  411. // starting PTS value
  412. while (i--) {
  413. tags_.unshift(
  414. {
  415. pts: (i * 1000) + (29 * 1000),
  416. bytes: new Uint8Array([i])
  417. }
  418. );
  419. }
  420. dataMessage = createDataMessage(tags_);
  421. // mock the GOP structure
  422. dataMessage.data.segment.tags.videoTags[0].keyFrame = true;
  423. dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  424. dataMessage.data.segment.tags.videoTags[5].keyFrame = true;
  425. dataMessage.data.segment.tags.videoTags[8].keyFrame = true;
  426. sourceBuffer.transmuxer_.onmessage(dataMessage);
  427. // seek to 7 seconds into the new swegment
  428. this.mediaSource.tech_.seeking = function() {
  429. return true;
  430. };
  431. currentTime = 10 + 7;
  432. this.mediaSource.tech_.trigger('seeking');
  433. sourceBuffer.appendBuffer(new Uint8Array(10));
  434. this.swfCalls.length = 0;
  435. timers.runAll();
  436. QUnit.deepEqual(this.swfCalls[0], { call: 'adjustCurrentTime', value: 15 });
  437. QUnit.deepEqual(this.swfCalls[1].arguments[0], [5, 6, 7, 8, 9],
  438. '5 tags are appended');
  439. });
  440. QUnit.test('GOP trimming accounts for metadata tags prepended to key frames by mux.js', function() {
  441. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  442. let i = 10;
  443. let currentTime;
  444. let tags_ = [];
  445. sourceBuffer.transmuxer_.postMessage = postMessage_;
  446. this.mediaSource.tech_.currentTime = function() {
  447. return currentTime;
  448. };
  449. // push a tag into the buffer to establish the starting PTS value
  450. currentTime = 0;
  451. let dataMessage = createDataMessage([{
  452. pts: 19 * 1000,
  453. bytes: new Uint8Array(1)
  454. }]);
  455. sourceBuffer.transmuxer_.onmessage(dataMessage);
  456. timers.runAll();
  457. sourceBuffer.appendBuffer(new Uint8Array(10));
  458. timers.runAll();
  459. // mock out a new segment of FLV tags, starting 10s after the
  460. // starting PTS value
  461. while (i--) {
  462. tags_.unshift(
  463. {
  464. pts: (i * 1000) + (29 * 1000),
  465. bytes: new Uint8Array([i])
  466. }
  467. );
  468. }
  469. // add in the metadata tags
  470. tags_.splice(8, 0, {
  471. pts: tags_[8].pts,
  472. bytes: new Uint8Array([8])
  473. }, {
  474. pts: tags_[8].pts,
  475. bytes: new Uint8Array([8])
  476. });
  477. tags_.splice(5, 0, {
  478. pts: tags_[5].pts,
  479. bytes: new Uint8Array([5])
  480. }, {
  481. pts: tags_[5].pts,
  482. bytes: new Uint8Array([5])
  483. });
  484. tags_.splice(0, 0, {
  485. pts: tags_[0].pts,
  486. bytes: new Uint8Array([0])
  487. }, {
  488. pts: tags_[0].pts,
  489. bytes: new Uint8Array([0])
  490. });
  491. dataMessage = createDataMessage(tags_);
  492. // mock the GOP structure + metadata tags
  493. // if we see a metadata tag, that means the next tag will also be a metadata tag with
  494. // keyFrame true and the tag after that will be the keyFrame
  495. // e.g.
  496. // { keyFrame: false, metaDataTag: true},
  497. // { keyFrame: true, metaDataTag: true},
  498. // { keyFrame: true, metaDataTag: false}
  499. dataMessage.data.segment.tags.videoTags[0].metaDataTag = true;
  500. dataMessage.data.segment.tags.videoTags[1].metaDataTag = true;
  501. dataMessage.data.segment.tags.videoTags[1].keyFrame = true;
  502. dataMessage.data.segment.tags.videoTags[2].keyFrame = true;
  503. // no metadata tags in front of this key to test the case where mux.js does not prepend
  504. // the metadata tags
  505. dataMessage.data.segment.tags.videoTags[5].keyFrame = true;
  506. dataMessage.data.segment.tags.videoTags[7].metaDataTag = true;
  507. dataMessage.data.segment.tags.videoTags[8].metaDataTag = true;
  508. dataMessage.data.segment.tags.videoTags[8].keyFrame = true;
  509. dataMessage.data.segment.tags.videoTags[9].keyFrame = true;
  510. dataMessage.data.segment.tags.videoTags[12].metaDataTag = true;
  511. dataMessage.data.segment.tags.videoTags[13].metaDataTag = true;
  512. dataMessage.data.segment.tags.videoTags[13].keyFrame = true;
  513. dataMessage.data.segment.tags.videoTags[14].keyFrame = true;
  514. sourceBuffer.transmuxer_.onmessage(dataMessage);
  515. // seek to 7 seconds into the new swegment
  516. this.mediaSource.tech_.seeking = function() {
  517. return true;
  518. };
  519. currentTime = 10 + 7;
  520. this.mediaSource.tech_.trigger('seeking');
  521. sourceBuffer.appendBuffer(new Uint8Array(10));
  522. this.swfCalls.length = 0;
  523. timers.runAll();
  524. QUnit.deepEqual(this.swfCalls[0], { call: 'adjustCurrentTime', value: 15 });
  525. QUnit.deepEqual(this.swfCalls[1].arguments[0], [5, 5, 5, 6, 7, 8, 8, 8, 9],
  526. '10 tags are appended, 4 of which are metadata tags');
  527. });
  528. QUnit.test('drops all tags if target pts append time does not fall within segment', function() {
  529. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  530. let i = 10;
  531. let currentTime;
  532. let tags_ = [];
  533. this.mediaSource.tech_.currentTime = function() {
  534. return currentTime;
  535. };
  536. sourceBuffer.transmuxer_.postMessage = postMessage_;
  537. // push a tag into the buffer to establish the starting PTS value
  538. currentTime = 0;
  539. let dataMessage = createDataMessage([{
  540. pts: 19 * 1000,
  541. bytes: new Uint8Array(1)
  542. }]);
  543. sourceBuffer.transmuxer_.onmessage(dataMessage);
  544. timers.runAll();
  545. sourceBuffer.appendBuffer(new Uint8Array(10));
  546. timers.runAll();
  547. // mock out a new segment of FLV tags, starting 10s after the
  548. // starting PTS value
  549. while (i--) {
  550. tags_.unshift(
  551. {
  552. pts: (i * 1000) + (19 * 1000),
  553. bytes: new Uint8Array([i])
  554. }
  555. );
  556. }
  557. dataMessage = createDataMessage(tags_);
  558. // mock the GOP structure
  559. dataMessage.data.segment.tags.videoTags[0].keyFrame = true;
  560. dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  561. dataMessage.data.segment.tags.videoTags[5].keyFrame = true;
  562. dataMessage.data.segment.tags.videoTags[8].keyFrame = true;
  563. sourceBuffer.transmuxer_.onmessage(dataMessage);
  564. // seek to 7 seconds into the new swegment
  565. this.mediaSource.tech_.seeking = function() {
  566. return true;
  567. };
  568. currentTime = 10 + 7;
  569. this.mediaSource.tech_.trigger('seeking');
  570. sourceBuffer.appendBuffer(new Uint8Array(10));
  571. this.swfCalls.length = 0;
  572. timers.runAll();
  573. QUnit.equal(this.swfCalls.length, 0, 'dropped all tags and made no swf calls');
  574. });
  575. QUnit.test('seek targeting accounts for changing timestampOffsets', function() {
  576. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  577. let i = 10;
  578. let tags_ = [];
  579. let currentTime;
  580. this.mediaSource.tech_.currentTime = function() {
  581. return currentTime;
  582. };
  583. sourceBuffer.transmuxer_.postMessage = postMessage_;
  584. let dataMessage = createDataMessage([{
  585. pts: 19 * 1000,
  586. bytes: new Uint8Array(1)
  587. }]);
  588. // push a tag into the buffer to establish the starting PTS value
  589. currentTime = 0;
  590. sourceBuffer.transmuxer_.onmessage(dataMessage);
  591. timers.runAll();
  592. // to seek across a discontinuity:
  593. // 1. set the timestamp offset to the media timeline position for
  594. // the start of the segment
  595. // 2. set currentTime to the desired media timeline position
  596. sourceBuffer.timestampOffset = 22;
  597. currentTime = sourceBuffer.timestampOffset + 3.5;
  598. this.mediaSource.tech_.seeking = function() {
  599. return true;
  600. };
  601. // the new segment FLV tags are at disjoint PTS positions
  602. while (i--) {
  603. tags_.unshift({
  604. // (101 * 1000) !== the old PTS offset
  605. pts: (i * 1000) + (101 * 1000),
  606. bytes: new Uint8Array([i + sourceBuffer.timestampOffset])
  607. });
  608. }
  609. dataMessage = createDataMessage(tags_);
  610. // mock gop start at seek point
  611. dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  612. sourceBuffer.transmuxer_.onmessage(dataMessage);
  613. this.mediaSource.tech_.trigger('seeking');
  614. this.swfCalls.length = 0;
  615. timers.runAll();
  616. QUnit.equal(this.swfCalls[0].value, 25, 'adjusted current time');
  617. QUnit.deepEqual(this.swfCalls[1].arguments[0],
  618. [25, 26, 27, 28, 29, 30, 31],
  619. 'filtered the appended tags');
  620. });
  621. QUnit.test('calling endOfStream sets mediaSource readyState to ended', function() {
  622. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  623. sourceBuffer.transmuxer_.postMessage = postMessage_;
  624. /* eslint-disable camelcase */
  625. this.mediaSource.swfObj.vjs_endOfStream = () => {
  626. this.swfCalls.push('endOfStream');
  627. };
  628. /* eslint-enable camelcase */
  629. sourceBuffer.addEventListener('updateend', () => {
  630. this.mediaSource.endOfStream();
  631. });
  632. this.swfCalls.length = 0;
  633. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  634. timers.runAll();
  635. QUnit.strictEqual(sourceBuffer.mediaSource_.readyState,
  636. 'ended',
  637. 'readyState is \'ended\'');
  638. QUnit.strictEqual(this.swfCalls.length, 2, 'made two calls to swf');
  639. QUnit.deepEqual(this.swfCalls.shift().arguments[0],
  640. [0, 1],
  641. 'contains the data');
  642. QUnit.ok(this.swfCalls.shift().indexOf('endOfStream') === 0,
  643. 'the second call should be for the updateend');
  644. QUnit.strictEqual(timers.length, 0, 'no more appends are scheduled');
  645. });
  646. QUnit.test('opens the stream on sourceBuffer.appendBuffer after endOfStream', function() {
  647. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  648. let foo = () => {
  649. this.mediaSource.endOfStream();
  650. sourceBuffer.removeEventListener('updateend', foo);
  651. };
  652. sourceBuffer.transmuxer_.postMessage = postMessage_;
  653. /* eslint-disable camelcase */
  654. this.mediaSource.swfObj.vjs_endOfStream = () => {
  655. this.swfCalls.push('endOfStream');
  656. };
  657. /* eslint-enable camelcase */
  658. sourceBuffer.addEventListener('updateend', foo);
  659. this.swfCalls.length = 0;
  660. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  661. timers.runAll();
  662. QUnit.strictEqual(this.swfCalls.length, 2, 'made two calls to swf');
  663. QUnit.deepEqual(this.swfCalls.shift().arguments[0],
  664. [0, 1],
  665. 'contains the data');
  666. QUnit.equal(this.swfCalls.shift(),
  667. 'endOfStream',
  668. 'the second call should be for the updateend');
  669. sourceBuffer.appendBuffer(new Uint8Array([2, 3]));
  670. // remove previous video pts save because mock appends don't have actual timing data
  671. sourceBuffer.videoBufferEnd_ = NaN;
  672. timers.runAll();
  673. QUnit.strictEqual(this.swfCalls.length, 1, 'made one more append');
  674. QUnit.deepEqual(this.swfCalls.shift().arguments[0],
  675. [2, 3],
  676. 'contains the third and fourth bytes');
  677. QUnit.strictEqual(
  678. sourceBuffer.mediaSource_.readyState,
  679. 'open',
  680. 'The streams should be open if more bytes are appended to an "ended" stream'
  681. );
  682. QUnit.strictEqual(timers.length, 0, 'no more appends are scheduled');
  683. });
  684. QUnit.test('abort() clears any buffered input', function() {
  685. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  686. sourceBuffer.transmuxer_.postMessage = postMessage_;
  687. this.swfCalls.length = 0;
  688. sourceBuffer.appendBuffer(new Uint8Array([0]));
  689. sourceBuffer.abort();
  690. timers.pop()();
  691. QUnit.strictEqual(this.swfCalls.length, 1, 'called the swf');
  692. QUnit.strictEqual(this.swfCalls[0], 'abort', 'invoked abort');
  693. });
  694. // requestAnimationFrame is heavily throttled or unscheduled when
  695. // the browser tab running contrib-media-sources is in a background
  696. // tab. If that happens, video data can continuously build up in
  697. // memory and cause the tab or browser to crash.
  698. QUnit.test('does not use requestAnimationFrame', function() {
  699. let oldRFA = window.requestAnimationFrame;
  700. let requests = 0;
  701. let sourceBuffer;
  702. window.requestAnimationFrame = function() {
  703. requests++;
  704. };
  705. sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  706. sourceBuffer.transmuxer_.postMessage = postMessage_;
  707. sourceBuffer.appendBuffer(new Uint8Array([0, 1, 2, 3]));
  708. while (timers.length) {
  709. timers.pop()();
  710. }
  711. QUnit.equal(requests, 0, 'no calls to requestAnimationFrame were made');
  712. window.requestAnimationFrame = oldRFA;
  713. });
  714. QUnit.test('updating is true while an append is in progress', function() {
  715. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  716. let ended = false;
  717. sourceBuffer.transmuxer_.postMessage = postMessage_;
  718. sourceBuffer.addEventListener('updateend', function() {
  719. ended = true;
  720. });
  721. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  722. QUnit.equal(sourceBuffer.updating, true, 'updating is set');
  723. while (!ended) {
  724. timers.pop()();
  725. }
  726. QUnit.equal(sourceBuffer.updating, false, 'updating is unset');
  727. });
  728. QUnit.test('throws an error if append is called while updating', function() {
  729. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  730. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  731. sourceBuffer.transmuxer_.postMessage = postMessage_;
  732. QUnit.throws(function() {
  733. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  734. }, function(e) {
  735. return e.name === 'InvalidStateError' &&
  736. e.code === window.DOMException.INVALID_STATE_ERR;
  737. }, 'threw an InvalidStateError');
  738. });
  739. QUnit.test('stops updating if abort is called', function() {
  740. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  741. let updateEnds = 0;
  742. sourceBuffer.transmuxer_.postMessage = postMessage_;
  743. sourceBuffer.addEventListener('updateend', function() {
  744. updateEnds++;
  745. });
  746. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  747. sourceBuffer.abort();
  748. QUnit.equal(sourceBuffer.updating, false, 'no longer updating');
  749. QUnit.equal(updateEnds, 1, 'triggered updateend');
  750. });
  751. QUnit.test('forwards duration overrides to the SWF', function() {
  752. /* eslint-disable no-unused-vars */
  753. let ignored = this.mediaSource.duration;
  754. /* eslint-enable no-unused-vars */
  755. QUnit.deepEqual(this.swfCalls[1], {
  756. attr: 'duration'
  757. }, 'requests duration from the SWF');
  758. this.mediaSource.duration = 101.3;
  759. // Setting a duration results in two calls to the swf
  760. // Ignore the first call (this.swfCalls[2]) as it was just to get the
  761. // current duration
  762. QUnit.deepEqual(this.swfCalls[3], {
  763. attr: 'duration', value: 101.3
  764. }, 'set the duration override');
  765. });
  766. QUnit.test('returns NaN for duration before the SWF is ready', function() {
  767. this.mediaSource.swfObj = null;
  768. QUnit.ok(isNaN(this.mediaSource.duration), 'duration is NaN');
  769. });
  770. QUnit.test('calculates the base PTS for the media', function() {
  771. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  772. let tags_ = [];
  773. sourceBuffer.transmuxer_.postMessage = postMessage_;
  774. // seek to 15 seconds
  775. this.player.tech_.seeking = function() {
  776. return true;
  777. };
  778. this.player.tech_.currentTime = function() {
  779. return 15;
  780. };
  781. // FLV tags for this segment start at 10 seconds in the media
  782. // timeline
  783. tags_.push(
  784. // zero in the media timeline is PTS 3
  785. { pts: (10 + 3) * 1000, bytes: new Uint8Array([10]) },
  786. { pts: (15 + 3) * 1000, bytes: new Uint8Array([15]) }
  787. );
  788. let dataMessage = createDataMessage(tags_);
  789. // mock gop start at seek point
  790. dataMessage.data.segment.tags.videoTags[1].keyFrame = true;
  791. sourceBuffer.transmuxer_.onmessage(dataMessage);
  792. // let the source buffer know the segment start time
  793. sourceBuffer.timestampOffset = 10;
  794. this.swfCalls.length = 0;
  795. timers.runAll();
  796. QUnit.equal(this.swfCalls.length, 1, 'made a SWF call');
  797. QUnit.deepEqual(this.swfCalls[0].arguments[0], [15], 'dropped the early tag');
  798. });
  799. QUnit.test('remove fires update events', function() {
  800. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  801. let events = [];
  802. sourceBuffer.transmuxer_.postMessage = postMessage_;
  803. sourceBuffer.on(['update', 'updateend'], function(event) {
  804. events.push(event.type);
  805. });
  806. sourceBuffer.remove(0, 1);
  807. QUnit.deepEqual(events, ['update', 'updateend'], 'fired update events');
  808. QUnit.equal(sourceBuffer.updating, false, 'finished updating');
  809. });
  810. QUnit.test('passes endOfStream network errors to the tech', function() {
  811. this.mediaSource.readyState = 'ended';
  812. this.mediaSource.endOfStream('network');
  813. QUnit.equal(this.player.tech_.error().code, 2, 'set a network error');
  814. });
  815. QUnit.test('passes endOfStream decode errors to the tech', function() {
  816. this.mediaSource.readyState = 'ended';
  817. this.mediaSource.endOfStream('decode');
  818. QUnit.equal(this.player.tech_.error().code, 3, 'set a decode error');
  819. });
  820. QUnit.test('has addSeekableRange()', function() {
  821. QUnit.ok(this.mediaSource.addSeekableRange_, 'has addSeekableRange_');
  822. });
  823. QUnit.test('fires loadedmetadata after first segment append', function() {
  824. let loadedmetadataCount = 0;
  825. this.mediaSource.tech_.on('loadedmetadata', () => loadedmetadataCount++);
  826. let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  827. sourceBuffer.transmuxer_.postMessage = postMessage_;
  828. QUnit.equal(loadedmetadataCount, 0, 'loadedmetadata not called on buffer creation');
  829. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  830. QUnit.equal(loadedmetadataCount, 0, 'loadedmetadata not called on segment append');
  831. timers.runAll();
  832. QUnit.equal(loadedmetadataCount, 1, 'loadedmetadata fires after first append');
  833. sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  834. timers.runAll();
  835. QUnit.equal(loadedmetadataCount, 1, 'loadedmetadata does not fire after second append');
  836. });