1
0
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:
brandonocasey 2016-04-22 14:31:12 -04:00 committed by Pat O'Neill
parent 18cdf08c0e
commit 2e2dbde4b4
32 changed files with 2050 additions and 407 deletions

View File

@ -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)) * @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)) * @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)) * @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))
-------------------- --------------------

View File

@ -22,6 +22,8 @@ import safeParseTuple from 'safe-json-parse/tuple';
import assign from 'object.assign'; import assign from 'object.assign';
import mergeOptions from './utils/merge-options.js'; import mergeOptions from './utils/merge-options.js';
import textTrackConverter from './tracks/text-track-list-converter.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) // Include required child components (importing also registers them)
import MediaLoader from './tech/loader.js'; import MediaLoader from './tech/loader.js';
@ -555,7 +557,9 @@ class Player extends Component {
'source': source, 'source': source,
'playerId': this.id(), 'playerId': this.id(),
'techId': `${this.id()}_${techName}_api`, 'techId': `${this.id()}_${techName}_api`,
'videoTracks': this.videoTracks_,
'textTracks': this.textTracks_, 'textTracks': this.textTracks_,
'audioTracks': this.audioTracks_,
'autoplay': this.options_.autoplay, 'autoplay': this.options_.autoplay,
'preload': this.options_.preload, 'preload': this.options_.preload,
'loop': this.options_.loop, 'loop': this.options_.loop,
@ -648,7 +652,9 @@ class Player extends Component {
*/ */
unloadTech_() { unloadTech_() {
// Save the current text tracks so that we can reuse the same text tracks with the next tech // 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.textTracks_ = this.textTracks();
this.audioTracks_ = this.audioTracks();
this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
this.isReady_ = false; this.isReady_ = false;
@ -2509,11 +2515,47 @@ class Player extends Component {
} }
/** /**
* 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. * Text tracks are tracks of timed text events.
* Captions - text displayed over the video for the hearing impaired * 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 * 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 * 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 * Descriptions (not supported yet) - audio descriptions that are read back to the user by a screen reading device
*/ */
/** /**
@ -2609,8 +2651,6 @@ class Player extends Component {
// initialTime: function(){ return this.techCall_('initialTime'); }, // initialTime: function(){ return this.techCall_('initialTime'); },
// startOffsetTime: function(){ return this.techCall_('startOffsetTime'); }, // startOffsetTime: function(){ return this.techCall_('startOffsetTime'); },
// played: function(){ return this.techCall_('played'); }, // played: function(){ return this.techCall_('played'); },
// videoTracks: function(){ return this.techCall_('videoTracks'); },
// audioTracks: function(){ return this.techCall_('audioTracks'); },
// defaultPlaybackRate: function(){ return this.techCall_('defaultPlaybackRate'); }, // defaultPlaybackRate: function(){ return this.techCall_('defaultPlaybackRate'); },
// defaultMuted: function(){ return this.techCall_('defaultMuted'); } // defaultMuted: function(){ return this.techCall_('defaultMuted'); }

View File

@ -312,7 +312,7 @@ class Flash extends Tech {
// Create setters and getters for attributes // Create setters and getters for attributes
const _api = Flash.prototype; const _api = Flash.prototype;
const _readWrite = 'rtmpConnection,rtmpStream,preload,defaultPlaybackRate,playbackRate,autoplay,loop,mediaGroup,controller,controls,volume,muted,defaultMuted'.split(','); 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){ function _createSetter(attr){
var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1); var attrUpper = attr.charAt(0).toUpperCase() + attr.slice(1);

View File

@ -16,6 +16,7 @@ import document from 'global/document';
import window from 'global/window'; import window from 'global/window';
import assign from 'object.assign'; import assign from 'object.assign';
import mergeOptions from '../utils/merge-options.js'; import mergeOptions from '../utils/merge-options.js';
import toTitleCase from '../utils/to-title-case.js';
/** /**
* HTML5 Media Controller - Wrapper for HTML5 Media API * 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 (this.featuresNativeTextTracks) {
if (crossoriginTracks) { if (crossoriginTracks) {
log.warn(tsml`Text Tracks are being loaded from another origin but the crossorigin attribute isn't used. 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 * @method dispose
*/ */
dispose() { dispose() {
let tt = this.el().textTracks; // Un-ProxyNativeTracks
let emulatedTt = this.textTracks(); ['audio', 'video', 'text'].forEach((type) => {
let capitalType = toTitleCase(type);
let tl = this.el_[`${type}Tracks`];
// remove native event listeners if (tl && tl.removeEventListener) {
if (tt && tt.removeEventListener) { tl.removeEventListener('change', this[`handle${capitalType}TrackChange_`]);
tt.removeEventListener('change', this.handleTextTrackChange_); tl.removeEventListener('addtrack', this[`handle${capitalType}TrackAdd_`]);
tt.removeEventListener('addtrack', this.handleTextTrackAdd_); tl.removeEventListener('removetrack', this[`handle${capitalType}TrackRemove_`]);
tt.removeEventListener('removetrack', this.handleTextTrackRemove_);
} }
});
// clearout the emulated text track list.
let i = emulatedTt.length;
while (i--) {
emulatedTt.removeTrack_(emulatedTt[i]);
}
Html5.disposeMediaElement(this.el_); Html5.disposeMediaElement(this.el_);
// tech will handle clearing of the emulated track list
super.dispose(); super.dispose();
} }
@ -303,6 +317,43 @@ class Html5 extends Tech {
this.textTracks().removeTrack_(e.track); 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 * Play for html5 tech
* *
@ -989,6 +1040,27 @@ Html5.supportsNativeTextTracks = function() {
return supportsTextTracks; 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. * An array of events available on the Html5 tech.
* *
@ -1062,6 +1134,21 @@ Html5.prototype['featuresProgressEvents'] = true;
*/ */
Html5.prototype['featuresNativeTextTracks'] = Html5.supportsNativeTextTracks(); 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 --------------------------------- // // HTML5 Feature detection and Device Fixes --------------------------------- //
let canPlayType; let canPlayType;
const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; const mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i;

View File

@ -10,6 +10,10 @@ import HTMLTrackElementList from '../tracks/html-track-element-list';
import mergeOptions from '../utils/merge-options.js'; import mergeOptions from '../utils/merge-options.js';
import TextTrack from '../tracks/text-track'; import TextTrack from '../tracks/text-track';
import TextTrackList from '../tracks/text-track-list'; 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 * as Fn from '../utils/fn.js';
import log from '../utils/log.js'; import log from '../utils/log.js';
import { createTimeRange } from '../utils/time-ranges.js'; import { createTimeRange } from '../utils/time-ranges.js';
@ -45,6 +49,8 @@ class Tech extends Component {
}); });
this.textTracks_ = options.textTracks; 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. // Manually track progress in cases where the browser/flash player doesn't report it.
if (!this.featuresProgressEvents) { if (!this.featuresProgressEvents) {
@ -65,6 +71,7 @@ class Tech extends Component {
} }
this.initTextTrackListeners(); this.initTextTrackListeners();
this.initTrackListeners();
// Turn on component tap events // Turn on component tap events
this.emitTapEvents(); this.emitTapEvents();
@ -218,15 +225,9 @@ class Tech extends Component {
* @method dispose * @method dispose
*/ */
dispose() { dispose() {
// clear out text tracks because we can't reuse them between techs
let textTracks = this.textTracks();
if (textTracks) { // clear out all tracks because we can't reuse them between techs
let i = textTracks.length; this.clearTracks(['audio', 'video', 'text']);
while(i--) {
this.removeRemoteTextTrack(textTracks[i]);
}
}
// Turn off any manual progress or timeupdate tracking // Turn off any manual progress or timeupdate tracking
if (this.manualProgress) { this.manualProgressOff(); } if (this.manualProgress) { this.manualProgressOff(); }
@ -236,6 +237,33 @@ class Tech extends Component {
super.dispose(); 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. * 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 * Emulate texttracks
* *
@ -337,8 +391,10 @@ class Tech extends Component {
script.onload = null; script.onload = null;
script.onerror = 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; window['WebVTT'] = true;
this.el().parentNode.appendChild(script);
} }
let updateDisplay = () => this.trigger('texttrackchange'); 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. * Provide default methods for text tracks.
* *
@ -536,14 +614,31 @@ class Tech extends Component {
} }
} }
/* /**
* List of associated text tracks * List of associated text tracks
* *
* @type {Array} * @type {TextTrackList}
* @private * @private
*/ */
Tech.prototype.textTracks_; 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={}) { var createTrackHelper = function(self, kind, label, language, options={}) {
let tracks = self.textTracks(); let tracks = self.textTracks();
@ -713,6 +808,12 @@ Tech.withSourceHandlers = function(_Tech){
this.disposeSourceHandler(); this.disposeSourceHandler();
this.off('dispose', 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.currentSource_ = source;
this.sourceHandler_ = sh.handleSource(source, this, this.options_); this.sourceHandler_ = sh.handleSource(source, this, this.options_);
this.on('dispose', this.disposeSourceHandler); this.on('dispose', this.disposeSourceHandler);

View 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;

View 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;

View File

@ -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 */

View File

@ -1,14 +1,15 @@
/** /**
* @file text-track-list.js * @file text-track-list.js
*/ */
import EventTarget from '../event-target'; import TrackList from './track-list';
import * as Fn from '../utils/fn.js'; import * as Fn from '../utils/fn.js';
import * as browser from '../utils/browser.js'; import * as browser from '../utils/browser.js';
import document from 'global/document'; import document from 'global/document';
/** /**
* A text track list as defined in: * A list of possible text tracks. All functionality is in the
* https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist * base class TrackList. The spec for TextTrackList is located at:
* @link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist
* *
* interface TextTrackList : EventTarget { * interface TextTrackList : EventTarget {
* readonly attribute unsigned long length; * readonly attribute unsigned long length;
@ -20,19 +21,23 @@ import document from 'global/document';
* attribute EventHandler onremovetrack; * attribute EventHandler onremovetrack;
* }; * };
* *
* @param {Track[]} tracks A list of tracks to initialize the list with * @param {TextTrack[]} tracks A list of tracks to initialize the list with
* @extends EventTarget * @extends TrackList
* @class TextTrackList * @class TextTrackList
*/ */
class TextTrackList extends TrackList {
class TextTrackList extends EventTarget {
constructor(tracks = []) { constructor(tracks = []) {
super(); let list;
let list = this;
// IE8 forces us to implement inheritance ourselves
// as it does not support Object.defineProperty properly
if (browser.IS_IE8) { if (browser.IS_IE8) {
list = document.createElement('custom'); list = document.createElement('custom');
for (let prop in TrackList.prototype) {
if (prop !== 'constructor') {
list[prop] = TrackList.prototype[prop];
}
}
for (let prop in TextTrackList.prototype) { for (let prop in TextTrackList.prototype) {
if (prop !== 'constructor') { if (prop !== 'constructor') {
list[prop] = TextTrackList.prototype[prop]; list[prop] = TextTrackList.prototype[prop];
@ -40,54 +45,15 @@ class TextTrackList extends EventTarget {
} }
} }
list.tracks_ = []; list = super(tracks, list);
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; return list;
} }
}
/**
* Add TextTrack from TextTrackList
*
* @param {TextTrack} track
* @method addTrack_
* @private
*/
addTrack_(track) { addTrack_(track) {
let index = this.tracks_.length; super.addTrack_(track);
if (!('' + index in this)) {
Object.defineProperty(this, index, {
get() {
return this.tracks_[index];
}
});
}
track.addEventListener('modechange', Fn.bind(this, function() { track.addEventListener('modechange', Fn.bind(this, function() {
this.trigger('change'); 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; 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; export default TextTrackList;

View File

@ -3,15 +3,15 @@
*/ */
import TextTrackCueList from './text-track-cue-list'; import TextTrackCueList from './text-track-cue-list';
import * as Fn from '../utils/fn.js'; import * as Fn from '../utils/fn.js';
import * as Guid from '../utils/guid.js'; import {TextTrackKind, TextTrackMode} from './track-enums';
import * as browser from '../utils/browser.js';
import * as TextTrackEnum from './text-track-enums';
import log from '../utils/log.js'; import log from '../utils/log.js';
import EventTarget from '../event-target';
import document from 'global/document'; import document from 'global/document';
import window from 'global/window'; import window from 'global/window';
import Track from './track.js';
import { isCrossOrigin } from '../utils/url.js'; import { isCrossOrigin } from '../utils/url.js';
import XHR from 'xhr'; 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 * takes a webvtt file contents and parses it into cues
@ -54,7 +54,6 @@ const parseCues = function(srcContent, track) {
parser.flush(); parser.flush();
}; };
/** /**
* load a track from a specifed url * load a track from a specifed url
* *
@ -99,7 +98,7 @@ const loadTrack = function(src, track) {
/** /**
* A single text track as defined in: * 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 { * interface TextTrack : EventTarget {
* readonly attribute TextTrackKind kind; * readonly attribute TextTrackKind kind;
@ -121,21 +120,31 @@ const loadTrack = function(src, track) {
* }; * };
* *
* @param {Object=} options Object of option names and values * @param {Object=} options Object of option names and values
* @extends EventTarget * @extends Track
* @class TextTrack * @class TextTrack
*/ */
class TextTrack extends EventTarget { class TextTrack extends Track {
constructor(options = {}) { constructor(options = {}) {
super();
if (!options.tech) { if (!options.tech) {
throw new Error('A tech was not provided.'); 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) { if (browser.IS_IE8) {
tt = document.createElement('custom');
for (let prop in TextTrack.prototype) { for (let prop in TextTrack.prototype) {
if (prop !== 'constructor') { if (prop !== 'constructor') {
tt[prop] = TextTrack.prototype[prop]; 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.cues_ = [];
tt.activeCues_ = []; tt.activeCues_ = [];
@ -174,34 +170,6 @@ class TextTrack extends EventTarget {
tt.tech_.on('timeupdate', timeupdateHandler); 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', { Object.defineProperty(tt, 'default', {
get() { get() {
return default_; return default_;
@ -214,7 +182,7 @@ class TextTrack extends EventTarget {
return mode; return mode;
}, },
set(newMode) { set(newMode) {
if (!TextTrackEnum.TextTrackMode[newMode]) { if (!TextTrackMode[newMode]) {
return; return;
} }
mode = newMode; mode = newMode;
@ -282,17 +250,15 @@ class TextTrack extends EventTarget {
set() {} set() {}
}); });
if (options.src) { if (settings.src) {
tt.src = options.src; tt.src = settings.src;
loadTrack(options.src, tt); loadTrack(settings.src, tt);
} else { } else {
tt.loaded_ = true; tt.loaded_ = true;
} }
if (browser.IS_IE8) {
return tt; return tt;
} }
}
/** /**
* add a cue to the internal list of cues * add a cue to the internal list of cues

View 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
View 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
View 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;

View 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;

View 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;

View File

@ -13,6 +13,8 @@ import plugin from './plugins.js';
import mergeOptions from '../../src/js/utils/merge-options.js'; import mergeOptions from '../../src/js/utils/merge-options.js';
import * as Fn from './utils/fn.js'; import * as Fn from './utils/fn.js';
import TextTrack from './tracks/text-track.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 assign from 'object.assign';
import { createTimeRanges } from './utils/time-ranges.js'; import { createTimeRanges } from './utils/time-ranges.js';
@ -549,6 +551,22 @@ videojs.xhr = xhr;
*/ */
videojs.TextTrack = TextTrack; 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. * Determines, via duck typing, whether or not a value is a DOM element.
* *

View File

@ -59,7 +59,9 @@ test('should be able to access expected player API methods', function() {
ok(player.usingNativeControls, 'usingNativeControls exists'); ok(player.usingNativeControls, 'usingNativeControls exists');
ok(player.isFullscreen, 'isFullscreen 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.textTracks, 'textTracks exists');
ok(player.remoteTextTrackEls, 'remoteTextTrackEls exists'); ok(player.remoteTextTrackEls, 'remoteTextTrackEls exists');
ok(player.remoteTextTracks, 'remoteTextTracks exists'); ok(player.remoteTextTracks, 'remoteTextTracks exists');

View File

@ -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 // This test appears to test bad functionaly that was fixed
// so it's debateable whether or not it's useful // so it's debateable whether or not it's useful
let dispose = Flash.prototype.dispose; let dispose = Flash.prototype.dispose;
let mockFlash = {}; let mockFlash = new MockFlash();
let noop = function(){}; let noop = function(){};
// Mock required functions for dispose // Mock required functions for dispose
mockFlash.off = noop; mockFlash.off = noop;
mockFlash.trigger = noop; mockFlash.trigger = noop;
mockFlash.el_ = {}; mockFlash.el_ = {};
mockFlash.textTracks = () => ([]);
dispose.call(mockFlash); dispose.call(mockFlash);
strictEqual(mockFlash.el_, null, 'swf el is nulled'); strictEqual(mockFlash.el_, null, 'swf el is nulled');

View File

@ -249,6 +249,97 @@ if (Html5.supportsNativeTextTracks()) {
equal(adds[2][0], rems[2][0], 'removetrack event handler removed'); 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(){ test('should always return currentSource_ if set', function(){
let currentSrc = Html5.prototype.currentSrc; let currentSrc = Html5.prototype.currentSrc;
equal(currentSrc.call({el_: {currentSrc:'test1'}}), 'test1', 'sould return source from element if nothing else set'); equal(currentSrc.call({el_: {currentSrc:'test1'}}), 'test1', 'sould return source from element if nothing else set');

View File

@ -7,6 +7,13 @@ import Button from '../../../src/js/button.js';
import { createTimeRange } from '../../../src/js/utils/time-ranges.js'; import { createTimeRange } from '../../../src/js/utils/time-ranges.js';
import extendFn from '../../../src/js/extend.js'; import extendFn from '../../../src/js/extend.js';
import MediaError from '../../../src/js/media-error.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', { q.module('Media Tech', {
'setup': function() { 'setup': function() {
@ -14,20 +21,10 @@ q.module('Media Tech', {
this.clock = sinon.useFakeTimers(); this.clock = sinon.useFakeTimers();
this.featuresProgessEvents = Tech.prototype['featuresProgessEvents']; this.featuresProgessEvents = Tech.prototype['featuresProgessEvents'];
Tech.prototype['featuresProgressEvents'] = false; 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() { 'teardown': function() {
this.clock.restore(); this.clock.restore();
Tech.prototype['featuresProgessEvents'] = this.featuresProgessEvents; 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'); 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(){ test('should add the source handler interface to a tech', function(){
var sourceA = { src: 'foo.mp4', type: 'video/mp4' }; var sourceA = { src: 'foo.mp4', type: 'video/mp4' };
var sourceB = { src: 'no-support', type: 'no-support' }; 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(sourceA), 'probably', 'the Tech returned probably for the valid source');
strictEqual(MyTech.canPlaySource(sourceB), '', 'the Tech returned an empty string for the invalid 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 // Pass a source through the source handler process of a tech instance
tech.setSource(sourceA); 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'); strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was 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 // 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(); tech.dispose();
ok(disposeCalled, 'the handler dispose method was called when the tech was disposed'); ok(disposeCalled, 'the handler dispose method was called when the tech was disposed');
}); });

View 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');
});

View 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');
});

View 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');
});

View File

@ -2,155 +2,7 @@ import TextTrackList from '../../../src/js/tracks/text-track-list.js';
import TextTrack from '../../../src/js/tracks/text-track.js'; import TextTrack from '../../../src/js/tracks/text-track.js';
import EventTarget from '../../../src/js/event-target.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'); 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() { test('trigger "change" event when "modechange" is fired on a track', function() {
let tt = new EventTarget(); let tt = new EventTarget();
let ttl = new TextTrackList([tt]); 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); ttl.on('change', changeHandler);
tt.trigger('modechange'); tt.trigger('modechange');
ttl.off('change', changeHandler); ttl.off('change', changeHandler);
ttl.onchange = changeHandler; ttl.onchange = changeHandler;
tt.trigger('modechange'); tt.trigger('modechange');
equal(changes, 2, 'two change events should have fired'); 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); ttl.on('change', changeHandler);
tt.mode = 'showing'; tt.mode = 'showing';
ttl.off('change', changeHandler); ttl.off('change', changeHandler);
ttl.onchange = changeHandler; ttl.onchange = changeHandler;
tt.mode = 'hidden'; tt.mode = 'hidden';

View File

@ -1,5 +1,7 @@
import window from 'global/window'; import window from 'global/window';
import EventTarget from '../../../src/js/event-target.js'; 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 TextTrack from '../../../src/js/tracks/text-track.js';
import TestHelpers from '../test-helpers.js'; import TestHelpers from '../test-helpers.js';
import log from '../../../src/js/utils/log.js'; import log from '../../../src/js/utils/log.js';
@ -13,40 +15,38 @@ const defaultTech = {
off() {}, off() {},
currentTime() {} currentTime() {}
}; };
q.module('Text Track'); q.module('Text Track');
test('text-track requires a tech', function() { // do baseline track testing
let error = new Error('A tech was not provided.'); TrackBaseline(TextTrack, {
id: '1',
q.throws(() => new TextTrack(), error, 'a tech is required for text track'); kind: 'subtitles',
mode: 'disabled',
label: 'English',
language: 'en',
tech: defaultTech
}); });
test('can create a TextTrack with various properties', function() { test('requires a tech', function() {
let kind = 'captions'; let error = new Error('A tech was not provided.');
let label = 'English';
let language = 'en'; q.throws(() => new TextTrack({}), error, 'a tech is required');
let id = '1'; 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 mode = 'disabled';
let tt = new TextTrack({ let tt = new TextTrack({
kind,
label,
language,
id,
mode, mode,
tech: defaultTech 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'); equal(tt.mode, mode, 'we have a mode');
}); });
test('defaults when items not provided', function() { test('defaults when items not provided', function() {
let tt = new TextTrack({ let tt = new TextTrack({
tech: defaultTech tech: TechFaker
}); });
equal(tt.kind, 'subtitles', 'kind defaulted to subtitles'); 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'); equal(tt.mode, 'showing', 'the mode is set to showing');
}); });
test('kind, label, language, id, cue, and activeCues are read only', function() { test('cue and activeCues are read only', function() {
let kind = 'captions';
let label = 'English';
let language = 'en';
let id = '1';
let mode = 'disabled'; let mode = 'disabled';
let tt = new TextTrack({ let tt = new TextTrack({
kind,
label,
language,
id,
mode, mode,
tech: defaultTech tech: defaultTech,
}); });
tt.kind = 'subtitles';
tt.label = 'Spanish';
tt.language = 'es';
tt.id = '2';
tt.cues = 'foo'; tt.cues = 'foo';
tt.activeCues = 'bar'; 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.cues, 'foo', 'cues is still original value');
notEqual(tt.activeCues, 'bar', 'activeCues is still original value'); notEqual(tt.activeCues, 'bar', 'activeCues is still original value');
}); });

View 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;

View 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');
});

View 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');
});

View 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');
});

View 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');
});

View 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');
});