1
0
mirror of https://github.com/videojs/video.js.git synced 2025-01-25 11:13:52 +02:00

Finishing off TextTrack support.

This commit is contained in:
Steve Heffernan 2012-03-16 12:29:38 -07:00
parent 72a423237c
commit 2aa5a2ee09
6 changed files with 520 additions and 247 deletions

View File

@ -53,7 +53,7 @@ body.vjs-full-window {
/* Text Track Styles */
/* Overall track holder for both captions and subtitles */
.video-js .vjs-text-track-display { text-align: center; position: absolute; bottom: 4em; left: 1em; right: 1em; }
.video-js .vjs-text-track-display { text-align: center; position: absolute; bottom: 4em; left: 1em; right: 1em; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
/* Individual tracks */
.video-js .vjs-text-track {
display: none; color: #fff; font-size: 1.4em; text-align: center; margin-bottom: 0.1em;
@ -125,7 +125,8 @@ so you can upgrade to newer versions easier. You can remove all these styles by
}
.vjs-default-skin .vjs-control:focus {
outline: 0;
outline: 1;
/* background-color: #555;*/
}
/* Hide control text visually, but have it available for screenreaders: h5bp.com/v */
@ -330,8 +331,8 @@ so you can upgrade to newer versions easier. You can remove all these styles by
---------------------------------------------------------*/
.vjs-default-skin .vjs-big-play-button {
display: block; /* Start hidden */ z-index: 2;
position: absolute; top: 50%; left: 50%; width: 8.0em; height: 8.0em; margin: -43px 0 0 -43px; text-align: center; vertical-align: center; cursor: pointer !important;
border: 0.3em solid #fff; opacity: 0.95;
position: absolute; top: 50%; left: 50%; width: 8.0em; height: 8.0em; margin: -42px 0 0 -42px; text-align: center; vertical-align: center; cursor: pointer !important;
border: 0.2em solid #fff; opacity: 0.95;
-webkit-border-radius: 25px; -moz-border-radius: 25px; border-radius: 25px;
background: #454545;
@ -437,9 +438,9 @@ div.vjs-loading-spinner .ball7 { opacity: 0.87; position:absolute; left: 0px; to
div.vjs-loading-spinner .ball8 { opacity: 1.00; position:absolute; left: 6px; top: 6px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
/* Feature Buttons (Captions/Subtitles/etc.)
/* Menu Buttons (Captions/Subtitles/etc.)
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-feature-button {
.vjs-default-skin .vjs-menu-button {
float: right; margin: 0.2em 0.5em 0 0; padding: 0; width: 3em; height: 2em; cursor: pointer !important;
border: 1px solid #111; -moz-border-radius: 0.3em; -webkit-border-radius: 0.3em; border-radius: 0.3em;
@ -453,15 +454,15 @@ div.vjs-loading-spinner .ball8 { opacity: 1.00; position:absolute; left: 6px; to
background: linear-gradient(top, #4d4d4d 0%,#3f3f3f 50%,#333333 50%,#252525 100%);
}
/* Button Icon */
.vjs-default-skin .vjs-feature-button div { background: url('video-js.png') 0px -75px no-repeat; width: 16px; height: 16px; margin: 0.2em auto 0; padding: 0; }
.vjs-default-skin .vjs-menu-button div { background: url('video-js.png') 0px -75px no-repeat; width: 16px; height: 16px; margin: 0.2em auto 0; padding: 0; }
.vjs-default-skin .vjs-menu-button:focus { border: 1px solid #fff; }
/* Button Pop-up Menu */
.vjs-default-skin .vjs-feature-button ul { display: none; /* Start hidden. Hover will show. */ }
.vjs-default-skin .vjs-feature-button:hover ul {
display: block;
.vjs-default-skin .vjs-menu-button ul {
display: none; /* Start hidden. Hover will show. */
opacity: 0.8;
list-style: none; padding: 0; margin: 0;
position: absolute; width: 10em; bottom: 2em;
padding: 0; margin: 0;
position: absolute; width: 10em; bottom: 2em; max-height: 15em;
left: -3.5em; /* Width of menu - width of button / 2 */
background-color: #111;
border: 2px solid #333;
@ -469,8 +470,32 @@ div.vjs-loading-spinner .ball8 { opacity: 1.00; position:absolute; left: 6px; to
-webkit-box-shadow: 0 2px 4px 0 #000; -moz-box-shadow: 0 2px 4px 0 #000; box-shadow: 0 2px 4px 0 #000;
overflow: auto;
}
.vjs-default-skin .vjs-feature-button ul li { list-style: none; margin: 0 0 2px 0; padding: 0; line-height: 1.5em; font-size: 1.4em; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: #333; }
.vjs-default-skin .vjs-feature-button ul li:hover { background-color: #ccc; color: #333; }
.vjs-default-skin .vjs-menu-button:hover ul,
.vjs-default-skin .vjs-menu-button:focus ul { display: block; list-style: none; }
.vjs-default-skin .vjs-menu-button ul li { list-style: none; margin: 0; padding: 0.3em 0 0.3em 20px; line-height: 1.4em; font-size: 1.2em; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; text-align: left; }
.vjs-default-skin .vjs-menu-button ul li:hover,
.vjs-default-skin .vjs-menu-button ul li:focus,
.vjs-default-skin .vjs-menu-button ul li.vjs-selected:hover,
.vjs-default-skin .vjs-menu-button ul li.vjs-selected:focus { background-color: #ccc; color: #111; }
.vjs-default-skin .vjs-menu-button ul li:focus { outline: 0; }
.vjs-default-skin .vjs-menu-button ul li.vjs-selected { text-decoration: underline; background: url('video-js.png') -125px -50px no-repeat; }
.vjs-default-skin .vjs-menu-button ul li.vjs-menu-title {
text-align: center; text-transform: uppercase; font-size: 1em; line-height: 2em; padding: 0; margin: 0 0 0.3em 0;
color: #fff; font-weight: bold;
cursor: default;
background: #4d4d4d;
background: -moz-linear-gradient(top, #4d4d4d 0%, #3f3f3f 50%, #333333 50%, #252525 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4d4d4d), color-stop(50%,#3f3f3f), color-stop(50%,#333333), color-stop(100%,#252525));
background: -webkit-linear-gradient(top, #4d4d4d 0%,#3f3f3f 50%,#333333 50%,#252525 100%);
background: -o-linear-gradient(top, #4d4d4d 0%,#3f3f3f 50%,#333333 50%,#252525 100%);
background: -ms-linear-gradient(top, #4d4d4d 0%,#3f3f3f 50%,#333333 50%,#252525 100%);
background: linear-gradient(top, #4d4d4d 0%,#3f3f3f 50%,#333333 50%,#252525 100%);
}
/* Subtitles Button */
.vjs-default-skin .vjs-captions-button div { background-position: -25px -75px; }
.vjs-default-skin .vjs-captions-button div { background-position: -25px -75px; }
.vjs-default-skin .vjs-chapters-button div { background-position: -100px -75px; }
.vjs-default-skin .vjs-chapters-button ul { width: 20em; left: -8.5em; /* Width of menu - width of button / 2 */ }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -105,7 +105,7 @@ _V_.Component = _V_.Class.extend({
options = options || {};
// Assume name of set is a lowercased name of the UI Class (PlayButton, etc.)
componentClass = options.componentClass || _V_.capitalize(name);
componentClass = options.componentClass || _V_.uc(name);
// Create a new object & element for this controls set
// If there's no .player, this is a player
@ -146,6 +146,20 @@ _V_.Component = _V_.Class.extend({
this.addClass("vjs-fade-out");
},
lockShowing: function(){
var style = this.el.style;
style.display = "block";
style.opacity = 1;
style.visiblity = "visible";
},
unlockShowing: function(){
var style = this.el.style;
style.display = "";
style.opacity = "";
style.visiblity = "";
},
addClass: function(classToAdd){
_V_.addClass(this.el, classToAdd);
},

98
src/controls.js vendored
View File

@ -8,6 +8,58 @@ _V_.Control = _V_.Component.extend({
});
/* Control Bar
================================================================================ */
_V_.ControlBar = _V_.Component.extend({
options: {
loadEvent: "play",
components: {
"playToggle": {},
"fullscreenToggle": {},
"currentTimeDisplay": {},
"timeDivider": {},
"durationDisplay": {},
"remainingTimeDisplay": {},
"progressControl": {},
"volumeControl": {},
"muteToggle": {}
}
},
init: function(player, options){
this._super(player, options);
player.addEvent("play", this.proxy(function(){
this.fadeIn();
this.player.addEvent("mouseover", this.proxy(this.fadeIn));
this.player.addEvent("mouseout", this.proxy(this.fadeOut));
}));
},
createElement: function(){
return _V_.createElement("div", {
className: "vjs-controls"
});
},
fadeIn: function(){
this._super();
this.player.triggerEvent("controlsvisible");
},
fadeOut: function(){
this._super();
this.player.triggerEvent("controlshidden");
},
lockShowing: function(){
this.el.style.opacity = "1";
}
});
/* Button - Base class for all buttons
================================================================================ */
_V_.Button = _V_.Control.extend({
@ -219,52 +271,6 @@ _V_.LoadingSpinner = _V_.Component.extend({
}
});
/* Control Bar
================================================================================ */
_V_.ControlBar = _V_.Component.extend({
options: {
loadEvent: "play",
components: {
"playToggle": {},
"fullscreenToggle": {},
"currentTimeDisplay": {},
"timeDivider": {},
"durationDisplay": {},
"remainingTimeDisplay": {},
"progressControl": {},
"volumeControl": {},
"muteToggle": {}
}
},
init: function(player, options){
this._super(player, options);
player.addEvent("play", this.proxy(function(){
this.fadeIn();
this.player.addEvent("mouseover", this.proxy(this.fadeIn));
this.player.addEvent("mouseout", this.proxy(this.fadeOut));
}));
},
createElement: function(){
return _V_.createElement("div", {
className: "vjs-controls"
});
},
fadeIn: function(){
this._super();
this.player.triggerEvent("controlsvisible");
},
fadeOut: function(){
this._super();
this.player.triggerEvent("controlshidden");
}
});
/* Time
================================================================================ */
_V_.CurrentTimeDisplay = _V_.Component.extend({

View File

@ -125,7 +125,7 @@ _V_.extend({
return h + m + s;
},
capitalize: function(string){
uc: function(string){
return string.charAt(0).toUpperCase() + string.slice(1);
},

View File

@ -1,52 +1,85 @@
// Player Extenstions
// TEXT TRACKS
// Text tracks are tracks of timed text events.
// Captions - text displayed over the video for the hearing impared
// Subtitles - text displayed over the video for those who don't understand langauge 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
// Player Track Functions - Functions add to the player object for easier access to tracks
_V_.merge(_V_.Player.prototype, {
// Add an array of text tracks. captions, subtitles, chapters, descriptions
// Track objects will be stored in the player.textTracks array
addTextTracks: function(trackObjects){
var tracks = this.textTracks = (this.textTracks) ? this.textTracks : [],
i = 0,
j = trackObjects.length,
track, Kind;
i = 0, j = trackObjects.length, track, Kind;
for (;i<j;i++) {
// HTML5 Spec says default to subtitles.
// Captitalize first letter to match class names
Kind = _V_.capitalize(trackObjects[i].kind || "subtitles");
// Uppercase (uc) first letter to match class names
Kind = _V_.uc(trackObjects[i].kind || "subtitles");
// Create correct texttrack class. CaptionsTrack, etc.
track = new _V_[Kind + "Track"](this, trackObjects[i]);
tracks.push(track);
if (track.default) {
// If track.default is set, start showing immediately
// TODO: Add a process to deterime the best track to show for the specific kind
// Incase there are mulitple defaulted tracks of the same kind
// Or the user has a set preference of a specific language that should override the default
if (track['default']) {
this.ready(_V_.proxy(track, track.show));
}
}
// Return the track so it can be appended to the display component
return this;
},
getTextTrackByKind: function(kind, srclang, label){
// Show a text track
// disableSameKind: disable all other tracks of the same kind. Value should be a track kind (captions, etc.)
showTextTrack: function(id, disableSameKind){
var tracks = this.textTracks,
i = 0,
j = tracks.length,
track;
track, showTrack, kind;
// Find Track with same ID
for (;i<j;i++) {
track = tracks[i];
if (track.kind == kind && (!srclang || track.language == srclang) && (!label || label == track.label)) {
break;
if (track.id === id) {
track.show();
showTrack = track;
// Disable tracks of the same kind
} else if (disableSameKind && track.kind == disableSameKind && track.mode > 0) {
track.disable();
}
}
return track;
// Get track kind from shown track or disableSameKind
kind = (showTrack) ? showTrack.kind : ((disableSameKind) ? disableSameKind : false);
// Trigger trackchange event, captionstrackchange, subtitlestrackchange, etc.
if (kind) {
this.triggerEvent(kind+"trackchange");
}
return this;
}
});
// Track Class
// Contains track methods for loading, showing, parsing cues of tracks
_V_.Track = _V_.Component.extend({
init: function(player, options){
this._super(player, options);
// Apply track info to track object
// Options will often be a track element
_V_.merge(this, {
// Build ID if one doesn't exist
id: options.id || ("vjs_" + options.kind + "_" + options.language + "_" + _V_.guid++),
@ -57,18 +90,23 @@ _V_.Track = _V_.Component.extend({
"default": options["default"], // 'default' is reserved-ish
title: options.title,
// Language - two letter string to represent track language, e.g. "en" for English
// readonly attribute DOMString language;
language: options.srclang,
// Track label e.g. "English"
// readonly attribute DOMString label;
label: options.label,
// All cues of the track. Cues have a startTime, endTime, text, and other properties.
// readonly attribute TextTrackCueList cues;
cues: [],
// ActiveCues is all cues that are currently showing
// readonly attribute TextTrackCueList activeCues;
activeCues: [],
// ReadyState describes if the text file has been loaded
// const unsigned short NONE = 0;
// const unsigned short LOADING = 1;
// const unsigned short LOADED = 2;
@ -76,21 +114,16 @@ _V_.Track = _V_.Component.extend({
// readonly attribute unsigned short readyState;
readyState: 0,
// Mode describes if the track is showing, hidden, or disabled
// const unsigned short OFF = 0;
// const unsigned short HIDDEN = 1;
// const unsigned short HIDDEN = 1; (still triggering cuechange events, but not visible)
// const unsigned short SHOWING = 2;
// attribute unsigned short mode;
mode: 0,
currentCue: false,
lastCueIndex: 0
mode: 0
});
// this.update = this.proxy(this.update);
// this.update.guid = this.kind + this.update.guid;
},
// Create basic div to hold cue text
createElement: function(){
return this._super("div", {
className: "vjs-" + this.kind + " vjs-text-track"
@ -142,10 +175,10 @@ _V_.Track = _V_.Component.extend({
this.mode = 0;
},
// Turn on cue tracking. Tracks that are showing OR hidden are active.
activate: function(){
if (this.readyState == 0) {
this.load();
}
// Load text file if it hasn't been yet.
if (this.readyState == 0) { this.load(); }
// Only activate if not already active.
if (this.mode == 0) {
@ -157,10 +190,13 @@ _V_.Track = _V_.Component.extend({
this.player.addEvent("ended", this.proxy(this.reset, this.id));
// Add to display
this.player.textTrackDisplay.addComponent(this);
if (this.kind == "captions" || this.kind == "subtitles") {
this.player.textTrackDisplay.addComponent(this);
}
}
},
// Turn off cue tracking.
deactivate: function(){
// Using unique ID for proxy function so other tracks don't remove listener
this.player.removeEvent("timeupdate", this.proxy(this.update, this.id));
@ -189,6 +225,7 @@ _V_.Track = _V_.Component.extend({
// Only load if not loaded yet.
if (this.readyState == 0) {
this.readyState = 1;
_V_.get(this.src, this.proxy(this.parseCues), this.proxy(this.onError));
}
@ -196,9 +233,12 @@ _V_.Track = _V_.Component.extend({
onError: function(err){
this.error = err;
this.readyState = 3;
this.triggerEvent("error");
},
// Parse the WebVTT text format for cue times.
// TODO: Separate parser into own class so alternative timed text formats can be used. (TTML, DFXP)
parseCues: function(srcContent) {
var cue, time, text,
lines = srcContent.split("\n"),
@ -239,6 +279,7 @@ _V_.Track = _V_.Component.extend({
}
}
this.readyState = 2;
this.triggerEvent("loaded");
},
@ -263,6 +304,7 @@ _V_.Track = _V_.Component.extend({
return time;
},
// Update active cues whenever timeupdate events are triggered on the player.
update: function(){
if (this.cues.length > 0) {
@ -274,24 +316,27 @@ _V_.Track = _V_.Component.extend({
var cues = this.cues,
// Create a new time box for this state.
nextChange = 0,
prevChange = this.player.duration(),
reverse = false,
newCues = [],
firstActiveIndex,
lastActiveIndex,
html = "",
cue, i, j;
newNextChange = this.player.duration(), // Start at beginning of the timeline
newPrevChange = 0, // Start at end
reverse = false, // Set the direction of the loop through the cues. Optimized the cue check.
newCues = [], // Store new active cues.
// Store where in the loop the current active cues are, to provide a smart starting point for the next loop.
firstActiveIndex, lastActiveIndex,
html = "", // Create cue text HTML to add to the display
cue, i, j; // Loop vars
// Check if time is going forwards or backwards (scrubbing/rewinding)
// If we know the direction we can optimize the starting position and direction of the loop through the cues array.
if (nextChange <= time) {
if (time >= this.nextChange || this.nextChange === undefined) { // NextChange should happen
// Forwards, so start at the index of the first active cue and loop forward
i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0;
} else {
// Backwards, so start at the index of the last active cue and loop backward
reverse = true;
i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length;
i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length - 1;
}
while (true) { // Loop until broken
@ -299,25 +344,30 @@ _V_.Track = _V_.Component.extend({
// Cue ended at this point
if (cue.endTime <= time) {
prevChange = Math.max(prevChange, cue.endTime);
newPrevChange = Math.max(newPrevChange, cue.endTime);
if (cue.active) {
cue.active = false;
}
// No earlier cues should have an active start time.
// Nevermind. Assume first cue could have a duration the same as the video.
// In that case we need to loop all the way back to the beginning.
// if (reverse && cue.startTime) { break; }
// Cue hasn't started
} else if (time < cue.startTime) {
nextChange = Math.min(nextChange, cue.startTime);
newNextChange = Math.min(newNextChange, cue.startTime);
if (cue.active) {
cue.active = false;
}
// No later cues should have an active start time.
break;
if (!reverse) { break; }
// Cue is current
} else if (time < cue.endTime) {
} else {
if (reverse) {
// Add cue to front of array to keep in time order
@ -335,81 +385,49 @@ _V_.Track = _V_.Component.extend({
lastActiveIndex = i;
}
nextChange = Math.min(nextChange, cue.endTime);
prevChange = Math.max(prevChange, cue.startTime);
newNextChange = Math.min(newNextChange, cue.endTime);
newPrevChange = Math.max(newPrevChange, cue.startTime);
cue.active = true;
}
if (reverse) {
// Reverse down the array of cues, break if at first
if (i === 0) { break; } else { i--; }
} else {
// Walk up the array fo cues, break if at last
if (i === cues.length - 1) { break; } else { i++; }
}
}
this.nextChange = nextChange;
this.prevChange = prevChange;
this.activeCues = newCues;
this.nextChange = newNextChange;
this.prevChange = newPrevChange;
this.firstActiveIndex = firstActiveIndex;
this.lastActiveIndex = lastActiveIndex;
for (i=0,j=newCues.length;i<j;i++) {
html += "<span class='vjs-tt-cue'>"+cue.text+"</span>";
}
this.el.innerHTML = html;
this.updateDisplay();
this.triggerEvent("cuechange");
}
}
// // Assuming all cues are in order by time, and do not overlap
// if (this.cues && this.cues.length > 0) {
// var time = this.player.currentTime();
// // If current cue should stay showing, don't do anything. Otherwise, find new cue.
// if (!this.currentCue || this.currentCue.startTime >= time || this.currentCue.endTime < time) {
// var newSubIndex = false,
// // // Loop in reverse if lastCue is after current time (optimization)
// // // Meaning the user is scrubbing in reverse or rewinding
// // reverse = (this.cues[this.lastCueIndex].startTime > time),
// // // If reverse, step back 1 becase we know it's not the lastCue
// // i = this.lastCueIndex - (reverse ? 1 : 0);
// // while (true) { // Loop until broken
// // if (reverse) { // Looping in reverse
// // // Stop if no more, or this cue ends before the current time (no earlier cues should apply)
// // if (i < 0 || this.cues[i].endTime < time) { break; }
// // // End is greater than time, so if start is less, show this cue
// // if (this.cues[i].startTime < time) {
// // newSubIndex = i;
// // break;
// // }
// // i--;
// // } else { // Looping forward
// // // Stop if no more, or this cue starts after time (no later cues should apply)
// // if (i >= this.cues.length || this.cues[i].startTime > time) { break; }
// // // Start is less than time, so if end is later, show this cue
// // if (this.cues[i].endTime > time) {
// // newSubIndex = i;
// // break;
// // }
// // i++;
// // }
// // }
//
// // // Set or clear current cue
// // if (newSubIndex !== false) {
// // this.currentCue = this.cues[newSubIndex];
// // this.lastCueIndex = newSubIndex;
// // this.updatePlayer(this.currentCue.text);
// // } else if (this.currentCue) {
// // this.currentCue = false;
// // this.updatePlayer("");
// // }
// }
// }
},
// Add cue HTML to display
updateDisplay: function(){
var cues = this.activeCues,
html = "",
i=0,j=cues.length;
for (;i<j;i++) {
html += "<span class='vjs-tt-cue'>"+cues[i].text+"</span>";
}
this.el.innerHTML = html;
},
// Set all loop helper values back
reset: function(){
this.nextChange = 0;
this.prevChange = this.player.duration();
@ -419,6 +437,7 @@ _V_.Track = _V_.Component.extend({
});
// Create specific track types
_V_.CaptionsTrack = _V_.Track.extend({
kind: "captions"
});
@ -427,39 +446,140 @@ _V_.SubtitlesTrack = _V_.Track.extend({
kind: "subtitles"
});
_V_.ChaptersTrack = _V_.Track.extend({
kind: "chapters"
});
/* Text Track Displays
/* Text Track Display
================================================================================ */
// Create a behavior type for each text track type (subtitlesDisplay, captionsDisplay, etc.).
// Then you can easily do something like.
// player.addBehavior(myDiv, "subtitlesDisplay");
// And the myDiv's content will be updated with the text change.
// Base class for all track displays. Should not be instantiated on its own.
// Global container for both subtitle and captions text. Simple div container.
_V_.TextTrackDisplay = _V_.Component.extend({
// init: function(player, options){
// this._super(player, options);
//
// player.addEvent(this.trackType + "update", _V_.proxy(this, this.update));
// },
createElement: function(){
return this._super("div", {
className: "vjs-" + this.trackType + " vjs-text-track-display"
className: "vjs-text-track-display"
});
},
update: function(){
this.el.innerHTML = this.player.textTrackValue(this.trackType);
}
});
// _V_.SubtitlesDisplay = _V_.TextTrackDisplay.extend({ trackType: "subtitles" });
// _V_.CaptionsDisplay = _V_.TextTrackDisplay.extend({ trackType: "captions" });
// _V_.ChaptersDisplay = _V_.TextTrackDisplay.extend({ trackType: "chapters" });
// _V_.DescriptionsDisplay = _V_.TextTrackDisplay.extend({ trackType: "descriptions" });
/* Menu
================================================================================ */
_V_.Menu = _V_.Component.extend({
init: function(player, options){
this._super(player, options);
},
addItem: function(component){
this.addComponent(component);
component.addEvent("click", this.proxy(function(){
this.unlockShowing();
}));
},
createElement: function(){
return this._super("ul", {
className: "vjs-menu"
});
}
});
_V_.MenuItem = _V_.Button.extend({
init: function(player, options){
this._super(player, options);
if (options.selected) {
this.addClass("vjs-selected");
}
},
createElement: function(type, attrs){
return this._super("li", _V_.merge({
className: "vjs-menu-item",
innerHTML: this.options.label
}, attrs));
},
onClick: function(){
this.selected(true);
},
selected: function(selected){
if (selected) {
this.addClass("vjs-selected");
} else {
this.removeClass("vjs-selected")
}
}
});
_V_.TextTrackMenuItem = _V_.MenuItem.extend({
init: function(player, options){
var track = this.track = options.track;
// Modify options for parent MenuItem class's init.
options.label = track.label;
options.selected = track["default"];
this._super(player, options);
this.player.addEvent(track.kind + "trackchange", _V_.proxy(this, this.update));
},
onClick: function(){
this._super();
this.player.showTextTrack(this.track.id, this.track.kind);
},
update: function(){
if (this.track.mode == 2) {
this.selected(true);
} else {
this.selected(false);
}
}
});
_V_.OffTextTrackMenuItem = _V_.TextTrackMenuItem.extend({
init: function(player, options){
// Create pseudo track info
// Requires options.kind
options.track = { kind: options.kind, player: player, label: "Off" }
this._super(player, options);
},
onClick: function(){
this._super();
this.player.showTextTrack(this.track.id, this.track.kind);
},
update: function(){
var tracks = this.player.textTracks,
i=0, j=tracks.length, track,
off = true;
for (;i<j;i++) {
track = tracks[i];
if (track.kind == this.track.kind && track.mode == 2) {
off = false;
}
}
if (off) {
this.selected(true);
} else {
this.selected(false);
}
}
});
/* Captions Button
================================================================================ */
@ -468,85 +588,77 @@ _V_.TextTrackButton = _V_.Button.extend({
init: function(player, options){
this._super(player, options);
this.list = _V_.createElement("ul");
this.menu = this.createMenu();
var count = 0,
lis = [],
li,
off = _V_.createElement("li", { innerHTML: "OFF" });
_V_.addEvent(off, "click", this.proxy(this.turnOff));
this.each(this.player.textTracks, function(track){
if (track.kind === this.kind) {
count++;
li = _V_.createElement("li", { innerHTML: track.label });
var tempLang = track.language,
tempLabel = track.label;
_V_.addEvent(li, "click", this.proxy(function(){
this.turnOn(tempLang, tempLabel);
}));
lis.push(li);
}
});
if (count > 0) {
// Only one lang
if (count == 1) {
lis[0].innerHTML = "ON";
lis.push(off);
} else {
// Add Off to the top of the list
lis.splice(0,0,off);
}
for (var i=0;i<lis.length;i++) {
this.list.appendChild(lis[i]);
}
this.el.appendChild(this.list);
} else {
if (this.items.length === 0) {
this.hide();
}
},
turnOn: function(lang, label){
var tracks = this.player.textTracks,
i=0, j=tracks.length,
track;
createMenu: function(){
var menu = new _V_.Menu(this.player);
for (;i<j;i++) {
track = tracks[i];
if (track.kind == this.kind) {
if (track.language == lang && track.label == label) {
track.show();
} else if (track.mode > 0) {
track.disable();
}
}
}
// Add a title list item to the top
menu.el.appendChild(_V_.createElement("li", {
className: "vjs-menu-title",
innerHTML: _V_.uc(this.kind)
}));
// Add an OFF menu item to turn all tracks off
menu.addItem(new _V_.OffTextTrackMenuItem(this.player, { kind: this.kind }))
this.items = this.createItems();
// Add menu items to the menu
this.each(this.items, function(item){
menu.addItem(item);
});
// Add list to element
this.addComponent(menu);
return menu;
},
turnOff: function(){
var tracks = this.player.textTracks,
i=0, j=tracks.length,
track;
for (;i<j;i++) {
track = tracks[i];
if (track.kind == this.kind && track.mode > 0) {
track.disable();
// Create a menu item for each text track
createItems: function(){
var items = [];
this.each(this.player.textTracks, function(track){
if (track.kind === this.kind) {
items.push(new _V_.TextTrackMenuItem(this.player, {
track: track
}));
}
}
});
return items;
},
buildCSSClass: function(){
return this.className + " vjs-feature-button " + this._super();
return this.className + " vjs-menu-button " + this._super();
},
// Focus - Add keyboard functionality to element
onFocus: function(){
// Show the menu, and keep showing when the menu items are in focus
this.menu.lockShowing();
// this.menu.el.style.display = "block";
// When tabbing through, the menu should hide when focus goes from the last menu item to the next tabbed element.
_V_.one(this.menu.el.childNodes[this.menu.el.childNodes.length - 1], "blur", this.proxy(function(){
this.menu.unlockShowing();
}));
},
// Can't turn off list display that we turned on with focus, because list would go away.
onBlur: function(){},
onClick: function(){
// When you click the button it adds focus, which will show the menu indefinitely.
// So we'll remove focus when the mouse leaves the button.
// Focus is needed for tab navigation.
this.one("mouseout", this.proxy(function(){
this.menu.unlockShowing();
this.el.blur();
}));
}
});
@ -563,10 +675,126 @@ _V_.SubtitlesButton = _V_.TextTrackButton.extend({
className: "vjs-subtitles-button"
});
// Chapters act much differently than other text tracks
// Cues are navigation vs. other tracks of alternative languages
_V_.ChaptersButton = _V_.TextTrackButton.extend({
kind: "chapters",
buttonText: "Chapters",
className: "vjs-chapters-button",
// Create a menu item for each text track
createItems: function(chaptersTrack){
var items = [];
this.each(this.player.textTracks, function(track){
if (track.kind === this.kind) {
items.push(new _V_.TextTrackMenuItem(this.player, {
track: track
}));
}
});
return items;
},
createMenu: function(){
var tracks = this.player.textTracks,
i = 0,
j = tracks.length,
track, chaptersTrack,
items = this.items = [];
for (;i<j;i++) {
track = tracks[i];
if (track.kind == this.kind && track["default"]) {
if (track.readyState < 2) {
this.chaptersTrack = track;
track.addEvent("loaded", this.proxy(this.createMenu));
return;
} else {
chaptersTrack = track;
break;
}
}
}
var menu = this.menu = new _V_.Menu(this.player);
menu.el.appendChild(_V_.createElement("li", {
className: "vjs-menu-title",
innerHTML: _V_.uc(this.kind)
}));
if (chaptersTrack) {
var cues = chaptersTrack.cues,
i = 0, j = cues.length, cue, mi;
for (;i<j;i++) {
cue = cues[i];
mi = new _V_.ChaptersTrackMenuItem(this.player, {
track: chaptersTrack,
cue: cue
});
items.push(mi);
menu.addComponent(mi);
}
}
// Add list to element
this.addComponent(menu);
if (this.items.length > 0) {
this.show();
}
return menu;
}
});
_V_.ChaptersTrackMenuItem = _V_.MenuItem.extend({
init: function(player, options){
var track = this.track = options.track,
cue = this.cue = options.cue,
currentTime = player.currentTime();
// Modify options for parent MenuItem class's init.
options.label = cue.text;
options.selected = (cue.startTime <= currentTime && currentTime < cue.endTime);
this._super(player, options);
track.addEvent("cuechange", _V_.proxy(this, this.update));
},
onClick: function(){
this._super();
this.player.currentTime(this.cue.startTime);
this.update(this.cue.startTime);
},
update: function(time){
var cue = this.cue,
currentTime = this.player.currentTime();
// _V_.log(currentTime, cue.startTime);
if (cue.startTime <= currentTime && currentTime < cue.endTime) {
this.selected(true);
} else {
this.selected(false);
}
}
});
// Add Buttons to controlBar
_V_.merge(_V_.ControlBar.prototype.options.components, {
"subtitlesButton": {},
"captionsButton": {}
"captionsButton": {},
"chaptersButton": {}
});
// _V_.Cue = _V_.Component.extend({