1
0
mirror of https://github.com/videojs/video.js.git synced 2025-07-17 01:42:41 +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 */ /* Text Track Styles */
/* Overall track holder for both captions and subtitles */ /* 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 */ /* Individual tracks */
.video-js .vjs-text-track { .video-js .vjs-text-track {
display: none; color: #fff; font-size: 1.4em; text-align: center; margin-bottom: 0.1em; 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 { .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 */ /* 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 { .vjs-default-skin .vjs-big-play-button {
display: block; /* Start hidden */ z-index: 2; 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; 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.3em solid #fff; opacity: 0.95; border: 0.2em solid #fff; opacity: 0.95;
-webkit-border-radius: 25px; -moz-border-radius: 25px; border-radius: 25px; -webkit-border-radius: 25px; -moz-border-radius: 25px; border-radius: 25px;
background: #454545; 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; 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; } 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; 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; 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%); background: linear-gradient(top, #4d4d4d 0%,#3f3f3f 50%,#333333 50%,#252525 100%);
} }
/* Button Icon */ /* 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 */ /* Button Pop-up Menu */
.vjs-default-skin .vjs-feature-button ul { display: none; /* Start hidden. Hover will show. */ } .vjs-default-skin .vjs-menu-button ul {
.vjs-default-skin .vjs-feature-button:hover ul { display: none; /* Start hidden. Hover will show. */
display: block;
opacity: 0.8; opacity: 0.8;
list-style: none; padding: 0; margin: 0; padding: 0; margin: 0;
position: absolute; width: 10em; bottom: 2em; position: absolute; width: 10em; bottom: 2em; max-height: 15em;
left: -3.5em; /* Width of menu - width of button / 2 */ left: -3.5em; /* Width of menu - width of button / 2 */
background-color: #111; background-color: #111;
border: 2px solid #333; 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; -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; 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-menu-button:hover ul,
.vjs-default-skin .vjs-feature-button ul li:hover { background-color: #ccc; color: #333; } .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 */ /* 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 || {}; options = options || {};
// Assume name of set is a lowercased name of the UI Class (PlayButton, etc.) // 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 // Create a new object & element for this controls set
// If there's no .player, this is a player // If there's no .player, this is a player
@ -146,6 +146,20 @@ _V_.Component = _V_.Class.extend({
this.addClass("vjs-fade-out"); 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){ addClass: function(classToAdd){
_V_.addClass(this.el, 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 /* Button - Base class for all buttons
================================================================================ */ ================================================================================ */
_V_.Button = _V_.Control.extend({ _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 /* Time
================================================================================ */ ================================================================================ */
_V_.CurrentTimeDisplay = _V_.Component.extend({ _V_.CurrentTimeDisplay = _V_.Component.extend({

View File

@ -125,7 +125,7 @@ _V_.extend({
return h + m + s; return h + m + s;
}, },
capitalize: function(string){ uc: function(string){
return string.charAt(0).toUpperCase() + string.slice(1); 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, { _V_.merge(_V_.Player.prototype, {
// Add an array of text tracks. captions, subtitles, chapters, descriptions // Add an array of text tracks. captions, subtitles, chapters, descriptions
// Track objects will be stored in the player.textTracks array
addTextTracks: function(trackObjects){ addTextTracks: function(trackObjects){
var tracks = this.textTracks = (this.textTracks) ? this.textTracks : [], var tracks = this.textTracks = (this.textTracks) ? this.textTracks : [],
i = 0, i = 0, j = trackObjects.length, track, Kind;
j = trackObjects.length,
track, Kind;
for (;i<j;i++) { for (;i<j;i++) {
// HTML5 Spec says default to subtitles. // HTML5 Spec says default to subtitles.
// Captitalize first letter to match class names // Uppercase (uc) first letter to match class names
Kind = _V_.capitalize(trackObjects[i].kind || "subtitles"); Kind = _V_.uc(trackObjects[i].kind || "subtitles");
// Create correct texttrack class. CaptionsTrack, etc. // Create correct texttrack class. CaptionsTrack, etc.
track = new _V_[Kind + "Track"](this, trackObjects[i]); track = new _V_[Kind + "Track"](this, trackObjects[i]);
tracks.push(track); 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)); 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, var tracks = this.textTracks,
i = 0, i = 0,
j = tracks.length, j = tracks.length,
track; track, showTrack, kind;
// Find Track with same ID
for (;i<j;i++) { for (;i<j;i++) {
track = tracks[i]; track = tracks[i];
if (track.kind == kind && (!srclang || track.language == srclang) && (!label || label == track.label)) { if (track.id === id) {
break; 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({ _V_.Track = _V_.Component.extend({
init: function(player, options){ init: function(player, options){
this._super(player, options); this._super(player, options);
// Apply track info to track object
// Options will often be a track element
_V_.merge(this, { _V_.merge(this, {
// Build ID if one doesn't exist // Build ID if one doesn't exist
id: options.id || ("vjs_" + options.kind + "_" + options.language + "_" + _V_.guid++), 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 "default": options["default"], // 'default' is reserved-ish
title: options.title, title: options.title,
// Language - two letter string to represent track language, e.g. "en" for English
// readonly attribute DOMString language; // readonly attribute DOMString language;
language: options.srclang, language: options.srclang,
// Track label e.g. "English"
// readonly attribute DOMString label; // readonly attribute DOMString label;
label: options.label, label: options.label,
// All cues of the track. Cues have a startTime, endTime, text, and other properties.
// readonly attribute TextTrackCueList cues; // readonly attribute TextTrackCueList cues;
cues: [], cues: [],
// ActiveCues is all cues that are currently showing
// readonly attribute TextTrackCueList activeCues; // readonly attribute TextTrackCueList activeCues;
activeCues: [], activeCues: [],
// ReadyState describes if the text file has been loaded
// const unsigned short NONE = 0; // const unsigned short NONE = 0;
// const unsigned short LOADING = 1; // const unsigned short LOADING = 1;
// const unsigned short LOADED = 2; // const unsigned short LOADED = 2;
@ -76,21 +114,16 @@ _V_.Track = _V_.Component.extend({
// readonly attribute unsigned short readyState; // readonly attribute unsigned short readyState;
readyState: 0, readyState: 0,
// Mode describes if the track is showing, hidden, or disabled
// const unsigned short OFF = 0; // 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; // const unsigned short SHOWING = 2;
// attribute unsigned short mode; // attribute unsigned short mode;
mode: 0, mode: 0
currentCue: false,
lastCueIndex: 0
}); });
// this.update = this.proxy(this.update);
// this.update.guid = this.kind + this.update.guid;
}, },
// Create basic div to hold cue text
createElement: function(){ createElement: function(){
return this._super("div", { return this._super("div", {
className: "vjs-" + this.kind + " vjs-text-track" className: "vjs-" + this.kind + " vjs-text-track"
@ -142,10 +175,10 @@ _V_.Track = _V_.Component.extend({
this.mode = 0; this.mode = 0;
}, },
// Turn on cue tracking. Tracks that are showing OR hidden are active.
activate: function(){ activate: function(){
if (this.readyState == 0) { // Load text file if it hasn't been yet.
this.load(); if (this.readyState == 0) { this.load(); }
}
// Only activate if not already active. // Only activate if not already active.
if (this.mode == 0) { if (this.mode == 0) {
@ -157,10 +190,13 @@ _V_.Track = _V_.Component.extend({
this.player.addEvent("ended", this.proxy(this.reset, this.id)); this.player.addEvent("ended", this.proxy(this.reset, this.id));
// Add to display // Add to display
if (this.kind == "captions" || this.kind == "subtitles") {
this.player.textTrackDisplay.addComponent(this); this.player.textTrackDisplay.addComponent(this);
} }
}
}, },
// Turn off cue tracking.
deactivate: function(){ deactivate: function(){
// Using unique ID for proxy function so other tracks don't remove listener // Using unique ID for proxy function so other tracks don't remove listener
this.player.removeEvent("timeupdate", this.proxy(this.update, this.id)); 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. // Only load if not loaded yet.
if (this.readyState == 0) { if (this.readyState == 0) {
this.readyState = 1;
_V_.get(this.src, this.proxy(this.parseCues), this.proxy(this.onError)); _V_.get(this.src, this.proxy(this.parseCues), this.proxy(this.onError));
} }
@ -196,9 +233,12 @@ _V_.Track = _V_.Component.extend({
onError: function(err){ onError: function(err){
this.error = err; this.error = err;
this.readyState = 3;
this.triggerEvent("error"); 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) { parseCues: function(srcContent) {
var cue, time, text, var cue, time, text,
lines = srcContent.split("\n"), lines = srcContent.split("\n"),
@ -239,6 +279,7 @@ _V_.Track = _V_.Component.extend({
} }
} }
this.readyState = 2;
this.triggerEvent("loaded"); this.triggerEvent("loaded");
}, },
@ -263,6 +304,7 @@ _V_.Track = _V_.Component.extend({
return time; return time;
}, },
// Update active cues whenever timeupdate events are triggered on the player.
update: function(){ update: function(){
if (this.cues.length > 0) { if (this.cues.length > 0) {
@ -274,24 +316,27 @@ _V_.Track = _V_.Component.extend({
var cues = this.cues, var cues = this.cues,
// Create a new time box for this state. // Create a new time box for this state.
nextChange = 0, newNextChange = this.player.duration(), // Start at beginning of the timeline
prevChange = this.player.duration(), newPrevChange = 0, // Start at end
reverse = false,
newCues = [], reverse = false, // Set the direction of the loop through the cues. Optimized the cue check.
firstActiveIndex, newCues = [], // Store new active cues.
lastActiveIndex,
html = "", // Store where in the loop the current active cues are, to provide a smart starting point for the next loop.
cue, i, j; 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) // 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 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 // Forwards, so start at the index of the first active cue and loop forward
i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0; i = (this.firstActiveIndex !== undefined) ? this.firstActiveIndex : 0;
} else { } else {
// Backwards, so start at the index of the last active cue and loop backward // Backwards, so start at the index of the last active cue and loop backward
reverse = true; reverse = true;
i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length; i = (this.lastActiveIndex !== undefined) ? this.lastActiveIndex : cues.length - 1;
} }
while (true) { // Loop until broken while (true) { // Loop until broken
@ -299,25 +344,30 @@ _V_.Track = _V_.Component.extend({
// Cue ended at this point // Cue ended at this point
if (cue.endTime <= time) { if (cue.endTime <= time) {
prevChange = Math.max(prevChange, cue.endTime); newPrevChange = Math.max(newPrevChange, cue.endTime);
if (cue.active) { if (cue.active) {
cue.active = false; 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 // Cue hasn't started
} else if (time < cue.startTime) { } else if (time < cue.startTime) {
nextChange = Math.min(nextChange, cue.startTime); newNextChange = Math.min(newNextChange, cue.startTime);
if (cue.active) { if (cue.active) {
cue.active = false; cue.active = false;
} }
// No later cues should have an active start time. // No later cues should have an active start time.
break; if (!reverse) { break; }
// Cue is current // Cue is current
} else if (time < cue.endTime) { } else {
if (reverse) { if (reverse) {
// Add cue to front of array to keep in time order // Add cue to front of array to keep in time order
@ -335,81 +385,49 @@ _V_.Track = _V_.Component.extend({
lastActiveIndex = i; lastActiveIndex = i;
} }
nextChange = Math.min(nextChange, cue.endTime); newNextChange = Math.min(newNextChange, cue.endTime);
prevChange = Math.max(prevChange, cue.startTime); newPrevChange = Math.max(newPrevChange, cue.startTime);
cue.active = true; cue.active = true;
} }
if (reverse) { if (reverse) {
// Reverse down the array of cues, break if at first
if (i === 0) { break; } else { i--; } if (i === 0) { break; } else { i--; }
} else { } else {
// Walk up the array fo cues, break if at last
if (i === cues.length - 1) { break; } else { i++; } if (i === cues.length - 1) { break; } else { i++; }
} }
} }
this.nextChange = nextChange; this.activeCues = newCues;
this.prevChange = prevChange; this.nextChange = newNextChange;
this.prevChange = newPrevChange;
this.firstActiveIndex = firstActiveIndex; this.firstActiveIndex = firstActiveIndex;
this.lastActiveIndex = lastActiveIndex; this.lastActiveIndex = lastActiveIndex;
for (i=0,j=newCues.length;i<j;i++) { this.updateDisplay();
html += "<span class='vjs-tt-cue'>"+cue.text+"</span>";
this.triggerEvent("cuechange");
}
}
},
// 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; this.el.innerHTML = html;
}
}
// // 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("");
// // }
// }
// }
}, },
// Set all loop helper values back
reset: function(){ reset: function(){
this.nextChange = 0; this.nextChange = 0;
this.prevChange = this.player.duration(); this.prevChange = this.player.duration();
@ -419,6 +437,7 @@ _V_.Track = _V_.Component.extend({
}); });
// Create specific track types
_V_.CaptionsTrack = _V_.Track.extend({ _V_.CaptionsTrack = _V_.Track.extend({
kind: "captions" kind: "captions"
}); });
@ -427,39 +446,140 @@ _V_.SubtitlesTrack = _V_.Track.extend({
kind: "subtitles" 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.). // Global container for both subtitle and captions text. Simple div container.
// 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.
_V_.TextTrackDisplay = _V_.Component.extend({ _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(){ createElement: function(){
return this._super("div", { 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" }); /* Menu
// _V_.CaptionsDisplay = _V_.TextTrackDisplay.extend({ trackType: "captions" }); ================================================================================ */
// _V_.ChaptersDisplay = _V_.TextTrackDisplay.extend({ trackType: "chapters" }); _V_.Menu = _V_.Component.extend({
// _V_.DescriptionsDisplay = _V_.TextTrackDisplay.extend({ trackType: "descriptions" });
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 /* Captions Button
================================================================================ */ ================================================================================ */
@ -468,85 +588,77 @@ _V_.TextTrackButton = _V_.Button.extend({
init: function(player, options){ init: function(player, options){
this._super(player, options); this._super(player, options);
this.list = _V_.createElement("ul"); this.menu = this.createMenu();
var count = 0, if (this.items.length === 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 {
this.hide(); this.hide();
} }
}, },
turnOn: function(lang, label){ createMenu: function(){
var tracks = this.player.textTracks, var menu = new _V_.Menu(this.player);
i=0, j=tracks.length,
track;
for (;i<j;i++) { // Add a title list item to the top
track = tracks[i]; menu.el.appendChild(_V_.createElement("li", {
if (track.kind == this.kind) { className: "vjs-menu-title",
if (track.language == lang && track.label == label) { innerHTML: _V_.uc(this.kind)
track.show(); }));
} else if (track.mode > 0) {
track.disable(); // 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(){ // Create a menu item for each text track
var tracks = this.player.textTracks, createItems: function(){
i=0, j=tracks.length, var items = [];
track; this.each(this.player.textTracks, function(track){
if (track.kind === this.kind) {
for (;i<j;i++) { items.push(new _V_.TextTrackMenuItem(this.player, {
track = tracks[i]; track: track
if (track.kind == this.kind && track.mode > 0) { }));
track.disable();
}
} }
});
return items;
}, },
buildCSSClass: function(){ 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" 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 // Add Buttons to controlBar
_V_.merge(_V_.ControlBar.prototype.options.components, { _V_.merge(_V_.ControlBar.prototype.options.components, {
"subtitlesButton": {}, "subtitlesButton": {},
"captionsButton": {} "captionsButton": {},
"chaptersButton": {}
}); });
// _V_.Cue = _V_.Component.extend({ // _V_.Cue = _V_.Component.extend({