Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(302)

Side by Side Diff: chrome/browser/resources/file_manager/js/image_editor/media_controls.js

Issue 9583009: [File Manager] Cleanup: Moving js/css/html files to dedicated directories (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: 2011->2012 Created 8 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 /**
6 * @fileoverview MediaControls class implements media playback controls
7 * that exist outside of the audio/video HTML element.
8 */
9
10 /**
11 * @param {HTMLElement} containerElement The container for the controls.
12 * @param {function} onMediaError Function to display an error message.
13 * @constructor
14 */
15 function MediaControls(containerElement, onMediaError) {
16 this.container_ = containerElement;
17 this.document_ = this.container_.ownerDocument;
18 this.media_ = null;
19
20 this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true);
21 this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false);
22 this.onMediaDurationBound_ = this.onMediaDuration_.bind(this);
23 this.onMediaProgressBound_ = this.onMediaProgress_.bind(this);
24 this.onMediaError_ = onMediaError || function(){};
25 }
26
27 MediaControls.prototype.getMedia = function() { return this.media_ };
28
29 /**
30 * Format the time in hh:mm:ss format (omitting redundant leading zeros).
31 *
32 * @param {number} timeInSec Time in seconds.
33 * @return {string} Formatted time string.
34 */
35 MediaControls.formatTime_ = function(timeInSec) {
36 var seconds = Math.floor(timeInSec % 60);
37 var minutes = Math.floor((timeInSec / 60) % 60);
38 var hours = Math.floor(timeInSec / 60 / 60);
39 var result = '';
40 if (hours) result += hours + ':';
41 if (hours && (minutes < 10)) result += '0';
42 result += minutes + ':';
43 if (seconds < 10) result += '0';
44 result += seconds;
45 return result;
46 };
47
48 /**
49 * Create a custom control.
50 *
51 * @param {string} className
52 * @param {HTMLElement=} opt_parent Parent element or container if undefined.
53 * @return {HTMLElement}
54 */
55 MediaControls.prototype.createControl = function(className, opt_parent) {
56 var parent = opt_parent || this.container_;
57 var control = this.document_.createElement('div');
58 control.className = className;
59 parent.appendChild(control);
60 return control;
61 };
62
63 /**
64 * Create a custom button.
65 *
66 * @param {string} className
67 * @param {function(Event)} handler
68 * @param {HTMLElement=} opt_parent Parent element or container if undefined.
69 * @param {Boolean} opt_toggle True if the button has toggle state.
70 * @return {HTMLElement}
71 */
72 MediaControls.prototype.createButton = function(
73 className, handler, opt_parent, opt_toggle) {
74 var button = this.createControl(className, opt_parent);
75 button.classList.add('media-button');
76 button.addEventListener('click', handler);
77
78 var numStates = opt_toggle ? 2 : 1;
79 for (var state = 0; state != numStates; state++) {
80 var stateClass = 'state' + state;
81 this.createControl('normal ' + stateClass, button);
82 this.createControl('hover ' + stateClass, button);
83 this.createControl('active ' + stateClass, button);
84 }
85 this.createControl('disabled', button);
86
87 button.setAttribute('state', 0);
88 button.addEventListener('click', handler);
89 return button;
90 };
91
92 MediaControls.prototype.enableControls_ = function(selector, on) {
93 var controls = this.container_.querySelectorAll(selector);
94 for (var i = 0; i != controls.length; i++) {
95 var classList = controls[i].classList;
96 if (on)
97 classList.remove('disabled');
98 else
99 classList.add('disabled');
100 }
101 };
102
103 /*
104 * Playback control.
105 */
106
107 MediaControls.prototype.play = function() {
108 this.media_.play();
109 };
110
111 MediaControls.prototype.pause = function() {
112 this.media_.pause();
113 };
114
115 MediaControls.prototype.isPlaying = function() {
116 return !this.media_.paused && !this.media_.ended;
117 };
118
119 MediaControls.prototype.togglePlayState = function() {
120 if (this.isPlaying())
121 this.pause();
122 else
123 this.play();
124 };
125
126 MediaControls.prototype.initPlayButton = function(opt_parent) {
127 this.playButton_ = this.createButton('play media-control',
128 this.togglePlayState.bind(this), opt_parent, true /* toggle */);
129 };
130
131 /*
132 * Time controls
133 */
134
135 // The default range of 100 is too coarse for the media progress slider.
136 // 1000 should be enough as the entire media controls area is never longer
137 // than 800px.
138 MediaControls.PROGRESS_RANGE = 1000;
139
140 MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) {
141 var timeControls = this.createControl('time-controls', opt_parent);
142
143 var sliderConstructor =
144 opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider;
145
146 this.progressSlider_ = new sliderConstructor(
147 this.createControl('progress media-control', timeControls),
148 0, /* value */
149 MediaControls.PROGRESS_RANGE,
150 this.onProgressChange_.bind(this),
151 this.onProgressDrag_.bind(this));
152
153 var timeBox = this.createControl('time media-control', timeControls);
154
155 this.duration_ = this.createControl('duration', timeBox);
156 // Set the initial width to the minimum to reduce the flicker.
157 this.duration_.textContent = MediaControls.formatTime_(0);
158
159 this.currentTime_ = this.createControl('current', timeBox);
160 };
161
162 MediaControls.prototype.displayProgress_ = function(current, duration) {
163 var ratio = current / duration;
164 this.progressSlider_.setValue(ratio);
165 this.currentTime_.textContent = MediaControls.formatTime_(current);
166 };
167
168 MediaControls.prototype.onProgressChange_ = function(value) {
169 if (!this.media_.seekable || !this.media_.duration) {
170 console.error("Inconsistent media state");
171 return;
172 }
173
174 var current = this.media_.duration * value;
175 this.media_.currentTime = current;
176 this.currentTime_.textContent = MediaControls.formatTime_(current);
177 };
178
179 MediaControls.prototype.onProgressDrag_ = function(on) {
180 if (on) {
181 this.resumeAfterDrag_ = this.isPlaying();
182 this.media_.pause();
183 } else {
184 if (this.resumeAfterDrag_) {
185 if (this.media_.ended)
186 this.onMediaPlay_(false);
187 else
188 this.media_.play();
189 }
190 }
191 };
192
193 /*
194 * Volume controls
195 */
196
197 MediaControls.prototype.initVolumeControls = function(opt_parent) {
198 var volumeControls = this.createControl('volume-controls', opt_parent);
199
200 this.soundButton_ = this.createButton('sound media-control',
201 this.onSoundButtonClick_.bind(this), volumeControls);
202 this.soundButton_.setAttribute('level', 3); // max level.
203
204 this.volume_ = new MediaControls.AnimatedSlider(
205 this.createControl('volume media-control', volumeControls),
206 1, /* value */
207 100 /* range */,
208 this.onVolumeChange_.bind(this),
209 this.onVolumeDrag_.bind(this));
210 };
211
212 MediaControls.prototype.onSoundButtonClick_ = function() {
213 if (this.media_.volume == 0) {
214 this.volume_.setValue(this.savedVolume_ || 1);
215 } else {
216 this.savedVolume_ = this.media_.volume;
217 this.volume_.setValue(0);
218 }
219 this.onVolumeChange_(this.volume_.getValue());
220 };
221
222 MediaControls.getVolumeLevel_ = function(value) {
223 if (value == 0) return 0;
224 if (value <= 1/3) return 1;
225 if (value <= 2/3) return 2;
226 return 3;
227 };
228
229 MediaControls.prototype.onVolumeChange_ = function(value) {
230 this.media_.volume = value;
231 this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value));
232 };
233
234 MediaControls.prototype.onVolumeDrag_ = function(on) {
235 if (on && (this.media_.volume != 0)) {
236 this.savedVolume_ = this.media_.volume;
237 }
238 };
239
240 /*
241 * Media event handlers.
242 */
243
244 /**
245 * Attach a media element.
246 *
247 * @param {HTMLMediaElement} mediaElement The media element to control.
248 */
249 MediaControls.prototype.attachMedia = function(mediaElement) {
250 this.media_ = mediaElement;
251
252 this.media_.addEventListener('play', this.onMediaPlayBound_);
253 this.media_.addEventListener('pause', this.onMediaPauseBound_);
254 this.media_.addEventListener('durationchange', this.onMediaDurationBound_);
255 this.media_.addEventListener('timeupdate', this.onMediaProgressBound_);
256 this.media_.addEventListener('error', this.onMediaError_);
257
258 // Reset the UI.
259 this.enableControls_('.media-control', false);
260 this.playButton_.setAttribute('state', 0);
261 this.displayProgress_(0, 1);
262 if (this.volume_) {
263 /* Copy the user selected volume to the new media element. */
264 this.media_.volume = this.volume_.getValue();
265 }
266 };
267
268 /**
269 * Detach media event handlers.
270 */
271 MediaControls.prototype.detachMedia = function() {
272 if (!this.media_)
273 return;
274
275 this.media_.removeEventListener('play', this.onMediaPlayBound_);
276 this.media_.removeEventListener('pause', this.onMediaPauseBound_);
277 this.media_.removeEventListener('durationchange', this.onMediaDurationBound_);
278 this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_);
279 this.media_.removeEventListener('error', this.onMediaError_);
280
281 this.media_ = null;
282 };
283
284 MediaControls.prototype.onMediaPlay_ = function(playing) {
285 if (this.progressSlider_.isDragging())
286 return;
287
288 this.playButton_.setAttribute('state', playing ? '1' : '0');
289 };
290
291 MediaControls.prototype.onMediaDuration_ = function() {
292 if (!this.media_.duration)
293 return;
294
295 this.enableControls_('.media-control', true);
296
297 var sliderContainer = this.progressSlider_.getContainer();
298 if (this.media_.seekable)
299 sliderContainer.classList.remove('readonly');
300 else
301 sliderContainer.classList.add('readonly');
302
303 var valueToString = function(value) {
304 return MediaControls.formatTime_(this.media_.duration * value);
305 }.bind(this);
306
307 this.duration_.textContent = valueToString(1);
308
309 if (this.progressSlider_.setValueToStringFunction)
310 this.progressSlider_.setValueToStringFunction(valueToString);
311 };
312
313 MediaControls.prototype.onMediaProgress_ = function(e) {
314 if (!this.media_.duration)
315 return;
316
317 var current = this.media_.currentTime;
318 var duration = this.media_.duration;
319
320 if (this.progressSlider_.isDragging())
321 return;
322
323 this.displayProgress_(current, duration);
324
325 if (current == duration) {
326 this.onMediaComplete();
327 }
328 };
329
330 MediaControls.prototype.onMediaComplete = function() {};
331
332 /**
333 * Create a customized slider control.
334 *
335 * @param {HTMLElement} container The containing div element.
336 * @param {number} value Initial value [0..1].
337 * @param {number} range Number of distinct slider positions to be supported.
338 * @param {function(number)} onChange
339 * @param {function(boolean)} onDrag
340 * @constructor
341 */
342
343 MediaControls.Slider = function(container, value, range, onChange, onDrag) {
344 this.container_ = container;
345 this.onChange_ = onChange;
346 this.onDrag_ = onDrag;
347
348 var document = this.container_.ownerDocument;
349
350 this.container_.classList.add('custom-slider');
351
352 this.input_ = document.createElement('input');
353 this.input_.type = 'range';
354 this.input_.min = 0;
355 this.input_.max = range;
356 this.input_.value = value * range;
357 this.container_.appendChild(this.input_);
358
359 this.input_.addEventListener(
360 'change', this.onInputChange_.bind(this));
361 this.input_.addEventListener(
362 'mousedown', this.onInputDrag_.bind(this, true));
363 this.input_.addEventListener(
364 'mouseup', this.onInputDrag_.bind(this, false));
365
366 this.bar_ = document.createElement('div');
367 this.bar_.className = 'bar';
368 this.container_.appendChild(this.bar_);
369
370 this.filled_ = document.createElement('div');
371 this.filled_.className = 'filled';
372 this.bar_.appendChild(this.filled_);
373
374 var leftCap = document.createElement('div');
375 leftCap.className = 'cap left';
376 this.bar_.appendChild(leftCap);
377
378 var rightCap = document.createElement('div');
379 rightCap.className = 'cap right';
380 this.bar_.appendChild(rightCap);
381
382 this.value_ = value;
383 this.setFilled_(value);
384 };
385
386 /**
387 * @return {HTMLElement} The container element.
388 */
389 MediaControls.Slider.prototype.getContainer = function() {
390 return this.container_;
391 };
392
393 /**
394 * @return {HTMLElement} The standard input element.
395 */
396 MediaControls.Slider.prototype.getInput_ = function() {
397 return this.input_;
398 };
399
400 /**
401 * @return {HTMLElement} The slider bar element.
402 */
403 MediaControls.Slider.prototype.getBar = function() {
404 return this.bar_;
405 };
406
407 /**
408 * @return {number} [0..1] The current value.
409 */
410 MediaControls.Slider.prototype.getValue = function() {
411 return this.value_;
412 };
413
414 /**
415 * @param {number} value [0..1]
416 */
417 MediaControls.Slider.prototype.setValue = function(value) {
418 this.value_ = value;
419 this.setValueToUI_(value);
420 };
421
422 /**
423 * Fill the given proportion the slider bar (from the left).
424 *
425 * @param {number} proportion [0..1]
426 */
427 MediaControls.Slider.prototype.setFilled_ = function(proportion) {
428 this.filled_.style.width = proportion * 100 + '%';
429 };
430
431 /**
432 * Get the value from the input element.
433 *
434 * @param {number} proportion [0..1]
435 */
436 MediaControls.Slider.prototype.getValueFromUI_ = function() {
437 return this.input_.value / this.input_.max;
438 };
439
440 /**
441 * Update the UI with the current value.
442 *
443 * @param {number} value [0..1]
444 */
445 MediaControls.Slider.prototype.setValueToUI_ = function(value) {
446 this.input_.value = value * this.input_.max;
447 this.setFilled_(value);
448 };
449
450 /**
451 * Compute the proportion in which the given position divides the slider bar.
452 *
453 * @param {number} position in pixels.
454 * @return {number} [0..1] proportion.
455 */
456 MediaControls.Slider.prototype.getProportion = function(position) {
457 var rect = this.bar_.getBoundingClientRect();
458 return Math.max(0, Math.min(1, (position - rect.left) / rect.width));
459 };
460
461 MediaControls.Slider.prototype.onInputChange_ = function() {
462 this.value_ = this.getValueFromUI_();
463 this.setFilled_(this.value_);
464 this.onChange_(this.value_);
465 };
466
467 MediaControls.Slider.prototype.isDragging = function() {
468 return this.isDragging_;
469 };
470
471 MediaControls.Slider.prototype.onInputDrag_ = function(on, event) {
472 this.isDragging_ = on;
473 this.onDrag_(on);
474 };
475
476 /**
477 * Create a customized slider with animated thumb movement.
478 *
479 * @param {HTMLElement} container The containing div element.
480 * @param {number} value Initial value [0..1].
481 * @param {number} range Number of distinct slider positions to be supported.
482 * @param {function(number)} onChange
483 * @param {function(boolean)} onDrag
484 */
485 MediaControls.AnimatedSlider = function(
486 container, value, range, onChange, onDrag, formatFunction) {
487 MediaControls.Slider.apply(this, arguments);
488 };
489
490 MediaControls.AnimatedSlider.prototype = {
491 __proto__: MediaControls.Slider.prototype
492 };
493
494 MediaControls.AnimatedSlider.STEPS = 10;
495 MediaControls.AnimatedSlider.DURATION = 100;
496
497 MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
498 if (this.animationInterval_) {
499 clearInterval(this.animationInterval_);
500 }
501 var oldValue = this.getValueFromUI_();
502 var step = 0;
503 this.animationInterval_ = setInterval(function() {
504 step++;
505 var currentValue = oldValue +
506 (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS);
507 MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue);
508 if (step == MediaControls.AnimatedSlider.STEPS) {
509 clearInterval(this.animationInterval_);
510 }
511 }.bind(this),
512 MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS);
513 };
514
515 /**
516 * Create a customized slider with a precise time feedback.
517 *
518 * The time value is shown above the slider bar at the mouse position.
519 *
520 * @param {HTMLElement} container The containing div element.
521 * @param {number} value Initial value [0..1].
522 * @param {number} range Number of distinct slider positions to be supported.
523 * @param {function(number)} onChange
524 * @param {function(boolean)} onDrag
525 */
526 MediaControls.PreciseSlider = function(
527 container, value, range, onChange, onDrag, formatFunction) {
528 MediaControls.Slider.apply(this, arguments);
529
530 var doc = this.container_.ownerDocument;
531
532 /**
533 * @type {function(number):string}
534 */
535 this.valueToString_ = null;
536
537 this.seekMark_ = doc.createElement('div');
538 this.seekMark_.className = 'seek-mark';
539 this.getBar().appendChild(this.seekMark_);
540
541 this.seekLabel_ = doc.createElement('div');
542 this.seekLabel_.className = 'seek-label';
543 this.seekMark_.appendChild(this.seekLabel_);
544
545 this.getContainer().addEventListener(
546 'mousemove', this.onMouseMove_.bind(this));
547 this.getContainer().addEventListener(
548 'mouseout', this.onMouseOut_.bind(this));
549 };
550
551 MediaControls.PreciseSlider.prototype = {
552 __proto__: MediaControls.Slider.prototype
553 };
554
555 MediaControls.PreciseSlider.SHOW_DELAY = 200;
556 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
557 MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
558 MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
559
560 MediaControls.PreciseSlider.prototype.setValueToStringFunction =
561 function(func) {
562 this.valueToString_ = func;
563
564 /* It is not completely accurate to assume that the max value corresponds
565 to the longest string, but generous CSS padding will compensate for that. */
566 var labelWidth = this.valueToString_(1).length / 2 + 1;
567 this.seekLabel_.style.width = labelWidth + 'em';
568 this.seekLabel_.style.marginLeft = -labelWidth/2 + 'em';
569 };
570
571 /**
572 * Show the time above the slider.
573 *
574 * @param {number} ratio [0..1] The proportion of the duration.
575 * @param {number} timeout Timeout in ms after which the label should be hidden.
576 * MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call.
577 */
578 MediaControls.PreciseSlider.prototype.showSeekMark_ =
579 function(ratio, timeout) {
580 // Do not update the seek mark for the first 500ms after the drag is finished.
581 if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now()))
582 return;
583
584 this.seekMark_.style.left = ratio * 100 + '%';
585
586 if (ratio < this.getValue()) {
587 this.seekMark_.classList.remove('inverted');
588 } else {
589 this.seekMark_.classList.add('inverted');
590 }
591 this.seekLabel_.textContent = this.valueToString_(ratio);
592
593 this.seekMark_.classList.add('visible');
594
595 if (this.seekMarkTimer_) {
596 clearTimeout(this.seekMarkTimer_);
597 this.seekMarkTimer_ = null;
598 }
599 if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
600 this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
601 }
602 };
603
604 MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() {
605 this.seekMarkTimer_ = null;
606 this.seekMark_.classList.remove('visible');
607 };
608
609 MediaControls.PreciseSlider.prototype.onMouseMove_ = function(event) {
610 this.latestSeekRatio_ = this.getProportion(event.clientX);
611
612 var self = this;
613 function showMark() {
614 if (!self.isDragging()) {
615 self.showSeekMark_(self.latestSeekRatio_,
616 MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
617 }
618 }
619
620 if (this.seekMark_.classList.contains('visible')) {
621 showMark();
622 } else if (!this.seekMarkTimer_) {
623 this.seekMarkTimer_ =
624 setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
625 }
626 };
627
628 MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
629 for (var element = e.relatedTarget; element; element = element.parentNode) {
630 if (element == this.getContainer())
631 return;
632 }
633 if (this.seekMarkTimer_) {
634 clearTimeout(this.seekMarkTimer_);
635 this.seekMarkTimer_ = null;
636 }
637 this.hideSeekMark_();
638 };
639
640 MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
641 MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
642 if (this.isDragging()) {
643 this.showSeekMark_(
644 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
645 }
646 };
647
648 MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on, event) {
649 MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
650
651 if (on) {
652 // Dragging started, align the seek mark with the thumb position.
653 this.showSeekMark_(
654 this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
655 } else {
656 // Just finished dragging.
657 // Show the label for the last time with a shorter timeout.
658 this.showSeekMark_(
659 this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
660 this.latestMouseUpTime_ = Date.now();
661 }
662 };
663
664 /**
665 * Create video controls.
666 *
667 * @param {HTMLElement} containerElement The container for the controls.
668 * @param {function} onMediaError Function to display an error message.
669 * @param {function} opt_fullScreenToggle Function to toggle fullscreen mode.
670 * @param {HTMLElement} opt_stateIconParent The parent for the icon that
671 * gives visual feedback when the playback state changes.
672 * @constructor
673 */
674 function VideoControls(containerElement, onMediaError,
675 opt_fullScreenToggle, opt_stateIconParent) {
676 MediaControls.call(this, containerElement, onMediaError);
677
678 this.container_.classList.add('video-controls');
679
680 this.initPlayButton();
681
682 this.initTimeControls(true /* show seek mark */);
683
684 this.initVolumeControls();
685
686 if (opt_fullScreenToggle) {
687 this.fullscreenButton_ =
688 this.createButton('fullscreen', opt_fullScreenToggle);
689 }
690
691 if (opt_stateIconParent) {
692 this.stateIcon_ = this.createControl(
693 'playback-state-icon', opt_stateIconParent);
694 }
695
696 this.resumePositions_ = new TimeLimitedMap(
697 'VideoResumePosition',
698 VideoControls.RESUME_POSITIONS_CAPACITY,
699 VideoControls.RESUME_POSITION_LIFETIME);
700 }
701
702 VideoControls.RESUME_POSITIONS_CAPACITY = 100;
703 VideoControls.RESUME_POSITION_LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days.
704 VideoControls.RESUME_MARGIN = 0.03;
705 VideoControls.RESUME_THRESHOLD = 5 * 60; // No resume for videos < 5 min.
706 VideoControls.RESUME_REWIND = 5; // Rewind 5 seconds back when resuming.
707
708 VideoControls.prototype = { __proto__: MediaControls.prototype };
709
710 VideoControls.prototype.onMediaComplete = function() {
711 this.onMediaPlay_(false); // Just update the UI.
712 this.savePosition(); // This will effectively forget the position.
713 };
714
715 VideoControls.prototype.togglePlayStateWithFeedback = function(e) {
716 if (!this.getMedia().duration)
717 return;
718
719 this.togglePlayState();
720
721 var self = this;
722
723 var delay = function(action, opt_timeout) {
724 if (self.statusIconTimer_) {
725 clearTimeout(self.statusIconTimer_);
726 }
727 self.statusIconTimer_ = setTimeout(function() {
728 self.statusIconTimer_ = null;
729 action();
730 }, opt_timeout || 0);
731 };
732
733 function hideStatusIcon() {
734 self.stateIcon_.removeAttribute('visible');
735 self.stateIcon_.removeAttribute('state');
736 }
737
738 hideStatusIcon();
739
740 // The delays are required to trigger the layout between attribute changes.
741 // Otherwise everything just goes to the final state without the animation.
742 delay(function() {
743 self.stateIcon_.setAttribute('visible', true);
744 delay(function(){
745 self.stateIcon_.setAttribute(
746 'state', self.isPlaying() ? 'play' : 'pause');
747 delay(hideStatusIcon, 1000); /* Twice the animation duration. */
748 });
749 });
750 };
751
752 VideoControls.prototype.onMediaDuration_ = function() {
753 MediaControls.prototype.onMediaDuration_.apply(this, arguments);
754 if (this.media_.duration &&
755 this.media_.duration >= VideoControls.RESUME_THRESHOLD &&
756 this.media_.seekable) {
757 var position = this.resumePositions_.getValue(this.media_.src);
758 if (position) {
759 this.media_.currentTime = position;
760 }
761 }
762 };
763
764 VideoControls.prototype.togglePlayState = function(e) {
765 if (this.isPlaying()) {
766 // User gave the Pause command.
767 this.savePosition();
768 }
769 MediaControls.prototype.togglePlayState.apply(this, arguments);
770 };
771
772 VideoControls.prototype.savePosition = function() {
773 if (!this.media_.duration ||
774 this.media_.duration_ < VideoControls.RESUME_THRESHOLD)
775 return;
776
777 var ratio = this.media_.currentTime / this.media_.duration;
778 if (ratio < VideoControls.RESUME_MARGIN ||
779 ratio > (1 - VideoControls.RESUME_MARGIN)) {
780 // We are too close to the beginning or the end.
781 // Remove the resume position so that next time we start from the beginning.
782 this.resumePositions_.removeValue(this.media_.src);
783 } else {
784 this.resumePositions_.setValue(this.media_.src, Math.floor(Math.max(0,
785 this.media_.currentTime - VideoControls.RESUME_REWIND)));
786 }
787 };
788
789 /**
790 * TimeLimitedMap is persistent timestamped key-value storage backed by
791 * HTML5 local storage.
792 *
793 * It is not designed for frequent access. In order to avoid costly
794 * localStorage iteration all data is kept in a single localStorage item.
795 * There is no in-memory caching, so concurrent access is OK.
796 *
797 * @param {string} localStorageKey A key in the local storage.
798 * @param {number} capacity Maximim number of items. If exceeded, oldest items
799 * are removed.
800 * @param {number} lifetime Maximim time to keep an item (in milliseconds).
801 */
802 function TimeLimitedMap(localStorageKey, capacity, lifetime) {
803 this.localStorageKey_ = localStorageKey;
804 this.capacity_ = capacity;
805 this.lifetime_ = lifetime;
806 }
807
808 /**
809 * @param {string} key
810 * @return {string} value
811 */
812 TimeLimitedMap.prototype.getValue = function(key) {
813 var map = this.read_();
814 var entry = map[key];
815 return entry && entry.value;
816 };
817
818 /**
819 * @param {string} key
820 * @param {string} value
821 */
822 TimeLimitedMap.prototype.setValue = function(key, value) {
823 var map = this.read_();
824 map[key] = { value: value, timestamp: Date.now() };
825 this.cleanup_(map);
826 this.write_(map);
827 };
828
829 /**
830 * @param {string} key
831 */
832 TimeLimitedMap.prototype.removeValue = function(key) {
833 var map = this.read_();
834 if (!(key in map))
835 return; // Nothing to do.
836
837 delete map[key];
838 this.cleanup_(map);
839 this.write_(map);
840 };
841
842 /**
843 * @return {Object} A map of timestamped key-value pairs.
844 */
845 TimeLimitedMap.prototype.read_ = function() {
846 var json = localStorage[this.localStorageKey_];
847 if (json) {
848 try {
849 return JSON.parse(json);
850 } catch(e) {
851 // The localStorage item somehow got messed up, start fresh.
852 }
853 }
854 return {};
855 };
856
857 /**
858 * @param {Object} map A map of timestamped key-value pairs.
859 */
860 TimeLimitedMap.prototype.write_ = function(map) {
861 localStorage[this.localStorageKey_] = JSON.stringify(map);
862 };
863
864 /**
865 * Remove over-capacity and obsolete items.
866 *
867 * @param {Object} map A map of timestamped key-value pairs.
868 */
869 TimeLimitedMap.prototype.cleanup_ = function(map) {
870 // Sort keys by ascending timestamps.
871 var keys = [];
872 for (var key in map) {
873 keys.push(key);
874 }
875 keys.sort(function(a, b) { return map[a].timestamp > map[b].timestamp });
876
877 var cutoff = Date.now() - this.lifetime_;
878
879 var obsolete = 0;
880 while (obsolete < keys.length &&
881 map[keys[obsolete]].timestamp < cutoff) {
882 obsolete++;
883 }
884
885 var overCapacity = Math.max(0, keys.length - this.capacity_);
886
887 var itemsToDelete = Math.max(obsolete, overCapacity);
888 for (var i = 0; i != itemsToDelete; i++) {
889 delete map[keys[i]];
890 }
891 };
892
893
894 /**
895 * Create audio controls.
896 *
897 * @param {HTMLElement} container
898 * @param {function(boolean)} advanceTrack Parameter: true=forward.
899 * @constructor
900 */
901 function AudioControls(container, advanceTrack) {
902 MediaControls.call(this, container, null /* onError */);
903
904 this.container_.classList.add('audio-controls');
905
906 this.advanceTrack_ = advanceTrack;
907
908 this.initPlayButton();
909 this.initTimeControls(false /* no seek mark */);
910 /* No volume controls */
911 this.createButton('previous', this.onAdvanceClick_.bind(this, false));
912 this.createButton('next', this.onAdvanceClick_.bind(this, true));
913 }
914
915 AudioControls.prototype = { __proto__: MediaControls.prototype };
916
917 AudioControls.prototype.onMediaComplete = function() {
918 this.advanceTrack_(true);
919 };
920
921 AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds.
922
923 AudioControls.prototype.onAdvanceClick_ = function(forward) {
924 if (!forward &&
925 (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) {
926 // We are far enough from the beginning of the current track.
927 // Restart it instead of than skipping to the previous one.
928 this.getMedia().currentTime = 0;
929 } else {
930 this.advanceTrack_(forward);
931 }
932 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698