Backed out 2 changesets (bug 1479270) for android lint on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Fri, 24 Aug 2018 20:16:14 +0300
changeset 491095 bbbab4206c4696bf21ae5e8b93f06bd38dd015b3
parent 491094 f83d8d85c94f2d1cab19085ec1662808def93fe9
child 491096 92426f37b247c4876a4d9df0394dc36bd96e804c
push id1815
push userffxbld-merge
push dateMon, 15 Oct 2018 10:40:45 +0000
treeherdermozilla-release@18d4c09e9378 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1479270
milestone63.0a1
backs out1676f895a7102a9fab1a84c2a5e43449edd7f870
643ef11ea7208e35571e6b8a2beba280a7829fb8
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 2 changesets (bug 1479270) for android lint on a CLOSED TREE Backed out changeset 1676f895a710 (bug 1479270) Backed out changeset 643ef11ea720 (bug 1479270)
dom/html/HTMLMediaElement.cpp
dom/html/HTMLMediaElement.h
mobile/android/app/src/main/res/layout/gecko_app.xml
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java
mobile/android/chrome/content/browser.js
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -1372,16 +1372,17 @@ private:
   bool mCancelled = false;
 };
 
 class HTMLMediaElement::ErrorSink
 {
 public:
   explicit ErrorSink(HTMLMediaElement* aOwner)
     : mOwner(aOwner)
+    , mSrcIsUnsupportedTypeMedia(false)
   {
     MOZ_ASSERT(mOwner);
   }
 
   void SetError(uint16_t aErrorCode, const nsACString& aErrorDetails)
   {
     // Since we have multiple paths calling into DecodeError, e.g.
     // MediaKeys::Terminated and EMEH264Decoder::Error. We should take the 1st
@@ -1390,54 +1391,94 @@ public:
       return;
     }
 
     if (!IsValidErrorCode(aErrorCode)) {
       NS_ASSERTION(false, "Undefined MediaError codes!");
       return;
     }
 
-
-    mError = new MediaError(mOwner, aErrorCode, aErrorDetails);
-    mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("error"));
-    if (mOwner->ReadyState() == HAVE_NOTHING &&
-        aErrorCode == MEDIA_ERR_ABORTED) {
-      // https://html.spec.whatwg.org/multipage/embedded-content.html#media-data-processing-steps-list
-      // "If the media data fetching process is aborted by the user"
-      mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("abort"));
-      mOwner->ChangeNetworkState(NETWORK_EMPTY);
-      mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("emptied"));
-      if (mOwner->mDecoder) {
-        mOwner->ShutdownDecoder();
+    // TODO : remove unsupported type related codes after finishing native
+    // support for HLS, see bug 1350842.
+    if (CanOwnerPlayUnsupportedTypeMedia() &&
+        aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) {
+      // On Fennec, we do some hack for unsupported type media, we don't set
+      // its error state in order to open it with external app.
+      mSrcIsUnsupportedTypeMedia = true;
+      mOwner->ChangeNetworkState(NETWORK_NO_SOURCE);
+      MaybeOpenUnsupportedMediaForOwner();
+    } else {
+      mError = new MediaError(mOwner, aErrorCode, aErrorDetails);
+      mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("error"));
+      if (mOwner->ReadyState() == HAVE_NOTHING &&
+          aErrorCode == MEDIA_ERR_ABORTED) {
+        // https://html.spec.whatwg.org/multipage/embedded-content.html#media-data-processing-steps-list
+        // "If the media data fetching process is aborted by the user"
+        mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("abort"));
+        mOwner->ChangeNetworkState(NETWORK_EMPTY);
+        mOwner->DispatchAsyncEvent(NS_LITERAL_STRING("emptied"));
+        if (mOwner->mDecoder) {
+          mOwner->ShutdownDecoder();
+        }
+      } else if (aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) {
+        mOwner->ChangeNetworkState(NETWORK_NO_SOURCE);
+      } else {
+        mOwner->ChangeNetworkState(NETWORK_IDLE);
       }
-    } else if (aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) {
-      mOwner->ChangeNetworkState(NETWORK_NO_SOURCE);
-    } else {
-      mOwner->ChangeNetworkState(NETWORK_IDLE);
     }
   }
 
   void ResetError()
   {
     mError = nullptr;
+    mSrcIsUnsupportedTypeMedia = false;
+  }
+
+  void MaybeOpenUnsupportedMediaForOwner() const
+  {
+    // Src is supported type or we don't open the pref for external app.
+    if (!mSrcIsUnsupportedTypeMedia || !CanOwnerPlayUnsupportedTypeMedia()) {
+      return;
+    }
+
+    // If media doesn't start playing, we don't need to open it.
+    if (mOwner->Paused()) {
+      return;
+    }
+
+    nsContentUtils::DispatchTrustedEvent(
+      mOwner->OwnerDoc(),
+      static_cast<nsIContent*>(mOwner),
+      NS_LITERAL_STRING("OpenMediaWithExternalApp"),
+      CanBubble::eYes, Cancelable::eYes);
   }
 
   RefPtr<MediaError> mError;
 
 private:
   bool IsValidErrorCode(const uint16_t& aErrorCode) const
   {
     return (aErrorCode == MEDIA_ERR_DECODE || aErrorCode == MEDIA_ERR_NETWORK ||
             aErrorCode == MEDIA_ERR_ABORTED ||
             aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED);
   }
 
+  bool CanOwnerPlayUnsupportedTypeMedia() const
+  {
+#if defined(MOZ_WIDGET_ANDROID)
+    // On Fennec, we will use an external app to open unsupported media types.
+    return Preferences::GetBool("media.openUnsupportedTypeWithExternalApp");
+#endif
+    return false;
+  }
+
   // Media elememt's life cycle would be longer than error sink, so we use the
   // raw pointer and this class would only be referenced by media element.
   HTMLMediaElement* mOwner;
+  bool mSrcIsUnsupportedTypeMedia;
 };
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement,
                                                   nsGenericHTMLElement)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcMediaSource)
@@ -6972,16 +7013,22 @@ HTMLMediaElement::GetSrcMediaStream() co
 
 MediaError*
 HTMLMediaElement::GetError() const
 {
   return mErrorSink->mError;
 }
 
 void
+HTMLMediaElement::OpenUnsupportedMediaWithExternalAppIfNeeded() const
+{
+  mErrorSink->MaybeOpenUnsupportedMediaForOwner();
+}
+
+void
 HTMLMediaElement::GetCurrentSpec(nsCString& aString)
 {
   if (mLoadingSrc) {
     mLoadingSrc->GetSpec(aString);
   } else {
     aString.Truncate();
   }
 }
@@ -7915,16 +7962,17 @@ HTMLMediaElement::MarkAsContentSource(Ca
          IsInComposedDoc(),
          static_cast<int>(aAPI)));
   }
 }
 
 void
 HTMLMediaElement::UpdateCustomPolicyAfterPlayed()
 {
+  OpenUnsupportedMediaWithExternalAppIfNeeded();
   if (mAudioChannelWrapper) {
     mAudioChannelWrapper->NotifyPlayStateChanged();
   }
 }
 
 AbstractThread*
 HTMLMediaElement::AbstractMainThread() const
 {
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -1305,16 +1305,20 @@ protected:
 
   class nsAsyncEventRunner;
   class nsNotifyAboutPlayingRunner;
   class nsResolveOrRejectPendingPlayPromisesRunner;
   using nsGenericHTMLElement::DispatchEvent;
   // For nsAsyncEventRunner.
   nsresult DispatchEvent(const nsAString& aName);
 
+  // Open unsupported types media with the external app when the media element
+  // triggers play() after loaded fail. eg. preload the data before start play.
+  void OpenUnsupportedMediaWithExternalAppIfNeeded() const;
+
   // This method moves the mPendingPlayPromises into a temperate object. So the
   // mPendingPlayPromises is cleared after this method call.
   nsTArray<RefPtr<PlayPromise>> TakePendingPlayPromises();
 
   // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises()
   // and queues a task to resolve them.
   void AsyncResolvePendingPlayPromises();
 
--- a/mobile/android/app/src/main/res/layout/gecko_app.xml
+++ b/mobile/android/app/src/main/res/layout/gecko_app.xml
@@ -32,16 +32,21 @@
                                              android:layout_height="match_parent"
                                              android:scrollbars="none"/>
 
             <org.mozilla.gecko.FormAssistPopup android:id="@+id/form_assist_popup"
                                                android:layout_width="match_parent"
                                                android:layout_height="match_parent"
                                                android:visibility="gone"/>
 
+            <view class="org.mozilla.gecko.media.VideoPlayer" android:id="@+id/video_player"
+                         android:layout_height="match_parent"
+                         android:layout_width="match_parent">
+            </view>
+
             <FrameLayout android:id="@+id/home_screen_container"
                          android:layout_width="match_parent"
                          android:layout_height="match_parent"
                          android:visibility="gone">
 
                 <ViewStub android:id="@+id/home_pager_stub"
                           android:layout="@layout/home_pager"
                           android:layout_width="match_parent"
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -105,16 +105,17 @@ import org.mozilla.gecko.home.HomeScreen
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.icons.IconsHelper;
 import org.mozilla.gecko.icons.decoders.FaviconDecoder;
 import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.media.PictureInPictureController;
+import org.mozilla.gecko.media.VideoPlayer;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mma.MmaDelegate;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.NotificationHelper;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
@@ -226,16 +227,17 @@ public class BrowserApp extends GeckoApp
     public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
 
     private BrowserSearch mBrowserSearch;
     private View mBrowserSearchContainer;
 
     public ViewGroup mBrowserChrome;
     public ViewFlipper mActionBarFlipper;
     public ActionModeCompatView mActionBar;
+    private VideoPlayer mVideoPlayer;
     private PictureInPictureController mPipController;
     private BrowserToolbar mBrowserToolbar;
     private View doorhangerOverlay;
     // We can't name the TabStrip class because it's not included on API 9.
     private TabStripInterface mTabStrip;
     private AnimatedProgressBar mProgressView;
     private HomeScreen mHomeScreen;
     private TabsPanel mTabsPanel;
@@ -367,16 +369,20 @@ public class BrowserApp extends GeckoApp
             }
 
             return;
         }
 
         Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
         switch (msg) {
             case SELECTED:
+                if (mVideoPlayer.isPlaying()) {
+                    mVideoPlayer.stop();
+                }
+
                 if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) {
                     final VisibilityTransition transition = (tab.getShouldShowToolbarWithoutAnimationOnFirstSelection()) ?
                             VisibilityTransition.IMMEDIATE : VisibilityTransition.ANIMATE;
                     mDynamicToolbar.setVisible(true, transition);
 
                     // The first selection has happened - reset the state.
                     tab.setShouldShowToolbarWithoutAnimationOnFirstSelection(false);
                 }
@@ -642,16 +648,26 @@ public class BrowserApp extends GeckoApp
 
         mOnboardingHelper = new OnboardingHelper(this, safeStartingIntent);
         initSwitchboardAndMma(this, safeStartingIntent, isInAutomation);
         initTelemetryUploader(isInAutomation);
 
         mBrowserChrome = (ViewGroup) findViewById(R.id.browser_chrome);
         mActionBarFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
         mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
+
+        mVideoPlayer = (VideoPlayer) findViewById(R.id.video_player);
+        mVideoPlayer.setFullScreenListener(new VideoPlayer.FullScreenListener() {
+            @Override
+            public void onFullScreenChanged(boolean fullScreen) {
+                mVideoPlayer.setFullScreen(fullScreen);
+                setFullScreen(fullScreen);
+            }
+        });
+
         mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
         mBrowserToolbar.setTouchEventInterceptor(new TouchEventInterceptor() {
             @Override
             public boolean onInterceptTouchEvent(View view, MotionEvent event) {
                 // Manually dismiss text selection bar if it's not overlaying the toolbar.
                 mTextSelection.dismiss();
                 return false;
             }
@@ -760,16 +776,17 @@ public class BrowserApp extends GeckoApp
             "Search:Keyword",
             null);
 
         EventDispatcher.getInstance().registerUiThreadListener(this,
             "GeckoView:AccessibilityEnabled",
             "Menu:Open",
             "LightweightTheme:Update",
             "Tab:Added",
+            "Video:Play",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Settings:Show",
             "Updater:Launch",
             "Sanitize:Finished",
             "Sanitize:OpenTabs",
             "NotificationSettings:FeatureTipsStatusUpdated",
             null);
@@ -952,16 +969,27 @@ public class BrowserApp extends GeckoApp
             endActionMode();
             return;
         }
 
         if (hideFirstrunPager(TelemetryContract.Method.BACK)) {
             return;
         }
 
+        if (mVideoPlayer.isFullScreen()) {
+            mVideoPlayer.setFullScreen(false);
+            setFullScreen(false);
+            return;
+        }
+
+        if (mVideoPlayer.isPlaying()) {
+            mVideoPlayer.stop();
+            return;
+        }
+
         super.onBackPressed();
     }
 
     @Override
     public void onAttachedToWindow() {
         final SafeIntent intent = new SafeIntent(getIntent());
 
         if (!IntentUtils.getIsInAutomationFromEnvironment(intent)) {
@@ -1483,16 +1511,17 @@ public class BrowserApp extends GeckoApp
             "Search:Keyword",
             null);
 
         EventDispatcher.getInstance().unregisterUiThreadListener(this,
             "GeckoView:AccessibilityEnabled",
             "Menu:Open",
             "LightweightTheme:Update",
             "Tab:Added",
+            "Video:Play",
             "CharEncoding:Data",
             "CharEncoding:State",
             "Settings:Show",
             "Updater:Launch",
             "Sanitize:Finished",
             "Sanitize:OpenTabs",
             "NotificationSettings:FeatureTipsStatusUpdated",
             null);
@@ -1766,16 +1795,24 @@ public class BrowserApp extends GeckoApp
                 if (message.getBoolean("cancelEditMode")) {
                     // Set the target tab to null so it does not get selected (on editing
                     // mode exit) in lieu of the tab that we're going to open and select.
                     mTargetTabForEditingMode = null;
                     mBrowserToolbar.cancelEdit();
                 }
                 break;
 
+            case "Video:Play":
+                if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) {
+                    mVideoPlayer.start(Uri.parse(message.getString("uri")));
+                    Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
+                                          TelemetryContract.Method.CONTENT, "playhls");
+                }
+                break;
+
             case "CharEncoding:Data":
                 final GeckoBundle[] charsets = message.getBundleArray("charsets");
                 final int selected = message.getInt("selected");
 
                 final String[] titleArray = new String[charsets.length];
                 final String[] codeArray = new String[charsets.length];
                 for (int i = 0; i < charsets.length; i++) {
                     final GeckoBundle charset = charsets[i];
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/VideoPlayer.java
@@ -0,0 +1,204 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.media;
+
+import android.content.Context;
+
+import android.graphics.Color;
+
+import android.net.Uri;
+
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import android.widget.ImageButton;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.MediaController;
+import android.widget.VideoView;
+
+import org.mozilla.gecko.R;
+
+public class VideoPlayer extends FrameLayout {
+    private VideoView video;
+    private FullScreenMediaController controller;
+    private FullScreenListener fullScreenListener;
+
+    private boolean isFullScreen;
+
+    public VideoPlayer(Context ctx) {
+        this(ctx, null);
+    }
+
+    public VideoPlayer(Context ctx, AttributeSet attrs) {
+        this(ctx, attrs, 0);
+    }
+
+    public VideoPlayer(Context ctx, AttributeSet attrs, int defStyle) {
+        super(ctx, attrs, defStyle);
+        setFullScreen(false);
+        setVisibility(View.GONE);
+    }
+
+    public void start(Uri uri) {
+        stop();
+
+        video = new VideoView(getContext());
+        controller = new FullScreenMediaController(getContext());
+        video.setMediaController(controller);
+        controller.setAnchorView(video);
+
+        video.setVideoURI(uri);
+
+        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+            FrameLayout.LayoutParams.MATCH_PARENT,
+            FrameLayout.LayoutParams.WRAP_CONTENT,
+            Gravity.CENTER);
+
+        addView(video, layoutParams);
+        setVisibility(View.VISIBLE);
+
+        video.setZOrderOnTop(true);
+        video.start();
+    }
+
+    public boolean isPlaying() {
+        return video != null;
+    }
+
+    public void stop() {
+        if (video == null) {
+            return;
+        }
+
+        removeAllViews();
+        setVisibility(View.GONE);
+        video.stopPlayback();
+
+        video = null;
+        controller = null;
+    }
+
+    public void setFullScreenListener(FullScreenListener listener) {
+        fullScreenListener = listener;
+    }
+
+    public boolean isFullScreen() {
+        return isFullScreen;
+    }
+
+    public void setFullScreen(boolean fullScreen) {
+        isFullScreen = fullScreen;
+        if (fullScreen) {
+            setBackgroundColor(Color.BLACK);
+        } else {
+            setBackgroundResource(R.color.dark_transparent_overlay);
+        }
+
+        if (controller != null) {
+            controller.setFullScreen(fullScreen);
+        }
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (event.isSystem()) {
+            return super.onKeyDown(keyCode, event);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (event.isSystem()) {
+            return super.onKeyUp(keyCode, event);
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        super.onTouchEvent(event);
+        return true;
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent event) {
+        super.onTrackballEvent(event);
+        return true;
+    }
+
+    public interface FullScreenListener {
+        void onFullScreenChanged(boolean fullScreen);
+    }
+
+    private class FullScreenMediaController extends MediaController {
+        private ImageButton mButton;
+
+        public FullScreenMediaController(Context ctx) {
+            super(ctx);
+
+            mButton = new ImageButton(getContext());
+            mButton.setScaleType(ImageView.ScaleType.FIT_CENTER);
+            mButton.setBackgroundColor(Color.TRANSPARENT);
+            mButton.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    FullScreenMediaController.this.onFullScreenClicked();
+                }
+            });
+
+            updateFullScreenButton(false);
+        }
+
+        public void setFullScreen(boolean fullScreen) {
+            updateFullScreenButton(fullScreen);
+        }
+
+        private void updateFullScreenButton(boolean fullScreen) {
+            mButton.setImageResource(fullScreen ? R.drawable.exit_fullscreen : R.drawable.fullscreen);
+        }
+
+        private void onFullScreenClicked() {
+            if (VideoPlayer.this.fullScreenListener != null) {
+                boolean fullScreen = !VideoPlayer.this.isFullScreen();
+                VideoPlayer.this.fullScreenListener.onFullScreenChanged(fullScreen);
+            }
+        }
+
+        @Override
+        public void setAnchorView(final View view) {
+            super.setAnchorView(view);
+
+            // Add the fullscreen button here because this is where the parent class actually creates
+            // the media buttons and their layout.
+            //
+            // http://androidxref.com/6.0.1_r10/xref/frameworks/base/core/java/android/widget/MediaController.java#239
+            //
+            // The media buttons are in a horizontal linear layout which is itself packed into
+            // a vertical layout. The vertical layout is the only child of the FrameLayout which
+            // MediaController inherits from.
+            LinearLayout child = (LinearLayout) getChildAt(0);
+            LinearLayout buttons = (LinearLayout) child.getChildAt(0);
+
+            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+                                                                             LayoutParams.MATCH_PARENT);
+            params.gravity = Gravity.CENTER_VERTICAL;
+
+            if (mButton.getParent() != null) {
+                ((ViewGroup)mButton.getParent()).removeView(mButton);
+            }
+
+            buttons.addView(mButton, params);
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -4843,16 +4843,17 @@ Tab.prototype = {
 };
 
 var BrowserEventHandler = {
   init: function init() {
     BrowserApp.deck.addEventListener("touchend", this, true);
 
     BrowserApp.deck.addEventListener("DOMUpdateBlockedPopups", PopupBlockerObserver.onUpdateBlockedPopups);
     BrowserApp.deck.addEventListener("MozMouseHittest", this, true);
+    BrowserApp.deck.addEventListener("OpenMediaWithExternalApp", this, true);
 
     // ReaderViews support backPress listeners.
     WindowEventDispatcher.registerListener((event, data, callback) => {
       callback.onSuccess(Reader.onBackPress(BrowserApp.selectedTab.id));
     }, "Browser:OnBackPressed");
   },
 
   handleEvent: function(aEvent) {
@@ -4860,16 +4861,26 @@ var BrowserEventHandler = {
       case 'touchend':
         if (this._inCluster) {
           aEvent.preventDefault();
         }
         break;
       case 'MozMouseHittest':
         this._handleRetargetedTouchStart(aEvent);
         break;
+      case 'OpenMediaWithExternalApp': {
+        let mediaSrc = aEvent.target.currentSrc || aEvent.target.src;
+        let uuid = uuidgen.generateUUID().toString();
+        GlobalEventDispatcher.sendRequest({
+          type: "Video:Play",
+          uri: mediaSrc,
+          uuid: uuid
+        });
+        break;
+      }
     }
   },
 
   _handleRetargetedTouchStart: function(aEvent) {
     // we should only get this called just after a new touchstart with a single
     // touch point.
     if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.defaultPrevented) {
       return;