mobile/android/chrome/content/CastingApps.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4 "use strict";
michael@0 5
michael@0 6 var CastingApps = {
michael@0 7 _castMenuId: -1,
michael@0 8
michael@0 9 init: function ca_init() {
michael@0 10 if (!this.isEnabled()) {
michael@0 11 return;
michael@0 12 }
michael@0 13
michael@0 14 // Register a service target
michael@0 15 SimpleServiceDiscovery.registerTarget("roku:ecp", function(aService, aApp) {
michael@0 16 Cu.import("resource://gre/modules/RokuApp.jsm");
michael@0 17 return new RokuApp(aService, "FirefoxTest");
michael@0 18 });
michael@0 19
michael@0 20 // Search for devices continuously every 120 seconds
michael@0 21 SimpleServiceDiscovery.search(120 * 1000);
michael@0 22
michael@0 23 this._castMenuId = NativeWindow.contextmenus.add(
michael@0 24 Strings.browser.GetStringFromName("contextmenu.castToScreen"),
michael@0 25 this.filterCast,
michael@0 26 this.openExternal.bind(this)
michael@0 27 );
michael@0 28
michael@0 29 Services.obs.addObserver(this, "Casting:Play", false);
michael@0 30 Services.obs.addObserver(this, "Casting:Pause", false);
michael@0 31 Services.obs.addObserver(this, "Casting:Stop", false);
michael@0 32
michael@0 33 BrowserApp.deck.addEventListener("TabSelect", this, true);
michael@0 34 BrowserApp.deck.addEventListener("pageshow", this, true);
michael@0 35 BrowserApp.deck.addEventListener("playing", this, true);
michael@0 36 BrowserApp.deck.addEventListener("ended", this, true);
michael@0 37 },
michael@0 38
michael@0 39 uninit: function ca_uninit() {
michael@0 40 BrowserApp.deck.removeEventListener("TabSelect", this, true);
michael@0 41 BrowserApp.deck.removeEventListener("pageshow", this, true);
michael@0 42 BrowserApp.deck.removeEventListener("playing", this, true);
michael@0 43 BrowserApp.deck.removeEventListener("ended", this, true);
michael@0 44
michael@0 45 Services.obs.removeObserver(this, "Casting:Play");
michael@0 46 Services.obs.removeObserver(this, "Casting:Pause");
michael@0 47 Services.obs.removeObserver(this, "Casting:Stop");
michael@0 48
michael@0 49 NativeWindow.contextmenus.remove(this._castMenuId);
michael@0 50 },
michael@0 51
michael@0 52 isEnabled: function isEnabled() {
michael@0 53 return Services.prefs.getBoolPref("browser.casting.enabled");
michael@0 54 },
michael@0 55
michael@0 56 observe: function (aSubject, aTopic, aData) {
michael@0 57 switch (aTopic) {
michael@0 58 case "Casting:Play":
michael@0 59 if (this.session && this.session.remoteMedia.status == "paused") {
michael@0 60 this.session.remoteMedia.play();
michael@0 61 }
michael@0 62 break;
michael@0 63 case "Casting:Pause":
michael@0 64 if (this.session && this.session.remoteMedia.status == "started") {
michael@0 65 this.session.remoteMedia.pause();
michael@0 66 }
michael@0 67 break;
michael@0 68 case "Casting:Stop":
michael@0 69 if (this.session) {
michael@0 70 this.closeExternal();
michael@0 71 }
michael@0 72 break;
michael@0 73 }
michael@0 74 },
michael@0 75
michael@0 76 handleEvent: function(aEvent) {
michael@0 77 switch (aEvent.type) {
michael@0 78 case "TabSelect": {
michael@0 79 let tab = BrowserApp.getTabForBrowser(aEvent.target);
michael@0 80 this._updatePageActionForTab(tab, aEvent);
michael@0 81 break;
michael@0 82 }
michael@0 83 case "pageshow": {
michael@0 84 let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView);
michael@0 85 this._updatePageActionForTab(tab, aEvent);
michael@0 86 break;
michael@0 87 }
michael@0 88 case "playing":
michael@0 89 case "ended": {
michael@0 90 let video = aEvent.target;
michael@0 91 if (video instanceof HTMLVideoElement) {
michael@0 92 // If playing, send the <video>, but if ended we send nothing to shutdown the pageaction
michael@0 93 this._updatePageActionForVideo(aEvent.type === "playing" ? video : null);
michael@0 94 }
michael@0 95 break;
michael@0 96 }
michael@0 97 }
michael@0 98 },
michael@0 99
michael@0 100 _sendEventToVideo: function _sendEventToVideo(aElement, aData) {
michael@0 101 let event = aElement.ownerDocument.createEvent("CustomEvent");
michael@0 102 event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(aData));
michael@0 103 aElement.dispatchEvent(event);
michael@0 104 },
michael@0 105
michael@0 106 handleVideoBindingAttached: function handleVideoBindingAttached(aTab, aEvent) {
michael@0 107 // Let's figure out if we have everything needed to cast a video. The binding
michael@0 108 // defaults to |false| so we only need to send an event if |true|.
michael@0 109 let video = aEvent.target;
michael@0 110 if (!video instanceof HTMLVideoElement) {
michael@0 111 return;
michael@0 112 }
michael@0 113
michael@0 114 if (SimpleServiceDiscovery.services.length == 0) {
michael@0 115 return;
michael@0 116 }
michael@0 117
michael@0 118 if (!this.getVideo(video, 0, 0)) {
michael@0 119 return;
michael@0 120 }
michael@0 121
michael@0 122 // Let the binding know casting is allowed
michael@0 123 this._sendEventToVideo(video, { allow: true });
michael@0 124 },
michael@0 125
michael@0 126 handleVideoBindingCast: function handleVideoBindingCast(aTab, aEvent) {
michael@0 127 // The binding wants to start a casting session
michael@0 128 let video = aEvent.target;
michael@0 129 if (!video instanceof HTMLVideoElement) {
michael@0 130 return;
michael@0 131 }
michael@0 132
michael@0 133 // Close an existing session first. closeExternal has checks for an exsting
michael@0 134 // session and handles remote and video binding shutdown.
michael@0 135 this.closeExternal();
michael@0 136
michael@0 137 // Start the new session
michael@0 138 this.openExternal(video, 0, 0);
michael@0 139 },
michael@0 140
michael@0 141 makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
michael@0 142 return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
michael@0 143 },
michael@0 144
michael@0 145 getVideo: function(aElement, aX, aY) {
michael@0 146 // Fast path: Is the given element a video element
michael@0 147 let video = this._getVideo(aElement);
michael@0 148 if (video) {
michael@0 149 return video;
michael@0 150 }
michael@0 151
michael@0 152 // The context menu system will keep walking up the DOM giving us a chance
michael@0 153 // to find an element we match. When it hits <html> things can go BOOM.
michael@0 154 try {
michael@0 155 // Maybe this is an overlay, with the video element under it
michael@0 156 // Use the (x, y) location to guess at a <video> element
michael@0 157 let elements = aElement.ownerDocument.querySelectorAll("video");
michael@0 158 for (let element of elements) {
michael@0 159 // Look for a video element contained in the overlay bounds
michael@0 160 let rect = element.getBoundingClientRect();
michael@0 161 if (aY >= rect.top && aX >= rect.left && aY <= rect.bottom && aX <= rect.right) {
michael@0 162 video = this._getVideo(element);
michael@0 163 if (video) {
michael@0 164 break;
michael@0 165 }
michael@0 166 }
michael@0 167 }
michael@0 168 } catch(e) {}
michael@0 169
michael@0 170 // Could be null
michael@0 171 return video;
michael@0 172 },
michael@0 173
michael@0 174 _getVideo: function(aElement) {
michael@0 175 // Given the hardware support for H264, let's only look for 'mp4' sources
michael@0 176 if (!aElement instanceof HTMLVideoElement) {
michael@0 177 return null;
michael@0 178 }
michael@0 179
michael@0 180 function allowableExtension(aURI) {
michael@0 181 if (aURI && aURI instanceof Ci.nsIURL) {
michael@0 182 return (aURI.fileExtension == "mp4");
michael@0 183 }
michael@0 184 return false;
michael@0 185 }
michael@0 186
michael@0 187 // Grab the poster attribute from the <video>
michael@0 188 let posterURL = aElement.poster;
michael@0 189
michael@0 190 // First, look to see if the <video> has a src attribute
michael@0 191 let sourceURL = aElement.src;
michael@0 192
michael@0 193 // If empty, try the currentSrc
michael@0 194 if (!sourceURL) {
michael@0 195 sourceURL = aElement.currentSrc;
michael@0 196 }
michael@0 197
michael@0 198 if (sourceURL) {
michael@0 199 // Use the file extension to guess the mime type
michael@0 200 let sourceURI = this.makeURI(sourceURL, null, this.makeURI(aElement.baseURI));
michael@0 201 if (allowableExtension(sourceURI)) {
michael@0 202 return { element: aElement, source: sourceURI.spec, poster: posterURL };
michael@0 203 }
michael@0 204 }
michael@0 205
michael@0 206 // Next, look to see if there is a <source> child element that meets
michael@0 207 // our needs
michael@0 208 let sourceNodes = aElement.getElementsByTagName("source");
michael@0 209 for (let sourceNode of sourceNodes) {
michael@0 210 let sourceURI = this.makeURI(sourceNode.src, null, this.makeURI(sourceNode.baseURI));
michael@0 211
michael@0 212 // Using the type attribute is our ideal way to guess the mime type. Otherwise,
michael@0 213 // fallback to using the file extension to guess the mime type
michael@0 214 if (sourceNode.type == "video/mp4" || allowableExtension(sourceURI)) {
michael@0 215 return { element: aElement, source: sourceURI.spec, poster: posterURL };
michael@0 216 }
michael@0 217 }
michael@0 218
michael@0 219 return null;
michael@0 220 },
michael@0 221
michael@0 222 filterCast: {
michael@0 223 matches: function(aElement, aX, aY) {
michael@0 224 if (SimpleServiceDiscovery.services.length == 0)
michael@0 225 return false;
michael@0 226 let video = CastingApps.getVideo(aElement, aX, aY);
michael@0 227 if (CastingApps.session) {
michael@0 228 return (video && CastingApps.session.data.source != video.source);
michael@0 229 }
michael@0 230 return (video != null);
michael@0 231 }
michael@0 232 },
michael@0 233
michael@0 234 pageAction: {
michael@0 235 click: function() {
michael@0 236 // Since this is a pageaction, we use the selected browser
michael@0 237 let browser = BrowserApp.selectedBrowser;
michael@0 238 if (!browser) {
michael@0 239 return;
michael@0 240 }
michael@0 241
michael@0 242 // Look for a castable <video> that is playing, and start casting it
michael@0 243 let videos = browser.contentDocument.querySelectorAll("video");
michael@0 244 for (let video of videos) {
michael@0 245 if (!video.paused && video.mozAllowCasting) {
michael@0 246 CastingApps.openExternal(video, 0, 0);
michael@0 247 return;
michael@0 248 }
michael@0 249 }
michael@0 250 }
michael@0 251 },
michael@0 252
michael@0 253 _findCastableVideo: function _findCastableVideo(aBrowser) {
michael@0 254 // Scan for a <video> being actively cast. Also look for a castable <video>
michael@0 255 // on the page.
michael@0 256 let castableVideo = null;
michael@0 257 let videos = aBrowser.contentDocument.querySelectorAll("video");
michael@0 258 for (let video of videos) {
michael@0 259 if (video.mozIsCasting) {
michael@0 260 // This <video> is cast-active. Break out of loop.
michael@0 261 return video;
michael@0 262 }
michael@0 263
michael@0 264 if (!video.paused && video.mozAllowCasting) {
michael@0 265 // This <video> is cast-ready. Keep looking so cast-active could be found.
michael@0 266 castableVideo = video;
michael@0 267 }
michael@0 268 }
michael@0 269
michael@0 270 // Could be null
michael@0 271 return castableVideo;
michael@0 272 },
michael@0 273
michael@0 274 _updatePageActionForTab: function _updatePageActionForTab(aTab, aEvent) {
michael@0 275 // We only care about events on the selected tab
michael@0 276 if (aTab != BrowserApp.selectedTab) {
michael@0 277 return;
michael@0 278 }
michael@0 279
michael@0 280 // Update the page action, scanning for a castable <video>
michael@0 281 this._updatePageAction();
michael@0 282 },
michael@0 283
michael@0 284 _updatePageActionForVideo: function _updatePageActionForVideo(aVideo) {
michael@0 285 this._updatePageAction(aVideo);
michael@0 286 },
michael@0 287
michael@0 288 _updatePageAction: function _updatePageAction(aVideo) {
michael@0 289 // Remove any exising pageaction first, in case state changes or we don't have
michael@0 290 // a castable video
michael@0 291 if (this.pageAction.id) {
michael@0 292 NativeWindow.pageactions.remove(this.pageAction.id);
michael@0 293 delete this.pageAction.id;
michael@0 294 }
michael@0 295
michael@0 296 if (!aVideo) {
michael@0 297 aVideo = this._findCastableVideo(BrowserApp.selectedBrowser);
michael@0 298 if (!aVideo) {
michael@0 299 return;
michael@0 300 }
michael@0 301 }
michael@0 302
michael@0 303 // We only show pageactions if the <video> is from the selected tab
michael@0 304 if (BrowserApp.selectedTab != BrowserApp.getTabForWindow(aVideo.ownerDocument.defaultView.top)) {
michael@0 305 return;
michael@0 306 }
michael@0 307
michael@0 308 // We check for two state here:
michael@0 309 // 1. The video is actively being cast
michael@0 310 // 2. The video is allowed to be cast and is currently playing
michael@0 311 // Both states have the same action: Show the cast page action
michael@0 312 if (aVideo.mozIsCasting) {
michael@0 313 this.pageAction.id = NativeWindow.pageactions.add({
michael@0 314 title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
michael@0 315 icon: "drawable://casting_active",
michael@0 316 clickCallback: this.pageAction.click,
michael@0 317 important: true
michael@0 318 });
michael@0 319 } else if (aVideo.mozAllowCasting) {
michael@0 320 this.pageAction.id = NativeWindow.pageactions.add({
michael@0 321 title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
michael@0 322 icon: "drawable://casting",
michael@0 323 clickCallback: this.pageAction.click,
michael@0 324 important: true
michael@0 325 });
michael@0 326 }
michael@0 327 },
michael@0 328
michael@0 329 prompt: function(aCallback) {
michael@0 330 let items = [];
michael@0 331 SimpleServiceDiscovery.services.forEach(function(aService) {
michael@0 332 let item = {
michael@0 333 label: aService.friendlyName,
michael@0 334 selected: false
michael@0 335 };
michael@0 336 items.push(item);
michael@0 337 });
michael@0 338
michael@0 339 let prompt = new Prompt({
michael@0 340 title: Strings.browser.GetStringFromName("casting.prompt")
michael@0 341 }).setSingleChoiceItems(items).show(function(data) {
michael@0 342 let selected = data.button;
michael@0 343 let service = selected == -1 ? null : SimpleServiceDiscovery.services[selected];
michael@0 344 if (aCallback)
michael@0 345 aCallback(service);
michael@0 346 });
michael@0 347 },
michael@0 348
michael@0 349 openExternal: function(aElement, aX, aY) {
michael@0 350 // Start a second screen media service
michael@0 351 let video = this.getVideo(aElement, aX, aY);
michael@0 352 if (!video) {
michael@0 353 return;
michael@0 354 }
michael@0 355
michael@0 356 this.prompt(function(aService) {
michael@0 357 if (!aService)
michael@0 358 return;
michael@0 359
michael@0 360 // Make sure we have a player app for the given service
michael@0 361 let app = SimpleServiceDiscovery.findAppForService(aService, "video-sharing");
michael@0 362 if (!app)
michael@0 363 return;
michael@0 364
michael@0 365 video.title = aElement.ownerDocument.defaultView.top.document.title;
michael@0 366 if (video.element) {
michael@0 367 // If the video is currently playing on the device, pause it
michael@0 368 if (!video.element.paused) {
michael@0 369 video.element.pause();
michael@0 370 }
michael@0 371 }
michael@0 372
michael@0 373 app.stop(function() {
michael@0 374 app.start(function(aStarted) {
michael@0 375 if (!aStarted) {
michael@0 376 dump("CastingApps: Unable to start app");
michael@0 377 return;
michael@0 378 }
michael@0 379
michael@0 380 app.remoteMedia(function(aRemoteMedia) {
michael@0 381 if (!aRemoteMedia) {
michael@0 382 dump("CastingApps: Failed to create remotemedia");
michael@0 383 return;
michael@0 384 }
michael@0 385
michael@0 386 this.session = {
michael@0 387 service: aService,
michael@0 388 app: app,
michael@0 389 remoteMedia: aRemoteMedia,
michael@0 390 data: {
michael@0 391 title: video.title,
michael@0 392 source: video.source,
michael@0 393 poster: video.poster
michael@0 394 },
michael@0 395 videoRef: Cu.getWeakReference(video.element)
michael@0 396 };
michael@0 397 }.bind(this), this);
michael@0 398 }.bind(this));
michael@0 399 }.bind(this));
michael@0 400 }.bind(this));
michael@0 401 },
michael@0 402
michael@0 403 closeExternal: function() {
michael@0 404 if (!this.session) {
michael@0 405 return;
michael@0 406 }
michael@0 407
michael@0 408 this.session.remoteMedia.shutdown();
michael@0 409 this.session.app.stop();
michael@0 410
michael@0 411 let video = this.session.videoRef.get();
michael@0 412 if (video) {
michael@0 413 this._sendEventToVideo(video, { active: false });
michael@0 414 this._updatePageAction();
michael@0 415 }
michael@0 416
michael@0 417 delete this.session;
michael@0 418 },
michael@0 419
michael@0 420 // RemoteMedia callback API methods
michael@0 421 onRemoteMediaStart: function(aRemoteMedia) {
michael@0 422 if (!this.session) {
michael@0 423 return;
michael@0 424 }
michael@0 425
michael@0 426 aRemoteMedia.load(this.session.data);
michael@0 427 sendMessageToJava({ type: "Casting:Started", device: this.session.service.friendlyName });
michael@0 428
michael@0 429 let video = this.session.videoRef.get();
michael@0 430 if (video) {
michael@0 431 this._sendEventToVideo(video, { active: true });
michael@0 432 this._updatePageAction(video);
michael@0 433 }
michael@0 434 },
michael@0 435
michael@0 436 onRemoteMediaStop: function(aRemoteMedia) {
michael@0 437 sendMessageToJava({ type: "Casting:Stopped" });
michael@0 438 },
michael@0 439
michael@0 440 onRemoteMediaStatus: function(aRemoteMedia) {
michael@0 441 if (!this.session) {
michael@0 442 return;
michael@0 443 }
michael@0 444
michael@0 445 let status = aRemoteMedia.status;
michael@0 446 if (status == "completed") {
michael@0 447 this.closeExternal();
michael@0 448 }
michael@0 449 }
michael@0 450 };
michael@0 451

mercurial