Greasy Fork is available in English.

Audio Output Picker

Pick a preferred audio output device for HTML5 audio and video elements.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Audio Output Picker
// @namespace    https://greasyfork.org/en/users/670188-hacker09?sort=daily_installs
// @version      5
// @description  Pick a preferred audio output device for HTML5 audio and video elements.
// @author       hacker09
// @include      *
// @icon         https://i.imgur.com/RHFAjq3.png
// @grant        GM_registerMenuCommand
// @grant        GM_deleteValue
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(async()=>{
  'use strict';
  const findMediaElement = async ()=>{//Finds the active media element, whether it's a <video> or an <audio> tag.
    while (true) {
      let mediaElement = document.querySelector('video, audio, .video-stream');//Look for the standard video player OR any audio element first.
      if (mediaElement) {return mediaElement;}
      await new Promise(resolve => setTimeout(resolve, 150));//If nothing is found, wait and retry.
    }
  };

  const getDevicesWithPermission = async () => {//Get permissions and devices.
    try { await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (e) {}//Prevent crash when tab is in background and cannot prompt for permission.
    return (await navigator.mediaDevices.enumerateDevices()).filter(d => d.kind === "audiooutput");
  };

  const applySavedDevice = async () => {
    if(applySavedDevice.busy) return;//Prevent AbortErrors from simultaneous event triggers.
    applySavedDevice.busy = true;
    const savedLabel = GM_getValue(location.href) || GM_getValue(location.hostname);//Retrieve saved Name/Label instead of ID.
    if (!savedLabel) { applySavedDevice.busy = false; return; }//Only act if a saved setting exists.

    for (let i = 0; i < 5; i++) {//Try for 5 seconds to give Windows and the browser time to sync the BT connection.
      const devices = await getDevicesWithPermission();
      const target = devices.find(d => d.label === savedLabel);//Find the current ID for the saved Name.

      if (target) {
        const mediaElement = await findMediaElement();//Re-fetch the video element in case YouTube destroyed and recreated it in the background.
        try {
          await mediaElement.setSinkId('');//Force release of the dead audio pipeline before setting the new one.
          await mediaElement.setSinkId(target.deviceId);
          if(!mediaElement.paused) mediaElement.currentTime += 0.01;//Force audio buffer reset to fix Chromium silent stream bug.
        } catch(e){}//Catch silent promise rejections.
        break;//Stop trying once connected.
      }
      await new Promise(r => setTimeout(r, 1000));//Wait 1 second before checking the hardware list again.
    }
    applySavedDevice.busy = false;
  };

  const selectAudioDevice = async (scope) => {
    const devices = await getDevicesWithPermission();//Ask for mic permission.
    if (!devices || devices.length === 0) return;//Exit if mic permission denied or no devices.

    const choice = parseInt(prompt(devices.map((d,i) => `${i+1}: ${d.label||`Device ${i+1}`}`).join('\n')), 10)-1;
    if(devices[choice]) {
      const key = scope === 'URL' ? location.href : location.hostname;
      GM_setValue(key, devices[choice].label);//Save the label (Name) to persist across ID changes.
      const mediaElement = await findMediaElement();
      try { mediaElement.setSinkId(devices[choice].deviceId); } catch(e){}
      location.reload();//Reload to apply the setting and update the menu text from "Save" to "DELETE".
    }
  };

  ['URL', 'DOMAIN'].forEach(scope => {
    const key = scope === 'URL' ? location.href : location.hostname;
    if (GM_getValue(key)) {
      GM_registerMenuCommand(`DELETE ${scope}`, () => {
        GM_deleteValue(key);
        location.reload();
      });
    } else {
      GM_registerMenuCommand(`Save ${scope}`, () => selectAudioDevice(scope));
    }
  });

  navigator.mediaDevices.addEventListener('devicechange', applySavedDevice);//Reconnects when a device is turned on or off.
  document.addEventListener('visibilitychange', () => { if (!document.hidden) applySavedDevice(); });//Trigger reconnection when returning to a throttled background tab.
  window.addEventListener('focus', applySavedDevice);//Trigger reconnection when the tab is focused again.

  const initialMedia = await findMediaElement();
  initialMedia.addEventListener('loadedmetadata', applySavedDevice);
  initialMedia.addEventListener('play', applySavedDevice);//Trigger reconnection when media starts playing.
  applySavedDevice();//Attempt to apply immediately in case metadata is already loaded.
})();