1function createControls(root, video, host) 2{ 3 return new Controller(root, video, host); 4}; 5 6function Controller(root, video, host) 7{ 8 this.video = video; 9 this.root = root; 10 this.host = host; 11 this.controls = {}; 12 this.listeners = {}; 13 this.isLive = false; 14 this.statusHidden = true; 15 16 this.addVideoListeners(); 17 this.createBase(); 18 this.createControls(); 19 this.updateBase(); 20 this.updateControls(); 21 this.updateDuration(); 22 this.updateProgress(); 23 this.updateTime(); 24 this.updateReadyState(); 25 this.updatePlaying(); 26 this.updateThumbnail(); 27 this.updateCaptionButton(); 28 this.updateCaptionContainer(); 29 this.updateFullscreenButton(); 30 this.updateVolume(); 31 this.updateHasAudio(); 32 this.updateHasVideo(); 33}; 34 35/* Enums */ 36Controller.InlineControls = 0; 37Controller.FullScreenControls = 1; 38 39Controller.PlayAfterSeeking = 0; 40Controller.PauseAfterSeeking = 1; 41 42/* Globals */ 43Controller.gLastTimelineId = 0; 44 45Controller.prototype = { 46 47 /* Constants */ 48 HandledVideoEvents: { 49 loadstart: 'handleLoadStart', 50 error: 'handleError', 51 abort: 'handleAbort', 52 suspend: 'handleSuspend', 53 stalled: 'handleStalled', 54 waiting: 'handleWaiting', 55 emptied: 'handleReadyStateChange', 56 loadedmetadata: 'handleReadyStateChange', 57 loadeddata: 'handleReadyStateChange', 58 canplay: 'handleReadyStateChange', 59 canplaythrough: 'handleReadyStateChange', 60 timeupdate: 'handleTimeUpdate', 61 durationchange: 'handleDurationChange', 62 playing: 'handlePlay', 63 pause: 'handlePause', 64 progress: 'handleProgress', 65 volumechange: 'handleVolumeChange', 66 webkitfullscreenchange: 'handleFullscreenChange', 67 webkitbeginfullscreen: 'handleFullscreenChange', 68 webkitendfullscreen: 'handleFullscreenChange', 69 }, 70 HideControlsDelay: 4 * 1000, 71 RewindAmount: 30, 72 MaximumSeekRate: 8, 73 SeekDelay: 1500, 74 ClassNames: { 75 active: 'active', 76 exit: 'exit', 77 failed: 'failed', 78 hidden: 'hidden', 79 hiding: 'hiding', 80 hourLongTime: 'hour-long-time', 81 list: 'list', 82 muteBox: 'mute-box', 83 muted: 'muted', 84 paused: 'paused', 85 playing: 'playing', 86 selected: 'selected', 87 show: 'show', 88 thumbnail: 'thumbnail', 89 thumbnailImage: 'thumbnail-image', 90 thumbnailTrack: 'thumbnail-track', 91 volumeBox: 'volume-box', 92 noVideo: 'no-video', 93 down: 'down', 94 out: 'out', 95 }, 96 KeyCodes: { 97 enter: 13, 98 escape: 27, 99 space: 32, 100 pageUp: 33, 101 pageDown: 34, 102 end: 35, 103 home: 36, 104 left: 37, 105 up: 38, 106 right: 39, 107 down: 40 108 }, 109 110 extend: function(child) 111 { 112 for (var property in this) { 113 if (!child.hasOwnProperty(property)) 114 child[property] = this[property]; 115 } 116 }, 117 118 UIString: function(developmentString, replaceString, replacementString) 119 { 120 var localized = UIStringTable[developmentString]; 121 if (replaceString && replacementString) 122 return localized.replace(replaceString, replacementString); 123 124 if (localized) 125 return localized; 126 127 console.error("Localization for string \"" + developmentString + "\" not found."); 128 return "LOCALIZED STRING NOT FOUND"; 129 }, 130 131 listenFor: function(element, eventName, handler, useCapture) 132 { 133 if (typeof useCapture === 'undefined') 134 useCapture = false; 135 136 if (!(this.listeners[eventName] instanceof Array)) 137 this.listeners[eventName] = []; 138 this.listeners[eventName].push({element:element, handler:handler, useCapture:useCapture}); 139 element.addEventListener(eventName, this, useCapture); 140 }, 141 142 stopListeningFor: function(element, eventName, handler, useCapture) 143 { 144 if (typeof useCapture === 'undefined') 145 useCapture = false; 146 147 if (!(this.listeners[eventName] instanceof Array)) 148 return; 149 150 this.listeners[eventName] = this.listeners[eventName].filter(function(entry) { 151 return !(entry.element === element && entry.handler === handler && entry.useCapture === useCapture); 152 }); 153 element.removeEventListener(eventName, this, useCapture); 154 }, 155 156 addVideoListeners: function() 157 { 158 for (name in this.HandledVideoEvents) { 159 this.listenFor(this.video, name, this.HandledVideoEvents[name]); 160 }; 161 162 /* text tracks */ 163 this.listenFor(this.video.textTracks, 'change', this.handleTextTrackChange); 164 this.listenFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd); 165 this.listenFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove); 166 167 /* audio tracks */ 168 this.listenFor(this.video.audioTracks, 'change', this.updateHasAudio); 169 this.listenFor(this.video.audioTracks, 'addtrack', this.updateHasAudio); 170 this.listenFor(this.video.audioTracks, 'removetrack', this.updateHasAudio); 171 172 /* video tracks */ 173 this.listenFor(this.video.videoTracks, 'change', this.updateHasVideo); 174 this.listenFor(this.video.videoTracks, 'addtrack', this.updateHasVideo); 175 this.listenFor(this.video.videoTracks, 'removetrack', this.updateHasVideo); 176 177 /* controls attribute */ 178 this.controlsObserver = new MutationObserver(this.handleControlsChange.bind(this)); 179 this.controlsObserver.observe(this.video, { attributes: true, attributeFilter: ['controls'] }); 180 }, 181 182 removeVideoListeners: function() 183 { 184 for (name in this.HandledVideoEvents) { 185 this.stopListeningFor(this.video, name, this.HandledVideoEvents[name]); 186 }; 187 188 /* text tracks */ 189 this.stopListeningFor(this.video.textTracks, 'change', this.handleTextTrackChange); 190 this.stopListeningFor(this.video.textTracks, 'addtrack', this.handleTextTrackAdd); 191 this.stopListeningFor(this.video.textTracks, 'removetrack', this.handleTextTrackRemove); 192 193 /* audio tracks */ 194 this.stopListeningFor(this.video.audioTracks, 'change', this.updateHasAudio); 195 this.stopListeningFor(this.video.audioTracks, 'addtrack', this.updateHasAudio); 196 this.stopListeningFor(this.video.audioTracks, 'removetrack', this.updateHasAudio); 197 198 /* video tracks */ 199 this.stopListeningFor(this.video.videoTracks, 'change', this.updateHasVideo); 200 this.stopListeningFor(this.video.videoTracks, 'addtrack', this.updateHasVideo); 201 this.stopListeningFor(this.video.videoTracks, 'removetrack', this.updateHasVideo); 202 203 /* controls attribute */ 204 this.controlsObserver.disconnect(); 205 delete(this.controlsObserver); 206 }, 207 208 handleEvent: function(event) 209 { 210 var preventDefault = false; 211 212 try { 213 if (event.target === this.video) { 214 var handlerName = this.HandledVideoEvents[event.type]; 215 var handler = this[handlerName]; 216 if (handler && handler instanceof Function) 217 handler.call(this, event); 218 } 219 220 if (!(this.listeners[event.type] instanceof Array)) 221 return; 222 223 this.listeners[event.type].forEach(function(entry) { 224 if (entry.element === event.currentTarget && entry.handler instanceof Function) 225 preventDefault |= entry.handler.call(this, event); 226 }, this); 227 } catch(e) { 228 if (window.console) 229 console.error(e); 230 } 231 232 if (preventDefault) { 233 event.stopPropagation(); 234 event.preventDefault(); 235 } 236 }, 237 238 createBase: function() 239 { 240 var base = this.base = document.createElement('div'); 241 base.setAttribute('pseudo', '-webkit-media-controls'); 242 this.listenFor(base, 'mousemove', this.handleWrapperMouseMove); 243 this.listenFor(base, 'mouseout', this.handleWrapperMouseOut); 244 if (this.host.textTrackContainer) 245 base.appendChild(this.host.textTrackContainer); 246 }, 247 248 shouldHaveAnyUI: function() 249 { 250 return this.shouldHaveControls() || (this.video.textTracks && this.video.textTracks.length); 251 }, 252 253 shouldHaveControls: function() 254 { 255 return this.video.controls || this.isFullScreen(); 256 }, 257 258 setNeedsTimelineMetricsUpdate: function() 259 { 260 this.timelineMetricsNeedsUpdate = true; 261 }, 262 263 updateTimelineMetricsIfNeeded: function() 264 { 265 if (this.timelineMetricsNeedsUpdate) { 266 this.timelineLeft = this.controls.timeline.offsetLeft; 267 this.timelineWidth = this.controls.timeline.offsetWidth; 268 this.timelineHeight = this.controls.timeline.offsetHeight; 269 this.timelineMetricsNeedsUpdate = false; 270 } 271 }, 272 273 updateBase: function() 274 { 275 if (this.shouldHaveAnyUI()) { 276 if (!this.base.parentNode) { 277 this.root.appendChild(this.base); 278 } 279 } else { 280 if (this.base.parentNode) { 281 this.base.parentNode.removeChild(this.base); 282 } 283 } 284 }, 285 286 createControls: function() 287 { 288 var panelCompositedParent = this.controls.panelCompositedParent = document.createElement('div'); 289 panelCompositedParent.setAttribute('pseudo', '-webkit-media-controls-panel-composited-parent'); 290 291 var panel = this.controls.panel = document.createElement('div'); 292 panel.setAttribute('pseudo', '-webkit-media-controls-panel'); 293 panel.setAttribute('aria-label', (this.isAudio() ? this.UIString('Audio Playback') : this.UIString('Video Playback'))); 294 panel.setAttribute('role', 'toolbar'); 295 this.listenFor(panel, 'mousedown', this.handlePanelMouseDown); 296 this.listenFor(panel, 'transitionend', this.handlePanelTransitionEnd); 297 this.listenFor(panel, 'click', this.handlePanelClick); 298 this.listenFor(panel, 'dblclick', this.handlePanelClick); 299 this.listenFor(panel, 'dragstart', this.handlePanelDragStart); 300 301 var rewindButton = this.controls.rewindButton = document.createElement('button'); 302 rewindButton.setAttribute('pseudo', '-webkit-media-controls-rewind-button'); 303 rewindButton.setAttribute('aria-label', this.UIString('Rewind ##sec## Seconds', '##sec##', this.RewindAmount)); 304 this.listenFor(rewindButton, 'click', this.handleRewindButtonClicked); 305 306 var seekBackButton = this.controls.seekBackButton = document.createElement('button'); 307 seekBackButton.setAttribute('pseudo', '-webkit-media-controls-seek-back-button'); 308 seekBackButton.setAttribute('aria-label', this.UIString('Rewind')); 309 this.listenFor(seekBackButton, 'mousedown', this.handleSeekBackMouseDown); 310 this.listenFor(seekBackButton, 'mouseup', this.handleSeekBackMouseUp); 311 312 var seekForwardButton = this.controls.seekForwardButton = document.createElement('button'); 313 seekForwardButton.setAttribute('pseudo', '-webkit-media-controls-seek-forward-button'); 314 seekForwardButton.setAttribute('aria-label', this.UIString('Fast Forward')); 315 this.listenFor(seekForwardButton, 'mousedown', this.handleSeekForwardMouseDown); 316 this.listenFor(seekForwardButton, 'mouseup', this.handleSeekForwardMouseUp); 317 318 var playButton = this.controls.playButton = document.createElement('button'); 319 playButton.setAttribute('pseudo', '-webkit-media-controls-play-button'); 320 playButton.setAttribute('aria-label', this.UIString('Play')); 321 this.listenFor(playButton, 'click', this.handlePlayButtonClicked); 322 323 var statusDisplay = this.controls.statusDisplay = document.createElement('div'); 324 statusDisplay.setAttribute('pseudo', '-webkit-media-controls-status-display'); 325 statusDisplay.classList.add(this.ClassNames.hidden); 326 327 var timelineBox = this.controls.timelineBox = document.createElement('div'); 328 timelineBox.setAttribute('pseudo', '-webkit-media-controls-timeline-container'); 329 330 var currentTime = this.controls.currentTime = document.createElement('div'); 331 currentTime.setAttribute('pseudo', '-webkit-media-controls-current-time-display'); 332 currentTime.setAttribute('aria-label', this.UIString('Elapsed')); 333 currentTime.setAttribute('role', 'timer'); 334 335 var timeline = this.controls.timeline = document.createElement('input'); 336 this.timelineID = ++Controller.gLastTimelineId; 337 timeline.setAttribute('pseudo', '-webkit-media-controls-timeline'); 338 timeline.setAttribute('aria-label', this.UIString('Duration')); 339 timeline.style.backgroundImage = '-webkit-canvas(timeline-' + this.timelineID + ')'; 340 timeline.type = 'range'; 341 this.listenFor(timeline, 'input', this.handleTimelineChange); 342 this.listenFor(timeline, 'mouseover', this.handleTimelineMouseOver); 343 this.listenFor(timeline, 'mouseout', this.handleTimelineMouseOut); 344 this.listenFor(timeline, 'mousemove', this.handleTimelineMouseMove); 345 this.listenFor(timeline, 'mousedown', this.handleTimelineMouseDown); 346 this.listenFor(timeline, 'mouseup', this.handleTimelineMouseUp); 347 timeline.step = .01; 348 349 var thumbnailTrack = this.controls.thumbnailTrack = document.createElement('div'); 350 thumbnailTrack.classList.add(this.ClassNames.thumbnailTrack); 351 352 var thumbnail = this.controls.thumbnail = document.createElement('div'); 353 thumbnail.classList.add(this.ClassNames.thumbnail); 354 355 var thumbnailImage = this.controls.thumbnailImage = document.createElement('img'); 356 thumbnailImage.classList.add(this.ClassNames.thumbnailImage); 357 358 var remainingTime = this.controls.remainingTime = document.createElement('div'); 359 remainingTime.setAttribute('pseudo', '-webkit-media-controls-time-remaining-display'); 360 remainingTime.setAttribute('aria-label', this.UIString('Remaining')); 361 remainingTime.setAttribute('role', 'timer'); 362 363 var muteBox = this.controls.muteBox = document.createElement('div'); 364 muteBox.classList.add(this.ClassNames.muteBox); 365 366 var muteButton = this.controls.muteButton = document.createElement('button'); 367 muteButton.setAttribute('pseudo', '-webkit-media-controls-mute-button'); 368 muteButton.setAttribute('aria-label', this.UIString('Mute')); 369 this.listenFor(muteButton, 'click', this.handleMuteButtonClicked); 370 371 var minButton = this.controls.minButton = document.createElement('button'); 372 minButton.setAttribute('pseudo', '-webkit-media-controls-volume-min-button'); 373 minButton.setAttribute('aria-label', this.UIString('Minimum Volume')); 374 this.listenFor(minButton, 'click', this.handleMinButtonClicked); 375 376 var maxButton = this.controls.maxButton = document.createElement('button'); 377 maxButton.setAttribute('pseudo', '-webkit-media-controls-volume-max-button'); 378 maxButton.setAttribute('aria-label', this.UIString('Maximum Volume')); 379 this.listenFor(maxButton, 'click', this.handleMaxButtonClicked); 380 381 var volumeBox = this.controls.volumeBox = document.createElement('div'); 382 volumeBox.setAttribute('pseudo', '-webkit-media-controls-volume-slider-container'); 383 volumeBox.classList.add(this.ClassNames.volumeBox); 384 385 var volume = this.controls.volume = document.createElement('input'); 386 volume.setAttribute('pseudo', '-webkit-media-controls-volume-slider'); 387 volume.setAttribute('aria-label', this.UIString('Volume')); 388 volume.type = 'range'; 389 volume.min = 0; 390 volume.max = 1; 391 volume.step = .01; 392 this.listenFor(volume, 'change', this.handleVolumeSliderChange); 393 394 var captionButton = this.controls.captionButton = document.createElement('button'); 395 captionButton.setAttribute('pseudo', '-webkit-media-controls-toggle-closed-captions-button'); 396 captionButton.setAttribute('aria-label', this.UIString('Captions')); 397 captionButton.setAttribute('aria-haspopup', 'true'); 398 this.listenFor(captionButton, 'click', this.handleCaptionButtonClicked); 399 400 var fullscreenButton = this.controls.fullscreenButton = document.createElement('button'); 401 fullscreenButton.setAttribute('pseudo', '-webkit-media-controls-fullscreen-button'); 402 fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen')); 403 this.listenFor(fullscreenButton, 'click', this.handleFullscreenButtonClicked); 404 }, 405 406 setControlsType: function(type) 407 { 408 if (type === this.controlsType) 409 return; 410 this.controlsType = type; 411 412 this.reconnectControls(); 413 }, 414 415 setIsLive: function(live) 416 { 417 if (live === this.isLive) 418 return; 419 this.isLive = live; 420 421 this.updateStatusDisplay(); 422 423 this.reconnectControls(); 424 }, 425 426 reconnectControls: function() 427 { 428 this.disconnectControls(); 429 430 if (this.controlsType === Controller.InlineControls) 431 this.configureInlineControls(); 432 else if (this.controlsType == Controller.FullScreenControls) 433 this.configureFullScreenControls(); 434 435 if (this.shouldHaveControls()) 436 this.addControls(); 437 }, 438 439 disconnectControls: function(event) 440 { 441 for (item in this.controls) { 442 var control = this.controls[item]; 443 if (control && control.parentNode) 444 control.parentNode.removeChild(control); 445 } 446 }, 447 448 configureInlineControls: function() 449 { 450 if (!this.isLive) 451 this.controls.panel.appendChild(this.controls.rewindButton); 452 this.controls.panel.appendChild(this.controls.playButton); 453 this.controls.panel.appendChild(this.controls.statusDisplay); 454 if (!this.isLive) { 455 this.controls.panel.appendChild(this.controls.timelineBox); 456 this.controls.timelineBox.appendChild(this.controls.currentTime); 457 this.controls.timelineBox.appendChild(this.controls.thumbnailTrack); 458 this.controls.thumbnailTrack.appendChild(this.controls.timeline); 459 this.controls.thumbnailTrack.appendChild(this.controls.thumbnail); 460 this.controls.thumbnail.appendChild(this.controls.thumbnailImage); 461 this.controls.timelineBox.appendChild(this.controls.remainingTime); 462 } 463 this.controls.panel.appendChild(this.controls.muteBox); 464 this.controls.muteBox.appendChild(this.controls.volumeBox); 465 this.controls.volumeBox.appendChild(this.controls.volume); 466 this.controls.muteBox.appendChild(this.controls.muteButton); 467 this.controls.panel.appendChild(this.controls.captionButton); 468 if (!this.isAudio()) 469 this.controls.panel.appendChild(this.controls.fullscreenButton); 470 471 this.controls.panel.style.removeProperty('left'); 472 this.controls.panel.style.removeProperty('top'); 473 this.controls.panel.style.removeProperty('bottom'); 474 }, 475 476 configureFullScreenControls: function() 477 { 478 this.controls.panel.appendChild(this.controls.volumeBox); 479 this.controls.volumeBox.appendChild(this.controls.minButton); 480 this.controls.volumeBox.appendChild(this.controls.volume); 481 this.controls.volumeBox.appendChild(this.controls.maxButton); 482 this.controls.panel.appendChild(this.controls.seekBackButton); 483 this.controls.panel.appendChild(this.controls.playButton); 484 this.controls.panel.appendChild(this.controls.seekForwardButton); 485 this.controls.panel.appendChild(this.controls.captionButton); 486 if (!this.isAudio()) 487 this.controls.panel.appendChild(this.controls.fullscreenButton); 488 if (!this.isLive) { 489 this.controls.panel.appendChild(this.controls.timelineBox); 490 this.controls.timelineBox.appendChild(this.controls.currentTime); 491 this.controls.timelineBox.appendChild(this.controls.thumbnailTrack); 492 this.controls.thumbnailTrack.appendChild(this.controls.timeline); 493 this.controls.thumbnailTrack.appendChild(this.controls.thumbnail); 494 this.controls.thumbnail.appendChild(this.controls.thumbnailImage); 495 this.controls.timelineBox.appendChild(this.controls.remainingTime); 496 } else 497 this.controls.panel.appendChild(this.controls.statusDisplay); 498 }, 499 500 updateControls: function() 501 { 502 if (this.isFullScreen()) 503 this.setControlsType(Controller.FullScreenControls); 504 else 505 this.setControlsType(Controller.InlineControls); 506 507 this.setNeedsTimelineMetricsUpdate(); 508 }, 509 510 updateStatusDisplay: function(event) 511 { 512 if (this.video.error !== null) 513 this.controls.statusDisplay.innerText = this.UIString('Error'); 514 else if (this.isLive && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) 515 this.controls.statusDisplay.innerText = this.UIString('Live Broadcast'); 516 else if (this.video.networkState === HTMLMediaElement.NETWORK_LOADING) 517 this.controls.statusDisplay.innerText = this.UIString('Loading'); 518 else 519 this.controls.statusDisplay.innerText = ''; 520 521 this.setStatusHidden(!this.isLive && this.video.readyState > HTMLMediaElement.HAVE_NOTHING && !this.video.error); 522 }, 523 524 handleLoadStart: function(event) 525 { 526 this.updateStatusDisplay(); 527 this.updateProgress(); 528 }, 529 530 handleError: function(event) 531 { 532 this.updateStatusDisplay(); 533 }, 534 535 handleAbort: function(event) 536 { 537 this.updateStatusDisplay(); 538 }, 539 540 handleSuspend: function(event) 541 { 542 this.updateStatusDisplay(); 543 }, 544 545 handleStalled: function(event) 546 { 547 this.updateStatusDisplay(); 548 this.updateProgress(); 549 }, 550 551 handleWaiting: function(event) 552 { 553 this.updateStatusDisplay(); 554 }, 555 556 handleReadyStateChange: function(event) 557 { 558 this.updateReadyState(); 559 this.updateDuration(); 560 this.updateCaptionButton(); 561 this.updateCaptionContainer(); 562 this.updateFullscreenButton(); 563 this.updateProgress(); 564 }, 565 566 handleTimeUpdate: function(event) 567 { 568 if (!this.scrubbing) 569 this.updateTime(); 570 }, 571 572 handleDurationChange: function(event) 573 { 574 this.updateDuration(); 575 this.updateTime(); 576 this.updateProgress(); 577 }, 578 579 handlePlay: function(event) 580 { 581 this.setPlaying(true); 582 }, 583 584 handlePause: function(event) 585 { 586 this.setPlaying(false); 587 }, 588 589 handleProgress: function(event) 590 { 591 this.updateProgress(); 592 }, 593 594 handleVolumeChange: function(event) 595 { 596 this.updateVolume(); 597 }, 598 599 handleTextTrackChange: function(event) 600 { 601 this.updateCaptionContainer(); 602 }, 603 604 handleTextTrackAdd: function(event) 605 { 606 var track = event.track; 607 608 if (this.trackHasThumbnails(track) && track.mode === 'disabled') 609 track.mode = 'hidden'; 610 611 this.updateThumbnail(); 612 this.updateCaptionButton(); 613 this.updateCaptionContainer(); 614 }, 615 616 handleTextTrackRemove: function(event) 617 { 618 this.updateThumbnail(); 619 this.updateCaptionButton(); 620 this.updateCaptionContainer(); 621 }, 622 623 isFullScreen: function() 624 { 625 return this.video.webkitDisplayingFullscreen; 626 }, 627 628 handleFullscreenChange: function(event) 629 { 630 this.updateBase(); 631 this.updateControls(); 632 633 if (this.isFullScreen()) { 634 this.controls.fullscreenButton.classList.add(this.ClassNames.exit); 635 this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Exit Full Screen')); 636 this.host.enteredFullscreen(); 637 } else { 638 this.controls.fullscreenButton.classList.remove(this.ClassNames.exit); 639 this.controls.fullscreenButton.setAttribute('aria-label', this.UIString('Display Full Screen')); 640 this.host.exitedFullscreen(); 641 } 642 }, 643 644 handleWrapperMouseMove: function(event) 645 { 646 this.showControls(); 647 this.resetHideControlsTimer(); 648 649 if (!this.isDragging) 650 return; 651 var delta = new WebKitPoint(event.clientX - this.initialDragLocation.x, event.clientY - this.initialDragLocation.y); 652 this.controls.panel.style.left = this.initialOffset.x + delta.x + 'px'; 653 this.controls.panel.style.top = this.initialOffset.y + delta.y + 'px'; 654 event.stopPropagation() 655 }, 656 657 handleWrapperMouseOut: function(event) 658 { 659 this.hideControls(); 660 this.clearHideControlsTimer(); 661 }, 662 663 handleWrapperMouseUp: function(event) 664 { 665 this.isDragging = false; 666 this.stopListeningFor(this.base, 'mouseup', 'handleWrapperMouseUp', true); 667 }, 668 669 handlePanelMouseDown: function(event) 670 { 671 if (event.target != this.controls.panel) 672 return; 673 674 if (!this.isFullScreen()) 675 return; 676 677 this.listenFor(this.base, 'mouseup', this.handleWrapperMouseUp, true); 678 this.isDragging = true; 679 this.initialDragLocation = new WebKitPoint(event.clientX, event.clientY); 680 this.initialOffset = new WebKitPoint( 681 parseInt(this.controls.panel.style.left) | 0, 682 parseInt(this.controls.panel.style.top) | 0 683 ); 684 }, 685 686 handlePanelTransitionEnd: function(event) 687 { 688 var opacity = window.getComputedStyle(this.controls.panel).opacity; 689 if (parseInt(opacity) > 0) 690 this.controls.panel.classList.remove(this.ClassNames.hidden); 691 else 692 this.controls.panel.classList.add(this.ClassNames.hidden); 693 }, 694 695 handlePanelClick: function(event) 696 { 697 // Prevent clicks in the panel from playing or pausing the video in a MediaDocument. 698 event.preventDefault(); 699 }, 700 701 handlePanelDragStart: function(event) 702 { 703 // Prevent drags in the panel from triggering a drag event on the <video> element. 704 event.preventDefault(); 705 }, 706 707 handleRewindButtonClicked: function(event) 708 { 709 var newTime = Math.max( 710 this.video.currentTime - this.RewindAmount, 711 this.video.seekable.start(0)); 712 this.video.currentTime = newTime; 713 return true; 714 }, 715 716 canPlay: function() 717 { 718 return this.video.paused || this.video.ended || this.video.readyState < HTMLMediaElement.HAVE_METADATA; 719 }, 720 721 handlePlayButtonClicked: function(event) 722 { 723 if (this.canPlay()) 724 this.video.play(); 725 else 726 this.video.pause(); 727 return true; 728 }, 729 730 handleTimelineChange: function(event) 731 { 732 this.video.fastSeek(this.controls.timeline.value); 733 }, 734 735 handleTimelineDown: function(event) 736 { 737 this.controls.thumbnail.classList.add(this.ClassNames.show); 738 }, 739 740 handleTimelineUp: function(event) 741 { 742 this.controls.thumbnail.classList.remove(this.ClassNames.show); 743 }, 744 745 handleTimelineMouseOver: function(event) 746 { 747 this.controls.thumbnail.classList.add(this.ClassNames.show); 748 }, 749 750 handleTimelineMouseOut: function(event) 751 { 752 this.controls.thumbnail.classList.remove(this.ClassNames.show); 753 }, 754 755 handleTimelineMouseMove: function(event) 756 { 757 if (this.controls.thumbnail.classList.contains(this.ClassNames.hidden)) 758 return; 759 760 this.updateTimelineMetricsIfNeeded(); 761 this.controls.thumbnail.classList.add(this.ClassNames.show); 762 var localPoint = webkitConvertPointFromPageToNode(this.controls.timeline, new WebKitPoint(event.clientX, event.clientY)); 763 var percent = (localPoint.x - this.timelineLeft) / this.timelineWidth; 764 percent = Math.max(Math.min(1, percent), 0); 765 this.controls.thumbnail.style.left = percent * 100 + '%'; 766 767 var thumbnailTime = percent * this.video.duration; 768 for (var i = 0; i < this.video.textTracks.length; ++i) { 769 var track = this.video.textTracks[i]; 770 if (!this.trackHasThumbnails(track)) 771 continue; 772 773 if (!track.cues) 774 continue; 775 776 for (var j = 0; j < track.cues.length; ++j) { 777 var cue = track.cues[j]; 778 if (thumbnailTime >= cue.startTime && thumbnailTime < cue.endTime) { 779 this.controls.thumbnailImage.src = cue.text; 780 return; 781 } 782 } 783 } 784 }, 785 786 handleTimelineMouseDown: function(event) 787 { 788 this.scrubbing = true; 789 }, 790 791 handleTimelineMouseUp: function(event) 792 { 793 this.scrubbing = false; 794 795 // Do a precise seek when we lift the mouse: 796 this.video.currentTime = this.controls.timeline.value; 797 }, 798 799 handleMuteButtonClicked: function(event) 800 { 801 this.video.muted = !this.video.muted; 802 if (this.video.muted) 803 this.controls.muteButton.setAttribute('aria-label', this.UIString('Unmute')); 804 return true; 805 }, 806 807 handleMinButtonClicked: function(event) 808 { 809 if (this.video.muted) { 810 this.video.muted = false; 811 this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); 812 } 813 this.video.volume = 0; 814 return true; 815 }, 816 817 handleMaxButtonClicked: function(event) 818 { 819 if (this.video.muted) { 820 this.video.muted = false; 821 this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); 822 } 823 this.video.volume = 1; 824 }, 825 826 handleVolumeSliderChange: function(event) 827 { 828 if (this.video.muted) { 829 this.video.muted = false; 830 this.controls.muteButton.setAttribute('aria-label', this.UIString('Mute')); 831 } 832 this.video.volume = this.controls.volume.value; 833 }, 834 835 handleCaptionButtonClicked: function(event) 836 { 837 if (this.captionMenu) 838 this.destroyCaptionMenu(); 839 else 840 this.buildCaptionMenu(); 841 return true; 842 }, 843 844 updateFullscreenButton: function() 845 { 846 this.controls.fullscreenButton.classList.toggle(this.ClassNames.hidden, !this.video.webkitSupportsFullscreen); 847 }, 848 849 handleFullscreenButtonClicked: function(event) 850 { 851 if (this.isFullScreen()) 852 this.video.webkitExitFullscreen(); 853 else 854 this.video.webkitEnterFullscreen(); 855 return true; 856 }, 857 858 handleControlsChange: function() 859 { 860 try { 861 this.updateBase(); 862 863 if (this.shouldHaveControls()) 864 this.addControls(); 865 else 866 this.removeControls(); 867 } catch(e) { 868 if (window.console) 869 console.error(e); 870 } 871 }, 872 873 nextRate: function() 874 { 875 return Math.min(this.MaximumSeekRate, Math.abs(this.video.playbackRate * 2)); 876 }, 877 878 handleSeekBackMouseDown: function(event) 879 { 880 this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking); 881 this.video.play(); 882 this.video.playbackRate = this.nextRate() * -1; 883 this.seekInterval = setInterval(this.seekBackFaster.bind(this), this.SeekDelay); 884 }, 885 886 seekBackFaster: function() 887 { 888 this.video.playbackRate = this.nextRate() * -1; 889 }, 890 891 handleSeekBackMouseUp: function(event) 892 { 893 this.video.playbackRate = this.video.defaultPlaybackRate; 894 if (this.actionAfterSeeking === Controller.PauseAfterSeeking) 895 this.video.pause(); 896 else if (this.actionAfterSeeking === Controller.PlayAfterSeeking) 897 this.video.play(); 898 if (this.seekInterval) 899 clearInterval(this.seekInterval); 900 }, 901 902 handleSeekForwardMouseDown: function(event) 903 { 904 this.actionAfterSeeking = (this.canPlay() ? Controller.PauseAfterSeeking : Controller.PlayAfterSeeking); 905 this.video.play(); 906 this.video.playbackRate = this.nextRate(); 907 this.seekInterval = setInterval(this.seekForwardFaster.bind(this), this.SeekDelay); 908 }, 909 910 seekForwardFaster: function() 911 { 912 this.video.playbackRate = this.nextRate(); 913 }, 914 915 handleSeekForwardMouseUp: function(event) 916 { 917 this.video.playbackRate = this.video.defaultPlaybackRate; 918 if (this.actionAfterSeeking === Controller.PauseAfterSeeking) 919 this.video.pause(); 920 else if (this.actionAfterSeeking === Controller.PlayAfterSeeking) 921 this.video.play(); 922 if (this.seekInterval) 923 clearInterval(this.seekInterval); 924 }, 925 926 updateDuration: function() 927 { 928 var duration = this.video.duration; 929 this.controls.timeline.min = 0; 930 this.controls.timeline.max = duration; 931 932 this.setIsLive(duration === Number.POSITIVE_INFINITY); 933 934 this.controls.currentTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60); 935 this.controls.remainingTime.classList.toggle(this.ClassNames.hourLongTime, duration >= 60*60); 936 }, 937 938 progressFillStyle: function(context) 939 { 940 var height = this.timelineHeight; 941 var gradient = context.createLinearGradient(0, 0, 0, height); 942 gradient.addColorStop(0, 'rgb(2, 2, 2)'); 943 gradient.addColorStop(1, 'rgb(23, 23, 23)'); 944 return gradient; 945 }, 946 947 updateProgress: function() 948 { 949 this.updateTimelineMetricsIfNeeded(); 950 951 var width = this.timelineWidth; 952 var height = this.timelineHeight; 953 954 var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height); 955 context.clearRect(0, 0, width, height); 956 957 context.fillStyle = this.progressFillStyle(context); 958 959 var duration = this.video.duration; 960 var buffered = this.video.buffered; 961 for (var i = 0, end = buffered.length; i < end; ++i) { 962 var startTime = buffered.start(i); 963 var endTime = buffered.end(i); 964 965 var startX = width * startTime / duration; 966 var endX = width * endTime / duration; 967 context.fillRect(startX, 0, endX - startX, height); 968 } 969 }, 970 971 formatTime: function(time) 972 { 973 if (isNaN(time)) 974 time = 0; 975 var absTime = Math.abs(time); 976 var intSeconds = Math.floor(absTime % 60).toFixed(0); 977 var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); 978 var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); 979 var sign = time < 0 ? '-' : String(); 980 981 if (intHours > 0) 982 return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2); 983 984 return sign + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2) 985 }, 986 987 updatePlaying: function() 988 { 989 this.setPlaying(!this.canPlay()); 990 }, 991 992 setPlaying: function(isPlaying) 993 { 994 if (this.isPlaying === isPlaying) 995 return; 996 this.isPlaying = isPlaying; 997 998 if (!isPlaying) { 999 this.controls.panel.classList.add(this.ClassNames.paused); 1000 this.controls.playButton.classList.add(this.ClassNames.paused); 1001 this.controls.playButton.setAttribute('aria-label', this.UIString('Play')); 1002 } else { 1003 this.controls.panel.classList.remove(this.ClassNames.paused); 1004 this.controls.playButton.classList.remove(this.ClassNames.paused); 1005 this.controls.playButton.setAttribute('aria-label', this.UIString('Pause')); 1006 1007 this.hideControls(); 1008 this.resetHideControlsTimer(); 1009 } 1010 }, 1011 1012 showControls: function() 1013 { 1014 this.controls.panel.classList.add(this.ClassNames.show); 1015 this.controls.panel.classList.remove(this.ClassNames.hidden); 1016 1017 this.setNeedsTimelineMetricsUpdate(); 1018 }, 1019 1020 hideControls: function() 1021 { 1022 this.controls.panel.classList.remove(this.ClassNames.show); 1023 }, 1024 1025 controlsAreHidden: function() 1026 { 1027 return !this.controls.panel.classList.contains(this.ClassNames.show) || this.controls.panel.classList.contains(this.ClassNames.hidden); 1028 }, 1029 1030 removeControls: function() 1031 { 1032 if (this.controls.panel.parentNode) 1033 this.controls.panel.parentNode.removeChild(this.controls.panel); 1034 this.destroyCaptionMenu(); 1035 }, 1036 1037 addControls: function() 1038 { 1039 this.base.appendChild(this.controls.panelCompositedParent); 1040 this.controls.panelCompositedParent.appendChild(this.controls.panel); 1041 this.setNeedsTimelineMetricsUpdate(); 1042 }, 1043 1044 updateTime: function() 1045 { 1046 var currentTime = this.video.currentTime; 1047 var timeRemaining = currentTime - this.video.duration; 1048 this.controls.currentTime.innerText = this.formatTime(currentTime); 1049 this.controls.timeline.value = this.video.currentTime; 1050 this.controls.remainingTime.innerText = this.formatTime(timeRemaining); 1051 }, 1052 1053 updateReadyState: function() 1054 { 1055 this.updateStatusDisplay(); 1056 }, 1057 1058 setStatusHidden: function(hidden) 1059 { 1060 if (this.statusHidden === hidden) 1061 return; 1062 1063 this.statusHidden = hidden; 1064 1065 if (hidden) { 1066 this.controls.statusDisplay.classList.add(this.ClassNames.hidden); 1067 this.controls.currentTime.classList.remove(this.ClassNames.hidden); 1068 this.controls.timeline.classList.remove(this.ClassNames.hidden); 1069 this.controls.remainingTime.classList.remove(this.ClassNames.hidden); 1070 this.setNeedsTimelineMetricsUpdate(); 1071 } else { 1072 this.controls.statusDisplay.classList.remove(this.ClassNames.hidden); 1073 this.controls.currentTime.classList.add(this.ClassNames.hidden); 1074 this.controls.timeline.classList.add(this.ClassNames.hidden); 1075 this.controls.remainingTime.classList.add(this.ClassNames.hidden); 1076 } 1077 }, 1078 1079 trackHasThumbnails: function(track) 1080 { 1081 return track.kind === 'thumbnails' || (track.kind === 'metadata' && track.label === 'thumbnails'); 1082 }, 1083 1084 updateThumbnail: function() 1085 { 1086 for (var i = 0; i < this.video.textTracks.length; ++i) { 1087 var track = this.video.textTracks[i]; 1088 if (this.trackHasThumbnails(track)) { 1089 this.controls.thumbnail.classList.remove(this.ClassNames.hidden); 1090 return; 1091 } 1092 } 1093 1094 this.controls.thumbnail.classList.add(this.ClassNames.hidden); 1095 }, 1096 1097 updateCaptionButton: function() 1098 { 1099 if (this.video.webkitHasClosedCaptions) 1100 this.controls.captionButton.classList.remove(this.ClassNames.hidden); 1101 else 1102 this.controls.captionButton.classList.add(this.ClassNames.hidden); 1103 }, 1104 1105 updateCaptionContainer: function() 1106 { 1107 if (!this.host.textTrackContainer) 1108 return; 1109 1110 var hasClosedCaptions = this.video.webkitHasClosedCaptions; 1111 var hasHiddenClass = this.host.textTrackContainer.classList.contains(this.ClassNames.hidden); 1112 1113 if (hasClosedCaptions && hasHiddenClass) 1114 this.host.textTrackContainer.classList.remove(this.ClassNames.hidden); 1115 else if (!hasClosedCaptions && !hasHiddenClass) 1116 this.host.textTrackContainer.classList.add(this.ClassNames.hidden); 1117 1118 this.updateBase(); 1119 this.host.updateTextTrackContainer(); 1120 }, 1121 1122 buildCaptionMenu: function() 1123 { 1124 var tracks = this.host.sortedTrackListForMenu(this.video.textTracks); 1125 if (!tracks || !tracks.length) 1126 return; 1127 1128 this.captionMenu = document.createElement('div'); 1129 this.captionMenu.setAttribute('pseudo', '-webkit-media-controls-closed-captions-container'); 1130 this.base.appendChild(this.captionMenu); 1131 this.captionMenuItems = []; 1132 1133 var offItem = this.host.captionMenuOffItem; 1134 var automaticItem = this.host.captionMenuAutomaticItem; 1135 var displayMode = this.host.captionDisplayMode; 1136 1137 var list = document.createElement('div'); 1138 this.captionMenu.appendChild(list); 1139 list.classList.add(this.ClassNames.list); 1140 1141 var heading = document.createElement('h3'); 1142 heading.id = 'webkitMediaControlsClosedCaptionsHeading'; // for AX menu label 1143 list.appendChild(heading); 1144 heading.innerText = this.UIString('Subtitles'); 1145 1146 var ul = document.createElement('ul'); 1147 ul.setAttribute('role', 'menu'); 1148 ul.setAttribute('aria-labelledby', 'webkitMediaControlsClosedCaptionsHeading'); 1149 list.appendChild(ul); 1150 1151 for (var i = 0; i < tracks.length; ++i) { 1152 var menuItem = document.createElement('li'); 1153 menuItem.setAttribute('role', 'menuitemradio'); 1154 menuItem.setAttribute('tabindex', '-1'); 1155 this.captionMenuItems.push(menuItem); 1156 this.listenFor(menuItem, 'click', this.captionItemSelected); 1157 this.listenFor(menuItem, 'keyup', this.handleCaptionItemKeyUp); 1158 ul.appendChild(menuItem); 1159 1160 var track = tracks[i]; 1161 menuItem.innerText = this.host.displayNameForTrack(track); 1162 menuItem.track = track; 1163 1164 if (track === offItem) { 1165 var offMenu = menuItem; 1166 continue; 1167 } 1168 1169 if (track === automaticItem) { 1170 if (displayMode === 'automatic') { 1171 menuItem.classList.add(this.ClassNames.selected); 1172 menuItem.setAttribute('tabindex', '0'); 1173 menuItem.setAttribute('aria-checked', 'true'); 1174 } 1175 continue; 1176 } 1177 1178 if (displayMode != 'automatic' && track.mode === 'showing') { 1179 var trackMenuItemSelected = true; 1180 menuItem.classList.add(this.ClassNames.selected); 1181 menuItem.setAttribute('tabindex', '0'); 1182 menuItem.setAttribute('aria-checked', 'true'); 1183 } 1184 1185 } 1186 1187 if (offMenu && displayMode === 'forced-only' && !trackMenuItemSelected) { 1188 offMenu.classList.add(this.ClassNames.selected); 1189 menuItem.setAttribute('tabindex', '0'); 1190 menuItem.setAttribute('aria-checked', 'true'); 1191 } 1192 1193 // focus first selected menuitem 1194 for (var i = 0, c = this.captionMenuItems.length; i < c; i++) { 1195 var item = this.captionMenuItems[i]; 1196 if (item.classList.contains(this.ClassNames.selected)) { 1197 item.focus(); 1198 break; 1199 } 1200 } 1201 1202 }, 1203 1204 captionItemSelected: function(event) 1205 { 1206 this.host.setSelectedTextTrack(event.target.track); 1207 this.destroyCaptionMenu(); 1208 }, 1209 1210 focusSiblingCaptionItem: function(event) 1211 { 1212 var currentItem = event.target; 1213 var pendingItem = false; 1214 switch(event.keyCode) { 1215 case this.KeyCodes.left: 1216 case this.KeyCodes.up: 1217 pendingItem = currentItem.previousSibling; 1218 break; 1219 case this.KeyCodes.right: 1220 case this.KeyCodes.down: 1221 pendingItem = currentItem.nextSibling; 1222 break; 1223 } 1224 if (pendingItem) { 1225 currentItem.setAttribute('tabindex', '-1'); 1226 pendingItem.setAttribute('tabindex', '0'); 1227 pendingItem.focus(); 1228 } 1229 }, 1230 1231 handleCaptionItemKeyUp: function(event) 1232 { 1233 switch (event.keyCode) { 1234 case this.KeyCodes.enter: 1235 case this.KeyCodes.space: 1236 this.captionItemSelected(event); 1237 break; 1238 case this.KeyCodes.escape: 1239 this.destroyCaptionMenu(); 1240 break; 1241 case this.KeyCodes.left: 1242 case this.KeyCodes.up: 1243 case this.KeyCodes.right: 1244 case this.KeyCodes.down: 1245 this.focusSiblingCaptionItem(event); 1246 break; 1247 default: 1248 return; 1249 } 1250 // handled 1251 event.stopPropagation(); 1252 event.preventDefault(); 1253 }, 1254 1255 destroyCaptionMenu: function() 1256 { 1257 if (!this.captionMenu) 1258 return; 1259 1260 this.captionMenuItems.forEach(function(item){ 1261 this.stopListeningFor(item, 'click', this.captionItemSelected); 1262 this.stopListeningFor(item, 'keyup', this.handleCaptionItemKeyUp); 1263 }, this); 1264 1265 // FKA and AX: focus the trigger before destroying the element with focus 1266 if (this.controls.captionButton) 1267 this.controls.captionButton.focus(); 1268 1269 if (this.captionMenu.parentNode) 1270 this.captionMenu.parentNode.removeChild(this.captionMenu); 1271 delete this.captionMenu; 1272 delete this.captionMenuItems; 1273 }, 1274 1275 updateHasAudio: function() 1276 { 1277 if (this.video.audioTracks.length) 1278 this.controls.muteBox.classList.remove(this.ClassNames.hidden); 1279 else 1280 this.controls.muteBox.classList.add(this.ClassNames.hidden); 1281 }, 1282 1283 updateHasVideo: function() 1284 { 1285 if (this.video.videoTracks.length) 1286 this.controls.panel.classList.remove(this.ClassNames.noVideo); 1287 else 1288 this.controls.panel.classList.add(this.ClassNames.noVideo); 1289 }, 1290 1291 updateVolume: function() 1292 { 1293 if (this.video.muted || !this.video.volume) { 1294 this.controls.muteButton.classList.add(this.ClassNames.muted); 1295 this.controls.volume.value = 0; 1296 } else { 1297 this.controls.muteButton.classList.remove(this.ClassNames.muted); 1298 this.controls.volume.value = this.video.volume; 1299 } 1300 }, 1301 1302 isAudio: function() 1303 { 1304 return this.video instanceof HTMLAudioElement; 1305 }, 1306 1307 clearHideControlsTimer: function() 1308 { 1309 if (this.hideTimer) 1310 clearTimeout(this.hideTimer); 1311 this.hideTimer = null; 1312 }, 1313 1314 resetHideControlsTimer: function() 1315 { 1316 if (this.hideTimer) 1317 clearTimeout(this.hideTimer); 1318 this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay); 1319 }, 1320}; 1321