Bug 1458039 - Implement ability to mute/unmute a selection of tabs. r?jaws draft multiselect_mute_unmute_tabs
authorlayely <ablayelyfondou@gmail.com>
Thu, 31 May 2018 03:26:25 +0000
branchmultiselect_mute_unmute_tabs
changeset 808809 c96907fb2e1c3d380ff78674952fe040a0d05dfa
parent 808726 681eb7dfa324dd50403c382888929ea8b8b11b00
push id113498
push userbmo:ablayelyfondou@gmail.com
push dateWed, 20 Jun 2018 20:13:21 +0000
reviewersjaws
bugs1458039
milestone62.0a1
Bug 1458039 - Implement ability to mute/unmute a selection of tabs. r?jaws MozReview-Commit-ID: 7aw08gxOOtk
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_multiselect_tabs_close.js
browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js
browser/locales/en-US/chrome/browser/browser.properties
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7888,31 +7888,34 @@ var MenuTouchModeObserver = {
 
   uninit() {
     window.removeEventListener("popupshowing", this, true);
   },
 };
 
 var TabContextMenu = {
   contextTab: null,
-  _updateToggleMuteMenuItem(aTab, aConditionFn) {
+  _updateToggleMuteMenuItems(aTab, aConditionFn) {
     ["muted", "soundplaying"].forEach(attr => {
       if (!aConditionFn || aConditionFn(attr)) {
         if (aTab.hasAttribute(attr)) {
           aTab.toggleMuteMenuItem.setAttribute(attr, "true");
+          aTab.toggleMultiSelectMuteMenuItem.setAttribute(attr, "true");
         } else {
           aTab.toggleMuteMenuItem.removeAttribute(attr);
+          aTab.toggleMultiSelectMuteMenuItem.removeAttribute(attr);
         }
       }
     });
   },
   updateContextMenu: function updateContextMenu(aPopupMenu) {
     this.contextTab = aPopupMenu.triggerNode.localName == "tab" ?
                       aPopupMenu.triggerNode : gBrowser.selectedTab;
     let disabled = gBrowser.tabs.length == 1;
+    let multiselectionContext = this.contextTab.multiselected;
 
     var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
     for (let menuItem of menuItems)
       menuItem.disabled = disabled;
 
     if (this.contextTab.hasAttribute("customizemode"))
       document.getElementById("context_openTabInWindow").disabled = true;
 
@@ -7937,58 +7940,76 @@ var TabContextMenu = {
     // Disable "Close other Tabs" if there are no unpinned tabs.
     let unpinnedTabsToClose = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs;
     if (!this.contextTab.pinned) {
       unpinnedTabsToClose--;
     }
     document.getElementById("context_closeOtherTabs").disabled = unpinnedTabsToClose < 1;
 
     // Only one of close_tab/close_selected_tabs should be visible
-    let hasMultiSelectedTabs = !!gBrowser.multiSelectedTabsCount;
-    document.getElementById("context_closeTab").hidden = hasMultiSelectedTabs;
-    document.getElementById("context_closeSelectedTabs").hidden = !hasMultiSelectedTabs;
+    document.getElementById("context_closeTab").hidden = multiselectionContext;
+    document.getElementById("context_closeSelectedTabs").hidden = !multiselectionContext;
 
     // Hide "Bookmark All Tabs" for a pinned tab.  Update its state if visible.
     let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs");
     bookmarkAllTabs.hidden = this.contextTab.pinned;
     if (!bookmarkAllTabs.hidden)
       PlacesCommandHook.updateBookmarkAllTabsCommand();
 
+    let toggleMute = document.getElementById("context_toggleMuteTab");
+    let toggleMultiSelectMute = document.getElementById("context_toggleMuteSelectedTabs");
+
+    // Only one of mute_unmute_tab/mute_unmute_selected_tabs should be visible
+    toggleMute.hidden = multiselectionContext;
+    toggleMultiSelectMute.hidden = !multiselectionContext;
+
     // Adjust the state of the toggle mute menu item.
-    let toggleMute = document.getElementById("context_toggleMuteTab");
     if (this.contextTab.hasAttribute("activemedia-blocked")) {
       toggleMute.label = gNavigatorBundle.getString("playTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("playTab.accesskey");
     } else if (this.contextTab.hasAttribute("muted")) {
       toggleMute.label = gNavigatorBundle.getString("unmuteTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("unmuteTab.accesskey");
     } else {
       toggleMute.label = gNavigatorBundle.getString("muteTab.label");
       toggleMute.accessKey = gNavigatorBundle.getString("muteTab.accesskey");
     }
 
+    // Adjust the state of the toggle mute menu item for multi-selected tabs.
+    if (this.contextTab.hasAttribute("activemedia-blocked")) {
+      toggleMultiSelectMute.label = gNavigatorBundle.getString("playTabs.label");
+      toggleMultiSelectMute.accessKey = gNavigatorBundle.getString("playTabs.accesskey");
+    } else if (this.contextTab.hasAttribute("muted")) {
+      toggleMultiSelectMute.label = gNavigatorBundle.getString("unmuteSelectedTabs.label");
+      toggleMultiSelectMute.accessKey = gNavigatorBundle.getString("unmuteSelectedTabs.accesskey");
+    } else {
+      toggleMultiSelectMute.label = gNavigatorBundle.getString("muteSelectedTabs.label");
+      toggleMultiSelectMute.accessKey = gNavigatorBundle.getString("muteSelectedTabs.accesskey");
+    }
+
     this.contextTab.toggleMuteMenuItem = toggleMute;
-    this._updateToggleMuteMenuItem(this.contextTab);
+    this.contextTab.toggleMultiSelectMuteMenuItem = toggleMultiSelectMute;
+    this._updateToggleMuteMenuItems(this.contextTab);
 
     this.contextTab.addEventListener("TabAttrModified", this);
     aPopupMenu.addEventListener("popuphiding", this);
 
     gSync.updateTabContextMenu(aPopupMenu, this.contextTab);
 
     updateTabMenuUserContextUIVisibility("context_reopenInContainer");
   },
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "popuphiding":
         gBrowser.removeEventListener("TabAttrModified", this);
         aEvent.target.removeEventListener("popuphiding", this);
         break;
       case "TabAttrModified":
         let tab = aEvent.target;
-        this._updateToggleMuteMenuItem(tab,
+        this._updateToggleMuteMenuItems(tab,
           attr => aEvent.detail.changed.includes(attr));
         break;
     }
   }
 };
 
 // Prompt user to restart the browser in safe mode
 function safeModeRestart() {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -95,16 +95,18 @@
 
   <popupset id="mainPopupSet">
     <menupopup id="tabContextMenu"
                onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);"
                onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;">
       <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;"
                 oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/>
+      <menuitem id="context_toggleMuteSelectedTabs" hidden="true"
+                oncommand="gBrowser.toggleMuteAudioOnMultiSelectedTabs(TabContextMenu.contextTab);"/>
       <menuseparator/>
       <menuitem id="context_pinTab" label="&pinTab.label;"
                 accesskey="&pinTab.accesskey;"
                 oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true"
                 accesskey="&unpinTab.accesskey;"
                 oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_duplicateTab" label="&duplicateTab.label;"
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -3699,16 +3699,40 @@ window._gBrowser = {
     }
     return gBrowser.selectedTab;
   },
 
   set lastMultiSelectedTab(aTab) {
     this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
   },
 
+  toggleMuteAudioOnMultiSelectedTabs(aTab) {
+    const selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
+                                    .filter(tab => tab.isConnected);
+    let tabsToToggle;
+
+    if (aTab.activeMediaBlocked) {
+      tabsToToggle = selectedTabs.filter(tab =>
+        tab.activeMediaBlocked || tab.linkedBrowser.audioMuted
+      );
+    } else {
+      let tabMuted = aTab.linkedBrowser.audioMuted;
+      tabsToToggle = selectedTabs.filter(tab =>
+        // When a user is looking to mute selected tabs, then media-blocked tabs
+        // should not be toggled. Otherwise those media-blocked tabs are going into a
+        // playing and unmuted state.
+        tab.linkedBrowser.audioMuted == tabMuted && !tab.activeMediaBlocked ||
+        tab.activeMediaBlocked && tabMuted
+      );
+    }
+    for (let tab of tabsToToggle) {
+      tab.toggleMuteAudio();
+    }
+  },
+
   activateBrowserForPrintPreview(aBrowser) {
     this._printPreviewBrowsers.add(aBrowser);
     if (this._switcher) {
       this._switcher.activateBrowserForPrintPreview(aBrowser);
     }
     aBrowser.docShellIsActive = true;
   },
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2010,30 +2010,34 @@
             } else {
               gBrowser.addToMultiSelectedTabs(this);
               gBrowser.lastMultiSelectedTab = this;
             }
             return;
           }
 
           const overCloseButton = event.originalTarget.getAttribute("anonid") == "close-button";
-          if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton) {
+          if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton && !this._overPlayingIcon) {
             // Tabs were previously multi-selected and user clicks on a tab
             // without holding Ctrl/Cmd Key
 
             // Force positional attributes to update when the
             // target (of the click) is the "active" tab.
             let updatePositionalAttr = gBrowser.selectedTab == this;
 
             gBrowser.clearMultiSelectedTabs(updatePositionalAttr);
           }
         }
 
         if (this._overPlayingIcon) {
-          this.toggleMuteAudio();
+          if (this.multiselected) {
+            gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
+          } else {
+            this.toggleMuteAudio();
+          }
           return;
         }
 
         if (event.originalTarget.getAttribute("anonid") == "close-button") {
           if (this.multiselected) {
             gBrowser.removeMultiSelectedTabs();
           } else {
             gBrowser.removeTab(this, {
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -45,8 +45,12 @@ skip-if = (debug && os == 'mac') || (deb
 [browser_visibleTabs_contextMenu.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
 [browser_multiselect_tabs_using_Ctrl.js]
 [browser_multiselect_tabs_using_Shift.js]
 [browser_multiselect_tabs_close.js]
 [browser_multiselect_tabs_positional_attrs.js]
 [browser_multiselect_tabs_close_using_shortcuts.js]
+[browser_multiselect_tabs_mute_unmute.js]
+support-files =
+  ../general/audio.ogg
+  ../general/file_mediaPlayback.html
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close.js
@@ -64,32 +64,32 @@ add_task(async function usingTabContextM
     let tab3 = await addTab();
     let tab4 = await addTab();
 
     let menuItemCloseTab = document.getElementById("context_closeTab");
     let menuItemCloseSelectedTabs = document.getElementById("context_closeSelectedTabs");
 
     is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
 
-    // Check the context menu with zero multiselected tabs
-    updateTabContextMenu(tab4);
-    is(menuItemCloseTab.hidden, false, "Close Tab is visible");
-    is(menuItemCloseSelectedTabs.hidden, true, "Close Selected Tabs is hidden");
-
     await triggerClickOn(tab1, { ctrlKey: true });
     await triggerClickOn(tab2, { ctrlKey: true });
 
     ok(tab1.multiselected, "Tab1 is multiselected");
     ok(tab2.multiselected, "Tab2 is multiselected");
     ok(!tab3.multiselected, "Tab3 is not multiselected");
     ok(!tab4.multiselected, "Tab4 is not multiselected");
     is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
 
-    // Check the context menu with two multiselected tabs
+    // Check the context menu with a non-multiselected tab
     updateTabContextMenu(tab4);
+    is(menuItemCloseTab.hidden, false, "Close Tab is visible");
+    is(menuItemCloseSelectedTabs.hidden, true, "Close Selected Tabs is hidden");
+
+    // Check the context menu with a multiselected tab
+    updateTabContextMenu(tab2);
     is(menuItemCloseTab.hidden, true, "Close Tab is hidden");
     is(menuItemCloseSelectedTabs.hidden, false, "Close Selected Tabs is visible");
 
     let tab1Closing = BrowserTestUtils.waitForTabClosing(tab1);
     let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
     menuItemCloseSelectedTabs.click();
     await tab1Closing;
     await tab2Closing;
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_mute_unmute.js
@@ -0,0 +1,389 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+const PAGE = "https://example.com/browser/browser/base/content/test/general/file_mediaPlayback.html";
+
+async function wait_for_tab_playing_event(tab, expectPlaying) {
+  if (tab.soundPlaying == expectPlaying) {
+    ok(true, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+    return true;
+  }
+  return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => {
+    if (event.detail.changed.includes("soundplaying")) {
+      is(tab.hasAttribute("soundplaying"), expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+      is(tab.soundPlaying, expectPlaying, "The tab should " + (expectPlaying ? "" : "not ") + "be playing");
+      return true;
+    }
+    return false;
+  });
+}
+
+async function waitForTabMuteStateChangeEvent(tab) {
+  return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, (event) => {
+    for (let attr of ["activemedia-blocked", "muted", "soundplaying"]) {
+      if (event.detail.changed.includes(attr)) {
+        return true;
+      }
+    }
+    return false;
+  });
+}
+
+async function is_audio_playing(tab) {
+  let browser = tab.linkedBrowser;
+  let isPlaying = await ContentTask.spawn(browser, {}, async function() {
+    let audio = content.document.querySelector("audio");
+    return !audio.paused;
+  });
+  return isPlaying;
+}
+
+async function play(tab) {
+  let browser = tab.linkedBrowser;
+  await ContentTask.spawn(browser, {}, async function() {
+    let audio = content.document.querySelector("audio");
+    audio.play();
+  });
+
+  // If the tab has already been muted, it means the tab won't get soundplaying,
+  // so we don't need to check this attribute.
+  if (browser.audioMuted) {
+    return;
+  }
+
+  await waitForTabMuteStateChangeEvent(tab);
+}
+
+function disable_non_test_mouse(disable) {
+  let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
+  utils.disableNonTestMouseEvents(disable);
+}
+
+function hover_icon(icon, tooltip) {
+  disable_non_test_mouse(true);
+
+  let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+  EventUtils.synthesizeMouse(icon, 1, 1, {type: "mouseover"});
+  EventUtils.synthesizeMouse(icon, 2, 2, {type: "mousemove"});
+  EventUtils.synthesizeMouse(icon, 3, 3, {type: "mousemove"});
+  EventUtils.synthesizeMouse(icon, 4, 4, {type: "mousemove"});
+  return popupShownPromise;
+}
+
+function leave_icon(icon) {
+  EventUtils.synthesizeMouse(icon, 0, 0, {type: "mouseout"});
+  EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+  EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+  EventUtils.synthesizeMouseAtCenter(document.documentElement, {type: "mousemove"});
+
+  disable_non_test_mouse(false);
+}
+
+// The set of tabs which have ever had their mute state changed.
+// Used to determine whether the tab should have a muteReason value.
+let everMutedTabs = new WeakSet();
+
+function get_wait_for_mute_promise(tab, expectMuted) {
+  return BrowserTestUtils.waitForEvent(tab, "TabAttrModified", false, event => {
+    if (event.detail.changed.includes("muted") || event.detail.changed.includes("activemedia-blocked")) {
+      is(tab.hasAttribute("muted"), expectMuted, "The tab should " + (expectMuted ? "" : "not ") + "be muted");
+      is(tab.muted, expectMuted, "The tab muted property " + (expectMuted ? "" : "not ") + "be true");
+
+      if (expectMuted || everMutedTabs.has(tab)) {
+        everMutedTabs.add(tab);
+        is(tab.muteReason, null, "The tab should have a null muteReason value");
+      } else {
+        is(tab.muteReason, undefined, "The tab should have an undefined muteReason value");
+      }
+      return true;
+    }
+    return false;
+  });
+}
+
+async function test_mute_tab(tab, icon, expectMuted) {
+  let mutedPromise = waitForTabMuteStateChangeEvent(tab);
+
+  let activeTab = gBrowser.selectedTab;
+
+  let tooltip = document.getElementById("tabbrowser-tab-tooltip");
+
+  await hover_icon(icon, tooltip);
+  EventUtils.synthesizeMouseAtCenter(icon, {button: 0});
+  leave_icon(icon);
+
+  is(gBrowser.selectedTab, activeTab, "Clicking on mute should not change the currently selected tab");
+
+  // If the audio is playing, we should check whether clicking on icon affects
+  // the media element's playing state.
+  let isAudioPlaying = await is_audio_playing(tab);
+  if (isAudioPlaying) {
+    await wait_for_tab_playing_event(tab, !expectMuted);
+  }
+
+  return mutedPromise;
+}
+
+function muted(tab) {
+  return tab.linkedBrowser.audioMuted;
+}
+
+function activeMediaBlocked(tab) {
+  return tab.activeMediaBlocked;
+}
+
+async function addMediaTab() {
+  const tab = BrowserTestUtils.addTab(gBrowser, PAGE, { skipAnimation: true });
+  const browser = gBrowser.getBrowserForTab(tab);
+  await BrowserTestUtils.browserLoaded(browser);
+  return tab;
+}
+
+add_task(async function setPref() {
+  await SpecialPowers.pushPrefEnv({
+    set: [[PREF_MULTISELECT_TABS, true]]
+  });
+});
+
+add_task(async function muteTabs_usingButton() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tab4 = await addMediaTab();
+
+  let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  await play(tab0);
+  await play(tab1);
+  await play(tab2);
+
+  // Multiselecting tab1, tab2 and tab3
+  await BrowserTestUtils.switchTab(gBrowser, tab1);
+  await triggerClickOn(tab3, { shiftKey: true });
+
+  is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+  ok(!tab0.multiselected, "Tab0 is not multiselected");
+  ok(!tab4.multiselected, "Tab4 is not multiselected");
+
+  // tab1,tab2 and tab3 should be multiselected.
+  for (let i = 1; i <= 3; i++) {
+    ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+  }
+
+  // All five tabs are unmuted
+  for (let i = 0; i < 5; i++) {
+    ok(!muted(tabs[i]), "Tab" + i + " is not muted");
+  }
+
+  // Mute tab0 which is not multiselected, thus other tabs muted state should not be affected
+  let tab0MuteAudioBtn = document.getAnonymousElementByAttribute(tab0, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab0, tab0MuteAudioBtn, true);
+
+  ok(muted(tab0), "Tab0 is muted");
+  for (let i = 1; i <= 4; i++) {
+    ok(!muted(tabs[i]), "Tab" + i + " is not muted");
+  }
+
+  // Now we multiselect tab0
+  await triggerClickOn(tab0, { ctrlKey: true });
+
+  // tab0, tab1, tab2, tab3 are multiselected
+  for (let i = 0; i <= 3; i++) {
+    ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+  }
+  ok(!tab4.multiselected, "tab4 is not multiselected");
+
+  // Check mute state
+  ok(muted(tab0), "Tab0 is still muted");
+  ok(!muted(tab1) && !activeMediaBlocked(tab1), "Tab1 is not muted");
+  ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is not muted and not activemedia-blocked");
+  ok(!muted(tab4) && !activeMediaBlocked(tab4), "Tab4 is not muted and not activemedia-blocked");
+
+  // Mute tab1 which is mutliselected, thus other multiselected tabs should be affected too
+  // in the following way:
+  //  a) muted tabs (tab0) will remain muted.
+  //  b) unmuted tabs (tab1, tab3) will become muted.
+  //  b) media-blocked tabs (tab2) will remain media-blocked.
+  // However tab4 (unmuted) which is not multiselected should not be affected.
+  let tab1MuteAudioBtn = document.getAnonymousElementByAttribute(tab1, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab1, tab1MuteAudioBtn, true);
+
+  // Check mute state
+  ok(muted(tab0), "Tab0 is still muted");
+  ok(muted(tab1), "Tab1 is muted");
+  ok(activeMediaBlocked(tab2), "Tab2 is still media-blocked");
+  ok(muted(tab3), "Tab3 is now muted");
+  ok(!muted(tab4) && !activeMediaBlocked(tab4), "Tab4 is not muted and not activemedia-blocked");
+
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function unmuteTabs_usingButton() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tab4 = await addMediaTab();
+
+  let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  await play(tab0);
+  await play(tab1);
+  await play(tab2);
+
+  // Mute tab3 and tab4
+  tab3.toggleMuteAudio();
+  tab4.toggleMuteAudio();
+
+  // Multiselecting tab0, tab1, tab2 and tab3
+  await triggerClickOn(tab3, { shiftKey: true });
+
+  // Check mutliselection
+  for (let i = 0; i <= 3; i++) {
+    ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+  }
+  ok(!tab4.multiselected, "tab4 is not multiselected");
+
+  // Check tabs mute state
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is not muted and not media-blocked");
+  ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+  ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+  ok(muted(tab3), "Tab3 is muted");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+  // unmute tab0 which is mutliselected, thus other multiselected tabs should be affected too
+  // in the following way:
+  //  a) muted tabs (tab3) will become unmuted.
+  //  b) unmuted tabs (tab0) will remain unmuted.
+  //  b) media-blocked tabs (tab1, tab2) will get playing. (media not blocked anymore)
+  // However tab4 (muted) which is not multiselected should not be affected.
+  let tab3MuteAudioBtn = document.getAnonymousElementByAttribute(tab3, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab3, tab3MuteAudioBtn, false);
+
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is unmuted and not media-blocked");
+  ok(!muted(tab1) && !activeMediaBlocked(tab1), "Tab1 is unmuted and not media-blocked");
+  ok(!muted(tab2) && !activeMediaBlocked(tab2), "Tab2 is unmuted and not media-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is unmuted and not media-blocked");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function playTabs_usingButton() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tab4 = await addMediaTab();
+
+  let tabs = [tab0, tab1, tab2, tab3, tab4];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab0);
+  await play(tab0);
+  await play(tab1);
+  await play(tab2);
+
+  // Multiselecting tab0, tab1, tab2 and tab3.
+  await triggerClickOn(tab3, { shiftKey: true });
+
+  // Mute tab0 and tab4
+  tab0.toggleMuteAudio();
+  tab4.toggleMuteAudio();
+
+  // Check mutliselection
+  for (let i = 0; i <= 3; i++) {
+    ok(tabs[i].multiselected, "tab" + i + " is multiselected");
+  }
+  ok(!tab4.multiselected, "tab4 is not multiselected");
+
+  // Check mute state
+  ok(muted(tab0), "Tab0 is muted");
+  ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+  ok(activeMediaBlocked(tab2), "Tab2 is media-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is not muted and not activemedia-blocked");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+  // play tab2 which is mutliselected, thus other multiselected tabs should be affected too
+  // in the following way:
+  //  a) muted tabs (tab0) will become unmuted.
+  //  b) unmuted tabs (tab3) will remain unmuted.
+  //  b) media-blocked tabs (tab1, tab2) will get playing. (media not blocked anymore)
+  // However tab4 (muted) which is not multiselected should not be affected.
+  let tab2MuteAudioBtn = document.getAnonymousElementByAttribute(tab2, "anonid", "soundplaying-icon");
+  await test_mute_tab(tab2, tab2MuteAudioBtn, false);
+
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is unmuted and not activemedia-blocked");
+  ok(!muted(tab1) && !activeMediaBlocked(tab1), "Tab1 is unmuted and not activemedia-blocked");
+  ok(!muted(tab2) && !activeMediaBlocked(tab2), "Tab2 is unmuted and not activemedia-blocked");
+  ok(!muted(tab3) && !activeMediaBlocked(tab3), "Tab3 is unmuted and not activemedia-blocked");
+  ok(muted(tab4), "Tab4 is muted");
+  is(gBrowser.selectedTab, tab0, "Tab0 is active");
+
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
+
+add_task(async function checkTabContextMenu() {
+  let tab0 = await addMediaTab();
+  let tab1 = await addMediaTab();
+  let tab2 = await addMediaTab();
+  let tab3 = await addMediaTab();
+  let tabs = [tab0, tab1, tab2, tab3];
+
+  let menuItemToggleMuteTab = document.getElementById("context_toggleMuteTab");
+  let menuItemToggleMuteSelectedTabs = document.getElementById("context_toggleMuteSelectedTabs");
+
+  await play(tab0);
+  tab0.toggleMuteAudio();
+  await play(tab1);
+  tab2.toggleMuteAudio();
+
+  // Mutliselect tab0, tab1, tab2.
+  await triggerClickOn(tab0, { ctrlKey: true });
+  await triggerClickOn(tab1, { ctrlKey: true });
+  await triggerClickOn(tab2, { ctrlKey: true });
+
+  // Check mutliselected tabs
+  for (let i = 0; i <= 2; i++) {
+    ok(tabs[i].multiselected, "Tab" + i + " is multi-selected");
+  }
+  ok(!tab3.multiselected, "Tab3 is not multiselected");
+
+  // Check mute state for tabs
+  ok(!muted(tab0) && !activeMediaBlocked(tab0), "Tab0 is not muted and not activemedia-blocked");
+  ok(activeMediaBlocked(tab1), "Tab1 is media-blocked");
+  ok(muted(tab2), "Tab2 is muted");
+  ok(!muted(tab3, "Tab3 is not muted"));
+
+  let labels = ["Mute Tabs", "Play Tabs", "Unmute Tabs"];
+
+  for (let i = 0; i <= 2; i++) {
+    updateTabContextMenu(tabs[i]);
+    ok(menuItemToggleMuteTab.hidden,
+      "toggleMuteAudio menu for one tab is hidden - contextTab" + i);
+    ok(!menuItemToggleMuteSelectedTabs.hidden,
+      "toggleMuteAudio menu for selected tab is not hidden - contextTab" + i);
+    is(menuItemToggleMuteSelectedTabs.label, labels[i], labels[i] + " should be shown");
+  }
+
+  updateTabContextMenu(tab3);
+  ok(!menuItemToggleMuteTab.hidden, "toggleMuteAudio menu for one tab is not hidden");
+  ok(menuItemToggleMuteSelectedTabs.hidden, "toggleMuteAudio menu for selected tab is hidden");
+
+  for (let tab of tabs) {
+    BrowserTestUtils.removeTab(tab);
+  }
+});
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -882,16 +882,23 @@ userContextOpenLink.label = Open Link in
 
 muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = m
 playTab.label = Play Tab
 playTab.accesskey = l
 
+muteSelectedTabs.label = Mute Tabs
+muteSelectedTabs.accesskey = u
+unmuteSelectedTabs.label = Unmute Tabs
+unmuteSelectedTabs.accesskey = b
+playTabs.label = Play Tabs
+playTabs.accesskey = y
+
 # LOCALIZATION NOTE (certErrorDetails*.label): These are text strings that
 # appear in the about:certerror page, so that the user can copy and send them to
 # the server administrators for troubleshooting.
 certErrorDetailsHSTS.label = HTTP Strict Transport Security: %S
 certErrorDetailsKeyPinning.label = HTTP Public Key Pinning: %S
 certErrorDetailsCertChain.label = Certificate chain:
 
 # LOCALIZATION NOTE (pendingCrashReports2.label): Semi-colon list of plural forms