OLD | NEW |
(Empty) | |
| 1 <link rel="import" href="../polymer/polymer.html"> |
| 2 <link rel="import" href="google-cast-sender-api.html"> |
| 3 |
| 4 <!-- TODO(tschaeff): |
| 5 - ratechange to change playbackspeed (if even possible) |
| 6 --> |
| 7 <script> |
| 8 (function() { |
| 9 // Set static variable for the timeupdate delay. |
| 10 var _GOOGLE_CASTABLE_VIDEO_TIMEUPDATE_DELAY = 250; |
| 11 /** |
| 12 The `google-castable-video` element enables your HTML5 videos to be casted to an
y Chromecast. |
| 13 |
| 14 It behaves exactly like an HTML5 video element except for some added methods and
events. |
| 15 |
| 16 Instead of listening for the video element's `timeupdate` event please listen fo
r the `google-castable-video-timeupdate` event. This event is fired if the video
is playing locally and on the Chromecast device. |
| 17 |
| 18 ##### Example |
| 19 |
| 20 <video is="google-castable-video"> |
| 21 <source src="video.mp4" type="video/mp4"> |
| 22 </video> |
| 23 |
| 24 @demo |
| 25 */ |
| 26 Polymer({ |
| 27 |
| 28 is: 'google-castable-video', |
| 29 |
| 30 extends: 'video', |
| 31 |
| 32 properties: { |
| 33 /** |
| 34 * The appId that references which receiver the Chromecast will load. |
| 35 * |
| 36 * @default chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID |
| 37 */ |
| 38 appId: { |
| 39 type: String, |
| 40 value: null |
| 41 } |
| 42 }, |
| 43 |
| 44 listeners: { |
| 45 'seeked': '_onSeeked', |
| 46 'volumechange': '_onVolumechange', |
| 47 'timeupdate': '_onTimeupdate' |
| 48 }, |
| 49 |
| 50 /** |
| 51 * The real paused state for local and cast playback. |
| 52 * |
| 53 * @property bothPaused |
| 54 * @type bool |
| 55 * @default true |
| 56 */ |
| 57 _bothPaused: true, |
| 58 get bothPaused() { |
| 59 return this._bothPaused; |
| 60 }, |
| 61 |
| 62 /** |
| 63 * Sets or returns the current playback position (in seconds). |
| 64 * Since the local video is paused when the video is playing on the Chrome
cast device |
| 65 * the objects currentTime property doesn't represent the actual currentTi
me of the video |
| 66 * playing on the Chromecast device. To always get the actual position ple
ase use bothCurrentTime. |
| 67 * |
| 68 * @property bothCurrentTime |
| 69 * @type number |
| 70 * @default 0 |
| 71 */ |
| 72 get bothCurrentTime() { |
| 73 if (this._casting && this._castMedia) { |
| 74 return this._castMedia.getEstimatedTime(); |
| 75 } else { |
| 76 return this.currentTime; |
| 77 } |
| 78 }, |
| 79 |
| 80 set bothCurrentTime(val) { |
| 81 return this.currentTime = val; |
| 82 }, |
| 83 |
| 84 /** |
| 85 * The mode state depending on whether the video is playing locally or on
the cast device. |
| 86 * |
| 87 * @property casting |
| 88 * @type bool |
| 89 * @default false |
| 90 */ |
| 91 _casting: false, |
| 92 get casting() { |
| 93 return this._casting; |
| 94 }, |
| 95 |
| 96 /** |
| 97 * Returns if any Chromecast is available. |
| 98 * |
| 99 * @property receiverAvailable |
| 100 * @type bool |
| 101 * @default false |
| 102 */ |
| 103 _receiverAvailable: false, |
| 104 get receiverAvailable() { |
| 105 return this._receiverAvailable; |
| 106 }, |
| 107 |
| 108 /** |
| 109 * The `chrome.cast.Media` object. |
| 110 * |
| 111 * @property castMedia |
| 112 * @type chrome.cast.Media |
| 113 * @default null |
| 114 */ |
| 115 _castMedia: null, |
| 116 get castMedia() { |
| 117 return this._castMedia; |
| 118 }, |
| 119 |
| 120 /** |
| 121 * The `chrome.cast.Session` object. |
| 122 * |
| 123 * @property session |
| 124 * @type chrome.cast.Session |
| 125 * @default null |
| 126 */ |
| 127 _session: null, |
| 128 get session() { |
| 129 return this._session; |
| 130 }, |
| 131 |
| 132 ready: function() { |
| 133 // Initialize the cast api. |
| 134 window['__onGCastApiAvailable'] = function(loaded, errorInfo) { |
| 135 if (loaded) { |
| 136 this._initializeCastApi(); |
| 137 } else { |
| 138 this._triggerError('INITIALIZE_ERROR'); |
| 139 } |
| 140 }.bind(this); |
| 141 }, |
| 142 |
| 143 // Called internally when the cast sender api has been loaded. |
| 144 _initializeCastApi: function() { |
| 145 if (this.appId === null || typeof this.appId === "undefined") { |
| 146 this.appId = chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID; |
| 147 // TODO ... process for selecting styled media receiver |
| 148 } |
| 149 var sessionRequest = new chrome.cast.SessionRequest(this.appId); |
| 150 var apiConfig = new chrome.cast.ApiConfig(sessionRequest, |
| 151 function(e){ |
| 152 // The sessionListener. |
| 153 this._triggerCasting(true); |
| 154 this._session = e; |
| 155 if (this._session.media.length) { |
| 156 this._onMediaDiscovered.call(this, 'onRequestSessionSuccess', this
._session.media[0]); |
| 157 } |
| 158 // Bind the session update listener. |
| 159 this._session.addUpdateListener(this._sessionUpdateListener.bind(thi
s)); |
| 160 // Set interval for cast timeupdate. |
| 161 this._timeupdateInterval = setInterval(function(){ |
| 162 if (this._castMedia && this._castMedia.playerState === 'PLAYING')
{ |
| 163 this._triggerTimeupdate(this._castMedia.getEstimatedTime()); |
| 164 this._bothPaused = false; |
| 165 } else { |
| 166 this._bothPaused = true; |
| 167 } |
| 168 }.bind(this), _GOOGLE_CASTABLE_VIDEO_TIMEUPDATE_DELAY); |
| 169 // Start playing on cast if playing locally. |
| 170 if (!this.paused) { |
| 171 this.play(); |
| 172 this.pause(false); |
| 173 } |
| 174 }.bind(this), |
| 175 function(e){ |
| 176 // The receiverListener |
| 177 if (e === chrome.cast.ReceiverAvailability.AVAILABLE) { |
| 178 this._triggerAvailability(true); |
| 179 } else { |
| 180 this._triggerAvailability(false); |
| 181 } |
| 182 }.bind(this)); |
| 183 chrome.cast.initialize(apiConfig, function(){ |
| 184 // The onInitSuccess method. |
| 185 /** |
| 186 * The `google-castable-video-initialized` event is fired when |
| 187 * the cast client API has been initialized. |
| 188 * |
| 189 * @event google-castable-video-initialized |
| 190 */ |
| 191 this.fire('google-castable-video-initialized'); |
| 192 }.bind(this), function(){ |
| 193 this._triggerError('INITIALIZE_ERROR'); |
| 194 }); |
| 195 }, |
| 196 |
| 197 /** |
| 198 * Call this when the user clicks the cast icon. |
| 199 * Opens the cast extension to create a session with the selected receiver. |
| 200 * |
| 201 * @method launchSessionManager |
| 202 */ |
| 203 launchSessionManager: function(){ |
| 204 if (this._receiverAvailable) { |
| 205 // Create the session with the receiver. |
| 206 chrome.cast.requestSession(function(e){ |
| 207 // The onRequestSessionSuccess handler gets executed when we're conn
ected. |
| 208 this._triggerCasting(true); |
| 209 this._session = e; |
| 210 this._session.addUpdateListener(this._sessionUpdateListener.bind(thi
s)); |
| 211 // If video is playing start playing on chromecast at same position. |
| 212 if (!this.paused) { |
| 213 this.play(); |
| 214 this.pause(false); |
| 215 } |
| 216 // Set interval for cast timeupdate. |
| 217 this._timeupdateInterval = setInterval(function(){ |
| 218 if (this._castMedia && this._castMedia.playerState === 'PLAYING')
{ |
| 219 this._triggerTimeupdate(this._castMedia.getEstimatedTime()); |
| 220 this._bothPaused = false; |
| 221 } else { |
| 222 this._bothPaused = true; |
| 223 } |
| 224 }.bind(this), _GOOGLE_CASTABLE_VIDEO_TIMEUPDATE_DELAY); |
| 225 }.bind(this)); |
| 226 } |
| 227 }, |
| 228 |
| 229 // Internal method gets called when the cast session status changes. |
| 230 _sessionUpdateListener: function(isAlive){ |
| 231 if (!isAlive) { |
| 232 this._triggerCasting(false); |
| 233 this._synchronizeMedia(true); |
| 234 // If video was playing on the receiver start playing locally. |
| 235 if (this._castMedia.playerState === 'PLAYING') { |
| 236 this.play(); |
| 237 } |
| 238 this._castMedia = null; |
| 239 this._session = null; |
| 240 // The session died so remove the timeupdate interval. |
| 241 clearInterval(this._timeupdateInterval); |
| 242 } |
| 243 }, |
| 244 |
| 245 // Internal method gets called when media was set through `launchsession` |
| 246 // or was already playing on cast device. |
| 247 _onMediaDiscovered: function(how, media) { |
| 248 this._castMedia = media; |
| 249 if (how === 'loadMedia') { |
| 250 this._synchronizeMedia(false); |
| 251 } |
| 252 }, |
| 253 |
| 254 // Internal method to synchronize the media objects. |
| 255 _synchronizeMedia: function(castMaster){ |
| 256 if (castMaster) { |
| 257 var position = this._castMedia.getEstimatedTime(); |
| 258 this.currentTime = position; |
| 259 } else { |
| 260 var position = this.currentTime; |
| 261 var req = new chrome.cast.media.SeekRequest(); |
| 262 req.currentTime = position; |
| 263 this._castMedia.seek(req); |
| 264 } |
| 265 }, |
| 266 /** |
| 267 * Call the `play` method from your controls. |
| 268 * |
| 269 * @method play |
| 270 */ |
| 271 play: function(cast){ |
| 272 if ((cast != undefined && !cast) || (!cast && !this._casting)) { |
| 273 Object.getPrototypeOf(Object.getPrototypeOf(this)).play.call(this); |
| 274 // this.super(); |
| 275 } else { |
| 276 // Handle cast media. |
| 277 if (!this._castMedia) { |
| 278 var mediaInfo = new chrome.cast.media.MediaInfo(this.currentSrc); |
| 279 [].forEach.call( // loop through DOM video sources to find the conte
ntType of the current source |
| 280 document.querySelectorAll("video source"), |
| 281 function(el) { |
| 282 // the HTML5 video API resolves the DOM 'src' attribute to an abs
olute URL for the value of 'currentSrc'; to make it matchable, then, we can only
rely on the last segment of the URL |
| 283 if (el.getAttribute("src").split('/').pop()===this.currentSrc.spl
it('/').pop()) { |
| 284 mediaInfo.contentType = el.getAttribute('type'); |
| 285 } |
| 286 } |
| 287 ); |
| 288 var request = new chrome.cast.media.LoadRequest(mediaInfo); |
| 289 this._session.loadMedia(request, |
| 290 this._onMediaDiscovered.bind(this, 'loadMedia'), |
| 291 function(e){ |
| 292 this._triggerError('LOAD_MEDIA_ERROR'); |
| 293 }.bind(this) |
| 294 ); |
| 295 } else { |
| 296 this._castMedia.play(); |
| 297 } |
| 298 } |
| 299 }, |
| 300 |
| 301 /** |
| 302 * Call the `pause` method from your controls. |
| 303 * |
| 304 * @method pause |
| 305 */ |
| 306 pause: function(cast){ |
| 307 if ((cast != undefined && !cast) || (!cast && !this._casting)) { |
| 308 Object.getPrototypeOf(Object.getPrototypeOf(this)).pause.call(this); |
| 309 // this.super(); |
| 310 } else { |
| 311 this._castMedia.pause(); |
| 312 } |
| 313 }, |
| 314 |
| 315 /** |
| 316 * The `google-castable-video-timeupdate` event is fired whenever |
| 317 * the video's playback position changes. |
| 318 * |
| 319 * @event google-castable-video-timeupdate |
| 320 * @param {Object} detail |
| 321 * @param {number} detail.currentTime The current video position. |
| 322 */ |
| 323 _triggerTimeupdate: function(position) { |
| 324 this.fire('google-castable-video-timeupdate', { currentTime: position })
; |
| 325 }, |
| 326 |
| 327 /** |
| 328 * The `google-castable-video-error` event is fired whenever |
| 329 * an error occurs. |
| 330 * |
| 331 * @event google-castable-video-error |
| 332 * @param {Object} detail |
| 333 * @param {string} detail.error The error type. |
| 334 */ |
| 335 _triggerError: function(description) { |
| 336 this.fire('google-castable-video-error', { error: description }); |
| 337 }, |
| 338 |
| 339 /** |
| 340 * The `google-castable-video-receiver-status` event is fired whenever |
| 341 * the availability of Chromecasts changes. Use this to show or hide the c
ast icon. |
| 342 * |
| 343 * @event google-castable-video-receiver-status |
| 344 * @param {Object} detail |
| 345 * @param {bool} detail.available Shows if receivers are available. |
| 346 */ |
| 347 _triggerAvailability: function(availability) { |
| 348 this._receiverAvailable = availability; |
| 349 this.fire('google-castable-video-receiver-status', { available: availabi
lity }); |
| 350 }, |
| 351 |
| 352 /** |
| 353 * The `google-castable-video-casting` event is fired whenever the |
| 354 * connection status to a Chromecast changes. Use this to change the cast
icon. |
| 355 * |
| 356 * @event google-castable-video-casting |
| 357 * @param {Object} detail |
| 358 * @param {bool} detail.casting True if connected. |
| 359 */ |
| 360 _triggerCasting: function(casting) { |
| 361 this._casting = casting; |
| 362 this.fire('google-castable-video-casting', { casting: casting }); |
| 363 }, |
| 364 |
| 365 // Redirecting `seeked` event to Chromecast. |
| 366 _onSeeked: function(){ |
| 367 if (this._casting) { |
| 368 var req = new chrome.cast.media.SeekRequest(); |
| 369 req.currentTime = this.currentTime; |
| 370 this._castMedia.seek(req); |
| 371 } |
| 372 }, |
| 373 |
| 374 // Redirecting `volumechange` event to Chromecast. |
| 375 _onVolumechange: function(){ |
| 376 if (this._casting) { |
| 377 var volume = new chrome.cast.Volume(this.volume, this.muted); |
| 378 var volumeRequest = new chrome.cast.media.VolumeRequest(volume); |
| 379 this._castMedia.setVolume(volumeRequest); |
| 380 } |
| 381 }, |
| 382 |
| 383 // Redirecting `timeupdate` event to `google-castable-video-timeupdate`. |
| 384 _onTimeupdate: function(){ |
| 385 this._triggerTimeupdate(this.currentTime); |
| 386 this._bothPaused = this.paused; |
| 387 } |
| 388 }); |
| 389 })(); |
| 390 </script> |
OLD | NEW |