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:
parent
72a423237c
commit
2aa5a2ee09
@ -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 |
@ -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
98
src/controls.js
vendored
@ -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({
|
||||
|
@ -125,7 +125,7 @@ _V_.extend({
|
||||
return h + m + s;
|
||||
},
|
||||
|
||||
capitalize: function(string){
|
||||
uc: function(string){
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
|
596
src/tracks.js
596
src/tracks.js
@ -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({
|
||||
|
Loading…
x
Reference in New Issue
Block a user