mirror of
https://github.com/videojs/video.js.git
synced 2024-12-25 02:42:10 +02:00
@BrandonOCasey added audio and video track support. closes #3173
This commit is contained in:
parent
18cdf08c0e
commit
2e2dbde4b4
@ -6,6 +6,7 @@ CHANGELOG
|
||||
* @gkatsev Use fonts 2.0 that do not require wrapping codepoints ([view](https://github.com/videojs/video.js/pull/3252))
|
||||
* @chrisauclair Make controls visible for accessibility reasons ([view](https://github.com/videojs/video.js/pull/3237))
|
||||
* @gkatsev updated text track documentation and crossorigin warning. Fixes #1888, #1958, #2628, #3202 ([view](https://github.com/videojs/video.js/pull/3256))
|
||||
* @BrandonOCasey added audio and video track support ([view](https://github.com/videojs/video.js/pull/3173))
|
||||
|
||||
--------------------
|
||||
|
||||
|
@ -22,6 +22,8 @@ import safeParseTuple from 'safe-json-parse/tuple';
|
||||
import assign from 'object.assign';
|
||||
import mergeOptions from './utils/merge-options.js';
|
||||
import textTrackConverter from './tracks/text-track-list-converter.js';
|
||||
import AudioTrackList from './tracks/audio-track-list.js';
|
||||
import VideoTrackList from './tracks/video-track-list.js';
|
||||
|
||||
// Include required child components (importing also registers them)
|
||||
import MediaLoader from './tech/loader.js';
|
||||
@ -555,7 +557,9 @@ class Player extends Component {
|
||||
'source': source,
|
||||
'playerId': this.id(),
|
||||
'techId': `${this.id()}_${techName}_api`,
|
||||
'videoTracks': this.videoTracks_,
|
||||
'textTracks': this.textTracks_,
|
||||
'audioTracks': this.audioTracks_,
|
||||
'autoplay': this.options_.autoplay,
|
||||
'preload': this.options_.preload,
|
||||
'loop': this.options_.loop,
|
||||
@ -648,7 +652,9 @@ class Player extends Component {
|
||||
*/
|
||||
unloadTech_() {
|
||||
// Save the current text tracks so that we can reuse the same text tracks with the next tech
|
||||
this.videoTracks_ = this.videoTracks();
|
||||
this.textTracks_ = this.textTracks();
|
||||
this.audioTracks_ = this.audioTracks();
|
||||
this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
|
||||
|
||||
this.isReady_ = false;
|
||||
@ -2509,12 +2515,48 @@ class Player extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Text tracks are tracks of timed text events.
|
||||
* Captions - text displayed over the video for the hearing impaired
|
||||
* Subtitles - text displayed over the video for those who don't understand language in the video
|
||||
* Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video
|
||||
* Descriptions - audio descriptions that are read back to the user by a screen reading device
|
||||
* Get a video track list
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
|
||||
*
|
||||
* @return {VideoTrackList} thes current video track list
|
||||
* @method videoTracks
|
||||
*/
|
||||
videoTracks() {
|
||||
// if we have not yet loadTech_, we create videoTracks_
|
||||
// these will be passed to the tech during loading
|
||||
if (!this.tech_) {
|
||||
this.videoTracks_ = this.videoTracks_ || new VideoTrackList();
|
||||
return this.videoTracks_;
|
||||
}
|
||||
|
||||
return this.tech_.videoTracks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an audio track list
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
|
||||
*
|
||||
* @return {AudioTrackList} thes current audio track list
|
||||
* @method audioTracks
|
||||
*/
|
||||
audioTracks() {
|
||||
// if we have not yet loadTech_, we create videoTracks_
|
||||
// these will be passed to the tech during loading
|
||||
if (!this.tech_) {
|
||||
this.audioTracks_ = this.audioTracks_ || new AudioTrackList();
|
||||
return this.audioTracks_;
|
||||
}
|
||||
|
||||
return this.tech_.audioTracks();
|
||||
}
|
||||
|
||||
/*
|
||||
* Text tracks are tracks of timed text events.
|
||||
* Captions - text displayed over the video for the hearing impaired
|
||||
* Subtitles - text displayed over the video for those who don't understand language in the video
|
||||
* Chapters - text displayed in a menu allowing the user to jump to particular points (chapters) in the video
|
||||
* Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get an array of associated text tracks. captions, subtitles, chapters, descriptions
|
||||
@ -2609,8 +2651,6 @@ class Player extends Component {
|
||||
// initialTime: function(){ return this.techCall_('initialTime'); },
|
||||
// startOffsetTime: function(){ return this.techCall_('startOffsetTime'); },
|
||||
// played: function(){ return this.techCall_('played'); },
|
||||
// videoTracks: function(){ return this.techCall_('videoTracks'); },
|
||||
// audioTracks: function(){ return this.techCall_('audioTracks'); },
|
||||
// defaultPlaybackRate: function(){ return this.techCall_('defaultPlaybackRate'); },
|
||||
// defaultMuted: function(){ return this.techCall_('defaultMuted'); }
|
||||
|
||||
|
@ -312,7 +312,7 @@ class Flash extends Tech {
|
||||
// Create setters and getters for attributes
|
||||
const _api = Flash.prototype;
|
||||
const _readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(',');
|
||||
const _readOnly = 'networkState,readyState,initialTime,duration,startOffsetTime,paused,ended,videoTracks,audioTracks,videoWidth,videoHeight'.split(',');
|
||||
const _readOnly = 'networkState,readyState,initialTime,duration,startOffsetTime,paused,ended,videoWidth,videoHeight'.split(',');
|
||||
|
||||
function _createSetter(attr){
|
||||
var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1);
|
||||
|
@ -16,6 +16,7 @@ import document from 'global/document';
|
||||
import window from 'global/window';
|
||||
import assign from 'object.assign';
|
||||
import mergeOptions from '../utils/merge-options.js';
|
||||
import toTitleCase from '../utils/to-title-case.js';
|
||||
|
||||
/**
|
||||
* HTML5 Media Controller - Wrapper for HTML5 Media API
|
||||
@ -78,6 +79,24 @@ class Html5 extends Tech {
|
||||
}
|
||||
}
|
||||
|
||||
let trackTypes = ['audio', 'video'];
|
||||
|
||||
// ProxyNativeTextTracks
|
||||
trackTypes.forEach((type) => {
|
||||
let capitalType = toTitleCase(type);
|
||||
|
||||
if (!this[`featuresNative${capitalType}Tracks`]) {
|
||||
return;
|
||||
}
|
||||
let tl = this.el()[`${type}Tracks`];
|
||||
|
||||
if (tl && tl.addEventListener) {
|
||||
tl.addEventListener('change', Fn.bind(this, this[`handle${capitalType}TrackChange_`]));
|
||||
tl.addEventListener('addtrack', Fn.bind(this, this[`handle${capitalType}TrackAdd_`]));
|
||||
tl.addEventListener('removetrack', Fn.bind(this, this[`handle${capitalType}TrackRemove_`]));
|
||||
}
|
||||
});
|
||||
|
||||
if (this.featuresNativeTextTracks) {
|
||||
if (crossoriginTracks) {
|
||||
log.warn(tsml`Text Tracks are being loaded from another origin but the crossorigin attribute isn't used.
|
||||
@ -109,25 +128,20 @@ class Html5 extends Tech {
|
||||
* @method dispose
|
||||
*/
|
||||
dispose() {
|
||||
let tt = this.el().textTracks;
|
||||
let emulatedTt = this.textTracks();
|
||||
|
||||
// remove native event listeners
|
||||
if (tt && tt.removeEventListener) {
|
||||
tt.removeEventListener('change', this.handleTextTrackChange_);
|
||||
tt.removeEventListener('addtrack', this.handleTextTrackAdd_);
|
||||
tt.removeEventListener('removetrack', this.handleTextTrackRemove_);
|
||||
}
|
||||
|
||||
// clearout the emulated text track list.
|
||||
let i = emulatedTt.length;
|
||||
|
||||
while (i--) {
|
||||
emulatedTt.removeTrack_(emulatedTt[i]);
|
||||
}
|
||||
// Un-ProxyNativeTracks
|
||||
['audio', 'video', 'text'].forEach((type) => {
|
||||
let capitalType = toTitleCase(type);
|
||||
let tl = this.el_[`${type}Tracks`];
|
||||
|
||||
if (tl && tl.removeEventListener) {
|
||||
tl.removeEventListener('change', this[`handle${capitalType}TrackChange_`]);
|
||||
tl.removeEventListener('addtrack', this[`handle${capitalType}TrackAdd_`]);
|
||||
tl.removeEventListener('removetrack', this[`handle${capitalType}TrackRemove_`]);
|
||||
}
|
||||
});
|
||||
|
||||
Html5.disposeMediaElement(this.el_);
|
||||
// tech will handle clearing of the emulated track list
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -303,6 +317,43 @@ class Html5 extends Tech {
|
||||
this.textTracks().removeTrack_(e.track);
|
||||
}
|
||||
|
||||
handleVideoTrackChange_(e) {
|
||||
let vt = this.videoTracks();
|
||||
this.videoTracks().trigger({
|
||||
type: 'change',
|
||||
target: vt,
|
||||
currentTarget: vt,
|
||||
srcElement: vt
|
||||
});
|
||||
}
|
||||
|
||||
handleVideoTrackAdd_(e) {
|
||||
this.videoTracks().addTrack_(e.track);
|
||||
}
|
||||
|
||||
handleVideoTrackRemove_(e) {
|
||||
this.videoTracks().removeTrack_(e.track);
|
||||
}
|
||||
|
||||
handleAudioTrackChange_(e) {
|
||||
let audioTrackList = this.audioTracks();
|
||||
this.audioTracks().trigger({
|
||||
type: 'change',
|
||||
target: audioTrackList,
|
||||
currentTarget: audioTrackList,
|
||||
srcElement: audioTrackList
|
||||
});
|
||||
}
|
||||
|
||||
handleAudioTrackAdd_(e) {
|
||||
this.audioTracks().addTrack_(e.track);
|
||||
}
|
||||
|
||||
handleAudioTrackRemove_(e) {
|
||||
this.audioTracks().removeTrack_(e.track);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Play for html5 tech
|
||||
*
|
||||
@ -989,6 +1040,27 @@ Html5.supportsNativeTextTracks = function() {
|
||||
return supportsTextTracks;
|
||||
};
|
||||
|
||||
/*
|
||||
* Check to see if native video tracks are supported by this browser/device
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Html5.supportsNativeVideoTracks = function() {
|
||||
let supportsVideoTracks = !!Html5.TEST_VID.videoTracks;
|
||||
return supportsVideoTracks;
|
||||
};
|
||||
|
||||
/*
|
||||
* Check to see if native audio tracks are supported by this browser/device
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Html5.supportsNativeAudioTracks = function() {
|
||||
let supportsAudioTracks = !!Html5.TEST_VID.audioTracks;
|
||||
return supportsAudioTracks;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An array of events available on the Html5 tech.
|
||||
*
|
||||
@ -1062,6 +1134,21 @@ Html5.prototype['featuresProgressEvents'] = true;
|
||||
*/
|
||||
Html5.prototype['featuresNativeTextTracks'] = Html5.supportsNativeTextTracks();
|
||||
|
||||
/**
|
||||
* Sets the tech's status on native text track support
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Html5.prototype['featuresNativeVideoTracks'] = Html5.supportsNativeVideoTracks();
|
||||
|
||||
/**
|
||||
* Sets the tech's status on native audio track support
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
Html5.prototype['featuresNativeAudioTracks'] = Html5.supportsNativeAudioTracks();
|
||||
|
||||
|
||||
// HTML5 Feature detection and Device Fixes --------------------------------- //
|
||||
let canPlayType;
|
||||
const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;
|
||||
|
@ -10,6 +10,10 @@ import HTMLTrackElementList from '../tracks/html-track-element-list';
|
||||
import mergeOptions from '../utils/merge-options.js';
|
||||
import TextTrack from '../tracks/text-track';
|
||||
import TextTrackList from '../tracks/text-track-list';
|
||||
import VideoTrack from '../tracks/video-track';
|
||||
import VideoTrackList from '../tracks/video-track-list';
|
||||
import AudioTrackList from '../tracks/audio-track-list';
|
||||
import AudioTrack from '../tracks/audio-track';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import log from '../utils/log.js';
|
||||
import { createTimeRange } from '../utils/time-ranges.js';
|
||||
@ -45,6 +49,8 @@ class Tech extends Component {
|
||||
});
|
||||
|
||||
this.textTracks_ = options.textTracks;
|
||||
this.videoTracks_ = options.videoTracks;
|
||||
this.audioTracks_ = options.audioTracks;
|
||||
|
||||
// Manually track progress in cases where the browser/flash player doesn't report it.
|
||||
if (!this.featuresProgressEvents) {
|
||||
@ -65,6 +71,7 @@ class Tech extends Component {
|
||||
}
|
||||
|
||||
this.initTextTrackListeners();
|
||||
this.initTrackListeners();
|
||||
|
||||
// Turn on component tap events
|
||||
this.emitTapEvents();
|
||||
@ -218,15 +225,9 @@ class Tech extends Component {
|
||||
* @method dispose
|
||||
*/
|
||||
dispose() {
|
||||
// clear out text tracks because we can't reuse them between techs
|
||||
let textTracks = this.textTracks();
|
||||
|
||||
if (textTracks) {
|
||||
let i = textTracks.length;
|
||||
while(i--) {
|
||||
this.removeRemoteTextTrack(textTracks[i]);
|
||||
}
|
||||
}
|
||||
// clear out all tracks because we can't reuse them between techs
|
||||
this.clearTracks(['audio', 'video', 'text']);
|
||||
|
||||
// Turn off any manual progress or timeupdate tracking
|
||||
if (this.manualProgress) { this.manualProgressOff(); }
|
||||
@ -236,6 +237,33 @@ class Tech extends Component {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* clear out a track list, or multiple track lists
|
||||
*
|
||||
* Note: Techs without source handlers should call this between
|
||||
* sources for video & audio tracks, as usually you don't want
|
||||
* to use them between tracks and we have no automatic way to do
|
||||
* it for you
|
||||
*
|
||||
* @method clearTracks
|
||||
* @param {Array|String} types type(s) of track lists to empty
|
||||
*/
|
||||
clearTracks(types) {
|
||||
types = [].concat(types);
|
||||
// clear out all tracks because we can't reuse them between techs
|
||||
types.forEach((type) => {
|
||||
let list = this[`${type}Tracks`]() || [];
|
||||
let i = list.length;
|
||||
while (i--) {
|
||||
let track = list[i];
|
||||
if (type === 'text') {
|
||||
this.removeRemoteTextTrack(track);
|
||||
}
|
||||
list.removeTrack_(track);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the tech. Removes all sources and resets readyState.
|
||||
*
|
||||
@ -313,6 +341,32 @@ class Tech extends Component {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize audio and video track listeners
|
||||
*
|
||||
* @method initTrackListeners
|
||||
*/
|
||||
initTrackListeners() {
|
||||
const trackTypes = ['video', 'audio'];
|
||||
|
||||
trackTypes.forEach((type) => {
|
||||
let trackListChanges = () => {
|
||||
this.trigger(`${type}trackchange`);
|
||||
};
|
||||
|
||||
let tracks = this[`${type}Tracks`]();
|
||||
|
||||
tracks.addEventListener('removetrack', trackListChanges);
|
||||
tracks.addEventListener('addtrack', trackListChanges);
|
||||
|
||||
this.on('dispose', () => {
|
||||
tracks.removeEventListener('removetrack', trackListChanges);
|
||||
tracks.removeEventListener('addtrack', trackListChanges);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulate texttracks
|
||||
*
|
||||
@ -337,8 +391,10 @@ class Tech extends Component {
|
||||
script.onload = null;
|
||||
script.onerror = null;
|
||||
});
|
||||
this.el().parentNode.appendChild(script);
|
||||
// but have not loaded yet and we set it to true before the inject so that
|
||||
// we don't overwrite the injected window.WebVTT if it loads right away
|
||||
window['WebVTT'] = true;
|
||||
this.el().parentNode.appendChild(script);
|
||||
}
|
||||
|
||||
let updateDisplay = () => this.trigger('texttrackchange');
|
||||
@ -362,6 +418,28 @@ class Tech extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get videotracks
|
||||
*
|
||||
* @returns {VideoTrackList}
|
||||
* @method videoTracks
|
||||
*/
|
||||
videoTracks() {
|
||||
this.videoTracks_ = this.videoTracks_ || new VideoTrackList();
|
||||
return this.videoTracks_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audiotracklist
|
||||
*
|
||||
* @returns {AudioTrackList}
|
||||
* @method audioTracks
|
||||
*/
|
||||
audioTracks() {
|
||||
this.audioTracks_ = this.audioTracks_ || new AudioTrackList();
|
||||
return this.audioTracks_;
|
||||
}
|
||||
|
||||
/*
|
||||
* Provide default methods for text tracks.
|
||||
*
|
||||
@ -536,14 +614,31 @@ class Tech extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* List of associated text tracks
|
||||
*
|
||||
* @type {Array}
|
||||
* @type {TextTrackList}
|
||||
* @private
|
||||
*/
|
||||
Tech.prototype.textTracks_;
|
||||
|
||||
/**
|
||||
* List of associated audio tracks
|
||||
*
|
||||
* @type {AudioTrackList}
|
||||
* @private
|
||||
*/
|
||||
Tech.prototype.audioTracks_;
|
||||
|
||||
/**
|
||||
* List of associated video tracks
|
||||
*
|
||||
* @type {VideoTrackList}
|
||||
* @private
|
||||
*/
|
||||
Tech.prototype.videoTracks_;
|
||||
|
||||
|
||||
var createTrackHelper = function(self, kind, label, language, options={}) {
|
||||
let tracks = self.textTracks();
|
||||
|
||||
@ -713,6 +808,12 @@ Tech.withSourceHandlers = function(_Tech){
|
||||
this.disposeSourceHandler();
|
||||
this.off('dispose', this.disposeSourceHandler);
|
||||
|
||||
// if we have a source and get another one
|
||||
// then we are loading something new
|
||||
// than clear all of our current tracks
|
||||
if (this.currentSource_) {
|
||||
this.clearTracks(['audio', 'video']);
|
||||
}
|
||||
this.currentSource_ = source;
|
||||
this.sourceHandler_ = sh.handleSource(source, this, this.options_);
|
||||
this.on('dispose', this.disposeSourceHandler);
|
||||
|
113
src/js/tracks/audio-track-list.js
Normal file
113
src/js/tracks/audio-track-list.js
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @file audio-track-list.js
|
||||
*/
|
||||
import TrackList from './track-list';
|
||||
import * as browser from '../utils/browser.js';
|
||||
import document from 'global/document';
|
||||
|
||||
/**
|
||||
* anywhere we call this function we diverge from the spec
|
||||
* as we only support one enabled audiotrack at a time
|
||||
*
|
||||
* @param {Array|AudioTrackList} list list to work on
|
||||
* @param {AudioTrack} track the track to skip
|
||||
*/
|
||||
const disableOthers = function(list, track) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (track.id === list[i].id) {
|
||||
continue;
|
||||
}
|
||||
// another audio track is enabled, disable it
|
||||
list[i].enabled = false;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* A list of possible audio tracks. All functionality is in the
|
||||
* base class Tracklist and the spec for AudioTrackList is located at:
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
|
||||
*
|
||||
* interface AudioTrackList : EventTarget {
|
||||
* readonly attribute unsigned long length;
|
||||
* getter AudioTrack (unsigned long index);
|
||||
* AudioTrack? getTrackById(DOMString id);
|
||||
*
|
||||
* attribute EventHandler onchange;
|
||||
* attribute EventHandler onaddtrack;
|
||||
* attribute EventHandler onremovetrack;
|
||||
* };
|
||||
*
|
||||
* @param {AudioTrack[]} tracks a list of audio tracks to instantiate the list with
|
||||
* @extends TrackList
|
||||
* @class AudioTrackList
|
||||
*/
|
||||
class AudioTrackList extends TrackList {
|
||||
constructor(tracks = []) {
|
||||
let list;
|
||||
|
||||
// make sure only 1 track is enabled
|
||||
// sorted from last index to first index
|
||||
for (let i = tracks.length - 1; i >= 0; i--) {
|
||||
if (tracks[i].enabled) {
|
||||
disableOthers(tracks, tracks[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// IE8 forces us to implement inheritance ourselves
|
||||
// as it does not support Object.defineProperty properly
|
||||
if (browser.IS_IE8) {
|
||||
list = document.createElement('custom');
|
||||
for (let prop in TrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = TrackList.prototype[prop];
|
||||
}
|
||||
}
|
||||
for (let prop in AudioTrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = AudioTrackList.prototype[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list = super(tracks, list);
|
||||
list.changing_ = false;
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
addTrack_(track) {
|
||||
if (track.enabled) {
|
||||
disableOthers(this, track);
|
||||
}
|
||||
|
||||
super.addTrack_(track);
|
||||
// native tracks don't have this
|
||||
if (!track.addEventListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
track.addEventListener('enabledchange', () => {
|
||||
// when we are disabling other tracks (since we don't support
|
||||
// more than one track at a time) we will set changing_
|
||||
// to true so that we don't trigger additional change events
|
||||
if (this.changing_) {
|
||||
return;
|
||||
}
|
||||
this.changing_ = true;
|
||||
disableOthers(this, track);
|
||||
this.changing_ = false;
|
||||
this.trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
addTrack(track) {
|
||||
this.addTrack_(track);
|
||||
}
|
||||
|
||||
removeTrack(track) {
|
||||
super.removeTrack_(track);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AudioTrackList;
|
63
src/js/tracks/audio-track.js
Normal file
63
src/js/tracks/audio-track.js
Normal file
@ -0,0 +1,63 @@
|
||||
import {AudioTrackKind} from './track-enums';
|
||||
import Track from './track';
|
||||
import merge from '../utils/merge-options';
|
||||
import * as browser from '../utils/browser.js';
|
||||
|
||||
/**
|
||||
* A single audio text track as defined in:
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack
|
||||
*
|
||||
* interface AudioTrack {
|
||||
* readonly attribute DOMString id;
|
||||
* readonly attribute DOMString kind;
|
||||
* readonly attribute DOMString label;
|
||||
* readonly attribute DOMString language;
|
||||
* attribute boolean enabled;
|
||||
* };
|
||||
*
|
||||
* @param {Object=} options Object of option names and values
|
||||
* @class AudioTrack
|
||||
*/
|
||||
class AudioTrack extends Track {
|
||||
constructor(options = {}) {
|
||||
let settings = merge(options, {
|
||||
kind: AudioTrackKind[options.kind] || ''
|
||||
});
|
||||
// on IE8 this will be a document element
|
||||
// for every other browser this will be a normal object
|
||||
let track = super(settings);
|
||||
let enabled = false;
|
||||
|
||||
if (browser.IS_IE8) {
|
||||
for (let prop in AudioTrack.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
track[prop] = AudioTrack.prototype[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(track, 'enabled', {
|
||||
get() { return enabled; },
|
||||
set(newEnabled) {
|
||||
// an invalid or unchanged value
|
||||
if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
|
||||
return;
|
||||
}
|
||||
enabled = newEnabled;
|
||||
this.trigger('enabledchange');
|
||||
}
|
||||
});
|
||||
|
||||
// if the user sets this track to selected then
|
||||
// set selected to that true value otherwise
|
||||
// we keep it false
|
||||
if (settings.enabled) {
|
||||
track.enabled = settings.enabled;
|
||||
}
|
||||
track.loaded_ = true;
|
||||
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioTrack;
|
@ -1,39 +0,0 @@
|
||||
/**
|
||||
* @file text-track-enums.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
|
||||
*
|
||||
* enum TextTrackMode { "disabled", "hidden", "showing" };
|
||||
*/
|
||||
const TextTrackMode = {
|
||||
disabled: 'disabled',
|
||||
hidden: 'hidden',
|
||||
showing: 'showing'
|
||||
};
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackkind
|
||||
*
|
||||
* enum TextTrackKind {
|
||||
* "subtitles",
|
||||
* "captions",
|
||||
* "descriptions",
|
||||
* "chapters",
|
||||
* "metadata"
|
||||
* };
|
||||
*/
|
||||
const TextTrackKind = {
|
||||
subtitles: 'subtitles',
|
||||
captions: 'captions',
|
||||
descriptions: 'descriptions',
|
||||
chapters: 'chapters',
|
||||
metadata: 'metadata'
|
||||
};
|
||||
|
||||
/* jshint ignore:start */
|
||||
// we ignore jshint here because it does not see
|
||||
// TextTrackMode or TextTrackKind as defined here somehow...
|
||||
export { TextTrackMode, TextTrackKind };
|
||||
/* jshint ignore:end */
|
@ -1,14 +1,15 @@
|
||||
/**
|
||||
* @file text-track-list.js
|
||||
*/
|
||||
import EventTarget from '../event-target';
|
||||
import TrackList from './track-list';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import * as browser from '../utils/browser.js';
|
||||
import document from 'global/document';
|
||||
|
||||
/**
|
||||
* A text track list as defined in:
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist
|
||||
* A list of possible text tracks. All functionality is in the
|
||||
* base class TrackList. The spec for TextTrackList is located at:
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist
|
||||
*
|
||||
* interface TextTrackList : EventTarget {
|
||||
* readonly attribute unsigned long length;
|
||||
@ -20,19 +21,23 @@ import document from 'global/document';
|
||||
* attribute EventHandler onremovetrack;
|
||||
* };
|
||||
*
|
||||
* @param {Track[]} tracks A list of tracks to initialize the list with
|
||||
* @extends EventTarget
|
||||
* @param {TextTrack[]} tracks A list of tracks to initialize the list with
|
||||
* @extends TrackList
|
||||
* @class TextTrackList
|
||||
*/
|
||||
|
||||
class TextTrackList extends EventTarget {
|
||||
class TextTrackList extends TrackList {
|
||||
constructor(tracks = []) {
|
||||
super();
|
||||
let list = this;
|
||||
let list;
|
||||
|
||||
// IE8 forces us to implement inheritance ourselves
|
||||
// as it does not support Object.defineProperty properly
|
||||
if (browser.IS_IE8) {
|
||||
list = document.createElement('custom');
|
||||
|
||||
for (let prop in TrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = TrackList.prototype[prop];
|
||||
}
|
||||
}
|
||||
for (let prop in TextTrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = TextTrackList.prototype[prop];
|
||||
@ -40,54 +45,15 @@ class TextTrackList extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
list.tracks_ = [];
|
||||
|
||||
Object.defineProperty(list, 'length', {
|
||||
get() {
|
||||
return this.tracks_.length;
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
list.addTrack_(tracks[i]);
|
||||
}
|
||||
|
||||
if (browser.IS_IE8) {
|
||||
return list;
|
||||
}
|
||||
list = super(tracks, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add TextTrack from TextTrackList
|
||||
*
|
||||
* @param {TextTrack} track
|
||||
* @method addTrack_
|
||||
* @private
|
||||
*/
|
||||
addTrack_(track) {
|
||||
let index = this.tracks_.length;
|
||||
|
||||
if (!('' + index in this)) {
|
||||
Object.defineProperty(this, index, {
|
||||
get() {
|
||||
return this.tracks_[index];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
super.addTrack_(track);
|
||||
track.addEventListener('modechange', Fn.bind(this, function() {
|
||||
this.trigger('change');
|
||||
}));
|
||||
|
||||
// Do not add duplicate tracks
|
||||
if (this.tracks_.indexOf(track) === -1) {
|
||||
this.tracks_.push(track);
|
||||
this.trigger({
|
||||
track,
|
||||
type: 'addtrack'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,21 +113,4 @@ class TextTrackList extends EventTarget {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change - One or more tracks in the track list have been enabled or disabled.
|
||||
* addtrack - A track has been added to the track list.
|
||||
* removetrack - A track has been removed from the track list.
|
||||
*/
|
||||
TextTrackList.prototype.allowedEvents_ = {
|
||||
change: 'change',
|
||||
addtrack: 'addtrack',
|
||||
removetrack: 'removetrack'
|
||||
};
|
||||
|
||||
// emulate attribute EventHandler support to allow for feature detection
|
||||
for (let event in TextTrackList.prototype.allowedEvents_) {
|
||||
TextTrackList.prototype['on' + event] = null;
|
||||
}
|
||||
|
||||
export default TextTrackList;
|
||||
|
@ -3,15 +3,15 @@
|
||||
*/
|
||||
import TextTrackCueList from './text-track-cue-list';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import * as Guid from '../utils/guid.js';
|
||||
import * as browser from '../utils/browser.js';
|
||||
import * as TextTrackEnum from './text-track-enums';
|
||||
import {TextTrackKind, TextTrackMode} from './track-enums';
|
||||
import log from '../utils/log.js';
|
||||
import EventTarget from '../event-target';
|
||||
import document from 'global/document';
|
||||
import window from 'global/window';
|
||||
import Track from './track.js';
|
||||
import { isCrossOrigin } from '../utils/url.js';
|
||||
import XHR from 'xhr';
|
||||
import merge from '../utils/merge-options';
|
||||
import * as browser from '../utils/browser.js';
|
||||
|
||||
/**
|
||||
* takes a webvtt file contents and parses it into cues
|
||||
@ -54,7 +54,6 @@ const parseCues = function(srcContent, track) {
|
||||
parser.flush();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* load a track from a specifed url
|
||||
*
|
||||
@ -99,7 +98,7 @@ const loadTrack = function(src, track) {
|
||||
|
||||
/**
|
||||
* A single text track as defined in:
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack
|
||||
*
|
||||
* interface TextTrack : EventTarget {
|
||||
* readonly attribute TextTrackKind kind;
|
||||
@ -121,21 +120,31 @@ const loadTrack = function(src, track) {
|
||||
* };
|
||||
*
|
||||
* @param {Object=} options Object of option names and values
|
||||
* @extends EventTarget
|
||||
* @extends Track
|
||||
* @class TextTrack
|
||||
*/
|
||||
class TextTrack extends EventTarget {
|
||||
class TextTrack extends Track {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
if (!options.tech) {
|
||||
throw new Error('A tech was not provided.');
|
||||
}
|
||||
|
||||
let tt = this;
|
||||
let settings = merge(options, {
|
||||
kind: TextTrackKind[options.kind] || 'subtitles',
|
||||
language: options.language || options.srclang || ''
|
||||
});
|
||||
let mode = TextTrackMode[settings.mode] || 'disabled';
|
||||
let default_ = settings.default;
|
||||
|
||||
if (settings.kind === 'metadata' || settings.kind === 'chapters') {
|
||||
mode = 'hidden';
|
||||
}
|
||||
// on IE8 this will be a document element
|
||||
// for every other browser this will be a normal object
|
||||
let tt = super(settings);
|
||||
tt.tech_ = settings.tech;
|
||||
|
||||
if (browser.IS_IE8) {
|
||||
tt = document.createElement('custom');
|
||||
|
||||
for (let prop in TextTrack.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
tt[prop] = TextTrack.prototype[prop];
|
||||
@ -143,19 +152,6 @@ class TextTrack extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
tt.tech_ = options.tech;
|
||||
|
||||
let mode = TextTrackEnum.TextTrackMode[options.mode] || 'disabled';
|
||||
let kind = TextTrackEnum.TextTrackKind[options.kind] || 'subtitles';
|
||||
let default_ = options.default;
|
||||
let label = options.label || '';
|
||||
let language = options.language || options.srclang || '';
|
||||
let id = options.id || 'vjs_text_track_' + Guid.newGUID();
|
||||
|
||||
if (kind === 'metadata' || kind === 'chapters') {
|
||||
mode = 'hidden';
|
||||
}
|
||||
|
||||
tt.cues_ = [];
|
||||
tt.activeCues_ = [];
|
||||
|
||||
@ -174,34 +170,6 @@ class TextTrack extends EventTarget {
|
||||
tt.tech_.on('timeupdate', timeupdateHandler);
|
||||
}
|
||||
|
||||
Object.defineProperty(tt, 'kind', {
|
||||
get() {
|
||||
return kind;
|
||||
},
|
||||
set() {}
|
||||
});
|
||||
|
||||
Object.defineProperty(tt, 'label', {
|
||||
get() {
|
||||
return label;
|
||||
},
|
||||
set() {}
|
||||
});
|
||||
|
||||
Object.defineProperty(tt, 'language', {
|
||||
get() {
|
||||
return language;
|
||||
},
|
||||
set() {}
|
||||
});
|
||||
|
||||
Object.defineProperty(tt, 'id', {
|
||||
get() {
|
||||
return id;
|
||||
},
|
||||
set() {}
|
||||
});
|
||||
|
||||
Object.defineProperty(tt, 'default', {
|
||||
get() {
|
||||
return default_;
|
||||
@ -214,7 +182,7 @@ class TextTrack extends EventTarget {
|
||||
return mode;
|
||||
},
|
||||
set(newMode) {
|
||||
if (!TextTrackEnum.TextTrackMode[newMode]) {
|
||||
if (!TextTrackMode[newMode]) {
|
||||
return;
|
||||
}
|
||||
mode = newMode;
|
||||
@ -282,16 +250,14 @@ class TextTrack extends EventTarget {
|
||||
set() {}
|
||||
});
|
||||
|
||||
if (options.src) {
|
||||
tt.src = options.src;
|
||||
loadTrack(options.src, tt);
|
||||
if (settings.src) {
|
||||
tt.src = settings.src;
|
||||
loadTrack(settings.src, tt);
|
||||
} else {
|
||||
tt.loaded_ = true;
|
||||
}
|
||||
|
||||
if (browser.IS_IE8) {
|
||||
return tt;
|
||||
}
|
||||
return tt;
|
||||
}
|
||||
|
||||
/**
|
||||
|
86
src/js/tracks/track-enums.js
Normal file
86
src/js/tracks/track-enums.js
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @file track-kinds.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
|
||||
*
|
||||
* enum VideoTrackKind {
|
||||
* "alternative",
|
||||
* "captions",
|
||||
* "main",
|
||||
* "sign",
|
||||
* "subtitles",
|
||||
* "commentary",
|
||||
* "",
|
||||
* };
|
||||
*/
|
||||
const VideoTrackKind = {
|
||||
alternative: 'alternative',
|
||||
captions: 'captions',
|
||||
main: 'main',
|
||||
sign: 'sign',
|
||||
subtitles: 'subtitles',
|
||||
commentary: 'commentary',
|
||||
};
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
|
||||
*
|
||||
* enum AudioTrackKind {
|
||||
* "alternative",
|
||||
* "descriptions",
|
||||
* "main",
|
||||
* "main-desc",
|
||||
* "translation",
|
||||
* "commentary",
|
||||
* "",
|
||||
* };
|
||||
*/
|
||||
const AudioTrackKind = {
|
||||
alternative: 'alternative',
|
||||
descriptions: 'descriptions',
|
||||
main: 'main',
|
||||
'main-desc': 'main-desc',
|
||||
translation: 'translation',
|
||||
commentary: 'commentary',
|
||||
};
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackkind
|
||||
*
|
||||
* enum TextTrackKind {
|
||||
* "subtitles",
|
||||
* "captions",
|
||||
* "descriptions",
|
||||
* "chapters",
|
||||
* "metadata"
|
||||
* };
|
||||
*/
|
||||
const TextTrackKind = {
|
||||
subtitles: 'subtitles',
|
||||
captions: 'captions',
|
||||
descriptions: 'descriptions',
|
||||
chapters: 'chapters',
|
||||
metadata: 'metadata'
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
|
||||
*
|
||||
* enum TextTrackMode { "disabled", "hidden", "showing" };
|
||||
*/
|
||||
const TextTrackMode = {
|
||||
disabled: 'disabled',
|
||||
hidden: 'hidden',
|
||||
showing: 'showing'
|
||||
};
|
||||
|
||||
/* jshint ignore:start */
|
||||
// we ignore jshint here because it does not see
|
||||
// AudioTrackKind as defined here
|
||||
export default { VideoTrackKind, AudioTrackKind, TextTrackKind, TextTrackMode };
|
||||
/* jshint ignore:end */
|
||||
|
148
src/js/tracks/track-list.js
Normal file
148
src/js/tracks/track-list.js
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @file track-list.js
|
||||
*/
|
||||
import EventTarget from '../event-target';
|
||||
import * as Fn from '../utils/fn.js';
|
||||
import * as browser from '../utils/browser.js';
|
||||
import document from 'global/document';
|
||||
|
||||
/**
|
||||
* Common functionaliy between Text, Audio, and Video TrackLists
|
||||
* Interfaces defined in the following spec:
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html
|
||||
*
|
||||
* @param {Track[]} tracks A list of tracks to initialize the list with
|
||||
* @param {Object} list the child object with inheritance done manually for ie8
|
||||
* @extends EventTarget
|
||||
* @class TrackList
|
||||
*/
|
||||
class TrackList extends EventTarget {
|
||||
constructor(tracks = [], list = null) {
|
||||
super();
|
||||
if (!list) {
|
||||
list = this;
|
||||
if (browser.IS_IE8) {
|
||||
list = document.createElement('custom');
|
||||
for (let prop in TrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = TrackList.prototype[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.tracks_ = [];
|
||||
Object.defineProperty(list, 'length', {
|
||||
get() {
|
||||
return this.tracks_.length;
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
list.addTrack_(tracks[i]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Track from TrackList
|
||||
*
|
||||
* @param {Mixed} track
|
||||
* @method addTrack_
|
||||
* @private
|
||||
*/
|
||||
addTrack_(track) {
|
||||
let index = this.tracks_.length;
|
||||
|
||||
if (!('' + index in this)) {
|
||||
Object.defineProperty(this, index, {
|
||||
get() {
|
||||
return this.tracks_[index];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Do not add duplicate tracks
|
||||
if (this.tracks_.indexOf(track) === -1) {
|
||||
this.tracks_.push(track);
|
||||
this.trigger({
|
||||
track,
|
||||
type: 'addtrack'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Track from TrackList
|
||||
*
|
||||
* @param {Track} rtrack track to be removed
|
||||
* @method removeTrack_
|
||||
* @private
|
||||
*/
|
||||
removeTrack_(rtrack) {
|
||||
let track;
|
||||
|
||||
for (let i = 0, l = this.length; i < l; i++) {
|
||||
if (this[i] === rtrack) {
|
||||
track = this[i];
|
||||
if (track.off) {
|
||||
track.off();
|
||||
}
|
||||
|
||||
this.tracks_.splice(i, 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.trigger({
|
||||
track,
|
||||
type: 'removetrack'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Track from the TrackList by a tracks id
|
||||
*
|
||||
* @param {String} id - the id of the track to get
|
||||
* @method getTrackById
|
||||
* @return {Track}
|
||||
* @private
|
||||
*/
|
||||
getTrackById(id) {
|
||||
let result = null;
|
||||
|
||||
for (let i = 0, l = this.length; i < l; i++) {
|
||||
let track = this[i];
|
||||
if (track.id === id) {
|
||||
result = track;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* change - One or more tracks in the track list have been enabled or disabled.
|
||||
* addtrack - A track has been added to the track list.
|
||||
* removetrack - A track has been removed from the track list.
|
||||
*/
|
||||
TrackList.prototype.allowedEvents_ = {
|
||||
change: 'change',
|
||||
addtrack: 'addtrack',
|
||||
removetrack: 'removetrack'
|
||||
};
|
||||
|
||||
// emulate attribute EventHandler support to allow for feature detection
|
||||
for (let event in TrackList.prototype.allowedEvents_) {
|
||||
TrackList.prototype['on' + event] = null;
|
||||
}
|
||||
|
||||
export default TrackList;
|
50
src/js/tracks/track.js
Normal file
50
src/js/tracks/track.js
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @file track.js
|
||||
*/
|
||||
import * as browser from '../utils/browser.js';
|
||||
import document from 'global/document';
|
||||
import * as Guid from '../utils/guid.js';
|
||||
import EventTarget from '../event-target';
|
||||
|
||||
/**
|
||||
* setup the common parts of an audio, video, or text track
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html
|
||||
*
|
||||
* @param {String} type The type of track we are dealing with audio|video|text
|
||||
* @param {Object=} options Object of option names and values
|
||||
* @extends EventTarget
|
||||
* @class Track
|
||||
*/
|
||||
class Track extends EventTarget {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
let track = this;
|
||||
if (browser.IS_IE8) {
|
||||
track = document.createElement('custom');
|
||||
for (let prop in Track.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
track[prop] = Track.prototype[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let trackProps = {
|
||||
id: options.id || 'vjs_track_' + Guid.newGUID(),
|
||||
kind: options.kind || '',
|
||||
label: options.label || '',
|
||||
language: options.language || ''
|
||||
};
|
||||
|
||||
for (let key in trackProps) {
|
||||
Object.defineProperty(track, key, {
|
||||
get() { return trackProps[key]; },
|
||||
set() {}
|
||||
});
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
export default Track;
|
123
src/js/tracks/video-track-list.js
Normal file
123
src/js/tracks/video-track-list.js
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @file video-track-list.js
|
||||
*/
|
||||
import TrackList from './track-list';
|
||||
import * as browser from '../utils/browser.js';
|
||||
import document from 'global/document';
|
||||
|
||||
/**
|
||||
* disable other video tracks before selecting the new one
|
||||
*
|
||||
* @param {Array|VideoTrackList} list list to work on
|
||||
* @param {VideoTrack} track the track to skip
|
||||
*/
|
||||
const disableOthers = function(list, track) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (track.id === list[i].id) {
|
||||
continue;
|
||||
}
|
||||
// another audio track is enabled, disable it
|
||||
list[i].selected = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A list of possiblee video tracks. Most functionality is in the
|
||||
* base class Tracklist and the spec for VideoTrackList is located at:
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
|
||||
*
|
||||
* interface VideoTrackList : EventTarget {
|
||||
* readonly attribute unsigned long length;
|
||||
* getter VideoTrack (unsigned long index);
|
||||
* VideoTrack? getTrackById(DOMString id);
|
||||
* readonly attribute long selectedIndex;
|
||||
*
|
||||
* attribute EventHandler onchange;
|
||||
* attribute EventHandler onaddtrack;
|
||||
* attribute EventHandler onremovetrack;
|
||||
* };
|
||||
*
|
||||
* @param {VideoTrack[]} tracks a list of video tracks to instantiate the list with
|
||||
# @extends TrackList
|
||||
* @class VideoTrackList
|
||||
*/
|
||||
class VideoTrackList extends TrackList {
|
||||
|
||||
constructor(tracks = []) {
|
||||
let list;
|
||||
|
||||
// make sure only 1 track is enabled
|
||||
// sorted from last index to first index
|
||||
for (let i = tracks.length - 1; i >= 0; i--) {
|
||||
if (tracks[i].selected) {
|
||||
disableOthers(tracks, tracks[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// IE8 forces us to implement inheritance ourselves
|
||||
// as it does not support Object.defineProperty properly
|
||||
if (browser.IS_IE8) {
|
||||
list = document.createElement('custom');
|
||||
for (let prop in TrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = TrackList.prototype[prop];
|
||||
}
|
||||
}
|
||||
for (let prop in VideoTrackList.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
list[prop] = VideoTrackList.prototype[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list = super(tracks, list);
|
||||
list.changing_ = false;
|
||||
|
||||
Object.defineProperty(list, 'selectedIndex', {
|
||||
get() {
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (this[i].selected) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
set() {}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
addTrack_(track) {
|
||||
if (track.selected) {
|
||||
disableOthers(this, track);
|
||||
}
|
||||
|
||||
super.addTrack_(track);
|
||||
// native tracks don't have this
|
||||
if (!track.addEventListener) {
|
||||
return;
|
||||
}
|
||||
track.addEventListener('selectedchange', () => {
|
||||
if (this.changing_) {
|
||||
return;
|
||||
}
|
||||
this.changing_ = true;
|
||||
disableOthers(this, track);
|
||||
this.changing_ = false;
|
||||
this.trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
addTrack(track) {
|
||||
this.addTrack_(track);
|
||||
}
|
||||
|
||||
removeTrack(track) {
|
||||
super.removeTrack_(track);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default VideoTrackList;
|
63
src/js/tracks/video-track.js
Normal file
63
src/js/tracks/video-track.js
Normal file
@ -0,0 +1,63 @@
|
||||
import {VideoTrackKind} from './track-enums';
|
||||
import Track from './track';
|
||||
import merge from '../utils/merge-options';
|
||||
import * as browser from '../utils/browser.js';
|
||||
|
||||
/**
|
||||
* A single video text track as defined in:
|
||||
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack
|
||||
*
|
||||
* interface VideoTrack {
|
||||
* readonly attribute DOMString id;
|
||||
* readonly attribute DOMString kind;
|
||||
* readonly attribute DOMString label;
|
||||
* readonly attribute DOMString language;
|
||||
* attribute boolean selected;
|
||||
* };
|
||||
*
|
||||
* @param {Object=} options Object of option names and values
|
||||
* @class VideoTrack
|
||||
*/
|
||||
class VideoTrack extends Track {
|
||||
constructor(options = {}) {
|
||||
let settings = merge(options, {
|
||||
kind: VideoTrackKind[options.kind] || ''
|
||||
});
|
||||
|
||||
// on IE8 this will be a document element
|
||||
// for every other browser this will be a normal object
|
||||
let track = super(settings);
|
||||
let selected = false;
|
||||
|
||||
if (browser.IS_IE8) {
|
||||
for (let prop in VideoTrack.prototype) {
|
||||
if (prop !== 'constructor') {
|
||||
track[prop] = VideoTrack.prototype[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(track, 'selected', {
|
||||
get() { return selected; },
|
||||
set(newSelected) {
|
||||
// an invalid or unchanged value
|
||||
if (typeof newSelected !== 'boolean' || newSelected === selected) {
|
||||
return;
|
||||
}
|
||||
selected = newSelected;
|
||||
this.trigger('selectedchange');
|
||||
}
|
||||
});
|
||||
|
||||
// if the user sets this track to selected then
|
||||
// set selected to that true value otherwise
|
||||
// we keep it false
|
||||
if (settings.selected) {
|
||||
track.selected = settings.selected;
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoTrack;
|
@ -13,6 +13,8 @@ import plugin from './plugins.js';
|
||||
import mergeOptions from '../../src/js/utils/merge-options.js';
|
||||
import * as Fn from './utils/fn.js';
|
||||
import TextTrack from './tracks/text-track.js';
|
||||
import AudioTrack from './tracks/audio-track.js';
|
||||
import VideoTrack from './tracks/video-track.js';
|
||||
|
||||
import assign from 'object.assign';
|
||||
import { createTimeRanges } from './utils/time-ranges.js';
|
||||
@ -549,6 +551,22 @@ videojs.xhr = xhr;
|
||||
*/
|
||||
videojs.TextTrack = TextTrack;
|
||||
|
||||
/**
|
||||
* export the AudioTrack class so that source handlers can create
|
||||
* AudioTracks and then add them to the players AudioTrackList
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
videojs.AudioTrack = AudioTrack;
|
||||
|
||||
/**
|
||||
* export the VideoTrack class so that source handlers can create
|
||||
* VideoTracks and then add them to the players VideoTrackList
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
videojs.VideoTrack = VideoTrack;
|
||||
|
||||
/**
|
||||
* Determines, via duck typing, whether or not a value is a DOM element.
|
||||
*
|
||||
|
@ -59,7 +59,9 @@ test('should be able to access expected player API methods', function() {
|
||||
ok(player.usingNativeControls, 'usingNativeControls exists');
|
||||
ok(player.isFullscreen, 'isFullscreen exists');
|
||||
|
||||
// TextTrack methods
|
||||
// Track methods
|
||||
ok(player.audioTracks, 'audioTracks exists');
|
||||
ok(player.videoTracks, 'videoTracks exists');
|
||||
ok(player.textTracks, 'textTracks exists');
|
||||
ok(player.remoteTextTrackEls, 'remoteTextTrackEls exists');
|
||||
ok(player.remoteTextTracks, 'remoteTextTracks exists');
|
||||
|
@ -86,14 +86,13 @@ test('dispose removes the object element even before ready fires', function() {
|
||||
// This test appears to test bad functionaly that was fixed
|
||||
// so it's debateable whether or not it's useful
|
||||
let dispose = Flash.prototype.dispose;
|
||||
let mockFlash = {};
|
||||
let mockFlash = new MockFlash();
|
||||
let noop = function(){};
|
||||
|
||||
// Mock required functions for dispose
|
||||
mockFlash.off = noop;
|
||||
mockFlash.trigger = noop;
|
||||
mockFlash.el_ = {};
|
||||
mockFlash.textTracks = () => ([]);
|
||||
|
||||
dispose.call(mockFlash);
|
||||
strictEqual(mockFlash.el_, null, 'swf el is nulled');
|
||||
|
@ -249,6 +249,97 @@ if (Html5.supportsNativeTextTracks()) {
|
||||
equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
|
||||
});
|
||||
}
|
||||
|
||||
if (Html5.supportsNativeAudioTracks()) {
|
||||
test('add native audioTrack listeners on startup', function() {
|
||||
let adds = [];
|
||||
let rems = [];
|
||||
let at = {
|
||||
length: 0,
|
||||
addEventListener: (type, fn) => adds.push([type, fn]),
|
||||
removeEventListener: (type, fn) => rems.push([type, fn]),
|
||||
};
|
||||
let el = document.createElement('div');
|
||||
el.audioTracks = at;
|
||||
|
||||
let htmlTech = new Html5({el});
|
||||
|
||||
equal(adds[0][0], 'change', 'change event handler added');
|
||||
equal(adds[1][0], 'addtrack', 'addtrack event handler added');
|
||||
equal(adds[2][0], 'removetrack', 'removetrack event handler added');
|
||||
});
|
||||
|
||||
test('remove all tracks from emulated list on dispose', function() {
|
||||
let adds = [];
|
||||
let rems = [];
|
||||
let at = {
|
||||
length: 0,
|
||||
addEventListener: (type, fn) => adds.push([type, fn]),
|
||||
removeEventListener: (type, fn) => rems.push([type, fn]),
|
||||
};
|
||||
let el = document.createElement('div');
|
||||
el.audioTracks = at;
|
||||
|
||||
let htmlTech = new Html5({el});
|
||||
htmlTech.dispose();
|
||||
|
||||
equal(adds[0][0], 'change', 'change event handler added');
|
||||
equal(adds[1][0], 'addtrack', 'addtrack event handler added');
|
||||
equal(adds[2][0], 'removetrack', 'removetrack event handler added');
|
||||
equal(rems[0][0], 'change', 'change event handler removed');
|
||||
equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
|
||||
equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
|
||||
equal(adds[0][0], rems[0][0], 'change event handler removed');
|
||||
equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
|
||||
equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
|
||||
});
|
||||
}
|
||||
|
||||
if (Html5.supportsNativeVideoTracks()) {
|
||||
test('add native videoTrack listeners on startup', function() {
|
||||
let adds = [];
|
||||
let rems = [];
|
||||
let vt = {
|
||||
length: 0,
|
||||
addEventListener: (type, fn) => adds.push([type, fn]),
|
||||
removeEventListener: (type, fn) => rems.push([type, fn]),
|
||||
};
|
||||
let el = document.createElement('div');
|
||||
el.videoTracks = vt;
|
||||
|
||||
let htmlTech = new Html5({el});
|
||||
|
||||
equal(adds[0][0], 'change', 'change event handler added');
|
||||
equal(adds[1][0], 'addtrack', 'addtrack event handler added');
|
||||
equal(adds[2][0], 'removetrack', 'removetrack event handler added');
|
||||
});
|
||||
|
||||
test('remove all tracks from emulated list on dispose', function() {
|
||||
let adds = [];
|
||||
let rems = [];
|
||||
let vt = {
|
||||
length: 0,
|
||||
addEventListener: (type, fn) => adds.push([type, fn]),
|
||||
removeEventListener: (type, fn) => rems.push([type, fn]),
|
||||
};
|
||||
let el = document.createElement('div');
|
||||
el.videoTracks = vt;
|
||||
|
||||
let htmlTech = new Html5({el});
|
||||
htmlTech.dispose();
|
||||
|
||||
equal(adds[0][0], 'change', 'change event handler added');
|
||||
equal(adds[1][0], 'addtrack', 'addtrack event handler added');
|
||||
equal(adds[2][0], 'removetrack', 'removetrack event handler added');
|
||||
equal(rems[0][0], 'change', 'change event handler removed');
|
||||
equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
|
||||
equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
|
||||
equal(adds[0][0], rems[0][0], 'change event handler removed');
|
||||
equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
|
||||
equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
|
||||
});
|
||||
}
|
||||
|
||||
test('should always return currentSource_ if set', function(){
|
||||
let currentSrc = Html5.prototype.currentSrc;
|
||||
equal(currentSrc.call({el_: {currentSrc:'test1'}}), 'test1', 'sould return source from element if nothing else set');
|
||||
|
@ -7,6 +7,13 @@ 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';
|
||||
|
||||
|
||||
q.module('Media Tech', {
|
||||
'setup': function() {
|
||||
@ -14,20 +21,10 @@ q.module('Media Tech', {
|
||||
this.clock = sinon.useFakeTimers();
|
||||
this.featuresProgessEvents = Tech.prototype['featuresProgessEvents'];
|
||||
Tech.prototype['featuresProgressEvents'] = false;
|
||||
Tech.prototype['featuresNativeTextTracks'] = true;
|
||||
oldTextTracks = Tech.prototype.textTracks;
|
||||
Tech.prototype.textTracks = function() {
|
||||
return {
|
||||
addEventListener: Function.prototype,
|
||||
removeEventListener: Function.prototype
|
||||
};
|
||||
};
|
||||
},
|
||||
'teardown': function() {
|
||||
this.clock.restore();
|
||||
Tech.prototype['featuresProgessEvents'] = this.featuresProgessEvents;
|
||||
Tech.prototype['featuresNativeTextTracks'] = false;
|
||||
Tech.prototype.textTracks = oldTextTracks;
|
||||
}
|
||||
});
|
||||
|
||||
@ -103,6 +100,54 @@ test('dispose() should stop time tracking', function() {
|
||||
ok(true, 'no exception was thrown');
|
||||
});
|
||||
|
||||
test('dispose() should clear all tracks that are passed when its created', function() {
|
||||
var audioTracks = new AudioTrackList([new AudioTrack(), new AudioTrack()]);
|
||||
var videoTracks = new VideoTrackList([new VideoTrack(), new VideoTrack()]);
|
||||
var textTracks = new TextTrackList([new TextTrack({tech: {}}), new TextTrack({tech: {}})]);
|
||||
|
||||
equal(audioTracks.length, 2, 'should have two audio tracks at the start');
|
||||
equal(videoTracks.length, 2, 'should have two video tracks at the start');
|
||||
equal(textTracks.length, 2, 'should have two text tracks at the start');
|
||||
|
||||
var tech = new Tech({audioTracks, videoTracks, textTracks});
|
||||
equal(tech.videoTracks().length, videoTracks.length, 'should hold video tracks that we passed');
|
||||
equal(tech.audioTracks().length, audioTracks.length, 'should hold audio tracks that we passed');
|
||||
equal(tech.textTracks().length, textTracks.length, 'should hold text tracks that we passed');
|
||||
|
||||
tech.dispose();
|
||||
|
||||
equal(audioTracks.length, 0, 'should have zero audio tracks after dispose');
|
||||
equal(videoTracks.length, 0, 'should have zero video tracks after dispose');
|
||||
equal(textTracks.length, 0, 'should have zero text tracks after dispose');
|
||||
});
|
||||
|
||||
test('dispose() should clear all tracks that are added after creation', function() {
|
||||
var 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());
|
||||
|
||||
equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
|
||||
equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
|
||||
equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
|
||||
equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els');
|
||||
equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
|
||||
|
||||
tech.dispose();
|
||||
|
||||
equal(tech.audioTracks().length, 0, 'should have zero audio tracks after dispose');
|
||||
equal(tech.videoTracks().length, 0, 'should have zero video tracks after dispose');
|
||||
equal(tech.remoteTextTrackEls().length, 0, 'should have zero remote text tracks els');
|
||||
equal(tech.remoteTextTracks().length, 0, 'should have zero remote text tracks');
|
||||
equal(tech.textTracks().length, 0, 'should have zero video tracks after dispose');
|
||||
});
|
||||
|
||||
test('should add the source handler interface to a tech', function(){
|
||||
var sourceA = { src: 'foo.mp4', type: 'video/mp4' };
|
||||
var sourceB = { src: 'no-support', type: 'no-support' };
|
||||
@ -185,13 +230,49 @@ test('should add the source handler interface to a tech', function(){
|
||||
strictEqual(MyTech.canPlaySource(sourceA), 'probably', 'the Tech returned probably for the valid source');
|
||||
strictEqual(MyTech.canPlaySource(sourceB), '', '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());
|
||||
|
||||
equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
|
||||
equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
|
||||
equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
|
||||
equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els');
|
||||
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
|
||||
equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
|
||||
equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
|
||||
equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
|
||||
equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els');
|
||||
equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
|
||||
|
||||
strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
|
||||
ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored');
|
||||
|
||||
// Pass a second source
|
||||
tech.setSource(sourceA);
|
||||
strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
|
||||
ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored');
|
||||
|
||||
// verify that all the tracks were removed as we got a new source
|
||||
equal(tech.audioTracks().length, 0, 'should have zero audio tracks');
|
||||
equal(tech.videoTracks().length, 0, 'should have zero video tracks');
|
||||
equal(tech.textTracks().length, 2, 'should have two video tracks');
|
||||
equal(tech.remoteTextTrackEls().length, 2, 'should have two remote text tracks els');
|
||||
equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
|
||||
|
||||
// Check that the handler dipose method works
|
||||
ok(!disposeCalled, 'dispose has not been called for the handler yet');
|
||||
ok(disposeCalled, 'dispose has been called for the handler yet');
|
||||
disposeCalled = false;
|
||||
tech.dispose();
|
||||
ok(disposeCalled, 'the handler dispose method was called when the tech was disposed');
|
||||
});
|
||||
|
97
test/unit/tracks/audio-track-list.test.js
Normal file
97
test/unit/tracks/audio-track-list.test.js
Normal file
@ -0,0 +1,97 @@
|
||||
import AudioTrackList from '../../../src/js/tracks/audio-track-list.js';
|
||||
import AudioTrack from '../../../src/js/tracks/audio-track.js';
|
||||
import EventTarget from '../../../src/js/event-target.js';
|
||||
|
||||
q.module('Audio Track List');
|
||||
|
||||
test('trigger "change" when "enabledchange" is fired on a track', function() {
|
||||
let track = new EventTarget();
|
||||
track.loaded_ = true;
|
||||
let audioTrackList = new AudioTrackList([track]);
|
||||
let changes = 0;
|
||||
let changeHandler = function() {
|
||||
changes++;
|
||||
};
|
||||
audioTrackList.on('change', changeHandler);
|
||||
track.trigger('enabledchange');
|
||||
equal(changes, 1, 'one change events for trigger');
|
||||
|
||||
audioTrackList.off('change', changeHandler);
|
||||
audioTrackList.onchange = changeHandler;
|
||||
|
||||
track.trigger('enabledchange');
|
||||
equal(changes, 2, 'one change events for another trigger');
|
||||
});
|
||||
|
||||
test('only one track is ever enabled', function() {
|
||||
let track = new AudioTrack({enabled: true});
|
||||
let track2 = new AudioTrack({enabled: true});
|
||||
let track3 = new AudioTrack({enabled: true});
|
||||
let track4 = new AudioTrack();
|
||||
let list = new AudioTrackList([track, track2]);
|
||||
equal(track.enabled, false, 'track is disabled');
|
||||
equal(track2.enabled, true, 'track2 is enabled');
|
||||
|
||||
track.enabled = true;
|
||||
equal(track.enabled, true, 'track is enabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
|
||||
list.addTrack_(track3);
|
||||
equal(track.enabled, false, 'track is disabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
equal(track3.enabled, true, 'track3 is enabled');
|
||||
|
||||
track.enabled = true;
|
||||
equal(track.enabled, true, 'track is disabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
equal(track3.enabled, false, 'track3 is disabled');
|
||||
|
||||
list.addTrack_(track4);
|
||||
equal(track.enabled, true, 'track is enabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
equal(track3.enabled, false, 'track3 is disabled');
|
||||
equal(track4.enabled, false, 'track4 is disabled');
|
||||
|
||||
});
|
||||
|
||||
test('all tracks can be disabled', function() {
|
||||
let track = new AudioTrack();
|
||||
let track2 = new AudioTrack();
|
||||
let list = new AudioTrackList([track, track2]);
|
||||
equal(track.enabled, false, 'track is disabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
|
||||
track.enabled = true;
|
||||
equal(track.enabled, true, 'track is enabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
|
||||
track.enabled = false;
|
||||
equal(track.enabled, false, 'track is disabled');
|
||||
equal(track2.enabled, false, 'track2 is disabled');
|
||||
});
|
||||
|
||||
test('trigger a change event per enabled change', function() {
|
||||
let track = new AudioTrack({enabled: true});
|
||||
let track2 = new AudioTrack({enabled: true});
|
||||
let track3 = new AudioTrack({enabled: true});
|
||||
let track4 = new AudioTrack();
|
||||
let list = new AudioTrackList([track, track2]);
|
||||
|
||||
let change = 0;
|
||||
list.on('change', () => change++);
|
||||
track.enabled = true;
|
||||
equal(change, 1, 'one change triggered');
|
||||
|
||||
list.addTrack_(track3);
|
||||
equal(change, 2, 'another change triggered by adding an enabled track');
|
||||
|
||||
track.enabled = true;
|
||||
equal(change, 3, 'another change trigger by changing enabled');
|
||||
|
||||
track.enabled = false;
|
||||
equal(change, 4, 'another change trigger by changing enabled');
|
||||
|
||||
list.addTrack_(track4);
|
||||
equal(change, 4, 'no change triggered by adding a disabled track');
|
||||
|
||||
});
|
118
test/unit/tracks/audio-track.test.js
Normal file
118
test/unit/tracks/audio-track.test.js
Normal file
@ -0,0 +1,118 @@
|
||||
import AudioTrack from '../../../src/js/tracks/audio-track.js';
|
||||
import {AudioTrackKind} from '../../../src/js/tracks/track-enums.js';
|
||||
import TrackBaseline from './track-baseline';
|
||||
|
||||
q.module('Audio Track');
|
||||
|
||||
// do baseline track testing
|
||||
TrackBaseline(AudioTrack, {
|
||||
id: '1',
|
||||
language: 'en',
|
||||
label: 'English',
|
||||
kind: 'main',
|
||||
});
|
||||
|
||||
test('can create an enabled propert on an AudioTrack', function() {
|
||||
let enabled = true;
|
||||
let track = new AudioTrack({
|
||||
enabled,
|
||||
});
|
||||
equal(track.enabled, enabled, 'enabled value matches what we passed in');
|
||||
});
|
||||
|
||||
test('defaults when items not provided', function() {
|
||||
let track = new AudioTrack();
|
||||
|
||||
equal(track.kind, '', 'kind defaulted to empty string');
|
||||
equal(track.enabled, false, 'enabled defaulted to true since there is one track');
|
||||
equal(track.label, '', 'label defaults to empty string');
|
||||
equal(track.language, '', 'language defaults to empty string');
|
||||
ok(track.id.match(/vjs_track_\d{5}/), 'id defaults to vjs_track_GUID');
|
||||
});
|
||||
|
||||
test('kind can only be one of several options, defaults to empty string', function() {
|
||||
let track = new AudioTrack({
|
||||
kind: 'foo'
|
||||
});
|
||||
|
||||
equal(track.kind, '', 'the kind is set to empty string, not foo');
|
||||
notEqual(track.kind, 'foo', 'the kind is set to empty string, not foo');
|
||||
|
||||
// loop through all possible kinds to verify
|
||||
for (let key in AudioTrackKind) {
|
||||
let currentKind = AudioTrackKind[key];
|
||||
let track = new AudioTrack({
|
||||
kind: currentKind,
|
||||
});
|
||||
equal(track.kind, currentKind, 'the kind is set to ' + currentKind);
|
||||
}
|
||||
});
|
||||
|
||||
test('enabled can only be instantiated to true or false, defaults to false', function() {
|
||||
let track = new AudioTrack({
|
||||
enabled: 'foo'
|
||||
});
|
||||
|
||||
equal(track.enabled, false, 'the enabled value is set to false, not foo');
|
||||
notEqual(track.enabled, 'foo', 'the enabled value is not set to foo');
|
||||
|
||||
track = new AudioTrack({
|
||||
enabled: true
|
||||
});
|
||||
|
||||
equal(track.enabled, true, 'the enabled value is set to true');
|
||||
|
||||
track = new AudioTrack({
|
||||
enabled: false
|
||||
});
|
||||
|
||||
equal(track.enabled, false, 'the enabled value is set to false');
|
||||
});
|
||||
|
||||
test('enabled can only be changed to true or false', function() {
|
||||
let track = new AudioTrack();
|
||||
|
||||
track.enabled = 'foo';
|
||||
notEqual(track.enabled, 'foo', 'enabled not set to invalid value, foo');
|
||||
equal(track.enabled, false, 'enabled remains on the old value, false');
|
||||
|
||||
track.enabled = true;
|
||||
equal(track.enabled, true, 'enabled was set to true');
|
||||
|
||||
track.enabled = 'baz';
|
||||
notEqual(track.enabled, 'baz', 'enabled not set to invalid value, baz');
|
||||
equal(track.enabled, true, 'enabled remains on the old value, true');
|
||||
|
||||
track.enabled = false;
|
||||
equal(track.enabled, false, 'enabled was set to false');
|
||||
});
|
||||
|
||||
test('when enabled is changed enabledchange event is fired', function() {
|
||||
let track = new AudioTrack({
|
||||
tech: this.tech,
|
||||
enabled: false
|
||||
});
|
||||
let eventsTriggered = 0;
|
||||
track.addEventListener('enabledchange', () => {
|
||||
eventsTriggered++;
|
||||
});
|
||||
|
||||
// two events
|
||||
track.enabled = true;
|
||||
track.enabled = false;
|
||||
equal(eventsTriggered, 2, 'two enabled changes');
|
||||
|
||||
// no event here
|
||||
track.enabled = false;
|
||||
track.enabled = false;
|
||||
equal(eventsTriggered, 2, 'still two enabled changes');
|
||||
|
||||
// one event
|
||||
track.enabled = true;
|
||||
equal(eventsTriggered, 3, 'three enabled changes');
|
||||
|
||||
// no events
|
||||
track.enabled = true;
|
||||
track.enabled = true;
|
||||
equal(eventsTriggered, 3, 'still three enabled changes');
|
||||
});
|
114
test/unit/tracks/audio-tracks.test.js
Normal file
114
test/unit/tracks/audio-tracks.test.js
Normal file
@ -0,0 +1,114 @@
|
||||
import AudioTrack from '../../../src/js/tracks/text-track.js';
|
||||
import Html5 from '../../../src/js/tech/html5.js';
|
||||
import Tech from '../../../src/js/tech/tech.js';
|
||||
import Component from '../../../src/js/component.js';
|
||||
|
||||
import * as browser from '../../../src/js/utils/browser.js';
|
||||
import TestHelpers from '../test-helpers.js';
|
||||
import document from 'global/document';
|
||||
|
||||
q.module('Tracks', {
|
||||
setup() {
|
||||
this.clock = sinon.useFakeTimers();
|
||||
},
|
||||
teardown() {
|
||||
this.clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('Player track methods call the tech', function() {
|
||||
let player;
|
||||
let calls = 0;
|
||||
|
||||
player = TestHelpers.makePlayer();
|
||||
|
||||
player.tech_.audioTracks = function() {
|
||||
calls++;
|
||||
};
|
||||
|
||||
player.audioTracks();
|
||||
|
||||
equal(calls, 1, 'audioTrack defers to the tech');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
test('listen to remove and add track events in native audio tracks', function() {
|
||||
let oldTestVid = Html5.TEST_VID;
|
||||
let player;
|
||||
let options;
|
||||
let oldAudioTracks = Html5.prototype.audioTracks;
|
||||
let events = {};
|
||||
let html;
|
||||
|
||||
Html5.prototype.audioTracks = function() {
|
||||
return {
|
||||
addEventListener(type, handler) {
|
||||
events[type] = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Html5.TEST_VID = {
|
||||
audioTracks: []
|
||||
};
|
||||
|
||||
player = {
|
||||
// Function.prototype is a built-in no-op function.
|
||||
controls() {},
|
||||
ready() {},
|
||||
options() {
|
||||
return {};
|
||||
},
|
||||
addChild() {},
|
||||
id() {},
|
||||
el() {
|
||||
return {
|
||||
insertBefore() {},
|
||||
appendChild() {}
|
||||
};
|
||||
}
|
||||
};
|
||||
player.player_ = player;
|
||||
player.options_ = options = {};
|
||||
|
||||
html = new Html5(options);
|
||||
|
||||
ok(events.removetrack, 'removetrack listener was added');
|
||||
ok(events.addtrack, 'addtrack listener was added');
|
||||
|
||||
Html5.TEST_VID = oldTestVid;
|
||||
Html5.prototype.audioTracks = oldAudioTracks;
|
||||
});
|
||||
|
||||
test('html5 tech supports native audio tracks if the video supports it', function() {
|
||||
let oldTestVid = Html5.TEST_VID;
|
||||
|
||||
Html5.TEST_VID = {
|
||||
audioTracks: []
|
||||
};
|
||||
|
||||
ok(Html5.supportsNativeAudioTracks(), 'native audio tracks are supported');
|
||||
|
||||
Html5.TEST_VID = oldTestVid;
|
||||
});
|
||||
|
||||
test('html5 tech does not support native audio tracks if the video does not supports it', function() {
|
||||
let oldTestVid = Html5.TEST_VID;
|
||||
Html5.TEST_VID = {};
|
||||
|
||||
ok(!Html5.supportsNativeAudioTracks(), 'native audio tracks are not supported');
|
||||
|
||||
Html5.TEST_VID = oldTestVid;
|
||||
});
|
||||
|
||||
test('when switching techs, we should not get a new audio track', function() {
|
||||
let player = TestHelpers.makePlayer();
|
||||
|
||||
player.loadTech_('TechFaker');
|
||||
let firstTracks = player.audioTracks();
|
||||
|
||||
player.loadTech_('TechFaker');
|
||||
let secondTracks = player.audioTracks();
|
||||
|
||||
ok(firstTracks === secondTracks, 'the tracks are equal');
|
||||
});
|
@ -2,155 +2,7 @@ import TextTrackList from '../../../src/js/tracks/text-track-list.js';
|
||||
import TextTrack from '../../../src/js/tracks/text-track.js';
|
||||
import EventTarget from '../../../src/js/event-target.js';
|
||||
|
||||
const genericTracks = [
|
||||
{
|
||||
id: '1',
|
||||
addEventListener() {},
|
||||
off() {}
|
||||
}, {
|
||||
id: '2',
|
||||
addEventListener() {},
|
||||
off() {}
|
||||
}, {
|
||||
id: '3',
|
||||
addEventListener() {},
|
||||
off() {}
|
||||
}
|
||||
];
|
||||
|
||||
q.module('Text Track List');
|
||||
|
||||
test('TextTrackList\'s length is set correctly', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
|
||||
equal(ttl.length, genericTracks.length, 'the length is ' + genericTracks.length);
|
||||
});
|
||||
|
||||
test('can get text tracks by id', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
|
||||
equal(ttl.getTrackById('1').id, 1, 'id "1" has id of "1"');
|
||||
equal(ttl.getTrackById('2').id, 2, 'id "2" has id of "2"');
|
||||
equal(ttl.getTrackById('3').id, 3, 'id "3" has id of "3"');
|
||||
ok(!ttl.getTrackById(1), 'there isn\'t an item with "numeric" id of `1`');
|
||||
});
|
||||
|
||||
test('length is updated when new tracks are added or removed', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
|
||||
ttl.addTrack_({id: '100', addEventListener() {}, off() {}});
|
||||
equal(ttl.length, genericTracks.length + 1, 'the length is ' + (genericTracks.length + 1));
|
||||
ttl.addTrack_({id: '101', addEventListener() {}, off() {}});
|
||||
equal(ttl.length, genericTracks.length + 2, 'the length is ' + (genericTracks.length + 2));
|
||||
|
||||
ttl.removeTrack_(ttl.getTrackById('101'));
|
||||
equal(ttl.length, genericTracks.length + 1, 'the length is ' + (genericTracks.length + 1));
|
||||
ttl.removeTrack_(ttl.getTrackById('100'));
|
||||
equal(ttl.length, genericTracks.length, 'the length is ' + genericTracks.length);
|
||||
});
|
||||
|
||||
test('can access items by index', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
let i = 0;
|
||||
let length = ttl.length;
|
||||
|
||||
expect(length);
|
||||
|
||||
for (; i < length; i++) {
|
||||
equal(ttl[i].id, String(i + 1), 'the id of a track matches the index + 1');
|
||||
}
|
||||
});
|
||||
|
||||
test('can access new items by index', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
|
||||
ttl.addTrack_({id: '100', addEventListener() {}});
|
||||
equal(ttl[3].id, '100', 'id of item at index 3 is 100');
|
||||
ttl.addTrack_({id: '101', addEventListener() {}});
|
||||
equal(ttl[4].id, '101', 'id of item at index 4 is 101');
|
||||
});
|
||||
|
||||
test('cannot access removed items by index', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
|
||||
ttl.addTrack_({id: '100', addEventListener() {}, off() {}});
|
||||
ttl.addTrack_({id: '101', addEventListener() {}, off() {}});
|
||||
equal(ttl[3].id, '100', 'id of item at index 3 is 100');
|
||||
equal(ttl[4].id, '101', 'id of item at index 4 is 101');
|
||||
|
||||
ttl.removeTrack_(ttl.getTrackById('101'));
|
||||
ttl.removeTrack_(ttl.getTrackById('100'));
|
||||
|
||||
ok(!ttl[3], 'nothing at index 3');
|
||||
ok(!ttl[4], 'nothing at index 4');
|
||||
});
|
||||
|
||||
test('new item available at old index', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
|
||||
ttl.addTrack_({id: '100', addEventListener() {}, off() {}});
|
||||
equal(ttl[3].id, '100', 'id of item at index 3 is 100');
|
||||
|
||||
ttl.removeTrack_(ttl.getTrackById('100'));
|
||||
ok(!ttl[3], 'nothing at index 3');
|
||||
|
||||
ttl.addTrack_({id: '101', addEventListener() {}});
|
||||
equal(ttl[3].id, '101', 'id of new item at index 3 is now 101');
|
||||
});
|
||||
|
||||
test('a "addtrack" event is triggered when new tracks are added', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
let tracks = 0;
|
||||
let adds = 0;
|
||||
let addHandler = function(e) {
|
||||
if (e.track) {
|
||||
tracks++;
|
||||
}
|
||||
adds++;
|
||||
};
|
||||
|
||||
ttl.on('addtrack', addHandler);
|
||||
|
||||
ttl.addTrack_({id: '100', addEventListener() {}});
|
||||
ttl.addTrack_({id: '101', addEventListener() {}});
|
||||
|
||||
ttl.off('addtrack', addHandler);
|
||||
|
||||
ttl.onaddtrack = addHandler;
|
||||
|
||||
ttl.addTrack_({id: '102', addEventListener() {}});
|
||||
ttl.addTrack_({id: '103', addEventListener() {}});
|
||||
|
||||
equal(adds, 4, 'we got ' + adds + ' "addtrack" events');
|
||||
equal(tracks, 4, 'we got a track with every event');
|
||||
});
|
||||
|
||||
test('a "removetrack" event is triggered when tracks are removed', function() {
|
||||
let ttl = new TextTrackList(genericTracks);
|
||||
let tracks = 0;
|
||||
let rms = 0;
|
||||
let rmHandler = function(e) {
|
||||
if (e.track) {
|
||||
tracks++;
|
||||
}
|
||||
rms++;
|
||||
};
|
||||
|
||||
ttl.on('removetrack', rmHandler);
|
||||
|
||||
ttl.removeTrack_(ttl.getTrackById('1'));
|
||||
ttl.removeTrack_(ttl.getTrackById('2'));
|
||||
|
||||
ttl.off('removetrack', rmHandler);
|
||||
|
||||
ttl.onremovetrack = rmHandler;
|
||||
|
||||
ttl.removeTrack_(ttl.getTrackById('3'));
|
||||
|
||||
equal(rms, 3, 'we got ' + rms + ' "removetrack" events');
|
||||
equal(tracks, 3, 'we got a track with every event');
|
||||
});
|
||||
|
||||
test('trigger "change" event when "modechange" is fired on a track', function() {
|
||||
let tt = new EventTarget();
|
||||
let ttl = new TextTrackList([tt]);
|
||||
@ -160,15 +12,12 @@ test('trigger "change" event when "modechange" is fired on a track', function()
|
||||
};
|
||||
|
||||
ttl.on('change', changeHandler);
|
||||
|
||||
tt.trigger('modechange');
|
||||
|
||||
ttl.off('change', changeHandler);
|
||||
|
||||
ttl.onchange = changeHandler;
|
||||
|
||||
tt.trigger('modechange');
|
||||
|
||||
equal(changes, 2, 'two change events should have fired');
|
||||
});
|
||||
|
||||
@ -185,11 +34,9 @@ test('trigger "change" event when mode changes on a TextTrack', function() {
|
||||
};
|
||||
|
||||
ttl.on('change', changeHandler);
|
||||
|
||||
tt.mode = 'showing';
|
||||
|
||||
ttl.off('change', changeHandler);
|
||||
|
||||
ttl.onchange = changeHandler;
|
||||
|
||||
tt.mode = 'hidden';
|
||||
|
@ -1,5 +1,7 @@
|
||||
import window from 'global/window';
|
||||
import EventTarget from '../../../src/js/event-target.js';
|
||||
import TrackBaseline from './track-baseline';
|
||||
import TechFaker from '../tech/tech-faker';
|
||||
import TextTrack from '../../../src/js/tracks/text-track.js';
|
||||
import TestHelpers from '../test-helpers.js';
|
||||
import log from '../../../src/js/utils/log.js';
|
||||
@ -13,40 +15,38 @@ const defaultTech = {
|
||||
off() {},
|
||||
currentTime() {}
|
||||
};
|
||||
|
||||
q.module('Text Track');
|
||||
|
||||
test('text-track requires a tech', function() {
|
||||
let error = new Error('A tech was not provided.');
|
||||
|
||||
q.throws(() => new TextTrack(), error, 'a tech is required for text track');
|
||||
// do baseline track testing
|
||||
TrackBaseline(TextTrack, {
|
||||
id: '1',
|
||||
kind: 'subtitles',
|
||||
mode: 'disabled',
|
||||
label: 'English',
|
||||
language: 'en',
|
||||
tech: defaultTech
|
||||
});
|
||||
|
||||
test('can create a TextTrack with various properties', function() {
|
||||
let kind = 'captions';
|
||||
let label = 'English';
|
||||
let language = 'en';
|
||||
let id = '1';
|
||||
test('requires a tech', function() {
|
||||
let error = new Error('A tech was not provided.');
|
||||
|
||||
q.throws(() => new TextTrack({}), error, 'a tech is required');
|
||||
q.throws(() => new TextTrack({tech: null}), error, 'a tech is required');
|
||||
});
|
||||
|
||||
test('can create a TextTrack with a mode property', function() {
|
||||
let mode = 'disabled';
|
||||
let tt = new TextTrack({
|
||||
kind,
|
||||
label,
|
||||
language,
|
||||
id,
|
||||
mode,
|
||||
tech: defaultTech
|
||||
});
|
||||
|
||||
equal(tt.kind, kind, 'we have a kind');
|
||||
equal(tt.label, label, 'we have a label');
|
||||
equal(tt.language, language, 'we have a language');
|
||||
equal(tt.id, id, 'we have a id');
|
||||
equal(tt.mode, mode, 'we have a mode');
|
||||
});
|
||||
|
||||
test('defaults when items not provided', function() {
|
||||
let tt = new TextTrack({
|
||||
tech: defaultTech
|
||||
tech: TechFaker
|
||||
});
|
||||
|
||||
equal(tt.kind, 'subtitles', 'kind defaulted to subtitles');
|
||||
@ -131,32 +131,16 @@ test('mode can only be one of several options, defaults to disabled', function()
|
||||
equal(tt.mode, 'showing', 'the mode is set to showing');
|
||||
});
|
||||
|
||||
test('kind, label, language, id, cue, and activeCues are read only', function() {
|
||||
let kind = 'captions';
|
||||
let label = 'English';
|
||||
let language = 'en';
|
||||
let id = '1';
|
||||
test('cue and activeCues are read only', function() {
|
||||
let mode = 'disabled';
|
||||
let tt = new TextTrack({
|
||||
kind,
|
||||
label,
|
||||
language,
|
||||
id,
|
||||
mode,
|
||||
tech: defaultTech
|
||||
tech: defaultTech,
|
||||
});
|
||||
|
||||
tt.kind = 'subtitles';
|
||||
tt.label = 'Spanish';
|
||||
tt.language = 'es';
|
||||
tt.id = '2';
|
||||
tt.cues = 'foo';
|
||||
tt.activeCues = 'bar';
|
||||
|
||||
equal(tt.kind, kind, 'kind is still set to captions');
|
||||
equal(tt.label, label, 'label is still set to English');
|
||||
equal(tt.language, language, 'language is still set to en');
|
||||
equal(tt.id, id, 'id is still set to \'1\'');
|
||||
notEqual(tt.cues, 'foo', 'cues is still original value');
|
||||
notEqual(tt.activeCues, 'bar', 'activeCues is still original value');
|
||||
});
|
||||
|
43
test/unit/tracks/track-baseline.js
Normal file
43
test/unit/tracks/track-baseline.js
Normal file
@ -0,0 +1,43 @@
|
||||
import * as browser from '../../../src/js/utils/browser.js';
|
||||
import document from 'global/document';
|
||||
|
||||
/**
|
||||
* Tests baseline functionality for all tracks
|
||||
*
|
||||
# @param {Track} TrackClass the track class object to use for testing
|
||||
# @param {Object} options the options to setup a track with
|
||||
*/
|
||||
const TrackBaseline = function(TrackClass, options) {
|
||||
|
||||
test('is setup with id, kind, label, and language', function() {
|
||||
let track = new TrackClass(options);
|
||||
equal(track.kind, options.kind, 'we have a kind');
|
||||
equal(track.label, options.label, 'we have a label');
|
||||
equal(track.language, options.language, 'we have a language');
|
||||
equal(track.id, options.id, 'we have a id');
|
||||
});
|
||||
|
||||
test('kind, label, language, id, are read only', function() {
|
||||
let track = new TrackClass(options);
|
||||
track.kind = 'subtitles';
|
||||
track.label = 'Spanish';
|
||||
track.language = 'es';
|
||||
track.id = '2';
|
||||
|
||||
equal(track.kind, options.kind, 'we have a kind');
|
||||
equal(track.label, options.label, 'we have a label');
|
||||
equal(track.language, options.language, 'we have a language');
|
||||
equal(track.id, options.id, 'we have an id');
|
||||
});
|
||||
|
||||
test('returns an instance of itself on non ie8 browsers', function() {
|
||||
let track = new TrackClass(options);
|
||||
if (browser.IS_IE8) {
|
||||
ok(track, 'returns an object on ie8');
|
||||
return;
|
||||
}
|
||||
ok(track instanceof TrackClass, 'returns an instance');
|
||||
});
|
||||
};
|
||||
|
||||
export default TrackBaseline;
|
143
test/unit/tracks/track-list.test.js
Normal file
143
test/unit/tracks/track-list.test.js
Normal file
@ -0,0 +1,143 @@
|
||||
import TrackList from '../../../src/js/tracks/track-list.js';
|
||||
import EventTarget from '../../../src/js/event-target.js';
|
||||
|
||||
const newTrack = function(id) {
|
||||
return {
|
||||
id,
|
||||
addEventListener() {},
|
||||
off() {},
|
||||
};
|
||||
};
|
||||
|
||||
q.module('Track List', {
|
||||
beforeEach() {
|
||||
this.tracks = [newTrack('1'), newTrack('2'), newTrack('3')];
|
||||
}
|
||||
});
|
||||
|
||||
test('TrackList\'s length is set correctly', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
|
||||
equal(trackList.length, this.tracks.length, 'length is ' + this.tracks.length);
|
||||
});
|
||||
|
||||
test('can get tracks by int and string id', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
|
||||
equal(trackList.getTrackById('1').id, '1', 'id "1" has id of "1"');
|
||||
equal(trackList.getTrackById('2').id, '2', 'id "2" has id of "2"');
|
||||
equal(trackList.getTrackById('3').id, '3', 'id "3" has id of "3"');
|
||||
|
||||
});
|
||||
|
||||
test('length is updated when new tracks are added or removed', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
|
||||
trackList.addTrack_(newTrack('100'));
|
||||
equal(trackList.length, this.tracks.length + 1, 'the length is ' + (this.tracks.length + 1));
|
||||
trackList.addTrack_(newTrack('101'));
|
||||
equal(trackList.length, this.tracks.length + 2, 'the length is ' + (this.tracks.length + 2));
|
||||
|
||||
trackList.removeTrack_(trackList.getTrackById('101'));
|
||||
equal(trackList.length, this.tracks.length + 1, 'the length is ' + (this.tracks.length + 1));
|
||||
trackList.removeTrack_(trackList.getTrackById('100'));
|
||||
equal(trackList.length, this.tracks.length, 'the length is ' + this.tracks.length);
|
||||
});
|
||||
|
||||
test('can access items by index', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
let length = trackList.length;
|
||||
|
||||
expect(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
equal(trackList[i].id, String(i + 1), 'the id of a track matches the index + 1');
|
||||
}
|
||||
});
|
||||
|
||||
test('can access new items by index', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
|
||||
trackList.addTrack_(newTrack('100'));
|
||||
equal(trackList[3].id, '100', 'id of item at index 3 is 100');
|
||||
|
||||
trackList.addTrack_(newTrack('101'));
|
||||
equal(trackList[4].id, '101', 'id of item at index 4 is 101');
|
||||
});
|
||||
|
||||
test('cannot access removed items by index', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
|
||||
trackList.addTrack_(newTrack('100'));
|
||||
trackList.addTrack_(newTrack('101'));
|
||||
equal(trackList[3].id, '100', 'id of item at index 3 is 100');
|
||||
equal(trackList[4].id, '101', 'id of item at index 4 is 101');
|
||||
|
||||
trackList.removeTrack_(trackList.getTrackById('101'));
|
||||
trackList.removeTrack_(trackList.getTrackById('100'));
|
||||
|
||||
ok(!trackList[3], 'nothing at index 3');
|
||||
ok(!trackList[4], 'nothing at index 4');
|
||||
});
|
||||
|
||||
test('new item available at old index', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
|
||||
trackList.addTrack_(newTrack('100'));
|
||||
equal(trackList[3].id, '100', 'id of item at index 3 is 100');
|
||||
|
||||
trackList.removeTrack_(trackList.getTrackById('100'));
|
||||
ok(!trackList[3], 'nothing at index 3');
|
||||
|
||||
trackList.addTrack_(newTrack('101'));
|
||||
equal(trackList[3].id, '101', 'id of new item at index 3 is now 101');
|
||||
});
|
||||
|
||||
test('a "addtrack" event is triggered when new tracks are added', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
let tracks = 0;
|
||||
let adds = 0;
|
||||
let addHandler = (e) => {
|
||||
if (e.track) {
|
||||
tracks++;
|
||||
}
|
||||
adds++;
|
||||
};
|
||||
|
||||
trackList.on('addtrack', addHandler);
|
||||
|
||||
trackList.addTrack_(newTrack('100'));
|
||||
trackList.addTrack_(newTrack('101'));
|
||||
|
||||
trackList.off('addtrack', addHandler);
|
||||
trackList.onaddtrack = addHandler;
|
||||
|
||||
trackList.addTrack_(newTrack('102'));
|
||||
trackList.addTrack_(newTrack('103'));
|
||||
|
||||
equal(adds, 4, 'we got ' + adds + ' "addtrack" events');
|
||||
equal(tracks, 4, 'we got a track with every event');
|
||||
});
|
||||
|
||||
test('a "removetrack" event is triggered when tracks are removed', function() {
|
||||
let trackList = new TrackList(this.tracks);
|
||||
let tracks = 0;
|
||||
let rms = 0;
|
||||
let rmHandler = (e) => {
|
||||
if (e.track) {
|
||||
tracks++;
|
||||
}
|
||||
rms++;
|
||||
};
|
||||
|
||||
trackList.on('removetrack', rmHandler);
|
||||
trackList.removeTrack_(trackList.getTrackById('1'));
|
||||
trackList.removeTrack_(trackList.getTrackById('2'));
|
||||
|
||||
trackList.off('removetrack', rmHandler);
|
||||
trackList.onremovetrack = rmHandler;
|
||||
trackList.removeTrack_(trackList.getTrackById('3'));
|
||||
|
||||
equal(rms, 3, 'we got ' + rms + ' "removetrack" events');
|
||||
equal(tracks, 3, 'we got a track with every event');
|
||||
});
|
34
test/unit/tracks/track.test.js
Normal file
34
test/unit/tracks/track.test.js
Normal file
@ -0,0 +1,34 @@
|
||||
import TechFaker from '../tech/tech-faker';
|
||||
import TrackBaseline from './track-baseline';
|
||||
import Track from '../../../src/js/tracks/track.js';
|
||||
import * as browser from '../../../src/js/utils/browser.js';
|
||||
|
||||
const defaultTech = {
|
||||
textTracks() {},
|
||||
on() {},
|
||||
off() {},
|
||||
currentTime() {}
|
||||
};
|
||||
|
||||
// do baseline track testing
|
||||
q.module('Track');
|
||||
|
||||
TrackBaseline(Track, {
|
||||
id: '1',
|
||||
kind: 'subtitles',
|
||||
mode: 'disabled',
|
||||
label: 'English',
|
||||
language: 'en',
|
||||
tech: new TechFaker()
|
||||
});
|
||||
|
||||
test('defaults when items not provided', function() {
|
||||
let track = new Track({
|
||||
tech: defaultTech
|
||||
});
|
||||
|
||||
equal(track.kind, '', 'kind defaulted to empty string');
|
||||
equal(track.label, '', 'label defaults to empty string');
|
||||
equal(track.language, '', 'language defaults to empty string');
|
||||
ok(track.id.match(/vjs_track_\d{5}/), 'id defaults to vjs_track_GUID');
|
||||
});
|
96
test/unit/tracks/video-track-list.test.js
Normal file
96
test/unit/tracks/video-track-list.test.js
Normal file
@ -0,0 +1,96 @@
|
||||
import VideoTrackList from '../../../src/js/tracks/video-track-list.js';
|
||||
import VideoTrack from '../../../src/js/tracks/video-track.js';
|
||||
import EventTarget from '../../../src/js/event-target.js';
|
||||
|
||||
q.module('Video Track List');
|
||||
|
||||
test('trigger "change" when "selectedchange" is fired on a track', function() {
|
||||
let track = new EventTarget();
|
||||
track.loaded_ = true;
|
||||
let audioTrackList = new VideoTrackList([track]);
|
||||
let changes = 0;
|
||||
let changeHandler = function() {
|
||||
changes++;
|
||||
};
|
||||
audioTrackList.on('change', changeHandler);
|
||||
track.trigger('selectedchange');
|
||||
equal(changes, 1, 'one change events for trigger');
|
||||
|
||||
audioTrackList.off('change', changeHandler);
|
||||
audioTrackList.onchange = changeHandler;
|
||||
|
||||
track.trigger('selectedchange');
|
||||
equal(changes, 2, 'one change events for another trigger');
|
||||
});
|
||||
|
||||
test('only one track is ever selected', function() {
|
||||
let track = new VideoTrack({selected: true});
|
||||
let track2 = new VideoTrack({selected: true});
|
||||
let track3 = new VideoTrack({selected: true});
|
||||
let track4 = new VideoTrack();
|
||||
let list = new VideoTrackList([track, track2]);
|
||||
equal(track.selected, false, 'track is unselected');
|
||||
equal(track2.selected, true, 'track2 is selected');
|
||||
|
||||
track.selected = true;
|
||||
equal(track.selected, true, 'track is selected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
|
||||
list.addTrack_(track3);
|
||||
equal(track.selected, false, 'track is unselected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
equal(track3.selected, true, 'track3 is selected');
|
||||
|
||||
track.selected = true;
|
||||
equal(track.selected, true, 'track is unselected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
equal(track3.selected, false, 'track3 is unselected');
|
||||
|
||||
list.addTrack_(track4);
|
||||
equal(track.selected, true, 'track is selected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
equal(track3.selected, false, 'track3 is unselected');
|
||||
equal(track4.selected, false, 'track4 is unselected');
|
||||
|
||||
});
|
||||
|
||||
test('all tracks can be unselected', function() {
|
||||
let track = new VideoTrack();
|
||||
let track2 = new VideoTrack();
|
||||
let list = new VideoTrackList([track, track2]);
|
||||
equal(track.selected, false, 'track is unselected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
|
||||
track.selected = true;
|
||||
equal(track.selected, true, 'track is selected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
|
||||
track.selected = false;
|
||||
equal(track.selected, false, 'track is unselected');
|
||||
equal(track2.selected, false, 'track2 is unselected');
|
||||
});
|
||||
|
||||
test('trigger a change event per selected change', function() {
|
||||
let track = new VideoTrack({selected: true});
|
||||
let track2 = new VideoTrack({selected: true});
|
||||
let track3 = new VideoTrack({selected: true});
|
||||
let track4 = new VideoTrack();
|
||||
let list = new VideoTrackList([track, track2]);
|
||||
|
||||
let change = 0;
|
||||
list.on('change', () => change++);
|
||||
track.selected = true;
|
||||
equal(change, 1, 'one change triggered');
|
||||
|
||||
list.addTrack_(track3);
|
||||
equal(change, 2, 'another change triggered by adding an selected track');
|
||||
|
||||
track.selected = true;
|
||||
equal(change, 3, 'another change trigger by changing selected');
|
||||
|
||||
track.selected = false;
|
||||
equal(change, 4, 'another change trigger by changing selected');
|
||||
|
||||
list.addTrack_(track4);
|
||||
equal(change, 4, 'no change triggered by adding a unselected track');
|
||||
});
|
111
test/unit/tracks/video-track.test.js
Normal file
111
test/unit/tracks/video-track.test.js
Normal file
@ -0,0 +1,111 @@
|
||||
import VideoTrack from '../../../src/js/tracks/video-track';
|
||||
import VideoTrackList from '../../../src/js/tracks/video-track-list';
|
||||
import {VideoTrackKind} from '../../../src/js/tracks/track-enums';
|
||||
import TrackBaseline from './track-baseline';
|
||||
|
||||
q.module('Video Track');
|
||||
|
||||
// do baseline track testing
|
||||
TrackBaseline(VideoTrack, {
|
||||
id: '1',
|
||||
language: 'en',
|
||||
label: 'English',
|
||||
kind: 'main',
|
||||
});
|
||||
|
||||
test('can create an VideoTrack a selected property', function() {
|
||||
let selected = true;
|
||||
let track = new VideoTrack({
|
||||
selected,
|
||||
});
|
||||
equal(track.selected, selected, 'selected value matches what we passed in');
|
||||
});
|
||||
|
||||
test('defaults when items not provided', function() {
|
||||
let track = new VideoTrack();
|
||||
|
||||
equal(track.kind, '', 'kind defaulted to empty string');
|
||||
equal(track.selected, false, 'selected defaulted to true since there is one track');
|
||||
equal(track.label, '', 'label defaults to empty string');
|
||||
equal(track.language, '', 'language defaults to empty string');
|
||||
ok(track.id.match(/vjs_track_\d{5}/), 'id defaults to vjs_track_GUID');
|
||||
});
|
||||
|
||||
test('kind can only be one of several options, defaults to empty string', function() {
|
||||
let track = new VideoTrack({
|
||||
kind: 'foo'
|
||||
});
|
||||
|
||||
equal(track.kind, '', 'the kind is set to empty string, not foo');
|
||||
notEqual(track.kind, 'foo', 'the kind is set to empty string, not foo');
|
||||
|
||||
// loop through all possible kinds to verify
|
||||
for (let key in VideoTrackKind) {
|
||||
let currentKind = VideoTrackKind[key];
|
||||
let track = new VideoTrack({kind: currentKind});
|
||||
equal(track.kind, currentKind, 'the kind is set to ' + currentKind);
|
||||
}
|
||||
});
|
||||
|
||||
test('selected can only be instantiated to true or false, defaults to false', function() {
|
||||
let track = new VideoTrack({
|
||||
selected: 'foo'
|
||||
});
|
||||
|
||||
equal(track.selected, false, 'the selected value is set to false, not foo');
|
||||
notEqual(track.selected, 'foo', 'the selected value is not set to foo');
|
||||
|
||||
track = new VideoTrack({
|
||||
selected: true
|
||||
});
|
||||
|
||||
equal(track.selected, true, 'the selected value is set to true');
|
||||
|
||||
track = new VideoTrack({
|
||||
selected: false
|
||||
});
|
||||
|
||||
equal(track.selected, false, 'the selected value is set to false');
|
||||
});
|
||||
|
||||
test('selected can only be changed to true or false', function() {
|
||||
let track = new VideoTrack();
|
||||
|
||||
track.selected = 'foo';
|
||||
notEqual(track.selected, 'foo', 'selected not set to invalid value, foo');
|
||||
equal(track.selected, false, 'selected remains on the old value, false');
|
||||
|
||||
track.selected = true;
|
||||
equal(track.selected, true, 'selected was set to true');
|
||||
|
||||
track.selected = 'baz';
|
||||
notEqual(track.selected, 'baz', 'selected not set to invalid value, baz');
|
||||
equal(track.selected, true, 'selected remains on the old value, true');
|
||||
|
||||
track.selected = false;
|
||||
equal(track.selected, false, 'selected was set to false');
|
||||
});
|
||||
|
||||
test('when selected is changed selectedchange event is fired', function() {
|
||||
let track = new VideoTrack({
|
||||
selected: false
|
||||
});
|
||||
let eventsTriggered = 0;
|
||||
track.addEventListener('selectedchange', () => {
|
||||
eventsTriggered++;
|
||||
});
|
||||
|
||||
// two events
|
||||
track.selected = true;
|
||||
track.selected = false;
|
||||
equal(eventsTriggered, 2, 'two selected changes');
|
||||
|
||||
// no event here
|
||||
track.selected = false;
|
||||
track.selected = false;
|
||||
equal(eventsTriggered, 2, 'still two selected changes');
|
||||
|
||||
// one event
|
||||
track.selected = true;
|
||||
equal(eventsTriggered, 3, 'three selected changes');
|
||||
});
|
114
test/unit/tracks/video-tracks.test.js
Normal file
114
test/unit/tracks/video-tracks.test.js
Normal file
@ -0,0 +1,114 @@
|
||||
import VideoTrack from '../../../src/js/tracks/video-track.js';
|
||||
import Html5 from '../../../src/js/tech/html5.js';
|
||||
import Tech from '../../../src/js/tech/tech.js';
|
||||
import Component from '../../../src/js/component.js';
|
||||
|
||||
import * as browser from '../../../src/js/utils/browser.js';
|
||||
import TestHelpers from '../test-helpers.js';
|
||||
import document from 'global/document';
|
||||
|
||||
q.module('Video Tracks', {
|
||||
setup() {
|
||||
this.clock = sinon.useFakeTimers();
|
||||
},
|
||||
teardown() {
|
||||
this.clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('Player track methods call the tech', function() {
|
||||
let player;
|
||||
let calls = 0;
|
||||
|
||||
player = TestHelpers.makePlayer();
|
||||
|
||||
player.tech_.videoTracks = function() {
|
||||
calls++;
|
||||
};
|
||||
|
||||
player.videoTracks();
|
||||
|
||||
equal(calls, 1, 'videoTrack defers to the tech');
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
test('listen to remove and add track events in native video tracks', function() {
|
||||
let oldTestVid = Html5.TEST_VID;
|
||||
let player;
|
||||
let options;
|
||||
let oldVideoTracks = Html5.prototype.videoTracks;
|
||||
let events = {};
|
||||
let html;
|
||||
|
||||
Html5.prototype.videoTracks = function() {
|
||||
return {
|
||||
addEventListener(type, handler) {
|
||||
events[type] = true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Html5.TEST_VID = {
|
||||
videoTracks: []
|
||||
};
|
||||
|
||||
player = {
|
||||
// Function.prototype is a built-in no-op function.
|
||||
controls() {},
|
||||
ready() {},
|
||||
options() {
|
||||
return {};
|
||||
},
|
||||
addChild() {},
|
||||
id() {},
|
||||
el() {
|
||||
return {
|
||||
insertBefore() {},
|
||||
appendChild() {}
|
||||
};
|
||||
}
|
||||
};
|
||||
player.player_ = player;
|
||||
player.options_ = options = {};
|
||||
|
||||
html = new Html5(options);
|
||||
|
||||
ok(events.removetrack, 'removetrack listener was added');
|
||||
ok(events.addtrack, 'addtrack listener was added');
|
||||
|
||||
Html5.TEST_VID = oldTestVid;
|
||||
Html5.prototype.videoTracks = oldVideoTracks;
|
||||
});
|
||||
|
||||
test('html5 tech supports native video tracks if the video supports it', function() {
|
||||
let oldTestVid = Html5.TEST_VID;
|
||||
|
||||
Html5.TEST_VID = {
|
||||
videoTracks: []
|
||||
};
|
||||
|
||||
ok(Html5.supportsNativeVideoTracks(), 'native video tracks are supported');
|
||||
|
||||
Html5.TEST_VID = oldTestVid;
|
||||
});
|
||||
|
||||
test('html5 tech does not support native video tracks if the video does not supports it', function() {
|
||||
let oldTestVid = Html5.TEST_VID;
|
||||
Html5.TEST_VID = {};
|
||||
|
||||
ok(!Html5.supportsNativeVideoTracks(), 'native video tracks are not supported');
|
||||
|
||||
Html5.TEST_VID = oldTestVid;
|
||||
});
|
||||
|
||||
test('when switching techs, we should not get a new video track', function() {
|
||||
let player = TestHelpers.makePlayer();
|
||||
|
||||
player.loadTech_('TechFaker');
|
||||
let firstTracks = player.videoTracks();
|
||||
|
||||
player.loadTech_('TechFaker');
|
||||
let secondTracks = player.videoTracks();
|
||||
|
||||
ok(firstTracks === secondTracks, 'the tracks are equal');
|
||||
});
|
Loading…
Reference in New Issue
Block a user