/* eslint-env qunit */ import Tech from '../../../src/js/tech/tech.js'; import Html5 from '../../../src/js/tech/html5.js'; import Flash from '../../../src/js/tech/flash.js'; import Button from '../../../src/js/button.js'; import { createTimeRange } from '../../../src/js/utils/time-ranges.js'; import extendFn from '../../../src/js/extend.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'; QUnit.module('Media Tech', { setup() { this.noop = function() {}; this.clock = sinon.useFakeTimers(); this.featuresProgessEvents = Tech.prototype.featuresProgessEvents; Tech.prototype.featuresProgressEvents = false; }, teardown() { this.clock.restore(); Tech.prototype.featuresProgessEvents = this.featuresProgessEvents; } }); QUnit.test('should synthesize timeupdate events by default', function() { let timeupdates = 0; const tech = new Tech(); tech.on('timeupdate', function() { timeupdates++; }); tech.trigger('play'); this.clock.tick(250); QUnit.equal(timeupdates, 1, 'triggered at least one timeupdate'); }); QUnit.test('stops manual timeupdates while paused', function() { let timeupdates = 0; const tech = new Tech(); tech.on('timeupdate', function() { timeupdates++; }); tech.trigger('play'); this.clock.tick(10 * 250); QUnit.ok(timeupdates > 0, 'timeupdates fire during playback'); tech.trigger('pause'); timeupdates = 0; this.clock.tick(10 * 250); QUnit.equal(timeupdates, 0, 'timeupdates do not fire when paused'); tech.trigger('play'); this.clock.tick(10 * 250); QUnit.ok(timeupdates > 0, 'timeupdates fire when playback resumes'); }); QUnit.test('should synthesize progress events by default', function() { 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); QUnit.equal(progresses, 0, 'waits until ready'); tech.trigger('ready'); this.clock.tick(500); QUnit.equal(progresses, 1, 'triggered one event'); tech.trigger('ready'); bufferedPercent = 0.75; this.clock.tick(500); QUnit.equal(progresses, 2, 'repeated readies are ok'); }); QUnit.test('dispose() should stop time tracking', function() { 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 QUnit.equal(e, undefined, 'threw an exception'); } QUnit.ok(true, 'no exception was thrown'); }); QUnit.test('dispose() should clear all tracks that are passed when its created', function() { const audioTracks = new AudioTrackList([new AudioTrack(), new AudioTrack()]); const videoTracks = new VideoTrackList([new VideoTrack(), new VideoTrack()]); const textTracks = new TextTrackList([new TextTrack({tech: {}}), new TextTrack({tech: {}})]); QUnit.equal(audioTracks.length, 2, 'should have two audio tracks at the start'); QUnit.equal(videoTracks.length, 2, 'should have two video tracks at the start'); QUnit.equal(textTracks.length, 2, 'should have two text tracks at the start'); const tech = new Tech({audioTracks, videoTracks, textTracks}); QUnit.equal(tech.videoTracks().length, videoTracks.length, 'should hold video tracks that we passed'); QUnit.equal(tech.audioTracks().length, audioTracks.length, 'should hold audio tracks that we passed'); QUnit.equal(tech.textTracks().length, textTracks.length, 'should hold text tracks that we passed'); tech.dispose(); QUnit.equal(audioTracks.length, 0, 'should have zero audio tracks after dispose'); QUnit.equal(videoTracks.length, 0, 'should have zero video tracks after dispose'); QUnit.equal(textTracks.length, 0, 'should have zero text tracks after dispose'); }); QUnit.test('dispose() should clear all tracks that are added after creation', function() { const tech = new Tech(); tech.addRemoteTextTrack({}); tech.addRemoteTextTrack({}); tech.audioTracks().addTrack_(new AudioTrack()); tech.audioTracks().addTrack_(new AudioTrack()); tech.videoTracks().addTrack_(new VideoTrack()); tech.videoTracks().addTrack_(new VideoTrack()); QUnit.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start'); QUnit.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start'); QUnit.equal(tech.textTracks().length, 2, 'should have two video tracks at the start'); QUnit.equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els'); QUnit.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); tech.dispose(); QUnit.equal(tech.audioTracks().length, 0, 'should have zero audio tracks after dispose'); QUnit.equal(tech.videoTracks().length, 0, 'should have zero video tracks after dispose'); QUnit.equal(tech.remoteTextTrackEls().length, 0, 'should have zero remote text tracks els'); QUnit.equal(tech.remoteTextTracks().length, 0, 'should have zero remote text tracks'); QUnit.equal(tech.textTracks().length, 0, 'should have zero video tracks after dispose'); }); QUnit.test('should add the source handler interface to a tech', function() { const sourceA = { src: 'foo.mp4', type: 'video/mp4' }; const sourceB = { src: 'no-support', type: 'no-support' }; // Define a new tech class const MyTech = extendFn(Tech); // Extend Tech with source handlers Tech.withSourceHandlers(MyTech); // Check for the expected class methods QUnit.ok(MyTech.registerSourceHandler, 'added a registerSourceHandler function to the Tech'); QUnit.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 QUnit.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; const HandlerInternalState = function() {}; HandlerInternalState.prototype.dispose = function() { disposeCalled = true; }; // Create source handlers const handlerOne = { canPlayType(type) { if (type !== 'no-support') { return 'probably'; } return ''; }, canHandleSource(source, options) { QUnit.strictEqual(tech.options_, options, 'tech options passed to canHandleSource'); if (source.type !== 'no-support') { return 'probably'; } return ''; }, handleSource(s, t, o) { QUnit.strictEqual(tech, t, 'tech instance passed to source handler'); QUnit.strictEqual(sourceA, s, 'tech instance passed to the source handler'); QUnit.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) { QUnit.ok(false, 'handlerTwo supports nothing and should never be called'); } }; // Test registering source handlers MyTech.registerSourceHandler(handlerOne); QUnit.strictEqual(MyTech.sourceHandlers[0], handlerOne, 'handlerOne was added to the source handler array'); MyTech.registerSourceHandler(handlerTwo, 0); QUnit.strictEqual(MyTech.sourceHandlers[0], handlerTwo, 'handlerTwo was registered at the correct index (0)'); // Test handler selection QUnit.strictEqual(MyTech.selectSourceHandler(sourceA, tech.options_), handlerOne, 'handlerOne was selected to handle the valid source'); QUnit.strictEqual(MyTech.selectSourceHandler(sourceB, tech.options_), null, 'no handler was selected to handle the invalid source'); // Test canPlayType return values QUnit.strictEqual(MyTech.canPlayType(sourceA.type), 'probably', 'the Tech returned probably for the valid source'); QUnit.strictEqual(MyTech.canPlayType(sourceB.type), '', 'the Tech returned an empty string for the invalid source'); // Test canPlaySource return values QUnit.strictEqual(MyTech.canPlaySource(sourceA, tech.options_), 'probably', 'the Tech returned probably for the valid source'); QUnit.strictEqual(MyTech.canPlaySource(sourceB, tech.options_), '', 'the Tech returned an empty string for the invalid source'); tech.addRemoteTextTrack({}); tech.addRemoteTextTrack({}); tech.audioTracks().addTrack_(new AudioTrack()); tech.audioTracks().addTrack_(new AudioTrack()); tech.videoTracks().addTrack_(new VideoTrack()); tech.videoTracks().addTrack_(new VideoTrack()); QUnit.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start'); QUnit.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start'); QUnit.equal(tech.textTracks().length, 2, 'should have two video tracks at the start'); QUnit.equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els'); QUnit.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 QUnit.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start'); QUnit.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start'); QUnit.equal(tech.textTracks().length, 2, 'should have two video tracks at the start'); QUnit.equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els'); QUnit.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); QUnit.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored'); QUnit.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored'); // Pass a second source tech.setSource(sourceA); QUnit.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored'); QUnit.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored'); // verify that all the tracks were removed as we got a new source QUnit.equal(tech.audioTracks().length, 0, 'should have zero audio tracks'); QUnit.equal(tech.videoTracks().length, 0, 'should have zero video tracks'); QUnit.equal(tech.textTracks().length, 2, 'should have two text tracks'); QUnit.equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els'); QUnit.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks'); // Check that the handler dipose method works QUnit.ok(disposeCalled, 'dispose has been called for the handler yet'); disposeCalled = false; tech.dispose(); QUnit.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() { // Define a new tech class const MyTech = extendFn(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(''); QUnit.ok(usedNative, 'native source handler was used when an unsupported source was set'); }); QUnit.test('should allow custom error events to be set', function() { const tech = new Tech(); const errors = []; tech.on('error', function() { errors.push(tech.error()); }); QUnit.equal(tech.error(), null, 'error is null by default'); tech.error(new MediaError(1)); QUnit.equal(errors.length, 1, 'triggered an error event'); QUnit.equal(errors[0].code, 1, 'set the proper code'); tech.error(2); QUnit.equal(errors.length, 2, 'triggered an error event'); QUnit.equal(errors[1].code, 2, 'wrapped the error code'); }); QUnit.test('should track whether a video has played', function() { const tech = new Tech(); QUnit.equal(tech.played().length, 0, 'starts with zero length'); tech.trigger('playing'); QUnit.equal(tech.played().length, 1, 'has length after playing'); }); QUnit.test('delegates seekable to the source handler', function() { const MyTech = extendFn(Tech, { seekable() { throw new Error('You should not be calling me!'); } }); Tech.withSourceHandlers(MyTech); let seekableCount = 0; const handler = { seekable() { seekableCount++; return createTimeRange(0, 0); } }; MyTech.registerSourceHandler({ canPlayType() { return true; }, canHandleSource() { return true; }, handleSource(source, tech, options) { return handler; } }); const tech = new MyTech(); tech.setSource({ src: 'example.mp4', type: 'video/mp4' }); tech.seekable(); QUnit.equal(seekableCount, 1, 'called the source handler'); }); QUnit.test('Tech.isTech returns correct answers for techs and components', function() { const isTech = Tech.isTech; QUnit.ok(isTech(Tech), 'Tech is a Tech'); QUnit.ok(isTech(Html5), 'Html5 is a Tech'); QUnit.ok(isTech(new Html5({}, {})), 'An html5 instance is a Tech'); QUnit.ok(isTech(Flash), 'Flash is a Tech'); QUnit.ok(!isTech(5), 'A number is not a Tech'); QUnit.ok(!isTech('this is a tech'), 'A string is not a Tech'); QUnit.ok(!isTech(Button), 'A Button is not a Tech'); QUnit.ok(!isTech(new Button({}, {})), 'A Button instance is not a Tech'); QUnit.ok(!isTech(isTech), 'A function is not a Tech'); }); QUnit.test('Tech#setSource clears currentSource_ after repeated loadstart', function() { let disposed = false; const MyTech = extendFn(Tech); Tech.withSourceHandlers(MyTech); const tech = new MyTech(); const sourceHandler = { canPlayType(type) { return true; }, canHandleSource(source, options) { return true; }, handleSource(source, tech_, options) { return { dispose() { disposed = true; } }; } }; // Test registering source handlers MyTech.registerSourceHandler(sourceHandler); // First loadstart tech.setSource('test'); tech.currentSource_ = 'test'; tech.trigger('loadstart'); QUnit.equal(tech.currentSource_, 'test', 'Current source is test'); // Second loadstart tech.trigger('loadstart'); QUnit.equal(tech.currentSource_, null, 'Current source is null'); QUnit.equal(disposed, true, 'disposed is true'); // Third loadstart tech.currentSource_ = 'test'; tech.trigger('loadstart'); QUnit.equal(tech.currentSource_, null, 'Current source is still null'); }); QUnit.test('setSource after tech dispose should dispose source handler once', function() { const MyTech = extendFn(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'); QUnit.equal(disposeCount, 0, 'did not call sourceHandler_ dispose for initial dispose'); tech.dispose(); QUnit.ok(!tech.sourceHandler_, 'sourceHandler should be unset'); QUnit.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'); QUnit.equal(disposeCount, 1, 'did not dispose after initial setSource'); tech.setSource('test'); QUnit.equal(disposeCount, 2, 'did dispose on second setSource'); }); QUnit.test('setSource after previous setSource should dispose source handler once', function() { const MyTech = extendFn(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'); QUnit.equal(disposeCount, 0, 'did not call dispose for initial setSource'); tech.setSource('test'); QUnit.equal(disposeCount, 1, 'did dispose for second setSource'); tech.setSource('test'); QUnit.equal(disposeCount, 2, 'did dispose for third setSource'); });