Bug 1001280 - Add a pageaction for casting videos r=wesj a=sylvestre
authorMark Finkle <mfinkle@mozilla.com>
Thu, 01 May 2014 23:32:05 -0400
changeset 199119 a77e021c801f64b7c286810145708025944e2c91
parent 199118 a94ba8aa407fec2e650ba65099613875754c3d4a
child 199120 374edcad4574f9ec5e1184e04d77c29c812204e5
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj, sylvestre
bugs1001280
milestone31.0a2
Bug 1001280 - Add a pageaction for casting videos r=wesj a=sylvestre
mobile/android/base/resources/drawable-hdpi/casting.png
mobile/android/base/resources/drawable-hdpi/casting_active.png
mobile/android/base/resources/drawable-mdpi/casting.png
mobile/android/base/resources/drawable-mdpi/casting_active.png
mobile/android/base/resources/drawable-xhdpi/casting.png
mobile/android/base/resources/drawable-xhdpi/casting_active.png
mobile/android/chrome/content/CastingApps.js
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f3e70cc10e805d716be2bc4dd1057fae29448ec9
GIT binary patch
literal 408
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`uqAoByD<C*
z!3BGlPX>x`7I;J!Gca%qgD@k*tT_@uE!v(gjv*0;-(K7AEtVj1^rOF+E}xUfB>P|D
zQ86(x#vLB*%vNlsY=OLjf{I?=j}n^e9k#@D6qINzJb71_BRWS^isfKdr8v7X=l$=Z
z^WIm?e>m^?oYPM)Wtd3yCdXL3R6O~9b5>NS`Zw7Z=Iwfa&*%G{-oN8sIiIQHbH5As
z+kUAn`+aDkO8Lh2>3@Z8y1z*<Ke<_@d9T=(9^aV5*)_gKw~ZY`zb;ypRv?>SC@Me6
z=H*qMx9XMsovW%ktbcTxd%pXsv`JYf!TO|^`aVmUB;Au4+gW=P<m^94WQTs`TyM~^
zblp68Pq{?d@D06%caEjk92Yod(At|fM}GB>rrJC2ugnX->X@z?b8Kzjf%v<(u1v0o
z3hnmXm#B5pVA}@<^Hr~8^|o1WznEciZi4;UDBrhQllR5|!;-<%)z4*}Q$iB}-m|86
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4bd3b344bc1f657144963a6c75f8f245a519fbff
GIT binary patch
literal 454
zc$@*o0XhDOP)<h;3K|Lk000e1NJLTq001Na001Ni1^@s6;Q*MJ00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00045Nkl<ZSi|ks
zJ!_XS6o&DiwhEmD5k+GC3W9Oz;;3R25vPt7?I0A3br2B|@n!J^t%Kkyj#3Iia8Y#X
zkV&^f(IK4+4uWWPc@~F)()Q_p$Tglo&V9~(5)O?<qtW<_ZEGFGTz_v?(vrfOdAAQ*
zhlbZv&HwT*T_4`4i#DpTfwQa$TF(p}!_k50P|WoVmT|KSoy4cDv}_Ka@g?SZ4IgkA
zPq#|YUA)D9+{YqLl@u;x605xmDk<D3DICW^Ji}hRj=9d16kcGa?-^Q&xxT_^Nnr}7
z@eL1Rt`m5IH+@?63*%UbxjyXdJBuHf?W~-_&%U1hKAgiNT){|5;Zn@?7VhCrN#Q(p
z^s;`$T&M9IyRm=+NO*^@*oA;G%=fW=JjO??;6fL>UQ+npeF-~pq7%5<hh=aCS4#@F
wW3EdWi@EN>dtAh!lEO<2wLQ1dXf!sFKYRUM!EfPUEdT%j07*qoM6N<$f|7j1V*mgE
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..097895726fa7ed147c0c0a48e5f3e2b69afc4b97
GIT binary patch
literal 532
zc$@(g0_**WP)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0004}Nkl<ZNXPAz
zy=zoa5XFCI_vsQ0+E^H{u~AUeLJ+|;+65wt+_AS%5Ud16w6he_h+rdDR^C?z!N0&F
zRoavi`~a)0Yh)FV&A#R3nPPp}Ac6w}cLwg9Idkq@_-}vPR1YFzwwc?34RX#ui+aX(
zUz$#R+dW{|*+NI!m{RvcNizV&Hpm-gSsrLxIU)>v1NH!Pc2VDhyA8k?Af;5ro07gy
zsrHAl24z_i5k26d?UNO#By!F>LMdwEK9tmv^fsk*xGc-{k!Wo}zHZS#(wyy^z%r0i
zO4n0L-JCNK(FWj0Q{-EcP5_INW+c6k^f)4VEf}m;KSHNAqSv;UY_9?Hz;oaZa4)6Q
zZNZ{wj%ZWTHDC?60o(>M@JQ0}KV)$lIMm$aE5Iq>DX;_#Y(J264tNI4r<CR-y`KP}
z3k;hYJOPeMx&W*I_azO0H^BLbI40@xgbeomrU7sXcm*r~cWoa57J+5k*uDz999^t@
zZ_SHQZa>ho{joWs1z;9f2R;L5ZNHP$mvqMVr=0VbMyK0teZM-4e~3w6NXvcr$L$vg
Wzl<9Q_>S)Y0000<MNUMnLSTaL{^5E6
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a32e037f3f3bbd9ca5d937a87d1d07d83c021f93
GIT binary patch
literal 566
zc$@(?0?GY}P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0005WNkl<ZNXPA!
zK}b|l6o$Wlm{rKoRwstaM04p2mu+hIq!w)iH??RHxUrT}5QMpP<*snkCJ3P!H-+FP
zR}V#q$b}Kk;7U=Fhys5XZ)SQ<&wPX9&I6CjIp^}uIsgCPd*R%&SWu~*$$y8+Rg@?)
z0z`@Y7SNqolkxvqH?RjBL}Bq)K9P9e$?gq<z?a-v!7jJ}kWa~#Y)jIgL~I}EYT37U
zv}F$<jR4P+;j{vIU==8|*RlWxS_}LGkxfTo%LSEcgOxtRoW!<J*I%wqSs)585tM5M
zM2}!R3X8jz?*N<F0z0E>QoRNoeYTf?-Jnu^4O#(u5pNpco-~b)@p5&4L(+up1<+mW
z8t@2s2uv9c0QqFdzNRS#$8CO-bRTn7;wdl#DgiHm5-_ILMze5GALtr*9(xfOG>ihX
zz!P9p@C?|oW0|FXu>`NDP7v!}p~XwVE3BgNgrt7VW6MiI!^m}{Qc0Gio%90cqT#p1
z4dA216tD<PB9?`2+WsVIEgQ*hMj9kvmiT~u4|rhvCukb@46FgSC9Su06UD-4Li7Xt
z9qAVER^lCSrM_t%=miQ<SX8_30YN$YW79nK=hjI(1XWL+(5edbEdT%j07*qoM6N<$
Eg4+A=nE(I)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c12d50ab8d509c81a74360295aa3102ed9348967
GIT binary patch
literal 648
zc$@)<0(bq1P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0006TNkl<ZXx{Ca
zO^A+B6vuzB@%<5+8Vf}z3le34m5*Y<f`$G*h7=27VKHTGWr3wxn50RK#-GxV6-^ln
zg=VERWtvGEnNm_ydU?Gz=dIh+)X2yCJh^xCoO{l__53>jb3dM^Qm@x3bDKfQTL#Dg
z86X2>fd3m{=2QVmh1#NRSF(gkC84R(C+T;kFzvC^k#Uw#2FL&zAOrk0fXBc@X#s5S
z0hR*0O9dclc>?jx_LH(5CEiP#0A7TQdTo!CO+pKRoxmkvSkfa&Tgn130{lq&6<{y$
zTv9i%q|gAiS4Z(40B!@-1mpxT2y860glvDY-DkTUSP$F*8b{kzz%yWbA&Xd256}-h
zv^{S7EpS*;S7c-vun6djCH7^}GH?{Q0gOqyC23`3r61S|49B>|z&)U~s5@~Ba2yzt
zbS@x9fnC5@j9CG677gGO@BsK8{W-ujNmrx)4mbkTqJIe3QKVJqrtST}I^by%>X39k
z>;d3<61&(efXd`QG?H3ve+s(==mw5OxC(3n-T-aDC@?Q#8-UlqX5iJIe$ij~F7!cC
zr=*3pYayvdCT0Vd!yX5El6md%90Wc#vj%1XXMur$r~+q_n%D=d3A;C$*EqG-CrR(0
zODJ`8**+cqyTHNlU$A{e(p=zk6nYJqAHapVk)4#(7WVzbzb$n66&OmE-L*|G;(RC0
i02v?yWPl7%ruGxes;h;z*R)Fj0000<MNUMnLSTa4rx4`;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..11881bee959e181ad9e932002c1656b2b4027c6c
GIT binary patch
literal 666
zc$@*60%iS)P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!JV``BRCwC#nN3JlK@`W|wGYD-6TwA6
zQ9)ovE>x^l-z{uWi=YpaKo^oOTnK_~Dg%RRS&JefB%>&Zf^=aD)}l?g4~?S4s8xhv
zsP*~{{2v@1JusB}=HeXqF=x(j;r^L(&dhx;N~hDQBwIl$;T8l00YN|z5Ckl50V{JY
z)DBE}bws~ySRaKHO0s|`ZgQo}@+smR7FS_Mp$3A0ARq`x4Z+VOx6#9}3ic#w2e`y-
zFw;4BY35g8#`hGOM8q`X2z0|{vvwYK!m#f(uom_~FT95rP_MNKrPqZRPv-v;@B#j0
z9qSG72JXQ+t@pqgnDt5kx4<n7WpE50m=|<H&cYZ}E9Tal^SKiRIlg{=Fg$}ssDel4
zC0$LtHr=UTwc<?|>W3EG3X}6E;UIiftV9CqG`|XizU;thxC2wr2b&akU1^t!+F2<)
zfpYCmD@^-z60d+Jcn9r@eT4lI=WHX~l>b>fk#_+ueV)U-=7n$ru511YPQh=@PrzRJ
zmzU_1a#3CAE*!<(4x?tN6FTI-f?H<oqEAI!E_WObz<smqQlwJ;C3CHC9chRB8r8(2
zSm^Ik|4N}RJnZtiSZUH?c4D>3^KWXQ8Ee!yX1Y~gYQWt<+mt57I>^qMnm7Vm<v%pf
z9Wo#Oqj3p{vj61E+Pd4+L#yJC8ICL7Mf@7EHTd6E=x(=)h|l`cMx8bDl9>bTsfq7a
z!T(a5&9vQZaTjqE{$>pVf`A|(2nYg_Y<~q90B4AtKUs*Y^Z)<=07*qoM6N<$f>c8&
AGXMYp
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -24,19 +24,29 @@ var CastingApps = {
       Strings.browser.GetStringFromName("contextmenu.castToScreen"),
       this.filterCast,
       this.openExternal.bind(this)
     );
 
     Services.obs.addObserver(this, "Casting:Play", false);
     Services.obs.addObserver(this, "Casting:Pause", false);
     Services.obs.addObserver(this, "Casting:Stop", false);
+
+    BrowserApp.deck.addEventListener("TabSelect", this, true);
+    BrowserApp.deck.addEventListener("pageshow", this, true);
+    BrowserApp.deck.addEventListener("playing", this, true);
+    BrowserApp.deck.addEventListener("ended", this, true);
   },
 
   uninit: function ca_uninit() {
+    BrowserApp.deck.removeEventListener("TabSelect", this, true);
+    BrowserApp.deck.removeEventListener("pageshow", this, true);
+    BrowserApp.deck.removeEventListener("playing", this, true);
+    BrowserApp.deck.removeEventListener("ended", this, true);
+
     Services.obs.removeObserver(this, "Casting:Play");
     Services.obs.removeObserver(this, "Casting:Pause");
     Services.obs.removeObserver(this, "Casting:Stop");
 
     NativeWindow.contextmenus.remove(this._castMenuId);
   },
 
   isEnabled: function isEnabled() {
@@ -58,16 +68,39 @@ var CastingApps = {
       case "Casting:Stop":
         if (this.session) {
           this.closeExternal();
         }
         break;
     }
   },
 
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "TabSelect": {
+        let tab = BrowserApp.getTabForBrowser(aEvent.target);
+        this._updatePageActionForTab(tab, aEvent);
+        break;
+      }
+      case "pageshow": {
+        let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView);
+        this._updatePageActionForTab(tab, aEvent);
+        break;
+      }
+      case "playing":
+      case "ended": {
+        let video = aEvent.target;
+        if (video instanceof HTMLVideoElement) {
+          this._updatePageActionForVideo(video);
+        }
+        break;
+      }
+    }
+  },
+
   _sendEventToVideo: function _sendEventToVideo(aElement, aData) {
     let event = aElement.ownerDocument.createEvent("CustomEvent");
     event.initCustomEvent("media-videoCasting", false, true, JSON.stringify(aData));
     aElement.dispatchEvent(event);
   },
 
   handleVideoBindingAttached: function handleVideoBindingAttached(aTab, aEvent) {
     // Let's figure out if we have everything needed to cast a video. The binding
@@ -198,16 +231,115 @@ var CastingApps = {
       let video = CastingApps.getVideo(aElement, aX, aY);
       if (CastingApps.session) {
         return (video && CastingApps.session.data.source != video.source);
       }
       return (video != null);
     }
   },
 
+  pageAction: {
+    click: function() {
+      // Since this is a pageaction, we use the selected browser
+      let browser = BrowserApp.selectedBrowser;
+      if (!browser) {
+        return;
+      }
+
+      // Look for a castable <video> that is playing, and start casting it
+      let videos = browser.contentDocument.querySelectorAll("video");
+      for (let video of videos) {
+        let unwrappedVideo = XPCNativeWrapper.unwrap(video);
+        if (!video.paused && unwrappedVideo.mozAllowCasting) {
+          CastingApps.openExternal(video, 0, 0);
+          return;
+        }
+      }
+    }
+  },
+
+  _findCastableVideo: function _findCastableVideo(aBrowser) {
+      // Scan for a <video> being actively cast. Also look for a castable <video>
+      // on the page.
+      let castableVideo = null;
+      let videos = aBrowser.contentDocument.querySelectorAll("video");
+      for (let video of videos) {
+        let unwrappedVideo = XPCNativeWrapper.unwrap(video);
+        if (unwrappedVideo.mozIsCasting) {
+          // This <video> is cast-active. Break out of loop.
+          return video;
+        }
+
+        if (!video.paused && unwrappedVideo.mozAllowCasting) {
+          // This <video> is cast-ready. Keep looking so cast-active could be found.
+          castableVideo = video;
+        }
+      }
+
+      // Could be null
+      return castableVideo;
+  },
+
+  _updatePageActionForTab: function _updatePageActionForTab(aTab, aEvent) {
+    // We only care about events on the selected tab
+    if (aTab != BrowserApp.selectedTab) {
+      return;
+    }
+
+    // Update the page action, scanning for a castable <video>
+    this._updatePageAction();
+  },
+
+  _updatePageActionForVideo: function _updatePageActionForVideo(aVideo) {
+    // If playing, send the <video>, but if ended we send nothing to shutdown the pageaction
+    this._updatePageAction(aEvent.type == "playing" ? video : null);
+  },
+
+  _updatePageAction: function _updatePageAction(aVideo) {
+    // Remove any exising pageaction first, in case state changes or we don't have
+    // a castable video
+    if (this.pageAction.id) {
+      NativeWindow.pageactions.remove(this.pageAction.id);
+      delete this.pageAction.id;
+    }
+
+    if (!aVideo) {
+      aVideo = this._findCastableVideo(BrowserApp.selectedBrowser);
+      if (!aVideo) {
+        return;
+      }
+    }
+
+    // We only show pageactions if the <video> is from the selected tab
+    if (BrowserApp.selectedTab != BrowserApp.getTabForWindow(aVideo.ownerDocument.defaultView.top)) {
+      return;
+    }
+
+    // We check for two state here:
+    // 1. The video is actively being cast
+    // 2. The video is allowed to be cast and is currently playing
+    // Both states have the same action: Show the cast page action
+    let unwrappedVideo = XPCNativeWrapper.unwrap(aVideo);
+    if (unwrappedVideo.mozIsCasting) {
+      this.pageAction.id = NativeWindow.pageactions.add({
+        title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
+        icon: "drawable://casting_active",
+        clickCallback: this.pageAction.click,
+        important: true
+      });
+    } else if (unwrappedVideo.mozAllowCasting) {
+      this.pageAction.id = NativeWindow.pageactions.add({
+        title: Strings.browser.GetStringFromName("contextmenu.castToScreen"),
+        icon: "drawable://casting",
+        clickCallback: this.pageAction.click,
+        important: true
+      });
+    }
+  },
+
   prompt: function(aCallback) {
     let items = [];
     SimpleServiceDiscovery.services.forEach(function(aService) {
       let item = {
         label: aService.friendlyName,
         selected: false
       };
       items.push(item);
@@ -283,16 +415,17 @@ var CastingApps = {
     }
 
     this.session.remoteMedia.shutdown();
     this.session.app.stop();
 
     let video = this.session.videoRef.get();
     if (video) {
       this._sendEventToVideo(video, { active: false });
+      this._updatePageAction();
     }
 
     delete this.session;
   },
 
   // RemoteMedia callback API methods
   onRemoteMediaStart: function(aRemoteMedia) {
     if (!this.session) {
@@ -300,16 +433,17 @@ var CastingApps = {
     }
 
     aRemoteMedia.load(this.session.data);
     sendMessageToJava({ type: "Casting:Started", device: this.session.service.friendlyName });
 
     let video = this.session.videoRef.get();
     if (video) {
       this._sendEventToVideo(video, { active: true });
+      this._updatePageAction(video);
     }
   },
 
   onRemoteMediaStop: function(aRemoteMedia) {
     sendMessageToJava({ type: "Casting:Stopped" });
   },
 
   onRemoteMediaStatus: function(aRemoteMedia) {