blob: f78696d6490fef9f66e990bf73f20fac20e9fde9 [file] [log] [blame]
function createControls(root, video, host)
{
return new Controller(root, video, host);
};
function Controller(root, video, host)
{
this.video = video;
this.root = root;
this.host = host;
this.controls = {};
this.listeners = {};
this.isLive = false;
this.statusHidden = true;
this.hasVisualMedia = false;
this.addVideoListeners();
this.createBase();
this.createControls();
this.updateBase();
this.updateControls();
this.updateDuration();
this.updateProgress();
this.updateTime();
this.updateReadyState();
this.updatePlaying();
this.updateThumbnail();
this.updateCaptionButton();
this.updateCaptionContainer();
this.updateFullscreenButton();
this.updateVolume();
this.updateHasAudio();
this.updateHasVideo();
};
/* Enums */
Controller.InlineControls = 0;
Controller.FullScreenControls = 1;
Controller.PlayAfterSeeking = 0;
Controller.PauseAfterSeeking = 1;
/* Globals */
Controller.gLastTimelineId = 0;
Controller.prototype = {
/* Constants */
HandledVideoEvents: {
loadstart: 'handleLoadStart',
error: 'handleError',
abort: 'handleAbort',
suspend: 'handleSuspend',
stalled: 'handleStalled',
waiting: 'handleWaiting',
emptied: 'handleReadyStateChange',
loadedmetadata: 'handleReadyStateChange',
loadeddata: 'handleReadyStateChange',
canplay: 'handleReadyStateChange',
canplaythrough: 'handleReadyStateChange',
timeupdate: 'handleTimeUpdate',
durationchange: 'handleDurationChange',
playing: 'handlePlay',
pause: 'handlePause',
progress: 'handleProgress',
volumechange: 'handleVolumeChange',
webkitfullscreenchange: 'handleFullscreenChange',
webkitbeginfullscreen: 'handleFullscreenChange',
webkitendfullscreen: 'handleFullscreenChange',
},
HideControlsDelay: 4 * 1000,
RewindAmount: 30,
MaximumSeekRate: 8,
SeekDelay: 1500,
ClassNames: {
active: 'active',
exit: 'exit',
failed: 'failed',
hidden: 'hidden',
hiding: 'hiding',
hourLongTime: 'hour-long-time',
list: 'list',
muteBox: 'mute-box',
muted: 'muted',
paused: 'paused',
playing: 'playing',
selected: 'selected',
show: 'show',
thumbnail: 'thumbnail',
thumbnailImage: 'thumbnail-image',
thumbnailTrack: 'thumbnail-track',
volumeBox: 'volume-box',
noVideo: 'no-video',
down: 'down',
out: 'out',
},
KeyCodes: {
enter: 13,
escape: 27,
space: 32,
pageUp: 33,
pageDown: 34,
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40
},
extend: function(child)
{
for (var property in this) {
if (!child.hasOwnProperty(property))
child[property] = this[property];
}
},
UIString: function(developmentString, replaceString, replacementString)
{
var localized = UIStringTable[developmentString];
if (replaceString && replacementString)
return localized.replace(replaceString, replacementString);
if (localized)
return localized;
console.error("Localization for string \"" + developmentString + "\" not found.");
return "LOCALIZED STRING NOT FOUND";
},
listenFor: function(element, eventName, handler, useCapture)
{
if (typeof useCapture === 'undefined')
useCapture = false;
if (!(this.listeners[eventName] instanceof Array))
this.listeners[eventName] = [];
this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
element.addEventListener(eventName, this, useCapture);
},
stopListeningFor: function(element, eventName, handler, useCapture)
{
if (typeof useCapture === 'undefined')
useCapture = false;
if (!(this.listeners[eventName] instanceof Array))
return;
this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
});
element.removeEventListener(eventName, this, useCapture);
},
addVideoListeners: function()
{
for (var name in this.HandledVideoEvents) {
this.listenFor(this.video, name, this.HandledVideoEvents[name]);
};
/* text tracks */
this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
/* audio tracks */
this.listenFor(this.video.audioTracks, 'change', this.updateHasAudio);
this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
/* video tracks */
this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
/* controls attribute */
this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
},
removeVideoListeners: function()
{
for (var name in this.HandledVideoEvents) {
this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
};
/* text tracks */
this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
/* audio tracks */
this.stopListeningFor(this.video.audioTracks, 'change', this.updateHasAudio);
this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio);
this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio);
/* video tracks */
this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
/* controls attribute */
this.controlsObserver.disconnect();
delete(this.controlsObserver);
},
handleEvent: function(event)
{
var preventDefault = false;
try {
if (event.target === this.video) {
var handlerName = this.HandledVideoEvents[event.type];
var handler = this[handlerName];
if (handler && handler instanceof Function)
handler.call(this, event);
}
if (!(this.listeners[event.type] instanceof Array))
return;
this.listeners[event.type].forEach(function(entry) {
if (entry.element === event.currentTarget && entry.handler instanceof Function)
preventDefault |= entry.handler.call(this, event);
}, this);
} catch(e) {
if (window.console)
console.error(e);
}
if (preventDefault) {
event.stopPropagation();
event.preventDefault();
}
},
createBase: function()
{
var base = this.base = document.createElement('div');
base.setAttribute('pseudo', '-webkit-media-controls');
this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
this.listenFor(base, 'mouseout', this.handleWrapperMouseOut);
if (this.host.textTrackContainer)
base.appendChild(this.host.textTrackContainer);
},
shouldHaveAnyUI: function()
{
return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length);
},
shouldHaveControls: function()
{
return this.video.controls || this.isFullScreen();
},
setNeedsTimelineMetricsUpdate: function()
{
this.timelineMetricsNeedsUpdate = true;
},
updateTimelineMetricsIfNeeded: function()
{
if (this.timelineMetricsNeedsUpdate) {
this.timelineLeft = this.controls.timeline.offsetLeft;
this.timelineWidth = this.controls.timeline.offsetWidth;
this.timelineHeight = this.controls.timeline.offsetHeight;
this.timelineMetricsNeedsUpdate = false;
}
},
updateBase: function()
{
if (this.shouldHaveAnyUI()) {
if (!this.base.parentNode) {
this.root.appendChild(this.base);
}
} else {
if (this.base.parentNode) {
this.base.parentNode.removeChild(this.base);
}
}
},
createControls: function()
{
var panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div');
panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent');
var panel = this.controls.panel = document.createElement('div');
panel.setAttribute('pseudo', '-webkit-media-controls-panel');
panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
panel.setAttribute('role', 'toolbar');
this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
this.listenFor(panel, 'click', this.handlePanelClick);
this.listenFor(panel, 'dblclick', this.handlePanelClick);
var rewindButton = this.controls.rewindButton = document.createElement('button');
rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
var seekBackButton = this.controls.seekBackButton = document.createElement('button');
seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
var playButton = this.controls.playButton = document.createElement('button');
playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
playButton.setAttribute('aria-label', this.UIString('Play'));
this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
var statusDisplay = this.controls.statusDisplay = document.createElement('div');
statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
statusDisplay.classList.add(this.ClassNames.hidden);
var timelineBox = this.controls.timelineBox = document.createElement('div');
timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
var currentTime = this.controls.currentTime = document.createElement('div');
currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
currentTime.setAttribute('role', 'timer');
var timeline = this.controls.timeline = document.createElement('input');
this.timelineID = ++Controller.gLastTimelineId;
timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
timeline.setAttribute('aria-label', this.UIString('Duration'));
timeline.style.backgroundImage = '-webkit-canvas(timeline-' + this.timelineID + ')';
timeline.type = 'range';
timeline.value = 0;
this.listenFor(timeline, 'input', this.handleTimelineChange);
this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
timeline.step = .01;
var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
var thumbnail = this.controls.thumbnail = document.createElement('div');
thumbnail.classList.add(this.ClassNames.thumbnail);
var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
var remainingTime = this.controls.remainingTime = document.createElement('div');
remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
remainingTime.setAttribute('role', 'timer');
var muteBox = this.controls.muteBox = document.createElement('div');
muteBox.classList.add(this.ClassNames.muteBox);
var muteButton = this.controls.muteButton = document.createElement('button');
muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
muteButton.setAttribute('aria-label', this.UIString('Mute'));
this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
var minButton = this.controls.minButton = document.createElement('button');
minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
this.listenFor(minButton, 'click', this.handleMinButtonClicked);
var maxButton = this.controls.maxButton = document.createElement('button');
maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
var volumeBox = this.controls.volumeBox = document.createElement('div');
volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
volumeBox.classList.add(this.ClassNames.volumeBox);
var volume = this.controls.volume = document.createElement('input');
volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
volume.setAttribute('aria-label', this.UIString('Volume'));
volume.type = 'range';
volume.min = 0;
volume.max = 1;
volume.step = .01;
this.listenFor(volume, 'input', this.handleVolumeSliderInput);
var captionButton = this.controls.captionButton = document.createElement('button');
captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
captionButton.setAttribute('aria-label', this.UIString('Captions'));
captionButton.setAttribute('aria-haspopup', 'true');
captionButton.setAttribute('aria-owns', 'audioTrackMenu');
this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
},
setControlsType: function(type)
{
if (type === this.controlsType)
return;
this.controlsType = type;
this.reconnectControls();
},
setIsLive: function(live)
{
if (live === this.isLive)
return;
this.isLive = live;
this.updateStatusDisplay();
this.reconnectControls();
},
reconnectControls: function()
{
this.disconnectControls();
if (this.controlsType === Controller.InlineControls)
this.configureInlineControls();
else if (this.controlsType == Controller.FullScreenControls)
this.configureFullScreenControls();
if (this.shouldHaveControls())
this.addControls();
},
disconnectControls: function(event)
{
for (var item in this.controls) {
var control = this.controls[item];
if (control && control.parentNode)
control.parentNode.removeChild(control);
}
},
configureInlineControls: function()
{
if (!this.isLive)
this.controls.panel.appendChild(this.controls.rewindButton);
this.controls.panel.appendChild(this.controls.playButton);
this.controls.panel.appendChild(this.controls.statusDisplay);
if (!this.isLive) {
this.controls.panel.appendChild(this.controls.timelineBox);
this.controls.timelineBox.appendChild(this.controls.currentTime);
this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
this.controls.thumbnailTrack.appendChild(this.controls.timeline);
this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
this.controls.timelineBox.appendChild(this.controls.remainingTime);
}
this.controls.panel.appendChild(this.controls.muteBox);
this.controls.muteBox.appendChild(this.controls.volumeBox);
this.controls.volumeBox.appendChild(this.controls.volume);
this.controls.muteBox.appendChild(this.controls.muteButton);
this.controls.panel.appendChild(this.controls.captionButton);
if (!this.isAudio())
this.controls.panel.appendChild(this.controls.fullscreenButton);
this.controls.panel.style.removeProperty('left');
this.controls.panel.style.removeProperty('top');
this.controls.panel.style.removeProperty('bottom');
},
configureFullScreenControls: function()
{
this.controls.panel.appendChild(this.controls.volumeBox);
this.controls.volumeBox.appendChild(this.controls.minButton);
this.controls.volumeBox.appendChild(this.controls.volume);
this.controls.volumeBox.appendChild(this.controls.maxButton);
this.controls.panel.appendChild(this.controls.seekBackButton);
this.controls.panel.appendChild(this.controls.playButton);
this.controls.panel.appendChild(this.controls.seekForwardButton);
this.controls.panel.appendChild(this.controls.captionButton);
if (!this.isAudio())
this.controls.panel.appendChild(this.controls.fullscreenButton);
if (!this.isLive) {
this.controls.panel.appendChild(this.controls.timelineBox);
this.controls.timelineBox.appendChild(this.controls.currentTime);
this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
this.controls.thumbnailTrack.appendChild(this.controls.timeline);
this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
this.controls.timelineBox.appendChild(this.controls.remainingTime);
} else
this.controls.panel.appendChild(this.controls.statusDisplay);
},
updateControls: function()
{
if (this.isFullScreen())
this.setControlsType(Controller.FullScreenControls);
else
this.setControlsType(Controller.InlineControls);
this.setNeedsTimelineMetricsUpdate();
},
updateStatusDisplay: function(event)
{
if (this.video.error !== null)
this.controls.statusDisplay.innerText = this.UIString('Error');
else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
else if (this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
this.controls.statusDisplay.innerText = this.UIString('Loading');
else
this.controls.statusDisplay.innerText = '';
this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error);
},
handleLoadStart: function(event)
{
this.updateStatusDisplay();
this.updateProgress();
},
handleError: function(event)
{
this.updateStatusDisplay();
},
handleAbort: function(event)
{
this.updateStatusDisplay();
},
handleSuspend: function(event)
{
this.updateStatusDisplay();
},
handleStalled: function(event)
{
this.updateStatusDisplay();
this.updateProgress();
},
handleWaiting: function(event)
{
this.updateStatusDisplay();
},
handleReadyStateChange: function(event)
{
this.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0;
this.updateReadyState();
this.updateDuration();
this.updateCaptionButton();
this.updateCaptionContainer();
this.updateFullscreenButton();
this.updateProgress();
},
handleTimeUpdate: function(event)
{
if (!this.scrubbing)
this.updateTime();
},
handleDurationChange: function(event)
{
this.updateDuration();
this.updateTime(true);
this.updateProgress(true);
},
handlePlay: function(event)
{
this.setPlaying(true);
},
handlePause: function(event)
{
this.setPlaying(false);
},
handleProgress: function(event)
{
this.updateProgress();
},
handleVolumeChange: function(event)
{
this.updateVolume();
},
handleTextTrackChange: function(event)
{
this.updateCaptionContainer();
},
handleTextTrackAdd: function(event)
{
var track = event.track;
if (this.trackHasThumbnails(track) && track.mode === 'disabled')
track.mode = 'hidden';
this.updateThumbnail();
this.updateCaptionButton();
this.updateCaptionContainer();
},
handleTextTrackRemove: function(event)
{
this.updateThumbnail();
this.updateCaptionButton();
this.updateCaptionContainer();
},
isFullScreen: function()
{
return this.video.webkitDisplayingFullscreen;
},
handleFullscreenChange: function(event)
{
this.updateBase();
this.updateControls();
if (this.isFullScreen()) {
this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
this.host.enteredFullscreen();
} else {
this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
this.host.exitedFullscreen();
}
},
handleWrapperMouseMove: function(event)
{
this.showControls();
this.resetHideControlsTimer();
if (!this.isDragging)
return;
var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
event.stopPropagation()
},
handleWrapperMouseOut: function(event)
{
this.hideControls();
this.clearHideControlsTimer();
},
handleWrapperMouseUp: function(event)
{
this.isDragging = false;
this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
},
handlePanelMouseDown: function(event)
{
if (event.target != this.controls.panel)
return;
if (!this.isFullScreen())
return;
this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
this.isDragging = true;
this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
this.initialOffset = new WebKitPoint(
parseInt(this.controls.panel.style.left) | 0,
parseInt(this.controls.panel.style.top) | 0
);
},
handlePanelTransitionEnd: function(event)
{
var opacity = window.getComputedStyle(this.controls.panel).opacity;
if (parseInt(opacity) > 0)
this.controls.panel.classList.remove(this.ClassNames.hidden);
else
this.controls.panel.classList.add(this.ClassNames.hidden);
},
handlePanelClick: function(event)
{
// Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
event.preventDefault();
},
handleRewindButtonClicked: function(event)
{
var newTime = Math.max(
this.video.currentTime - this.RewindAmount,
this.video.seekable.start(0));
this.video.currentTime = newTime;
return true;
},
canPlay: function()
{
return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
},
handlePlayButtonClicked: function(event)
{
if (this.canPlay())
this.video.play();
else
this.video.pause();
return true;
},
handleTimelineChange: function(event)
{
this.video.fastSeek(this.controls.timeline.value);
},
handleTimelineDown: function(event)
{
this.controls.thumbnail.classList.add(this.ClassNames.show);
},
handleTimelineUp: function(event)
{
this.controls.thumbnail.classList.remove(this.ClassNames.show);
},
handleTimelineMouseOver: function(event)
{
this.controls.thumbnail.classList.add(this.ClassNames.show);
},
handleTimelineMouseOut: function(event)
{
this.controls.thumbnail.classList.remove(this.ClassNames.show);
},
handleTimelineMouseMove: function(event)
{
if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
return;
this.updateTimelineMetricsIfNeeded();
this.controls.thumbnail.classList.add(this.ClassNames.show);
var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
percent = Math.max(Math.min(1, percent), 0);
this.controls.thumbnail.style.left = percent * 100 + '%';
var thumbnailTime = percent * this.video.duration;
for (var i = 0; i < this.video.textTracks.length; ++i) {
var track = this.video.textTracks[i];
if (!this.trackHasThumbnails(track))
continue;
if (!track.cues)
continue;
for (var j = 0; j < track.cues.length; ++j) {
var cue = track.cues[j];
if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
this.controls.thumbnailImage.src = cue.text;
return;
}
}
}
},
handleTimelineMouseDown: function(event)
{
this.scrubbing = true;
},
handleTimelineMouseUp: function(event)
{
this.scrubbing = false;
// Do a precise seek when we lift the mouse:
this.video.currentTime = this.controls.timeline.value;
},
handleMuteButtonClicked: function(event)
{
this.video.muted = !this.video.muted;
if (this.video.muted)
this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute'));
return true;
},
handleMinButtonClicked: function(event)
{
if (this.video.muted) {
this.video.muted = false;
this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
}
this.video.volume = 0;
return true;
},
handleMaxButtonClicked: function(event)
{
if (this.video.muted) {
this.video.muted = false;
this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
}
this.video.volume = 1;
},
handleVolumeSliderInput: function(event)
{
if (this.video.muted) {
this.video.muted = false;
this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
}
this.video.volume = this.controls.volume.value;
},
handleCaptionButtonClicked: function(event)
{
if (this.captionMenu)
this.destroyCaptionMenu();
else
this.buildCaptionMenu();
return true;
},
updateFullscreenButton: function()
{
this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, (!this.video.webkitSupportsFullscreen || !this.hasVisualMedia));
},
handleFullscreenButtonClicked: function(event)
{
if (this.isFullScreen())
this.video.webkitExitFullscreen();
else
this.video.webkitEnterFullscreen();
return true;
},
handleControlsChange: function()
{
try {
this.updateBase();
if (this.shouldHaveControls())
this.addControls();
else
this.removeControls();
} catch(e) {
if (window.console)
console.error(e);
}
},
nextRate: function()
{
return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
},
handleSeekBackMouseDown: function(event)
{
this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
this.video.play();
this.video.playbackRate = this.nextRate() * -1;
this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
},
seekBackFaster: function()
{
this.video.playbackRate = this.nextRate() * -1;
},
handleSeekBackMouseUp: function(event)
{
this.video.playbackRate = this.video.defaultPlaybackRate;
if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
this.video.pause();
else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
this.video.play();
if (this.seekInterval)
clearInterval(this.seekInterval);
},
handleSeekForwardMouseDown: function(event)
{
this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
this.video.play();
this.video.playbackRate = this.nextRate();
this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
},
seekForwardFaster: function()
{
this.video.playbackRate = this.nextRate();
},
handleSeekForwardMouseUp: function(event)
{
this.video.playbackRate = this.video.defaultPlaybackRate;
if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
this.video.pause();
else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
this.video.play();
if (this.seekInterval)
clearInterval(this.seekInterval);
},
updateDuration: function()
{
var duration = this.video.duration;
this.controls.timeline.min = 0;
this.controls.timeline.max = duration;
this.setIsLive(duration === Number.POSITIVE_INFINITY);
this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
},
progressFillStyle: function(context)
{
var height = this.timelineHeight;
var gradient = context.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, 'rgb(2, 2, 2)');
gradient.addColorStop(1, 'rgb(23, 23, 23)');
return gradient;
},
updateProgress: function(forceUpdate)
{
if (!forceUpdate && this.controlsAreHidden())
return;
this.updateTimelineMetricsIfNeeded();
var width = this.timelineWidth;
var height = this.timelineHeight;
var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height);
context.clearRect(0, 0, width, height);
context.fillStyle = this.progressFillStyle(context);
var duration = this.video.duration;
var buffered = this.video.buffered;
for (var i = 0, end = buffered.length; i < end; ++i) {
var startTime = buffered.start(i);
var endTime = buffered.end(i);
var startX = width * startTime / duration;
var endX = width * endTime / duration;
context.fillRect(startX, 0, endX - startX, height);
}
},
formatTime: function(time)
{
if (isNaN(time))
time = 0;
var absTime = Math.abs(time);
var intSeconds = Math.floor(absTime % 60).toFixed(0);
var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
var sign = time < 0 ? '-' : String();
if (intHours > 0)
return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
},
updatePlaying: function()
{
this.setPlaying(!this.canPlay());
},
setPlaying: function(isPlaying)
{
if (this.isPlaying === isPlaying)
return;
this.isPlaying = isPlaying;
if (!isPlaying) {
this.controls.panel.classList.add(this.ClassNames.paused);
this.controls.playButton.classList.add(this.ClassNames.paused);
this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
this.showControls();
} else {
this.controls.panel.classList.remove(this.ClassNames.paused);
this.controls.playButton.classList.remove(this.ClassNames.paused);
this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
this.hideControls();
this.resetHideControlsTimer();
}
},
showControls: function()
{
this.controls.panel.classList.add(this.ClassNames.show);
this.controls.panel.classList.remove(this.ClassNames.hidden);
this.updateTime();
this.setNeedsTimelineMetricsUpdate();
},
hideControls: function()
{
this.controls.panel.classList.remove(this.ClassNames.show);
},
controlsAreAlwaysVisible: function()
{
return this.controls.panel.classList.contains(this.ClassNames.noVideo);
},
controlsAreHidden: function()
{
if (this.controlsAreAlwaysVisible())
return false;
var panel = this.controls.panel;
return (!panel.classList.contains(this.ClassNames.show) || panel.classList.contains(this.ClassNames.hidden))
&& (panel.parentElement.querySelector(':hover') !== panel);
},
removeControls: function()
{
if (this.controls.panel.parentNode)
this.controls.panel.parentNode.removeChild(this.controls.panel);
this.destroyCaptionMenu();
},
addControls: function()
{
this.base.appendChild(this.controls.panelCompositedParent);
this.controls.panelCompositedParent.appendChild(this.controls.panel);
this.setNeedsTimelineMetricsUpdate();
},
updateTime: function(forceUpdate)
{
if (!forceUpdate && this.controlsAreHidden())
return;
var currentTime = this.video.currentTime;
var timeRemaining = currentTime - this.video.duration;
this.controls.currentTime.innerText = this.formatTime(currentTime);
this.controls.timeline.value = this.video.currentTime;
this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
},
updateReadyState: function()
{
this.updateStatusDisplay();
},
setStatusHidden: function(hidden)
{
if (this.statusHidden === hidden)
return;
this.statusHidden = hidden;
if (hidden) {
this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
this.controls.currentTime.classList.remove(this.ClassNames.hidden);
this.controls.timeline.classList.remove(this.ClassNames.hidden);
this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
this.setNeedsTimelineMetricsUpdate();
} else {
this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
this.controls.currentTime.classList.add(this.ClassNames.hidden);
this.controls.timeline.classList.add(this.ClassNames.hidden);
this.controls.remainingTime.classList.add(this.ClassNames.hidden);
}
},
trackHasThumbnails: function(track)
{
return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
},
updateThumbnail: function()
{
for (var i = 0; i < this.video.textTracks.length; ++i) {
var track = this.video.textTracks[i];
if (this.trackHasThumbnails(track)) {
this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
return;
}
}
this.controls.thumbnail.classList.add(this.ClassNames.hidden);
},
updateCaptionButton: function()
{
if (this.video.webkitHasClosedCaptions)
this.controls.captionButton.classList.remove(this.ClassNames.hidden);
else
this.controls.captionButton.classList.add(this.ClassNames.hidden);
},
updateCaptionContainer: function()
{
if (!this.host.textTrackContainer)
return;
var hasClosedCaptions = this.video.webkitHasClosedCaptions;
var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
if (hasClosedCaptions && hasHiddenClass)
this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
else if (!hasClosedCaptions && !hasHiddenClass)
this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
this.updateBase();
this.host.updateTextTrackContainer();
},
buildCaptionMenu: function()
{
var tracks = this.host.sortedTrackListForMenu(this.video.textTracks);
if (!tracks || !tracks.length)
return;
this.captionMenu = document.createElement('div');
this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
this.captionMenu.setAttribute('id', 'audioTrackMenu');
this.base.appendChild(this.captionMenu);
this.captionMenuItems = [];
var offItem = this.host.captionMenuOffItem;
var automaticItem = this.host.captionMenuAutomaticItem;
var displayMode = this.host.captionDisplayMode;
var list = document.createElement('div');
this.captionMenu.appendChild(list);
list.classList.add(this.ClassNames.list);
var heading = document.createElement('h3');
heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
list.appendChild(heading);
heading.innerText = this.UIString('Subtitles');
var ul = document.createElement('ul');
ul.setAttribute('role', 'menu');
ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
list.appendChild(ul);
for (var i = 0; i < tracks.length; ++i) {
var menuItem = document.createElement('li');
menuItem.setAttribute('role', 'menuitemradio');
menuItem.setAttribute('tabindex', '-1');
this.captionMenuItems.push(menuItem);
this.listenFor(menuItem, 'click', this.captionItemSelected);
this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
ul.appendChild(menuItem);
var track = tracks[i];
menuItem.innerText = this.host.displayNameForTrack(track);
menuItem.track = track;
if (track === offItem) {
var offMenu = menuItem;
continue;
}
if (track === automaticItem) {
if (displayMode === 'automatic') {
menuItem.classList.add(this.ClassNames.selected);
menuItem.setAttribute('tabindex', '0');
menuItem.setAttribute('aria-checked', 'true');
}
continue;
}
if (displayMode != 'automatic' && track.mode === 'showing') {
var trackMenuItemSelected = true;
menuItem.classList.add(this.ClassNames.selected);
menuItem.setAttribute('tabindex', '0');
menuItem.setAttribute('aria-checked', 'true');
}
}
if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
offMenu.classList.add(this.ClassNames.selected);
menuItem.setAttribute('tabindex', '0');
menuItem.setAttribute('aria-checked', 'true');
}
// focus first selected menuitem
for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
var item = this.captionMenuItems[i];
if (item.classList.contains(this.ClassNames.selected)) {
item.focus();
break;
}
}
},
captionItemSelected: function(event)
{
this.host.setSelectedTextTrack(event.target.track);
this.destroyCaptionMenu();
},
focusSiblingCaptionItem: function(event)
{
var currentItem = event.target;
var pendingItem = false;
switch(event.keyCode) {
case this.KeyCodes.left:
case this.KeyCodes.up:
pendingItem = currentItem.previousSibling;
break;
case this.KeyCodes.right:
case this.KeyCodes.down:
pendingItem = currentItem.nextSibling;
break;
}
if (pendingItem) {
currentItem.setAttribute('tabindex', '-1');
pendingItem.setAttribute('tabindex', '0');
pendingItem.focus();
}
},
handleCaptionItemKeyUp: function(event)
{
switch (event.keyCode) {
case this.KeyCodes.enter:
case this.KeyCodes.space:
this.captionItemSelected(event);
break;
case this.KeyCodes.escape:
this.destroyCaptionMenu();
break;
case this.KeyCodes.left:
case this.KeyCodes.up:
case this.KeyCodes.right:
case this.KeyCodes.down:
this.focusSiblingCaptionItem(event);
break;
default:
return;
}
// handled
event.stopPropagation();
event.preventDefault();
},
destroyCaptionMenu: function()
{
if (!this.captionMenu)
return;
this.captionMenuItems.forEach(function(item){
this.stopListeningFor(item, 'click', this.captionItemSelected);
this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
}, this);
// FKA and AX: focus the trigger before destroying the element with focus
if (this.controls.captionButton)
this.controls.captionButton.focus();
if (this.captionMenu.parentNode)
this.captionMenu.parentNode.removeChild(this.captionMenu);
delete this.captionMenu;
delete this.captionMenuItems;
},
updateHasAudio: function()
{
if (this.video.audioTracks.length)
this.controls.muteBox.classList.remove(this.ClassNames.hidden);
else
this.controls.muteBox.classList.add(this.ClassNames.hidden);
},
updateHasVideo: function()
{
if (this.video.videoTracks.length)
this.controls.panel.classList.remove(this.ClassNames.noVideo);
else
this.controls.panel.classList.add(this.ClassNames.noVideo);
},
updateVolume: function()
{
if (this.video.muted || !this.video.volume) {
this.controls.muteButton.classList.add(this.ClassNames.muted);
this.controls.volume.value = 0;
} else {
this.controls.muteButton.classList.remove(this.ClassNames.muted);
this.controls.volume.value = this.video.volume;
}
},
isAudio: function()
{
return this.video instanceof HTMLAudioElement;
},
clearHideControlsTimer: function()
{
if (this.hideTimer)
clearTimeout(this.hideTimer);
this.hideTimer = null;
},
resetHideControlsTimer: function()
{
if (this.hideTimer)
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
},
};