Files
archived-vdo.ninja/devices.html
steveseguin c9380616de merge fix
2025-05-12 21:45:08 -04:00

300 lines
9.4 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<html>
<head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./main.css" />
<link rel="stylesheet" href="./devices.css?ver=2" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf8" />
</head>
<body style="position:static" >
<div id="header">
<a
id="logoname"
href="./"
style="text-decoration: none; color: white; margin: 2px"
>
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div id="devices">
<div class="notice">
Device IDs are bound to a combination of domain and browser. <br />If
you want to use electron-capture, open this URL on the electron-capture
app. <br/>
Click device names to preset them. Select multiple audio inputs by clicking multiple devices.
</div>
<div class="notice">
Check for browser and camera capabilities <a href="/supports">here</a>.
</div>
<div class="card">
<h1>🎤 Audio Inputs</h1>
<div id="audioInputs"></div>
</div>
<div class="card">
<h1>📹 Video Inputs</h1>
<div id="videoInputs"></div>
</div>
<div class="card">
<h1>🔉 Audio Outputs</h1>
<div id="audioOutputs"></div>
</div>
<div class="notice info-box">
<h3>🔑 Important Device Information</h3>
<ul>
<li><strong>Device IDs are security-scoped</strong> to specific origins/domains and browsers. They will appear different across domains and after clearing browser data for security reasons.</li>
<li><strong>Speaker/Output selection</strong> requires granting microphone permissions first. Some browsers may require additional permissions.</li>
<li>For iframes, add <code>allow="microphone *; camera *"</code> to enable device access.</li>
<li><strong>Browser compatibility:</strong> Firefox does not support audio output selection via <code>sinkId</code>. Safari added partial support in recent versions.</li>
<li>If device labels appear as "Default" or are missing, you need to grant media permissions before they'll become visible.</li>
</ul>
</div>
</div>
<div id="sharedDevices" style="display: none">
<span>Click to copy. Use this URL to preset audio/video devices.</span>
<span id="close" onclick="this.parentNode.style.display='none'">×</span>
<input id="devicesUrl" value="" />
</div>
<script>
const list = [];
const url = new URL(document.location.origin);
const audioInputDevices = [];
function isAudioInput(value) {
return value.kind === "audioinput";
}
function isAudioOutput(value) {
return value.kind === "audiooutput";
}
function isVideoInput(value) {
return value.kind === "videoinput";
}
function sanitizeDeviceName(deviceName) {
return String(deviceName).toLowerCase().replace(/[\W]+/g, "_");
}
function addDevice(element) {
const type = element.dataset.deviceType;
const device = sanitizeDeviceName(element.querySelector('span').innerText);
if (type === "audioinput") {
setAudioSearchParams(element);
}
if (type === "videoinput") {
setVideoSearchParams(element);
}
if (type === "audiooutput") {
setAudioOutputSearchParams(element);
}
/*
Allows for multiple audio devices to be selected
Will be output as a comma separated string to &ad
*/
function setAudioSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
// Remove device from list of selected devices
const index = audioInputDevices.indexOf(device);
if (index !== -1) {
audioInputDevices.splice(index, 1);
}
// Set the url param to the devices that are left
url.searchParams.set("ad", audioInputDevices.join(","));
element.className = "device";
// If no audio devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("ad");
}
} else {
// Device is unselected
audioInputDevices.push(device);
url.searchParams.set("ad", audioInputDevices.join(","));
element.className = "device selected";
}
}
/*
Only allows for a single video device to be selected
*/
function setVideoSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
element.className = "device";
// Set the url param to the devices that are left
url.searchParams.set("vd", device);
element.className = "device";
// If no devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("vd");
}
} else {
// Device is unselected
try {
element.parentElement.querySelector('.device.selected').className = "device";
} catch (error) {
console.log("There was no video device already selected.");
}
url.searchParams.set("vd", device);
element.className = "device selected";
}
}
/*
Only allows for a single audio output device to be selected
*/
function setAudioOutputSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
element.className = "device";
// Set the url param to the devices that are left
url.searchParams.set("od", device);
element.className = "device";
// If no devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("od");
}
} else {
// Device is unselected
try {
element.parentElement.querySelector('.device.selected').className = "device";
} catch (error) {
console.log("There was no video device already selected.");
}
url.searchParams.set("od", device);
element.className = "device selected";
}
}
// Update UI
showDeviceIdsPopup();
}
function showDeviceIdsPopup() {
document.getElementById("devicesUrl").value = decodeURIComponent(url);
document.getElementById("sharedDevices").style.display = "block";
}
function prettyPrint(json, element) {
let output = "<div class='prettyJson two-col'>";
let nestedObjs;
Object.entries(json)
.sort()
.forEach(([key, value]) => {
output += `
<div class='device' onclick='addDevice(this)' data-device-type='${value.kind}'>
<span class='device-name'>${value.label}</span>
<span class='device-id'>
${value.deviceId}
</span>
</div>`;
});
output += "</div>";
document.getElementById(element).innerHTML = output;
}
document.getElementById("devicesUrl").onclick = (e) => {
e.target.select();
document.execCommand("copy");
};
async function requestPermissions() {
try {
// Request temporary audio/video access to get device labels
await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
.then(stream => {
// Stop all tracks immediately after getting permission
stream.getTracks().forEach(track => track.stop());
})
.catch(err => {
console.warn("Partial permission denied:", err.name);
// Still try to enumerate even with partial permission
});
} catch (e) {
console.warn("Permission request failed:", e);
}
}
async function enumerateDevices() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
// If we don't have labels, request permissions and try again
if (devices.some(device => !device.label)) {
await requestPermissions();
const devicesWithLabels = await navigator.mediaDevices.enumerateDevices();
devices.forEach((device) => {
console.log(
`${device.kind}: ${device.label} id = ${device.deviceId}`
);
list.push(device);
});
prettyPrint(devicesWithLabels.filter(isAudioInput), "audioInputs");
prettyPrint(devicesWithLabels.filter(isAudioOutput), "audioOutputs");
prettyPrint(devicesWithLabels.filter(isVideoInput), "videoInputs");
} else {
// We already have labels, proceed normally
devices.forEach((device) => {
console.log(
`${device.kind}: ${device.label} id = ${device.deviceId}`
);
list.push(device);
});
prettyPrint(devices.filter(isAudioInput), "audioInputs");
prettyPrint(devices.filter(isAudioOutput), "audioOutputs");
prettyPrint(devices.filter(isVideoInput), "videoInputs");
}
} catch (err) {
console.error(`${err.name}: ${err.message}`);
}
}
enumerateDevices();
</script>
<style>
.info-box {
background-color: #f8f9fa;
border-left: 4px solid #17a2b8;
padding: 10px 15px;
margin-bottom: 20px;
text-align: left;
color: black;;
}
.info-box h3 {
margin-top: 0;
color: #17a2b8;
}
.info-box ul {
margin-bottom: 0;
padding-left: 20px;
color:black;
}
.info-box li {
margin-bottom: 5px;
}
.info-box code {
background-color: #e9ecef;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
</style>
</body>
</html>