diff options
Diffstat (limited to 'javascript/videojs/test/unit/tech/tech.test.js')
| -rw-r--r-- | javascript/videojs/test/unit/tech/tech.test.js | 776 |
1 files changed, 776 insertions, 0 deletions
diff --git a/javascript/videojs/test/unit/tech/tech.test.js b/javascript/videojs/test/unit/tech/tech.test.js new file mode 100644 index 0000000..a64ca57 --- /dev/null +++ b/javascript/videojs/test/unit/tech/tech.test.js @@ -0,0 +1,776 @@ +/* eslint-env qunit */ +import Tech from '../../../src/js/tech/tech.js'; +import Html5 from '../../../src/js/tech/html5.js'; +import Button from '../../../src/js/button.js'; +import { createTimeRange } from '../../../src/js/utils/time.js'; +import MediaError from '../../../src/js/media-error.js'; +import AudioTrack from '../../../src/js/tracks/audio-track'; +import VideoTrack from '../../../src/js/tracks/video-track'; +import TextTrack from '../../../src/js/tracks/text-track'; +import AudioTrackList from '../../../src/js/tracks/audio-track-list'; +import VideoTrackList from '../../../src/js/tracks/video-track-list'; +import TextTrackList from '../../../src/js/tracks/text-track-list'; +import sinon from 'sinon'; +import log from '../../../src/js/utils/log.js'; +import TestHelpers from '../test-helpers.js'; + +function stubbedSourceHandler(handler) { + return { + canPlayType() { + return true; + }, + canHandleSource() { + return true; + }, + handleSource(source, tech, options) { + return handler; + } + }; +} + +QUnit.module('Media Tech', { + beforeEach(assert) { + this.noop = function() {}; + this.clock = sinon.useFakeTimers(); + this.featuresProgessEvents = Tech.prototype.featuresProgessEvents; + Tech.prototype.featuresProgressEvents = false; + }, + afterEach(assert) { + this.clock.restore(); + Tech.prototype.featuresProgressEvents = this.featuresProgessEvents; + } +}); + +QUnit.test('Tech.registerTech and Tech.getTech', function(assert) { + class MyTech extends Tech {} + const oldTechs = Tech.techs_; + const oldDefaultTechOrder = Tech.defaultTechOrder_; + + Tech.registerTech('MyTech', MyTech); + + assert.ok(Tech.techs_.MyTech, 'Tech is stored in the global list'); + assert.notEqual(Tech.defaultTechOrder_.indexOf('MyTech'), -1, 'Tech is stored in the defaultTechOrder array'); + assert.strictEqual(Tech.getTech('myTech'), MyTech, 'can get a tech using `camelCase` name'); + assert.strictEqual(Tech.getTech('MyTech'), MyTech, 'can get a tech using `titleCase` name'); + + // reset techs and defaultTechOrder + Tech.techs_ = oldTechs; + Tech.defaultTechOrder_ = oldDefaultTechOrder; +}); + +QUnit.test('should synthesize timeupdate events by default', function(assert) { + let timeupdates = 0; + const tech = new Tech(); + + tech.on('timeupdate', function() { + timeupdates++; + }); + + tech.trigger('play'); + + this.clock.tick(250); + assert.equal(timeupdates, 1, 'triggered at least one timeupdate'); + + tech.dispose(); +}); + +QUnit.test('stops manual timeupdates while paused', function(assert) { + let timeupdates = 0; + const tech = new Tech(); + + tech.on('timeupdate', function() { + timeupdates++; + }); + + tech.trigger('play'); + this.clock.tick(10 * 250); + assert.ok(timeupdates > 0, 'timeupdates fire during playback'); + + tech.trigger('pause'); + timeupdates = 0; + this.clock.tick(10 * 250); + assert.equal(timeupdates, 0, 'timeupdates do not fire when paused'); + + tech.trigger('play'); + this.clock.tick(10 * 250); + assert.ok(timeupdates > 0, 'timeupdates fire when playback resumes'); + tech.dispose(); +}); + +QUnit.test('should synthesize progress events by default', function(assert) { + let progresses = 0; + let bufferedPercent = 0.5; + const tech = new Tech(); + + tech.on('progress', function() { + progresses++; + }); + tech.bufferedPercent = function() { + return bufferedPercent; + }; + + this.clock.tick(500); + assert.equal(progresses, 0, 'waits until ready'); + + tech.trigger('ready'); + this.clock.tick(500); + assert.equal(progresses, 1, 'triggered one event'); + + tech.trigger('ready'); + bufferedPercent = 0.75; + this.clock.tick(500); + assert.equal(progresses, 2, 'repeated readies are ok'); + tech.dispose(); +}); + +QUnit.test('dispose() should stop time tracking', function(assert) { + const tech = new Tech(); + + tech.dispose(); + + // progress and timeupdate events will throw exceptions after the + // tech is disposed + try { + this.clock.tick(10 * 1000); + } catch (e) { + return assert.equal(e, undefined, 'threw an exception'); + } + assert.ok(true, 'no exception was thrown'); +}); + +QUnit.test('dispose() should clear all tracks that are passed when its created', function(assert) { + const audioTracks = new AudioTrackList([new AudioTrack(), new AudioTrack()]); + const videoTracks = new VideoTrackList([new VideoTrack(), new VideoTrack()]); + const pretech = new Tech(); + const textTracks = new TextTrackList([new TextTrack({tech: pretech}), + new TextTrack({tech: pretech})]); + + assert.equal(audioTracks.length, 2, 'should have two audio tracks at the start'); + assert.equal(videoTracks.length, 2, 'should have two video tracks at the start'); + assert.equal(textTracks.length, 2, 'should have two text tracks at the start'); + + const tech = new Tech({audioTracks, videoTracks, textTracks}); + + assert.equal( + tech.videoTracks().length, + videoTracks.length, + 'should hold video tracks that we passed' + ); + assert.equal( + tech.audioTracks().length, + audioTracks.length, + 'should hold audio tracks that we passed' + ); + assert.equal( + tech.textTracks().length, + textTracks.length, + 'should hold text tracks that we passed' + ); + + pretech.dispose(); + tech.dispose(); + + assert.equal(audioTracks.length, 0, 'should have zero audio tracks after dispose'); + assert.equal(videoTracks.length, 0, 'should have zero video tracks after dispose'); + assert.equal(textTracks.length, 0, 'should have zero text tracks after dispose'); +}); + +QUnit.test('dispose() should clear all tracks that are added after creation', function(assert) { + const tech = new Tech(); + + tech.addRemoteTextTrack({}, true); + tech.addRemoteTextTrack({}, true); + + tech.audioTracks().addTrack(new AudioTrack()); + tech.audioTracks().addTrack(new AudioTrack()); + + tech.videoTracks().addTrack(new VideoTrack()); + tech.videoTracks().addTrack(new VideoTrack()); + + assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start'); + assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start'); + assert.equal(tech.textTracks().length, 2, 'should have two text tracks at the start'); + assert.equal( + tech.remoteTextTrackEls().length, + 2, + 'should have two remote text tracks els' + ); + assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); + + tech.dispose(); + + assert.equal( + tech.audioTracks().length, + 0, + 'should have zero audio tracks after dispose' + ); + assert.equal( + tech.videoTracks().length, + 0, + 'should have zero video tracks after dispose' + ); + assert.equal( + tech.remoteTextTrackEls().length, + 0, + 'should have zero remote text tracks els' + ); + assert.equal(tech.remoteTextTracks().length, 0, 'should have zero remote text tracks'); + assert.equal(tech.textTracks().length, 0, 'should have zero video tracks after dispose'); +}); + +QUnit.test('switching sources should clear all remote tracks that are added with the default manualCleanup = false', function(assert) { + const oldLogWarn = log.warn; + + // Define a new tech class + class MyTech extends Tech {} + + // Create source handler + const handler = { + canPlayType: () => 'probably', + canHandleSource: () => 'probably', + handleSource: () => { + return { + dispose: () => {} + }; + } + }; + + // Extend Tech with source handlers + Tech.withSourceHandlers(MyTech); + + MyTech.registerSourceHandler(handler); + + const tech = new MyTech(); + + tech.triggerReady(); + + // set the initial source + tech.setSource({src: 'foo.mp4', type: 'mp4'}); + + // should not be automatically cleaned up when source changes + tech.addRemoteTextTrack({}, true); + // should be automatically cleaned up when source changes + tech.addRemoteTextTrack({}); + this.clock.tick(1); + + assert.equal(tech.textTracks().length, 2, 'should have two text tracks at the start'); + assert.equal( + tech.remoteTextTrackEls().length, + 2, + 'should have two remote text tracks els' + ); + assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); + assert.equal( + tech.autoRemoteTextTracks_.length, + 1, + 'should have one auto-cleanup remote text track' + ); + + // change source to force cleanup of auto remote text tracks + tech.setSource({src: 'bar.mp4', type: 'mp4'}); + this.clock.tick(1); + + assert.equal( + tech.textTracks().length, + 1, + 'should have one text track after source change' + ); + assert.equal( + tech.remoteTextTrackEls().length, + 1, + 'should have one remote remote text track els after source change' + ); + assert.equal( + tech.remoteTextTracks().length, + 1, + 'should have one remote text track after source change' + ); + assert.equal( + tech.autoRemoteTextTracks_.length, + 0, + 'should have zero auto-cleanup remote text tracks' + ); + + log.warn = oldLogWarn; + tech.dispose(); +}); + +QUnit.test('should add the source handler interface to a tech', function(assert) { + const sourceA = { src: 'foo.mp4', type: 'video/mp4' }; + const sourceB = { src: 'no-support', type: 'no-support' }; + + // Define a new tech class + class MyTech extends Tech {} + + // Extend Tech with source handlers + Tech.withSourceHandlers(MyTech); + + // Check for the expected class methods + assert.ok( + MyTech.registerSourceHandler, + 'added a registerSourceHandler function to the Tech' + ); + assert.ok( + MyTech.selectSourceHandler, + 'added a selectSourceHandler function to the Tech' + ); + + // Create an instance of Tech + const tech = new MyTech(); + + // Check for the expected instance methods + assert.ok(tech.setSource, 'added a setSource function to the tech instance'); + + // Create an internal state class for the source handler + // The internal class would be used by a source handler to maintain state + // and provde a dispose method for the handler. + // This is optional for source handlers + let disposeCalled = false; + + class HandlerInternalState { + dispose() { + disposeCalled = true; + } + } + + // Create source handlers + const handlerOne = { + canPlayType(type) { + if (type !== 'no-support') { + return 'probably'; + } + return ''; + }, + canHandleSource(source, options) { + assert.strictEqual( + tech.options_, + options, + 'tech options passed to canHandleSource' + ); + if (source.type !== 'no-support') { + return 'probably'; + } + return ''; + }, + handleSource(s, t, o) { + assert.strictEqual( + tech, + t, + 'tech instance passed to source handler' + ); + assert.strictEqual( + sourceA, + s, + 'tech instance passed to the source handler' + ); + assert.strictEqual( + tech.options_, + o, + 'tech options passed to the source handler handleSource' + ); + return new HandlerInternalState(); + } + }; + + const handlerTwo = { + canPlayType(type) { + // no support + return ''; + }, + canHandleSource(source, options) { + // no support + return ''; + }, + handleSource(source, tech_, options) { + assert.ok(false, 'handlerTwo supports nothing and should never be called'); + } + }; + + // Test registering source handlers + MyTech.registerSourceHandler(handlerOne); + assert.strictEqual( + MyTech.sourceHandlers[0], + handlerOne, + 'handlerOne was added to the source handler array' + ); + MyTech.registerSourceHandler(handlerTwo, 0); + assert.strictEqual( + MyTech.sourceHandlers[0], + handlerTwo, + 'handlerTwo was registered at the correct index (0)' + ); + + // Test handler selection + assert.strictEqual( + MyTech.selectSourceHandler(sourceA, tech.options_), + handlerOne, + 'handlerOne was selected to handle the valid source' + ); + assert.strictEqual( + MyTech.selectSourceHandler(sourceB, tech.options_), + null, + 'no handler was selected to handle the invalid source' + ); + + // Test canPlayType return values + assert.strictEqual( + MyTech.canPlayType(sourceA.type), + 'probably', + 'the Tech returned probably for the valid source' + ); + assert.strictEqual( + MyTech.canPlayType(sourceB.type), + '', + 'the Tech returned an empty string for the invalid source' + ); + + // Test canPlaySource return values + assert.strictEqual( + MyTech.canPlaySource(sourceA, tech.options_), + 'probably', + 'the Tech returned probably for the valid source' + ); + assert.strictEqual( + MyTech.canPlaySource(sourceB, tech.options_), + '', + 'the Tech returned an empty string for the invalid source' + ); + + tech.addRemoteTextTrack({}, true); + tech.addRemoteTextTrack({}, true); + + tech.audioTracks().addTrack(new AudioTrack()); + tech.audioTracks().addTrack(new AudioTrack()); + + tech.videoTracks().addTrack(new VideoTrack()); + tech.videoTracks().addTrack(new VideoTrack()); + + assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start'); + assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start'); + assert.equal(tech.textTracks().length, 2, 'should have two video tracks at the start'); + assert.equal( + tech.remoteTextTrackEls().length, + 2, + 'should have two remote text tracks els' + ); + assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); + + // Pass a source through the source handler process of a tech instance + tech.setSource(sourceA); + + // verify that the Tracks are still there + assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start'); + assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start'); + assert.equal(tech.textTracks().length, 2, 'should have two video tracks at the start'); + assert.equal( + tech.remoteTextTrackEls().length, + 2, + 'should have two remote text tracks els' + ); + assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); + + assert.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored'); + assert.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored'); + + // Pass a second source + tech.setSource(sourceA); + assert.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored'); + assert.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored'); + + // verify that all the tracks were removed as we got a new source + assert.equal(tech.audioTracks().length, 0, 'should have zero audio tracks'); + assert.equal(tech.videoTracks().length, 0, 'should have zero video tracks'); + assert.equal(tech.textTracks().length, 2, 'should have two text tracks'); + assert.equal( + tech.remoteTextTrackEls().length, + 2, + 'should have two remote text tracks els' + ); + assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); + + // Check that the handler dispose method works + assert.ok(disposeCalled, 'dispose has been called for the handler yet'); + disposeCalled = false; + tech.dispose(); + assert.ok( + disposeCalled, + 'the handler dispose method was called when the tech was disposed' + ); +}); + +QUnit.test('should handle unsupported sources with the source handler API', function(assert) { + // Define a new tech class + class MyTech extends Tech {} + + // Extend Tech with source handlers + Tech.withSourceHandlers(MyTech); + // Create an instance of Tech + const tech = new MyTech(); + let usedNative; + + MyTech.nativeSourceHandler = { + handleSource() { + usedNative = true; + } + }; + + tech.setSource(''); + assert.ok( + usedNative, + 'native source handler was used when an unsupported source was set' + ); + tech.dispose(); +}); + +QUnit.test('should allow custom error events to be set', function(assert) { + const tech = new Tech(); + const errors = []; + + tech.on('error', function() { + errors.push(tech.error()); + }); + + assert.equal(tech.error(), null, 'error is null by default'); + + tech.error(new MediaError(1)); + assert.equal(errors.length, 1, 'triggered an error event'); + assert.equal(errors[0].code, 1, 'set the proper code'); + + tech.error(2); + assert.equal(errors.length, 2, 'triggered an error event'); + assert.equal(errors[1].code, 2, 'wrapped the error code'); + tech.dispose(); +}); + +QUnit.test('should track whether a video has played', function(assert) { + const tech = new Tech(); + + assert.equal(tech.played().length, 0, 'starts with zero length'); + tech.trigger('playing'); + assert.equal(tech.played().length, 1, 'has length after playing'); + tech.dispose(); +}); + +QUnit.test('delegates deferrables to the source handler', function(assert) { + class MyTech extends Tech { + seekable() { + throw new Error('You should not be calling me!'); + } + seeking() { + throw new Error('You should not be calling me!'); + } + duration() { + throw new Error('You should not be calling me!'); + } + } + + Tech.withSourceHandlers(MyTech); + + let seekableCount = 0; + let seekingCount = 0; + let durationCount = 0; + const handler = { + seekable() { + seekableCount++; + return createTimeRange(0, 0); + }, + seeking() { + seekingCount++; + return false; + }, + duration() { + durationCount++; + return 0; + } + }; + + MyTech.registerSourceHandler(stubbedSourceHandler(handler)); + + const tech = new MyTech(); + + tech.setSource({ + src: 'example.mp4', + type: 'video/mp4' + }); + tech.seekable(); + tech.seeking(); + tech.duration(); + assert.equal(seekableCount, 1, 'called the source handler'); + assert.equal(seekingCount, 1, 'called the source handler'); + assert.equal(durationCount, 1, 'called the source handler'); + tech.dispose(); +}); + +QUnit.test('delegates only deferred deferrables to the source handler', function(assert) { + let seekingCount = 0; + + class MyTech extends Tech { + seekable() { + throw new Error('You should not be calling me!'); + } + seeking() { + seekingCount++; + return false; + } + duration() { + throw new Error('You should not be calling me!'); + } + } + + Tech.withSourceHandlers(MyTech); + + let seekableCount = 0; + let durationCount = 0; + const handler = { + seekable() { + seekableCount++; + return createTimeRange(0, 0); + }, + duration() { + durationCount++; + return 0; + } + }; + + MyTech.registerSourceHandler(stubbedSourceHandler(handler)); + + const tech = new MyTech(); + + tech.setSource({ + src: 'example.mp4', + type: 'video/mp4' + }); + tech.seekable(); + tech.seeking(); + tech.duration(); + assert.equal(seekableCount, 1, 'called the source handler'); + assert.equal(seekingCount, 1, 'called the tech itself'); + assert.equal(durationCount, 1, 'called the source handler'); + tech.dispose(); +}); + +QUnit.test('Tech.isTech returns correct answers for techs and components', function(assert) { + const isTech = Tech.isTech; + const tech = new Html5({}, {}); + const button = new Button(TestHelpers.makePlayer(), {}); + + assert.ok(isTech(Tech), 'Tech is a Tech'); + assert.ok(isTech(Html5), 'Html5 is a Tech'); + assert.ok(isTech(tech), 'An html5 instance is a Tech'); + assert.ok(!isTech(5), 'A number is not a Tech'); + assert.ok(!isTech('this is a tech'), 'A string is not a Tech'); + assert.ok(!isTech(Button), 'A Button is not a Tech'); + assert.ok(!isTech(button), 'A Button instance is not a Tech'); + assert.ok(!isTech(isTech), 'A function is not a Tech'); + + tech.dispose(); + button.dispose(); +}); + +QUnit.test('setSource after tech dispose should dispose source handler once', function(assert) { + class MyTech extends Tech {} + + Tech.withSourceHandlers(MyTech); + + let disposeCount = 0; + const handler = { + dispose() { + disposeCount++; + } + }; + + MyTech.registerSourceHandler({ + canPlayType() { + return true; + }, + canHandleSource() { + return true; + }, + handleSource(source, tech, options) { + return handler; + } + }); + + const tech = new MyTech(); + + tech.setSource('test'); + + assert.equal(disposeCount, 0, 'did not call sourceHandler_ dispose for initial dispose'); + tech.dispose(); + assert.ok(!tech.sourceHandler_, 'sourceHandler should be unset'); + assert.equal(disposeCount, 1, 'called the source handler dispose'); + + // this would normally be done above tech on src after dispose + tech.el_ = tech.createEl(); + + tech.setSource('test'); + assert.equal(disposeCount, 1, 'did not dispose after initial setSource'); + + tech.setSource('test'); + assert.equal(disposeCount, 2, 'did dispose on second setSource'); + tech.dispose(); + +}); + +QUnit.test('setSource after previous setSource should dispose source handler once', function(assert) { + class MyTech extends Tech {} + + Tech.withSourceHandlers(MyTech); + + let disposeCount = 0; + const handler = { + dispose() { + disposeCount++; + } + }; + + MyTech.registerSourceHandler({ + canPlayType() { + return true; + }, + canHandleSource() { + return true; + }, + handleSource(source, tech, options) { + return handler; + } + }); + + const tech = new MyTech(); + + tech.setSource('test'); + assert.equal(disposeCount, 0, 'did not call dispose for initial setSource'); + + tech.setSource('test'); + assert.equal(disposeCount, 1, 'did dispose for second setSource'); + + tech.setSource('test'); + assert.equal(disposeCount, 2, 'did dispose for third setSource'); + tech.dispose(); + +}); + +QUnit.test('returns an empty object for getVideoPlaybackQuality', function(assert) { + const tech = new Tech(); + + assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'returns an empty object'); + tech.dispose(); +}); + +QUnit.test('requestVideoFrameCallback waits if tech not ready', function(assert) { + const tech = new Tech(); + const cbSpy = sinon.spy(); + + tech.paused = sinon.spy(); + tech.isReady_ = false; + + tech.requestVideoFrameCallback(cbSpy); + + assert.notOk(tech.paused.called, 'paused not called on tech that is not ready'); + + tech.trigger('playing'); + + assert.ok(cbSpy.called, 'callback was called on tech playing'); + + tech.dispose(); +}); |
