Universal Audio Device Selector

Allows you to select audio output device on any sites (except iframe)

当前为 2025-03-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Universal Audio Device Selector
// @name:zh-cn  音频输出切换器
// @name:ja     ユニバーサル音声切替器
// @namespace   Violentmonkey Scripts
// @match       *://*/*
// @grant       none
// @version     1.0
// @author      tiamed
// @license     MIT
// @homepageURL https://github.com/tiamed/universal-audio-device-selector
// @description Allows you to select audio output device on any sites (except iframe)
// @description:zh-cn   可在任意网站切换音视频的音频输出设备(iframe除外)
// @description:ja     「あらゆるウェブサイトで音声出力デバイスの選択を可能にするスクリプト ※iframe内のコンテンツは除外」
// @run-at      document-end
// ==/UserScript==

(function() {
    'use strict';

    let devices = [];
    let currentDevice;
    const menu = document.createElement('div');
    let isInitialized = false;
    let isMenuVisible = false;

    // 菜单样式
    Object.assign(menu.style, {
        position: 'fixed',
        bottom: '60px',
        right: '20px',
        background: '#333',
        color: 'white',
        padding: '10px',
        borderRadius: '5px',
        zIndex: 999999,
        display: 'none',
        maxHeight: '60vh',
        overflowY: 'auto',
        boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
        minWidth: '200px'
    });

    async function initDevices() {
        try {
            // 只在首次点击时请求权限
            if (!isInitialized) {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                stream.getTracks().forEach(track => track.stop());
                isInitialized = true;
            }
            await updateDeviceList();
            return true;
        } catch(e) {
            console.error('Device access error:', e);
            return false;
        }
    }

    async function updateDeviceList() {
        devices = (await navigator.mediaDevices.enumerateDevices())
            .filter(d => d.kind === 'audiooutput' && d.deviceId !== 'default');

        const saved = localStorage.getItem('audioDevice');
        currentDevice = devices.find(d => d.deviceId === saved) || devices[0];
    }

    function createDeviceList() {
        menu.innerHTML = `
            <div style="margin-bottom:10px;font-weight:bold;padding:0 5px">音频输出设备</div>
            ${devices.map(d => `
                <div class="device-item"
                    data-device-id="${d.deviceId}"
                    style="padding:8px 12px;
                        cursor:pointer;
                        background:${d.deviceId === currentDevice?.deviceId ? '#444' : 'transparent'};
                        border-radius:4px;
                        margin:2px 0;
                        transition:background 0.2s;">
                    ${d.label}
                </div>
            `).join('')}
        `;

        menu.querySelectorAll('.device-item').forEach(item => {
            item.addEventListener('click', async () => {
                const deviceId = item.dataset.deviceId;
                currentDevice = devices.find(d => d.deviceId === deviceId);
                localStorage.setItem('audioDevice', deviceId);
                await updateMediaElements();
                createDeviceList();
                toggleMenu();
            });
        });
    }

    async function updateMediaElements() {
        const mediaElements = document.querySelectorAll('video, audio');
        for (const media of mediaElements) {
            if (currentDevice && media.setSinkId) {
                try {
                    await media.setSinkId(currentDevice.deviceId);
                } catch(e) {
                    console.error('Failed to set sink:', e);
                }
            }
        }
    }

    function toggleMenu() {
        isMenuVisible = !isMenuVisible;
        menu.style.display = isMenuVisible ? 'block' : 'none';
    }

    // 主入口
    async function main() {
        if (window.self !== window.top) return;

        // 添加控制按钮
        const btn = document.createElement('div');
        btn.innerHTML = '🔊';
        Object.assign(btn.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            background: '#444',
            color: 'white',
            padding: '12px 15px',
            borderRadius: '50%',
            cursor: 'pointer',
            zIndex: '999999',
            fontSize: '18px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
            transition: 'transform 0.2s'
        });

        btn.addEventListener('mouseover', () => btn.style.transform = 'scale(1.1)');
        btn.addEventListener('mouseout', () => btn.style.transform = 'scale(1)');

        btn.addEventListener('click', async () => {
            if (!isInitialized) {
                const success = await initDevices();
                if (!success) return;
            }
            if (!devices.length) await updateDeviceList();
            createDeviceList();
            toggleMenu();
        });

        document.body.appendChild(btn);
        document.body.appendChild(menu);

        new MutationObserver(updateMediaElements)
            .observe(document, { subtree: true, childList: true });
    }

    // 点击外部关闭菜单
    document.addEventListener('click', (e) => {
        if (!menu.contains(e.target) && !e.target.isEqualNode(btn)) {
            menu.style.display = 'none';
            isMenuVisible = false;
        }
    });

    window.addEventListener('load', main);
})();