| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * @fileoverview MediaControls class implements media playback controls | |
| 9 * that exist outside of the audio/video HTML element. | |
| 10 */ | |
| 11 | |
| 12 /** | |
| 13 * @param {HTMLElement} containerElement The container for the controls. | |
| 14 * @param {function} onMediaError Function to display an error message. | |
| 15 * @constructor | |
| 16 */ | |
| 17 function MediaControls(containerElement, onMediaError) { | |
| 18 this.container_ = containerElement; | |
| 19 this.document_ = this.container_.ownerDocument; | |
| 20 this.media_ = null; | |
| 21 | |
| 22 this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true); | |
| 23 this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false); | |
| 24 this.onMediaDurationBound_ = this.onMediaDuration_.bind(this); | |
| 25 this.onMediaProgressBound_ = this.onMediaProgress_.bind(this); | |
| 26 this.onMediaError_ = onMediaError || function() {}; | |
| 27 } | |
| 28 | |
| 29 /** | |
| 30 * Button's state types. Values are used as CSS class names. | |
| 31 * @enum {string} | |
| 32 */ | |
| 33 MediaControls.ButtonStateType = { | |
| 34 DEFAULT: 'default', | |
| 35 PLAYING: 'playing', | |
| 36 ENDED: 'ended' | |
| 37 }; | |
| 38 | |
| 39 /** | |
| 40 * @return {HTMLAudioElement|HTMLVideoElement} The media element. | |
| 41 */ | |
| 42 MediaControls.prototype.getMedia = function() { return this.media_ }; | |
| 43 | |
| 44 /** | |
| 45 * Format the time in hh:mm:ss format (omitting redundant leading zeros). | |
| 46 * | |
| 47 * @param {number} timeInSec Time in seconds. | |
| 48 * @return {string} Formatted time string. | |
| 49 * @private | |
| 50 */ | |
| 51 MediaControls.formatTime_ = function(timeInSec) { | |
| 52 var seconds = Math.floor(timeInSec % 60); | |
| 53 var minutes = Math.floor((timeInSec / 60) % 60); | |
| 54 var hours = Math.floor(timeInSec / 60 / 60); | |
| 55 var result = ''; | |
| 56 if (hours) result += hours + ':'; | |
| 57 if (hours && (minutes < 10)) result += '0'; | |
| 58 result += minutes + ':'; | |
| 59 if (seconds < 10) result += '0'; | |
| 60 result += seconds; | |
| 61 return result; | |
| 62 }; | |
| 63 | |
| 64 /** | |
| 65 * Create a custom control. | |
| 66 * | |
| 67 * @param {string} className Class name. | |
| 68 * @param {HTMLElement=} opt_parent Parent element or container if undefined. | |
| 69 * @return {HTMLElement} The new control element. | |
| 70 */ | |
| 71 MediaControls.prototype.createControl = function(className, opt_parent) { | |
| 72 var parent = opt_parent || this.container_; | |
| 73 var control = this.document_.createElement('div'); | |
| 74 control.className = className; | |
| 75 parent.appendChild(control); | |
| 76 return control; | |
| 77 }; | |
| 78 | |
| 79 /** | |
| 80 * Create a custom button. | |
| 81 * | |
| 82 * @param {string} className Class name. | |
| 83 * @param {function(Event)} handler Click handler. | |
| 84 * @param {HTMLElement=} opt_parent Parent element or container if undefined. | |
| 85 * @param {number=} opt_numStates Number of states, default: 1. | |
| 86 * @return {HTMLElement} The new button element. | |
| 87 */ | |
| 88 MediaControls.prototype.createButton = function( | |
| 89 className, handler, opt_parent, opt_numStates) { | |
| 90 opt_numStates = opt_numStates || 1; | |
| 91 | |
| 92 var button = this.createControl(className, opt_parent); | |
| 93 button.classList.add('media-button'); | |
| 94 button.addEventListener('click', handler); | |
| 95 | |
| 96 var stateTypes = Object.keys(MediaControls.ButtonStateType); | |
| 97 for (var state = 0; state != opt_numStates; state++) { | |
| 98 var stateClass = MediaControls.ButtonStateType[stateTypes[state]]; | |
| 99 this.createControl('normal ' + stateClass, button); | |
| 100 this.createControl('hover ' + stateClass, button); | |
| 101 this.createControl('active ' + stateClass, button); | |
| 102 } | |
| 103 this.createControl('disabled', button); | |
| 104 | |
| 105 button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT); | |
| 106 button.addEventListener('click', handler); | |
| 107 return button; | |
| 108 }; | |
| 109 | |
| 110 /** | |
| 111 * Enable/disable controls matching a given selector. | |
| 112 * | |
| 113 * @param {string} selector CSS selector. | |
| 114 * @param {boolean} on True if enable, false if disable. | |
| 115 * @private | |
| 116 */ | |
| 117 MediaControls.prototype.enableControls_ = function(selector, on) { | |
| 118 var controls = this.container_.querySelectorAll(selector); | |
| 119 for (var i = 0; i != controls.length; i++) { | |
| 120 var classList = controls[i].classList; | |
| 121 if (on) | |
| 122 classList.remove('disabled'); | |
| 123 else | |
| 124 classList.add('disabled'); | |
| 125 } | |
| 126 }; | |
| 127 | |
| 128 /* | |
| 129 * Playback control. | |
| 130 */ | |
| 131 | |
| 132 /** | |
| 133 * Play the media. | |
| 134 */ | |
| 135 MediaControls.prototype.play = function() { | |
| 136 this.media_.play(); | |
| 137 }; | |
| 138 | |
| 139 /** | |
| 140 * Pause the media. | |
| 141 */ | |
| 142 MediaControls.prototype.pause = function() { | |
| 143 this.media_.pause(); | |
| 144 }; | |
| 145 | |
| 146 /** | |
| 147 * @return {boolean} True if the media is currently playing. | |
| 148 */ | |
| 149 MediaControls.prototype.isPlaying = function() { | |
| 150 return !this.media_.paused && !this.media_.ended; | |
| 151 }; | |
| 152 | |
| 153 /** | |
| 154 * Toggle play/pause. | |
| 155 */ | |
| 156 MediaControls.prototype.togglePlayState = function() { | |
| 157 if (this.isPlaying()) | |
| 158 this.pause(); | |
| 159 else | |
| 160 this.play(); | |
| 161 }; | |
| 162 | |
| 163 /** | |
| 164 * Toggle play/pause state on a mouse click on the play/pause button. Can be | |
| 165 * called externally. TODO(mtomasz): Remove it. http://www.crbug.com/254318. | |
| 166 * | |
| 167 * @param {Event=} opt_event Mouse click event. | |
| 168 */ | |
| 169 MediaControls.prototype.onPlayButtonClicked = function(opt_event) { | |
| 170 this.togglePlayState(); | |
| 171 }; | |
| 172 | |
| 173 /** | |
| 174 * @param {HTMLElement=} opt_parent Parent container. | |
| 175 */ | |
| 176 MediaControls.prototype.initPlayButton = function(opt_parent) { | |
| 177 this.playButton_ = this.createButton('play media-control', | |
| 178 this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */); | |
| 179 }; | |
| 180 | |
| 181 /* | |
| 182 * Time controls | |
| 183 */ | |
| 184 | |
| 185 /** | |
| 186 * The default range of 100 is too coarse for the media progress slider. | |
| 187 */ | |
| 188 MediaControls.PROGRESS_RANGE = 5000; | |
| 189 | |
| 190 /** | |
| 191 * @param {boolean=} opt_seekMark True if the progress slider should have | |
| 192 * a seek mark. | |
| 193 * @param {HTMLElement=} opt_parent Parent container. | |
| 194 */ | |
| 195 MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) { | |
| 196 var timeControls = this.createControl('time-controls', opt_parent); | |
| 197 | |
| 198 var sliderConstructor = | |
| 199 opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider; | |
| 200 | |
| 201 this.progressSlider_ = new sliderConstructor( | |
| 202 this.createControl('progress media-control', timeControls), | |
| 203 0, /* value */ | |
| 204 MediaControls.PROGRESS_RANGE, | |
| 205 this.onProgressChange_.bind(this), | |
| 206 this.onProgressDrag_.bind(this)); | |
| 207 | |
| 208 var timeBox = this.createControl('time media-control', timeControls); | |
| 209 | |
| 210 this.duration_ = this.createControl('duration', timeBox); | |
| 211 // Set the initial width to the minimum to reduce the flicker. | |
| 212 this.duration_.textContent = MediaControls.formatTime_(0); | |
| 213 | |
| 214 this.currentTime_ = this.createControl('current', timeBox); | |
| 215 }; | |
| 216 | |
| 217 /** | |
| 218 * @param {number} current Current time is seconds. | |
| 219 * @param {number} duration Duration in seconds. | |
| 220 * @private | |
| 221 */ | |
| 222 MediaControls.prototype.displayProgress_ = function(current, duration) { | |
| 223 var ratio = current / duration; | |
| 224 this.progressSlider_.setValue(ratio); | |
| 225 this.currentTime_.textContent = MediaControls.formatTime_(current); | |
| 226 }; | |
| 227 | |
| 228 /** | |
| 229 * @param {number} value Progress [0..1]. | |
| 230 * @private | |
| 231 */ | |
| 232 MediaControls.prototype.onProgressChange_ = function(value) { | |
| 233 if (!this.media_.seekable || !this.media_.duration) { | |
| 234 console.error('Inconsistent media state'); | |
| 235 return; | |
| 236 } | |
| 237 | |
| 238 var current = this.media_.duration * value; | |
| 239 this.media_.currentTime = current; | |
| 240 this.currentTime_.textContent = MediaControls.formatTime_(current); | |
| 241 }; | |
| 242 | |
| 243 /** | |
| 244 * @param {boolean} on True if dragging. | |
| 245 * @private | |
| 246 */ | |
| 247 MediaControls.prototype.onProgressDrag_ = function(on) { | |
| 248 if (on) { | |
| 249 this.resumeAfterDrag_ = this.isPlaying(); | |
| 250 this.media_.pause(); | |
| 251 } else { | |
| 252 if (this.resumeAfterDrag_) { | |
| 253 if (this.media_.ended) | |
| 254 this.onMediaPlay_(false); | |
| 255 else | |
| 256 this.media_.play(); | |
| 257 } | |
| 258 this.updatePlayButtonState_(this.isPlaying()); | |
| 259 } | |
| 260 }; | |
| 261 | |
| 262 /* | |
| 263 * Volume controls | |
| 264 */ | |
| 265 | |
| 266 /** | |
| 267 * @param {HTMLElement=} opt_parent Parent element for the controls. | |
| 268 */ | |
| 269 MediaControls.prototype.initVolumeControls = function(opt_parent) { | |
| 270 var volumeControls = this.createControl('volume-controls', opt_parent); | |
| 271 | |
| 272 this.soundButton_ = this.createButton('sound media-control', | |
| 273 this.onSoundButtonClick_.bind(this), volumeControls); | |
| 274 this.soundButton_.setAttribute('level', 3); // max level. | |
| 275 | |
| 276 this.volume_ = new MediaControls.AnimatedSlider( | |
| 277 this.createControl('volume media-control', volumeControls), | |
| 278 1, /* value */ | |
| 279 100 /* range */, | |
| 280 this.onVolumeChange_.bind(this), | |
| 281 this.onVolumeDrag_.bind(this)); | |
| 282 }; | |
| 283 | |
| 284 /** | |
| 285 * Click handler for the sound level button. | |
| 286 * @private | |
| 287 */ | |
| 288 MediaControls.prototype.onSoundButtonClick_ = function() { | |
| 289 if (this.media_.volume == 0) { | |
| 290 this.volume_.setValue(this.savedVolume_ || 1); | |
| 291 } else { | |
| 292 this.savedVolume_ = this.media_.volume; | |
| 293 this.volume_.setValue(0); | |
| 294 } | |
| 295 this.onVolumeChange_(this.volume_.getValue()); | |
| 296 }; | |
| 297 | |
| 298 /** | |
| 299 * @param {number} value Volume [0..1]. | |
| 300 * @return {number} The rough level [0..3] used to pick an icon. | |
| 301 * @private | |
| 302 */ | |
| 303 MediaControls.getVolumeLevel_ = function(value) { | |
| 304 if (value == 0) return 0; | |
| 305 if (value <= 1 / 3) return 1; | |
| 306 if (value <= 2 / 3) return 2; | |
| 307 return 3; | |
| 308 }; | |
| 309 | |
| 310 /** | |
| 311 * @param {number} value Volume [0..1]. | |
| 312 * @private | |
| 313 */ | |
| 314 MediaControls.prototype.onVolumeChange_ = function(value) { | |
| 315 this.media_.volume = value; | |
| 316 this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value)); | |
| 317 }; | |
| 318 | |
| 319 /** | |
| 320 * @param {boolean} on True if dragging is in progress. | |
| 321 * @private | |
| 322 */ | |
| 323 MediaControls.prototype.onVolumeDrag_ = function(on) { | |
| 324 if (on && (this.media_.volume != 0)) { | |
| 325 this.savedVolume_ = this.media_.volume; | |
| 326 } | |
| 327 }; | |
| 328 | |
| 329 /* | |
| 330 * Media event handlers. | |
| 331 */ | |
| 332 | |
| 333 /** | |
| 334 * Attach a media element. | |
| 335 * | |
| 336 * @param {HTMLMediaElement} mediaElement The media element to control. | |
| 337 */ | |
| 338 MediaControls.prototype.attachMedia = function(mediaElement) { | |
| 339 this.media_ = mediaElement; | |
| 340 | |
| 341 this.media_.addEventListener('play', this.onMediaPlayBound_); | |
| 342 this.media_.addEventListener('pause', this.onMediaPauseBound_); | |
| 343 this.media_.addEventListener('durationchange', this.onMediaDurationBound_); | |
| 344 this.media_.addEventListener('timeupdate', this.onMediaProgressBound_); | |
| 345 this.media_.addEventListener('error', this.onMediaError_); | |
| 346 | |
| 347 // Reflect the media state in the UI. | |
| 348 this.onMediaDuration_(); | |
| 349 this.onMediaPlay_(this.isPlaying()); | |
| 350 this.onMediaProgress_(); | |
| 351 if (this.volume_) { | |
| 352 /* Copy the user selected volume to the new media element. */ | |
| 353 this.media_.volume = this.volume_.getValue(); | |
| 354 } | |
| 355 }; | |
| 356 | |
| 357 /** | |
| 358 * Detach media event handlers. | |
| 359 */ | |
| 360 MediaControls.prototype.detachMedia = function() { | |
| 361 if (!this.media_) | |
| 362 return; | |
| 363 | |
| 364 this.media_.removeEventListener('play', this.onMediaPlayBound_); | |
| 365 this.media_.removeEventListener('pause', this.onMediaPauseBound_); | |
| 366 this.media_.removeEventListener('durationchange', this.onMediaDurationBound_); | |
| 367 this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_); | |
| 368 this.media_.removeEventListener('error', this.onMediaError_); | |
| 369 | |
| 370 this.media_ = null; | |
| 371 }; | |
| 372 | |
| 373 /** | |
| 374 * Force-empty the media pipeline. This is a workaround for crbug.com/149957. | |
| 375 * The document is not going to be GC-ed until the last Files app window closes, | |
| 376 * but we want the media pipeline to deinitialize ASAP to minimize leakage. | |
| 377 */ | |
| 378 MediaControls.prototype.cleanup = function() { | |
| 379 this.media_.src = ''; | |
| 380 this.media_.load(); | |
| 381 this.detachMedia(); | |
| 382 }; | |
| 383 | |
| 384 /** | |
| 385 * 'play' and 'pause' event handler. | |
| 386 * @param {boolean} playing True if playing. | |
| 387 * @private | |
| 388 */ | |
| 389 MediaControls.prototype.onMediaPlay_ = function(playing) { | |
| 390 if (this.progressSlider_.isDragging()) | |
| 391 return; | |
| 392 | |
| 393 this.updatePlayButtonState_(playing); | |
| 394 this.onPlayStateChanged(); | |
| 395 }; | |
| 396 | |
| 397 /** | |
| 398 * 'durationchange' event handler. | |
| 399 * @private | |
| 400 */ | |
| 401 MediaControls.prototype.onMediaDuration_ = function() { | |
| 402 if (!this.media_.duration) { | |
| 403 this.enableControls_('.media-control', false); | |
| 404 return; | |
| 405 } | |
| 406 | |
| 407 this.enableControls_('.media-control', true); | |
| 408 | |
| 409 var sliderContainer = this.progressSlider_.getContainer(); | |
| 410 if (this.media_.seekable) | |
| 411 sliderContainer.classList.remove('readonly'); | |
| 412 else | |
| 413 sliderContainer.classList.add('readonly'); | |
| 414 | |
| 415 var valueToString = function(value) { | |
| 416 return MediaControls.formatTime_(this.media_.duration * value); | |
| 417 }.bind(this); | |
| 418 | |
| 419 this.duration_.textContent = valueToString(1); | |
| 420 | |
| 421 if (this.progressSlider_.setValueToStringFunction) | |
| 422 this.progressSlider_.setValueToStringFunction(valueToString); | |
| 423 | |
| 424 if (this.media_.seekable) | |
| 425 this.restorePlayState(); | |
| 426 }; | |
| 427 | |
| 428 /** | |
| 429 * 'timeupdate' event handler. | |
| 430 * @private | |
| 431 */ | |
| 432 MediaControls.prototype.onMediaProgress_ = function() { | |
| 433 if (!this.media_.duration) { | |
| 434 this.displayProgress_(0, 1); | |
| 435 return; | |
| 436 } | |
| 437 | |
| 438 var current = this.media_.currentTime; | |
| 439 var duration = this.media_.duration; | |
| 440 | |
| 441 if (this.progressSlider_.isDragging()) | |
| 442 return; | |
| 443 | |
| 444 this.displayProgress_(current, duration); | |
| 445 | |
| 446 if (current == duration) { | |
| 447 this.onMediaComplete(); | |
| 448 } | |
| 449 this.onPlayStateChanged(); | |
| 450 }; | |
| 451 | |
| 452 /** | |
| 453 * Called when the media playback is complete. | |
| 454 */ | |
| 455 MediaControls.prototype.onMediaComplete = function() {}; | |
| 456 | |
| 457 /** | |
| 458 * Called when play/pause state is changed or on playback progress. | |
| 459 * This is the right moment to save the play state. | |
| 460 */ | |
| 461 MediaControls.prototype.onPlayStateChanged = function() {}; | |
| 462 | |
| 463 /** | |
| 464 * Updates the play button state. | |
| 465 * @param {boolean} playing If the video is playing. | |
| 466 * @private | |
| 467 */ | |
| 468 MediaControls.prototype.updatePlayButtonState_ = function(playing) { | |
| 469 if (playing) { | |
| 470 this.playButton_.setAttribute('state', | |
| 471 MediaControls.ButtonStateType.PLAYING); | |
| 472 } else if (!this.media_.ended) { | |
| 473 this.playButton_.setAttribute('state', | |
| 474 MediaControls.ButtonStateType.DEFAULT); | |
| 475 } else { | |
| 476 this.playButton_.setAttribute('state', | |
| 477 MediaControls.ButtonStateType.ENDED); | |
| 478 } | |
| 479 }; | |
| 480 | |
| 481 /** | |
| 482 * Restore play state. Base implementation is empty. | |
| 483 */ | |
| 484 MediaControls.prototype.restorePlayState = function() {}; | |
| 485 | |
| 486 /** | |
| 487 * Encode current state into the page URL or the app state. | |
| 488 */ | |
| 489 MediaControls.prototype.encodeState = function() { | |
| 490 if (!this.media_.duration) | |
| 491 return; | |
| 492 | |
| 493 if (window.appState) { | |
| 494 window.appState.time = this.media_.currentTime; | |
| 495 util.saveAppState(); | |
| 496 return; | |
| 497 } | |
| 498 | |
| 499 var playState = JSON.stringify({ | |
| 500 play: this.isPlaying(), | |
| 501 time: this.media_.currentTime | |
| 502 }); | |
| 503 | |
| 504 var newLocation = document.location.origin + document.location.pathname + | |
| 505 document.location.search + '#' + playState; | |
| 506 | |
| 507 document.location.href = newLocation; | |
| 508 }; | |
| 509 | |
| 510 /** | |
| 511 * Decode current state from the page URL or the app state. | |
| 512 * @return {boolean} True if decode succeeded. | |
| 513 */ | |
| 514 MediaControls.prototype.decodeState = function() { | |
| 515 if (window.appState) { | |
| 516 if (!('time' in window.appState)) | |
| 517 return false; | |
| 518 // There is no page reload for apps v2, only app restart. | |
| 519 // Always restart in paused state. | |
| 520 this.media_.currentTime = appState.time; | |
| 521 this.pause(); | |
| 522 return true; | |
| 523 } | |
| 524 | |
| 525 var hash = document.location.hash.substring(1); | |
| 526 if (hash) { | |
| 527 try { | |
| 528 var playState = JSON.parse(hash); | |
| 529 if (!('time' in playState)) | |
| 530 return false; | |
| 531 | |
| 532 this.media_.currentTime = playState.time; | |
| 533 | |
| 534 if (playState.play) | |
| 535 this.play(); | |
| 536 else | |
| 537 this.pause(); | |
| 538 | |
| 539 return true; | |
| 540 } catch (e) { | |
| 541 console.warn('Cannot decode play state'); | |
| 542 } | |
| 543 } | |
| 544 return false; | |
| 545 }; | |
| 546 | |
| 547 /** | |
| 548 * Remove current state from the page URL or the app state. | |
| 549 */ | |
| 550 MediaControls.prototype.clearState = function() { | |
| 551 if (window.appState) { | |
| 552 if ('time' in window.appState) | |
| 553 delete window.appState.time; | |
| 554 util.saveAppState(); | |
| 555 return; | |
| 556 } | |
| 557 | |
| 558 var newLocation = document.location.origin + document.location.pathname + | |
| 559 document.location.search + '#'; | |
| 560 | |
| 561 document.location.href = newLocation; | |
| 562 }; | |
| 563 | |
| 564 /** | |
| 565 * Create a customized slider control. | |
| 566 * | |
| 567 * @param {HTMLElement} container The containing div element. | |
| 568 * @param {number} value Initial value [0..1]. | |
| 569 * @param {number} range Number of distinct slider positions to be supported. | |
| 570 * @param {function(number)} onChange Value change handler. | |
| 571 * @param {function(boolean)} onDrag Drag begin/end handler. | |
| 572 * @constructor | |
| 573 */ | |
| 574 | |
| 575 MediaControls.Slider = function(container, value, range, onChange, onDrag) { | |
| 576 this.container_ = container; | |
| 577 this.onChange_ = onChange; | |
| 578 this.onDrag_ = onDrag; | |
| 579 | |
| 580 var document = this.container_.ownerDocument; | |
| 581 | |
| 582 this.container_.classList.add('custom-slider'); | |
| 583 | |
| 584 this.input_ = document.createElement('input'); | |
| 585 this.input_.type = 'range'; | |
| 586 this.input_.min = 0; | |
| 587 this.input_.max = range; | |
| 588 this.input_.value = value * range; | |
| 589 this.container_.appendChild(this.input_); | |
| 590 | |
| 591 this.input_.addEventListener( | |
| 592 'change', this.onInputChange_.bind(this)); | |
| 593 this.input_.addEventListener( | |
| 594 'mousedown', this.onInputDrag_.bind(this, true)); | |
| 595 this.input_.addEventListener( | |
| 596 'mouseup', this.onInputDrag_.bind(this, false)); | |
| 597 | |
| 598 this.bar_ = document.createElement('div'); | |
| 599 this.bar_.className = 'bar'; | |
| 600 this.container_.appendChild(this.bar_); | |
| 601 | |
| 602 this.filled_ = document.createElement('div'); | |
| 603 this.filled_.className = 'filled'; | |
| 604 this.bar_.appendChild(this.filled_); | |
| 605 | |
| 606 var leftCap = document.createElement('div'); | |
| 607 leftCap.className = 'cap left'; | |
| 608 this.bar_.appendChild(leftCap); | |
| 609 | |
| 610 var rightCap = document.createElement('div'); | |
| 611 rightCap.className = 'cap right'; | |
| 612 this.bar_.appendChild(rightCap); | |
| 613 | |
| 614 this.value_ = value; | |
| 615 this.setFilled_(value); | |
| 616 }; | |
| 617 | |
| 618 /** | |
| 619 * @return {HTMLElement} The container element. | |
| 620 */ | |
| 621 MediaControls.Slider.prototype.getContainer = function() { | |
| 622 return this.container_; | |
| 623 }; | |
| 624 | |
| 625 /** | |
| 626 * @return {HTMLElement} The standard input element. | |
| 627 * @private | |
| 628 */ | |
| 629 MediaControls.Slider.prototype.getInput_ = function() { | |
| 630 return this.input_; | |
| 631 }; | |
| 632 | |
| 633 /** | |
| 634 * @return {HTMLElement} The slider bar element. | |
| 635 */ | |
| 636 MediaControls.Slider.prototype.getBar = function() { | |
| 637 return this.bar_; | |
| 638 }; | |
| 639 | |
| 640 /** | |
| 641 * @return {number} [0..1] The current value. | |
| 642 */ | |
| 643 MediaControls.Slider.prototype.getValue = function() { | |
| 644 return this.value_; | |
| 645 }; | |
| 646 | |
| 647 /** | |
| 648 * @param {number} value [0..1]. | |
| 649 */ | |
| 650 MediaControls.Slider.prototype.setValue = function(value) { | |
| 651 this.value_ = value; | |
| 652 this.setValueToUI_(value); | |
| 653 }; | |
| 654 | |
| 655 /** | |
| 656 * Fill the given proportion the slider bar (from the left). | |
| 657 * | |
| 658 * @param {number} proportion [0..1]. | |
| 659 * @private | |
| 660 */ | |
| 661 MediaControls.Slider.prototype.setFilled_ = function(proportion) { | |
| 662 this.filled_.style.width = proportion * 100 + '%'; | |
| 663 }; | |
| 664 | |
| 665 /** | |
| 666 * Get the value from the input element. | |
| 667 * | |
| 668 * @return {number} Value [0..1]. | |
| 669 * @private | |
| 670 */ | |
| 671 MediaControls.Slider.prototype.getValueFromUI_ = function() { | |
| 672 return this.input_.value / this.input_.max; | |
| 673 }; | |
| 674 | |
| 675 /** | |
| 676 * Update the UI with the current value. | |
| 677 * | |
| 678 * @param {number} value [0..1]. | |
| 679 * @private | |
| 680 */ | |
| 681 MediaControls.Slider.prototype.setValueToUI_ = function(value) { | |
| 682 this.input_.value = value * this.input_.max; | |
| 683 this.setFilled_(value); | |
| 684 }; | |
| 685 | |
| 686 /** | |
| 687 * Compute the proportion in which the given position divides the slider bar. | |
| 688 * | |
| 689 * @param {number} position in pixels. | |
| 690 * @return {number} [0..1] proportion. | |
| 691 */ | |
| 692 MediaControls.Slider.prototype.getProportion = function(position) { | |
| 693 var rect = this.bar_.getBoundingClientRect(); | |
| 694 return Math.max(0, Math.min(1, (position - rect.left) / rect.width)); | |
| 695 }; | |
| 696 | |
| 697 /** | |
| 698 * 'change' event handler. | |
| 699 * @private | |
| 700 */ | |
| 701 MediaControls.Slider.prototype.onInputChange_ = function() { | |
| 702 this.value_ = this.getValueFromUI_(); | |
| 703 this.setFilled_(this.value_); | |
| 704 this.onChange_(this.value_); | |
| 705 }; | |
| 706 | |
| 707 /** | |
| 708 * @return {boolean} True if dragging is in progress. | |
| 709 */ | |
| 710 MediaControls.Slider.prototype.isDragging = function() { | |
| 711 return this.isDragging_; | |
| 712 }; | |
| 713 | |
| 714 /** | |
| 715 * Mousedown/mouseup handler. | |
| 716 * @param {boolean} on True if the mouse is down. | |
| 717 * @private | |
| 718 */ | |
| 719 MediaControls.Slider.prototype.onInputDrag_ = function(on) { | |
| 720 this.isDragging_ = on; | |
| 721 this.onDrag_(on); | |
| 722 }; | |
| 723 | |
| 724 /** | |
| 725 * Create a customized slider with animated thumb movement. | |
| 726 * | |
| 727 * @param {HTMLElement} container The containing div element. | |
| 728 * @param {number} value Initial value [0..1]. | |
| 729 * @param {number} range Number of distinct slider positions to be supported. | |
| 730 * @param {function(number)} onChange Value change handler. | |
| 731 * @param {function(boolean)} onDrag Drag begin/end handler. | |
| 732 * @param {function(number):string} formatFunction Value formatting function. | |
| 733 * @constructor | |
| 734 */ | |
| 735 MediaControls.AnimatedSlider = function( | |
| 736 container, value, range, onChange, onDrag, formatFunction) { | |
| 737 MediaControls.Slider.apply(this, arguments); | |
| 738 }; | |
| 739 | |
| 740 MediaControls.AnimatedSlider.prototype = { | |
| 741 __proto__: MediaControls.Slider.prototype | |
| 742 }; | |
| 743 | |
| 744 /** | |
| 745 * Number of animation steps. | |
| 746 */ | |
| 747 MediaControls.AnimatedSlider.STEPS = 10; | |
| 748 | |
| 749 /** | |
| 750 * Animation duration. | |
| 751 */ | |
| 752 MediaControls.AnimatedSlider.DURATION = 100; | |
| 753 | |
| 754 /** | |
| 755 * @param {number} value [0..1]. | |
| 756 * @private | |
| 757 */ | |
| 758 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) { | |
| 759 if (this.animationInterval_) { | |
| 760 clearInterval(this.animationInterval_); | |
| 761 } | |
| 762 var oldValue = this.getValueFromUI_(); | |
| 763 var step = 0; | |
| 764 this.animationInterval_ = setInterval(function() { | |
| 765 step++; | |
| 766 var currentValue = oldValue + | |
| 767 (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS); | |
| 768 MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue); | |
| 769 if (step == MediaControls.AnimatedSlider.STEPS) { | |
| 770 clearInterval(this.animationInterval_); | |
| 771 } | |
| 772 }.bind(this), | |
| 773 MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS); | |
| 774 }; | |
| 775 | |
| 776 /** | |
| 777 * Create a customized slider with a precise time feedback. | |
| 778 * | |
| 779 * The time value is shown above the slider bar at the mouse position. | |
| 780 * | |
| 781 * @param {HTMLElement} container The containing div element. | |
| 782 * @param {number} value Initial value [0..1]. | |
| 783 * @param {number} range Number of distinct slider positions to be supported. | |
| 784 * @param {function(number)} onChange Value change handler. | |
| 785 * @param {function(boolean)} onDrag Drag begin/end handler. | |
| 786 * @param {function(number):string} formatFunction Value formatting function. | |
| 787 * @constructor | |
| 788 */ | |
| 789 MediaControls.PreciseSlider = function( | |
| 790 container, value, range, onChange, onDrag, formatFunction) { | |
| 791 MediaControls.Slider.apply(this, arguments); | |
| 792 | |
| 793 var doc = this.container_.ownerDocument; | |
| 794 | |
| 795 /** | |
| 796 * @type {function(number):string} | |
| 797 * @private | |
| 798 */ | |
| 799 this.valueToString_ = null; | |
| 800 | |
| 801 this.seekMark_ = doc.createElement('div'); | |
| 802 this.seekMark_.className = 'seek-mark'; | |
| 803 this.getBar().appendChild(this.seekMark_); | |
| 804 | |
| 805 this.seekLabel_ = doc.createElement('div'); | |
| 806 this.seekLabel_.className = 'seek-label'; | |
| 807 this.seekMark_.appendChild(this.seekLabel_); | |
| 808 | |
| 809 this.getContainer().addEventListener( | |
| 810 'mousemove', this.onMouseMove_.bind(this)); | |
| 811 this.getContainer().addEventListener( | |
| 812 'mouseout', this.onMouseOut_.bind(this)); | |
| 813 }; | |
| 814 | |
| 815 MediaControls.PreciseSlider.prototype = { | |
| 816 __proto__: MediaControls.Slider.prototype | |
| 817 }; | |
| 818 | |
| 819 /** | |
| 820 * Show the seek mark after a delay. | |
| 821 */ | |
| 822 MediaControls.PreciseSlider.SHOW_DELAY = 200; | |
| 823 | |
| 824 /** | |
| 825 * Hide the seek mark for this long after changing the position with a click. | |
| 826 */ | |
| 827 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500; | |
| 828 | |
| 829 /** | |
| 830 * Hide the seek mark for this long after changing the position with a drag. | |
| 831 */ | |
| 832 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750; | |
| 833 | |
| 834 /** | |
| 835 * Default hide timeout (no hiding). | |
| 836 */ | |
| 837 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0; | |
| 838 | |
| 839 /** | |
| 840 * @param {function(number):string} func Value formatting function. | |
| 841 */ | |
| 842 MediaControls.PreciseSlider.prototype.setValueToStringFunction = | |
| 843 function(func) { | |
| 844 this.valueToString_ = func; | |
| 845 | |
| 846 /* It is not completely accurate to assume that the max value corresponds | |
| 847 to the longest string, but generous CSS padding will compensate for that. */ | |
| 848 var labelWidth = this.valueToString_(1).length / 2 + 1; | |
| 849 this.seekLabel_.style.width = labelWidth + 'em'; | |
| 850 this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em'; | |
| 851 }; | |
| 852 | |
| 853 /** | |
| 854 * Show the time above the slider. | |
| 855 * | |
| 856 * @param {number} ratio [0..1] The proportion of the duration. | |
| 857 * @param {number} timeout Timeout in ms after which the label should be hidden. | |
| 858 * MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call. | |
| 859 * @private | |
| 860 */ | |
| 861 MediaControls.PreciseSlider.prototype.showSeekMark_ = | |
| 862 function(ratio, timeout) { | |
| 863 // Do not update the seek mark for the first 500ms after the drag is finished. | |
| 864 if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now())) | |
| 865 return; | |
| 866 | |
| 867 this.seekMark_.style.left = ratio * 100 + '%'; | |
| 868 | |
| 869 if (ratio < this.getValue()) { | |
| 870 this.seekMark_.classList.remove('inverted'); | |
| 871 } else { | |
| 872 this.seekMark_.classList.add('inverted'); | |
| 873 } | |
| 874 this.seekLabel_.textContent = this.valueToString_(ratio); | |
| 875 | |
| 876 this.seekMark_.classList.add('visible'); | |
| 877 | |
| 878 if (this.seekMarkTimer_) { | |
| 879 clearTimeout(this.seekMarkTimer_); | |
| 880 this.seekMarkTimer_ = null; | |
| 881 } | |
| 882 if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) { | |
| 883 this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout); | |
| 884 } | |
| 885 }; | |
| 886 | |
| 887 /** | |
| 888 * @private | |
| 889 */ | |
| 890 MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() { | |
| 891 this.seekMarkTimer_ = null; | |
| 892 this.seekMark_.classList.remove('visible'); | |
| 893 }; | |
| 894 | |
| 895 /** | |
| 896 * 'mouseout' event handler. | |
| 897 * @param {Event} e Event. | |
| 898 * @private | |
| 899 */ | |
| 900 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) { | |
| 901 this.latestSeekRatio_ = this.getProportion(e.clientX); | |
| 902 | |
| 903 var self = this; | |
| 904 function showMark() { | |
| 905 if (!self.isDragging()) { | |
| 906 self.showSeekMark_(self.latestSeekRatio_, | |
| 907 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY); | |
| 908 } | |
| 909 } | |
| 910 | |
| 911 if (this.seekMark_.classList.contains('visible')) { | |
| 912 showMark(); | |
| 913 } else if (!this.seekMarkTimer_) { | |
| 914 this.seekMarkTimer_ = | |
| 915 setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY); | |
| 916 } | |
| 917 }; | |
| 918 | |
| 919 /** | |
| 920 * 'mouseout' event handler. | |
| 921 * @param {Event} e Event. | |
| 922 * @private | |
| 923 */ | |
| 924 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) { | |
| 925 for (var element = e.relatedTarget; element; element = element.parentNode) { | |
| 926 if (element == this.getContainer()) | |
| 927 return; | |
| 928 } | |
| 929 if (this.seekMarkTimer_) { | |
| 930 clearTimeout(this.seekMarkTimer_); | |
| 931 this.seekMarkTimer_ = null; | |
| 932 } | |
| 933 this.hideSeekMark_(); | |
| 934 }; | |
| 935 | |
| 936 /** | |
| 937 * 'change' event handler. | |
| 938 * @private | |
| 939 */ | |
| 940 MediaControls.PreciseSlider.prototype.onInputChange_ = function() { | |
| 941 MediaControls.Slider.prototype.onInputChange_.apply(this, arguments); | |
| 942 if (this.isDragging()) { | |
| 943 this.showSeekMark_( | |
| 944 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE); | |
| 945 } | |
| 946 }; | |
| 947 | |
| 948 /** | |
| 949 * Mousedown/mouseup handler. | |
| 950 * @param {boolean} on True if the mouse is down. | |
| 951 * @private | |
| 952 */ | |
| 953 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) { | |
| 954 MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments); | |
| 955 | |
| 956 if (on) { | |
| 957 // Dragging started, align the seek mark with the thumb position. | |
| 958 this.showSeekMark_( | |
| 959 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE); | |
| 960 } else { | |
| 961 // Just finished dragging. | |
| 962 // Show the label for the last time with a shorter timeout. | |
| 963 this.showSeekMark_( | |
| 964 this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY); | |
| 965 this.latestMouseUpTime_ = Date.now(); | |
| 966 } | |
| 967 }; | |
| 968 | |
| 969 /** | |
| 970 * Create video controls. | |
| 971 * | |
| 972 * @param {HTMLElement} containerElement The container for the controls. | |
| 973 * @param {function} onMediaError Function to display an error message. | |
| 974 * @param {function(string):string} stringFunction Function providing localized | |
| 975 * strings. | |
| 976 * @param {function=} opt_fullScreenToggle Function to toggle fullscreen mode. | |
| 977 * @param {HTMLElement=} opt_stateIconParent The parent for the icon that | |
| 978 * gives visual feedback when the playback state changes. | |
| 979 * @constructor | |
| 980 */ | |
| 981 function VideoControls(containerElement, onMediaError, stringFunction, | |
| 982 opt_fullScreenToggle, opt_stateIconParent) { | |
| 983 MediaControls.call(this, containerElement, onMediaError); | |
| 984 this.stringFunction_ = stringFunction; | |
| 985 | |
| 986 this.container_.classList.add('video-controls'); | |
| 987 this.initPlayButton(); | |
| 988 this.initTimeControls(true /* show seek mark */); | |
| 989 this.initVolumeControls(); | |
| 990 | |
| 991 if (opt_fullScreenToggle) { | |
| 992 this.fullscreenButton_ = | |
| 993 this.createButton('fullscreen', opt_fullScreenToggle); | |
| 994 } | |
| 995 | |
| 996 if (opt_stateIconParent) { | |
| 997 this.stateIcon_ = this.createControl( | |
| 998 'playback-state-icon', opt_stateIconParent); | |
| 999 this.textBanner_ = this.createControl('text-banner', opt_stateIconParent); | |
| 1000 } | |
| 1001 | |
| 1002 var videoControls = this; | |
| 1003 chrome.mediaPlayerPrivate.onTogglePlayState.addListener( | |
| 1004 function() { videoControls.togglePlayStateWithFeedback(); }); | |
| 1005 } | |
| 1006 | |
| 1007 /** | |
| 1008 * No resume if we are within this margin from the start or the end. | |
| 1009 */ | |
| 1010 VideoControls.RESUME_MARGIN = 0.03; | |
| 1011 | |
| 1012 /** | |
| 1013 * No resume for videos shorter than this. | |
| 1014 */ | |
| 1015 VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min. | |
| 1016 | |
| 1017 /** | |
| 1018 * When resuming rewind back this much. | |
| 1019 */ | |
| 1020 VideoControls.RESUME_REWIND = 5; // seconds. | |
| 1021 | |
| 1022 VideoControls.prototype = { __proto__: MediaControls.prototype }; | |
| 1023 | |
| 1024 /** | |
| 1025 * Shows icon feedback for the current state of the video player. | |
| 1026 * @private | |
| 1027 */ | |
| 1028 VideoControls.prototype.showIconFeedback_ = function() { | |
| 1029 this.stateIcon_.removeAttribute('state'); | |
| 1030 setTimeout(function() { | |
| 1031 this.stateIcon_.setAttribute('state', this.isPlaying() ? 'play' : 'pause'); | |
| 1032 }.bind(this), 0); | |
| 1033 }; | |
| 1034 | |
| 1035 /** | |
| 1036 * Shows a text banner. | |
| 1037 * | |
| 1038 * @param {string} identifier String identifier. | |
| 1039 * @private | |
| 1040 */ | |
| 1041 VideoControls.prototype.showTextBanner_ = function(identifier) { | |
| 1042 this.textBanner_.removeAttribute('visible'); | |
| 1043 this.textBanner_.textContent = this.stringFunction_(identifier); | |
| 1044 setTimeout(function() { | |
| 1045 this.textBanner_.setAttribute('visible', 'true'); | |
| 1046 }.bind(this), 0); | |
| 1047 }; | |
| 1048 | |
| 1049 /** | |
| 1050 * Toggle play/pause state on a mouse click on the play/pause button. Can be | |
| 1051 * called externally. | |
| 1052 * | |
| 1053 * @param {Event} event Mouse click event. | |
| 1054 */ | |
| 1055 VideoControls.prototype.onPlayButtonClicked = function(event) { | |
| 1056 if (event.ctrlKey) { | |
| 1057 this.toggleLoopedModeWithFeedback(true); | |
| 1058 if (!this.isPlaying()) | |
| 1059 this.togglePlayState(); | |
| 1060 } else { | |
| 1061 this.togglePlayState(); | |
| 1062 } | |
| 1063 }; | |
| 1064 | |
| 1065 /** | |
| 1066 * Media completion handler. | |
| 1067 */ | |
| 1068 VideoControls.prototype.onMediaComplete = function() { | |
| 1069 this.onMediaPlay_(false); // Just update the UI. | |
| 1070 this.savePosition(); // This will effectively forget the position. | |
| 1071 }; | |
| 1072 | |
| 1073 /** | |
| 1074 * Toggles the looped mode with feedback. | |
| 1075 * @param {boolean} on Whether enabled or not. | |
| 1076 */ | |
| 1077 VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) { | |
| 1078 if (!this.getMedia().duration) | |
| 1079 return; | |
| 1080 this.toggleLoopedMode(on); | |
| 1081 if (on) { | |
| 1082 // TODO(mtomasz): Simplify, crbug.com/254318. | |
| 1083 this.showTextBanner_('GALLERY_VIDEO_LOOPED_MODE'); | |
| 1084 } | |
| 1085 }; | |
| 1086 | |
| 1087 /** | |
| 1088 * Toggles the looped mode. | |
| 1089 * @param {boolean} on Whether enabled or not. | |
| 1090 */ | |
| 1091 VideoControls.prototype.toggleLoopedMode = function(on) { | |
| 1092 this.getMedia().loop = on; | |
| 1093 }; | |
| 1094 | |
| 1095 /** | |
| 1096 * Toggles play/pause state and flash an icon over the video. | |
| 1097 */ | |
| 1098 VideoControls.prototype.togglePlayStateWithFeedback = function() { | |
| 1099 if (!this.getMedia().duration) | |
| 1100 return; | |
| 1101 | |
| 1102 this.togglePlayState(); | |
| 1103 this.showIconFeedback_(); | |
| 1104 }; | |
| 1105 | |
| 1106 /** | |
| 1107 * Toggles play/pause state. | |
| 1108 */ | |
| 1109 VideoControls.prototype.togglePlayState = function() { | |
| 1110 if (this.isPlaying()) { | |
| 1111 // User gave the Pause command. Save the state and reset the loop mode. | |
| 1112 this.toggleLoopedMode(false); | |
| 1113 this.savePosition(); | |
| 1114 } | |
| 1115 MediaControls.prototype.togglePlayState.apply(this, arguments); | |
| 1116 }; | |
| 1117 | |
| 1118 /** | |
| 1119 * Saves the playback position to the persistent storage. | |
| 1120 * @param {boolean=} opt_sync True if the position must be saved synchronously | |
| 1121 * (required when closing app windows). | |
| 1122 */ | |
| 1123 VideoControls.prototype.savePosition = function(opt_sync) { | |
| 1124 if (!this.media_.duration || | |
| 1125 this.media_.duration < VideoControls.RESUME_THRESHOLD) { | |
| 1126 return; | |
| 1127 } | |
| 1128 | |
| 1129 var ratio = this.media_.currentTime / this.media_.duration; | |
| 1130 var position; | |
| 1131 if (ratio < VideoControls.RESUME_MARGIN || | |
| 1132 ratio > (1 - VideoControls.RESUME_MARGIN)) { | |
| 1133 // We are too close to the beginning or the end. | |
| 1134 // Remove the resume position so that next time we start from the beginning. | |
| 1135 position = null; | |
| 1136 } else { | |
| 1137 position = Math.floor( | |
| 1138 Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND)); | |
| 1139 } | |
| 1140 | |
| 1141 if (opt_sync) { | |
| 1142 // Packaged apps cannot save synchronously. | |
| 1143 // Pass the data to the background page. | |
| 1144 if (!window.saveOnExit) | |
| 1145 window.saveOnExit = []; | |
| 1146 window.saveOnExit.push({ key: this.media_.src, value: position }); | |
| 1147 } else { | |
| 1148 util.AppCache.update(this.media_.src, position); | |
| 1149 } | |
| 1150 }; | |
| 1151 | |
| 1152 /** | |
| 1153 * Resumes the playback position saved in the persistent storage. | |
| 1154 */ | |
| 1155 VideoControls.prototype.restorePlayState = function() { | |
| 1156 if (this.media_.duration >= VideoControls.RESUME_THRESHOLD) { | |
| 1157 util.AppCache.getValue(this.media_.src, function(position) { | |
| 1158 if (position) | |
| 1159 this.media_.currentTime = position; | |
| 1160 }.bind(this)); | |
| 1161 } | |
| 1162 }; | |
| 1163 | |
| 1164 /** | |
| 1165 * Updates style to best fit the size of the container. | |
| 1166 */ | |
| 1167 VideoControls.prototype.updateStyle = function() { | |
| 1168 // We assume that the video controls element fills the parent container. | |
| 1169 // This is easier than adding margins to this.container_.clientWidth. | |
| 1170 var width = this.container_.parentNode.clientWidth; | |
| 1171 | |
| 1172 // Set the margin to 5px for width >= 400, 0px for width < 160, | |
| 1173 // interpolate linearly in between. | |
| 1174 this.container_.style.margin = | |
| 1175 Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px'; | |
| 1176 | |
| 1177 var hideBelow = function(selector, limit) { | |
| 1178 this.container_.querySelector(selector).style.display = | |
| 1179 width < limit ? 'none' : '-webkit-box'; | |
| 1180 }.bind(this); | |
| 1181 | |
| 1182 hideBelow('.time', 350); | |
| 1183 hideBelow('.volume', 275); | |
| 1184 hideBelow('.volume-controls', 210); | |
| 1185 hideBelow('.fullscreen', 150); | |
| 1186 }; | |
| 1187 | |
| 1188 /** | |
| 1189 * Creates audio controls. | |
| 1190 * | |
| 1191 * @param {HTMLElement} container Parent container. | |
| 1192 * @param {function(boolean)} advanceTrack Parameter: true=forward. | |
| 1193 * @param {function} onError Error handler. | |
| 1194 * @constructor | |
| 1195 */ | |
| 1196 function AudioControls(container, advanceTrack, onError) { | |
| 1197 MediaControls.call(this, container, onError); | |
| 1198 | |
| 1199 this.container_.classList.add('audio-controls'); | |
| 1200 | |
| 1201 this.advanceTrack_ = advanceTrack; | |
| 1202 | |
| 1203 this.initPlayButton(); | |
| 1204 this.initTimeControls(false /* no seek mark */); | |
| 1205 /* No volume controls */ | |
| 1206 this.createButton('previous', this.onAdvanceClick_.bind(this, false)); | |
| 1207 this.createButton('next', this.onAdvanceClick_.bind(this, true)); | |
| 1208 | |
| 1209 var audioControls = this; | |
| 1210 chrome.mediaPlayerPrivate.onNextTrack.addListener( | |
| 1211 function() { audioControls.onAdvanceClick_(true); }); | |
| 1212 chrome.mediaPlayerPrivate.onPrevTrack.addListener( | |
| 1213 function() { audioControls.onAdvanceClick_(false); }); | |
| 1214 chrome.mediaPlayerPrivate.onTogglePlayState.addListener( | |
| 1215 function() { audioControls.togglePlayState(); }); | |
| 1216 } | |
| 1217 | |
| 1218 AudioControls.prototype = { __proto__: MediaControls.prototype }; | |
| 1219 | |
| 1220 /** | |
| 1221 * Media completion handler. Advances to the next track. | |
| 1222 */ | |
| 1223 AudioControls.prototype.onMediaComplete = function() { | |
| 1224 this.advanceTrack_(true); | |
| 1225 }; | |
| 1226 | |
| 1227 /** | |
| 1228 * The track position after which "previous" button acts as "restart". | |
| 1229 */ | |
| 1230 AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds. | |
| 1231 | |
| 1232 /** | |
| 1233 * @param {boolean} forward True if advancing forward. | |
| 1234 * @private | |
| 1235 */ | |
| 1236 AudioControls.prototype.onAdvanceClick_ = function(forward) { | |
| 1237 if (!forward && | |
| 1238 (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) { | |
| 1239 // We are far enough from the beginning of the current track. | |
| 1240 // Restart it instead of than skipping to the previous one. | |
| 1241 this.getMedia().currentTime = 0; | |
| 1242 } else { | |
| 1243 this.advanceTrack_(forward); | |
| 1244 } | |
| 1245 }; | |
| OLD | NEW |