2509 lines
95 KiB
JavaScript
2509 lines
95 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.statusHidden = true;
|
|
this.hasWirelessPlaybackTargets = false;
|
|
this.canToggleShowControlsButton = false;
|
|
this.isListeningForPlaybackTargetAvailabilityEvent = false;
|
|
this.currentTargetIsWireless = false;
|
|
this.wirelessPlaybackDisabled = false;
|
|
this.isVolumeSliderActive = false;
|
|
this.currentDisplayWidth = 0;
|
|
this._scrubbing = false;
|
|
this._pageScaleFactor = 1;
|
|
|
|
this.addVideoListeners();
|
|
this.createBase();
|
|
this.createControls();
|
|
this.createTimeClones();
|
|
this.updateBase();
|
|
this.updateControls();
|
|
this.updateDuration();
|
|
this.updateProgress();
|
|
this.updateTime();
|
|
this.updateReadyState();
|
|
this.updatePlaying();
|
|
this.updateThumbnail();
|
|
this.updateCaptionButton();
|
|
this.updateCaptionContainer();
|
|
this.updateFullscreenButtons();
|
|
this.updateVolume();
|
|
this.updateHasAudio();
|
|
this.updateHasVideo();
|
|
this.updateWirelessTargetAvailable();
|
|
this.updateWirelessPlaybackStatus();
|
|
this.updatePictureInPicturePlaceholder();
|
|
this.scheduleUpdateLayoutForDisplayedWidth();
|
|
|
|
this.listenFor(this.root, 'resize', this.handleRootResize);
|
|
};
|
|
|
|
/* Enums */
|
|
Controller.InlineControls = 0;
|
|
Controller.FullScreenControls = 1;
|
|
|
|
Controller.PlayAfterSeeking = 0;
|
|
Controller.PauseAfterSeeking = 1;
|
|
|
|
/* Globals */
|
|
Controller.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets.
|
|
Controller.gSimulatePictureInPictureAvailable = false; // Used for testing when picture-in-picture is not available.
|
|
|
|
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',
|
|
},
|
|
PlaceholderPollingDelay: 33,
|
|
HideControlsDelay: 4 * 1000,
|
|
RewindAmount: 30,
|
|
MaximumSeekRate: 8,
|
|
SeekDelay: 1500,
|
|
ClassNames: {
|
|
active: 'active',
|
|
dropped: 'dropped',
|
|
exit: 'exit',
|
|
failed: 'failed',
|
|
hidden: 'hidden',
|
|
hiding: 'hiding',
|
|
threeDigitTime: 'three-digit-time',
|
|
fourDigitTime: 'four-digit-time',
|
|
fiveDigitTime: 'five-digit-time',
|
|
sixDigitTime: 'six-digit-time',
|
|
list: 'list',
|
|
muteBox: 'mute-box',
|
|
muted: 'muted',
|
|
paused: 'paused',
|
|
pictureInPicture: 'picture-in-picture',
|
|
playing: 'playing',
|
|
returnFromPictureInPicture: 'return-from-picture-in-picture',
|
|
selected: 'selected',
|
|
show: 'show',
|
|
small: 'small',
|
|
thumbnail: 'thumbnail',
|
|
thumbnailImage: 'thumbnail-image',
|
|
thumbnailTrack: 'thumbnail-track',
|
|
volumeBox: 'volume-box',
|
|
noVideo: 'no-video',
|
|
down: 'down',
|
|
out: 'out',
|
|
pictureInPictureButton: 'picture-in-picture-button',
|
|
placeholderShowing: 'placeholder-showing',
|
|
usesLTRUserInterfaceLayoutDirection: 'uses-ltr-user-interface-layout-direction',
|
|
appleTV: 'appletv',
|
|
},
|
|
KeyCodes: {
|
|
enter: 13,
|
|
escape: 27,
|
|
space: 32,
|
|
pageUp: 33,
|
|
pageDown: 34,
|
|
end: 35,
|
|
home: 36,
|
|
left: 37,
|
|
up: 38,
|
|
right: 39,
|
|
down: 40
|
|
},
|
|
MinimumTimelineWidth: 80,
|
|
ButtonWidth: 32,
|
|
|
|
extend: function(child)
|
|
{
|
|
// This function doesn't actually do what we want it to. In particular it
|
|
// is not copying the getters and setters to the child class, since they are
|
|
// not enumerable. What we should do is use ES6 classes, or assign the __proto__
|
|
// directly.
|
|
// FIXME: Use ES6 classes.
|
|
|
|
for (var property in this) {
|
|
if (!child.hasOwnProperty(property))
|
|
child[property] = this[property];
|
|
}
|
|
},
|
|
|
|
get idiom()
|
|
{
|
|
return "apple";
|
|
},
|
|
|
|
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.handleAudioTrackChange);
|
|
this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
|
|
this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
|
|
|
|
/* 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'] });
|
|
|
|
this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
|
|
|
|
if ('webkitPresentationMode' in this.video)
|
|
this.listenFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
|
|
},
|
|
|
|
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.handleAudioTrackChange);
|
|
this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
|
|
this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
|
|
|
|
/* 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);
|
|
|
|
this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange);
|
|
this.setShouldListenForPlaybackTargetAvailabilityEvent(false);
|
|
|
|
if ('webkitPresentationMode' in this.video)
|
|
this.stopListeningFor(this.video, 'webkitpresentationmodechanged', this.handlePresentationModeChange);
|
|
},
|
|
|
|
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(this.video, 'mouseout', this.handleWrapperMouseOut);
|
|
if (this.host.textTrackContainer)
|
|
base.appendChild(this.host.textTrackContainer);
|
|
},
|
|
|
|
shouldHaveAnyUI: function()
|
|
{
|
|
return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length) || this.currentPlaybackTargetIsWireless();
|
|
},
|
|
|
|
shouldShowControls: function()
|
|
{
|
|
if (!this.isAudio() && !this.host.allowsInlineMediaPlayback)
|
|
return true;
|
|
|
|
return this.video.controls || this.isFullScreen();
|
|
},
|
|
|
|
shouldHaveControls: function()
|
|
{
|
|
return this.shouldShowControls() || this.isFullScreen() || this.presentationMode() === 'picture-in-picture' || this.currentPlaybackTargetIsWireless();
|
|
},
|
|
|
|
|
|
setNeedsTimelineMetricsUpdate: function()
|
|
{
|
|
this.timelineMetricsNeedsUpdate = true;
|
|
},
|
|
|
|
scheduleUpdateLayoutForDisplayedWidth: function()
|
|
{
|
|
setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
|
|
},
|
|
|
|
updateTimelineMetricsIfNeeded: function()
|
|
{
|
|
if (this.timelineMetricsNeedsUpdate && !this.controlsAreHidden()) {
|
|
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 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);
|
|
this.listenFor(panel, 'dragstart', this.handlePanelDragStart);
|
|
|
|
var panelBackgroundContainer = this.controls.panelBackgroundContainer = document.createElement('div');
|
|
panelBackgroundContainer.setAttribute('pseudo', '-webkit-media-controls-panel-background-container');
|
|
|
|
var panelTint = this.controls.panelTint = document.createElement('div');
|
|
panelTint.setAttribute('pseudo', '-webkit-media-controls-panel-tint');
|
|
this.listenFor(panelTint, 'mousedown', this.handlePanelMouseDown);
|
|
this.listenFor(panelTint, 'transitionend', this.handlePanelTransitionEnd);
|
|
this.listenFor(panelTint, 'click', this.handlePanelClick);
|
|
this.listenFor(panelTint, 'dblclick', this.handlePanelClick);
|
|
this.listenFor(panelTint, 'dragstart', this.handlePanelDragStart);
|
|
|
|
var panelBackground = this.controls.panelBackground = document.createElement('div');
|
|
panelBackground.setAttribute('pseudo', '-webkit-media-controls-panel-background');
|
|
|
|
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');
|
|
timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
|
|
timeline.setAttribute('aria-label', this.UIString('Duration'));
|
|
timeline.type = 'range';
|
|
timeline.value = 0;
|
|
this.listenFor(timeline, 'input', this.handleTimelineInput);
|
|
this.listenFor(timeline, 'change', 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);
|
|
this.listenFor(timeline, 'keydown', this.handleTimelineKeyDown);
|
|
timeline.step = .01;
|
|
|
|
this.timelineContextName = "_webkit-media-controls-timeline-" + this.host.generateUUID();
|
|
timeline.style.backgroundImage = '-webkit-canvas(' + this.timelineContextName + ')';
|
|
|
|
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);
|
|
this.listenFor(muteBox, 'mouseover', this.handleMuteBoxOver);
|
|
|
|
var muteButton = this.controls.muteButton = document.createElement('button');
|
|
muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
|
|
muteButton.setAttribute('aria-label', this.UIString('Mute'));
|
|
// Make the mute button a checkbox since it only has on/off states.
|
|
muteButton.setAttribute('role', 'checkbox');
|
|
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 volumeBoxBackground = this.controls.volumeBoxBackground = document.createElement('div');
|
|
volumeBoxBackground.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-background');
|
|
|
|
var volumeBoxTint = this.controls.volumeBoxTint = document.createElement('div');
|
|
volumeBoxTint.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container-tint');
|
|
|
|
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 = .05;
|
|
this.listenFor(volume, 'input', this.handleVolumeSliderInput);
|
|
this.listenFor(volume, 'change', this.handleVolumeSliderChange);
|
|
this.listenFor(volume, 'mousedown', this.handleVolumeSliderMouseDown);
|
|
this.listenFor(volume, 'mouseup', this.handleVolumeSliderMouseUp);
|
|
|
|
this.volumeContextName = "_webkit-media-controls-volume-" + this.host.generateUUID();
|
|
volume.style.backgroundImage = '-webkit-canvas(' + this.volumeContextName + ')';
|
|
|
|
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', 'audioAndTextTrackMenu');
|
|
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);
|
|
|
|
var pictureInPictureButton = this.controls.pictureInPictureButton = document.createElement('button');
|
|
pictureInPictureButton.setAttribute('pseudo', '-webkit-media-controls-picture-in-picture-button');
|
|
pictureInPictureButton.setAttribute('aria-label', this.UIString('Display Picture in Picture'));
|
|
pictureInPictureButton.classList.add(this.ClassNames.pictureInPictureButton);
|
|
this.listenFor(pictureInPictureButton, 'click', this.handlePictureInPictureButtonClicked);
|
|
|
|
var inlinePlaybackPlaceholder = this.controls.inlinePlaybackPlaceholder = document.createElement('div');
|
|
inlinePlaybackPlaceholder.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status');
|
|
inlinePlaybackPlaceholder.setAttribute('aria-label', this.UIString('Video Playback Placeholder'));
|
|
this.listenFor(inlinePlaybackPlaceholder, 'click', this.handlePlaceholderClick);
|
|
this.listenFor(inlinePlaybackPlaceholder, 'dblclick', this.handlePlaceholderClick);
|
|
if (!Controller.gSimulatePictureInPictureAvailable)
|
|
inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
|
|
|
|
var inlinePlaybackPlaceholderText = this.controls.inlinePlaybackPlaceholderText = document.createElement('div');
|
|
inlinePlaybackPlaceholderText.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text');
|
|
|
|
var inlinePlaybackPlaceholderTextTop = this.controls.inlinePlaybackPlaceholderTextTop = document.createElement('p');
|
|
inlinePlaybackPlaceholderTextTop.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-top');
|
|
|
|
var inlinePlaybackPlaceholderTextBottom = this.controls.inlinePlaybackPlaceholderTextBottom = document.createElement('p');
|
|
inlinePlaybackPlaceholderTextBottom.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-text-bottom');
|
|
|
|
var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button');
|
|
wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button');
|
|
wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display'));
|
|
this.listenFor(wirelessTargetPicker, 'click', this.handleWirelessPickerButtonClicked);
|
|
|
|
// Show controls button is an accessibility workaround since the controls are now removed from the DOM. http://webkit.org/b/145684
|
|
var showControlsButton = this.showControlsButton = document.createElement('button');
|
|
showControlsButton.setAttribute('pseudo', '-webkit-media-show-controls');
|
|
this.showShowControlsButton(false);
|
|
showControlsButton.setAttribute('aria-label', this.UIString('Show Controls'));
|
|
this.listenFor(showControlsButton, 'click', this.handleShowControlsClick);
|
|
this.base.appendChild(showControlsButton);
|
|
|
|
if (!Controller.gSimulateWirelessPlaybackTarget)
|
|
wirelessTargetPicker.classList.add(this.ClassNames.hidden);
|
|
},
|
|
|
|
createTimeClones: function()
|
|
{
|
|
var currentTimeClone = this.currentTimeClone = document.createElement('div');
|
|
currentTimeClone.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
|
|
currentTimeClone.setAttribute('aria-hidden', 'true');
|
|
currentTimeClone.classList.add('clone');
|
|
this.base.appendChild(currentTimeClone);
|
|
|
|
var remainingTimeClone = this.remainingTimeClone = document.createElement('div');
|
|
remainingTimeClone.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
|
|
remainingTimeClone.setAttribute('aria-hidden', 'true');
|
|
remainingTimeClone.classList.add('clone');
|
|
this.base.appendChild(remainingTimeClone);
|
|
},
|
|
|
|
setControlsType: function(type)
|
|
{
|
|
if (type === this.controlsType)
|
|
return;
|
|
this.controlsType = type;
|
|
|
|
this.reconnectControls();
|
|
this.updateShouldListenForPlaybackTargetAvailabilityEvent();
|
|
},
|
|
|
|
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.currentPlaybackTargetIsWireless())
|
|
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()
|
|
{
|
|
this.controls.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
|
|
this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
|
|
this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
|
|
this.controls.panel.appendChild(this.controls.panelBackgroundContainer);
|
|
this.controls.panelBackgroundContainer.appendChild(this.controls.panelBackground);
|
|
this.controls.panelBackgroundContainer.appendChild(this.controls.panelTint);
|
|
this.controls.panel.appendChild(this.controls.playButton);
|
|
if (!this.isLive)
|
|
this.controls.panel.appendChild(this.controls.rewindButton);
|
|
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.volumeBoxBackground);
|
|
this.controls.volumeBox.appendChild(this.controls.volumeBoxTint);
|
|
this.controls.volumeBox.appendChild(this.controls.volume);
|
|
this.controls.muteBox.appendChild(this.controls.muteButton);
|
|
this.controls.panel.appendChild(this.controls.wirelessTargetPicker);
|
|
this.controls.panel.appendChild(this.controls.captionButton);
|
|
if (!this.isAudio()) {
|
|
this.updatePictureInPictureButton();
|
|
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.inlinePlaybackPlaceholder.appendChild(this.controls.inlinePlaybackPlaceholderText);
|
|
this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextTop);
|
|
this.controls.inlinePlaybackPlaceholderText.appendChild(this.controls.inlinePlaybackPlaceholderTextBottom);
|
|
this.controls.panel.appendChild(this.controls.panelBackground);
|
|
this.controls.panel.appendChild(this.controls.panelTint);
|
|
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.wirelessTargetPicker);
|
|
this.controls.panel.appendChild(this.controls.captionButton);
|
|
if (!this.isAudio()) {
|
|
this.updatePictureInPictureButton();
|
|
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.setNeedsUpdateForDisplayedWidth();
|
|
this.updateLayoutForDisplayedWidth();
|
|
this.setNeedsTimelineMetricsUpdate();
|
|
|
|
if (this.shouldShowControls()) {
|
|
this.controls.panel.classList.add(this.ClassNames.show);
|
|
this.controls.panel.classList.remove(this.ClassNames.hidden);
|
|
this.resetHideControlsTimer();
|
|
this.showShowControlsButton(false);
|
|
} else {
|
|
this.controls.panel.classList.remove(this.ClassNames.show);
|
|
this.controls.panel.classList.add(this.ClassNames.hidden);
|
|
this.showShowControlsButton(true);
|
|
}
|
|
},
|
|
|
|
isPlayable: function()
|
|
{
|
|
return this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error;
|
|
},
|
|
|
|
updateStatusDisplay: function(event)
|
|
{
|
|
this.updateShouldListenForPlaybackTargetAvailabilityEvent();
|
|
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.isPlayable() && this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
|
|
this.controls.statusDisplay.innerText = this.UIString('Loading');
|
|
else
|
|
this.controls.statusDisplay.innerText = '';
|
|
|
|
this.setStatusHidden(!this.isLive && this.isPlayable());
|
|
},
|
|
|
|
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.updateReadyState();
|
|
this.updateDuration();
|
|
this.updateCaptionButton();
|
|
this.updateCaptionContainer();
|
|
this.updateFullscreenButtons();
|
|
this.updateWirelessTargetAvailable();
|
|
this.updateWirelessTargetPickerButton();
|
|
this.updateProgress();
|
|
this.updateControls();
|
|
},
|
|
|
|
handleTimeUpdate: function(event)
|
|
{
|
|
if (!this.scrubbing) {
|
|
this.updateTime();
|
|
this.updateProgress();
|
|
}
|
|
this.drawTimelineBackground();
|
|
},
|
|
|
|
handleDurationChange: function(event)
|
|
{
|
|
this.updateDuration();
|
|
this.updateTime();
|
|
this.updateProgress();
|
|
},
|
|
|
|
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();
|
|
},
|
|
|
|
handleAudioTrackChange: function(event)
|
|
{
|
|
this.updateHasAudio();
|
|
},
|
|
|
|
handleAudioTrackAdd: function(event)
|
|
{
|
|
this.updateHasAudio();
|
|
this.updateCaptionButton();
|
|
},
|
|
|
|
handleAudioTrackRemove: function(event)
|
|
{
|
|
this.updateHasAudio();
|
|
this.updateCaptionButton();
|
|
},
|
|
|
|
presentationMode: function() {
|
|
if ('webkitPresentationMode' in this.video)
|
|
return this.video.webkitPresentationMode;
|
|
|
|
if (this.isFullScreen())
|
|
return 'fullscreen';
|
|
|
|
return 'inline';
|
|
},
|
|
|
|
isFullScreen: function()
|
|
{
|
|
if (!this.video.webkitDisplayingFullscreen)
|
|
return false;
|
|
|
|
if ('webkitPresentationMode' in this.video && this.video.webkitPresentationMode === 'picture-in-picture')
|
|
return false;
|
|
|
|
return true;
|
|
},
|
|
|
|
updatePictureInPictureButton: function()
|
|
{
|
|
var shouldShowPictureInPictureButton = (Controller.gSimulatePictureInPictureAvailable || ('webkitSupportsPresentationMode' in this.video && this.video.webkitSupportsPresentationMode('picture-in-picture'))) && this.hasVideo();
|
|
if (shouldShowPictureInPictureButton) {
|
|
if (!this.controls.pictureInPictureButton.parentElement) {
|
|
if (this.controls.fullscreenButton.parentElement == this.controls.panel)
|
|
this.controls.panel.insertBefore(this.controls.pictureInPictureButton, this.controls.fullscreenButton);
|
|
else
|
|
this.controls.panel.appendChild(this.controls.pictureInPictureButton);
|
|
}
|
|
this.controls.pictureInPictureButton.classList.remove(this.ClassNames.hidden);
|
|
} else
|
|
this.controls.pictureInPictureButton.classList.add(this.ClassNames.hidden);
|
|
},
|
|
|
|
timelineStepFromVideoDuration: function()
|
|
{
|
|
var step;
|
|
var duration = this.video.duration;
|
|
if (duration <= 10)
|
|
step = .5;
|
|
else if (duration <= 60)
|
|
step = 1;
|
|
else if (duration <= 600)
|
|
step = 10;
|
|
else if (duration <= 3600)
|
|
step = 30;
|
|
else
|
|
step = 60;
|
|
|
|
return step;
|
|
},
|
|
|
|
incrementTimelineValue: function()
|
|
{
|
|
var value = this.video.currentTime + this.timelineStepFromVideoDuration();
|
|
return value > this.video.duration ? this.video.duration : value;
|
|
},
|
|
|
|
decrementTimelineValue: function()
|
|
{
|
|
var value = this.video.currentTime - this.timelineStepFromVideoDuration();
|
|
return value < 0 ? 0 : value;
|
|
},
|
|
|
|
showInlinePlaybackPlaceholderWhenSafe: function() {
|
|
if (this.presentationMode() != 'picture-in-picture')
|
|
return;
|
|
|
|
if (!this.host.isVideoLayerInline) {
|
|
this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
|
|
this.base.classList.add(this.ClassNames.placeholderShowing);
|
|
} else
|
|
setTimeout(this.showInlinePlaybackPlaceholderWhenSafe.bind(this), this.PlaceholderPollingDelay);
|
|
},
|
|
|
|
shouldReturnVideoLayerToInline: function()
|
|
{
|
|
var presentationMode = this.presentationMode();
|
|
return presentationMode === 'inline' || presentationMode === 'fullscreen';
|
|
},
|
|
|
|
updatePictureInPicturePlaceholder: function()
|
|
{
|
|
var presentationMode = this.presentationMode();
|
|
|
|
switch (presentationMode) {
|
|
case 'inline':
|
|
this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
|
|
this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.base.classList.remove(this.ClassNames.placeholderShowing);
|
|
|
|
this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
|
|
break;
|
|
case 'picture-in-picture':
|
|
this.controls.panel.classList.add(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.pictureInPicture);
|
|
this.showInlinePlaybackPlaceholderWhenSafe();
|
|
|
|
this.controls.inlinePlaybackPlaceholderTextTop.innerText = this.UIString('This video is playing in picture in picture.');
|
|
this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.innerText = "";
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.pictureInPicture);
|
|
|
|
this.controls.pictureInPictureButton.classList.add(this.ClassNames.returnFromPictureInPicture);
|
|
break;
|
|
default:
|
|
this.controls.panel.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.pictureInPicture);
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.pictureInPicture);
|
|
|
|
this.controls.pictureInPictureButton.classList.remove(this.ClassNames.returnFromPictureInPicture);
|
|
break;
|
|
}
|
|
},
|
|
|
|
handlePresentationModeChange: function(event)
|
|
{
|
|
this.updatePictureInPicturePlaceholder();
|
|
this.updateControls();
|
|
this.updateCaptionContainer();
|
|
this.resetHideControlsTimer();
|
|
if (this.presentationMode() != 'fullscreen' && this.video.paused && this.controlsAreHidden())
|
|
this.showControls();
|
|
},
|
|
|
|
handleFullscreenChange: function(event)
|
|
{
|
|
this.updateBase();
|
|
this.updateControls();
|
|
this.updateFullscreenButtons();
|
|
this.updateWirelessPlaybackStatus();
|
|
|
|
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();
|
|
}
|
|
|
|
if ('webkitPresentationMode' in this.video)
|
|
this.handlePresentationModeChange(event);
|
|
},
|
|
|
|
handleShowControlsClick: function(event)
|
|
{
|
|
if (!this.video.controls && !this.isFullScreen())
|
|
return;
|
|
|
|
if (this.controlsAreHidden())
|
|
this.showControls(true);
|
|
},
|
|
|
|
handleWrapperMouseMove: function(event)
|
|
{
|
|
if (!this.video.controls && !this.isFullScreen())
|
|
return;
|
|
|
|
if (this.controlsAreHidden())
|
|
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.panelTint && event.target != this.controls.inlinePlaybackPlaceholder)
|
|
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) && !this.controlsAlwaysVisible() && (this.video.controls || this.isFullScreen())) {
|
|
this.base.removeChild(this.controls.inlinePlaybackPlaceholder);
|
|
this.base.removeChild(this.controls.panel);
|
|
}
|
|
},
|
|
|
|
handlePanelClick: function(event)
|
|
{
|
|
// Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
|
|
event.preventDefault();
|
|
},
|
|
|
|
handlePanelDragStart: function(event)
|
|
{
|
|
// Prevent drags in the panel from triggering a drag event on the <video> element.
|
|
event.preventDefault();
|
|
},
|
|
|
|
handlePlaceholderClick: function(event)
|
|
{
|
|
// Prevent clicks in the placeholder 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.canToggleShowControlsButton = true;
|
|
this.video.play();
|
|
} else
|
|
this.video.pause();
|
|
return true;
|
|
},
|
|
|
|
handleTimelineInput: function(event)
|
|
{
|
|
if (this.scrubbing)
|
|
this.video.pause();
|
|
|
|
this.video.fastSeek(this.controls.timeline.value);
|
|
this.updateControlsWhileScrubbing();
|
|
},
|
|
|
|
handleTimelineChange: function(event)
|
|
{
|
|
this.video.currentTime = this.controls.timeline.value;
|
|
this.updateProgress();
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
handleTimelineKeyDown: function(event)
|
|
{
|
|
if (event.keyCode == this.KeyCodes.left)
|
|
this.controls.timeline.value = this.decrementTimelineValue();
|
|
else if (event.keyCode == this.KeyCodes.right)
|
|
this.controls.timeline.value = this.incrementTimelineValue();
|
|
},
|
|
|
|
handleMuteButtonClicked: function(event)
|
|
{
|
|
this.video.muted = !this.video.muted;
|
|
if (this.video.muted)
|
|
this.controls.muteButton.setAttribute('aria-checked', 'true');
|
|
else
|
|
this.controls.muteButton.setAttribute('aria-checked', 'false');
|
|
this.drawVolumeBackground();
|
|
return true;
|
|
},
|
|
|
|
handleMuteBoxOver: function(event)
|
|
{
|
|
this.drawVolumeBackground();
|
|
},
|
|
|
|
handleMinButtonClicked: function(event)
|
|
{
|
|
if (this.video.muted) {
|
|
this.video.muted = false;
|
|
this.controls.muteButton.setAttribute('aria-checked', 'false');
|
|
}
|
|
this.video.volume = 0;
|
|
return true;
|
|
},
|
|
|
|
handleMaxButtonClicked: function(event)
|
|
{
|
|
if (this.video.muted) {
|
|
this.video.muted = false;
|
|
this.controls.muteButton.setAttribute('aria-checked', 'false');
|
|
}
|
|
this.video.volume = 1;
|
|
},
|
|
|
|
updateVideoVolume: function()
|
|
{
|
|
if (this.video.muted) {
|
|
this.video.muted = false;
|
|
this.controls.muteButton.setAttribute('aria-checked', 'false');
|
|
}
|
|
this.video.volume = this.controls.volume.value;
|
|
this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
|
|
},
|
|
|
|
handleVolumeSliderInput: function(event)
|
|
{
|
|
this.updateVideoVolume();
|
|
this.drawVolumeBackground();
|
|
},
|
|
|
|
handleVolumeSliderChange: function(event)
|
|
{
|
|
this.updateVideoVolume();
|
|
},
|
|
|
|
handleVolumeSliderMouseDown: function(event)
|
|
{
|
|
this.isVolumeSliderActive = true;
|
|
this.drawVolumeBackground();
|
|
},
|
|
|
|
handleVolumeSliderMouseUp: function(event)
|
|
{
|
|
this.isVolumeSliderActive = false;
|
|
this.drawVolumeBackground();
|
|
},
|
|
|
|
handleCaptionButtonClicked: function(event)
|
|
{
|
|
if (this.captionMenu)
|
|
this.destroyCaptionMenu();
|
|
else
|
|
this.buildCaptionMenu();
|
|
return true;
|
|
},
|
|
|
|
hasVideo: function()
|
|
{
|
|
return this.video.videoTracks && this.video.videoTracks.length;
|
|
},
|
|
|
|
updateFullscreenButtons: function()
|
|
{
|
|
var shouldBeHidden = !this.video.webkitSupportsFullscreen || !this.hasVideo();
|
|
this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, shouldBeHidden && !this.isFullScreen());
|
|
this.updatePictureInPictureButton();
|
|
this.setNeedsUpdateForDisplayedWidth();
|
|
this.updateLayoutForDisplayedWidth();
|
|
},
|
|
|
|
handleFullscreenButtonClicked: function(event)
|
|
{
|
|
if (this.isFullScreen())
|
|
this.video.webkitExitFullscreen();
|
|
else
|
|
this.video.webkitEnterFullscreen();
|
|
return true;
|
|
},
|
|
|
|
updateWirelessTargetPickerButton: function() {
|
|
var wirelessTargetPickerColor;
|
|
if (this.controls.wirelessTargetPicker.classList.contains('playing'))
|
|
wirelessTargetPickerColor = "-apple-wireless-playback-target-active";
|
|
else
|
|
wirelessTargetPickerColor = "rgba(255,255,255,0.45)";
|
|
if (window.devicePixelRatio == 2)
|
|
this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,0.5 L 16,0.5 L 16,15.5 L 0,15.5 z M 0,14.5 L 16,14.5 L 8,5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 3.5,13.25 L 12.5,13.25 L 8,8 z'/></svg>\")";
|
|
else
|
|
this.controls.wirelessTargetPicker.style.backgroundImage = "url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 15' stroke='" + wirelessTargetPickerColor + "'><defs> <clipPath fill-rule='evenodd' id='cut-hole'><path d='M 0,1 L 16,1 L 16,16 L 0,16 z M 0,15 L 16,15 L 8,5.5 z'/></clipPath></defs><rect fill='none' clip-path='url(#cut-hole)' x='0.5' y='2.5' width='15' height='8'/><path stroke='none' fill='" + wirelessTargetPickerColor +"' d='M 2.75,14 L 13.25,14 L 8,8.75 z'/></svg>\")";
|
|
},
|
|
|
|
handleControlsChange: function()
|
|
{
|
|
try {
|
|
this.updateBase();
|
|
|
|
if (this.shouldHaveControls() && !this.hasControls())
|
|
this.addControls();
|
|
else if (!this.shouldHaveControls() && this.hasControls())
|
|
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);
|
|
|
|
var timeControls = [this.controls.currentTime, this.controls.remainingTime, this.currentTimeClone, this.remainingTimeClone];
|
|
|
|
function removeTimeClass(className) {
|
|
for (let element of timeControls)
|
|
element.classList.remove(className);
|
|
}
|
|
|
|
function addTimeClass(className) {
|
|
for (let element of timeControls)
|
|
element.classList.add(className);
|
|
}
|
|
|
|
// Reset existing style.
|
|
removeTimeClass(this.ClassNames.threeDigitTime);
|
|
removeTimeClass(this.ClassNames.fourDigitTime);
|
|
removeTimeClass(this.ClassNames.fiveDigitTime);
|
|
removeTimeClass(this.ClassNames.sixDigitTime);
|
|
|
|
if (duration >= 60*60*10)
|
|
addTimeClass(this.ClassNames.sixDigitTime);
|
|
else if (duration >= 60*60)
|
|
addTimeClass(this.ClassNames.fiveDigitTime);
|
|
else if (duration >= 60*10)
|
|
addTimeClass(this.ClassNames.fourDigitTime);
|
|
else
|
|
addTimeClass(this.ClassNames.threeDigitTime);
|
|
},
|
|
|
|
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()
|
|
{
|
|
this.updateTimelineMetricsIfNeeded();
|
|
this.drawTimelineBackground();
|
|
},
|
|
|
|
addRoundedRect: function(ctx, x, y, width, height, radius) {
|
|
ctx.moveTo(x + radius, y);
|
|
ctx.arcTo(x + width, y, x + width, y + radius, radius);
|
|
ctx.lineTo(x + width, y + height - radius);
|
|
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
|
|
ctx.lineTo(x + radius, y + height);
|
|
ctx.arcTo(x, y + height, x, y + height - radius, radius);
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.arcTo(x, y, x + radius, y, radius);
|
|
},
|
|
|
|
drawTimelineBackground: function() {
|
|
var dpr = window.devicePixelRatio;
|
|
var width = this.timelineWidth * dpr;
|
|
var height = this.timelineHeight * dpr;
|
|
|
|
if (!width || !height)
|
|
return;
|
|
|
|
var played = this.controls.timeline.value / this.controls.timeline.max;
|
|
var buffered = 0;
|
|
for (var i = 0, end = this.video.buffered.length; i < end; ++i)
|
|
buffered = Math.max(this.video.buffered.end(i), buffered);
|
|
|
|
buffered /= this.video.duration;
|
|
|
|
var ctx = document.getCSSCanvasContext('2d', this.timelineContextName, width, height);
|
|
|
|
width /= dpr;
|
|
height /= dpr;
|
|
|
|
ctx.save();
|
|
ctx.scale(dpr, dpr);
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
var timelineHeight = 3;
|
|
var trackHeight = 1;
|
|
var scrubberWidth = 3;
|
|
var scrubberHeight = 15;
|
|
var borderSize = 2;
|
|
var scrubberPosition = Math.max(0, Math.min(width - scrubberWidth, Math.round(width * played)));
|
|
|
|
// Draw buffered section.
|
|
ctx.save();
|
|
if (this.isAudio())
|
|
ctx.fillStyle = "rgb(71, 71, 71)";
|
|
else
|
|
ctx.fillStyle = "rgb(30, 30, 30)";
|
|
ctx.fillRect(1, 8, Math.round(width * buffered) - borderSize, trackHeight);
|
|
ctx.restore();
|
|
|
|
// Draw timeline border.
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, scrubberPosition, 7, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
|
|
this.addRoundedRect(ctx, scrubberPosition + 1, 8, width - scrubberPosition - borderSize , trackHeight, trackHeight / 2.0);
|
|
ctx.closePath();
|
|
ctx.clip("evenodd");
|
|
if (this.isAudio())
|
|
ctx.fillStyle = "rgb(71, 71, 71)";
|
|
else
|
|
ctx.fillStyle = "rgb(30, 30, 30)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
|
|
// Draw played section.
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, 0, 7, width, timelineHeight, timelineHeight / 2.0);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
if (this.isAudio())
|
|
ctx.fillStyle = "rgb(116, 116, 116)";
|
|
else
|
|
ctx.fillStyle = "rgb(75, 75, 75)";
|
|
ctx.fillRect(0, 0, width * played, height);
|
|
ctx.restore();
|
|
|
|
// Draw the scrubber.
|
|
ctx.save();
|
|
ctx.clearRect(scrubberPosition - 1, 0, scrubberWidth + borderSize, height, 0);
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, scrubberPosition, 1, scrubberWidth, scrubberHeight, 1);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
if (this.isAudio())
|
|
ctx.fillStyle = "rgb(181, 181, 181)";
|
|
else
|
|
ctx.fillStyle = "rgb(140, 140, 140)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
drawVolumeBackground: function() {
|
|
var dpr = window.devicePixelRatio;
|
|
var width = this.controls.volume.offsetWidth * dpr;
|
|
var height = this.controls.volume.offsetHeight * dpr;
|
|
|
|
if (!width || !height)
|
|
return;
|
|
|
|
var ctx = document.getCSSCanvasContext('2d', this.volumeContextName, width, height);
|
|
|
|
width /= dpr;
|
|
height /= dpr;
|
|
|
|
ctx.save();
|
|
ctx.scale(dpr, dpr);
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
var seekerPosition = this.controls.volume.value;
|
|
var trackHeight = 1;
|
|
var timelineHeight = 3;
|
|
var scrubberRadius = 3.5;
|
|
var scrubberDiameter = 2 * scrubberRadius;
|
|
var borderSize = 2;
|
|
|
|
var scrubberPosition = Math.round(seekerPosition * (width - scrubberDiameter - borderSize));
|
|
|
|
|
|
// Draw portion of volume under slider thumb.
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, 0, 3, scrubberPosition + 2, timelineHeight, timelineHeight / 2.0);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
ctx.fillStyle = "rgb(75, 75, 75)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
|
|
// Draw portion of volume above slider thumb.
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, scrubberPosition, 3, width - scrubberPosition, timelineHeight, timelineHeight / 2.0);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
ctx.fillStyle = "rgb(30, 30, 30)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
|
|
// Clear a hole in the slider for the scrubber.
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, scrubberPosition, 0, scrubberDiameter + borderSize, height, (scrubberDiameter + borderSize) / 2.0);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.restore();
|
|
|
|
// Draw scrubber.
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
this.addRoundedRect(ctx, scrubberPosition + 1, 1, scrubberDiameter, scrubberDiameter, scrubberRadius);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
if (this.isVolumeSliderActive)
|
|
ctx.fillStyle = "white";
|
|
else
|
|
ctx.fillStyle = "rgb(140, 140, 140)";
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
formatTimeDescription: 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 secondString = intSeconds == 1 ? 'Second' : 'Seconds';
|
|
var minuteString = intMinutes == 1 ? 'Minute' : 'Minutes';
|
|
var hourString = intHours == 1 ? 'Hour' : 'Hours';
|
|
if (intHours > 0)
|
|
return `${intHours} ${this.UIString(hourString)}, ${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`;
|
|
if (intMinutes > 0)
|
|
return `${intMinutes} ${this.UIString(minuteString)}, ${intSeconds} ${this.UIString(secondString)}`;
|
|
return `${intSeconds} ${this.UIString(secondString)}`;
|
|
},
|
|
|
|
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.video.controls && !this.isFullScreen())
|
|
return;
|
|
|
|
if (this.isPlaying === isPlaying)
|
|
return;
|
|
this.isPlaying = isPlaying;
|
|
|
|
if (!isPlaying) {
|
|
this.controls.panel.classList.add(this.ClassNames.paused);
|
|
if (this.controls.panelBackground)
|
|
this.controls.panelBackground.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);
|
|
if (this.controls.panelBackground)
|
|
this.controls.panelBackground.classList.remove(this.ClassNames.paused);
|
|
this.controls.playButton.classList.remove(this.ClassNames.paused);
|
|
this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
|
|
this.resetHideControlsTimer();
|
|
this.canToggleShowControlsButton = true;
|
|
}
|
|
},
|
|
|
|
updateForShowingControls: function()
|
|
{
|
|
this.updateLayoutForDisplayedWidth();
|
|
this.setNeedsTimelineMetricsUpdate();
|
|
this.updateTime();
|
|
this.updateProgress();
|
|
this.drawVolumeBackground();
|
|
this.drawTimelineBackground();
|
|
this.controls.panel.classList.add(this.ClassNames.show);
|
|
this.controls.panel.classList.remove(this.ClassNames.hidden);
|
|
if (this.controls.panelBackground) {
|
|
this.controls.panelBackground.classList.add(this.ClassNames.show);
|
|
this.controls.panelBackground.classList.remove(this.ClassNames.hidden);
|
|
}
|
|
},
|
|
|
|
showShowControlsButton: function (shouldShow) {
|
|
this.showControlsButton.hidden = !shouldShow;
|
|
if (shouldShow && this.shouldHaveControls())
|
|
this.showControlsButton.focus();
|
|
},
|
|
|
|
showControls: function(focusControls)
|
|
{
|
|
this.updateShouldListenForPlaybackTargetAvailabilityEvent();
|
|
if (!this.video.controls && !this.isFullScreen())
|
|
return;
|
|
|
|
this.updateForShowingControls();
|
|
if (this.shouldHaveControls() && !this.controls.panel.parentElement) {
|
|
this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
|
|
this.base.appendChild(this.controls.panel);
|
|
if (focusControls)
|
|
this.controls.playButton.focus();
|
|
}
|
|
this.showShowControlsButton(false);
|
|
},
|
|
|
|
hideControls: function()
|
|
{
|
|
if (this.controlsAlwaysVisible())
|
|
return;
|
|
|
|
this.clearHideControlsTimer();
|
|
this.updateShouldListenForPlaybackTargetAvailabilityEvent();
|
|
this.controls.panel.classList.remove(this.ClassNames.show);
|
|
if (this.controls.panelBackground)
|
|
this.controls.panelBackground.classList.remove(this.ClassNames.show);
|
|
this.showShowControlsButton(this.isPlayable() && this.isPlaying && this.canToggleShowControlsButton);
|
|
},
|
|
|
|
setNeedsUpdateForDisplayedWidth: function()
|
|
{
|
|
this.currentDisplayWidth = 0;
|
|
},
|
|
|
|
scheduleUpdateLayoutForDisplayedWidth: function()
|
|
{
|
|
setTimeout(this.updateLayoutForDisplayedWidth.bind(this), 0);
|
|
},
|
|
|
|
isControlVisible: function(control)
|
|
{
|
|
if (!control)
|
|
return false;
|
|
if (!this.root.contains(control))
|
|
return false;
|
|
return !control.classList.contains(this.ClassNames.hidden)
|
|
},
|
|
|
|
updateLayoutForDisplayedWidth: function()
|
|
{
|
|
if (!this.controls || !this.controls.panel)
|
|
return;
|
|
|
|
var visibleWidth = this.controls.panel.getBoundingClientRect().width;
|
|
if (this._pageScaleFactor > 1)
|
|
visibleWidth *= this._pageScaleFactor;
|
|
|
|
if (visibleWidth <= 0 || visibleWidth == this.currentDisplayWidth)
|
|
return;
|
|
|
|
this.currentDisplayWidth = visibleWidth;
|
|
|
|
// Filter all the buttons which are not explicitly hidden.
|
|
var buttons = [this.controls.playButton, this.controls.rewindButton, this.controls.captionButton,
|
|
this.controls.fullscreenButton, this.controls.pictureInPictureButton,
|
|
this.controls.wirelessTargetPicker, this.controls.muteBox];
|
|
var visibleButtons = buttons.filter(this.isControlVisible, this);
|
|
|
|
// This tells us how much room we need in order to display every visible button.
|
|
var visibleButtonWidth = this.ButtonWidth * visibleButtons.length;
|
|
|
|
var currentTimeWidth = this.currentTimeClone.getBoundingClientRect().width;
|
|
var remainingTimeWidth = this.remainingTimeClone.getBoundingClientRect().width;
|
|
|
|
// Check if there is enough room for the scrubber.
|
|
var shouldDropTimeline = (visibleWidth - visibleButtonWidth - currentTimeWidth - remainingTimeWidth) < this.MinimumTimelineWidth;
|
|
this.controls.timeline.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
|
|
this.controls.currentTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
|
|
this.controls.thumbnailTrack.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
|
|
this.controls.remainingTime.classList.toggle(this.ClassNames.dropped, shouldDropTimeline);
|
|
|
|
// Then controls in the following order:
|
|
var removeOrder = [this.controls.wirelessTargetPicker, this.controls.pictureInPictureButton,
|
|
this.controls.captionButton, this.controls.muteBox, this.controls.rewindButton,
|
|
this.controls.fullscreenButton];
|
|
removeOrder.forEach(function(control) {
|
|
var shouldDropControl = visibleWidth < visibleButtonWidth && this.isControlVisible(control);
|
|
control.classList.toggle(this.ClassNames.dropped, shouldDropControl);
|
|
if (shouldDropControl)
|
|
visibleButtonWidth -= this.ButtonWidth;
|
|
}, this);
|
|
},
|
|
|
|
controlsAlwaysVisible: function()
|
|
{
|
|
if (this.presentationMode() === 'picture-in-picture')
|
|
return true;
|
|
|
|
return this.isAudio() || this.currentPlaybackTargetIsWireless() || this.scrubbing;
|
|
},
|
|
|
|
controlsAreHidden: function()
|
|
{
|
|
return !this.controlsAlwaysVisible() && !this.controls.panel.classList.contains(this.ClassNames.show) && !this.controls.panel.parentElement;
|
|
},
|
|
|
|
removeControls: function()
|
|
{
|
|
if (this.controls.panel.parentNode)
|
|
this.controls.panel.parentNode.removeChild(this.controls.panel);
|
|
this.destroyCaptionMenu();
|
|
},
|
|
|
|
addControls: function()
|
|
{
|
|
this.base.appendChild(this.controls.inlinePlaybackPlaceholder);
|
|
this.base.appendChild(this.controls.panel);
|
|
this.updateControls();
|
|
},
|
|
|
|
hasControls: function()
|
|
{
|
|
return this.controls.panel.parentElement;
|
|
},
|
|
|
|
updateTime: function()
|
|
{
|
|
var currentTime = this.video.currentTime;
|
|
var timeRemaining = currentTime - this.video.duration;
|
|
this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
|
|
this.controls.currentTime.setAttribute('aria-label', `${this.UIString('Elapsed')} ${this.formatTimeDescription(currentTime)}`);
|
|
this.controls.timeline.value = this.video.currentTime;
|
|
this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
|
|
this.controls.remainingTime.setAttribute('aria-label', `${this.UIString('Remaining')} ${this.formatTimeDescription(timeRemaining)}`);
|
|
|
|
// Mark the timeline value in percentage format in accessibility.
|
|
var timeElapsedPercent = currentTime / this.video.duration;
|
|
timeElapsedPercent = Math.max(Math.min(1, timeElapsedPercent), 0);
|
|
this.controls.timeline.setAttribute('aria-valuetext', `${parseInt(timeElapsedPercent * 100)}%`);
|
|
},
|
|
|
|
updateControlsWhileScrubbing: function()
|
|
{
|
|
if (!this.scrubbing)
|
|
return;
|
|
|
|
var currentTime = (this.controls.timeline.value / this.controls.timeline.max) * this.video.duration;
|
|
var timeRemaining = currentTime - this.video.duration;
|
|
this.currentTimeClone.innerText = this.controls.currentTime.innerText = this.formatTime(currentTime);
|
|
this.remainingTimeClone.innerText = this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
|
|
this.drawTimelineBackground();
|
|
},
|
|
|
|
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();
|
|
this.showControls();
|
|
} 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);
|
|
this.hideControls();
|
|
}
|
|
this.updateWirelessTargetAvailable();
|
|
},
|
|
|
|
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()
|
|
{
|
|
var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
|
|
var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
|
|
|
|
if ((textTracks && textTracks.length) || (audioTracks && audioTracks.length > 1))
|
|
this.controls.captionButton.classList.remove(this.ClassNames.hidden);
|
|
else
|
|
this.controls.captionButton.classList.add(this.ClassNames.hidden);
|
|
this.setNeedsUpdateForDisplayedWidth();
|
|
this.updateLayoutForDisplayedWidth();
|
|
},
|
|
|
|
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 audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
|
|
var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
|
|
|
|
if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
|
|
return;
|
|
|
|
this.captionMenu = document.createElement('div');
|
|
this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
|
|
this.captionMenu.setAttribute('id', 'audioAndTextTrackMenu');
|
|
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);
|
|
|
|
if (audioTracks && audioTracks.length > 1) {
|
|
var heading = document.createElement('h3');
|
|
heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
|
|
list.appendChild(heading);
|
|
heading.innerText = this.UIString('Audio');
|
|
|
|
var ul = document.createElement('ul');
|
|
ul.setAttribute('role', 'menu');
|
|
ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
|
|
list.appendChild(ul);
|
|
|
|
for (var i = 0; i < audioTracks.length; ++i) {
|
|
var menuItem = document.createElement('li');
|
|
menuItem.setAttribute('role', 'menuitemradio');
|
|
menuItem.setAttribute('tabindex', '-1');
|
|
this.captionMenuItems.push(menuItem);
|
|
this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
|
|
this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
|
|
ul.appendChild(menuItem);
|
|
|
|
var track = audioTracks[i];
|
|
menuItem.innerText = this.host.displayNameForTrack(track);
|
|
menuItem.track = track;
|
|
|
|
var itemCheckmark = document.createElement("img");
|
|
itemCheckmark.classList.add("checkmark-container");
|
|
menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
|
|
|
|
if (track.enabled) {
|
|
menuItem.classList.add(this.ClassNames.selected);
|
|
menuItem.setAttribute('tabindex', '0');
|
|
menuItem.setAttribute('aria-checked', 'true');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (textTracks && textTracks.length > 2) {
|
|
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 < textTracks.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 = textTracks[i];
|
|
menuItem.innerText = this.host.displayNameForTrack(track);
|
|
menuItem.track = track;
|
|
|
|
var itemCheckmark = document.createElement("img");
|
|
itemCheckmark.classList.add("checkmark-container");
|
|
menuItem.insertBefore(itemCheckmark, menuItem.firstChild);
|
|
|
|
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' || displayMode === 'manual') && !trackMenuItemSelected) {
|
|
offMenu.classList.add(this.ClassNames.selected);
|
|
offMenu.setAttribute('tabindex', '0');
|
|
offMenu.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();
|
|
},
|
|
|
|
audioTrackItemSelected: function(event)
|
|
{
|
|
for (var i = 0; i < this.video.audioTracks.length; ++i) {
|
|
var track = this.video.audioTracks[i];
|
|
track.enabled = (track == event.target.track);
|
|
}
|
|
|
|
this.destroyCaptionMenu();
|
|
},
|
|
|
|
focusSiblingAudioTrackItem: 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();
|
|
}
|
|
},
|
|
|
|
handleAudioTrackItemKeyUp: function(event)
|
|
{
|
|
switch (event.keyCode) {
|
|
case this.KeyCodes.enter:
|
|
case this.KeyCodes.space:
|
|
this.audioTrackItemSelected(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.focusSiblingAudioTrackItem(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.currentPlaybackTargetIsWireless())
|
|
this.controls.muteBox.classList.remove(this.ClassNames.hidden);
|
|
else
|
|
this.controls.muteBox.classList.add(this.ClassNames.hidden);
|
|
|
|
this.setNeedsUpdateForDisplayedWidth();
|
|
this.updateLayoutForDisplayedWidth();
|
|
},
|
|
|
|
updateHasVideo: function()
|
|
{
|
|
this.controls.panel.classList.toggle(this.ClassNames.noVideo, !this.hasVideo());
|
|
// The availability of the picture-in-picture button as well as the full-screen
|
|
// button depends no the value returned by hasVideo(), so make sure we invalidate
|
|
// the availability of both controls.
|
|
this.updateFullscreenButtons();
|
|
},
|
|
|
|
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;
|
|
}
|
|
this.controls.volume.setAttribute('aria-valuetext', `${parseInt(this.controls.volume.value * 100)}%`);
|
|
this.drawVolumeBackground();
|
|
},
|
|
|
|
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 = null;
|
|
}
|
|
|
|
if (this.isPlaying)
|
|
this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);
|
|
},
|
|
|
|
handlePictureInPictureButtonClicked: function(event) {
|
|
if (!('webkitSetPresentationMode' in this.video))
|
|
return;
|
|
|
|
if (this.presentationMode() === 'picture-in-picture')
|
|
this.video.webkitSetPresentationMode('inline');
|
|
else
|
|
this.video.webkitSetPresentationMode('picture-in-picture');
|
|
},
|
|
|
|
currentPlaybackTargetIsWireless: function() {
|
|
if (Controller.gSimulateWirelessPlaybackTarget)
|
|
return true;
|
|
|
|
if (!this.currentTargetIsWireless || this.wirelessPlaybackDisabled)
|
|
return false;
|
|
|
|
return true;
|
|
},
|
|
|
|
updateShouldListenForPlaybackTargetAvailabilityEvent: function() {
|
|
var shouldListen = true;
|
|
if (this.video.error)
|
|
shouldListen = false;
|
|
if (!this.isAudio() && !this.video.paused && this.controlsAreHidden())
|
|
shouldListen = false;
|
|
if (document.hidden)
|
|
shouldListen = false;
|
|
|
|
this.setShouldListenForPlaybackTargetAvailabilityEvent(shouldListen);
|
|
},
|
|
|
|
updateWirelessPlaybackStatus: function() {
|
|
if (this.currentPlaybackTargetIsWireless()) {
|
|
var deviceName = "";
|
|
var deviceType = "";
|
|
var type = this.host.externalDeviceType;
|
|
if (type == "airplay") {
|
|
deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##');
|
|
deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV");
|
|
} else if (type == "tvout") {
|
|
deviceType = this.UIString('##TVOUT_DEVICE_TYPE##');
|
|
deviceName = this.UIString('##TVOUT_DEVICE_NAME##');
|
|
}
|
|
|
|
this.controls.inlinePlaybackPlaceholderTextTop.innerText = deviceType;
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.innerText = deviceName;
|
|
this.controls.inlinePlaybackPlaceholder.setAttribute('aria-label', deviceType + ", " + deviceName);
|
|
this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.appleTV);
|
|
this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.hidden);
|
|
this.controls.wirelessTargetPicker.classList.add(this.ClassNames.playing);
|
|
if (!this.isFullScreen() && (this.video.offsetWidth <= 250 || this.video.offsetHeight <= 200)) {
|
|
this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.small);
|
|
this.controls.inlinePlaybackPlaceholderTextTop.classList.add(this.ClassNames.small);
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.classList.add(this.ClassNames.small);
|
|
} else {
|
|
this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.small);
|
|
this.controls.inlinePlaybackPlaceholderTextTop.classList.remove(this.ClassNames.small);
|
|
this.controls.inlinePlaybackPlaceholderTextBottom.classList.remove(this.ClassNames.small);
|
|
}
|
|
this.controls.volumeBox.classList.add(this.ClassNames.hidden);
|
|
this.controls.muteBox.classList.add(this.ClassNames.hidden);
|
|
this.updateBase();
|
|
this.showControls();
|
|
} else {
|
|
this.controls.inlinePlaybackPlaceholder.classList.add(this.ClassNames.hidden);
|
|
this.controls.inlinePlaybackPlaceholder.classList.remove(this.ClassNames.appleTV);
|
|
this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.playing);
|
|
this.controls.volumeBox.classList.remove(this.ClassNames.hidden);
|
|
this.controls.muteBox.classList.remove(this.ClassNames.hidden);
|
|
}
|
|
this.setNeedsUpdateForDisplayedWidth();
|
|
this.updateLayoutForDisplayedWidth();
|
|
this.reconnectControls();
|
|
this.updateWirelessTargetPickerButton();
|
|
},
|
|
|
|
updateWirelessTargetAvailable: function() {
|
|
this.currentTargetIsWireless = this.video.webkitCurrentPlaybackTargetIsWireless;
|
|
this.wirelessPlaybackDisabled = this.video.webkitWirelessVideoPlaybackDisabled;
|
|
|
|
var wirelessPlaybackTargetsAvailable = Controller.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets;
|
|
if (this.wirelessPlaybackDisabled)
|
|
wirelessPlaybackTargetsAvailable = false;
|
|
|
|
if (wirelessPlaybackTargetsAvailable && this.isPlayable())
|
|
this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden);
|
|
else
|
|
this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden);
|
|
this.setNeedsUpdateForDisplayedWidth();
|
|
this.updateLayoutForDisplayedWidth();
|
|
},
|
|
|
|
handleWirelessPickerButtonClicked: function(event)
|
|
{
|
|
this.video.webkitShowPlaybackTargetPicker();
|
|
return true;
|
|
},
|
|
|
|
handleWirelessPlaybackChange: function(event) {
|
|
this.updateWirelessTargetAvailable();
|
|
this.updateWirelessPlaybackStatus();
|
|
this.setNeedsTimelineMetricsUpdate();
|
|
},
|
|
|
|
handleWirelessTargetAvailableChange: function(event) {
|
|
var wirelessPlaybackTargetsAvailable = event.availability == "available";
|
|
if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable)
|
|
return;
|
|
|
|
this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable;
|
|
this.updateWirelessTargetAvailable();
|
|
this.setNeedsTimelineMetricsUpdate();
|
|
},
|
|
|
|
setShouldListenForPlaybackTargetAvailabilityEvent: function(shouldListen) {
|
|
if (!window.WebKitPlaybackTargetAvailabilityEvent || this.isListeningForPlaybackTargetAvailabilityEvent == shouldListen)
|
|
return;
|
|
|
|
if (shouldListen && this.video.error)
|
|
return;
|
|
|
|
this.isListeningForPlaybackTargetAvailabilityEvent = shouldListen;
|
|
if (shouldListen)
|
|
this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
|
|
else
|
|
this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange);
|
|
},
|
|
|
|
get scrubbing()
|
|
{
|
|
return this._scrubbing;
|
|
},
|
|
|
|
set scrubbing(flag)
|
|
{
|
|
if (this._scrubbing == flag)
|
|
return;
|
|
this._scrubbing = flag;
|
|
|
|
if (this._scrubbing)
|
|
this.wasPlayingWhenScrubbingStarted = !this.video.paused;
|
|
else if (this.wasPlayingWhenScrubbingStarted && this.video.paused) {
|
|
this.video.play();
|
|
this.resetHideControlsTimer();
|
|
}
|
|
},
|
|
|
|
get pageScaleFactor()
|
|
{
|
|
return this._pageScaleFactor;
|
|
},
|
|
|
|
set pageScaleFactor(newScaleFactor)
|
|
{
|
|
if (this._pageScaleFactor === newScaleFactor)
|
|
return;
|
|
|
|
this._pageScaleFactor = newScaleFactor;
|
|
},
|
|
|
|
set usesLTRUserInterfaceLayoutDirection(usesLTRUserInterfaceLayoutDirection)
|
|
{
|
|
this.controls.volumeBox.classList.toggle(this.ClassNames.usesLTRUserInterfaceLayoutDirection, usesLTRUserInterfaceLayoutDirection);
|
|
},
|
|
|
|
handleRootResize: function(event)
|
|
{
|
|
this.updateLayoutForDisplayedWidth();
|
|
this.setNeedsTimelineMetricsUpdate();
|
|
this.updateTimelineMetricsIfNeeded();
|
|
this.drawTimelineBackground();
|
|
},
|
|
|
|
getCurrentControlsStatus: function ()
|
|
{
|
|
var result = {
|
|
idiom: this.idiom,
|
|
status: "ok"
|
|
};
|
|
|
|
var elements = [
|
|
{
|
|
name: "Show Controls",
|
|
object: this.showControlsButton,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Status Display",
|
|
object: this.controls.statusDisplay,
|
|
styleValues: ["display"],
|
|
extraProperties: ["textContent"],
|
|
},
|
|
{
|
|
name: "Play Button",
|
|
object: this.controls.playButton,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Rewind Button",
|
|
object: this.controls.rewindButton,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Timeline Box",
|
|
object: this.controls.timelineBox,
|
|
},
|
|
{
|
|
name: "Mute Box",
|
|
object: this.controls.muteBox,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Fullscreen Button",
|
|
object: this.controls.fullscreenButton,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "AppleTV Device Picker",
|
|
object: this.controls.wirelessTargetPicker,
|
|
styleValues: ["display"],
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Picture-in-picture Button",
|
|
object: this.controls.pictureInPictureButton,
|
|
extraProperties: ["parentElement", "hidden"],
|
|
},
|
|
{
|
|
name: "Caption Button",
|
|
object: this.controls.captionButton,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Timeline",
|
|
object: this.controls.timeline,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Current Time",
|
|
object: this.controls.currentTime,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Thumbnail Track",
|
|
object: this.controls.thumbnailTrack,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Time Remaining",
|
|
object: this.controls.remainingTime,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Track Menu",
|
|
object: this.captionMenu,
|
|
},
|
|
{
|
|
name: "Inline playback placeholder",
|
|
object: this.controls.inlinePlaybackPlaceholder,
|
|
},
|
|
{
|
|
name: "Media Controls Panel",
|
|
object: this.controls.panel,
|
|
extraProperties: ["hidden"],
|
|
},
|
|
{
|
|
name: "Control Base Element",
|
|
object: this.base || null,
|
|
},
|
|
];
|
|
|
|
elements.forEach(function (element) {
|
|
var obj = element.object;
|
|
delete element.object;
|
|
|
|
element.computedStyle = {};
|
|
if (obj && element.styleValues) {
|
|
var computedStyle = window.getComputedStyle(obj);
|
|
element.styleValues.forEach(function (propertyName) {
|
|
element.computedStyle[propertyName] = computedStyle[propertyName];
|
|
});
|
|
delete element.styleValues;
|
|
}
|
|
|
|
element.bounds = obj ? obj.getBoundingClientRect() : null;
|
|
element.className = obj ? obj.className : null;
|
|
element.ariaLabel = obj ? obj.getAttribute('aria-label') : null;
|
|
|
|
if (element.extraProperties) {
|
|
element.extraProperties.forEach(function (property) {
|
|
element[property] = obj ? obj[property] : null;
|
|
});
|
|
delete element.extraProperties;
|
|
}
|
|
|
|
element.element = obj;
|
|
});
|
|
|
|
result.elements = elements;
|
|
|
|
return JSON.stringify(result);
|
|
}
|
|
|
|
};
|