音频输出切换器

可在任意网站切换音视频的音频输出设备(iframe除外)

目前为 2025-03-16 提交的版本。查看 最新版本

// ==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);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址