-
-
Save robiot/fb05b6528a76ec1142842913b5eca38a to your computer and use it in GitHub Desktop.
| // 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."); | |
| } |
I think they patched this, since I made this 10 months ago. :(
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?
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.
Seems like a problem at ome.tv's side. But man, thanks for the effort!
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!
You rock dude. Works amazing! Finally can use continuity cam on mac.
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, '"')}">
<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">β 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);
};
})();
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?