YTMusic Audio Device Selector

2/23/2025, 11:35:22 AM

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        YTMusic Audio Device Selector
// @namespace   Violentmonkey Scripts
// @match       https://music.youtube.com/*
// @match       https://www.youtube.com/*
// @match       https://m.youtube.com/*
// @grant       none
// @version     1.2
// @author      DoKM (https://github.com/DoKM)
// @description 2/23/2025, 11:35:22 AM
// @run-at document-end
// @homepageURL https://github.com/DoKM/Youtube-Music-Audio-Device-Selector
// @license MIT
// ==/UserScript==

(async function () {
    let audioDevices = [];
    let defaultAudioDevice;
    let defaultSpeaker;
    let currentDevice;
    let dropdown;

    const menuID = window.location.hostname === "music.youtube.com" ? "right-content" : "end";

    const starFilled = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/></svg>`;
    const speakerFilled = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M533.6 32.5C598.5 85.2 640 165.8 640 256s-41.5 170.7-106.4 223.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C557.5 398.2 592 331.2 592 256s-34.5-142.2-88.7-186.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM473.1 107c43.2 35.2 70.9 88.9 70.9 149s-27.7 113.8-70.9 149c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C475.3 341.3 496 301.1 496 256s-20.7-85.3-53.2-111.8c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zm-60.5 74.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM301.1 34.8C312.6 40 320 51.4 320 64l0 384c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352 64 352c-35.3 0-64-28.7-64-64l0-64c0-35.3 28.7-64 64-64l67.8 0L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3z"/></svg>`;

    async function init() {
        return new Promise(async (resolve, reject) => {
            if (window.trustedTypes && window.trustedTypes.createPolicy) {
                window.trustedTypes.createPolicy('default', {
                    createHTML: string => string
                });
            }
            const constrains = { audio: true, video: true }

            await navigator.mediaDevices.getUserMedia(constrains)

            await updateDevices();
            navigator.mediaDevices.addEventListener("devicechange", async () => {
                await updateDevices();
            });
            resolve();
        })
    }


    await init();

    async function updateDevices() {
        return new Promise(async (resolve, reject) => {
            let tempDevices = await navigator.mediaDevices.enumerateDevices();
            let getDevices = function (deviceList) {
                let outputDevices = [];  // Fixed typo
                let defaultDevice = undefined;

                for (let device of deviceList) {
                    if (device.kind === "audiooutput") {
                        if (device.deviceId === "default") {
                            defaultDevice = device;
                        } else if (device.deviceId !== "communications" && device.deviceId != undefined) {
                            outputDevices.push(device);
                        }
                    }
                }

                return {
                    defaultDevice: defaultDevice,
                    outputDevices: outputDevices // Fixed typo
                };
            };
            let { defaultDevice, outputDevices } = getDevices(tempDevices);
            audioDevices = outputDevices;
            defaultAudioDevice = defaultDevice;
            console.log(outputDevices);
            resolve();
        });
    }

    const musicPlayer = document.getElementsByTagName("video")[0];
    {
        const audioDeviceID = localStorage.getItem("dokm-audio-device-favoriteDevice");
        const speakerDeviceID = localStorage.getItem("dokm-audio-device-favoriteSpeaker");
        if (audioDeviceID || speakerDeviceID) {
            for (let device of audioDevices) {
                if (device.deviceId === audioDeviceID) {
                    defaultAudioDevice = device;
                    musicPlayer.setSinkId(device.deviceId);
                }
                if (device.deviceId === speakerDeviceID) {
                    defaultSpeaker = device;
                    if (!audioDeviceID) {
                        musicPlayer.setSinkId(device.deviceId);
                    }
                }
            }
        }
        if (!currentDevice) {
            currentDevice = defaultAudioDevice;
        }

    }
    createMenu()

    function setCurrentDevice(device) {
        if (!device) {
            return;
        }
        currentDevice = device;
        musicPlayer.setSinkId(device.deviceId);
    }
    function setFavorite(device) {
        // save the device ID to local storage
        localStorage.setItem("dokm-audio-device-favoriteDevice", device.deviceId);
        defaultAudioDevice = device;
    }
    function setFavoriteSpeaker(device) {
        // save the device ID to local storage
        localStorage.setItem("dokm-audio-device-favoriteSpeaker", device.deviceId);
        defaultSpeaker = device;
    }

    function createButton(elementName, innerHTML, colour = "#fff") {
        // Create the button element
        const button = document.createElement("button");
        button.classList.add(elementName);
        button.innerHTML = innerHTML;
        // Style the button for dark mode
        Object.assign(button.style, {
            background: "#222",
            color: "#fff",
            border: "none",
            padding: "8px",
            cursor: "pointer",
            borderRadius: "5px",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            transition: "background 0.3s",
            position: "relative",
            width: "40px", // Default width
            height: "40px", // Default height
            marginRight: "5px",
        });

        if (innerHTML.includes("svg")) {
            button.style.fill = colour;
        }

        button.addEventListener("mouseenter", () => (button.style.background = "#333"));
        button.addEventListener("mouseleave", () => (button.style.background = "#222"));

        return button;
    }

    function createDropdown(audioDevices) {
        const dropdown = document.createElement("div");
        dropdown.classList.add("dropdown-menu");

        // sort the devices, default device first, default speaker second and the rest in alphabetical order
        let sorted = audioDevices.sort((a, b) => {
            if (a.deviceId === defaultAudioDevice?.deviceId) {
                return -1;
            } else if (b.deviceId === defaultAudioDevice?.deviceId) {
                return 1;
            } else if (a.deviceId === defaultSpeaker?.deviceId) {
                return -1;
            } else if (b.deviceId === defaultSpeaker?.deviceId) {
                return 1;
            } else if (a.label < b.label) {
                return -1;
            } else if (a.label > b.label) {
                return 1;
            } else {
                return 0;
            }
        });

        // Style the dropdown menu
        Object.assign(dropdown.style, {
            position: "absolute",
            top: "0",
            right: "110%", // Move to the left of the button
            background: "#333",
            color: "#fff",
            padding: "10px",
            borderRadius: "5px",
            boxShadow: "0px 2px 10px rgba(0, 0, 0, 0.2)",
            display: "none",
            minWidth: "220px",
        });

        // Create list container
        const table = document.createElement("table");
        table.style.width = "100%";
        table.style.borderCollapse = "collapse";
        table.style.marginTop = "10px";
        table.style.color = "#fff";
        table.style.backgroundColor = "#333";
        table.style.border = "0px solid #444";

        sorted.forEach((device, index) => {
            const tr = document.createElement("tr");
            tr.setAttribute("data-device-id", device.deviceId);
            tr.style.cursor = "pointer";
            tr.style.borderBottom = "1px solid #444";

            // Highlight on hover
            tr.addEventListener("mouseenter", () => (tr.style.background = "#444"));
            tr.addEventListener("mouseleave", () => (tr.style.background = "transparent"));





            // Device label cell
            const labelCell = document.createElement("td");
            const labelContainer = document.createElement("span");
            labelContainer.style.display = "flex";
            labelContainer.style.alignItems = "center";

            const label = document.createElement("span");
            label.textContent = device.label;
            label.style.padding = "8px";
            label.style.display = "block";

            labelContainer.appendChild(label);

            if (device.deviceId === currentDevice.deviceId) {
                const currentDeviceIcon = document.createElement("span");
                currentDeviceIcon.innerHTML = speakerFilled;
                currentDeviceIcon.style.width = "40px";
                currentDeviceIcon.style.fill = "#DAA520";
                currentDeviceIcon.style.display = "inline-block";
                currentDeviceIcon.style.verticalAlign = "middle"; // Center vertically
                labelContainer.style.display = "flex"; // Ensure flex container
                labelContainer.style.alignItems = "center"; // Center vertically
                labelContainer.prepend(currentDeviceIcon);
            }

            labelCell.appendChild(labelContainer);

            labelCell.addEventListener("click", () => {
                setCurrentDevice(device);
            });

            // Star button cell
            const starCell = document.createElement("td");
            const starButton = createButton("button", starFilled, (device === defaultAudioDevice) ? "#DAA520" : "#fff");
            starButton.addEventListener("click", (e) => {
                //e.stopPropagation();
                setFavorite(device);
            });
            starCell.appendChild(starButton);

            // Speaker button cell
            const speakerCell = document.createElement("td");
            const speakerButton = createButton("button", speakerFilled, (device === defaultSpeaker) ? "#DAA520" : "#fff");
            speakerButton.addEventListener("click", (e) => {
                //e.stopPropagation();
                setFavoriteSpeaker(device);
            });
            speakerCell.appendChild(speakerButton);

            // Assemble table row
            tr.appendChild(labelCell);
            tr.appendChild(starCell);
            tr.appendChild(speakerCell);
            table.appendChild(tr);
        });

        dropdown.appendChild(table);

        return dropdown;
    }

    function createMenu() {
        const menuLocation = document.getElementById(menuID);

        if (!menuLocation) {
            console.error(`Element with ID '${menuID}' not found.`);
            return;
        }

        // Example array of audio devices


        const outputSelectorButton = createButton("output-selector-button", `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
        <path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path>
        <path d="M19.07 4.93a10 10 0 0 1 0 14.14"></path>
      </svg>`);


        // Toggle dropdown visibility on button click
        outputSelectorButton.addEventListener("click", (event) => {
            event.stopPropagation();
            if (dropdown) {
                dropdown.remove();
                dropdown = null;
            } else {
                dropdown = createDropdown(audioDevices);
                outputSelectorButton.appendChild(dropdown);
                dropdown.style.display = "block";
            }
        });

        // Close dropdown when clicking outside
        document.addEventListener("click", (event) => {
            if (dropdown && !outputSelectorButton.contains(event.target) && !dropdown.contains(event.target)) {
                dropdown.remove();
                dropdown = null;
            }
        });

        const selectDefaultButton = createButton("select-default-button", starFilled);
        const selectSpeakerButton = createButton("select-speaker-button", speakerFilled);

        selectDefaultButton.addEventListener("click", () => {
            setCurrentDevice(defaultAudioDevice);
        });
        selectSpeakerButton.addEventListener("click", () => {
            setCurrentDevice(defaultSpeaker);
        });

        // Append dropdown to button and insert into menu
        menuLocation.prepend(selectDefaultButton, selectSpeakerButton, outputSelectorButton);
    }

})();