OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 'use strict'; | 5 'use strict'; |
6 | 6 |
7 /** | 7 /** |
| 8 * Inverval for updating media info (in ms). |
| 9 * @type {number} |
| 10 * @const |
| 11 */ |
| 12 var MEDIA_UPDATE_INTERVAL = 250; |
| 13 |
| 14 /** |
8 * This class is the dummy class which has same interface as VideoElement. This | 15 * This class is the dummy class which has same interface as VideoElement. This |
9 * behaves like VideoElement, and is used for making Chromecast player | 16 * behaves like VideoElement, and is used for making Chromecast player |
10 * controlled instead of the true Video Element tag. | 17 * controlled instead of the true Video Element tag. |
11 * | 18 * |
| 19 * @param {chrome.cast.media.MediaInfo} mediaInfo Data of the media to play. |
| 20 * @param {chrome.cast.Session} session Session to play a video on. |
12 * @constructor | 21 * @constructor |
13 */ | 22 */ |
14 function CastVideoElement() { | 23 function CastVideoElement(mediaInfo, session) { |
15 this.duration_ = null; | 24 this.mediaInfo_ = mediaInfo; |
| 25 this.castMedia_ = null; |
| 26 this.castSession_ = session; |
16 this.currentTime_ = null; | 27 this.currentTime_ = null; |
17 this.src_ = ''; | 28 this.src_ = ''; |
18 this.volume_ = 100; | 29 this.volume_ = 100; |
| 30 this.currentMediaPlayerState_ = null; |
| 31 this.currentMediaCurrentTime_ = null; |
| 32 this.currentMediaDuration_ = null; |
| 33 this.pausing_ = false; |
| 34 |
| 35 this.onCastMediaUpdatedBound_ = this.onCastMediaUpdated_.bind(this); |
19 } | 36 } |
20 | 37 |
21 CastVideoElement.prototype = { | 38 CastVideoElement.prototype = { |
22 __proto__: cr.EventTarget.prototype, | 39 __proto__: cr.EventTarget.prototype, |
23 | 40 |
24 /** | 41 /** |
25 * Returns a parent node. This must always be null. | 42 * Returns a parent node. This must always be null. |
26 * @return {Element} | 43 * @return {Element} |
27 */ | 44 */ |
28 get parentNode() { | 45 get parentNode() { |
29 return null; | 46 return null; |
30 }, | 47 }, |
31 | 48 |
32 /** | 49 /** |
33 * The total time of the video. | 50 * The total time of the video (in sec). |
34 * @type {number} | 51 * @type {?number} |
35 */ | 52 */ |
36 get duration() { | 53 get duration() { |
37 return this.duration_; | 54 return this.currentMediaDuration_; |
38 }, | 55 }, |
39 | 56 |
40 /** | 57 /** |
41 * The current timestamp of the video. | 58 * The current timestamp of the video (in sec). |
42 * @type {number} | 59 * @type {?number} |
43 */ | 60 */ |
44 get currentTime() { | 61 get currentTime() { |
45 return this.currentTime_; | 62 return this.castMedia_ ? this.castMedia_.getEstimatedTime() : null; |
46 }, | 63 }, |
47 set currentTime(currentTime) { | 64 set currentTime(currentTime) { |
48 this.currentTime_ = currentTime; | 65 // TODO(yoshiki): Support seek. |
49 }, | 66 }, |
50 | 67 |
51 /** | 68 /** |
52 * If this video is pauses or not. | 69 * If this video is pauses or not. |
53 * @type {boolean} | 70 * @type {boolean} |
54 */ | 71 */ |
55 get paused() { | 72 get paused() { |
56 return false; | 73 if (!this.castMedia_) |
| 74 return false; |
| 75 |
| 76 return this.pausing_ || |
| 77 this.castMedia_.playerState === chrome.cast.media.PlayerState.PAUSED; |
57 }, | 78 }, |
58 | 79 |
59 /** | 80 /** |
60 * If this video is ended or not. | 81 * If this video is ended or not. |
61 * @type {boolean} | 82 * @type {boolean} |
62 */ | 83 */ |
63 get ended() { | 84 get ended() { |
64 return false; | 85 if (!this.castMedia_) |
| 86 return true; |
| 87 |
| 88 return this.castMedia_.idleReason === chrome.cast.media.IdleReason.FINISHED; |
65 }, | 89 }, |
66 | 90 |
67 /** | 91 /** |
68 * If this video is seelable or not. | 92 * If this video is seelable or not. |
69 * @type {boolean} | 93 * @type {boolean} |
70 */ | 94 */ |
71 get seekable() { | 95 get seekable() { |
| 96 // TODO(yoshiki): Support seek. |
72 return false; | 97 return false; |
73 }, | 98 }, |
74 | 99 |
75 /** | 100 /** |
76 * Value of the volume | 101 * Value of the volume |
77 * @type {number} | 102 * @type {number} |
78 */ | 103 */ |
79 get volume() { | 104 get volume() { |
80 return this.volume_; | 105 return this.castSession_.receiver.volume.muted ? |
| 106 0 : |
| 107 this.castSession_.receiver.volume.level; |
81 }, | 108 }, |
82 set volume(volume) { | 109 set volume(volume) { |
83 this.volume_ = volume; | 110 var VOLUME_EPS = 0.01; // Threshold for ignoring a small change. |
| 111 |
| 112 // Ignores < 1% change. |
| 113 if (Math.abs(this.castSession_.receiver.volume.level - volume) < VOLUME_EPS) |
| 114 return; |
| 115 |
| 116 if (this.castSession_.receiver.volume.muted) { |
| 117 if (volume < VOLUME_EPS) |
| 118 return; |
| 119 |
| 120 // Unmute before setting volume. |
| 121 this.castSession_.setReceiverMuted(false, |
| 122 function() {}, |
| 123 this.onCastCommandError_.wrap(this)); |
| 124 |
| 125 this.castSession_.setReceiverVolumeLevel(volume, |
| 126 function() {}, |
| 127 this.onCastCommandError_.wrap(this)); |
| 128 } else { |
| 129 if (volume < VOLUME_EPS) { |
| 130 this.castSession_.setReceiverMuted(true, |
| 131 function() {}, |
| 132 this.onCastCommandError_.wrap(this)); |
| 133 return; |
| 134 } |
| 135 |
| 136 this.castSession_.setReceiverVolumeLevel(volume, |
| 137 function() {}, |
| 138 this.onCastCommandError_.wrap(this)); |
| 139 } |
84 }, | 140 }, |
85 | 141 |
86 /** | 142 /** |
87 * Returns the source of the current video. | 143 * Returns the source of the current video. |
88 * @return {string} | 144 * @return {?string} |
89 */ | 145 */ |
90 get src() { | 146 get src() { |
91 return this.src_; | 147 return null; |
92 }, | 148 }, |
93 | 149 |
94 /** | 150 /** |
95 * Plays the video. | 151 * Plays the video. |
96 */ | 152 */ |
97 play: function() { | 153 play: function() { |
98 // TODO(yoshiki): Implement this. | 154 if (!this.castMedia_) { |
| 155 this.load(function() { |
| 156 this.castMedia_.play(null, |
| 157 function () {}, |
| 158 this.onCastCommandError_.wrap(this)); |
| 159 }.wrap(this)); |
| 160 return; |
| 161 } |
| 162 |
| 163 this.castMedia_.play(null, |
| 164 function () {}, |
| 165 this.onCastCommandError_.wrap(this)); |
99 }, | 166 }, |
100 | 167 |
101 /** | 168 /** |
102 * Pauses the video. | 169 * Pauses the video. |
103 */ | 170 */ |
104 pause: function() { | 171 pause: function() { |
105 // TODO(yoshiki): Implement this. | 172 if (!this.castMedia_) |
| 173 return; |
| 174 |
| 175 this.pausing_ = true; |
| 176 this.castMedia_.pause(null, |
| 177 function () { |
| 178 this.pausing_ = false; |
| 179 }.wrap(this), |
| 180 function () { |
| 181 this.pausing_ = false; |
| 182 this.onCastCommandError_(); |
| 183 }.wrap(this)); |
106 }, | 184 }, |
107 | 185 |
108 /** | 186 /** |
109 * Loads the video. | 187 * Loads the video. |
110 */ | 188 */ |
111 load: function() { | 189 load: function(opt_callback) { |
112 // TODO(yoshiki): Implement this. | 190 var request = new chrome.cast.media.LoadRequest(this.mediaInfo_); |
| 191 this.castSession_.loadMedia(request, |
| 192 function(media) { |
| 193 this.onMediaDiscovered_(media); |
| 194 if (opt_callback) |
| 195 opt_callback(); |
| 196 }.bind(this), |
| 197 this.onCastCommandError_.wrap(this)); |
| 198 }, |
| 199 |
| 200 /** |
| 201 * Unloads the video. |
| 202 * @private |
| 203 */ |
| 204 unloadMedia_: function() { |
| 205 this.castMedia_.removeUpdateListener(this.onCastMediaUpdatedBound_); |
| 206 this.castMedia_ = null; |
| 207 clearInterval(this.updateTimerId_); |
| 208 }, |
| 209 |
| 210 /** |
| 211 * This method is called periodically to update media information while the |
| 212 * media is loaded. |
| 213 * @private |
| 214 */ |
| 215 onPeriodicalUpdateTimer_: function() { |
| 216 if (!this.castMedia_) |
| 217 return; |
| 218 |
| 219 if (this.castMedia_.playerState === chrome.cast.media.PlayerState.PLAYING) |
| 220 this.onCastMediaUpdated_(true); |
| 221 }, |
| 222 |
| 223 /** |
| 224 * This method should be called when a media file is loaded. |
| 225 * @param {chrome.cast.Media} media Media object which was discovered. |
| 226 * @private |
| 227 */ |
| 228 onMediaDiscovered_: function(media) { |
| 229 if (this.castMedia_ !== null) { |
| 230 this.unloadMedia_(); |
| 231 console.info('New media is found and the old media is overridden.'); |
| 232 } |
| 233 |
| 234 this.castMedia_ = media; |
| 235 this.onCastMediaUpdated_(true); |
| 236 media.addUpdateListener(this.onCastMediaUpdatedBound_); |
| 237 this.updateTimerId_ = setInterval(this.onPeriodicalUpdateTimer_.bind(this), |
| 238 MEDIA_UPDATE_INTERVAL); |
| 239 }, |
| 240 |
| 241 /** |
| 242 * This method should be called when a media command to cast is failed. |
| 243 * @private |
| 244 */ |
| 245 onCastCommandError_: function() { |
| 246 this.unloadMedia_(); |
| 247 this.dispatchEvent(new Event('error')); |
| 248 }, |
| 249 |
| 250 /** |
| 251 * This is called when any media data is updated and by the periodical timer |
| 252 * is fired. |
| 253 * |
| 254 * @param {boolean} alive Media availability. False if it's unavailable. |
| 255 * @private |
| 256 */ |
| 257 onCastMediaUpdated_: function(alive) { |
| 258 if (!this.castMedia_) |
| 259 return; |
| 260 |
| 261 var media = this.castMedia_; |
| 262 if (this.currentMediaPlayerState_ !== media.playerState) { |
| 263 var oldPlayState = false; |
| 264 var oldState = this.currentMediaPlayerState_; |
| 265 if (oldState === chrome.cast.media.PlayerState.BUFFERING || |
| 266 oldState === chrome.cast.media.PlayerState.PLAYING) { |
| 267 oldPlayState = true; |
| 268 } |
| 269 var newPlayState = false; |
| 270 var newState = media.playerState; |
| 271 if (newState === chrome.cast.media.PlayerState.BUFFERING || |
| 272 newState === chrome.cast.media.PlayerState.PLAYING) { |
| 273 newPlayState = true; |
| 274 } |
| 275 if (!oldPlayState && newPlayState) |
| 276 this.dispatchEvent(new Event('play')); |
| 277 if (oldPlayState && !newPlayState) |
| 278 this.dispatchEvent(new Event('pause')); |
| 279 |
| 280 this.currentMediaPlayerState_ = newState; |
| 281 } |
| 282 if (this.currentMediaCurrentTime_ !== media.getEstimatedTime()) { |
| 283 this.dispatchEvent(new Event('timeupdate')); |
| 284 this.currentMediaCurrentTime_ = media.getEstimatedTime(); |
| 285 } |
| 286 |
| 287 if (this.currentMediaDuration_ !== media.media.duration) { |
| 288 this.dispatchEvent(new Event('durationchange')); |
| 289 this.currentMediaDuration_ = media.media.duration; |
| 290 } |
| 291 |
| 292 // Media is being unloaded. |
| 293 if (!alive) { |
| 294 this.unloadMedia_(); |
| 295 return; |
| 296 } |
113 }, | 297 }, |
114 }; | 298 }; |
OLD | NEW |