Add files via upload

This commit is contained in:
Steve Seguin
2025-05-09 03:01:56 -04:00
committed by GitHub
parent ca289d1bbb
commit 239a3a6d05
9 changed files with 3274 additions and 257 deletions

View File

@@ -186,14 +186,33 @@
document.getElementById("page2").classList.remove("hidden"); document.getElementById("page2").classList.remove("hidden");
} }
function next2(){ async function runCodecDetection() {
document.getElementById("page2").classList.add("hidden"); try {
document.getElementById("page2a").classList.remove("hidden"); console.log("Starting advanced codec detection...");
setTimeout(function(){ const detectedCodecs = await detectCodecs();
if (document.getElementById("playButton") && !document.getElementById("playButton").skip){ logged.push({detectedCodecs});
next2a(); console.log("Codec detection complete", detectedCodecs);
} return detectedCodecs;
},10000); } catch (error) {
console.error("Error in codec detection:", error);
logged.push({codecDetectionError: error.message});
return null;
}
}
// Call codec detection at appropriate time
function next2() {
document.getElementById("page2").classList.add("hidden");
document.getElementById("page2a").classList.remove("hidden");
// Run codec detection in parallel with speed test loading
runCodecDetection();
setTimeout(function() {
if (document.getElementById("playButton") && !document.getElementById("playButton").skip) {
next2a();
}
}, 10000);
} }
function next2a(){ function next2a(){
@@ -544,7 +563,623 @@
<!-- <option value="sing1">Singapore</option> --> <!-- <option value="sing1">Singapore</option> -->
<!-- <option value="ind1">Mumbai, India</option> --> <!-- <option value="ind1">Mumbai, India</option> -->
<!-- <option value="pol1">Warsaw, Poland</option> --> <!-- <option value="pol1">Warsaw, Poland</option> -->
function detectCodecs() {
const codecData = {
video: {},
audio: {},
webrtc: {},
webcodec: {},
mediaCapabilities: {}
};
// Basic mimeType detection function - returns all supported mime types
function getSupportedMimeTypes(media, types, codecs) {
const supported = [];
// First check simple types
types.forEach(type => {
const mimeType = `${media}/${type}`;
if (MediaRecorder.isTypeSupported(mimeType)) {
supported.push(mimeType);
}
});
// Then check with codecs
types.forEach(type => {
const mimeType = `${media}/${type}`;
codecs.forEach(codec => {
if (!codec) return;
const variation = `${mimeType};codecs=${codec.toLowerCase()}`;
if (MediaRecorder.isTypeSupported(variation)) {
supported.push(variation);
const codecKey = codec.toLowerCase();
if (media === 'video') {
codecData.video[codecKey] = codecData.video[codecKey] || {};
codecData.video[codecKey].mediaRecorder = true;
codecData.video[codecKey].canEncode = true;
} else {
codecData.audio[codecKey] = codecData.audio[codecKey] || {};
codecData.audio[codecKey].mediaRecorder = true;
codecData.audio[codecKey].canEncode = true;
}
}
});
// Check codec combinations for video/webm
if (media === 'video' && type === 'webm') {
codecs.forEach(videoCodec => {
if (!videoCodec) return;
const audioCodecs = ['opus', 'vorbis'];
audioCodecs.forEach(audioCodec => {
const variation = `${mimeType};codecs=${videoCodec.toLowerCase()},${audioCodec}`;
if (MediaRecorder.isTypeSupported(variation)) {
supported.push(variation);
}
});
});
}
});
return supported;
}
// WebRTC hardware acceleration checker
async function checkWebRTCHardwareAcceleration(codec) {
return new Promise(async (resolve) => {
try {
const config = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
iceCandidatePoolSize: 0
};
const pc1 = new RTCPeerConnection(config);
const pc2 = new RTCPeerConnection(config);
pc1.createDataChannel('test', {ordered: true});
pc1.onicecandidate = e => e.candidate && pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => e.candidate && pc1.addIceCandidate(e.candidate);
const canvas = document.createElement('canvas');
canvas.width = 1280;
canvas.height = 720;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const startTime = Date.now();
function animateCanvas() {
const elapsed = Date.now() - startTime;
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#e74c3c';
const x = 100 + Math.sin(elapsed / 500) * 100;
const y = canvas.height / 2 + Math.cos(elapsed / 500) * 100;
ctx.beginPath();
ctx.arc(x, y, 50, 0, Math.PI * 2);
ctx.fill();
if (Date.now() - startTime < 3000) {
requestAnimationFrame(animateCanvas);
}
}
animateCanvas();
const stream = canvas.captureStream(30);
stream.getVideoTracks().forEach(track => {
pc1.addTrack(track, stream);
});
const transceiver = pc1.getTransceivers()[0];
const codecs = RTCRtpSender.getCapabilities('video').codecs;
const targetCodec = codecs.find(c => {
return c.mimeType.toLowerCase().includes(codec.toLowerCase());
});
if (targetCodec) {
transceiver.setCodecPreferences([targetCodec]);
}
const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);
await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
setTimeout(async () => {
let hardwareAccelerated = false;
let profile = null;
try {
const stats = await pc1.getStats();
stats.forEach(stat => {
if (stat.type === 'outbound-rtp' && stat.kind === 'video') {
if (stat.encoderImplementation) {
const implementation = stat.encoderImplementation.toLowerCase();
hardwareAccelerated = implementation.includes('hardware') ||
implementation === 'externalencoder' ||
implementation === 'mediafoundationvideoacceleration' ||
implementation.includes('accelerator');
}
if (stat.codecId) {
stats.forEach(s => {
if (s.id === stat.codecId && s.sdpFmtpLine) {
const match = s.sdpFmtpLine.match(/profile-level-id=([0-9a-f]+)/i);
if (match) {
profile = match[1];
}
}
});
}
}
});
} catch (e) {
console.error(`Error getting WebRTC stats: ${e.message}`);
}
pc1.close();
pc2.close();
stream.getTracks().forEach(track => track.stop());
resolve({
hardwareAccelerated,
profile,
webrtc: true,
canEncode: true,
canDecode: true,
webrtcHwEncoding: hardwareAccelerated
});
}, 2000);
} catch (error) {
console.error(`WebRTC check failed for ${codec}: ${error.message}`);
resolve({ webrtc: false });
}
});
}
// WebCodec encoder detection
async function checkWebCodecEncoder(codec, width = 1280, height = 720, framerate = 30) {
if (!('VideoEncoder' in window)) {
return { webcodec: false };
}
let result = { webcodec: false };
try {
const codecMapping = {
'vp8': 'vp8',
'vp9': 'vp09.00.10.08',
'av1': 'av01.0.04M.08',
'h264': 'avc1.42001E',
'h265': 'hev1.1.6.L93.B0'
};
const webcodecFormat = codecMapping[codec.toLowerCase()] || codec;
// Check hardware acceleration with prefer-hardware
const hwConfig = {
codec: webcodecFormat,
hardwareAcceleration: 'prefer-hardware',
width,
height,
bitrate: 2000000,
framerate
};
const hwSupport = await VideoEncoder.isConfigSupported(hwConfig);
if (hwSupport && hwSupport.supported) {
result = {
webcodec: true,
hardwareAccelerated: true,
config: hwSupport.config,
canEncode: true
};
} else {
// Check with prefer-software
const swConfig = {
codec: webcodecFormat,
hardwareAcceleration: 'prefer-software',
width,
height,
bitrate: 2000000,
framerate
};
const swSupport = await VideoEncoder.isConfigSupported(swConfig);
if (swSupport && swSupport.supported) {
result = {
webcodec: true,
hardwareAccelerated: false,
config: swSupport.config,
canEncode: true
};
}
}
} catch (error) {
console.error(`WebCodec encoder check failed for ${codec}: ${error.message}`);
}
return result;
}
// WebCodec decoder detection
async function checkWebCodecDecoder(codec, width = 1280, height = 720, framerate = 30) {
if (!('VideoDecoder' in window)) {
return { webcodec: false };
}
let result = { webcodec: false };
try {
const codecMapping = {
'vp8': 'vp8',
'vp9': 'vp09.00.10.08',
'av1': 'av01.0.04M.08',
'h264': 'avc1.42001E',
'h265': 'hev1.1.6.L93.B0'
};
const webcodecFormat = codecMapping[codec.toLowerCase()] || codec;
// Check hardware acceleration with prefer-hardware
const hwConfig = {
codec: webcodecFormat,
hardwareAcceleration: 'prefer-hardware',
codedWidth: width,
codedHeight: height,
description: new Uint8Array(0)
};
const hwSupport = await VideoDecoder.isConfigSupported(hwConfig);
if (hwSupport && hwSupport.supported) {
result = {
webcodec: true,
hardwareAccelerated: true,
config: hwSupport.config,
canDecode: true
};
} else {
// Check with prefer-software
const swConfig = {
codec: webcodecFormat,
hardwareAcceleration: 'prefer-software',
codedWidth: width,
codedHeight: height,
description: new Uint8Array(0)
};
const swSupport = await VideoDecoder.isConfigSupported(swConfig);
if (swSupport && swSupport.supported) {
result = {
webcodec: true,
hardwareAccelerated: false,
config: swSupport.config,
canDecode: true
};
}
}
} catch (error) {
console.error(`WebCodec decoder check failed for ${codec}: ${error.message}`);
}
return result;
}
// Main detection function
async function runDetection() {
// Define codec lists
const videoTypes = ["webm", "mp4", "x-matroska", "ogg"];
const audioTypes = ["webm", "mp4", "ogg", "x-matroska", "wav"];
const videoCodecs = [
"vp8", "vp9", "av1", "h264", "h265",
"vp9.0", "vp8.0", "avc1", "av1x", "h.264", "h.265",
"av01.0.04M.08", "vp09.00.10.08", "avc1.42001E"
];
const audioCodecs = [
"opus", "vorbis", "mp3", "aac", "pcm",
"mp4a.40.2", "mp4a", "L16", "wav", "flac"
];
// 1. Detect MediaRecorder support
const supportedVideoTypes = getSupportedMimeTypes("video", videoTypes, videoCodecs);
const supportedAudioTypes = getSupportedMimeTypes("audio", audioTypes, audioCodecs);
console.log(`Found ${supportedVideoTypes.length} supported video MediaRecorder formats`);
console.log(`Found ${supportedAudioTypes.length} supported audio MediaRecorder formats`);
// 2. Detect WebCodec support
if ('VideoEncoder' in window || 'VideoDecoder' in window) {
console.log('Checking WebCodec support...');
const resolutions = [
{ width: 640, height: 360 },
{ width: 1280, height: 720 },
{ width: 1920, height: 1080 }
];
const webcodecCodecs = ['vp8', 'vp9', 'h264', 'av1', 'h265'];
for (const codec of webcodecCodecs) {
for (const res of resolutions) {
// Check encoder
const encoderResult = await checkWebCodecEncoder(codec, res.width, res.height, 30);
if (encoderResult.webcodec) {
codecData.video[codec] = codecData.video[codec] || {};
codecData.video[codec].webcodec = true;
codecData.video[codec].canEncode = true;
// Store specific hardware acceleration for WebCodec encoder
codecData.video[codec].webcodecHwEncoding = encoderResult.hardwareAccelerated;
// If any resolution has hardware acceleration, we'll mark it as supported
if (encoderResult.hardwareAccelerated) {
codecData.video[codec].hardwareAccelerated = true;
// Store the min resolution that allows hardware acceleration
if (!codecData.video[codec].minHwResolution ||
res.width * res.height <
codecData.video[codec].minHwResolution.width * codecData.video[codec].minHwResolution.height) {
codecData.video[codec].minHwResolution = res;
codecData.video[codec].resolutionLimit = `${res.width}x${res.height}+`;
}
}
// Only set hardware as false if it hasn't been set to true already
else if (codecData.video[codec].hardwareAccelerated !== true) {
codecData.video[codec].hardwareAccelerated = false;
}
if (encoderResult.config) {
codecData.video[codec].webcodecConfig = codecData.video[codec].webcodecConfig || {};
codecData.video[codec].webcodecConfig.encoder = encoderResult.config;
}
}
// Check decoder
const decoderResult = await checkWebCodecDecoder(codec, res.width, res.height, 30);
if (decoderResult.webcodec) {
codecData.video[codec] = codecData.video[codec] || {};
codecData.video[codec].webcodec = true;
codecData.video[codec].canDecode = true;
// Store specific hardware acceleration for WebCodec decoder
codecData.video[codec].webcodecHwDecoding = decoderResult.hardwareAccelerated;
// If any resolution has hardware acceleration, we'll mark it as supported
if (decoderResult.hardwareAccelerated) {
codecData.video[codec].hardwareAccelerated = true;
// Store the min resolution that allows hardware acceleration
if (!codecData.video[codec].minHwResolution ||
res.width * res.height <
codecData.video[codec].minHwResolution.width * codecData.video[codec].minHwResolution.height) {
codecData.video[codec].minHwResolution = res;
codecData.video[codec].resolutionLimit = `${res.width}x${res.height}+`;
}
}
// Only set hardware as false if it hasn't been set to true already
else if (codecData.video[codec].hardwareAccelerated !== true) {
codecData.video[codec].hardwareAccelerated = false;
}
if (decoderResult.config) {
codecData.video[codec].webcodecConfig = codecData.video[codec].webcodecConfig || {};
codecData.video[codec].webcodecConfig.decoder = decoderResult.config;
}
}
}
}
} else {
console.log('WebCodec API not supported by this browser');
}
// 3. Detect WebRTC support and hardware acceleration
if ('RTCPeerConnection' in window) {
console.log('Checking WebRTC support and hardware acceleration...');
const webrtcCodecs = ['VP8', 'VP9', 'AV1', 'H264'];
for (const codec of webrtcCodecs) {
const result = await checkWebRTCHardwareAcceleration(codec);
if (result.webrtc) {
const normalizedCodec = codec.toLowerCase();
codecData.video[normalizedCodec] = codecData.video[normalizedCodec] || {};
codecData.video[normalizedCodec].webrtc = true;
codecData.video[normalizedCodec].canEncode = true;
codecData.video[normalizedCodec].canDecode = true;
// Only update hardware acceleration if not already set
if (result.hardwareAccelerated) {
codecData.video[normalizedCodec].hardwareAccelerated = true;
} else if (codecData.video[normalizedCodec].hardwareAccelerated !== true) {
codecData.video[normalizedCodec].hardwareAccelerated = false;
}
if (result.profile) {
codecData.video[normalizedCodec].profile = result.profile;
}
if (result.webrtcHwEncoding) {
codecData.video[normalizedCodec].webrtcHwEncoding = true;
}
}
}
} else {
console.log('WebRTC not supported by this browser');
}
// 4. Check scalability modes and MediaCapabilities
if ('mediaCapabilities' in navigator) {
console.log('Checking MediaCapabilities API support...');
const videoTypes = ['vp8', 'vp9', 'av1', 'h264', 'h265', 'avc1'];
const resolutions = [
{ width: 640, height: 360 },
{ width: 1280, height: 720 },
{ width: 1920, height: 1080 }
];
// Define scalability modes to test
const scalabilityModes = [
"L1T1", "L1T2", "L1T3",
"L2T1", "L2T2", "L2T3",
"L3T1", "L3T2", "L3T3",
"S2T1", "S2T2", "S2T3",
"S3T1", "S3T2", "S3T3"
];
// Check browser-specific capability type
const isFirefox = navigator.userAgent.indexOf("Firefox") >= 0;
const capabilityType = isFirefox ? "transmission" : "webrtc";
// Check WebRTC codec capabilities
const webrtcCodecs = [];
if ('RTCRtpSender' in window && RTCRtpSender.getCapabilities) {
try {
const rtcCodecs = RTCRtpSender.getCapabilities('video').codecs;
rtcCodecs.forEach(codec => {
if (!['video/red', 'video/ulpfec', 'video/rtx'].includes(codec.mimeType)) {
const codecName = codec.mimeType.replace("video/", "").toLowerCase();
webrtcCodecs.push({
name: codecName,
mimeType: codec.mimeType,
sdpFmtpLine: codec.sdpFmtpLine,
clockRate: codec.clockRate
});
}
});
console.log(`Found ${webrtcCodecs.length} supported WebRTC codecs`);
} catch (e) {
console.error(`Error getting RTCRtpSender.getCapabilities: ${e.message}`);
}
}
// Process each codec with MediaCapabilities
for (const codec of webrtcCodecs) {
try {
const capabilityPromises = [];
const codecScalabilityModes = [];
// Test different scalability modes
for (const mode of scalabilityModes) {
for (const res of resolutions) {
capabilityPromises.push(
navigator.mediaCapabilities.encodingInfo({
type: capabilityType,
video: {
contentType: codec.mimeType,
width: res.width,
height: res.height,
bitrate: 2000000,
framerate: 30,
scalabilityMode: mode
}
}).then(result => {
return { mode, res, result };
}).catch(e => {
return { mode, res, error: e };
})
);
}
}
const results = await Promise.all(capabilityPromises);
// Process results
let isSupported = false;
let isSmooth = false;
let isPowerEfficient = false;
let supportedModes = [];
results.forEach(({ mode, res, result, error }) => {
if (result && !error) {
if (result.supported) {
isSupported = true;
if (result.smooth) {
isSmooth = true;
}
if (result.powerEfficient) {
isPowerEfficient = true;
}
if (!supportedModes.includes(mode)) {
supportedModes.push(mode);
}
}
}
});
if (isSupported) {
const codecName = codec.name;
codecData.video[codecName] = codecData.video[codecName] || {};
codecData.video[codecName].mediaCapabilities = true;
codecData.video[codecName].scalabilityModes = supportedModes;
// Assume MediaCapabilities is testing decoding
codecData.video[codecName].canDecode = true;
// If powerEfficient is true, this is a good indicator of hardware acceleration
if (isPowerEfficient && codecData.video[codecName].hardwareAccelerated !== true) {
codecData.video[codecName].hardwareAccelerated = true;
codecData.video[codecName].details = (codecData.video[codecName].details || '') +
'MediaCapabilities reports this codec as power efficient. ';
}
// Store SDP format parameters if available
if (codec.sdpFmtpLine && !codecData.video[codecName].profile) {
const profileMatch = codec.sdpFmtpLine.match(/profile-level-id=([0-9a-f]+)/i);
if (profileMatch) {
codecData.video[codecName].profile = profileMatch[1];
}
}
}
} catch (error) {
console.error(`MediaCapabilities check failed for ${codec.name}: ${error.message}`);
}
}
} else {
console.log('MediaCapabilities API not supported by this browser');
}
// Get browser info
const browserInfo = {
userAgent: navigator.userAgent,
platform: navigator.platform,
vendor: navigator.vendor
};
// Return complete codec data
return {
video: codecData.video,
audio: codecData.audio,
browserInfo
};
}
// Run detection and return results
return runDetection();
}
function loadIframe(zone="") { function loadIframe(zone="") {
@@ -673,11 +1308,10 @@
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open('POST', "https://record.vdo.workers.dev/?name="+recordResults); request.open('POST', "https://record.vdo.workers.dev/?name="+recordResults);
try { try {
logged = JSON.stringify(logged); request.send(JSON.stringify(logged));
} catch(e){ } catch(e){
console.error(e); console.error(e);
} }
request.send(logged);
timer = 91; timer = 91;
div.innerHTML = "Test ended"; div.innerHTML = "Test ended";

1769
codecs.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -278,7 +278,36 @@
position: absolute; position: absolute;
bottom: 0; bottom: 0;
} }
#lastUrls {
/* Keep your original styles for the select element */
font-size: calc(16px + 0.3vw);
width: 730px;
height: 100%;
flex: 20;
border-radius: 10px;
padding: 1em;
background: #101520;
color: white;
cursor: pointer;
}
/* These styles target the dropdown list in Webkit browsers */
#lastUrls::-webkit-listbox {
max-height: 200px !important;
}
/* For Firefox */
#lastUrls {
scrollbar-width: thin;
scrollbar-color: #384861 #182031;
}
/* This will help force the height of the dropdown menu in many browsers */
@supports (-moz-appearance:none) {
#lastUrls {
overflow: -moz-scrollbars-vertical;
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -321,28 +350,96 @@
* tree. Alternative licencing options can be made available on request. * tree. Alternative licencing options can be made available on request.
* *
*/ */
var lastUrls = JSON.parse(localStorage.getItem('lastUrls'));
if (lastUrls != undefined) {
document.querySelector("#changeText").value = lastUrls[0];
if (lastUrls.length>0){
lastUrls.forEach((url)=>{
var o = document.createElement('option');
o.value = url;
o.text = url;
document.querySelector("#lastUrls").appendChild(o);
})
} else {
document.querySelector("#history").style.display="none";
}
} else {
document.querySelector("#history").style.display="none";
}
function setUrl(){ function setUrl(){
document.querySelector("#changeText").value = document.querySelector("#lastUrls").value; document.querySelector("#changeText").value = document.querySelector("#lastUrls").value;
gohere(); gohere();
} }
function createCustomScrollableDropdown() {
const originalSelect = document.querySelector('#lastUrls');
if (!originalSelect) return;
// Create custom dropdown container
const dropdownContainer = document.createElement('div');
dropdownContainer.id = 'custom-lastUrls-container';
dropdownContainer.style.position = 'relative';
dropdownContainer.style.width = '730px';
dropdownContainer.style.flex = '20';
// Create the dropdown header (what shows when closed)
const dropdownHeader = document.createElement('div');
dropdownHeader.id = 'custom-lastUrls-header';
dropdownHeader.style.fontSize = 'calc(16px + 0.3vw)';
dropdownHeader.style.padding = '1em';
dropdownHeader.style.borderRadius = '10px';
dropdownHeader.style.background = '#FFF';
dropdownHeader.style.color = '#000';
dropdownHeader.style.cursor = 'pointer';
dropdownHeader.textContent = originalSelect.options.length > 0 ?
originalSelect.options[0].text : 'History';
// Create the dropdown list (what shows when opened)
const dropdownList = document.createElement('div');
dropdownList.id = 'custom-lastUrls-list';
dropdownList.style.display = 'none';
dropdownList.style.position = 'absolute';
dropdownList.style.width = '100%';
dropdownList.style.maxHeight = '200px';
dropdownList.style.overflowY = 'auto';
dropdownList.style.background = '#DDD';
dropdownList.style.borderRadius = '10px';
dropdownList.style.zIndex = '100';
// Add options to the dropdown list
Array.from(originalSelect.options).forEach((option, index) => {
const item = document.createElement('div');
item.className = 'custom-lastUrls-item';
item.style.padding = '0.5em 1em';
item.style.cursor = 'pointer';
item.textContent = option.text;
item.dataset.value = option.value;
if (index % 2 === 1) {
item.style.backgroundColor = '#FFF';
}
item.addEventListener('click', function() {
dropdownHeader.textContent = this.textContent;
dropdownList.style.display = 'none';
// Update the original select value and trigger its change event
originalSelect.value = this.dataset.value;
const event = new Event('change');
originalSelect.dispatchEvent(event);
});
dropdownList.appendChild(item);
});
// Toggle dropdown on header click
dropdownHeader.addEventListener('click', function() {
const isDisplayed = dropdownList.style.display === 'block';
dropdownList.style.display = isDisplayed ? 'none' : 'block';
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!dropdownContainer.contains(e.target)) {
dropdownList.style.display = 'none';
}
});
// Add everything to the container
dropdownContainer.appendChild(dropdownHeader);
dropdownContainer.appendChild(dropdownList);
// Replace the original select with our custom dropdown
originalSelect.style.display = 'none';
originalSelect.parentNode.insertBefore(dropdownContainer, originalSelect);
}
function resetHistory(){ function resetHistory(){
localStorage.clear(); localStorage.clear();
document.querySelector('#lastUrls').innerHTML = ''; document.querySelector('#lastUrls').innerHTML = '';
@@ -526,7 +623,6 @@ function createSpecialDeviceLink(deviceLabel) {
link.style.transition = 'opacity 2s ease-in-out'; link.style.transition = 'opacity 2s ease-in-out';
link.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; link.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
link.style.fontFamily = 'Arial, sans-serif'; link.style.fontFamily = 'Arial, sans-serif';
link.style.zIndex = '1000';
link.title = "Make this video device fully fill the window, making it perfect for screen capture."; link.title = "Make this video device fully fill the window, making it perfect for screen capture.";
document.body.appendChild(link); document.body.appendChild(link);
@@ -656,7 +752,7 @@ function addUrlToHistory(url){
} }
if ( lastUrls[0] != url ) { if ( lastUrls[0] != url ) {
lastUrls.unshift(url); lastUrls.unshift(url);
if (lastUrls.length == 6) { if (lastUrls.length == 100) {
lastUrls.pop(); lastUrls.pop();
} }
} }
@@ -696,6 +792,25 @@ function gohere(){
} }
window.location = url; window.location = url;
}; };
var lastUrls = JSON.parse(localStorage.getItem('lastUrls'));
if (lastUrls != undefined) {
document.querySelector("#changeText").value = lastUrls[0];
if (lastUrls.length > 0) {
lastUrls.forEach((url) => {
var o = document.createElement('option');
o.value = url;
o.text = url;
document.querySelector("#lastUrls").appendChild(o);
});
// Create custom dropdown after populating
createCustomScrollableDropdown();
} else {
document.querySelector("#history").style.display = "none";
}
} else {
document.querySelector("#history").style.display = "none";
}
getPermssions(); getPermssions();
</script> </script>
</body> </body>

157
ip.html Normal file
View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IP Address Decoder</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-top: 0;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-family: monospace;
}
textarea {
height: 80px;
resize: vertical;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.result {
margin-top: 20px;
}
footer {
margin-top: 20px;
text-align: center;
color: #777;
font-size: 0.8em;
}
</style>
</head>
<body>
<div class="container">
<h1>IP Address Decoder</h1>
<div class="input-group">
<label for="obfuscatedInput">Obfuscated Username:</label>
<input type="text" id="obfuscatedInput" placeholder="Enter encoded string (with or without timestamp part)">
</div>
<button id="decodeButton">Decode IP Address</button>
<div class="result">
<label for="decodedOutput">Decoded IP Address:</label>
<textarea id="decodedOutput" readonly></textarea>
</div>
</div>
<footer>
<p>IP Address Decoder Tool</p>
</footer>
<script>
function decodeIpAddress(obfuscatedUsername) {
try {
// Get the obfuscated part (remove timestamp part if it exists)
let encoded = obfuscatedUsername;
if (obfuscatedUsername.includes(':')) {
encoded = obfuscatedUsername.split(':')[1];
}
// 1. Move last character to the beginning
if (encoded.length > 1) {
const lastChar = encoded.charAt(encoded.length - 1);
encoded = lastChar + encoded.substring(0, encoded.length - 1);
}
// 2. Restore base64 standard format
encoded = encoded
.replace(/-/g, '+')
.replace(/_/g, '/');
// Add padding if needed
while (encoded.length % 4) {
encoded += '=';
}
// 3. Decode base64
const decoded = atob(encoded);
// 4. Remove the salt
const salt = "x";
if (decoded.endsWith(salt)) {
return decoded.substring(0, decoded.length - salt.length);
}
return decoded;
} catch (e) {
console.error('Error decoding IP:', e);
return 'Error: ' + e.message;
}
}
document.getElementById('decodeButton').addEventListener('click', function() {
const obfuscatedUsername = document.getElementById('obfuscatedInput').value.trim();
if (!obfuscatedUsername) {
document.getElementById('decodedOutput').value = 'Please enter an obfuscated username';
return;
}
const decodedIp = decodeIpAddress(obfuscatedUsername);
document.getElementById('decodedOutput').value = decodedIp || 'Failed to decode';
});
// Add sample data functionality
function addSampleData() {
const sampleButton = document.createElement('button');
sampleButton.textContent = 'Try Sample Data';
sampleButton.style.marginLeft = '10px';
sampleButton.style.backgroundColor = '#2196F3';
document.getElementById('decodeButton').after(sampleButton);
sampleButton.addEventListener('click', function() {
document.getElementById('obfuscatedInput').value = 'l0VwNVdlNjE0LjEyOS44OS4yMTF4';
document.getElementById('decodeButton').click();
});
}
addSampleData();
</script>
</body>
</html>

View File

@@ -1,63 +1,202 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" /> <link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./speedtest.css?ver=1" /> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<meta charset="utf8" /> <meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDON Speed Test</title> <title>VDON Speed Test</title>
<style> <style>
.fullscreen { :root {
width:100%; --primary-bg: #333;
height: calc(100% - 35px); --secondary-bg: #444;
position:absolute; --card-bg: #3a3a3a;
left:0; --text-color: white;
display:block; --accent-color: #4296f5;
background-color: #444; --accent-hover: #2979e2;
color:white; --success-color: #60ff60;
margin: auto; --warning-color: #f3f304;
padding-top: 35px; --error-color: #fb4949;
transition: all ease-in 1s; --border-color: #666;
animation-name: fadein;
animation-duration: .3s;
} }
body {
text-align: center;
background: var(--primary-bg);
font-family: 'Noto Sans', sans-serif;
color: var(--text-color);
margin: 0;
padding: 0;
line-height: 1.6;
}
h1, h2, h3 {
margin-top: 1rem;
margin-bottom: 1rem;
}
h2 {
width: 760px;
max-width: 90%;
margin: auto;
}
a {
color: white;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
#mainapp {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
#container {
background: var(--secondary-bg);
border-radius: 8px;
padding: 1.5rem;
margin: 1.5rem auto;
max-width: 900px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
#graphs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
margin: 2rem auto;
max-width: 1100px;
}
.graph {
background: var(--card-bg);
border-radius: 8px;
padding: 0;
width: 300px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.graph h3 {
margin-top: 0;
margin-bottom: 0.5rem;
}
.graph span {
display: block;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
canvas {
width: 100%;
height: 100px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
#details {
background: var(--secondary-bg);
border-radius: 8px;
padding: 1.5rem;
margin: 1.5rem auto;
max-width: 900px;
text-align: left;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
padding: 10px 15px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: var(--accent-hover);
}
.hidden {
display: none !important;
}
#detailed-info {
margin-top: 30px;
padding: 20px;
border-top: 1px solid var(--border-color);
background: rgba(0, 0, 0, 0.1);
border-radius: 0 0 8px 8px;
}
.info-buttons {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin-top: 15px;
}
.info-buttons button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
padding: 10px 15px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.info-buttons button:hover {
background-color: var(--accent-hover);
}
.success {
color: var(--success-color);
}
.warning {
color: var(--warning-color);
}
.error {
color: var(--error-color);
}
/* Blink animation for loading text */
@keyframes fadein { @keyframes fadein {
0% {opacity: 0.5;} 0% {opacity: 0.5;}
100% {opacity: 1;} 100% {opacity: 1;}
} }
a {
color: white; blink {
animation: blink 1s step-end infinite;
} }
#controls button {
cursor: pointer; @keyframes blink {
display: inline; 50% { opacity: 0; }
padding: 20px;
} }
.hidden {
display:none!important; /* Make design responsive */
} @media (max-width: 768px) {
body { #graphs {
text-align: center; flex-direction: column;
height:unset; align-items: center;
background: #444; }
font-family: 'Noto Sans', sans-serif;
color:white; .graph {
} width: 90%;
h2 { }
width: 760px;
max-width: 90%;
margin: auto;
}
li {
text-align: left;
}
button{
margin: 50px auto;
font-size: 120%;
padding: 20px 30px;
cursor:pointer;
} }
</style> </style>
</head> </head>
@@ -233,36 +372,47 @@
var PAK = 0; var PAK = 0;
var PAKCCC = 0; var PAKCCC = 0;
var packetLossSpikes = 0;
function process(arr) { function process(arr) {
var maxResolution = "0 x 0"; var maxResolution = "0 x 0";
console.log(arr); console.log(arr);
arr.forEach(data=>{
if ("bitrate" in data){ // Clear container
updateData("bitrate", data.bitrate); document.getElementById("container").innerHTML = "<h3>Quality Test Results</h3>";
if (data.bitrate!==null){
BBB += data.bitrate; arr.forEach(data => {
counter += 1; if ("bitrate" in data) {
updateData("bitrate", data.bitrate);
if (data.bitrate !== null) {
BBB += data.bitrate;
counter += 1;
}
}
if ("target" in data) {
updateData("target", data.target);
}
if ("buffer" in data) {
updateData("buffer", data.buffer);
if (data.buffer !== null) {
BUFF += data.buffer;
BUFFCCC += 1;
}
}
if (data.packetloss !== undefined && !isNaN(data.packetloss)) {
updateData("packetloss", data.packetloss);
if (data.packetloss !== null) {
PAK += parseFloat(data.packetloss) || 0;
PAKCCC += 1;
if (parseFloat(data.packetloss) > 2.0) {
packetLossSpikes++;
} }
}
} }
if ("target" in data){
updateData("target", data.target); if (data.timestart) {
} document.getElementById("details").innerHTML += "<br /><b>Test start time:</b> " + timeConverter(data.timestart) + "<br />";
if ("buffer" in data){
updateData("buffer", data.buffer);
if (data.buffer!==null){
BUFF += data.buffer;
BUFFCCC += 1;
}
}
if ("packetloss" in data){
updateData("packetloss", data.packetloss);
if (data.packetloss!==null){
PAK += parseFloat(data.packetloss) || 0;
PAKCCC += 1;
}
}
if (data.timestart){
document.getElementById("details").innerHTML += "<br /><b>Test start time:</b> "+timeConverter(data.timestart)+"<br />";
} }
if ("peakhour" in data){ if ("peakhour" in data){
if (!data.peakhour){ if (!data.peakhour){
@@ -271,6 +421,29 @@
} }
} }
if (data.codecs) {
var videoCodecs = Object.keys(data.codecs.video).join(', ');
var audioCodecs = Object.keys(data.codecs.audio).join(', ');
if (videoCodecs) {
document.getElementById("details").innerHTML += "<br /><b>Supported video codecs:</b> " + videoCodecs + "<br />";
}
if (audioCodecs) {
document.getElementById("details").innerHTML += "<b>Supported audio codecs:</b> " + audioCodecs + "<br />";
}
// Check for optimal codecs
if (data.codecs.video.h264 || data.codecs.video.vp9) {
document.getElementById("details").innerHTML += "<small><i>(Good codec support for streaming)</i></small><br />";
} else if (!data.codecs.video.h264) {
document.getElementById("details").innerHTML += "<small><i>(H.264 codec support not detected, which may limit compatibility)</i></small><br />";
}
}
if (data.userRegion) {
document.getElementById("details").innerHTML += "<br /><b>User region:</b> " + data.userRegion + "<br />";
}
if (("hostMode" in data) && ("srflxMode" in data)){ if (("hostMode" in data) && ("srflxMode" in data)){
if (!data.hostMode && !data.srflxMode){ if (!data.hostMode && !data.srflxMode){
@@ -355,66 +528,72 @@
} }
var total = QLR_1 + QLR_2 + QLR_3; // Apply styling to the results messages
if (QLR_2/total>0.5){ var total = QLR_1 + QLR_2 + QLR_3;
document.getElementById("container").innerHTML += "Serious CPU overload issues. Consider reducing the capture resolution.<br />"; if (QLR_2/total > 0.5) {
} else if (QLR_2/total>0.1){ document.getElementById("container").innerHTML += "<p class='error'>Serious CPU overload issues. Consider reducing the capture resolution.</p>";
document.getElementById("container").innerHTML += "Occassional CPU overload issues. Consider reducing the capture resolution.<br />"; } else if (QLR_2/total > 0.1) {
} document.getElementById("container").innerHTML += "<p>Occasional CPU overload issues. Consider reducing the capture resolution.</p>";
if (QLR_3/total>0.5){ }
document.getElementById("container").innerHTML += "The network quality or bandwidth limited the performance.<br />";
} else if (QLR_3/total>0.1){ if (QLR_3/total > 0.5) {
document.getElementById("container").innerHTML += "The network quality or bandwidth may have limited the performance.<br />"; document.getElementById("container").innerHTML += "<p class='error'>The network quality or bandwidth limited the performance.</p>";
} } else if (QLR_3/total > 0.1) {
document.getElementById("container").innerHTML += "<p>The network quality or bandwidth may have limited the performance.</p>";
document.getElementById("container").innerHTML += "The average video bitrate was: "+parseInt(BBB/counter)+"-kbps<br />"; }
if (BBB/counter<500){ document.getElementById("container").innerHTML += "<p><b>Average video bitrate:</b> " + parseInt(BBB/counter) + "-kbps</p>";
document.getElementById("container").innerHTML += "<small><i>Did they select an active camera?</i></small><h3>Bitrate is really <span style='color:#fb4949'>bad<span></h3>";
} if (BBB/counter < 500) {
else if (BBB/counter<1000){ document.getElementById("container").innerHTML += "<small><i>Did they select an active camera?</i></small><h3>Bitrate is really <span class='error'>bad</span></h3>";
} else if (BBB/counter < 1000) {
document.getElementById("container").innerHTML += "<h3>Bitrate is poor</h3>"; document.getElementById("container").innerHTML += "<h3>Bitrate is poor</h3>";
} } else if (BBB/counter < 2000) {
else if (BBB/counter<2000){
document.getElementById("container").innerHTML += "<h3>Bitrate a bit low</h3>"; document.getElementById("container").innerHTML += "<h3>Bitrate a bit low</h3>";
} } else {
else { document.getElementById("container").innerHTML += "<h3>Bitrate is <span class='success'>good</span></h3>";
document.getElementById("container").innerHTML += "<h3>Bitrate is <span style='color:#60ff60'>good<span></h3>"; }
}
document.getElementById("container").innerHTML += "<p><b>Average video buffer length:</b> " + parseInt(BUFF/BUFFCCC) + "-ms</p>";
document.getElementById("container").innerHTML += "<br />The average video buffer length was: "+parseInt(BUFF/BUFFCCC)+"-ms<br />";
if (BUFF/BUFFCCC > 500) {
if (BUFF/BUFFCCC>500){ document.getElementById("container").innerHTML += "<h3>Video delay is really <span class='error'>bad</span></h3>";
document.getElementById("container").innerHTML += "<h3>Video delay is really <span style='color:#fb4949'>bad<span></h3><br />"; } else if (BUFF/BUFFCCC > 200) {
} document.getElementById("container").innerHTML += "<h3>Video delay is poor</h3>";
else if (BUFF/BUFFCCC>200){ } else if (BUFF/BUFFCCC > 100) {
document.getElementById("container").innerHTML += "<h3>Video delay is poor</h3><br />"; document.getElementById("container").innerHTML += "<h3>Video delay is sub-optimal</h3>";
} } else {
else if (BUFF/BUFFCCC>100){ document.getElementById("container").innerHTML += "<h3>Video delay is <span class='success'>good</span></h3>";
document.getElementById("container").innerHTML += "<h3>Video delay is sub-optimal</h3><br />"; }
}
else { document.getElementById("container").innerHTML += "<p><b>Average video packet loss:</b> " + (parseInt(PAK*1000/PAKCCC)/1000.0) + "%</p>";
document.getElementById("container").innerHTML += "<h3>Video delay is <span style='color:#60ff60'>good<span></h3><br />";
} if (PAK/PAKCCC > 3) {
document.getElementById("container").innerHTML += "<h3>Packet loss is extremely <span class='error'>bad</span>; Must Fix This</h3>";
document.getElementById("container").innerHTML += "The average video packet loss was: "+(parseInt(PAK*1000/PAKCCC)/1000.0)+"%<br />"; } else if (PAK/PAKCCC > 0.8) {
document.getElementById("container").innerHTML += "<h3>Packet loss is quite <span class='error'>bad</span>; expect problems with audio and video</h3>";
if (PAK/PAKCCC>3){ } else if (PAK/PAKCCC > 0.15) {
document.getElementById("container").innerHTML += "<h3>Packet loss is extremely <span style='color:#fb4949'>bad<span>; Must Fix This</h3>";
}
else if (PAK/PAKCCC>0.8){
document.getElementById("container").innerHTML += "<h3>Packet loss is quite <span style='color:#fb4949'>bad<span>; expect problems with audio and video</h3>";
}
else if (PAK/PAKCCC>.15){
document.getElementById("container").innerHTML += "<h3>Packet loss is a bit high; might be a testing-server issue though</h3>"; document.getElementById("container").innerHTML += "<h3>Packet loss is a bit high; might be a testing-server issue though</h3>";
} } else {
else { document.getElementById("container").innerHTML += "<h3>Packet loss is <span class='success'>good</span></h3>";
document.getElementById("container").innerHTML += "<h3>Packet loss is <span style='color:#60ff60'>good<span></h3>"; }
if (packetLossSpikes > 0) {
document.getElementById("details").innerHTML += "<div class='warning'><b>Packet loss spikes detected:</b> " + packetLossSpikes +
" occurrences of packet loss > 2%<br><small><i>(Spikes in packet loss can cause video freezes or audio dropouts even if average packet loss is low)</i></small></div>";
}
// Render the codec details button (original code)
if (arr.some(data => data.codecs || data.detectedCodecs)) {
document.getElementById("details").innerHTML += `<br />
<button id="view-codec-details" onclick="viewCodecDetails('${streamID}')">View Codec Support Details</button>
`;
window.viewCodecDetails = function(resultsId) {
window.open(`./codecs.html?results=${resultsId}`, '_blank');
};
}
} }
console.log(QLR_1, QLR_2, QLR_3);
}
var xmlhttp = new XMLHttpRequest(); var xmlhttp = new XMLHttpRequest();
var url = "https://record.vdo.workers.dev/?name="+streamID; var url = "https://record.vdo.workers.dev/?name="+streamID;

View File

@@ -1,8 +1,14 @@
<!-- speedtest.html !-->
<html> <html>
<head> <head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" /> <link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./speedtest.css?ver=7" /> <link rel="stylesheet" href="./speedtest.css?ver=7" />
<meta charset="utf8" /> <meta charset="utf8" />
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link id="favicon1" rel="icon" type="image/png" sizes="32x32" href="./media/favicon-32x32.png" />
<link id="favicon2" rel="icon" type="image/png" sizes="16x16" href="./media/favicon-16x16.png" />
<link id="favicon3" rel="icon" href="./media/favicon.ico" />
<link rel="alternate" type="text/markdown" href="/rawdoc.md" title="Developer and User Documentation">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>VDON Speed Test</title> <title>VDON Speed Test</title>
</head> </head>
@@ -230,7 +236,7 @@
iframe.allowfullscreen ="true"; iframe.allowfullscreen ="true";
//iframe.allow = "autoplay"; //iframe.allow = "autoplay";
var srcString = "./?push=" + streamID + "&cleanoutput&privacy&"+testType+"&audiodevice=0&fullscreen&transparent&remote&speedtest="+zone; var srcString = "./?push=" + streamID + "&cleanish&privacy&"+testType+"&audiodevice=0&fullscreen&transparent&remote&speedtest="+zone;
if (urlParams.has("turn")) { if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn"); srcString = srcString + "&turn=" + urlParams.get("turn");
@@ -274,7 +280,7 @@
iframe.allow = "autoplay"; iframe.allow = "autoplay";
// I've removed &privacy from the view link, and left it just on the push link. This hopefully solves compatibility issues // I've removed &privacy from the view link, and left it just on the push link. This hopefully solves compatibility issues
var srcString = "./?view=" + streamID + "&cleanoutput&noaudio&scale=0&speedtest="+zone; // No TURN servers set on the reciever. Don't want to query for TURN servers needlessly. var srcString = "./?view=" + streamID + "&cleanish&noaudio&scale=0&speedtest="+zone; // No TURN servers set on the reciever. Don't want to query for TURN servers needlessly.
if (urlParams.has("turn")) { if (urlParams.has("turn")) {
srcString = srcString + "&turn=" + urlParams.get("turn"); srcString = srcString + "&turn=" + urlParams.get("turn");

View File

@@ -293,7 +293,14 @@
<option value="url" selected>Website URL</option> <option value="url" selected>Website URL</option>
<option value="twitch">Twitch</option> <option value="twitch">Twitch</option>
</select > </select >
<select style="border-radius:10px;margin-left:5px;margin-top: 13px;width:unset!important;" onchange="rotatePage();" class="changeText" id="zoomLevel" title="Change the scale of the content">
<option value="1" selected>100% (Normal)</option>
<option value="0.75">75%</option>
<option value="0.5">50%</option>
<option value="1.25">125%</option>
<option value="1.5">150%</option>
<option value="2">200%</option>
</select>
<select style="border-radius:10px;margin-left:5px;margin-top: 13px;width:unset!important;" onchange="rotatePage();" class="changeText" id="rotation" title="Which video bitrate target would you prefer?" > <select style="border-radius:10px;margin-left:5px;margin-top: 13px;width:unset!important;" onchange="rotatePage();" class="changeText" id="rotation" title="Which video bitrate target would you prefer?" >
<option value="0" selected>No Rotation</option> <option value="0" selected>No Rotation</option>
<option value="90">90° CW</option> <option value="90">90° CW</option>
@@ -406,6 +413,11 @@ if (urlParams.has("rotate")){
} }
if (urlParams.has("zoom")){
document.querySelector("#zoomLevel").value = urlParams.get("zoom") || 1;
} else if (localStorage.getItem('zoomLevel')){
document.querySelector("#zoomLevel").value = localStorage.getItem('zoomLevel') || 1;
}
if (urlParams.has("flip")){ if (urlParams.has("flip")){
document.querySelector("#transform").value = urlParams.get("flip") || 0; document.querySelector("#transform").value = urlParams.get("flip") || 0;
@@ -677,97 +689,99 @@ async function loadPage(){
document.body.appendChild(iframe); document.body.appendChild(iframe);
} }
function rotatePage(){ function rotatePage(){
var rotate = document.querySelector("#rotation").value || 0;
var rotate = document.querySelector("#rotation").value || 0; var flip = document.querySelector("#transform").value;
var flip = document.querySelector("#transform").value; var zoomLevel = parseFloat(document.querySelector("#zoomLevel").value || 1);
updateURL("rotate="+rotate, true); updateURL("rotate="+rotate, true);
updateURL("flip="+flip, true); updateURL("flip="+flip, true);
updateURL("zoom="+zoomLevel, true);
localStorage.setItem('iframeURL', document.getElementById('iframeURL').value);
localStorage.setItem('rotation', document.getElementById('rotation').value); localStorage.setItem('iframeURL', document.getElementById('iframeURL').value);
//localStorage.setItem('backgroundColor', document.getElementById('backgroundColor').value); localStorage.setItem('rotation', document.getElementById('rotation').value);
localStorage.setItem('transform', document.getElementById('transform').value); localStorage.setItem('transform', document.getElementById('transform').value);
localStorage.setItem('zoomLevel', document.getElementById('zoomLevel').value);
if (rotate==180){
iframe.style.transform = "rotate(180deg)"; // Remove existing iframe if it exists
iframe.style.width = "100vw"; if (iframe.parentNode) {
iframe.style.height = "calc(100vh - "+menuOffset+")"; iframe.parentNode.removeChild(iframe);
iframe.style.transformOrigin = "0 0;"; }
iframe.style.position = "rotate(180deg)";
iframe.style.left = "100vw"; // Create container for iframe if it doesn't exist
iframe.style.top = "calc(100vh)"; var container = document.getElementById('iframe-container');
if (!container) {
if (flip==1){ container = document.createElement('div');
iframe.style.transform += " scaleX(-1)"; container.id = 'iframe-container';
iframe.style.top = "calc(" + (iframe.style.top) +" + )"; container.style.overflow = 'hidden';
iframe.style.left = "calc(" + (iframe.style.left) +" - 100vw)"; container.style.position = 'absolute';
} else if (flip==2){ container.style.top = menuOffset;
iframe.style.transform += " scaleY(-1)"; container.style.left = '0';
iframe.style.top = "calc(" + (iframe.style.top) +" + "+menuOffset+" - 100vh)"; container.style.width = '100vw';
iframe.style.left = "calc(" + (iframe.style.left) +" )"; container.style.height = 'calc(100vh - ' + menuOffset + ')';
} document.body.appendChild(container);
}
} else if (rotate==270){
iframe.style.transform = "rotate(270deg)"; // Reset iframe styles
iframe.style.left = "0"; iframe.style.position = 'absolute';
iframe.style.top = "100vh"; iframe.style.border = 'none';
iframe.style.transformOrigin = "0 0;"; iframe.style.margin = '0';
iframe.style.width = "calc(100vh - "+menuOffset+")"; iframe.style.padding = '0';
iframe.style.height = "100vw";
// Calculate dimensions based on zoom
if (flip==1){ if (rotate == '90' || rotate == '270') {
iframe.style.transform += " scaleX(-1)"; // For rotated views, swap width and height
iframe.style.top = "calc(" + (iframe.style.top) +" + "+menuOffset+" - 100vh)"; iframe.style.width = (100 / zoomLevel) + 'vh';
iframe.style.left = "calc(" + (iframe.style.left) +" )"; iframe.style.height = (100 / zoomLevel) + 'vw';
} else if (flip==2){ } else {
iframe.style.transform += " scaleY(-1)"; iframe.style.width = (100 / zoomLevel) + 'vw';
iframe.style.top = "calc(" + (iframe.style.top) +" )"; iframe.style.height = 'calc(' + (100 / zoomLevel) + 'vh - ' + (parseInt(menuOffset) / zoomLevel) + 'px)';
iframe.style.left = "calc(" + (iframe.style.left) +" + 100vw)"; }
}
// Set transform origin
} else if (rotate==90){ iframe.style.transformOrigin = '0 0';
iframe.style.transform = "rotate(90deg)";
iframe.style.width = "calc(100vh - "+menuOffset+")"; // Apply transformations
iframe.style.height = "100vw"; var transforms = [];
iframe.style.transformOrigin = "0 0;";
iframe.style.left = "calc(100vw"; // Add scale if not 1
iframe.style.top = menuOffset; if (zoomLevel !== 1) {
transforms.push('scale(' + zoomLevel + ')');
if (flip==1){ }
iframe.style.transform += " scaleX(-1)";
iframe.style.top = "calc(" + (iframe.style.top) +" - "+menuOffset+" + 100vh)"; // Add rotation
iframe.style.left = "calc(" + (iframe.style.left) +" )"; if (rotate != '0') {
} else if (flip==2){ transforms.push('rotate(' + rotate + 'deg)');
iframe.style.transform += " scaleY(-1)"; }
iframe.style.top = "calc(" + (iframe.style.top) +" )";
iframe.style.left = "calc(" + (iframe.style.left) +" - 100vw)"; // Add flipping
} if (flip == '1') {
transforms.push('scaleX(-1)');
} else { } else if (flip == '2') {
iframe.style.transform = "rotate(0deg)"; transforms.push('scaleY(-1)');
iframe.style.width = "100vw"; }
iframe.style.height = "calc(100vh - "+menuOffset+")";
iframe.style.transformOrigin = "0 0;"; // Set the transform
iframe.style.position = "rotate(0deg)"; iframe.style.transform = transforms.join(' ');
iframe.style.left = "0";
iframe.style.top = menuOffset; // Position the iframe based on rotation and scale
if (rotate == '180') {
if (flip==1){ iframe.style.top = 'calc(100vh - ' + menuOffset + ')';
iframe.style.transform += " scaleX(-1)"; iframe.style.left = '100vw';
iframe.style.top = "calc(" + (iframe.style.top) +" )"; } else if (rotate == '270') {
iframe.style.left = "calc(" + (iframe.style.left) +" + 100vw)"; iframe.style.top = '100vh';
} else if (flip==2){ iframe.style.left = '0';
iframe.style.transform += " scaleY(-1)"; } else if (rotate == '90') {
iframe.style.top = "calc( 100vh)"; iframe.style.top = menuOffset;
iframe.style.left = '100vw';
//iframe.style.left = "calc(" + (iframe.style.left) +" + 100vw)"; } else {
} iframe.style.top = menuOffset;
} iframe.style.left = '0';
}
// Add the iframe to the container
container.appendChild(iframe);
} }
</script> </script>
</body> </body>
</html> </html>

88
tts.html Normal file
View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta content="utf-8" http-equiv="encoding" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Speech Synthesis Languages</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #333;
color: #ddd;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
padding: 20px;
}
.container {
text-align: center;
max-width: 800px;
}
#languageSelect, table {
margin-top: 20px;
padding: 10px;
font-size: 16px;
background-color: #555;
color: #fff;
border: none;
border-radius: 5px;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #777;
padding: 8px;
text-align: left;
}
th {
background-color: #444;
}
tr:nth-child(even) {
background-color: #555;
}
</style>
</head>
<body>
<div class="container">
<h1>Available TTS Language Options</h1>
<select style='display:none;' id="languageSelect"></select>
<table id="voicesTable">
<tr>
<th>Name</th>
<th>Language</th>
<th>Local Service</th>
<th>Default</th>
</tr>
<!-- Rows will be populated here -->
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const select = document.getElementById('languageSelect');
const table = document.getElementById('voicesTable');
function populateVoices() {
const voices = speechSynthesis.getVoices();
voices.forEach(voice => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
select.appendChild(option);
const row = table.insertRow();
row.insertCell().textContent = voice.name;
row.insertCell().textContent = voice.lang;
row.insertCell().textContent = voice.localService ? 'Yes' : 'No';
row.insertCell().textContent = voice.default ? 'Yes' : 'No';
});
}
speechSynthesis.onvoiceschanged = populateVoices;
});
</script>
</body>
</html>

View File

@@ -705,12 +705,36 @@
</div> </div>
</div> </div>
</div> </div>
<div class="troubleshooting" style="background: rgba(255, 193, 7, 0.2); border-left: 4px solid #ffc107;">
<i class="las la-exclamation-triangle" style="color: #ffc107; font-size: 1.3em;"></i>
<span><strong>Can't publish WHIP via OBS outside your LAN?</strong></span>
Download our patched OBS version:<a href="https://backup.vdo.ninja/OBS_VDO_Ninja.zip" target="_blank">[Windows]</a><a href="https://drive.google.com/file/d/1bDln_cOuAb3wA0fzvXwsY8WX1vZKGEIJ/view?usp=sharing" target="_blank">[macOS]</a><a href='https://github.com/steveseguin/obs-studio/' target="_blank">[source]</a>
</div>
<div class="troubleshooting"> <!-- NEW SETTINGS BOX -->
<i class="las la-question-circle"></i> <div class="info-box" style="background: rgba(106, 171, 35, 0.15); border-radius: 10px; padding: 20px; margin: 20px 0; border-left: 4px solid #6aab23;">
<span>Can't publish WHIP via OBS outside your LAN?</span> <h4 style="color: #6aab23; margin-top: 0; font-size: 1.1em;">📋 Recommended OBS WHIP Settings</h4>
<a href="https://backup.vdo.ninja/OBS_VDO_Ninja.zip" target="_blank">Download our patched OBS version</a> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px;">
<a href='https://github.com/steveseguin/obs-studio/' target="_blank">[source]</a> <div>
<ul style="margin: 0; padding-left: 20px; color: white;">
<li>Rate Control: <strong>CRF</strong></li>
<li>CRF: <strong>23</strong></li>
<li>Keyframe Interval: <strong>1s</strong></li>
<li>Preset: <strong>Veryfast</strong></li>
</ul>
</div>
<div>
<ul style="margin: 0; padding-left: 20px; color: white;">
<li>Profile: <strong>High</strong></li>
<li>Tune: <strong>Fastdecode</strong> (required)</li>
<li>x264 Options: <strong>bframes=0</strong> (required)</li>
</ul>
</div>
</div>
<p style="margin-top: 15px; color: #ddd; font-size: 0.9em;">
⚠️ <strong>Important:</strong> Using <code>&buffer=2500</code> in your view link can help reduce skipped frames at the cost of increased latency.
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -927,6 +951,22 @@ document.querySelector("#changeText1a").value = localStorage.getItem('changeText
document.querySelector("#changeText2").value = localStorage.getItem('changeText2') || ""; document.querySelector("#changeText2").value = localStorage.getItem('changeText2') || "";
const tabHashMap = {
'urlInput1': 'publish',
'urlInput2': 'obs',
'urlInput1a': 'twitch',
'urlInput3': 'play',
'urlInput4': 'host'
};
const hashTabMap = {
'publish': 'urlInput1',
'obs': 'urlInput2',
'twitch': 'urlInput1a',
'play': 'urlInput3',
'host': 'urlInput4'
};
if (localStorage.getItem('changeText3')!==null){ if (localStorage.getItem('changeText3')!==null){
document.getElementById('changeText3').value = localStorage.getItem('changeText3'); document.getElementById('changeText3').value = localStorage.getItem('changeText3');
} }
@@ -1132,7 +1172,7 @@ function gohere1t(){
function gohere2(){ function gohere2(){
if (document.getElementById('changeText2').value){ if (document.getElementById('changeText2').value){
localStorage.setItem('changeText2', document.getElementById('changeText2').value); localStorage.setItem('changeText2', document.getElementById('changeText2').value);
window.location = domain + "?hidemenu&whip=" + document.getElementById('changeText2').value; window.location = domain + "?whip=" + document.getElementById('changeText2').value;
} }
} }
@@ -1177,7 +1217,7 @@ function gohere3(){
addedon += "&whepwait="+document.getElementById('whepicewait').value; addedon += "&whepwait="+document.getElementById('whepicewait').value;
window.location = domain + "?&hidemenu&whepplay=" + encodeURIComponent(document.getElementById('changeText3').value)+addedon; window.location = domain + "?&whepplay=" + encodeURIComponent(document.getElementById('changeText3').value)+addedon;
} }
} }
@@ -1407,8 +1447,18 @@ function switchTab(targetId) {
tab.classList.toggle('active', index === Array.from(document.querySelectorAll('.urlInput')) tab.classList.toggle('active', index === Array.from(document.querySelectorAll('.urlInput'))
.findIndex(section => section.id === targetId)); .findIndex(section => section.id === targetId));
}); });
window.location.hash = tabHashMap[targetId];
} }
function checkHashAndSelectTab() {
const hash = window.location.hash.substring(1);
if (hash && hashTabMap[hash]) {
switchTab(hashTabMap[hash]);
}
}
function toggleAdvanced(section, toggle) { function toggleAdvanced(section, toggle) {
const isHidden = !section.classList.contains('visible'); const isHidden = !section.classList.contains('visible');
section.classList.toggle('visible'); section.classList.toggle('visible');
@@ -1437,7 +1487,12 @@ function generateNewToken() {
} }
// Initialize UI when page loads // Initialize UI when page loads
document.addEventListener('DOMContentLoaded', initializeUI); document.addEventListener('DOMContentLoaded', function() {
initializeUI();
checkHashAndSelectTab();
});
window.addEventListener('hashchange', checkHashAndSelectTab);
</script> </script>
</body> </body>
</html> </html>