mobile/android/chrome/content/CastingApps.js

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial