/* 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'; QUnit.module('HTML5', { beforeEach(assert) { const el = document.createElement('div'); el.innerHTML = '
'; 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.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 verisons 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.IS_ANDROID = true; browser.IS_CHROME = true; browser.CHROME_VERSION = 50; assert.strictEqual(Html5.canControlPlaybackRate(), false, 'canControlPlaybackRate should return false on older Chrome'); browser.CHROME_VERSION = 58; assert.strictEqual(Html5.canControlPlaybackRate(), true, 'canControlPlaybackRate should return true on newer Chrome'); browser.IS_ANDROID = oldIsAndroid; browser.IS_CHROME = oldIsChrome; browser.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('patchCanPlayType patches canplaytype with our function, conditionally', function(assert) { // the patch runs automatically so we need to first unpatch Html5.unpatchCanPlayType(); const oldAV = browser.ANDROID_VERSION; const oldIsFirefox = browser.IS_FIREFOX; const oldIsChrome = browser.IS_CHROME; const video = document.createElement('video'); const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; browser.ANDROID_VERSION = 4.0; browser.IS_FIREFOX = false; browser.IS_CHROME = false; Html5.patchCanPlayType(); assert.notStrictEqual(video.canPlayType, canPlayType, 'original canPlayType and patched canPlayType should not be equal'); const patchedCanPlayType = video.canPlayType; const unpatchedCanPlayType = Html5.unpatchCanPlayType(); assert.strictEqual(canPlayType, Html5.TEST_VID.constructor.prototype.canPlayType, 'original canPlayType and unpatched canPlayType should be equal'); assert.strictEqual(patchedCanPlayType, unpatchedCanPlayType, 'patched canPlayType and function returned from unpatch are equal'); browser.ANDROID_VERSION = oldAV; browser.IS_FIREFOX = oldIsFirefox; browser.IS_CHROME = oldIsChrome; Html5.unpatchCanPlayType(); }); QUnit.test('patchCanPlayType doesn\'t patch canplaytype with our function in Chrome for Android', function(assert) { // the patch runs automatically so we need to first unpatch Html5.unpatchCanPlayType(); const oldAV = browser.ANDROID_VERSION; const oldIsChrome = browser.IS_CHROME; const oldIsFirefox = browser.IS_FIREFOX; const video = document.createElement('video'); const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; browser.ANDROID_VERSION = 4.0; browser.IS_CHROME = true; browser.IS_FIREFOX = false; Html5.patchCanPlayType(); assert.strictEqual(video.canPlayType, canPlayType, 'original canPlayType and patched canPlayType should be equal'); browser.ANDROID_VERSION = oldAV; browser.IS_CHROME = oldIsChrome; browser.IS_FIREFOX = oldIsFirefox; Html5.unpatchCanPlayType(); }); QUnit.test('patchCanPlayType doesn\'t patch canplaytype with our function in Firefox for Android', function(assert) { // the patch runs automatically so we need to first unpatch Html5.unpatchCanPlayType(); const oldAV = browser.ANDROID_VERSION; const oldIsFirefox = browser.IS_FIREFOX; const oldIsChrome = browser.IS_CHROME; const video = document.createElement('video'); const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType; browser.ANDROID_VERSION = 4.0; browser.IS_FIREFOX = true; browser.IS_CHROME = false; Html5.patchCanPlayType(); assert.strictEqual(video.canPlayType, canPlayType, 'original canPlayType and patched canPlayType should be equal'); browser.ANDROID_VERSION = oldAV; browser.IS_FIREFOX = oldIsFirefox; browser.IS_CHROME = oldIsChrome; Html5.unpatchCanPlayType(); }); QUnit.test('should return maybe for HLS urls on Android 4.0 or above when not Chrome or Firefox', function(assert) { const oldAV = browser.ANDROID_VERSION; const oldIsFirefox = browser.IS_FIREFOX; const oldIsChrome = browser.IS_CHROME; const video = document.createElement('video'); browser.ANDROID_VERSION = 4.0; browser.IS_FIREFOX = false; browser.IS_CHROME = false; Html5.patchCanPlayType(); assert.strictEqual(video.canPlayType('application/x-mpegurl'), 'maybe', 'android version 4.0 or above should be a maybe for x-mpegurl'); assert.strictEqual(video.canPlayType('application/x-mpegURL'), 'maybe', 'android version 4.0 or above should be a maybe for x-mpegURL'); assert.strictEqual(video.canPlayType('application/vnd.apple.mpegurl'), 'maybe', 'android version 4.0 or above should be a ' + 'maybe for vnd.apple.mpegurl'); assert.strictEqual(video.canPlayType('application/vnd.apple.mpegURL'), 'maybe', 'android version 4.0 or above should be a ' + 'maybe for vnd.apple.mpegurl'); browser.ANDROID_VERSION = oldAV; browser.IS_FIREFOX = oldIsFirefox; browser.IS_CHROME = oldIsChrome; Html5.unpatchCanPlayType(); }); QUnit.test('should return a maybe for mp4 on OLD ANDROID', function(assert) { const isOldAndroid = browser.IS_OLD_ANDROID; const video = document.createElement('video'); browser.IS_OLD_ANDROID = true; Html5.patchCanPlayType(); assert.strictEqual(video.canPlayType('video/mp4'), 'maybe', 'old android should return a maybe for video/mp4'); browser.IS_OLD_ANDROID = isOldAndroid; Html5.unpatchCanPlayType(); }); 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; /* eslint-disable no-unused-vars */ const htmlTech = new Html5({el}); /* eslint-enable no-unused-vars */ 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'); }); 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 }); /* eslint-disable no-unused-vars */ const htmlTech = new Html5({el, nativeTextTracks: false}); /* eslint-enable no-unused-vars */ assert.equal(events.length, 0, 'no listeners added'); /* eslint-disable no-unused-vars */ const htmlTechAlternate = new Html5({el, nativeCaptions: false}); /* eslint-enable no-unused-vars */ assert.equal(events.length, 0, 'no listeners added'); }); 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; /* eslint-disable no-unused-vars */ const htmlTech = new Html5({el}); /* eslint-enable no-unused-vars */ 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'); }); 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; /* eslint-disable no-unused-vars */ const htmlTech = new Html5({el, nativeAudioTracks: false}); /* eslint-enable no-unused-vars */ assert.equal(events.length, 0, 'no listeners added'); }); 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'); }); } 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; /* eslint-disable no-unused-vars */ const htmlTech = new Html5({el}); /* eslint-enable no-unused-vars */ 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'); }); 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; /* eslint-disable no-unused-vars */ const htmlTech = new Html5({el, nativeVideoTracks: false}); /* eslint-enable no-unused-vars */ assert.equal(events.length, 0, 'no listeners added'); }); 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 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.IS_ANDROID = true; browser.IS_CHROME = true; tech.el_ = { duration: Infinity, currentTime: 0 }; assert.ok(Number.isNaN(tech.duration()), 'returned NaN with currentTime 0'); browser.IS_ANDROID = oldIsAndroid; browser.IS_CHROME = oldIsChrome; tech.el_ = oldEl; }); QUnit.test('supports getting available media playback quality metrics', function(assert) { const origPerformance = window.performance; const origDate = window.Date; 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_ = { webkitDroppedFrameCount: 1, webkitDecodedFrameCount: 2 }; window.Date = { now: () => 10 }; window.performance = { timing: { navigationStart: 3 } }; assert.deepEqual(tech.getVideoPlaybackQuality(), { droppedVideoFrames: 1, totalVideoFrames: 2, creationTime: 7 }, 'uses webkit prefixed metrics and Date.now() - navigationStart when ' + 'supported'); tech.el_ = {}; window.performance = void 0; assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'empty object when not supported'); window.performance = { now: () => 5 }; assert.deepEqual(tech.getVideoPlaybackQuality(), { creationTime: 5 }, 'only creation time when it\'s the only piece available'); window.performance = { timing: { navigationStart: 3 } }; 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; window.Date = origDate; });