diff --git a/design/video-js.css b/design/video-js.css
index 6f296d670..f1d383ef5 100644
--- a/design/video-js.css
+++ b/design/video-js.css
@@ -52,9 +52,12 @@ body.vjs-full-window {
}
/* Text Track Styles */
+.video-js .vjs-text-track-display {
+ text-align: center; position: absolute; bottom: 4em; left: 1em; right: 1em;
+}
.video-js .vjs-text-track {
- display: none; color: #fff; font-size: 2em; text-align: center; position: absolute; bottom: 2em; left: 1em; right: 1em;
- background: rgb(0, 0, 0); background: rgba(0, 0, 0, 0.25);
+ display: none; color: #fff; font-size: 1.4em; text-align: center; margin-bottom: 0.1em;
+ background: rgb(0, 0, 0); background: rgba(0, 0, 0, 0.50);
}
.video-js .vjs-subtitles { color: #fff; }
.video-js .vjs-captions { color: #fc6; }
@@ -109,8 +112,8 @@ so you can upgrade to newer versions easier. You can remove all these styles by
/* Start hidden and with 0 opacity. Opacity is used to fade in modern browsers. */
/* Can't use display block to hide initially because widths of slider handles aren't calculated and avaialbe for positioning correctly. */
- visibility: hidden;
- opacity: 0;
+/* visibility: hidden;
+ opacity: 0;*/
}
/* General styles for individual controls. */
@@ -447,3 +450,20 @@ div.vjs-loading-spinner .ball8 { opacity: 1.00; position:absolute; left: 6px; to
/* Play Icon */
.vjs-default-skin .vjs-captions-control div { background: url('video-js.png') 0px -75px; width: 16px; height: 16px; margin: 0.2em auto 0; padding: 0; }
+.vjs-default-skin .vjs-captions-control ul {
+ display: none; /* Start hidden. Hover will show. */
+}
+.vjs-default-skin .vjs-captions-control:hover ul {
+ display: block;
+ opacity: 0.8;
+ list-style: none; padding: 0; margin: 0;
+ position: absolute; width: 10em; height: 10em; bottom: 2em;
+ left: -3.5em; /* Width of menu - width of button / 2 */
+ background-color: #111;
+ border: 2px solid #333;
+ -moz-border-radius: 0.7em; -webkit-border-radius: 1em; border-radius: .5em;
+ -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-captions-control 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-captions-control ul li:hover { background-color: #ccc; color: #333; }
\ No newline at end of file
diff --git a/src/component.js b/src/component.js
index f97d552ec..b548605b2 100644
--- a/src/component.js
+++ b/src/component.js
@@ -79,7 +79,8 @@ _V_.Component = _V_.Class.extend({
// Allow waiting to add components until a specific event is called
var tempAdd = this.proxy(function(){
- this.addComponent(name, opts);
+ // Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy.
+ this[name] = this.addComponent(name, opts);
});
if (opts.loadEvent) {
@@ -95,23 +96,34 @@ _V_.Component = _V_.Class.extend({
// Will generate a new child component and then append child component's element to this component's element.
// Takes either the name of the UI component class, or an object that contains a name, UI Class, and options.
addComponent: function(name, options){
- var componentClass, component;
+ var component, componentClass;
- // Make sure options is at least an empty object to protect against errors
- options = options || {};
+ // If string, create new component with options
+ if (typeof name == "string") {
- // Assume name of set is a lowercased name of the UI Class (PlayButton, etc.)
- componentClass = options.componentClass || _V_.capitalize(name);
+ // Make sure options is at least an empty object to protect against errors
+ options = options || {};
- // Create a new object & element for this controls set
- // If there's no .player, this is a player
- component = new _V_[componentClass](this.player || this, options);
+ // Assume name of set is a lowercased name of the UI Class (PlayButton, etc.)
+ componentClass = options.componentClass || _V_.capitalize(name);
+
+ // Create a new object & element for this controls set
+ // If there's no .player, this is a player
+ component = new _V_[componentClass](this.player || this, options);
+
+ } else {
+ component = name;
+ }
// Add the UI object's element to the container div (box)
this.el.appendChild(component.el);
- // Set property name on player. Could cause conflicts with other prop names, but it's worth making refs easy.
- this[name] = component;
+ // Return so it can stored on parent object if desired.
+ return component;
+ },
+
+ removeComponent: function(component){
+ this.el.removeChild(component.el);
},
/* Display
@@ -144,8 +156,8 @@ _V_.Component = _V_.Class.extend({
/* Events
================================================================================ */
- addEvent: function(type, fn){
- return _V_.addEvent(this.el, type, _V_.proxy(this, fn));
+ addEvent: function(type, fn, uid){
+ return _V_.addEvent(this.el, type, _V_.proxy(this, fn, uid));
},
removeEvent: function(type, fn){
return _V_.removeEvent(this.el, type, fn);
diff --git a/src/controls.js b/src/controls.js
index ac9c5c197..f1893f79d 100644
--- a/src/controls.js
+++ b/src/controls.js
@@ -783,79 +783,4 @@ _V_.Poster = _V_.Button.extend({
onClick: function(){
this.player.play();
}
-});
-
-
-/* Text Track Displays
-================================================================================ */
-// 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.
-_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"
- });
- },
-
- 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"
-
-});
-
-/* Captions Button
-================================================================================ */
-_V_.CaptionsButton = _V_.Button.extend({
-
- buttonText: "Captions",
-
- buildCSSClass: function(){
- return "vjs-captions-control " + this._super();
- },
-
- onClick: function(){
- this.player.textTrackValue("captions", "Hi
hi2")
- var captionsDisplay = this.player.captionsDisplay;
- if (captionsDisplay.el.style.display == "block") {
- captionsDisplay.hide();
- } else {
- captionsDisplay.show();
- }
- }
-
});
\ No newline at end of file
diff --git a/src/core.js b/src/core.js
index c2812f0b7..bcfe0a691 100644
--- a/src/core.js
+++ b/src/core.js
@@ -64,11 +64,10 @@ VideoJS.options = {
// Included control sets
components: {
"poster": {},
+ "textTrackDisplay": {},
"loadingSpinner": {},
"bigPlayButton": {},
- "controlBar": {},
- "subtitlesDisplay": {},
- "captionsDisplay": {}
+ "controlBar": {}
}
// components: [
diff --git a/src/lib.js b/src/lib.js
index ba877e6bc..5e30f4d54 100644
--- a/src/lib.js
+++ b/src/lib.js
@@ -199,17 +199,25 @@ _V_.extend({
/* Proxy (a.k.a Bind or Context). A simple method for changing the context of a function
It also stores a unique id on the function so it can be easily removed from events
================================================================================ */
- proxy: function(context, fn) {
- // Make sure the function has a unique ID
- if (!fn.guid) { fn.guid = _V_.guid++; }
+ proxy: function(context, fn, uid) {
// Create the new function that changes the context
var ret = function() {
return fn.apply(context, arguments);
- };
+ },
+
+ // Make sure the function has a unique ID
+ guid = fn.guid || _V_.guid++;
+
+ // Allow for the ability to individualize this function
+ // Needed in the case where multiple items might share the same prototype function
+ // IF both items add an event listener with the same function, then you try to remove just one
+ // it will remove both because they both have the same guid.
+ // when using this, you need to use the proxy method both times.
+ if (uid) { guid = uid + "_" + guid }
// Give the new function the same ID
// (so that they are equivalent and can be easily removed)
- ret.guid = fn.guid;
+ ret.guid = guid;
return ret;
},
diff --git a/src/player.js b/src/player.js
index 54e0c53cb..2cc6f3658 100644
--- a/src/player.js
+++ b/src/player.js
@@ -83,6 +83,11 @@ _V_.Player = _V_.Component.extend({
});
}
+ // Tracks defined in tracks.js
+ if (options.tracks && options.tracks.length > 0) {
+ this.addTextTracks(options.tracks);
+ }
+
// If there are no sources when the player is initialized,
// load the first supported playback technology.
if (!options.sources || options.sources.length == 0) {
@@ -145,15 +150,14 @@ _V_.Player = _V_.Component.extend({
});
}
if (c.nodeName == "TRACK") {
- options.tracks.push(new _V_.Track({
+ options.tracks.push({
src: c.getAttribute("src"),
kind: c.getAttribute("kind"),
srclang: c.getAttribute("srclang"),
label: c.getAttribute("label"),
'default': c.getAttribute("default") !== null,
title: c.getAttribute("title")
- }, this));
-
+ });
}
}
}
@@ -789,15 +793,6 @@ _V_.Player = _V_.Component.extend({
return this.techGet("currentSrc") || this.values.src || "";
},
- textTrackValue: function(kind, value){
- if (value !== undefined) {
- this.values[kind] = value;
- this.triggerEvent(kind+"update");
- return this;
- }
- return this.values[kind];
- },
-
// Attributes/Options
preload: function(value){
if (value !== undefined) {
@@ -825,7 +820,6 @@ _V_.Player = _V_.Component.extend({
},
controls: function(){ return this.options.controls; },
- textTracks: function(){ return this.options.tracks; },
poster: function(){ return this.techGet("poster"); },
error: function(){ return this.techGet("error"); },
ended: function(){ return this.techGet("ended"); }
diff --git a/src/tracks.js b/src/tracks.js
index 40e7ef781..30bb58a2e 100644
--- a/src/tracks.js
+++ b/src/tracks.js
@@ -1,29 +1,203 @@
-_V_.Track = function(attributes, player){
- // Store reference to the parent player
- this.player = player;
+// Player Extenstions
+_V_.merge(_V_.Player.prototype, {
- this.src = attributes.src;
- this.kind = attributes.kind;
- this.srclang = attributes.srclang;
- this.label = attributes.label;
- this["default"] = attributes["default"]; // 'default' is reserved-ish
- this.title = attributes.title;
+ // Add an array of text tracks. captions, subtitles, chapters, descriptions
+ addTextTracks: function(trackObjects){
+ var tracks = this.textTracks = (this.textTracks) ? this.textTracks : [],
+ i = 0,
+ j = trackObjects.length,
+ track, Kind;
- this.cues = [];
- this.currentCue = false;
- this.lastCueIndex = 0;
+ for (;i');
// Add this cue
this.cues.push(cue);
}
}
+
+ this.triggerEvent("loaded");
},
parseCueTime: function(timeText) {
var parts = timeText.split(':'),
- time = 0;
+ time = 0,
+ flags, seconds;
// hours => seconds
time += parseFloat(parts[0])*60*60;
// minutes => seconds
time += parseFloat(parts[1])*60;
- // get seconds
- var seconds = parts[2].split(/\.|,/); // Either . or ,
- time += parseFloat(seconds[0]);
+ // get seconds and flags
+ // TODO: Make additional cue layout settings work
+ flags = parts[2].split(/\s+/)
+ // Seconds is the first part before any spaces.
+ // Could use either . or , for decimal
+ seconds = flags.splice(0,1)[0].split(/\.|,/);
+ time += parseFloat(seconds);
// add miliseconds
ms = parseFloat(seconds[1]);
if (ms) { time += ms/1000; }
@@ -86,55 +264,302 @@ _V_.Track.prototype = {
},
update: function(){
- // Assuming all cues are in order by time, and do not overlap
- if (this.cues && this.cues.length > 0) {
+ if (this.cues.length > 0) {
+
+ // Get curent player time
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++;
- }
+
+ // Check if the new time is outside the time box created by the the last update.
+ if (this.prevChange === undefined || time < this.prevChange || this.nextChange <= time) {
+ 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;
+
+ // 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) {
+ // 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;
}
- // 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("");
+ while (true) { // Loop until broken
+ cue = cues[i];
+
+ // Cue ended at this point
+ if (cue.endTime <= time) {
+ prevChange = Math.max(prevChange, cue.endTime);
+
+ if (cue.active) {
+ cue.active = false;
+ }
+
+ // Cue hasn't started
+ } else if (time < cue.startTime) {
+ nextChange = Math.min(nextChange, cue.startTime);
+
+ if (cue.active) {
+ cue.active = false;
+ }
+
+ // No later cues should have an active start time.
+ break;
+
+ // Cue is current
+ } else if (time < cue.endTime) {
+
+ if (reverse) {
+ // Add cue to front of array to keep in time order
+ newCues.splice(0,0,cue);
+
+ // If in reverse, the first current cue is our lastActiveCue
+ if (lastActiveIndex === undefined) { lastActiveIndex = i; }
+ firstActiveIndex = i;
+ } else {
+ // Add cue to end of array
+ newCues.push(cue);
+
+ // If forward, the first current cue is our firstActiveIndex
+ if (firstActiveIndex === undefined) { firstActiveIndex = i; }
+ lastActiveIndex = i;
+ }
+
+ nextChange = Math.min(nextChange, cue.endTime);
+ prevChange = Math.max(prevChange, cue.startTime);
+
+ cue.active = true;
+ }
+
+ if (reverse) {
+ if (i === 0) { break; } else { i--; }
+ } else {
+ if (i === cues.length - 1) { break; } else { i++; }
+ }
+
+ }
+
+ this.nextChange = nextChange;
+ this.prevChange = prevChange;
+ this.firstActiveIndex = firstActiveIndex;
+ this.lastActiveIndex = lastActiveIndex;
+
+ for (i=0,j=newCues.length;i"+cue.text+"";
+ }
+
+ 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("");
+ // // }
+ // }
+ // }
+ },
+
+ reset: function(){
+ this.nextChange = 0;
+ this.prevChange = this.player.duration();
+ this.firstActiveIndex = 0;
+ this.lastActiveIndex = 0;
+ }
+
+});
+
+_V_.CaptionsTrack = _V_.Track.extend({
+ kind: "captions"
+});
+
+_V_.SubtitlesTrack = _V_.Track.extend({
+ kind: "subtitles"
+});
+
+
+/* Text Track Displays
+================================================================================ */
+// 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.
+_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"
+ });
+ },
+
+ 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" });
+
+/* Captions Button
+================================================================================ */
+_V_.TextTrackButton = _V_.Button.extend({
+
+ buttonText: "Captions",
+
+ init: function(player, options){
+ this._super(player, options);
+
+ this.list = _V_.createElement("ul");
+
+ 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 === "captions") {
+ 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 0) {
+ track.disable();
}
}
}
},
+
+ turnOff: function(){
+ var tracks = this.player.textTracks,
+ i=0, j=tracks.length,
+ track;
- // Update the stored value for the current track kind
- // and trigger an event to update all text track displays.
- updatePlayer: function(text){
- this.player.textTrackValue(this.kind, text);
+ for (;i 0) {
+ track.disable();
+ }
+ }
}
-};
+
+});
+
+_V_.CaptionsButton = _V_.Button.extend({
+ kind: "captions",
+ buttonText: "Captions"
+});
+
+// _V_.Cue = _V_.Component.extend({
+// init: function(player, options){
+// this._super(player, options);
+// }
+// });
\ No newline at end of file