1 function createControls(root, video, host)
3 return new Controller(root, video, host);
6 function Controller(root, video, host)
14 this.statusHidden = true;
16 this.addVideoListeners();
18 this.createControls();
20 this.updateControls();
21 this.updateDuration();
22 this.updateProgress();
24 this.updateReadyState();
26 this.updateThumbnail();
27 this.updateCaptionButton();
28 this.updateCaptionContainer();
29 this.updateFullscreenButton();
31 this.updateHasAudio();
32 this.updateHasVideo();
36 Controller.InlineControls = 0;
37 Controller.FullScreenControls = 1;
39 Controller.PlayAfterSeeking = 0;
40 Controller.PauseAfterSeeking = 1;
42 Controller.prototype = {
46 loadstart: 'handleLoadStart',
49 suspend: 'handleSuspend',
50 stalled: 'handleStalled',
51 waiting: 'handleWaiting',
52 emptied: 'handleReadyStateChange',
53 loadedmetadata: 'handleReadyStateChange',
54 loadeddata: 'handleReadyStateChange',
55 canplay: 'handleReadyStateChange',
56 canplaythrough: 'handleReadyStateChange',
57 timeupdate: 'handleTimeUpdate',
58 durationchange: 'handleDurationChange',
59 playing: 'handlePlay',
61 progress: 'handleProgress',
62 volumechange: 'handleVolumeChange',
63 webkitfullscreenchange: 'handleFullscreenChange',
64 webkitbeginfullscreen: 'handleFullscreenChange',
65 webkitendfullscreen: 'handleFullscreenChange',
67 HideControlsDelay: 4 * 1000,
77 hourLongTime: 'hour-long-time',
85 thumbnail: 'thumbnail',
86 thumbnailImage: 'thumbnail-image',
87 thumbnailTrack: 'thumbnail-track',
88 volumeBox: 'volume-box',
107 extend: function(child)
109 for (var property in this) {
110 if (!child.hasOwnProperty(property))
111 child[property] = this[property];
115 UIString: function(developmentString, replaceString, replacementString)
117 var localized = UIStringTable[developmentString];
118 if (replaceString && replacementString)
119 return localized.replace(replaceString, replacementString);
124 console.error("Localization for string \"" + developmentString + "\" not found.");
125 return "LOCALIZED STRING NOT FOUND";
128 listenFor: function(element, eventName, handler, useCapture)
130 if (typeof useCapture === 'undefined')
133 if (!(this.listeners[eventName] instanceof Array))
134 this.listeners[eventName] = [];
135 this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture});
136 element.addEventListener(eventName, this, useCapture);
139 stopListeningFor: function(element, eventName, handler, useCapture)
141 if (typeof useCapture === 'undefined')
144 if (!(this.listeners[eventName] instanceof Array))
147 this.listeners[eventName] = this.listeners[eventName].filter(function(entry) {
148 return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture);
150 element.removeEventListener(eventName, this, useCapture);
153 addVideoListeners: function()
155 for (name in this.HandledVideoEvents) {
156 this.listenFor(this.video, name, this.HandledVideoEvents[name]);
160 this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange);
161 this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
162 this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
165 this.listenFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
166 this.listenFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
167 this.listenFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
170 this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo);
171 this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
172 this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
174 /* controls attribute */
175 this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this));
176 this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] });
179 removeVideoListeners: function()
181 for (name in this.HandledVideoEvents) {
182 this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]);
186 this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange);
187 this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd);
188 this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove);
191 this.stopListeningFor(this.video.audioTracks, 'change', this.handleAudioTrackChange);
192 this.stopListeningFor(this.video.audioTracks, 'addtrack', this.handleAudioTrackAdd);
193 this.stopListeningFor(this.video.audioTracks, 'removetrack', this.handleAudioTrackRemove);
196 this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo);
197 this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo);
198 this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo);
200 /* controls attribute */
201 this.controlsObserver.disconnect();
202 delete(this.controlsObserver);
205 handleEvent: function(event)
207 var preventDefault = false;
210 if (event.target === this.video) {
211 var handlerName = this.HandledVideoEvents[event.type];
212 var handler = this[handlerName];
213 if (handler && handler instanceof Function)
214 handler.call(this, event);
217 if (!(this.listeners[event.type] instanceof Array))
220 this.listeners[event.type].forEach(function(entry) {
221 if (entry.element === event.currentTarget && entry.handler instanceof Function)
222 preventDefault |= entry.handler.call(this, event);
229 if (preventDefault) {
230 event.stopPropagation();
231 event.preventDefault();
235 createBase: function()
237 var base = this.base = document.createElement('div');
238 base.setAttribute('pseudo', '-webkit-media-controls');
239 this.listenFor(base, 'mousemove', this.handleWrapperMouseMove);
240 this.listenFor(base, 'mouseout', this.handleWrapperMouseOut);
241 if (this.host.textTrackContainer)
242 base.appendChild(this.host.textTrackContainer);
245 shouldHaveAnyUI: function()
247 return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length);
250 shouldHaveControls: function()
252 return this.video.controls || this.isFullScreen();
255 setNeedsTimelineMetricsUpdate: function()
257 this.timelineMetricsNeedsUpdate = true;
260 updateTimelineMetricsIfNeeded: function()
262 if (this.timelineMetricsNeedsUpdate) {
263 this.timelineLeft = this.controls.timeline.offsetLeft;
264 this.timelineWidth = this.controls.timeline.offsetWidth;
265 this.timelineHeight = this.controls.timeline.offsetHeight;
266 this.timelineMetricsNeedsUpdate = false;
270 updateBase: function()
272 if (this.shouldHaveAnyUI()) {
273 if (!this.base.parentNode) {
274 this.root.appendChild(this.base);
277 if (this.base.parentNode) {
278 this.base.parentNode.removeChild(this.base);
283 createControls: function()
285 var panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div');
286 panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent');
288 var panel = this.controls.panel = document.createElement('div');
289 panel.setAttribute('pseudo', '-webkit-media-controls-panel');
290 panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback')));
291 panel.setAttribute('role', 'toolbar');
292 this.listenFor(panel, 'mousedown', this.handlePanelMouseDown);
293 this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd);
294 this.listenFor(panel, 'click', this.handlePanelClick);
295 this.listenFor(panel, 'dblclick', this.handlePanelClick);
296 this.listenFor(panel, 'dragstart', this.handlePanelDragStart);
298 var rewindButton = this.controls.rewindButton = document.createElement('button');
299 rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button');
300 rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount));
301 this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked);
303 var seekBackButton = this.controls.seekBackButton = document.createElement('button');
304 seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button');
305 seekBackButton.setAttribute('aria-label', this.UIString('Rewind'));
306 this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown);
307 this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp);
309 var seekForwardButton = this.controls.seekForwardButton = document.createElement('button');
310 seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button');
311 seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward'));
312 this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown);
313 this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp);
315 var playButton = this.controls.playButton = document.createElement('button');
316 playButton.setAttribute('pseudo', '-webkit-media-controls-play-button');
317 playButton.setAttribute('aria-label', this.UIString('Play'));
318 this.listenFor(playButton, 'click', this.handlePlayButtonClicked);
320 var statusDisplay = this.controls.statusDisplay = document.createElement('div');
321 statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display');
322 statusDisplay.classList.add(this.ClassNames.hidden);
324 var timelineBox = this.controls.timelineBox = document.createElement('div');
325 timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container');
327 var currentTime = this.controls.currentTime = document.createElement('div');
328 currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display');
329 currentTime.setAttribute('aria-label', this.UIString('Elapsed'));
330 currentTime.setAttribute('role', 'timer');
332 var timeline = this.controls.timeline = document.createElement('input');
333 timeline.setAttribute('pseudo', '-webkit-media-controls-timeline');
334 timeline.setAttribute('aria-label', this.UIString('Duration'));
335 timeline.type = 'range';
337 this.listenFor(timeline, 'input', this.handleTimelineInput);
338 this.listenFor(timeline, 'change', this.handleTimelineChange);
339 this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver);
340 this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut);
341 this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove);
342 this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown);
343 this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp);
346 var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div');
347 thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack);
349 var thumbnail = this.controls.thumbnail = document.createElement('div');
350 thumbnail.classList.add(this.ClassNames.thumbnail);
352 var thumbnailImage = this.controls.thumbnailImage = document.createElement('img');
353 thumbnailImage.classList.add(this.ClassNames.thumbnailImage);
355 var remainingTime = this.controls.remainingTime = document.createElement('div');
356 remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display');
357 remainingTime.setAttribute('aria-label', this.UIString('Remaining'));
358 remainingTime.setAttribute('role', 'timer');
360 var muteBox = this.controls.muteBox = document.createElement('div');
361 muteBox.classList.add(this.ClassNames.muteBox);
363 var muteButton = this.controls.muteButton = document.createElement('button');
364 muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button');
365 muteButton.setAttribute('aria-label', this.UIString('Mute'));
366 this.listenFor(muteButton, 'click', this.handleMuteButtonClicked);
368 var minButton = this.controls.minButton = document.createElement('button');
369 minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button');
370 minButton.setAttribute('aria-label', this.UIString('Minimum Volume'));
371 this.listenFor(minButton, 'click', this.handleMinButtonClicked);
373 var maxButton = this.controls.maxButton = document.createElement('button');
374 maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button');
375 maxButton.setAttribute('aria-label', this.UIString('Maximum Volume'));
376 this.listenFor(maxButton, 'click', this.handleMaxButtonClicked);
378 var volumeBox = this.controls.volumeBox = document.createElement('div');
379 volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container');
380 volumeBox.classList.add(this.ClassNames.volumeBox);
382 var volume = this.controls.volume = document.createElement('input');
383 volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider');
384 volume.setAttribute('aria-label', this.UIString('Volume'));
385 volume.type = 'range';
389 this.listenFor(volume, 'input', this.handleVolumeSliderInput);
391 var captionButton = this.controls.captionButton = document.createElement('button');
392 captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button');
393 captionButton.setAttribute('aria-label', this.UIString('Captions'));
394 captionButton.setAttribute('aria-haspopup', 'true');
395 this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked);
397 var fullscreenButton = this.controls.fullscreenButton = document.createElement('button');
398 fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button');
399 fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
400 this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked);
403 setControlsType: function(type)
405 if (type === this.controlsType)
407 this.controlsType = type;
409 this.reconnectControls();
412 setIsLive: function(live)
414 if (live === this.isLive)
418 this.updateStatusDisplay();
420 this.reconnectControls();
423 reconnectControls: function()
425 this.disconnectControls();
427 if (this.controlsType === Controller.InlineControls)
428 this.configureInlineControls();
429 else if (this.controlsType == Controller.FullScreenControls)
430 this.configureFullScreenControls();
432 if (this.shouldHaveControls())
436 disconnectControls: function(event)
438 for (item in this.controls) {
439 var control = this.controls[item];
440 if (control && control.parentNode)
441 control.parentNode.removeChild(control);
445 configureInlineControls: function()
448 this.controls.panel.appendChild(this.controls.rewindButton);
449 this.controls.panel.appendChild(this.controls.playButton);
450 this.controls.panel.appendChild(this.controls.statusDisplay);
452 this.controls.panel.appendChild(this.controls.timelineBox);
453 this.controls.timelineBox.appendChild(this.controls.currentTime);
454 this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
455 this.controls.thumbnailTrack.appendChild(this.controls.timeline);
456 this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
457 this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
458 this.controls.timelineBox.appendChild(this.controls.remainingTime);
460 this.controls.panel.appendChild(this.controls.muteBox);
461 this.controls.muteBox.appendChild(this.controls.volumeBox);
462 this.controls.volumeBox.appendChild(this.controls.volume);
463 this.controls.muteBox.appendChild(this.controls.muteButton);
464 this.controls.panel.appendChild(this.controls.captionButton);
466 this.controls.panel.appendChild(this.controls.fullscreenButton);
468 this.controls.panel.style.removeProperty('left');
469 this.controls.panel.style.removeProperty('top');
470 this.controls.panel.style.removeProperty('bottom');
473 configureFullScreenControls: function()
475 this.controls.panel.appendChild(this.controls.volumeBox);
476 this.controls.volumeBox.appendChild(this.controls.minButton);
477 this.controls.volumeBox.appendChild(this.controls.volume);
478 this.controls.volumeBox.appendChild(this.controls.maxButton);
479 this.controls.panel.appendChild(this.controls.seekBackButton);
480 this.controls.panel.appendChild(this.controls.playButton);
481 this.controls.panel.appendChild(this.controls.seekForwardButton);
482 this.controls.panel.appendChild(this.controls.captionButton);
484 this.controls.panel.appendChild(this.controls.fullscreenButton);
486 this.controls.panel.appendChild(this.controls.timelineBox);
487 this.controls.timelineBox.appendChild(this.controls.currentTime);
488 this.controls.timelineBox.appendChild(this.controls.thumbnailTrack);
489 this.controls.thumbnailTrack.appendChild(this.controls.timeline);
490 this.controls.thumbnailTrack.appendChild(this.controls.thumbnail);
491 this.controls.thumbnail.appendChild(this.controls.thumbnailImage);
492 this.controls.timelineBox.appendChild(this.controls.remainingTime);
494 this.controls.panel.appendChild(this.controls.statusDisplay);
497 updateControls: function()
499 if (this.isFullScreen())
500 this.setControlsType(Controller.FullScreenControls);
502 this.setControlsType(Controller.InlineControls);
504 this.setNeedsTimelineMetricsUpdate();
507 updateStatusDisplay: function(event)
509 if (this.video.error !== null)
510 this.controls.statusDisplay.innerText = this.UIString('Error');
511 else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA)
512 this.controls.statusDisplay.innerText = this.UIString('Live Broadcast');
513 else if (this.video.networkState === HTMLMediaElement.NETWORK_LOADING)
514 this.controls.statusDisplay.innerText = this.UIString('Loading');
516 this.controls.statusDisplay.innerText = '';
518 this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error);
521 handleLoadStart: function(event)
523 this.updateStatusDisplay();
524 this.updateProgress();
527 handleError: function(event)
529 this.updateStatusDisplay();
532 handleAbort: function(event)
534 this.updateStatusDisplay();
537 handleSuspend: function(event)
539 this.updateStatusDisplay();
542 handleStalled: function(event)
544 this.updateStatusDisplay();
545 this.updateProgress();
548 handleWaiting: function(event)
550 this.updateStatusDisplay();
553 handleReadyStateChange: function(event)
555 this.updateReadyState();
556 this.updateDuration();
557 this.updateCaptionButton();
558 this.updateCaptionContainer();
559 this.updateFullscreenButton();
560 this.updateProgress();
563 handleTimeUpdate: function(event)
569 handleDurationChange: function(event)
571 this.updateDuration();
572 this.updateTime(true);
573 this.updateProgress(true);
576 handlePlay: function(event)
578 this.setPlaying(true);
581 handlePause: function(event)
583 this.setPlaying(false);
586 handleProgress: function(event)
588 this.updateProgress();
591 handleVolumeChange: function(event)
596 handleTextTrackChange: function(event)
598 this.updateCaptionContainer();
601 handleTextTrackAdd: function(event)
603 var track = event.track;
605 if (this.trackHasThumbnails(track) && track.mode === 'disabled')
606 track.mode = 'hidden';
608 this.updateThumbnail();
609 this.updateCaptionButton();
610 this.updateCaptionContainer();
613 handleTextTrackRemove: function(event)
615 this.updateThumbnail();
616 this.updateCaptionButton();
617 this.updateCaptionContainer();
620 handleAudioTrackChange: function(event)
622 this.updateHasAudio();
625 handleAudioTrackAdd: function(event)
627 this.updateHasAudio();
628 this.updateCaptionButton();
631 handleAudioTrackRemove: function(event)
633 this.updateHasAudio();
634 this.updateCaptionButton();
637 isFullScreen: function()
639 return this.video.webkitDisplayingFullscreen;
642 handleFullscreenChange: function(event)
645 this.updateControls();
647 if (this.isFullScreen()) {
648 this.controls.fullscreenButton.classList.add(this.ClassNames.exit);
649 this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen'));
650 this.host.enteredFullscreen();
652 this.controls.fullscreenButton.classList.remove(this.ClassNames.exit);
653 this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen'));
654 this.host.exitedFullscreen();
658 handleWrapperMouseMove: function(event)
661 this.resetHideControlsTimer();
663 if (!this.isDragging)
665 var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y);
666 this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px';
667 this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px';
668 event.stopPropagation()
671 handleWrapperMouseOut: function(event)
674 this.clearHideControlsTimer();
677 handleWrapperMouseUp: function(event)
679 this.isDragging = false;
680 this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true);
683 handlePanelMouseDown: function(event)
685 if (event.target != this.controls.panel)
688 if (!this.isFullScreen())
691 this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true);
692 this.isDragging = true;
693 this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY);
694 this.initialOffset = new WebKitPoint(
695 parseInt(this.controls.panel.style.left) | 0,
696 parseInt(this.controls.panel.style.top) | 0
700 handlePanelTransitionEnd: function(event)
702 var opacity = window.getComputedStyle(this.controls.panel).opacity;
703 if (parseInt(opacity) > 0)
704 this.controls.panel.classList.remove(this.ClassNames.hidden);
706 this.controls.panel.classList.add(this.ClassNames.hidden);
709 handlePanelClick: function(event)
711 // Prevent clicks in the panel from playing or pausing the video in a MediaDocument.
712 event.preventDefault();
715 handlePanelDragStart: function(event)
717 // Prevent drags in the panel from triggering a drag event on the <video> element.
718 event.preventDefault();
721 handleRewindButtonClicked: function(event)
723 var newTime = Math.max(
724 this.video.currentTime - this.RewindAmount,
725 this.video.seekable.start(0));
726 this.video.currentTime = newTime;
732 return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA;
735 handlePlayButtonClicked: function(event)
744 handleTimelineInput: function(event)
746 this.video.fastSeek(this.controls.timeline.value);
749 handleTimelineChange: function(event)
751 this.video.currentTime = this.controls.timeline.value;
754 handleTimelineDown: function(event)
756 this.controls.thumbnail.classList.add(this.ClassNames.show);
759 handleTimelineUp: function(event)
761 this.controls.thumbnail.classList.remove(this.ClassNames.show);
764 handleTimelineMouseOver: function(event)
766 this.controls.thumbnail.classList.add(this.ClassNames.show);
769 handleTimelineMouseOut: function(event)
771 this.controls.thumbnail.classList.remove(this.ClassNames.show);
774 handleTimelineMouseMove: function(event)
776 if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden))
779 this.updateTimelineMetricsIfNeeded();
780 this.controls.thumbnail.classList.add(this.ClassNames.show);
781 var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY));
782 var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth;
783 percent = Math.max(Math.min(1, percent), 0);
784 this.controls.thumbnail.style.left = percent * 100 + '%';
786 var thumbnailTime = percent * this.video.duration;
787 for (var i = 0; i < this.video.textTracks.length; ++i) {
788 var track = this.video.textTracks[i];
789 if (!this.trackHasThumbnails(track))
795 for (var j = 0; j < track.cues.length; ++j) {
796 var cue = track.cues[j];
797 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) {
798 this.controls.thumbnailImage.src = cue.text;
805 handleTimelineMouseDown: function(event)
807 this.scrubbing = true;
810 handleTimelineMouseUp: function(event)
812 this.scrubbing = false;
815 handleMuteButtonClicked: function(event)
817 this.video.muted = !this.video.muted;
818 if (this.video.muted)
819 this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute'));
823 handleMinButtonClicked: function(event)
825 if (this.video.muted) {
826 this.video.muted = false;
827 this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
829 this.video.volume = 0;
833 handleMaxButtonClicked: function(event)
835 if (this.video.muted) {
836 this.video.muted = false;
837 this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
839 this.video.volume = 1;
842 handleVolumeSliderInput: function(event)
844 if (this.video.muted) {
845 this.video.muted = false;
846 this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute'));
848 this.video.volume = this.controls.volume.value;
851 handleCaptionButtonClicked: function(event)
853 if (this.captionMenu)
854 this.destroyCaptionMenu();
856 this.buildCaptionMenu();
860 updateFullscreenButton: function()
862 this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, !this.video.webkitSupportsFullscreen);
865 handleFullscreenButtonClicked: function(event)
867 if (this.isFullScreen())
868 this.video.webkitExitFullscreen();
870 this.video.webkitEnterFullscreen();
874 handleControlsChange: function()
879 if (this.shouldHaveControls())
882 this.removeControls();
891 return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2));
894 handleSeekBackMouseDown: function(event)
896 this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
898 this.video.playbackRate = this.nextRate() * -1;
899 this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay);
902 seekBackFaster: function()
904 this.video.playbackRate = this.nextRate() * -1;
907 handleSeekBackMouseUp: function(event)
909 this.video.playbackRate = this.video.defaultPlaybackRate;
910 if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
912 else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
914 if (this.seekInterval)
915 clearInterval(this.seekInterval);
918 handleSeekForwardMouseDown: function(event)
920 this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking);
922 this.video.playbackRate = this.nextRate();
923 this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay);
926 seekForwardFaster: function()
928 this.video.playbackRate = this.nextRate();
931 handleSeekForwardMouseUp: function(event)
933 this.video.playbackRate = this.video.defaultPlaybackRate;
934 if (this.actionAfterSeeking === Controller.PauseAfterSeeking)
936 else if (this.actionAfterSeeking === Controller.PlayAfterSeeking)
938 if (this.seekInterval)
939 clearInterval(this.seekInterval);
942 updateDuration: function()
944 var duration = this.video.duration;
945 this.controls.timeline.min = 0;
946 this.controls.timeline.max = duration;
948 this.setIsLive(duration === Number.POSITIVE_INFINITY);
950 this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
951 this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60);
954 progressFillStyle: function(context)
956 var height = this.timelineHeight;
957 var gradient = context.createLinearGradient(0, 0, 0, height);
958 gradient.addColorStop(0, 'rgb(2, 2, 2)');
959 gradient.addColorStop(1, 'rgb(23, 23, 23)');
963 updateProgress: function(forceUpdate)
965 if (!forceUpdate && this.controlsAreHidden())
968 this.updateTimelineMetricsIfNeeded();
970 var background = 'url(\'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="none"><linearGradient id="gradient" x2="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="rgb(2, 2, 2)"/><stop offset="1" stop-color="rgb(23, 23, 23)"/></linearGradient><g style="fill:url(#gradient)">'
972 var duration = this.video.duration;
973 var buffered = this.video.buffered;
974 for (var i = 0, end = buffered.length; i < end; ++i) {
975 var startTime = buffered.start(i);
976 var endTime = buffered.end(i);
978 var startX = startTime / duration;
979 var widthX = (endTime - startTime) / duration;
980 background += '<rect x="' + startX + '" y="0" width="' + widthX + '" height="1"/>';
983 background += '</g></svg>\')'
984 this.controls.timeline.style.backgroundImage = background;
987 formatTime: function(time)
991 var absTime = Math.abs(time);
992 var intSeconds = Math.floor(absTime % 60).toFixed(0);
993 var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0);
994 var intHours = Math.floor(absTime / (60 * 60)).toFixed(0);
995 var sign = time < 0 ? '-' : String();
998 return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2);
1000 return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2)
1003 updatePlaying: function()
1005 this.setPlaying(!this.canPlay());
1008 setPlaying: function(isPlaying)
1010 if (this.isPlaying === isPlaying)
1012 this.isPlaying = isPlaying;
1015 this.controls.panel.classList.add(this.ClassNames.paused);
1016 this.controls.playButton.classList.add(this.ClassNames.paused);
1017 this.controls.playButton.setAttribute('aria-label', this.UIString('Play'));
1019 this.controls.panel.classList.remove(this.ClassNames.paused);
1020 this.controls.playButton.classList.remove(this.ClassNames.paused);
1021 this.controls.playButton.setAttribute('aria-label', this.UIString('Pause'));
1023 this.hideControls();
1024 this.resetHideControlsTimer();
1028 showControls: function()
1030 this.controls.panel.classList.add(this.ClassNames.show);
1031 this.controls.panel.classList.remove(this.ClassNames.hidden);
1033 this.setNeedsTimelineMetricsUpdate();
1036 hideControls: function()
1038 this.controls.panel.classList.remove(this.ClassNames.show);
1041 controlsAreHidden: function()
1043 return !this.isAudio() && !this.controls.panel.classList.contains(this.ClassNames.show) || this.controls.panel.classList.contains(this.ClassNames.hidden);
1046 removeControls: function()
1048 if (this.controls.panel.parentNode)
1049 this.controls.panel.parentNode.removeChild(this.controls.panel);
1050 this.destroyCaptionMenu();
1053 addControls: function()
1055 this.base.appendChild(this.controls.panelCompositedParent);
1056 this.controls.panelCompositedParent.appendChild(this.controls.panel);
1057 this.setNeedsTimelineMetricsUpdate();
1060 updateTime: function(forceUpdate)
1062 if (!forceUpdate && this.controlsAreHidden())
1065 var currentTime = this.video.currentTime;
1066 var timeRemaining = currentTime - this.video.duration;
1067 this.controls.currentTime.innerText = this.formatTime(currentTime);
1068 this.controls.timeline.value = this.video.currentTime;
1069 this.controls.remainingTime.innerText = this.formatTime(timeRemaining);
1072 updateReadyState: function()
1074 this.updateStatusDisplay();
1077 setStatusHidden: function(hidden)
1079 if (this.statusHidden === hidden)
1082 this.statusHidden = hidden;
1085 this.controls.statusDisplay.classList.add(this.ClassNames.hidden);
1086 this.controls.currentTime.classList.remove(this.ClassNames.hidden);
1087 this.controls.timeline.classList.remove(this.ClassNames.hidden);
1088 this.controls.remainingTime.classList.remove(this.ClassNames.hidden);
1089 this.setNeedsTimelineMetricsUpdate();
1091 this.controls.statusDisplay.classList.remove(this.ClassNames.hidden);
1092 this.controls.currentTime.classList.add(this.ClassNames.hidden);
1093 this.controls.timeline.classList.add(this.ClassNames.hidden);
1094 this.controls.remainingTime.classList.add(this.ClassNames.hidden);
1098 trackHasThumbnails: function(track)
1100 return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails');
1103 updateThumbnail: function()
1105 for (var i = 0; i < this.video.textTracks.length; ++i) {
1106 var track = this.video.textTracks[i];
1107 if (this.trackHasThumbnails(track)) {
1108 this.controls.thumbnail.classList.remove(this.ClassNames.hidden);
1113 this.controls.thumbnail.classList.add(this.ClassNames.hidden);
1116 updateCaptionButton: function()
1118 if (this.video.webkitHasClosedCaptions || this.video.audioTracks.length > 1)
1119 this.controls.captionButton.classList.remove(this.ClassNames.hidden);
1121 this.controls.captionButton.classList.add(this.ClassNames.hidden);
1124 updateCaptionContainer: function()
1126 if (!this.host.textTrackContainer)
1129 var hasClosedCaptions = this.video.webkitHasClosedCaptions;
1130 var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden);
1132 if (hasClosedCaptions && hasHiddenClass)
1133 this.host.textTrackContainer.classList.remove(this.ClassNames.hidden);
1134 else if (!hasClosedCaptions && !hasHiddenClass)
1135 this.host.textTrackContainer.classList.add(this.ClassNames.hidden);
1138 this.host.updateTextTrackContainer();
1141 buildCaptionMenu: function()
1143 var audioTracks = this.host.sortedTrackListForMenu(this.video.audioTracks);
1144 var textTracks = this.host.sortedTrackListForMenu(this.video.textTracks);
1146 if ((!textTracks || !textTracks.length) && (!audioTracks || !audioTracks.length))
1149 this.captionMenu = document.createElement('div');
1150 this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container');
1151 this.base.appendChild(this.captionMenu);
1152 this.captionMenuItems = [];
1154 var offItem = this.host.captionMenuOffItem;
1155 var automaticItem = this.host.captionMenuAutomaticItem;
1156 var displayMode = this.host.captionDisplayMode;
1158 var list = document.createElement('div');
1159 this.captionMenu.appendChild(list);
1160 list.classList.add(this.ClassNames.list);
1162 if (audioTracks && audioTracks.length > 1) {
1163 var heading = document.createElement('h3');
1164 heading.id = 'webkitMediaControlsAudioTrackHeading'; // for AX menu label
1165 list.appendChild(heading);
1166 heading.innerText = this.UIString('Audio');
1168 var ul = document.createElement('ul');
1169 ul.setAttribute('role', 'menu');
1170 ul.setAttribute('aria-labelledby', 'webkitMediaControlsAudioTrackHeading');
1171 list.appendChild(ul);
1173 for (var i = 0; i < audioTracks.length; ++i) {
1174 var menuItem = document.createElement('li');
1175 menuItem.setAttribute('role', 'menuitemradio');
1176 menuItem.setAttribute('tabindex', '-1');
1177 this.captionMenuItems.push(menuItem);
1178 this.listenFor(menuItem, 'click', this.audioTrackItemSelected);
1179 this.listenFor(menuItem, 'keyup', this.handleAudioTrackItemKeyUp);
1180 ul.appendChild(menuItem);
1182 var track = audioTracks[i];
1183 menuItem.innerText = this.host.displayNameForTrack(track);
1184 menuItem.track = track;
1186 if (track.enabled) {
1187 var trackMenuItemSelected = true;
1188 menuItem.classList.add(this.ClassNames.selected);
1189 menuItem.setAttribute('tabindex', '0');
1190 menuItem.setAttribute('aria-checked', 'true');
1194 if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
1195 offMenu.classList.add(this.ClassNames.selected);
1196 menuItem.setAttribute('tabindex', '0');
1197 menuItem.setAttribute('aria-checked', 'true');
1201 if (textTracks && textTracks.length > 2) {
1202 var heading = document.createElement('h3');
1203 heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label
1204 list.appendChild(heading);
1205 heading.innerText = this.UIString('Subtitles');
1207 var ul = document.createElement('ul');
1208 ul.setAttribute('role', 'menu');
1209 ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading');
1210 list.appendChild(ul);
1212 for (var i = 0; i < textTracks.length; ++i) {
1213 var menuItem = document.createElement('li');
1214 menuItem.setAttribute('role', 'menuitemradio');
1215 menuItem.setAttribute('tabindex', '-1');
1216 this.captionMenuItems.push(menuItem);
1217 this.listenFor(menuItem, 'click', this.captionItemSelected);
1218 this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp);
1219 ul.appendChild(menuItem);
1221 var track = textTracks[i];
1222 menuItem.innerText = this.host.displayNameForTrack(track);
1223 menuItem.track = track;
1225 if (track === offItem) {
1226 var offMenu = menuItem;
1230 if (track === automaticItem) {
1231 if (displayMode === 'automatic') {
1232 menuItem.classList.add(this.ClassNames.selected);
1233 menuItem.setAttribute('tabindex', '0');
1234 menuItem.setAttribute('aria-checked', 'true');
1239 if (displayMode != 'automatic' && track.mode === 'showing') {
1240 var trackMenuItemSelected = true;
1241 menuItem.classList.add(this.ClassNames.selected);
1242 menuItem.setAttribute('tabindex', '0');
1243 menuItem.setAttribute('aria-checked', 'true');
1248 if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) {
1249 offMenu.classList.add(this.ClassNames.selected);
1250 menuItem.setAttribute('tabindex', '0');
1251 menuItem.setAttribute('aria-checked', 'true');
1255 // focus first selected menuitem
1256 for (var i = 0, c = this.captionMenuItems.length; i < c; i++) {
1257 var item = this.captionMenuItems[i];
1258 if (item.classList.contains(this.ClassNames.selected)) {
1266 captionItemSelected: function(event)
1268 this.host.setSelectedTextTrack(event.target.track);
1269 this.destroyCaptionMenu();
1272 focusSiblingCaptionItem: function(event)
1274 var currentItem = event.target;
1275 var pendingItem = false;
1276 switch(event.keyCode) {
1277 case this.KeyCodes.left:
1278 case this.KeyCodes.up:
1279 pendingItem = currentItem.previousSibling;
1281 case this.KeyCodes.right:
1282 case this.KeyCodes.down:
1283 pendingItem = currentItem.nextSibling;
1287 currentItem.setAttribute('tabindex', '-1');
1288 pendingItem.setAttribute('tabindex', '0');
1289 pendingItem.focus();
1293 handleCaptionItemKeyUp: function(event)
1295 switch (event.keyCode) {
1296 case this.KeyCodes.enter:
1297 case this.KeyCodes.space:
1298 this.captionItemSelected(event);
1300 case this.KeyCodes.escape:
1301 this.destroyCaptionMenu();
1303 case this.KeyCodes.left:
1304 case this.KeyCodes.up:
1305 case this.KeyCodes.right:
1306 case this.KeyCodes.down:
1307 this.focusSiblingCaptionItem(event);
1313 event.stopPropagation();
1314 event.preventDefault();
1317 audioTrackItemSelected: function(event)
1319 for (var i = 0; i < this.video.audioTracks.length; ++i) {
1320 var track = this.video.audioTracks[i];
1321 track.enabled = (track == event.target.track);
1324 this.destroyCaptionMenu();
1327 focusSiblingAudioTrackItem: function(event)
1329 var currentItem = event.target;
1330 var pendingItem = false;
1331 switch(event.keyCode) {
1332 case this.KeyCodes.left:
1333 case this.KeyCodes.up:
1334 pendingItem = currentItem.previousSibling;
1336 case this.KeyCodes.right:
1337 case this.KeyCodes.down:
1338 pendingItem = currentItem.nextSibling;
1342 currentItem.setAttribute('tabindex', '-1');
1343 pendingItem.setAttribute('tabindex', '0');
1344 pendingItem.focus();
1348 handleAudioTrackItemKeyUp: function(event)
1350 switch (event.keyCode) {
1351 case this.KeyCodes.enter:
1352 case this.KeyCodes.space:
1353 this.audioTrackItemSelected(event);
1355 case this.KeyCodes.escape:
1356 this.destroyCaptionMenu();
1358 case this.KeyCodes.left:
1359 case this.KeyCodes.up:
1360 case this.KeyCodes.right:
1361 case this.KeyCodes.down:
1362 this.focusSiblingAudioTrackItem(event);
1368 event.stopPropagation();
1369 event.preventDefault();
1372 destroyCaptionMenu: function()
1374 if (!this.captionMenu)
1377 this.captionMenuItems.forEach(function(item){
1378 this.stopListeningFor(item, 'click', this.captionItemSelected);
1379 this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp);
1382 // FKA and AX: focus the trigger before destroying the element with focus
1383 if (this.controls.captionButton)
1384 this.controls.captionButton.focus();
1386 if (this.captionMenu.parentNode)
1387 this.captionMenu.parentNode.removeChild(this.captionMenu);
1388 delete this.captionMenu;
1389 delete this.captionMenuItems;
1392 updateHasAudio: function()
1394 if (this.video.audioTracks.length)
1395 this.controls.muteBox.classList.remove(this.ClassNames.hidden);
1397 this.controls.muteBox.classList.add(this.ClassNames.hidden);
1400 updateHasVideo: function()
1402 if (this.video.videoTracks.length)
1403 this.controls.panel.classList.remove(this.ClassNames.noVideo);
1405 this.controls.panel.classList.add(this.ClassNames.noVideo);
1408 updateVolume: function()
1410 if (this.video.muted || !this.video.volume) {
1411 this.controls.muteButton.classList.add(this.ClassNames.muted);
1412 this.controls.volume.value = 0;
1414 this.controls.muteButton.classList.remove(this.ClassNames.muted);
1415 this.controls.volume.value = this.video.volume;
1421 return this.video instanceof HTMLAudioElement;
1424 clearHideControlsTimer: function()
1427 clearTimeout(this.hideTimer);
1428 this.hideTimer = null;
1431 resetHideControlsTimer: function()
1434 clearTimeout(this.hideTimer);
1435 this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay);