978 lines
33 KiB
JavaScript
978 lines
33 KiB
JavaScript
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.hasVisualMedia = false;
|
|
|
|
this.addVideoListeners();
|
|
this.createBase();
|
|
this.createControls();
|
|
this.updateBase();
|
|
this.updateControls();
|
|
this.updateDuration();
|
|
this.updateTime();
|
|
this.updatePlaying();
|
|
this.updateCaptionButton();
|
|
this.updateCaptionContainer();
|
|
this.updateFullscreenButton();
|
|
this.updateVolume();
|
|
this.updateHasAudio();
|
|
this.updateHasVideo();
|
|
};
|
|
|
|
Controller.prototype = {
|
|
|
|
/* Constants */
|
|
HandledVideoEvents: {
|
|
emptied: 'handleReadyStateChange',
|
|
loadedmetadata: 'handleReadyStateChange',
|
|
loadeddata: 'handleReadyStateChange',
|
|
canplay: 'handleReadyStateChange',
|
|
canplaythrough: 'handleReadyStateChange',
|
|
timeupdate: 'handleTimeUpdate',
|
|
durationchange: 'handleDurationChange',
|
|
playing: 'handlePlay',
|
|
pause: 'handlePause',
|
|
volumechange: 'handleVolumeChange',
|
|
webkitfullscreenchange: 'handleFullscreenChange',
|
|
webkitbeginfullscreen: 'handleFullscreenChange',
|
|
webkitendfullscreen: 'handleFullscreenChange',
|
|
},
|
|
HideControlsDelay: 4 * 1000,
|
|
ClassNames: {
|
|
exit: 'exit',
|
|
hidden: 'hidden',
|
|
hiding: 'hiding',
|
|
muteBox: 'mute-box',
|
|
muted: 'muted',
|
|
paused: 'paused',
|
|
selected: 'selected',
|
|
show: 'show',
|
|
noVideo: 'no-video',
|
|
noDuration: 'no-duration',
|
|
down: 'down',
|
|
out: 'out',
|
|
},
|
|
KeyCodes: {
|
|
enter: 13,
|
|
escape: 27,
|
|
space: 32,
|
|
left: 37,
|
|
up: 38,
|
|
right: 39,
|
|
down: 40
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
isAudio: function()
|
|
{
|
|
return this.video instanceof HTMLAudioElement;
|
|
},
|
|
|
|
isFullScreen: function()
|
|
{
|
|
return this.video.webkitDisplayingFullscreen;
|
|
},
|
|
|
|
shouldHaveControls: function()
|
|
{
|
|
if (!this.isAudio() && !this.host.allowsInlineMediaPlayback)
|
|
return true;
|
|
|
|
return this.video.controls || this.isFullScreen();
|
|
},
|
|
|
|
updateBase: function()
|
|
{
|
|
if (this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length)) {
|
|
if (!this.base.parentNode) {
|
|
this.root.appendChild(this.base);
|
|
}
|
|
} else {
|
|
if (this.base.parentNode) {
|
|
this.base.parentNode.removeChild(this.base);
|
|
}
|
|
}
|
|
},
|
|
|
|
createControls: function()
|
|
{
|
|
var enclosure = this.controls.enclosure = document.createElement('div');
|
|
enclosure.setAttribute('pseudo', '-webkit-media-controls-enclosure');
|
|
|
|
var panel = this.controls.panel = document.createElement('div');
|
|
panel.setAttribute('pseudo', '-webkit-media-controls-panel');
|
|
panel.setAttribute('aria-label', (this.isAudio() ? 'Audio Playback' : '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 playButton = this.controls.playButton = document.createElement('button');
|
|
playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
|
|
playButton.setAttribute('aria-label', 'Play');
|
|
this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
|
|
|
|
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', 'Elapsed');
|
|
currentTime.setAttribute('role', 'timer');
|
|
|
|
var timeline = this.controls.timeline = document.createElement('input');
|
|
timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
|
|
timeline.setAttribute('aria-label', 'Duration');
|
|
timeline.type = 'range';
|
|
timeline.value = 0;
|
|
timeline.step = .01;
|
|
this.listenFor(timeline, 'input', this.handleTimelineChange);
|
|
this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
|
|
|
|
var muteBox = this.controls.muteBox = document.createElement('div');
|
|
muteBox.classList.add(this.ClassNames.muteBox);
|
|
this.listenFor(muteBox, 'mouseout', this.handleVolumeBoxMouseOut);
|
|
|
|
var muteButton = this.controls.muteButton = document.createElement('button');
|
|
muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
|
|
muteButton.setAttribute('aria-label', 'Mute');
|
|
this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
|
|
this.listenFor(muteButton, 'mouseover', this.handleMuteButtonMouseOver);
|
|
|
|
var volumeBox = this.controls.volumeBox = document.createElement('div');
|
|
volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
|
|
volumeBox.classList.add(this.ClassNames.hiding);
|
|
this.listenFor(volumeBox, 'mouseover', this.handleMuteButtonMouseOver);
|
|
|
|
var volume = this.controls.volume = document.createElement('input');
|
|
volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
|
|
volume.setAttribute('aria-label', 'Volume');
|
|
volume.type = 'range';
|
|
volume.min = 0;
|
|
volume.max = 1;
|
|
volume.step = .01;
|
|
this.listenFor(volume, 'input', this.handleVolumeSliderInput);
|
|
this.listenFor(volume, 'mouseover', this.handleMuteButtonMouseOver);
|
|
|
|
var captionButton = this.controls.captionButton = document.createElement('button');
|
|
captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
|
|
captionButton.setAttribute('aria-label', 'Captions');
|
|
captionButton.setAttribute('aria-haspopup', 'true');
|
|
captionButton.setAttribute('aria-owns', 'audioTrackMenu');
|
|
this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
|
|
this.listenFor(captionButton, 'mouseover', this.handleCaptionButtonMouseOver);
|
|
this.listenFor(captionButton, 'mouseout', this.handleCaptionButtonMouseOut);
|
|
|
|
var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
|
|
fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
|
|
fullscreenButton.setAttribute('aria-label', 'Display Full Screen');
|
|
fullscreenButton.disabled = true;
|
|
this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
|
|
},
|
|
|
|
configureControls: function()
|
|
{
|
|
this.controls.panel.appendChild(this.controls.playButton);
|
|
this.controls.panel.appendChild(this.controls.timeline);
|
|
this.controls.panel.appendChild(this.controls.currentTime);
|
|
this.controls.panel.appendChild(this.controls.muteBox);
|
|
this.controls.muteBox.appendChild(this.controls.muteButton);
|
|
this.controls.muteBox.appendChild(this.controls.volumeBox);
|
|
this.controls.volumeBox.appendChild(this.controls.volume);
|
|
this.controls.panel.appendChild(this.controls.captionButton);
|
|
if (!this.isAudio())
|
|
this.controls.panel.appendChild(this.controls.fullscreenButton);
|
|
this.controls.enclosure.appendChild(this.controls.panel);
|
|
},
|
|
|
|
disconnectControls: function(event)
|
|
{
|
|
for (var item in this.controls) {
|
|
var control = this.controls[item];
|
|
if (control && control.parentNode)
|
|
control.parentNode.removeChild(control);
|
|
}
|
|
},
|
|
|
|
reconnectControls: function()
|
|
{
|
|
this.disconnectControls();
|
|
this.configureControls();
|
|
|
|
if (this.shouldHaveControls())
|
|
this.addControls();
|
|
},
|
|
|
|
showControls: function()
|
|
{
|
|
this.controls.panel.classList.remove(this.ClassNames.hidden);
|
|
this.controls.panel.classList.add(this.ClassNames.show);
|
|
|
|
this.updateTime();
|
|
},
|
|
|
|
hideControls: function()
|
|
{
|
|
this.controls.panel.classList.remove(this.ClassNames.show);
|
|
},
|
|
|
|
resetHideControlsTimer: function()
|
|
{
|
|
if (this.hideTimer)
|
|
clearTimeout(this.hideTimer);
|
|
this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
|
|
},
|
|
|
|
clearHideControlsTimer: function()
|
|
{
|
|
if (this.hideTimer)
|
|
clearTimeout(this.hideTimer);
|
|
this.hideTimer = null;
|
|
},
|
|
|
|
controlsAreAlwaysVisible: function()
|
|
{
|
|
return this.isAudio() || 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);
|
|
},
|
|
|
|
addControls: function()
|
|
{
|
|
this.base.appendChild(this.controls.enclosure);
|
|
},
|
|
|
|
removeControls: function()
|
|
{
|
|
if (this.controls.enclosure.parentNode)
|
|
this.controls.enclosure.parentNode.removeChild(this.controls.enclosure);
|
|
this.hideCaptionMenu();
|
|
},
|
|
|
|
updateControls: function()
|
|
{
|
|
this.reconnectControls();
|
|
},
|
|
|
|
setIsLive: function(live)
|
|
{
|
|
if (live === this.isLive)
|
|
return;
|
|
this.isLive = live;
|
|
|
|
this.reconnectControls();
|
|
this.controls.timeline.disabled = this.isLive;
|
|
},
|
|
|
|
updateDuration: function()
|
|
{
|
|
var duration = this.video.duration;
|
|
this.setIsLive(duration === Number.POSITIVE_INFINITY);
|
|
this.controls.timeline.min = 0;
|
|
this.controls.timeline.max = this.isLive ? 0 : duration;
|
|
if (this.isLive || isNaN(duration))
|
|
this.timeDigitsCount = 4;
|
|
else if (duration < 10 * 60) /* Ten minutes */
|
|
this.timeDigitsCount = 3;
|
|
else if (duration < 60 * 60) /* One hour */
|
|
this.timeDigitsCount = 4;
|
|
else if (duration < 10 * 60 * 60) /* Ten hours */
|
|
this.timeDigitsCount = 5;
|
|
else
|
|
this.timeDigitsCount = 6;
|
|
},
|
|
|
|
formatTime: function(time)
|
|
{
|
|
if (isNaN(time))
|
|
return '00:00';
|
|
|
|
const absTime = Math.abs(time);
|
|
const seconds = Math.floor(absTime % 60).toFixed(0);
|
|
const minutes = Math.floor((absTime / 60) % 60).toFixed(0);
|
|
const hours = Math.floor(absTime / (60 * 60)).toFixed(0);
|
|
|
|
function prependZeroIfNeeded(value) {
|
|
if (value < 10)
|
|
return `0${value}`;
|
|
return `${value}`;
|
|
}
|
|
|
|
switch (this.timeDigitsCount) {
|
|
case 3:
|
|
return minutes + ':' + prependZeroIfNeeded(seconds);
|
|
case 4:
|
|
return prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds);
|
|
case 5:
|
|
return hours + ':' + prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds);
|
|
case 6:
|
|
return prependZeroIfNeeded(hours) + ':' + prependZeroIfNeeded(minutes) + ':' + prependZeroIfNeeded(seconds);
|
|
}
|
|
},
|
|
|
|
updateTime: function(forceUpdate)
|
|
{
|
|
if (!forceUpdate && this.controlsAreHidden())
|
|
return;
|
|
|
|
var currentTime = this.video.currentTime;
|
|
this.controls.timeline.value = currentTime;
|
|
this.controls.currentTime.innerText = this.formatTime(currentTime);
|
|
if (!this.isLive) {
|
|
var duration = this.video.duration;
|
|
this.controls.currentTime.innerText += " / " + this.formatTime(duration);
|
|
this.controls.currentTime.classList.toggle(this.ClassNames.noDuration, !duration);
|
|
this.controls.timeline.disabled = !duration;
|
|
} else
|
|
this.controls.currentTime.classList.remove(this.ClassNames.noDuration);
|
|
},
|
|
|
|
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', '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', 'Pause');
|
|
|
|
this.hideControls();
|
|
this.resetHideControlsTimer();
|
|
}
|
|
},
|
|
|
|
updatePlaying: function()
|
|
{
|
|
this.setPlaying(!this.canPlay());
|
|
if (!this.canPlay())
|
|
this.showControls();
|
|
},
|
|
|
|
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();
|
|
},
|
|
|
|
updateFullscreenButton: function()
|
|
{
|
|
if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.hasVisualMedia) {
|
|
this.controls.fullscreenButton.classList.add(this.ClassNames.hidden);
|
|
return;
|
|
}
|
|
|
|
this.controls.fullscreenButton.disabled = !this.host.supportsFullscreen;
|
|
},
|
|
|
|
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;
|
|
}
|
|
},
|
|
|
|
updateHasAudio: function()
|
|
{
|
|
this.controls.muteButton.disabled = this.video.audioTracks.length == 0;
|
|
},
|
|
|
|
updateHasVideo: function()
|
|
{
|
|
if (this.video.videoTracks.length)
|
|
this.controls.panel.classList.remove(this.ClassNames.noVideo);
|
|
else
|
|
this.controls.panel.classList.add(this.ClassNames.noVideo);
|
|
},
|
|
|
|
handleReadyStateChange: function(event)
|
|
{
|
|
this.hasVisualMedia = this.video.videoTracks && this.video.videoTracks.length > 0;
|
|
this.updateVolume();
|
|
this.updateDuration();
|
|
this.updateCaptionButton();
|
|
this.updateCaptionContainer();
|
|
this.updateFullscreenButton();
|
|
},
|
|
|
|
handleTimeUpdate: function(event)
|
|
{
|
|
this.updateTime();
|
|
},
|
|
|
|
handleDurationChange: function(event)
|
|
{
|
|
this.updateDuration();
|
|
this.updateTime(true);
|
|
},
|
|
|
|
handlePlay: function(event)
|
|
{
|
|
this.setPlaying(true);
|
|
},
|
|
|
|
handlePause: function(event)
|
|
{
|
|
this.setPlaying(false);
|
|
},
|
|
|
|
handleVolumeChange: function(event)
|
|
{
|
|
this.updateVolume();
|
|
},
|
|
|
|
handleFullscreenChange: function(event)
|
|
{
|
|
this.updateBase();
|
|
this.updateControls();
|
|
|
|
if (this.isFullScreen()) {
|
|
this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
|
|
this.controls.fullscreenButton.setAttribute('aria-label', 'Exit Full Screen');
|
|
this.host.enteredFullscreen();
|
|
} else {
|
|
this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
|
|
this.controls.fullscreenButton.setAttribute('aria-label', 'Display Full Screen');
|
|
this.host.exitedFullscreen();
|
|
}
|
|
},
|
|
|
|
handleTextTrackChange: function(event)
|
|
{
|
|
this.updateCaptionContainer();
|
|
},
|
|
|
|
handleTextTrackAdd: function(event)
|
|
{
|
|
this.updateCaptionButton();
|
|
this.updateCaptionContainer();
|
|
},
|
|
|
|
handleTextTrackRemove: function(event)
|
|
{
|
|
this.updateCaptionButton();
|
|
this.updateCaptionContainer();
|
|
},
|
|
|
|
handleControlsChange: function()
|
|
{
|
|
try {
|
|
this.updateBase();
|
|
|
|
if (this.shouldHaveControls())
|
|
this.addControls();
|
|
else
|
|
this.removeControls();
|
|
} catch(e) {
|
|
if (window.console)
|
|
console.error(e);
|
|
}
|
|
},
|
|
|
|
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();
|
|
},
|
|
|
|
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);
|
|
},
|
|
|
|
handleTimelineMouseUp: function(event)
|
|
{
|
|
/* 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', 'Unmute');
|
|
return true;
|
|
},
|
|
|
|
handleMuteButtonMouseOver: function(event)
|
|
{
|
|
if (this.video.offsetTop + this.controls.enclosure.offsetTop < 105) {
|
|
this.controls.volumeBox.classList.add(this.ClassNames.down);
|
|
this.controls.panel.classList.add(this.ClassNames.down);
|
|
} else {
|
|
this.controls.volumeBox.classList.remove(this.ClassNames.down);
|
|
this.controls.panel.classList.remove(this.ClassNames.down);
|
|
}
|
|
this.controls.volumeBox.classList.remove(this.ClassNames.hiding);
|
|
|
|
return true;
|
|
},
|
|
|
|
handleVolumeBoxMouseOut: function(event)
|
|
{
|
|
this.controls.volumeBox.classList.add(this.ClassNames.hiding);
|
|
return true;
|
|
},
|
|
|
|
handleVolumeSliderInput: function(event)
|
|
{
|
|
if (this.video.muted) {
|
|
this.video.muted = false;
|
|
this.controls.muteButton.setAttribute('aria-label', 'Mute');
|
|
}
|
|
this.video.volume = this.controls.volume.value;
|
|
},
|
|
|
|
handleFullscreenButtonClicked: function(event)
|
|
{
|
|
if (this.isFullScreen())
|
|
this.video.webkitExitFullscreen();
|
|
else
|
|
this.video.webkitEnterFullscreen();
|
|
return true;
|
|
},
|
|
|
|
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.listenFor(this.captionMenu, 'mouseout', this.handleCaptionMenuMouseOut);
|
|
this.listenFor(this.captionMenu, 'transitionend', this.captionMenuTransitionEnd);
|
|
this.base.appendChild(this.captionMenu);
|
|
this.captionMenu.captionMenuTreeElements = [];
|
|
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);
|
|
this.captionMenu.captionMenuTreeElements.push(list);
|
|
|
|
var heading = document.createElement('h3');
|
|
heading.id = 'webkitMediaControlsClosedCaptionsHeading';
|
|
list.appendChild(heading);
|
|
heading.innerText = 'Subtitles';
|
|
this.captionMenu.captionMenuTreeElements.push(heading);
|
|
|
|
var ul = document.createElement('ul');
|
|
ul.setAttribute('role', 'menu');
|
|
ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
|
|
list.appendChild(ul);
|
|
this.captionMenu.captionMenuTreeElements.push(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;
|
|
}
|
|
}
|
|
|
|
/* Caption menu has to be centered to the caption button. */
|
|
var captionButtonCenter = this.controls.panel.offsetLeft + this.controls.captionButton.offsetLeft +
|
|
this.controls.captionButton.offsetWidth / 2;
|
|
var captionMenuLeft = (captionButtonCenter - this.captionMenu.offsetWidth / 2);
|
|
if (captionMenuLeft + this.captionMenu.offsetWidth > this.controls.panel.offsetLeft + this.controls.panel.offsetWidth)
|
|
this.captionMenu.classList.add(this.ClassNames.out);
|
|
this.captionMenu.style.left = captionMenuLeft + 'px';
|
|
/* As height is not in the css, it needs to be specified to animate it. */
|
|
this.captionMenu.height = this.captionMenu.offsetHeight;
|
|
this.captionMenu.style.height = 0;
|
|
},
|
|
|
|
captionItemSelected: function(event)
|
|
{
|
|
this.host.setSelectedTextTrack(event.target.track);
|
|
this.hideCaptionMenu();
|
|
},
|
|
|
|
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.hideCaptionMenu();
|
|
break;
|
|
case this.KeyCodes.left:
|
|
case this.KeyCodes.up:
|
|
case this.KeyCodes.right:
|
|
case this.KeyCodes.down:
|
|
this.focusSiblingCaptionItem(event);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
},
|
|
|
|
showCaptionMenu: function()
|
|
{
|
|
if (!this.captionMenu)
|
|
this.buildCaptionMenu();
|
|
this.captionMenu.style.height = this.captionMenu.height + 'px';
|
|
},
|
|
|
|
hideCaptionMenu: function()
|
|
{
|
|
if (!this.captionMenu)
|
|
return;
|
|
this.captionMenu.style.height = 0;
|
|
},
|
|
|
|
captionMenuTransitionEnd: function(event)
|
|
{
|
|
if (this.captionMenu.offsetHeight !== 0)
|
|
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;
|
|
},
|
|
|
|
captionMenuContainsNode: function(node)
|
|
{
|
|
return this.captionMenu.captionMenuTreeElements.find((item) => item == node)
|
|
|| this.captionMenuItems.find((item) => item == node);
|
|
},
|
|
|
|
handleCaptionButtonClicked: function(event)
|
|
{
|
|
this.showCaptionMenu();
|
|
return true;
|
|
},
|
|
|
|
handleCaptionButtonMouseOver: function(event)
|
|
{
|
|
this.showCaptionMenu();
|
|
return true;
|
|
},
|
|
|
|
handleCaptionButtonMouseOut: function(event)
|
|
{
|
|
if (this.captionMenu && !this.captionMenuContainsNode(event.relatedTarget))
|
|
this.hideCaptionMenu();
|
|
return true;
|
|
},
|
|
|
|
handleCaptionMenuMouseOut: function(event)
|
|
{
|
|
if (event.relatedTarget != this.controls.captionButton && !this.captionMenuContainsNode(event.relatedTarget))
|
|
this.hideCaptionMenu();
|
|
return true;
|
|
},
|
|
};
|