From 2aa5a2ee09ec754491b6b8d7d1de9288b8df003e Mon Sep 17 00:00:00 2001 From: Steve Heffernan Date: Fri, 16 Mar 2012 12:29:38 -0700 Subject: [PATCH] Finishing off TextTrack support. --- design/video-js.css | 55 ++-- design/video-js.png | Bin 8128 -> 8235 bytes src/component.js | 16 +- src/controls.js | 98 ++++---- src/lib.js | 2 +- src/tracks.js | 596 ++++++++++++++++++++++++++++++-------------- 6 files changed, 520 insertions(+), 247 deletions(-) diff --git a/design/video-js.css b/design/video-js.css index 953d12a7e..04d2aa722 100644 --- a/design/video-js.css +++ b/design/video-js.css @@ -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; } \ No newline at end of file +.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 */ } \ No newline at end of file diff --git a/design/video-js.png b/design/video-js.png index e16532f72748ff95a0f1ee54deeb4e964b7225c1..100bc7f8ed1e56b3c61417dba339abc131898f66 100644 GIT binary patch literal 8235 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 JRMs@~e*sa?f>{6n diff --git a/src/component.js b/src/component.js index 18bcfdcef..dfbea2aef 100644 --- a/src/component.js +++ b/src/component.js @@ -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); }, diff --git a/src/controls.js b/src/controls.js index 941576a90..0e27c90fb 100644 --- a/src/controls.js +++ b/src/controls.js @@ -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({ diff --git a/src/lib.js b/src/lib.js index e3f4fafad..a7094eb1a 100644 --- a/src/lib.js +++ b/src/lib.js @@ -125,7 +125,7 @@ _V_.extend({ return h + m + s; }, - capitalize: function(string){ + uc: function(string){ return string.charAt(0).toUpperCase() + string.slice(1); }, diff --git a/src/tracks.js b/src/tracks.js index 30dd29d31..899685e7f 100644 --- a/src/tracks.js +++ b/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 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"; - } - - 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"; + } + + 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 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(); - } - } - } + // 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 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 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({