Bug 776208 - Plugin preview overlay. r=johns,jaws,margaret sr=joshmoz
authorYury <async.processingjs@yahoo.com>
Thu, 23 Aug 2012 17:11:51 -0400
changeset 105322 e80dea0213a0e1afffa0d8723d948e54f87f0c56
parent 105321 fe4538ef86c5eb28a9c950419b32bd0548c82578
child 105323 9e6948f521011c737cb789c168524a32196fb710
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewersjohns, jaws, margaret, joshmoz
bugs776208
milestone17.0a1
Bug 776208 - Plugin preview overlay. r=johns,jaws,margaret sr=joshmoz
browser/base/content/browser-plugins.js
browser/base/content/browser.js
content/base/public/nsIObjectLoadingContent.idl
content/base/src/nsObjectLoadingContent.cpp
content/base/src/nsObjectLoadingContent.h
content/events/public/nsEventStates.h
dom/plugins/base/nsIPluginHost.idl
dom/plugins/base/nsPluginHost.cpp
dom/plugins/base/nsPluginHost.h
layout/style/nsCSSPseudoClassList.h
mobile/android/chrome/content/browser.js
toolkit/mozapps/plugins/content/pluginProblem.xml
toolkit/mozapps/plugins/content/pluginProblemBinding.css
toolkit/mozapps/plugins/content/pluginProblemContent.css
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -145,66 +145,86 @@ var gPluginHandler = {
         self.addLinkClickCallback(updateLink, "openPluginUpdatePage");
         /* FALLTHRU */
 
       case "PluginVulnerableNoUpdate":
       case "PluginClickToPlay":
         self._handleClickToPlayEvent(plugin);
         break;
 
+      case "PluginPlayPreview":
+        self._handlePlayPreviewEvent(plugin);
+        break;
+
       case "PluginDisabled":
         let manageLink = doc.getAnonymousElementByAttribute(plugin, "class", "managePluginsLink");
         self.addLinkClickCallback(manageLink, "managePlugins");
         break;
     }
 
     // Hide the in-content UI if it's too big. The crashed plugin handler already did this.
     if (event.type != "PluginCrashed") {
       let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
       /* overlay might be null, so only operate on it if it exists */
       if (overlay != null && self.isTooSmall(plugin, overlay))
           overlay.style.visibility = "hidden";
     }
   },
 
+  canActivatePlugin: function PH_canActivatePlugin(objLoadingContent) {
+    return !objLoadingContent.activated &&
+           objLoadingContent.pluginFallbackType !== Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW;
+  },
+
   activatePlugins: function PH_activatePlugins(aContentWindow) {
     let browser = gBrowser.getBrowserForDocument(aContentWindow.document);
     browser._clickToPlayPluginsActivated = true;
     let cwu = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIDOMWindowUtils);
     let plugins = cwu.plugins;
     for (let plugin of plugins) {
       let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-      if (!objLoadingContent.activated)
+      if (gPluginHandler.canActivatePlugin(objLoadingContent))
         objLoadingContent.playPlugin();
     }
     let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
     if (notification)
       notification.remove();
   },
 
   activateSinglePlugin: function PH_activateSinglePlugin(aContentWindow, aPlugin) {
     let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    if (!objLoadingContent.activated)
+    if (gPluginHandler.canActivatePlugin(objLoadingContent))
       objLoadingContent.playPlugin();
 
     let cwu = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIDOMWindowUtils);
     let haveUnplayedPlugins = cwu.plugins.some(function(plugin) {
       let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-      return (plugin != aPlugin && !objLoadingContent.activated);
+      return (plugin != aPlugin && gPluginHandler.canActivatePlugin(objLoadingContent));
     });
     let browser = gBrowser.getBrowserForDocument(aContentWindow.document);
     let notification = PopupNotifications.getNotification("click-to-play-plugins", browser);
     if (notification && !haveUnplayedPlugins) {
       browser._clickToPlayDoorhangerShown = false;
       notification.remove();
     }
   },
 
+  stopPlayPreview: function PH_stopPlayPreview(aPlugin, aPlayPlugin) {
+    let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
+    if (objLoadingContent.activated)
+      return;
+
+    if (aPlayPlugin)
+      objLoadingContent.playPlugin();
+    else
+      objLoadingContent.cancelPlayPreview();
+  },
+
   newPluginInstalled : function(event) {
     // browser elements are anonymous so we can't just use target.
     var browser = event.originalTarget;
     // clear the plugin list, now that at least one plugin has been installed
     browser.missingPlugins = null;
 
     var notificationBox = gBrowser.getNotificationBox(browser);
     var notification = notificationBox.getNotificationWithValue("missing-plugins");
@@ -285,16 +305,56 @@ var gPluginHandler = {
           gPluginHandler.activateSinglePlugin(aEvent.target.ownerDocument.defaultView.top, aPlugin);
       }, true);
     }
 
     if (!browser._clickToPlayDoorhangerShown)
       gPluginHandler._showClickToPlayNotification(browser);
   },
 
+  _handlePlayPreviewEvent: function PH_handlePlayPreviewEvent(aPlugin) {
+    let doc = aPlugin.ownerDocument;
+    let previewContent = doc.getAnonymousElementByAttribute(aPlugin, "class", "previewPluginContent");
+    if (!previewContent) {
+      // the XBL binding is not attached (element is display:none), fallback to click-to-play logic
+      gPluginHandler.stopPlayPreview(aPlugin, false);
+      return;
+    }
+    let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
+    if (!iframe) {
+      // lazy initialization of the iframe
+      iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+      iframe.className = "previewPluginContentFrame";
+      previewContent.appendChild(iframe);
+
+      // Force a style flush, so that we ensure our binding is attached.
+      aPlugin.clientTop;
+    }
+    let pluginInfo = getPluginInfo(aPlugin);
+    let playPreviewUri = "data:application/x-moz-playpreview;," + pluginInfo.mimetype;
+    iframe.src = playPreviewUri;
+
+    // MozPlayPlugin event can be dispatched from the extension chrome
+    // code to replace the preview content with the native plugin
+    previewContent.addEventListener("MozPlayPlugin", function playPluginHandler(aEvent) {
+      if (!aEvent.isTrusted)
+        return;
+
+      previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true);
+
+      let playPlugin = !aEvent.detail;
+      gPluginHandler.stopPlayPreview(aPlugin, playPlugin);
+
+      // cleaning up: removes overlay iframe from the DOM
+      let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
+      if (iframe)
+        previewContent.removeChild(iframe);
+    }, true);
+  },
+
   reshowClickToPlayNotification: function PH_reshowClickToPlayNotification() {
     if (!Services.prefs.getBoolPref("plugins.click_to_play"))
       return;
 
     let browser = gBrowser.selectedBrowser;
 
     let pluginsPermission = Services.perms.testPermission(browser.currentURI, "plugins");
     if (pluginsPermission == Ci.nsIPermissionManager.DENY_ACTION)
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1021,16 +1021,17 @@ var gBrowserInit = {
     gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
 
     gBrowser.addEventListener("PluginNotFound",     gPluginHandler, true);
     gBrowser.addEventListener("PluginCrashed",      gPluginHandler, true);
     gBrowser.addEventListener("PluginBlocklisted",  gPluginHandler, true);
     gBrowser.addEventListener("PluginOutdated",     gPluginHandler, true);
     gBrowser.addEventListener("PluginDisabled",     gPluginHandler, true);
     gBrowser.addEventListener("PluginClickToPlay",  gPluginHandler, true);
+    gBrowser.addEventListener("PluginPlayPreview",  gPluginHandler, true);
     gBrowser.addEventListener("PluginVulnerableUpdatable", gPluginHandler, true);
     gBrowser.addEventListener("PluginVulnerableNoUpdate", gPluginHandler, true);
     gBrowser.addEventListener("NewPluginInstalled", gPluginHandler.newPluginInstalled, true);
 #ifdef XP_MACOSX
     gBrowser.addEventListener("npapi-carbon-event-model-failure", gPluginHandler, true);
 #endif
 
     Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
--- a/content/base/public/nsIObjectLoadingContent.idl
+++ b/content/base/public/nsIObjectLoadingContent.idl
@@ -16,17 +16,17 @@ interface nsIURI;
 #include "nsNPAPIPluginInstance.h"
 %}
 [ptr] native nsNPAPIPluginInstancePtr(nsNPAPIPluginInstance);
 
 /**
  * This interface represents a content node that loads objects.
  */
 
-[scriptable, uuid(8ed953b4-5022-4a49-bed4-6818f85dc113)]
+[scriptable, uuid(a812424b-4820-4e28-96c8-dd2b69e36496)]
 interface nsIObjectLoadingContent : nsISupports
 {
   /**
    * See notes in nsObjectLoadingContent.h
    */
   const unsigned long TYPE_LOADING  = 0;
   const unsigned long TYPE_IMAGE    = 1;
   const unsigned long TYPE_PLUGIN   = 2;
@@ -50,16 +50,18 @@ interface nsIObjectLoadingContent : nsIS
   // Blocked by content policy
   const unsigned long PLUGIN_USER_DISABLED        = 7;
   // The plugin is disabled until the user clicks on it
   const unsigned long PLUGIN_CLICK_TO_PLAY        = 8;
   // The plugin is vulnerable (update available)
   const unsigned long PLUGIN_VULNERABLE_UPDATABLE = 9;
   // The plugin is vulnerable (no update available)
   const unsigned long PLUGIN_VULNERABLE_NO_UPDATE = 10;
+  // The plugin is in play preview mode
+  const unsigned long PLUGIN_PLAY_PREVIEW         = 11;
 
   /**
    * The actual mime type (the one we got back from the network
    * request) for the element.
    */
   readonly attribute ACString actualType;
 
   /**
@@ -108,26 +110,31 @@ interface nsIObjectLoadingContent : nsIS
 
   /**
    * This method will play a plugin that has been stopped by the
    * click-to-play plugins feature.
    */
   void playPlugin();
 
   /**
-   * This attribute will return true if the plugin has been activated
-   * and false if the plugin is still in the click-to-play state.
+   * This attribute will return true if the plugin has been activated and
+   * false if the plugin is still in the click-to-play or play preview state.
    */
   readonly attribute boolean activated;
 
   [noscript] void stopPluginInstance();
 
   [noscript] void syncStartPluginInstance();
   [noscript] void asyncStartPluginInstance();
 
   /**
    * The URL of the data/src loaded in the object. This may be null (i.e.
    * an <embed> with no src).
    */
   readonly attribute nsIURI srcURI;
 
   readonly attribute unsigned long pluginFallbackType;
+
+  /**
+   * This method will disable the play-preview plugin state.
+   */
+  void cancelPlayPreview();
 };
--- a/content/base/src/nsObjectLoadingContent.cpp
+++ b/content/base/src/nsObjectLoadingContent.cpp
@@ -183,16 +183,19 @@ nsPluginErrorEvent::Run()
       type = NS_LITERAL_STRING("PluginVulnerableUpdatable");
       break;
     case nsObjectLoadingContent::eFallbackVulnerableNoUpdate:
       type = NS_LITERAL_STRING("PluginVulnerableNoUpdate");
       break;
     case nsObjectLoadingContent::eFallbackClickToPlay:
       type = NS_LITERAL_STRING("PluginClickToPlay");
       break;
+    case nsObjectLoadingContent::eFallbackPlayPreview:
+      type = NS_LITERAL_STRING("PluginPlayPreview");
+      break;
     case nsObjectLoadingContent::eFallbackUnsupported:
       type = NS_LITERAL_STRING("PluginNotFound");
       break;
     case nsObjectLoadingContent::eFallbackDisabled:
       type = NS_LITERAL_STRING("PluginDisabled");
       break;
     case nsObjectLoadingContent::eFallbackBlocklisted:
       type = NS_LITERAL_STRING("PluginBlocklisted");
@@ -654,16 +657,17 @@ nsObjectLoadingContent::nsObjectLoadingC
   : mPendingInstantiateEvent(nullptr)
   , mChannel(nullptr)
   , mType(eType_Loading)
   , mFallbackType(eFallbackAlternate)
   , mChannelLoaded(false)
   , mInstantiating(false)
   , mNetworkCreated(true)
   , mActivated(false)
+  , mPlayPreviewCanceled(false)
   , mIsStopping(false)
   , mIsLoading(false)
   , mSrcStreamLoading(false) {}
 
 nsObjectLoadingContent::~nsObjectLoadingContent()
 {
   // Should have been unbound from the tree at this point, and InDocCheckEvent
   // keeps us alive
@@ -1036,16 +1040,18 @@ nsObjectLoadingContent::ObjectState() co
     case eType_Null:
       switch (mFallbackType) {
         case eFallbackSuppressed:
           return NS_EVENT_STATE_SUPPRESSED;
         case eFallbackUserDisabled:
           return NS_EVENT_STATE_USERDISABLED;
         case eFallbackClickToPlay:
           return NS_EVENT_STATE_TYPE_CLICK_TO_PLAY;
+        case eFallbackPlayPreview:
+          return NS_EVENT_STATE_TYPE_PLAY_PREVIEW;
         case eFallbackDisabled:
           return NS_EVENT_STATE_BROKEN | NS_EVENT_STATE_HANDLER_DISABLED;
         case eFallbackBlocklisted:
           return NS_EVENT_STATE_BROKEN | NS_EVENT_STATE_HANDLER_BLOCKED;
         case eFallbackCrashed:
           return NS_EVENT_STATE_BROKEN | NS_EVENT_STATE_HANDLER_CRASHED;
         case eFallbackUnsupported: {
           // Check to see if plugins are blocked on this platform.
@@ -1422,16 +1428,17 @@ nsObjectLoadingContent::UpdateObjectPara
   //   eAllowPluginSkipChannel, meaning it is possible that we have a URI, but
   //   are not going to open a channel for it. The old objLC code did this (in a
   //   less obviously-intended way), so it's probably best not to change our
   //   behavior at this point.
   //
 
   if (stateInvalid) {
     newType = eType_Null;
+    newMime.Truncate();
   } else if (useChannel) {
       // If useChannel is set above, we considered it in setting newMime
       newType = GetTypeOfContent(newMime);
       LOG(("OBJLC [%p]: Using channel type", this));
   } else if (((caps & eAllowPluginSkipChannel) || !newURI) &&
              (GetTypeOfContent(newMime) == eType_Plugin)) {
     newType = eType_Plugin;
     LOG(("OBJLC [%p]: Skipping loading channel, type plugin", this));
@@ -1640,16 +1647,25 @@ nsObjectLoadingContent::LoadObject(bool 
         //            reject plugins
         fallbackType = eFallbackUserDisabled;
       } else {
         fallbackType = eFallbackSuppressed;
       }
     }
   }
 
+  // Items resolved as Image/Document will not be checked for previews, as well
+  // as invalid plugins (they will not have the mContentType set).
+  if ((mType == eType_Null || mType == eType_Plugin) && ShouldPreview()) {
+    // If plugin preview exists, we shall use it
+    LOG(("OBJLC [%p]: Using plugin preview", this));
+    mType = eType_Null;
+    fallbackType = eFallbackPlayPreview;
+  }
+
   // If we're a plugin but shouldn't start yet, load fallback with
   // reason click-to-play instead
   FallbackType clickToPlayReason;
   if (mType == eType_Plugin && !ShouldPlay(clickToPlayReason)) {
     LOG(("OBJLC [%p]: Marking plugin as click-to-play", this));
     mType = eType_Null;
     fallbackType = clickToPlayReason;
   }
@@ -2446,28 +2462,53 @@ nsObjectLoadingContent::PlayPlugin()
   mActivated = true;
   return LoadObject(true, true);
 }
 
 NS_IMETHODIMP
 nsObjectLoadingContent::GetActivated(bool *aActivated)
 {
   FallbackType reason;
-  *aActivated = ShouldPlay(reason);
+  *aActivated = ShouldPlay(reason) && !ShouldPreview();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsObjectLoadingContent::GetPluginFallbackType(uint32_t* aPluginFallbackType)
 {
   NS_ENSURE_TRUE(nsContentUtils::IsCallerChrome(), NS_ERROR_NOT_AVAILABLE);
   *aPluginFallbackType = mFallbackType;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsObjectLoadingContent::CancelPlayPreview()
+{
+  if (!nsContentUtils::IsCallerChrome())
+    return NS_ERROR_NOT_AVAILABLE;
+
+  if (mPlayPreviewCanceled || mActivated)
+    return NS_OK;
+
+  mPlayPreviewCanceled = true;
+  return LoadObject(true, true);
+}
+
+bool
+nsObjectLoadingContent::ShouldPreview()
+{
+  if (mPlayPreviewCanceled || mActivated)
+    return false;
+
+  nsRefPtr<nsPluginHost> pluginHost =
+    already_AddRefed<nsPluginHost>(nsPluginHost::GetInst());
+
+  return pluginHost->IsPluginPlayPreviewForType(mContentType.get());
+}
+
 bool
 nsObjectLoadingContent::ShouldPlay(FallbackType &aReason)
 {
   // mActivated is true if we've been activated via PlayPlugin() (e.g. user has
   // clicked through). Otherwise, only play if click-to-play is off or if page
   // is whitelisted
 
   nsRefPtr<nsPluginHost> pluginHost =
--- a/content/base/src/nsObjectLoadingContent.h
+++ b/content/base/src/nsObjectLoadingContent.h
@@ -77,17 +77,20 @@ class nsObjectLoadingContent : public ns
       eFallbackSuppressed = nsIObjectLoadingContent::PLUGIN_SUPPRESSED,
       // Blocked by content policy
       eFallbackUserDisabled = nsIObjectLoadingContent::PLUGIN_USER_DISABLED,
       // The plugin is disabled until the user clicks on it
       eFallbackClickToPlay = nsIObjectLoadingContent::PLUGIN_CLICK_TO_PLAY,
       // The plugin is vulnerable (update available)
       eFallbackVulnerableUpdatable = nsIObjectLoadingContent::PLUGIN_VULNERABLE_UPDATABLE,
       // The plugin is vulnerable (no update available)
-      eFallbackVulnerableNoUpdate = nsIObjectLoadingContent::PLUGIN_VULNERABLE_NO_UPDATE
+      eFallbackVulnerableNoUpdate = nsIObjectLoadingContent::PLUGIN_VULNERABLE_NO_UPDATE,
+      // The plugin is disabled and play preview content is displayed until
+      // the extension code enables it by sending the MozPlayPlugin event
+      eFallbackPlayPreview = nsIObjectLoadingContent::PLUGIN_PLAY_PREVIEW
     };
 
     nsObjectLoadingContent();
     virtual ~nsObjectLoadingContent();
 
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSIFRAMELOADEROWNER
@@ -286,16 +289,21 @@ class nsObjectLoadingContent : public ns
     /**
      * If this object is allowed to play plugin content, or if it would display
      * click-to-play instead.
      * NOTE that this does not actually check if the object is a loadable plugin
      */
     bool ShouldPlay(FallbackType &aReason);
 
     /**
+     * If the object should display preview content for the current mContentType
+     */
+    bool ShouldPreview();
+
+    /**
      * Helper to check if our current URI passes policy
      *
      * @param aContentPolicy [out] The result of the content policy decision
      *
      * @return true if call succeeded and NS_CP_ACCEPTED(*aContentPolicy)
      */
     bool CheckLoadPolicy(int16_t *aContentPolicy);
 
@@ -418,16 +426,19 @@ class nsObjectLoadingContent : public ns
     // created using NS_FROM_PARSER_NETWORK flag. If the element is modified,
     // it may lose the flag.
     bool                        mNetworkCreated : 1;
 
     // Used to keep track of whether or not a plugin has been explicitly
     // activated by PlayPlugin(). (see ShouldPlay())
     bool                        mActivated : 1;
 
+    // Used to keep track of whether or not a plugin is blocked by play-preview.
+    bool                        mPlayPreviewCanceled : 1;
+
     // Protects DoStopPlugin from reentry (bug 724781).
     bool                        mIsStopping : 1;
 
     // Protects LoadObject from re-entry
     bool                        mIsLoading : 1;
 
     // Used to track when we might try to instantiate a plugin instance based on
     // a src data stream being delivered to this object. When this is true we
--- a/content/events/public/nsEventStates.h
+++ b/content/events/public/nsEventStates.h
@@ -247,16 +247,18 @@ private:
 // Handler for click to play plugin (vulnerable w/no update)
 #define NS_EVENT_STATE_VULNERABLE_NO_UPDATE NS_DEFINE_EVENT_STATE_MACRO(41)
 // Platform does not support plugin content (some mobile platforms)
 #define NS_EVENT_STATE_TYPE_UNSUPPORTED_PLATFORM NS_DEFINE_EVENT_STATE_MACRO(42)
 // Element is ltr (for :dir pseudo-class)
 #define NS_EVENT_STATE_LTR NS_DEFINE_EVENT_STATE_MACRO(43)
 // Element is rtl (for :dir pseudo-class)
 #define NS_EVENT_STATE_RTL NS_DEFINE_EVENT_STATE_MACRO(44)
+// Handler for play preview plugin
+#define NS_EVENT_STATE_TYPE_PLAY_PREVIEW NS_DEFINE_EVENT_STATE_MACRO(45)
 
 /**
  * NOTE: do not go over 63 without updating nsEventStates::InternalType!
  */
 
 #define DIRECTION_STATES (NS_EVENT_STATE_LTR | NS_EVENT_STATE_RTL)
 
 #define ESM_MANAGED_STATES (NS_EVENT_STATE_ACTIVE | NS_EVENT_STATE_FOCUS |     \
--- a/dom/plugins/base/nsIPluginHost.idl
+++ b/dom/plugins/base/nsIPluginHost.idl
@@ -7,17 +7,17 @@
 #include "nsISupports.idl"
 #include "nsIPluginTag.idl"
 
 %{C++
 #define MOZ_PLUGIN_HOST_CONTRACTID \
   "@mozilla.org/plugin/host;1"
 %}
 
-[scriptable, uuid(28F1F9E1-CD23-4FE2-BCC8-BBB0B2D49A4A)]
+[scriptable, uuid(fdb56ce3-89ac-4293-be64-9f4be88004cc)]
 interface nsIPluginHost : nsISupports
 {
   /**
    * Causes the plugins directory to be searched again for new plugin 
    * libraries.
    *
    * @param reloadPages - indicates whether currently visible pages should 
    * also be reloaded
@@ -67,10 +67,19 @@ interface nsIPluginHost : nsISupports
    * @param plugin: the plugin to query, such as one returned by
    *                nsIPluginHost.getPluginTags.
    * @param domain: the domain to test. If this argument is null, test if data
    *                is stored for any site. The base domain for the given domain
    *                will be determined; if any data for the base domain or its
    *                subdomains is found, return true.
    */
   boolean siteHasData(in nsIPluginTag plugin, in AUTF8String domain);
+
+  /**
+   * Registers the play preview plugin mode for specific mime type
+   *
+   * @param mimeType - specified mime type
+   */
+  void registerPlayPreviewMimeType(in AUTF8String mimeType);
+
+  void unregisterPlayPreviewMimeType(in AUTF8String mimeType);
 };
 
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -1304,16 +1304,27 @@ nsPluginHost::IsPluginClickToPlayForType
       (plugin->HasFlag(NS_PLUGIN_FLAG_CLICKTOPLAY) || mPluginsClickToPlay)) {
     return true;
   }
   else {
     return false;
   }
 }
 
+bool
+nsPluginHost::IsPluginPlayPreviewForType(const char* aMimeType)
+{
+  for (uint32_t i = 0; i < mPlayPreviewMimeTypes.Length(); i++) {
+    nsCString mt = mPlayPreviewMimeTypes[i];
+    if (PL_strcasecmp(mt.get(), aMimeType) == 0)
+      return true;
+  }
+  return false;
+}
+
 nsresult
 nsPluginHost::GetBlocklistStateForType(const char *aMimeType, uint32_t *aState) 
 {
   nsPluginTag *plugin = FindPluginForType(aMimeType, true);
   if (plugin) {
     nsCOMPtr<nsIBlocklistService> blocklist = do_GetService("@mozilla.org/extensions/blocklist;1");
     if (blocklist) {
       // The EmptyString()s are so we use the currently running application
@@ -1811,16 +1822,37 @@ nsPluginHost::EnumerateSiteData(const ns
       break;
     }
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsPluginHost::RegisterPlayPreviewMimeType(const nsACString& mimeType)
+{
+  mPlayPreviewMimeTypes.AppendElement(mimeType);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPluginHost::UnregisterPlayPreviewMimeType(const nsACString& mimeType)
+{
+  nsCAutoString mimeTypeToRemove(mimeType);
+  for (uint32_t i = mPlayPreviewMimeTypes.Length(); i > 0;) {
+    nsCString mt = mPlayPreviewMimeTypes[--i];
+    if (PL_strcasecmp(mt.get(), mimeTypeToRemove.get()) == 0) {
+      mPlayPreviewMimeTypes.RemoveElementAt(i);
+      break;
+    }
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsPluginHost::ClearSiteData(nsIPluginTag* plugin, const nsACString& domain,
                             uint64_t flags, int64_t maxAge)
 {
   // maxAge must be either a nonnegative integer or -1.
   NS_ENSURE_ARG(maxAge >= 0 || maxAge == -1);
 
   // Caller may give us a tag object that is no longer live.
   if (!IsLiveTag(plugin)) {
--- a/dom/plugins/base/nsPluginHost.h
+++ b/dom/plugins/base/nsPluginHost.h
@@ -81,16 +81,17 @@ public:
   nsresult UnloadPlugins();
 
   nsresult SetUpPluginInstance(const char *aMimeType,
                                nsIURI *aURL,
                                nsIPluginInstanceOwner *aOwner);
   nsresult IsPluginEnabledForType(const char* aMimeType);
   nsresult IsPluginEnabledForExtension(const char* aExtension, const char* &aMimeType);
   bool     IsPluginClickToPlayForType(const char *aMimeType);
+  bool     IsPluginPlayPreviewForType(const char *aMimeType);
   nsresult GetBlocklistStateForType(const char *aMimeType, uint32_t *state);
 
   nsresult GetPluginCount(uint32_t* aPluginCount);
   nsresult GetPlugins(uint32_t aPluginCount, nsIDOMPlugin** aPluginArray);
 
   nsresult GetURL(nsISupports* pluginInst,
                   const char* url,
                   const char* target,
@@ -276,16 +277,17 @@ private:
 
   nsresult EnsurePrivateDirServiceProvider();
 
   void OnPluginInstanceDestroyed(nsPluginTag* aPluginTag);
 
   nsRefPtr<nsPluginTag> mPlugins;
   nsRefPtr<nsPluginTag> mCachedPlugins;
   nsRefPtr<nsInvalidPluginTag> mInvalidPlugins;
+  nsTArray<nsCString> mPlayPreviewMimeTypes;
   bool mPluginsLoaded;
   bool mDontShowBadPluginMessage;
 
   // set by pref plugin.override_internal_types
   bool mOverrideInternalTypes;
 
   // set by pref plugin.disable
   bool mPluginsDisabled;
--- a/layout/style/nsCSSPseudoClassList.h
+++ b/layout/style/nsCSSPseudoClassList.h
@@ -149,16 +149,18 @@ CSS_STATE_PSEUDO_CLASS(mozSuppressed, ":
                        NS_EVENT_STATE_SUPPRESSED)
 CSS_STATE_PSEUDO_CLASS(mozLoading, ":-moz-loading", NS_EVENT_STATE_LOADING)
 CSS_STATE_PSEUDO_CLASS(mozTypeUnsupported, ":-moz-type-unsupported",
                        NS_EVENT_STATE_TYPE_UNSUPPORTED)
 CSS_STATE_PSEUDO_CLASS(mozTypeUnsupportedPlatform, ":-moz-type-unsupported-platform",
                        NS_EVENT_STATE_TYPE_UNSUPPORTED_PLATFORM)
 CSS_STATE_PSEUDO_CLASS(mozHandlerClickToPlay, ":-moz-handler-clicktoplay",
                        NS_EVENT_STATE_TYPE_CLICK_TO_PLAY)
+CSS_STATE_PSEUDO_CLASS(mozHandlerPlayPreview, ":-moz-handler-playpreview",
+                       NS_EVENT_STATE_TYPE_PLAY_PREVIEW)
 CSS_STATE_PSEUDO_CLASS(mozHandlerVulnerableUpdatable, ":-moz-handler-vulnerable-updatable",
                        NS_EVENT_STATE_VULNERABLE_UPDATABLE)
 CSS_STATE_PSEUDO_CLASS(mozHandlerVulnerableNoUpdate, ":-moz-handler-vulnerable-no-update",
                        NS_EVENT_STATE_VULNERABLE_NO_UPDATE)
 CSS_STATE_PSEUDO_CLASS(mozHandlerDisabled, ":-moz-handler-disabled",
                        NS_EVENT_STATE_HANDLER_DISABLED)
 CSS_STATE_PSEUDO_CLASS(mozHandlerBlocked, ":-moz-handler-blocked",
                        NS_EVENT_STATE_HANDLER_BLOCKED)
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -2182,16 +2182,17 @@ Tab.prototype = {
     this.browser.addEventListener("DOMContentLoaded", this, true);
     this.browser.addEventListener("DOMLinkAdded", this, true);
     this.browser.addEventListener("DOMTitleChanged", this, true);
     this.browser.addEventListener("DOMWindowClose", this, true);
     this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
     this.browser.addEventListener("scroll", this, true);
     this.browser.addEventListener("MozScrolledAreaChanged", this, true);
     this.browser.addEventListener("PluginClickToPlay", this, true);
+    this.browser.addEventListener("PluginPlayPreview", this, true);
     this.browser.addEventListener("PluginNotFound", this, true);
     this.browser.addEventListener("pageshow", this, true);
 
     Services.obs.addObserver(this, "before-first-paint", false);
     Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
 
     if (!aParams.delayLoad) {
       let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
@@ -2275,16 +2276,17 @@ Tab.prototype = {
     this.browser.removeProgressListener(this);
     this.browser.removeEventListener("DOMContentLoaded", this, true);
     this.browser.removeEventListener("DOMLinkAdded", this, true);
     this.browser.removeEventListener("DOMTitleChanged", this, true);
     this.browser.removeEventListener("DOMWindowClose", this, true);
     this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("PluginClickToPlay", this, true);
+    this.browser.removeEventListener("PluginPlayPreview", this, true);
     this.browser.removeEventListener("PluginNotFound", this, true);
     this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
 
     Services.obs.removeObserver(this, "before-first-paint");
     Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
 
     // Make sure the previously selected panel remains selected. The selected panel of a deck is
     // not stable when panels are removed.
@@ -2837,16 +2839,62 @@ Tab.prototype = {
           tab.clickToPlayPluginsActivated = true;
           PluginHelper.playAllPlugins(win);
 
           NativeWindow.doorhanger.hide("ask-to-play-plugins", tab.id);
         }, true);
         break;
       }
 
+      case "PluginPlayPreview": {
+        let plugin = aEvent.target;
+
+        // Force a style flush, so that we ensure our binding is attached.
+        plugin.clientTop;
+
+        let doc = plugin.ownerDocument;
+        let previewContent = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent");
+        if (!previewContent) {
+          // If the plugin is hidden, fallback to click-to-play logic
+          PluginHelper.stopPlayPreview(plugin, false);
+          break;
+        }
+        let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
+        if (!iframe) {
+          // lazy initialization of the iframe
+          iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+          iframe.className = "previewPluginContentFrame";
+          previewContent.appendChild(iframe);
+
+          // Force a style flush, so that we ensure our binding is attached.
+          plugin.clientTop;
+        }
+        let mimeType = PluginHelper.getPluginMimeType(plugin);
+        let playPreviewUri = "data:application/x-moz-playpreview;," + mimeType;
+        iframe.src = playPreviewUri;
+
+        // MozPlayPlugin event can be dispatched from the extension chrome
+        // code to replace the preview content with the native plugin
+        previewContent.addEventListener("MozPlayPlugin", function playPluginHandler(e) {
+          if (!e.isTrusted)
+            return;
+
+          previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true);
+
+          let playPlugin = !aEvent.detail;
+          PluginHelper.stopPlayPreview(plugin, playPlugin);
+
+          // cleaning up: removes overlay iframe from the DOM
+          let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
+          if (iframe)
+            previewContent.removeChild(iframe);
+        }, true);
+        break;
+      }
+
       case "PluginNotFound": {
         let plugin = aEvent.target;
         plugin.clientTop; // force style flush
 
         // On devices where we don't support Flash, there will be a "Learn More..." link in
         // the missing plugin error message.
         let learnMoreLink = plugin.ownerDocument.getAnonymousElementByAttribute(plugin, "class", "unsupportedLearnMoreLink");
         if (learnMoreLink) {
@@ -5232,20 +5280,32 @@ var PluginHelper = {
     if (!plugins || !plugins.length)
       return;
 
     plugins.forEach(this.playPlugin);
   },
 
   playPlugin: function(plugin) {
     let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    if (!objLoadingContent.activated)
+    if (!objLoadingContent.activated &&
+        objLoadingContent.pluginFallbackType !== Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW)
       objLoadingContent.playPlugin();
   },
 
+  stopPlayPreview: function(plugin, playPlugin) {
+    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+    if (objLoadingContent.activated)
+      return;
+
+    if (playPlugin)
+      objLoadingContent.playPlugin();
+    else
+      objLoadingContent.cancelPlayPreview();
+  },
+
   getPluginPreference: function getPluginPreference() {
     let pluginDisable = Services.prefs.getBoolPref("plugin.disable");
     if (pluginDisable)
       return "0";
 
     let clickToPlay = Services.prefs.getBoolPref("plugins.click_to_play");
     return clickToPlay ? "2" : "1";
   },
@@ -5272,16 +5332,32 @@ var PluginHelper = {
     // Is the <object>'s size too small to hold what we want to show?
     let pluginRect = plugin.getBoundingClientRect();
     // XXX bug 446693. The text-shadow on the submitted-report text at
     //     the bottom causes scrollHeight to be larger than it should be.
     let overflows = (overlay.scrollWidth > pluginRect.width) ||
                     (overlay.scrollHeight - 5 > pluginRect.height);
 
     return overflows;
+  },
+
+  getPluginMimeType: function (plugin) {
+    var tagMimetype;
+    if (plugin instanceof HTMLAppletElement) {
+      tagMimetype = "application/x-java-vm";
+    } else {
+      tagMimetype = plugin.QueryInterface(Components.interfaces.nsIObjectLoadingContent)
+                          .actualType;
+
+      if (tagMimetype == "") {
+        tagMimetype = plugin.type;
+      }
+    }
+
+    return tagMimetype;
   }
 };
 
 var PermissionsHelper = {
 
   _permissonTypes: ["password", "geolocation", "popup", "indexedDB",
                     "offline-app", "desktop-notification", "plugins"],
   _permissionStrings: {
--- a/toolkit/mozapps/plugins/content/pluginProblem.xml
+++ b/toolkit/mozapps/plugins/content/pluginProblem.xml
@@ -51,12 +51,13 @@
                 <!-- link href set at runtime -->
                 <html:div class="msg msgReload">&reloadPlugin.pre;<html:a class="reloadLink" href="">&reloadPlugin.middle;</html:a>&reloadPlugin.post;</html:div>
             </html:div>
             <xul:spacer flex="1"/>
             <html:div class="msg msgBottomLinks">
                 <html:span class="helpIcon" role="link"/>
             </html:div>
         </xul:vbox>
+        <html:div class="previewPluginContent"><!-- iframe and its src will be set at runtime --></html:div>
         <html:div style="display:none;"><children/></html:div>
     </content>
 </binding>
 </bindings>
--- a/toolkit/mozapps/plugins/content/pluginProblemBinding.css
+++ b/toolkit/mozapps/plugins/content/pluginProblemBinding.css
@@ -3,26 +3,29 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @namespace url(http://www.w3.org/1999/xhtml); /* set default namespace to HTML */
 
 embed:-moz-handler-disabled,
 embed:-moz-handler-blocked,
 embed:-moz-handler-crashed,
 embed:-moz-handler-clicktoplay,
+embed:-moz-handler-playpreview,
 embed:-moz-handler-vulnerable-updatable,
 embed:-moz-handler-vulnerable-no-update,
 applet:-moz-handler-disabled,
 applet:-moz-handler-blocked,
 applet:-moz-handler-crashed,
 applet:-moz-handler-clicktoplay,
+applet:-moz-handler-playpreview,
 applet:-moz-handler-vulnerable-updatable,
 applet:-moz-handler-vulnerable-no-update,
 object:-moz-has-handlerref:-moz-handler-disabled,
 object:-moz-has-handlerref:-moz-handler-blocked,
 object:-moz-handler-crashed,
 object:-moz-handler-clicktoplay,
+object:-moz-handler-playpreview,
 object:-moz-handler-vulnerable-updatable,
 object:-moz-handler-vulnerable-no-update {
     display: inline-block;
     overflow: hidden;
     -moz-binding: url('chrome://mozapps/content/plugins/pluginProblem.xml#pluginProblem') !important;
 }
--- a/toolkit/mozapps/plugins/content/pluginProblemContent.css
+++ b/toolkit/mozapps/plugins/content/pluginProblemContent.css
@@ -42,16 +42,37 @@ html|applet:not([height]), html|applet[h
   direction: ltr;
   unicode-bidi: embed;
 }
 
 .mainBox[chromedir="rtl"] {
   direction: rtl;
 }
 
+:-moz-handler-playpreview .mainBox {
+  display: none;
+}
+
+.previewPluginContent {
+  display: none;
+}
+
+.previewPluginContent > iframe {
+  width: inherit;
+  height: inherit;
+  border: none;
+}
+
+:-moz-handler-playpreview .previewPluginContent {
+  display: block;
+  width: inherit;
+  height: inherit;
+  overflow: hidden;
+}
+
 .msg {
   display: none;
 }
 
 :-moz-type-unsupported .msgUnsupported,
 :-moz-type-unsupported-platform .msgUnsupportedPlatform,
 :-moz-handler-clicktoplay .msgClickToPlay,
 :-moz-handler-vulnerable-updatable .msgVulnerableUpdatable,