1function createControls(root, video, host) 2{ 3 return new ControllerIOS(root, video, host); 4}; 5 6function ControllerIOS(root, video, host) 7{ 8 this.hasWirelessPlaybackTargets = false; 9 this._pageScaleFactor = 1; 10 Controller.call(this, root, video, host); 11 12 this.updateWirelessTargetAvailable(); 13 this.updateWirelessPlaybackStatus(); 14 this.setNeedsTimelineMetricsUpdate(); 15 16 host.controlsDependOnPageScaleFactor = true; 17}; 18 19/* Enums */ 20ControllerIOS.StartPlaybackControls = 2; 21 22/* Globals */ 23ControllerIOS.gWirelessImage = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 245"><g fill="#1060FE"><path d="M193.6,6.3v121.6H6.4V6.3H193.6 M199.1,0.7H0.9v132.7h198.2V0.7L199.1,0.7z"/><path d="M43.5,139.3c15.8,8,35.3,12.7,56.5,12.7s40.7-4.7,56.5-12.7H43.5z"/></g><g text-anchor="middle" font-family="Helvetica Neue"><text x="100" y="204" fill="white" font-size="24">##DEVICE_TYPE##</text><text x="100" y="234" fill="#5C5C5C" font-size="21">##DEVICE_NAME##</text></g></svg>'; 24ControllerIOS.gSimulateWirelessPlaybackTarget = false; // Used for testing when there are no wireless targets. 25 26ControllerIOS.prototype = { 27 addVideoListeners: function() { 28 Controller.prototype.addVideoListeners.call(this); 29 30 this.listenFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange); 31 this.listenFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange); 32 33 if (window.WebKitPlaybackTargetAvailabilityEvent) { 34 this.listenFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange); 35 this.listenFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange); 36 } 37 }, 38 39 removeVideoListeners: function() { 40 Controller.prototype.removeVideoListeners.call(this); 41 42 this.stopListeningFor(this.video, 'webkitbeginfullscreen', this.handleFullscreenChange); 43 this.stopListeningFor(this.video, 'webkitendfullscreen', this.handleFullscreenChange); 44 45 if (window.WebKitPlaybackTargetAvailabilityEvent) { 46 this.stopListeningFor(this.video, 'webkitcurrentplaybacktargetiswirelesschanged', this.handleWirelessPlaybackChange); 47 this.stopListeningFor(this.video, 'webkitplaybacktargetavailabilitychanged', this.handleWirelessTargetAvailableChange); 48 } 49 }, 50 51 createBase: function() { 52 Controller.prototype.createBase.call(this); 53 54 var startPlaybackButton = this.controls.startPlaybackButton = document.createElement('button'); 55 startPlaybackButton.setAttribute('pseudo', '-webkit-media-controls-start-playback-button'); 56 startPlaybackButton.setAttribute('aria-label', this.UIString('Start Playback')); 57 58 this.listenFor(this.base, 'gesturestart', this.handleBaseGestureStart); 59 this.listenFor(this.base, 'gesturechange', this.handleBaseGestureChange); 60 this.listenFor(this.base, 'gestureend', this.handleBaseGestureEnd); 61 this.listenFor(this.base, 'touchstart', this.handleWrapperTouchStart); 62 this.stopListeningFor(this.base, 'mousemove', this.handleWrapperMouseMove); 63 this.stopListeningFor(this.base, 'mouseout', this.handleWrapperMouseOut); 64 }, 65 66 shouldHaveStartPlaybackButton: function() { 67 var allowsInline = this.host.mediaPlaybackAllowsInline; 68 69 if (this.isAudio() && allowsInline) 70 return false; 71 72 if (this.isFullScreen()) 73 return false; 74 75 if (!this.video.currentSrc && this.video.error) 76 return false; 77 78 if (!this.video.controls && allowsInline) 79 return false; 80 81 if (this.video.currentSrc && this.video.error) 82 return true; 83 84 if (!this.host.userGestureRequired && allowsInline) 85 return false; 86 87 return true; 88 }, 89 90 shouldHaveControls: function() { 91 if (this.shouldHaveStartPlaybackButton()) 92 return false; 93 94 return Controller.prototype.shouldHaveControls.call(this); 95 }, 96 97 shouldHaveAnyUI: function() { 98 return this.shouldHaveStartPlaybackButton() || Controller.prototype.shouldHaveAnyUI.call(this) || this.currentPlaybackTargetIsWireless(); 99 }, 100 101 currentPlaybackTargetIsWireless: function() { 102 return ControllerIOS.gSimulateWirelessPlaybackTarget || (('webkitCurrentPlaybackTargetIsWireless' in this.video) && this.video.webkitCurrentPlaybackTargetIsWireless); 103 }, 104 105 updateWirelessPlaybackStatus: function() { 106 if (this.currentPlaybackTargetIsWireless()) { 107 var backgroundImageSVG = "url('" + ControllerIOS.gWirelessImage + "')"; 108 109 var deviceName = ""; 110 var deviceType = ""; 111 var type = this.host.externalDeviceType; 112 if (type == "airplay") { 113 deviceType = this.UIString('##WIRELESS_PLAYBACK_DEVICE_TYPE##'); 114 deviceName = this.UIString('##WIRELESS_PLAYBACK_DEVICE_NAME##', '##DEVICE_NAME##', this.host.externalDeviceDisplayName || "Apple TV"); 115 } else if (type == "tvout") { 116 deviceType = this.UIString('##TVOUT_DEVICE_TYPE##'); 117 deviceName = this.UIString('##TVOUT_DEVICE_NAME##'); 118 } 119 120 backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_TYPE##', deviceType); 121 backgroundImageSVG = backgroundImageSVG.replace('##DEVICE_NAME##', deviceName); 122 123 this.controls.wirelessPlaybackStatus.style.backgroundImage = backgroundImageSVG; 124 this.controls.wirelessPlaybackStatus.setAttribute('aria-label', deviceType + ", " + deviceName); 125 126 this.controls.wirelessPlaybackStatus.classList.remove(this.ClassNames.hidden); 127 this.controls.wirelessTargetPicker.classList.add(this.ClassNames.active); 128 } else { 129 this.controls.wirelessPlaybackStatus.classList.add(this.ClassNames.hidden); 130 this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.active); 131 } 132 }, 133 134 updateWirelessTargetAvailable: function() { 135 if (ControllerIOS.gSimulateWirelessPlaybackTarget || this.hasWirelessPlaybackTargets) 136 this.controls.wirelessTargetPicker.classList.remove(this.ClassNames.hidden); 137 else 138 this.controls.wirelessTargetPicker.classList.add(this.ClassNames.hidden); 139 }, 140 141 createControls: function() { 142 Controller.prototype.createControls.call(this); 143 144 var wirelessPlaybackStatus = this.controls.wirelessPlaybackStatus = document.createElement('div'); 145 wirelessPlaybackStatus.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-status'); 146 wirelessPlaybackStatus.classList.add(this.ClassNames.hidden); 147 148 var wirelessTargetPicker = this.controls.wirelessTargetPicker = document.createElement('button'); 149 wirelessTargetPicker.setAttribute('pseudo', '-webkit-media-controls-wireless-playback-picker-button'); 150 wirelessTargetPicker.setAttribute('aria-label', this.UIString('Choose Wireless Display')); 151 this.listenFor(wirelessTargetPicker, 'touchstart', this.handleWirelessPickerButtonTouchStart); 152 this.listenFor(wirelessTargetPicker, 'touchend', this.handleWirelessPickerButtonTouchEnd); 153 this.listenFor(wirelessTargetPicker, 'touchcancel', this.handleWirelessPickerButtonTouchCancel); 154 155 if (!ControllerIOS.gSimulateWirelessPlaybackTarget) 156 wirelessTargetPicker.classList.add(this.ClassNames.hidden); 157 158 this.listenFor(this.controls.startPlaybackButton, 'touchstart', this.handleStartPlaybackButtonTouchStart); 159 this.listenFor(this.controls.startPlaybackButton, 'touchend', this.handleStartPlaybackButtonTouchEnd); 160 this.listenFor(this.controls.startPlaybackButton, 'touchcancel', this.handleStartPlaybackButtonTouchCancel); 161 162 this.listenFor(this.controls.panel, 'touchstart', this.handlePanelTouchStart); 163 this.listenFor(this.controls.panel, 'touchend', this.handlePanelTouchEnd); 164 this.listenFor(this.controls.panel, 'touchcancel', this.handlePanelTouchCancel); 165 this.listenFor(this.controls.playButton, 'touchstart', this.handlePlayButtonTouchStart); 166 this.listenFor(this.controls.playButton, 'touchend', this.handlePlayButtonTouchEnd); 167 this.listenFor(this.controls.playButton, 'touchcancel', this.handlePlayButtonTouchCancel); 168 this.listenFor(this.controls.fullscreenButton, 'touchstart', this.handleFullscreenTouchStart); 169 this.listenFor(this.controls.fullscreenButton, 'touchend', this.handleFullscreenTouchEnd); 170 this.listenFor(this.controls.fullscreenButton, 'touchcancel', this.handleFullscreenTouchCancel); 171 this.stopListeningFor(this.controls.playButton, 'click', this.handlePlayButtonClicked); 172 }, 173 174 setControlsType: function(type) { 175 if (type === this.controlsType) 176 return; 177 Controller.prototype.setControlsType.call(this, type); 178 179 if (type === ControllerIOS.StartPlaybackControls) 180 this.addStartPlaybackControls(); 181 else 182 this.removeStartPlaybackControls(); 183 }, 184 185 addStartPlaybackControls: function() { 186 this.base.appendChild(this.controls.startPlaybackButton); 187 }, 188 189 removeStartPlaybackControls: function() { 190 if (this.controls.startPlaybackButton.parentNode) 191 this.controls.startPlaybackButton.parentNode.removeChild(this.controls.startPlaybackButton); 192 }, 193 194 configureInlineControls: function() { 195 this.base.appendChild(this.controls.wirelessPlaybackStatus); 196 197 this.controls.panel.appendChild(this.controls.playButton); 198 this.controls.panel.appendChild(this.controls.statusDisplay); 199 this.controls.panel.appendChild(this.controls.timelineBox); 200 this.controls.panel.appendChild(this.controls.wirelessTargetPicker); 201 if (!this.isLive) { 202 this.controls.timelineBox.appendChild(this.controls.currentTime); 203 this.controls.timelineBox.appendChild(this.controls.timeline); 204 this.controls.timelineBox.appendChild(this.controls.remainingTime); 205 } 206 if (!this.isAudio()) 207 this.controls.panel.appendChild(this.controls.fullscreenButton); 208 }, 209 210 configureFullScreenControls: function() { 211 // Do nothing 212 }, 213 214 updateControls: function() { 215 if (this.shouldHaveStartPlaybackButton()) 216 this.setControlsType(ControllerIOS.StartPlaybackControls); 217 else if (this.isFullScreen()) 218 this.setControlsType(Controller.FullScreenControls); 219 else 220 this.setControlsType(Controller.InlineControls); 221 222 this.setNeedsTimelineMetricsUpdate(); 223 }, 224 225 updateTime: function() { 226 Controller.prototype.updateTime.call(this); 227 this.updateProgress(); 228 }, 229 230 progressFillStyle: function() { 231 return 'rgba(0, 0, 0, 0.5)'; 232 }, 233 234 updateProgress: function() { 235 Controller.prototype.updateProgress.call(this); 236 237 var width = this.timelineWidth; 238 var height = this.timelineHeight; 239 240 // Magic number, matching the value for ::-webkit-media-controls-timeline::-webkit-slider-thumb 241 // in mediaControlsiOS.css. Since we cannot ask the thumb for its offsetWidth as it's in its own 242 // shadow dom, just hard-code the value. 243 var thumbWidth = 16; 244 var endX = thumbWidth / 2 + (width - thumbWidth) * this.video.currentTime / this.video.duration; 245 246 var context = document.getCSSCanvasContext('2d', 'timeline-' + this.timelineID, width, height); 247 context.fillStyle = 'white'; 248 context.fillRect(0, 0, endX, height); 249 }, 250 251 formatTime: function(time) { 252 if (isNaN(time)) 253 time = 0; 254 var absTime = Math.abs(time); 255 var intSeconds = Math.floor(absTime % 60).toFixed(0); 256 var intMinutes = Math.floor((absTime / 60) % 60).toFixed(0); 257 var intHours = Math.floor(absTime / (60 * 60)).toFixed(0); 258 var sign = time < 0 ? '-' : String(); 259 260 if (intHours > 0) 261 return sign + intHours + ':' + String('00' + intMinutes).slice(-2) + ":" + String('00' + intSeconds).slice(-2); 262 263 return sign + String('00' + intMinutes).slice(intMinutes >= 10 ? -2 : -1) + ":" + String('00' + intSeconds).slice(-2); 264 }, 265 266 handleTimelineChange: function(event) { 267 Controller.prototype.handleTimelineChange.call(this); 268 this.updateProgress(); 269 }, 270 271 handlePlayButtonTouchStart: function() { 272 this.controls.playButton.classList.add('active'); 273 }, 274 275 handlePlayButtonTouchEnd: function(event) { 276 this.controls.playButton.classList.remove('active'); 277 278 if (this.canPlay()) 279 this.video.play(); 280 else 281 this.video.pause(); 282 283 return true; 284 }, 285 286 handlePlayButtonTouchCancel: function(event) { 287 this.controls.playButton.classList.remove('active'); 288 return true; 289 }, 290 291 handleBaseGestureStart: function(event) { 292 this.gestureStartTime = new Date(); 293 // If this gesture started with two fingers inside the video, then 294 // don't treat it as a potential zoom, unless we're still waiting 295 // to play. 296 if (this.mostRecentNumberOfTargettedTouches == 2 && this.controlsType != ControllerIOS.StartPlaybackControls) 297 event.preventDefault(); 298 }, 299 300 handleBaseGestureChange: function(event) { 301 if (!this.video.controls || this.isAudio() || this.isFullScreen() || this.gestureStartTime === undefined || this.controlsType == ControllerIOS.StartPlaybackControls) 302 return; 303 304 if (this.mostRecentNumberOfTargettedTouches == 2 && event.scale >= 1.0) 305 event.preventDefault(); 306 307 var currentGestureTime = new Date(); 308 var duration = (currentGestureTime - this.gestureStartTime) / 1000; 309 if (!duration) 310 return; 311 312 var velocity = Math.abs(event.scale - 1) / duration; 313 314 if (event.scale < 1.25 || velocity < 2) 315 return; 316 317 delete this.gestureStartTime; 318 this.video.webkitEnterFullscreen(); 319 }, 320 321 handleBaseGestureEnd: function(event) { 322 delete this.gestureStartTime; 323 }, 324 325 handleWrapperTouchStart: function(event) { 326 if (event.target != this.base && event.target != this.controls.wirelessPlaybackStatus) 327 return; 328 329 this.mostRecentNumberOfTargettedTouches = event.targetTouches.length; 330 331 if (this.controlsAreHidden()) { 332 this.showControls(); 333 if (this.hideTimer) 334 clearTimeout(this.hideTimer); 335 this.hideTimer = setTimeout(this.hideControls.bind(this), this.HideControlsDelay); 336 } else if (!this.canPlay()) 337 this.hideControls(); 338 }, 339 340 handlePanelTouchStart: function(event) { 341 this.video.style.webkitUserSelect = 'none'; 342 }, 343 344 handlePanelTouchEnd: function(event) { 345 this.video.style.removeProperty('-webkit-user-select'); 346 }, 347 348 handlePanelTouchCancel: function(event) { 349 this.video.style.removeProperty('-webkit-user-select'); 350 }, 351 352 isFullScreen: function() 353 { 354 return this.video.webkitDisplayingFullscreen; 355 }, 356 357 handleFullscreenButtonClicked: function(event) { 358 if (this.isFullScreen()) 359 this.video.webkitExitFullscreen(); 360 else 361 this.video.webkitEnterFullscreen(); 362 }, 363 364 handleFullscreenTouchStart: function() { 365 this.controls.fullscreenButton.classList.add('active'); 366 }, 367 368 handleFullscreenTouchEnd: function(event) { 369 this.controls.fullscreenButton.classList.remove('active'); 370 371 this.handleFullscreenButtonClicked(); 372 373 return true; 374 }, 375 376 handleFullscreenTouchCancel: function(event) { 377 this.controls.fullscreenButton.classList.remove('active'); 378 return true; 379 }, 380 381 handleStartPlaybackButtonTouchStart: function(event) { 382 this.controls.fullscreenButton.classList.add('active'); 383 }, 384 385 handleStartPlaybackButtonTouchEnd: function(event) { 386 this.controls.fullscreenButton.classList.remove('active'); 387 if (this.video.error) 388 return true; 389 390 this.video.play(); 391 392 return true; 393 }, 394 395 handleStartPlaybackButtonTouchCancel: function(event) { 396 this.controls.fullscreenButton.classList.remove('active'); 397 return true; 398 }, 399 400 handleReadyStateChange: function(event) { 401 Controller.prototype.handleReadyStateChange.call(this, event); 402 this.updateControls(); 403 }, 404 405 handleWirelessPlaybackChange: function(event) { 406 this.updateWirelessPlaybackStatus(); 407 this.setNeedsTimelineMetricsUpdate(); 408 }, 409 410 handleWirelessTargetAvailableChange: function(event) { 411 var wirelessPlaybackTargetsAvailable = event.availability == "available"; 412 if (this.hasWirelessPlaybackTargets === wirelessPlaybackTargetsAvailable) 413 return; 414 415 this.hasWirelessPlaybackTargets = wirelessPlaybackTargetsAvailable; 416 this.updateWirelessTargetAvailable(); 417 this.setNeedsTimelineMetricsUpdate(); 418 }, 419 420 handleWirelessPickerButtonTouchStart: function() { 421 if (!this.video.error) 422 this.controls.wirelessTargetPicker.classList.add('active'); 423 }, 424 425 handleWirelessPickerButtonTouchEnd: function(event) { 426 this.controls.wirelessTargetPicker.classList.remove('active'); 427 this.video.webkitShowPlaybackTargetPicker(); 428 return true; 429 }, 430 431 handleWirelessPickerButtonTouchCancel: function(event) { 432 this.controls.wirelessTargetPicker.classList.remove('active'); 433 return true; 434 }, 435 436 updateStatusDisplay: function(event) 437 { 438 this.controls.startPlaybackButton.classList.toggle(this.ClassNames.failed, this.video.error !== null); 439 Controller.prototype.updateStatusDisplay.call(this, event); 440 }, 441 442 setPlaying: function(isPlaying) 443 { 444 this.updateControls(); 445 Controller.prototype.setPlaying.call(this, isPlaying); 446 }, 447 448 get pageScaleFactor() { 449 return this._pageScaleFactor; 450 }, 451 452 set pageScaleFactor(newScaleFactor) { 453 if (this._pageScaleFactor === newScaleFactor) 454 return; 455 456 this._pageScaleFactor = newScaleFactor; 457 458 if (newScaleFactor) { 459 var scaleValue = 1 / newScaleFactor; 460 var scaleTransform = "scale(" + scaleValue + ")"; 461 if (this.controls.startPlaybackButton) 462 this.controls.startPlaybackButton.style.webkitTransform = scaleTransform; 463 if (this.controls.panel) { 464 var bottomAligment = -2 * scaleValue; 465 this.controls.panel.style.bottom = bottomAligment + "px"; 466 this.controls.panel.style.paddingBottom = -(newScaleFactor * bottomAligment) + "px"; 467 this.controls.panel.style.width = Math.ceil(newScaleFactor * 100) + "%"; 468 this.controls.panel.style.webkitTransform = scaleTransform; 469 this.setNeedsTimelineMetricsUpdate(); 470 this.updateProgress(); 471 } 472 } 473 } 474}; 475 476Object.create(Controller.prototype).extend(ControllerIOS.prototype); 477Object.defineProperty(ControllerIOS.prototype, 'constructor', { enumerable: false, value: ControllerIOS }); 478