mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
300 lines
9.4 KiB
HTML
300 lines
9.4 KiB
HTML
<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> |