Skip to content

Instantly share code, notes, and snippets.

@robiot
Last active May 15, 2026 16:12
Show Gist options
  • Select an option

  • Save robiot/fb05b6528a76ec1142842913b5eca38a to your computer and use it in GitHub Desktop.

Select an option

Save robiot/fb05b6528a76ec1142842913b5eca38a to your computer and use it in GitHub Desktop.
OME.TV Camera Switcher that works with ex. OBS Virtual Camera
// 1. Open inspector with CTRL+SHIFT+I
// 2. Select the console tab in the top
// 3. Paste the script
// 4. Press enter
// 5. Hover over your own video, and there should be a video switcher that looks horrible but works.
const localVideo = document.getElementById("local-video");
if (localVideo) {
// Create a dropdown element for the camera switcher
const cameraSwitcher = document.createElement("select");
// Populate the dropdown with available cameras
const option = document.createElement("option");
option.value = "";
option.text = "Select camera";
option.disabled = true;
option.selected = true;
cameraSwitcher.appendChild(option);
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
devices
.filter((device) => device.kind === "videoinput")
.forEach((device) => {
const option = document.createElement("option");
option.value = device.deviceId;
option.text =
device.label ||
`Camera ${cameraSwitcher.options.length + 1}`;
cameraSwitcher.appendChild(option);
});
})
.catch((error) => {
console.error("Error enumerating devices:", error);
});
// Add an event listener to switch the camera when an option is selected
cameraSwitcher.addEventListener("change", function () {
const selectedDeviceId = this.value;
if (!selectedDeviceId) {
return;
}
// Stop the current video stream
if (localVideo.srcObject) {
const tracks = localVideo.srcObject.getTracks();
tracks.forEach((track) => track.stop());
}
// Get user media with the selected camera
navigator.mediaDevices
.getUserMedia({ video: { deviceId: selectedDeviceId } })
.then((stream) => {
// Assign the new stream to the video element
localVideo.srcObject = stream;
})
.catch((error) => {
console.error("Error accessing camera:", error);
});
});
// Create a microphone switcher dropdown
const microphoneSwitcher = document.createElement("select");
// Populate the dropdown with available microphones
const microphoneOption = document.createElement("option");
microphoneOption.value = "";
microphoneOption.text = "Select microphone";
microphoneOption.disabled = true;
microphoneOption.selected = true;
microphoneSwitcher.appendChild(microphoneOption);
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
devices
.filter((device) => device.kind === "audioinput")
.forEach((device) => {
console.log("Adding options");
const option = document.createElement("option");
option.value = device.deviceId;
option.text =
device.label ||
`Microphone ${microphoneSwitcher.options.length + 1}`;
microphoneSwitcher.appendChild(option);
});
})
.catch((error) => {
console.error("Error enumerating devices:", error);
});
// Add an event listener to switch the microphone when an option is selected
console.log("Adding event listener");
microphoneSwitcher.addEventListener("change", function () {
const selectedDeviceId = this.value;
console.log("1");
if (!selectedDeviceId) {
return;
}
const currentStream = localVideo.srcObject.clone();
// Stop the current video stream
if (localVideo.srcObject) {
console.log("Stopping the current stream");
const tracks = localVideo.srcObject.getTracks();
tracks.forEach((track) => track.stop());
}
console.log("Is changing", currentStream);
if (currentStream) {
const videoTracks = currentStream.getVideoTracks();
navigator.mediaDevices
.getUserMedia({ audio: { deviceId: selectedDeviceId } })
.then((audioStream) => {
// Combine the current video tracks with the new audio stream
console.log("We got the audio steam");
const combinedStream = new MediaStream([
...videoTracks,
...audioStream.getAudioTracks(),
]);
console.log("It is combined");
localVideo.srcObject = combinedStream;
})
.catch((error) => {
console.error("Error accessing microphone:", error);
});
}
});
// add audio playback so we can hear ourselves
localVideo.muted = false;
const mediaDevicesFrame = document.querySelector(".media-devices__frame");
if (mediaDevicesFrame) {
// Add the camera switcher dropdown as a child to media-devices__frame
mediaDevicesFrame.appendChild(cameraSwitcher);
// Add the microphone switcher dropdown as a child to media-devices__frame
mediaDevicesFrame.appendChild(microphoneSwitcher);
}
const theirElements = document.querySelector(".media-devices__wrapper");
if (theirElements) {
//remove
theirElements.remove();
}
} else {
console.error("Element with ID 'local-video' not found.");
}
@AsylumEscapist

Copy link
Copy Markdown

The code seems to be working but when I change my camera my microphone seems to stop working and others can't hear me on Ome.TV, is this an issue on my side or is there something wrong with the code?

@robiot

robiot commented Jul 10, 2024

Copy link
Copy Markdown
Author

I think they patched this, since I made this 10 months ago. :(

@AsylumEscapist

Copy link
Copy Markdown

I think they patched this, since I made this 10 months ago. :(

I mean, the camera change is working but the audio stops, could you fix by any chance?

@robiot

robiot commented Jul 10, 2024

Copy link
Copy Markdown
Author

Hmmm, I tried fucking around a bit, and I got the audio to atleast be added to the video stream. But after some testing, it seem like the audio still isn't broadcasted for some reason.

I added the updates to the file, but it still wont work.

Which is kindof weird tbh.

@AsylumEscapist

Copy link
Copy Markdown

Seems like a problem at ome.tv's side. But man, thanks for the effort!

@JollyJolli

Copy link
Copy Markdown

Hey man, thanks a ton! Been looking for this for a while. πŸ™Œ Just added it to my repo of Omegle tools (only got this one and one I built myself πŸ’€). I’ll make sure to credit you!

https://github.com/JollyJolli/omeglehappy

@Jack1212842

Copy link
Copy Markdown

You rock dude. Works amazing! Finally can use continuity cam on mac.

@Schaka

Schaka commented May 15, 2026

Copy link
Copy Markdown

I was experimenting with this a little bit and created a simple extension that would auto select the correct camera on site load, because one of mine is just broken and the browser can never get access to it, so this script didn't work.

To give an example, this is the hardcoded script I used to force a specific webcam in an extension:

(function () {
    const TARGET_LABEL = 'Logitech'; // change if needed

    const _gum = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);

    navigator.mediaDevices.getUserMedia = async function (constraints) {
        if (constraints && constraints.video) {
            try {
                const devices = await navigator.mediaDevices.enumerateDevices();
                const target = devices.find(
                    d => d.kind === 'videoinput' && d.label.includes(TARGET_LABEL)
                );
                if (target) {
                    const videoBase = typeof constraints.video === 'object' ? constraints.video : {};
                    constraints = {
                        ...constraints,
                        video: { ...videoBase, deviceId: { exact: target.deviceId } }
                    };
                    console.log('[OmeTV Fix] βœ… Routing to:', target.label);
                } else {
                    console.warn('[OmeTV Fix] ❌ Camera not found. Available:',
                                 devices.filter(d => d.kind === 'videoinput').map(d => d.label || '(no label)'));
                }
            } catch (e) {
                console.warn('[OmeTV Fix] Error:', e);
            }
        }
        return _gum(constraints);
    };

    console.log('[OmeTV Fix] πŸŽ₯ Override installed');
})();

Eventually I expanded on it and just made it so you can choose a webcam, reload the site and it will persist and overwrite it.
Hope it helps someone (ask an LLM how to turn it into an extension, I can't post a ZIP file here):

/**
 * OmeTV Camera Switcher β€” content.js
 * Runs at document_start in the MAIN world so it can intercept
 * navigator.mediaDevices.getUserMedia before OmeTV's React app initialises.
 */
(function () {
    'use strict';

    const STORAGE_KEY = 'ometv_camera_switcher_v1';

    // ─── Persistence (localStorage, available immediately at document_start) ──

    function loadPref() {
        try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || null; }
        catch { return null; }
    }

    function savePref(deviceId, label) {
        try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ deviceId, label })); }
        catch { /* storage blocked */ }
    }

    function clearPref() {
        try { localStorage.removeItem(STORAGE_KEY); }
        catch { /* ignore */ }
    }

    // ─── getUserMedia override ────────────────────────────────────────────────
    // Must happen before any page script runs (document_start + world:MAIN).

    let pref = loadPref();
    const _getUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);

    navigator.mediaDevices.getUserMedia = async function (constraints) {
        if (constraints && constraints.video && pref && pref.deviceId) {
            const videoBase = typeof constraints.video === 'object'
                ? { ...constraints.video }
                : {};
            delete videoBase.deviceId; // drop whatever the site requested
            constraints = {
                ...constraints,
                video: { ...videoBase, deviceId: { exact: pref.deviceId } },
            };
        }
        return _getUserMedia(constraints);
    };

    // ─── Helpers ─────────────────────────────────────────────────────────────

    function waitForElement(selector, timeout = 20000) {
        return new Promise(resolve => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);
            const obs = new MutationObserver(() => {
                const el = document.querySelector(selector);
                if (el) { obs.disconnect(); resolve(el); }
            });
            obs.observe(document.documentElement, { childList: true, subtree: true });
            setTimeout(() => { obs.disconnect(); resolve(null); }, timeout);
        });
    }

    function truncate(str, max) {
        return str && str.length > max ? str.slice(0, max) + '…' : (str || '');
    }

    // ─── Live camera switch ───────────────────────────────────────────────────

    async function switchLiveCamera(deviceId, label, video) {
        // Stop existing tracks
        if (video.srcObject) {
            video.srcObject.getTracks().forEach(t => t.stop());
        }
        try {
            const stream = await _getUserMedia({
                video: { deviceId: { exact: deviceId } },
                audio: false,
            });
            video.srcObject = stream;
            // Persist new preference so next page load auto-selects this camera
            savePref(deviceId, label);
            pref = { deviceId, label };
        } catch (err) {
            console.error('[OmeTV Switcher] Could not switch camera:', err);
        }
    }

    // ─── UI ───────────────────────────────────────────────────────────────────

    const STYLES = `
        #ometv-cam-switcher {
            position: absolute;
            bottom: 12px;
            left: 12px;
            z-index: 9999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        }
        #ometv-cam-btn {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 6px 10px;
            background: rgba(0, 0, 0, 0.65);
            color: #fff;
            border: 1px solid rgba(255, 255, 255, 0.18);
            border-radius: 8px;
            cursor: pointer;
            font-size: 12px;
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
            transition: background 0.15s, border-color 0.15s;
            white-space: nowrap;
            line-height: 1;
        }
        #ometv-cam-btn:hover {
            background: rgba(0, 0, 0, 0.85);
            border-color: rgba(255, 255, 255, 0.35);
        }
        #ometv-cam-dropdown {
            display: none;
            position: absolute;
            bottom: calc(100% + 8px);
            left: 0;
            min-width: 240px;
            background: rgba(16, 16, 22, 0.97);
            border: 1px solid rgba(255, 255, 255, 0.12);
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.65);
            backdrop-filter: blur(12px);
            -webkit-backdrop-filter: blur(12px);
        }
        #ometv-cam-dropdown.open {
            display: block;
            animation: ometv-fadein 0.12s ease;
        }
        @keyframes ometv-fadein {
            from { opacity: 0; transform: translateY(6px); }
            to   { opacity: 1; transform: translateY(0); }
        }
        #ometv-cam-header {
            padding: 8px 14px;
            font-size: 10px;
            font-weight: 600;
            letter-spacing: 0.6px;
            text-transform: uppercase;
            color: rgba(255, 255, 255, 0.35);
            border-bottom: 1px solid rgba(255, 255, 255, 0.07);
        }
        .ometv-cam-option {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px 14px;
            color: rgba(255, 255, 255, 0.7);
            font-size: 13px;
            cursor: pointer;
            border-bottom: 1px solid rgba(255, 255, 255, 0.05);
            transition: background 0.1s, color 0.1s;
            user-select: none;
        }
        .ometv-cam-option:last-child { border-bottom: none; }
        .ometv-cam-option:hover { background: rgba(255,255,255,0.07); color: #fff; }
        .ometv-cam-option.active { color: #5eead4; }
        .ometv-cam-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            border: 2px solid rgba(255, 255, 255, 0.25);
            flex-shrink: 0;
            transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
        }
        .ometv-cam-option.active .ometv-cam-dot {
            background: #5eead4;
            border-color: #5eead4;
            box-shadow: 0 0 6px rgba(94, 234, 212, 0.5);
        }
        #ometv-cam-clear {
            display: block;
            width: 100%;
            padding: 9px 14px;
            background: none;
            border: none;
            border-top: 1px solid rgba(255, 255, 255, 0.07);
            color: rgba(239, 68, 68, 0.65);
            font-size: 12px;
            text-align: left;
            cursor: pointer;
            transition: background 0.1s, color 0.1s;
        }
        #ometv-cam-clear:hover {
            background: rgba(239, 68, 68, 0.08);
            color: rgb(239, 68, 68);
        }
    `;

    const CAMERA_ICON = `<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0">
        <path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/>
    </svg>`;

    const CHEVRON = `<svg width="9" height="9" viewBox="0 0 10 6" fill="currentColor" style="opacity:0.5;flex-shrink:0">
        <path d="M0 0l5 6 5-6z"/>
    </svg>`;

    function buildUI(cameras, video) {
        const currentPref = loadPref();

        const wrap = document.createElement('div');
        wrap.id = 'ometv-cam-switcher';

        // Inject styles once
        if (!document.getElementById('ometv-cam-styles')) {
            const style = document.createElement('style');
            style.id = 'ometv-cam-styles';
            style.textContent = STYLES;
            document.head.appendChild(style);
        }

        // Build dropdown options
        const optionsHTML = cameras.map(cam => {
            const name = cam.label || `Camera ${cam.deviceId.slice(0, 8)}`;
            const isActive = currentPref && cam.deviceId === currentPref.deviceId;
            return `<div class="ometv-cam-option${isActive ? ' active' : ''}"
                        data-device-id="${cam.deviceId}"
                        data-label="${name.replace(/"/g, '&quot;')}">
                        <span class="ometv-cam-dot"></span>
                        <span>${name}</span>
                    </div>`;
        }).join('');

        const activeName = currentPref
            ? cameras.find(c => c.deviceId === currentPref.deviceId)?.label || currentPref.label
            : (cameras[0]?.label || 'Camera');

        wrap.innerHTML = `
            <div id="ometv-cam-dropdown">
                <div id="ometv-cam-header">Select Camera</div>
                ${optionsHTML}
                <button id="ometv-cam-clear">βœ• &nbsp;Clear saved preference</button>
            </div>
            <button id="ometv-cam-btn">
                ${CAMERA_ICON}
                <span id="ometv-cam-label">${truncate(activeName, 24)}</span>
                ${CHEVRON}
            </button>
        `;

        const btn       = wrap.querySelector('#ometv-cam-btn');
        const dropdown  = wrap.querySelector('#ometv-cam-dropdown');
        const labelEl   = wrap.querySelector('#ometv-cam-label');
        const clearBtn  = wrap.querySelector('#ometv-cam-clear');

        // Toggle dropdown
        btn.addEventListener('click', e => {
            e.stopPropagation();
            dropdown.classList.toggle('open');
        });

        // Close on outside click
        document.addEventListener('click', () => dropdown.classList.remove('open'));
        wrap.addEventListener('click', e => e.stopPropagation());

        // Camera selection
        wrap.querySelectorAll('.ometv-cam-option').forEach(option => {
            option.addEventListener('click', async () => {
                const deviceId = option.dataset.deviceId;
                const label = option.dataset.label;

                // Update UI immediately
                wrap.querySelectorAll('.ometv-cam-option').forEach(o => o.classList.remove('active'));
                option.classList.add('active');
                labelEl.textContent = truncate(label, 24);
                dropdown.classList.remove('open');

                await switchLiveCamera(deviceId, label, video);
            });
        });

        // Clear preference
        clearBtn.addEventListener('click', () => {
            clearPref();
            pref = null;
            wrap.querySelectorAll('.ometv-cam-option').forEach(o => o.classList.remove('active'));
            labelEl.textContent = truncate(cameras[0]?.label || 'Camera', 24);
            dropdown.classList.remove('open');
        });

        return wrap;
    }

    async function injectSwitcher() {
        // Don't inject twice (e.g. on SPA navigation)
        if (document.getElementById('ometv-cam-switcher')) return;

        const video = await waitForElement('#local-video');
        if (!video) return;

        let cameras = [];
        try {
            const devices = await navigator.mediaDevices.enumerateDevices();
            cameras = devices.filter(d => d.kind === 'videoinput');
        } catch (err) {
            console.warn('[OmeTV Switcher] enumerateDevices failed:', err);
            return;
        }

        if (cameras.length === 0) return;

        // Find the best anchor element
        const anchor =
            document.querySelector('.media-devices__frame') ||
            video.closest('[class]') ||
            video.parentElement;

        if (!anchor) return;

        anchor.style.position = 'relative';
        anchor.appendChild(buildUI(cameras, video));
    }

    // ─── Init ─────────────────────────────────────────────────────────────────

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', injectSwitcher);
    } else {
        injectSwitcher();
    }

    // Re-inject if OmeTV does a SPA route change that re-renders the video
    const _pushState = history.pushState.bind(history);
    history.pushState = function (...args) {
        _pushState(...args);
        setTimeout(injectSwitcher, 500);
    };

})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment