1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-23 02:04:34 +02:00
video.js/test/unit/tech/tech.test.js
2023-03-22 15:00:01 +01:00

777 lines
21 KiB
JavaScript

/* 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();
});