mirror of
				https://github.com/videojs/video.js.git
				synced 2025-10-31 00:08:01 +02:00 
			
		
		
		
	## Description It is useful to have methods for appending and removing `<source>` elements to the `<video>` element, as they are sometimes required to enable certain playback features, for example, using [Airplay with MSE](https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay). ## Specific Changes proposed Add new methods-- `addSourceElement()` and `removeSourceElement()` to the player and tech. The former will take a source object and create and append a new `<source>` element to the `<video>` element, and the latter will take a source url and remove any `<source>` element with a matching `src`. ## Requirements Checklist - [ ] Feature implemented / Bug fixed - [ ] If necessary, more likely in a feature request than a bug fix - [ ] Change has been verified in an actual browser (Chrome, Firefox, IE) - [ ] Unit Tests updated or fixed - [ ] Docs/guides updated - [ ] Example created ([starter template on JSBin](https://codepen.io/gkatsev/pen/GwZegv?editors=1000#0)) - [ ] Has no DOM changes which impact accessiblilty or trigger warnings (e.g. Chrome issues tab) - [ ] Has no changes to JSDoc which cause `npm run docs:api` to error - [ ] Reviewed by Two Core Contributors
		
			
				
	
	
		
			984 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			984 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* eslint-env qunit, browser */
 | |
| let player;
 | |
| let tech;
 | |
| 
 | |
| import Html5 from '../../../src/js/tech/html5.js';
 | |
| import * as browser from '../../../src/js/utils/browser.js';
 | |
| import document from 'global/document';
 | |
| import sinon from 'sinon';
 | |
| 
 | |
| QUnit.module('HTML5', {
 | |
|   beforeEach(assert) {
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.innerHTML = '<div />';
 | |
|     player = {
 | |
|       id() {
 | |
|         return 'id';
 | |
|       },
 | |
|       el() {
 | |
|         return el;
 | |
|       },
 | |
|       options_: {},
 | |
|       options() {
 | |
|         return this.options_;
 | |
|       },
 | |
|       bufferedPercent() {
 | |
|         return 0;
 | |
|       },
 | |
|       controls() {
 | |
|         return false;
 | |
|       },
 | |
|       usingNativeControls() {
 | |
|         return false;
 | |
|       },
 | |
|       on() {
 | |
|         return this;
 | |
|       },
 | |
|       off() {
 | |
|         return this;
 | |
|       },
 | |
|       ready() {},
 | |
|       addChild() {},
 | |
|       trigger() {}
 | |
|     };
 | |
|     tech = new Html5({});
 | |
|   },
 | |
|   afterEach(assert) {
 | |
|     tech.dispose();
 | |
|     player = null;
 | |
|     tech = null;
 | |
|   }
 | |
| });
 | |
| 
 | |
| QUnit[browser.IS_ANY_SAFARI ? 'test' : 'skip']('if setScrubbing is true and fastSeek is available, use it', function(assert) {
 | |
|   Object.defineProperty(tech.el(), 'currentTime', {
 | |
|     get: () => {},
 | |
|     set: () => {},
 | |
| 
 | |
|     writeable: true,
 | |
|     enumerable: false,
 | |
|     configurable: true
 | |
|   });
 | |
| 
 | |
|   const currentTimeSpy = sinon.spy(tech.el(), 'currentTime', ['set']);
 | |
| 
 | |
|   tech.setCurrentTime(5);
 | |
|   assert.ok(currentTimeSpy.set.called, 'currentTime setter was called');
 | |
|   assert.ok(currentTimeSpy.set.calledWith(5), 'currentTime setter was called with 5');
 | |
| 
 | |
|   tech.setScrubbing(true);
 | |
| 
 | |
|   // when scrubbing is set but fastSeek isn't available, currentTime should still be called
 | |
|   tech.el().fastSeek = null;
 | |
|   tech.setCurrentTime(10);
 | |
|   assert.ok(currentTimeSpy.set.called, 'currentTime setter was called');
 | |
|   assert.ok(currentTimeSpy.set.calledWith(10), 'currentTime setter was called with 10');
 | |
| 
 | |
|   const fastSeekSpy = tech.el().fastSeek = sinon.spy();
 | |
| 
 | |
|   tech.setCurrentTime(15);
 | |
|   assert.ok(currentTimeSpy.set.calledTwice, 'currentTime setter was only called twice and not a 3rd time for fastSeek');
 | |
|   assert.ok(fastSeekSpy.called, 'fastSeek called');
 | |
|   assert.ok(fastSeekSpy.calledWith(15), 'fastSeek called with 15');
 | |
| });
 | |
| 
 | |
| QUnit.test('should be able to set playsinline attribute', function(assert) {
 | |
|   assert.expect(2);
 | |
| 
 | |
|   tech.createEl();
 | |
|   tech.setPlaysinline(true);
 | |
| 
 | |
|   assert.ok(tech.el().hasAttribute('playsinline'), 'playsinline attribute was added');
 | |
| 
 | |
|   tech.setPlaysinline(false);
 | |
| 
 | |
|   assert.ok(!tech.el().hasAttribute('playsinline'), 'playsinline attribute was removed');
 | |
| });
 | |
| 
 | |
| QUnit.test('should detect whether the volume can be changed', function(assert) {
 | |
| 
 | |
|   if (!{}.__defineSetter__) {
 | |
|     assert.ok(true, 'your browser does not support this test, skipping it');
 | |
|     return;
 | |
|   }
 | |
|   const testVid = Html5.TEST_VID;
 | |
|   const ConstVolumeVideo = function() {
 | |
|     this.volume = 1;
 | |
|     this.__defineSetter__('volume', function() {});
 | |
|   };
 | |
| 
 | |
|   Html5.TEST_VID = new ConstVolumeVideo();
 | |
| 
 | |
|   assert.ok(!Html5.canControlVolume());
 | |
|   Html5.TEST_VID = testVid;
 | |
| });
 | |
| 
 | |
| QUnit.test('test playbackRate', function(assert) {
 | |
|   // Android 2.3 always returns 0 for playback rate
 | |
|   if (!Html5.canControlPlaybackRate()) {
 | |
|     assert.ok(true, 'Playback rate is not supported');
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   tech.createEl();
 | |
| 
 | |
|   tech.el().playbackRate = 1.25;
 | |
|   assert.strictEqual(tech.playbackRate(), 1.25, 'can be changed from the element');
 | |
| 
 | |
|   tech.setPlaybackRate(0.75);
 | |
|   assert.strictEqual(tech.playbackRate(), 0.75, 'can be changed from the API');
 | |
| });
 | |
| 
 | |
| QUnit.test('test defaultPlaybackRate', function(assert) {
 | |
|   // Android 2.3 always returns 0 for playback rate
 | |
|   if (!Html5.canControlPlaybackRate()) {
 | |
|     assert.ok(true, 'Playback rate is not supported');
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   tech.createEl();
 | |
| 
 | |
|   tech.el().defaultPlaybackRate = 1.25;
 | |
|   assert.strictEqual(tech.defaultPlaybackRate(), 1.25, 'can be changed from the element');
 | |
| 
 | |
|   tech.setDefaultPlaybackRate(0.75);
 | |
|   assert.strictEqual(tech.defaultPlaybackRate(), 0.75, 'can be changed from the API');
 | |
| });
 | |
| 
 | |
| QUnit.test('blacklist playbackRate support on older versions of Chrome on Android', function(assert) {
 | |
|   if (!Html5.canControlPlaybackRate()) {
 | |
|     assert.ok(true, 'playbackRate is not supported');
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   // Reset playbackrate - Firefox's rounding of playbackRate causes the rate not to change in canControlPlaybackRate() after a few instances
 | |
|   Html5.TEST_VID.playbackRate = 1;
 | |
| 
 | |
|   const oldIsAndroid = browser.IS_ANDROID;
 | |
|   const oldIsChrome = browser.IS_CHROME;
 | |
|   const oldChromeVersion = browser.CHROME_VERSION;
 | |
| 
 | |
|   browser.stub_IS_ANDROID(true);
 | |
|   browser.stub_IS_CHROME(true);
 | |
|   browser.stub_CHROME_VERSION(50);
 | |
|   assert.strictEqual(Html5.canControlPlaybackRate(), false, 'canControlPlaybackRate should return false on older Chrome');
 | |
| 
 | |
|   browser.stub_CHROME_VERSION(58);
 | |
|   assert.strictEqual(Html5.canControlPlaybackRate(), true, 'canControlPlaybackRate should return true on newer Chrome');
 | |
| 
 | |
|   browser.stub_IS_ANDROID(oldIsAndroid);
 | |
|   browser.stub_IS_CHROME(oldIsChrome);
 | |
|   browser.stub_CHROME_VERSION(oldChromeVersion);
 | |
| });
 | |
| 
 | |
| QUnit.test('test volume', function(assert) {
 | |
|   if (!Html5.canControlVolume()) {
 | |
|     assert.ok(true, 'Volume is not supported');
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   tech.createEl();
 | |
| 
 | |
|   tech.el().volume = 0.5;
 | |
|   assert.strictEqual(tech.volume(), 0.5, 'can be changed from the element');
 | |
| 
 | |
|   tech.setVolume(1);
 | |
|   assert.strictEqual(tech.volume(), 1, 'can be changed from the API');
 | |
| });
 | |
| 
 | |
| QUnit.test('test defaultMuted', function(assert) {
 | |
|   tech.createEl();
 | |
| 
 | |
|   tech.el().defaultMuted = true;
 | |
|   assert.strictEqual(tech.defaultMuted(), true, 'can be changed from the element');
 | |
| 
 | |
|   tech.setDefaultMuted(false);
 | |
|   assert.strictEqual(tech.defaultMuted(), false, 'can be changed from the API');
 | |
| });
 | |
| 
 | |
| QUnit.test('should export played', function(assert) {
 | |
|   tech.createEl();
 | |
|   assert.deepEqual(tech.played(), tech.el().played, 'returns the played attribute');
 | |
| });
 | |
| 
 | |
| QUnit.test('should remove the controls attribute when recreating the element', function(assert) {
 | |
|   player.tagAttributes = {
 | |
|     controls: true
 | |
|   };
 | |
|   // force custom controls so the test environment is equivalent on iOS
 | |
|   player.options_.nativeControlsForTouch = false;
 | |
|   const el = tech.createEl();
 | |
| 
 | |
|   // On the iPhone controls are always true
 | |
|   if (!browser.IS_IPHONE) {
 | |
|     assert.ok(!el.controls, 'controls attribute is absent');
 | |
|   }
 | |
| 
 | |
|   assert.ok(player.tagAttributes.controls, 'tag attribute is still present');
 | |
| });
 | |
| 
 | |
| QUnit.test('error events may not set the errors property', function(assert) {
 | |
|   assert.equal(tech.error(), undefined, 'no tech-level error');
 | |
|   tech.trigger('error');
 | |
|   assert.ok(true, 'no error was thrown');
 | |
| });
 | |
| 
 | |
| QUnit.test('should have the source handler interface', function(assert) {
 | |
|   assert.ok(Html5.registerSourceHandler, 'has the registerSourceHandler function');
 | |
| });
 | |
| 
 | |
| QUnit.test('native source handler canPlayType', function(assert) {
 | |
|   // Stub the test video canPlayType (used in canPlayType) to control results
 | |
|   const origCPT = Html5.TEST_VID.canPlayType;
 | |
| 
 | |
|   Html5.TEST_VID.canPlayType = function(type) {
 | |
|     if (type === 'video/mp4') {
 | |
|       return 'maybe';
 | |
|     }
 | |
|     return '';
 | |
|   };
 | |
| 
 | |
|   const canPlayType = Html5.nativeSourceHandler.canPlayType;
 | |
| 
 | |
|   assert.equal(
 | |
|     canPlayType('video/mp4'),
 | |
|     'maybe',
 | |
|     'Native source handler reported type support'
 | |
|   );
 | |
|   assert.equal(canPlayType('foo'), '', 'Native source handler handled bad type');
 | |
| 
 | |
|   // Reset test video canPlayType
 | |
|   Html5.TEST_VID.canPlayType = origCPT;
 | |
| });
 | |
| 
 | |
| QUnit.test('native source handler canHandleSource', function(assert) {
 | |
|   // Stub the test video canPlayType (used in canHandleSource) to control results
 | |
|   const origCPT = Html5.TEST_VID.canPlayType;
 | |
| 
 | |
|   Html5.TEST_VID.canPlayType = function(type) {
 | |
|     if (type === 'video/mp4') {
 | |
|       return 'maybe';
 | |
|     }
 | |
|     return '';
 | |
|   };
 | |
| 
 | |
|   const canHandleSource = Html5.nativeSourceHandler.canHandleSource;
 | |
| 
 | |
|   assert.equal(
 | |
|     canHandleSource({ type: 'video/mp4', src: 'video.flv' }, {}),
 | |
|     'maybe',
 | |
|     'Native source handler reported type support'
 | |
|   );
 | |
|   assert.equal(
 | |
|     canHandleSource({ src: 'http://www.example.com/video.mp4' }, {}),
 | |
|     'maybe',
 | |
|     'Native source handler reported extension support'
 | |
|   );
 | |
|   assert.equal(
 | |
|     canHandleSource({ src: 'https://example.com/video.sd.mp4?s=foo&token=bar' }, {}),
 | |
|     'maybe',
 | |
|     'Native source handler reported extension support'
 | |
|   );
 | |
|   assert.equal(
 | |
|     canHandleSource({ src: 'https://example.com/video.sd.mp4?s=foo' }, {}),
 | |
|     'maybe',
 | |
|     'Native source handler reported extension support'
 | |
|   );
 | |
| 
 | |
|   // Test for issue videojs/video.js#1785 and other potential failures
 | |
|   assert.equal(
 | |
|     canHandleSource({ src: '' }, {}),
 | |
|     '',
 | |
|     'Native source handler handled empty src'
 | |
|   );
 | |
|   assert.equal(
 | |
|     canHandleSource({}, {}),
 | |
|     '',
 | |
|     'Native source handler handled empty object'
 | |
|   );
 | |
|   assert.equal(
 | |
|     canHandleSource({ src: 'foo' }, {}),
 | |
|     '',
 | |
|     'Native source handler handled bad src'
 | |
|   );
 | |
|   assert.equal(
 | |
|     canHandleSource({ type: 'foo' }, {}),
 | |
|     '',
 | |
|     'Native source handler handled bad type'
 | |
|   );
 | |
| 
 | |
|   // Reset test video canPlayType
 | |
|   Html5.TEST_VID.canPlayType = origCPT;
 | |
| });
 | |
| 
 | |
| if (Html5.supportsNativeTextTracks()) {
 | |
|   QUnit.test('add native textTrack listeners on startup', function(assert) {
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const tt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => adds.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => rems.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.textTracks = tt;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     assert.equal(adds[0][0], 'change', 'change event handler added');
 | |
|     assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
 | |
|     assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| 
 | |
|   QUnit.test('does not add native textTrack listeners when disabled', function(assert) {
 | |
|     const events = [];
 | |
|     const tt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => events.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => events.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('video');
 | |
| 
 | |
|     Object.defineProperty(el, 'textTracks', {
 | |
|       get: () => tt
 | |
|     });
 | |
| 
 | |
|     const htmlTech = new Html5({el, nativeTextTracks: false});
 | |
| 
 | |
|     assert.equal(events.length, 0, 'no listeners added');
 | |
| 
 | |
|     const htmlTechAlternate = new Html5({el, nativeCaptions: false});
 | |
| 
 | |
|     assert.equal(events.length, 0, 'no listeners added');
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|     htmlTechAlternate.dispose();
 | |
|   });
 | |
| 
 | |
|   QUnit.test('remove all tracks from emulated list on dispose', function(assert) {
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const tt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => adds.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => rems.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.textTracks = tt;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     htmlTech.dispose();
 | |
| 
 | |
|     assert.equal(adds[0][0], 'change', 'change event handler added');
 | |
|     assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
 | |
|     assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
 | |
|     assert.equal(rems[0][0], 'change', 'change event handler removed');
 | |
|     assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
 | |
|     assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
 | |
|     assert.equal(adds[0][0], rems[0][0], 'change event handler removed');
 | |
|     assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
 | |
|     assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
 | |
|   });
 | |
| }
 | |
| 
 | |
| if (Html5.supportsNativeAudioTracks()) {
 | |
|   QUnit.test('add native audioTrack listeners on startup', function(assert) {
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const at = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => adds.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => rems.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.audioTracks = at;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     assert.equal(adds[0][0], 'change', 'change event handler added');
 | |
|     assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
 | |
|     assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| 
 | |
|   QUnit.test('does not add native audioTrack listeners when disabled', function(assert) {
 | |
|     const events = [];
 | |
|     const at = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => events.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => events.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.audioTracks = at;
 | |
| 
 | |
|     const htmlTech = new Html5({el, nativeAudioTracks: false});
 | |
| 
 | |
|     assert.equal(events.length, 0, 'no listeners added');
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| 
 | |
|   QUnit.test('remove all tracks from emulated list on dispose', function(assert) {
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const at = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => adds.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => rems.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.audioTracks = at;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     htmlTech.dispose();
 | |
| 
 | |
|     assert.equal(adds[0][0], 'change', 'change event handler added');
 | |
|     assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
 | |
|     assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
 | |
|     assert.equal(rems[0][0], 'change', 'change event handler removed');
 | |
|     assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
 | |
|     assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
 | |
|     assert.equal(adds[0][0], rems[0][0], 'change event handler removed');
 | |
|     assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
 | |
|     assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
 | |
|   });
 | |
| 
 | |
|   QUnit.test('should use overrideNativeTracks on audio correctly', function(assert) {
 | |
|     assert.expect(8);
 | |
| 
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const at = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => {
 | |
|         adds.push({ type, fn });
 | |
|       },
 | |
|       removeEventListener: (type, fn) => {
 | |
|         rems.push({ type, fn });
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.audioTracks = at;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 3,
 | |
|       'should have added change, remove, add listeners'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 0,
 | |
|       'no listeners should be removed'
 | |
|     );
 | |
| 
 | |
|     htmlTech.overrideNativeAudioTracks(true);
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 3,
 | |
|       'should not have added additional listeners'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 3,
 | |
|       'should have removed previous three listeners'
 | |
|     );
 | |
| 
 | |
|     htmlTech.overrideNativeAudioTracks(true);
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 3,
 | |
|       'no state change so do not add listeners'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 3,
 | |
|       'no state change so do not remove listeners'
 | |
|     );
 | |
| 
 | |
|     htmlTech.overrideNativeAudioTracks(false);
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 6,
 | |
|       'should add listeners because native tracks should be proxied'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 3,
 | |
|       'should not remove listeners because there where none added on previous state'
 | |
|     );
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| }
 | |
| 
 | |
| if (Html5.supportsNativeVideoTracks()) {
 | |
|   QUnit.test('add native videoTrack listeners on startup', function(assert) {
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const vt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => adds.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => rems.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.videoTracks = vt;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     assert.equal(adds[0][0], 'change', 'change event handler added');
 | |
|     assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
 | |
|     assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| 
 | |
|   QUnit.test('does not add native audioTrack listeners when disabled', function(assert) {
 | |
|     const events = [];
 | |
|     const vt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => events.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => events.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.videoTracks = vt;
 | |
| 
 | |
|     const htmlTech = new Html5({el, nativeVideoTracks: false});
 | |
| 
 | |
|     assert.equal(events.length, 0, 'no listeners added');
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| 
 | |
|   QUnit.test('remove all tracks from emulated list on dispose', function(assert) {
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const vt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => adds.push([type, fn]),
 | |
|       removeEventListener: (type, fn) => rems.push([type, fn])
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.videoTracks = vt;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     htmlTech.dispose();
 | |
| 
 | |
|     assert.equal(adds[0][0], 'change', 'change event handler added');
 | |
|     assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
 | |
|     assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
 | |
|     assert.equal(rems[0][0], 'change', 'change event handler removed');
 | |
|     assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
 | |
|     assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
 | |
|     assert.equal(adds[0][0], rems[0][0], 'change event handler removed');
 | |
|     assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
 | |
|     assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
 | |
|   });
 | |
| 
 | |
|   QUnit.test('should use overrideNativeTracks on video correctly', function(assert) {
 | |
|     assert.expect(8);
 | |
| 
 | |
|     const adds = [];
 | |
|     const rems = [];
 | |
|     const vt = {
 | |
|       length: 0,
 | |
|       addEventListener: (type, fn) => {
 | |
|         adds.push({ type, fn });
 | |
|       },
 | |
|       removeEventListener: (type, fn) => {
 | |
|         rems.push({ type, fn });
 | |
|       }
 | |
|     };
 | |
|     const el = document.createElement('div');
 | |
| 
 | |
|     el.videoTracks = vt;
 | |
| 
 | |
|     const htmlTech = new Html5({el});
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 3,
 | |
|       'should have added change, remove, add listeners'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 0,
 | |
|       'no listeners should be removed'
 | |
|     );
 | |
| 
 | |
|     htmlTech.overrideNativeVideoTracks(true);
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 3,
 | |
|       'should not have added additional listeners'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 3,
 | |
|       'should have removed previous three listeners'
 | |
|     );
 | |
| 
 | |
|     htmlTech.overrideNativeVideoTracks(true);
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 3,
 | |
|       'no state change so do not add listeners'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 3,
 | |
|       'no state change so do not remove listeners'
 | |
|     );
 | |
| 
 | |
|     htmlTech.overrideNativeVideoTracks(false);
 | |
| 
 | |
|     assert.equal(
 | |
|       adds.length, 6,
 | |
|       'should add listeners because native tracks should be proxied'
 | |
|     );
 | |
|     assert.equal(
 | |
|       rems.length, 3,
 | |
|       'should not remove listeners because there where none added on previous state'
 | |
|     );
 | |
| 
 | |
|     htmlTech.dispose();
 | |
|   });
 | |
| }
 | |
| 
 | |
| QUnit.test('should always return currentSource_ if set', function(assert) {
 | |
|   const currentSrc = Html5.prototype.currentSrc;
 | |
| 
 | |
|   assert.equal(
 | |
|     currentSrc.call({el_: {currentSrc: 'test1'}}),
 | |
|     'test1',
 | |
|     'sould return source from element if nothing else set'
 | |
|   );
 | |
|   assert.equal(
 | |
|     currentSrc.call({currentSource_: {src: 'test2'}}),
 | |
|     'test2',
 | |
|     'sould return source from currentSource_, if nothing else set'
 | |
|   );
 | |
|   assert.equal(
 | |
|     currentSrc.call({currentSource_: {src: 'test2'},
 | |
|       el_: {currentSrc: 'test1'}}),
 | |
|     'test2',
 | |
|     'sould return source from  source set, not from element'
 | |
|   );
 | |
| });
 | |
| 
 | |
| QUnit.test('should fire makeup events when a video tag is initialized late', function(assert) {
 | |
|   const lateInit = Html5.prototype.handleLateInit_;
 | |
|   let triggeredEvents = [];
 | |
|   const mockHtml5 = {
 | |
|     readyListeners: [],
 | |
|     ready(listener) {
 | |
|       this.readyListeners.push(listener);
 | |
|     },
 | |
|     triggerReady() {
 | |
|       this.readyListeners.forEach(function(listener) {
 | |
|         listener.call(this);
 | |
|       }, this);
 | |
|     },
 | |
|     trigger(type) {
 | |
|       triggeredEvents.push(type);
 | |
|     },
 | |
|     on() {},
 | |
|     off() {}
 | |
|   };
 | |
| 
 | |
|   function testStates(statesObject, expectedEvents) {
 | |
|     lateInit.call(mockHtml5, statesObject);
 | |
|     mockHtml5.triggerReady();
 | |
|     assert.deepEqual(
 | |
|       triggeredEvents,
 | |
|       expectedEvents,
 | |
|       'wrong events triggered for ' +
 | |
|                     `networkState:${statesObject.networkState} ` +
 | |
|                     `and readyState:${statesObject.readyState || 'no readyState'}`
 | |
|     );
 | |
| 
 | |
|     // reset mock
 | |
|     triggeredEvents = [];
 | |
|     mockHtml5.readyListeners = [];
 | |
|   }
 | |
| 
 | |
|   // Network States
 | |
|   testStates({ networkState: 0, readyState: 0 }, []);
 | |
|   testStates({ networkState: 1, readyState: 0 }, ['loadstart']);
 | |
|   testStates({ networkState: 2, readyState: 0 }, ['loadstart']);
 | |
|   testStates({ networkState: 3, readyState: 0 }, []);
 | |
| 
 | |
|   // Ready States
 | |
|   testStates({ networkState: 1, readyState: 0 }, ['loadstart']);
 | |
|   testStates({ networkState: 1, readyState: 1 }, ['loadstart', 'loadedmetadata']);
 | |
|   testStates(
 | |
|     { networkState: 1, readyState: 2 },
 | |
|     ['loadstart', 'loadedmetadata', 'loadeddata']
 | |
|   );
 | |
|   testStates(
 | |
|     { networkState: 1, readyState: 3 },
 | |
|     ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay']
 | |
|   );
 | |
|   testStates(
 | |
|     { networkState: 1, readyState: 4 },
 | |
|     ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough']
 | |
|   );
 | |
| });
 | |
| 
 | |
| QUnit.test('Html5.resetMediaElement should remove sources and call load', function(assert) {
 | |
|   let selector;
 | |
|   const removedChildren = [];
 | |
|   let removedAttribute;
 | |
|   let loaded;
 | |
|   const children = ['source1', 'source2', 'source3'];
 | |
|   const testEl = {
 | |
|     querySelectorAll(input) {
 | |
|       selector = input;
 | |
|       return children;
 | |
|     },
 | |
| 
 | |
|     removeChild(child) {
 | |
|       removedChildren.push(child);
 | |
|     },
 | |
| 
 | |
|     removeAttribute(attr) {
 | |
|       removedAttribute = attr;
 | |
|     },
 | |
| 
 | |
|     load() {
 | |
|       loaded = true;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   Html5.resetMediaElement(testEl);
 | |
|   assert.equal(selector, 'source', 'we got the source elements from the test el');
 | |
|   assert.deepEqual(
 | |
|     removedChildren,
 | |
|     children.reverse(),
 | |
|     'we removed the children that were present'
 | |
|   );
 | |
|   assert.equal(removedAttribute, 'src', 'we removed the src attribute');
 | |
|   assert.ok(loaded, 'we called load on the element');
 | |
| });
 | |
| 
 | |
| QUnit.test('Html5#reset calls Html5.resetMediaElement when called', function(assert) {
 | |
|   const oldResetMedia = Html5.resetMediaElement;
 | |
|   let resetEl;
 | |
| 
 | |
|   Html5.resetMediaElement = (el) => {
 | |
|     resetEl = el;
 | |
|   };
 | |
| 
 | |
|   const el = {};
 | |
| 
 | |
|   Html5.prototype.reset.call({el_: el});
 | |
| 
 | |
|   assert.equal(resetEl, el, 'we called resetMediaElement with the tech\'s el');
 | |
| 
 | |
|   Html5.resetMediaElement = oldResetMedia;
 | |
| });
 | |
| 
 | |
| QUnit.test('When Android Chrome reports Infinity duration with currentTime 0, return NaN', function(assert) {
 | |
|   const oldIsAndroid = browser.IS_ANDROID;
 | |
|   const oldIsChrome = browser.IS_CHROME;
 | |
|   const oldEl = tech.el_;
 | |
| 
 | |
|   browser.stub_IS_ANDROID(true);
 | |
|   browser.stub_IS_CHROME(true);
 | |
| 
 | |
|   tech.el_ = {
 | |
|     duration: Infinity,
 | |
|     currentTime: 0
 | |
|   };
 | |
|   assert.ok(Number.isNaN(tech.duration()), 'returned NaN with currentTime 0');
 | |
| 
 | |
|   browser.stub_IS_ANDROID(oldIsAndroid);
 | |
|   browser.stub_IS_CHROME(oldIsChrome);
 | |
|   tech.el_ = oldEl;
 | |
| });
 | |
| 
 | |
| QUnit.test('supports getting available media playback quality metrics', function(assert) {
 | |
|   const origPerformance = window.performance;
 | |
|   const oldEl = tech.el_;
 | |
|   const videoPlaybackQuality = {
 | |
|     creationTime: 1,
 | |
|     corruptedVideoFrames: 2,
 | |
|     droppedVideoFrames: 3,
 | |
|     totalVideoFrames: 5
 | |
|   };
 | |
| 
 | |
|   tech.el_ = {
 | |
|     getVideoPlaybackQuality: () => videoPlaybackQuality
 | |
|   };
 | |
|   assert.deepEqual(
 | |
|     tech.getVideoPlaybackQuality(),
 | |
|     videoPlaybackQuality,
 | |
|     'uses native implementation when supported'
 | |
|   );
 | |
|   tech.el_ = {
 | |
|     webkitDroppedFrameCount: 1,
 | |
|     webkitDecodedFrameCount: 2
 | |
|   };
 | |
|   window.performance = {
 | |
|     now: () => 4
 | |
|   };
 | |
|   assert.deepEqual(
 | |
|     tech.getVideoPlaybackQuality(),
 | |
|     { droppedVideoFrames: 1, totalVideoFrames: 2, creationTime: 4 },
 | |
|     'uses webkit prefixed metrics and performance.now when supported'
 | |
|   );
 | |
| 
 | |
|   tech.el_ = {};
 | |
|   window.performance = void 0;
 | |
|   assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'empty object when not supported');
 | |
| 
 | |
|   window.performance = {
 | |
|     now: () => 7
 | |
|   };
 | |
|   assert.deepEqual(
 | |
|     tech.getVideoPlaybackQuality(),
 | |
|     { creationTime: 7 },
 | |
|     'only creation time when it\'s the only piece available'
 | |
|   );
 | |
| 
 | |
|   tech.el_ = {
 | |
|     getVideoPlaybackQuality: () => videoPlaybackQuality,
 | |
|     webkitDroppedFrameCount: 1,
 | |
|     webkitDecodedFrameCount: 2
 | |
|   };
 | |
|   assert.deepEqual(
 | |
|     tech.getVideoPlaybackQuality(),
 | |
|     videoPlaybackQuality,
 | |
|     'prefers native implementation when supported'
 | |
|   );
 | |
| 
 | |
|   tech.el_ = oldEl;
 | |
|   window.performance = origPerformance;
 | |
| });
 | |
| 
 | |
| QUnit.test('featuresVideoFrameCallback is false for audio elements', function(assert) {
 | |
|   const el = document.createElement('audio');
 | |
|   const audioTech = new Html5({
 | |
|     el,
 | |
|     source: [{src: 'https://example.org/stream.m3u8'}]
 | |
|   });
 | |
| 
 | |
|   assert.strictEqual(audioTech.featuresVideoFrameCallback, false, 'Html5 with audio element should not support rvf');
 | |
| 
 | |
|   audioTech.dispose();
 | |
| });
 | |
| 
 | |
| QUnit.test('featuresVideoFrameCallback is false for Safari DRM', function(assert) {
 | |
|   // Looking for `super.requestVideoFrameCallback()` being called
 | |
|   const spy = sinon.spy(Object.getPrototypeOf(Object.getPrototypeOf(tech)), 'requestVideoFrameCallback');
 | |
| 
 | |
|   tech.featuresVideoFrameCallback = true;
 | |
| 
 | |
|   try {
 | |
|     tech.el_.webkitKeys = {};
 | |
|     tech.requestVideoFrameCallback(function() {});
 | |
| 
 | |
|     assert.ok(spy.calledOnce, false, 'rvf fallback used');
 | |
|   } catch (e) {
 | |
|     // video.webkitKeys isn't writable on Safari, so relying on the mocked property on other browsers
 | |
|     assert.ok(true, 'skipped because webkitKeys not writable');
 | |
|   }
 | |
| });
 | |
| 
 | |
| QUnit.test('supportsFullScreen is always with `webkitEnterFullScreen`', function(assert) {
 | |
|   const oldEl = tech.el_;
 | |
| 
 | |
|   tech.el_ = {
 | |
|     webkitEnterFullScreen: () => {}
 | |
|   };
 | |
| 
 | |
|   assert.ok(tech.supportsFullScreen(), 'supportsFullScreen() true with webkitEnterFullScreen');
 | |
| 
 | |
|   tech.el_ = oldEl;
 | |
| });
 | |
| 
 | |
| QUnit.test('addSourceElement adds a valid source element with srcUrl and mimeType', function(assert) {
 | |
|   const videoEl = document.createElement('video');
 | |
| 
 | |
|   tech.el_ = videoEl;
 | |
| 
 | |
|   const srcUrl = 'http://example.com/video.mp4';
 | |
|   const mimeType = 'video/mp4';
 | |
| 
 | |
|   const added = tech.addSourceElement(srcUrl, mimeType);
 | |
|   const sourceElement = videoEl.querySelector('source');
 | |
| 
 | |
|   assert.ok(added, 'Returned true');
 | |
|   assert.ok(sourceElement, 'A source element was added');
 | |
|   assert.equal(sourceElement.src, srcUrl, 'Source element has correct src');
 | |
|   assert.equal(sourceElement.type, mimeType, 'Source element has correct type');
 | |
| });
 | |
| 
 | |
| QUnit.test('addSourceElement adds a valid source element without a mimeType', function(assert) {
 | |
|   const videoEl = document.createElement('video');
 | |
| 
 | |
|   tech.el_ = videoEl;
 | |
| 
 | |
|   const srcUrl = 'http://example.com/video2.mp4';
 | |
| 
 | |
|   const added = tech.addSourceElement(srcUrl);
 | |
|   const sourceElement = videoEl.querySelector('source');
 | |
| 
 | |
|   assert.ok(added, 'Returned true');
 | |
|   assert.ok(sourceElement, 'A source element was added even without a type');
 | |
|   assert.equal(sourceElement.src, srcUrl, 'Source element has correct src');
 | |
|   assert.notOk(sourceElement.type, 'Source element does not have a type attribute');
 | |
| });
 | |
| 
 | |
| QUnit.test('addSourceElement does not add a source element for invalid source URL', function(assert) {
 | |
|   const videoEl = document.createElement('video');
 | |
| 
 | |
|   tech.el_ = videoEl;
 | |
| 
 | |
|   const added = tech.addSourceElement('');
 | |
|   const sourceElement = videoEl.querySelector('source');
 | |
| 
 | |
|   assert.notOk(added, 'Returned false');
 | |
|   assert.notOk(sourceElement, 'No source element was added for missing src');
 | |
| });
 | |
| 
 | |
| QUnit.test('removeSourceElement removes a source element by its URL', function(assert) {
 | |
|   const videoEl = document.createElement('video');
 | |
| 
 | |
|   tech.el_ = videoEl;
 | |
| 
 | |
|   tech.addSourceElement('http://example.com/video1.mp4');
 | |
|   tech.addSourceElement('http://example.com/video2.mp4');
 | |
| 
 | |
|   let sourceElement = videoEl.querySelector('source[src="http://example.com/video1.mp4"]');
 | |
| 
 | |
|   assert.ok(sourceElement, 'Source element for video1.mp4 was added');
 | |
| 
 | |
|   const removed = tech.removeSourceElement('http://example.com/video1.mp4');
 | |
| 
 | |
|   assert.ok(removed, 'Source element was successfully removed');
 | |
|   sourceElement = videoEl.querySelector('source[src="http://example.com/video1.mp4"]');
 | |
|   assert.notOk(sourceElement, 'Source element for video1.mp4 was removed');
 | |
| });
 | |
| 
 | |
| QUnit.test('removeSourceElement does not remove a source element if URL does not match', function(assert) {
 | |
|   const videoEl = document.createElement('video');
 | |
| 
 | |
|   tech.el_ = videoEl;
 | |
| 
 | |
|   tech.addSourceElement('http://example.com/video.mp4');
 | |
| 
 | |
|   const removed = tech.removeSourceElement('http://example.com/invalid.mp4');
 | |
| 
 | |
|   assert.notOk(removed, 'No source element was removed for non-matching URL');
 | |
| });
 |