diff --git a/check.html b/check.html
index e8d79d1..0f1683a 100644
--- a/check.html
+++ b/check.html
@@ -186,14 +186,33 @@
document.getElementById("page2").classList.remove("hidden");
}
- function next2(){
- document.getElementById("page2").classList.add("hidden");
- document.getElementById("page2a").classList.remove("hidden");
- setTimeout(function(){
- if (document.getElementById("playButton") && !document.getElementById("playButton").skip){
- next2a();
- }
- },10000);
+ async function runCodecDetection() {
+ try {
+ console.log("Starting advanced codec detection...");
+ const detectedCodecs = await detectCodecs();
+ logged.push({detectedCodecs});
+ console.log("Codec detection complete", detectedCodecs);
+ return detectedCodecs;
+ } 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(){
@@ -544,7 +563,623 @@
+
+ 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="") {
@@ -673,11 +1308,10 @@
var request = new XMLHttpRequest();
request.open('POST', "https://record.vdo.workers.dev/?name="+recordResults);
try {
- logged = JSON.stringify(logged);
+ request.send(JSON.stringify(logged));
} catch(e){
console.error(e);
}
- request.send(logged);
timer = 91;
div.innerHTML = "Test ended";
diff --git a/codecs.html b/codecs.html
new file mode 100644
index 0000000..8b815cb
--- /dev/null
+++ b/codecs.html
@@ -0,0 +1,1769 @@
+
+
+
+
+
+
+ VDO.Ninja Codec Support Detector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Video Codec Support Detector
+
+
+
Codec Detection
+
This tool detects video and audio codecs supported by your browser and identifies hardware acceleration capabilities for both encoding and decoding.
+
+
+ Detection Mode:
+
+
+
+
+ Testing Both Encode/Decode
+
+
+
+
+
Start Comprehensive Detection
+
+
+
+
+
+
Video Codecs
+
Audio Codecs
+
Raw Data
+
Codec Usage Guide
+
+
+
+
Video Codec Support
+
+
Click "Start Comprehensive Detection" to begin...
+
+
+
+
+
Audio Codec Support
+
+
Click "Start Comprehensive Detection" to begin...
+
+
+
+
+
+
+
+
How to Use Codecs in VDO.Ninja
+
+
+
+
Video Codec Selection (&codec)
+
Control which codec is used to encode and transmit video.
+
+
+
+
https://vdo.ninja/?view=abc123&codec=h264
+
https://vdo.ninja/?room=xxx7654&scene&bitrate=2000&codec=vp9
+
+
+
+
+
+
h264
+
Request H.264 codec
+
Better battery life on mobile devices, hardware acceleration on many devices
+
+
+
vp8
+
Request VP8 codec
+
Default codec, works on most devices
+
+
+
vp9
+
Request VP9 codec
+
Better compression, cleaner image for screen sharing
+
+
+
av1
+
Request AV1 codec
+
Most advanced compression, requires Chrome v90+
+
+
+
h265
+
Request H.265/HEVC codec
+
+
+
+
webp
+
Request WebP codec
+
Alternative image-based codec
+
+
+
hardware
+
Android-specific option for hardware encoding
+
Android devices struggling with video quality
+
+
+
av1,h264
+
Comma-separated fallback options
+
Try AV1 first, fall back to H264 if not supported
+
+
+
+
+
+
+ H.264: Hardware accelerated on many devices, good for mobile battery life
+ VP8: Default choice, software-encoded but compatible with most devices
+ VP9: Better quality at same bitrate vs VP8, but more CPU intensive
+ AV1: Best compression ratio but highest CPU usage, limited device support
+ H.265: Limited browser support, needs command-line flags in Chrome
+
+
+
+
+
+
+ The &codec parameter is added to the viewer-side (use with &view or &scene)
+ Hardware encoding capabilities vary by device, OS, and browser.
+ H.264 hardware encoding might use less battery but sometimes has compatibility issues
+ AV1 and VP9 tends to look better for screen sharing but uses more CPU
+ AV1 tends to offer more accurate colours; useful for chroma green screening
+ Use the detection tool above to see which codecs have hardware acceleration on your device
+
+
+
+
+
+
Recording Options (&record)
+
Configure how video and audio are recorded to disk.
+
+
+
+
https://vdo.ninja/?push=abc123&record=2000
+
https://vdo.ninja/?push=xxx7654&record=0
+
+
+
+
+
+
0
+
No video, audio recorded as 32bit PCM lossless
+
High-quality audio only recording
+
+
+
-120 (negative)
+
No video, audio at specified kbps (OPUS)
+
Audio-only recording with specific bitrate
+
+
+
2000 (positive)
+
Video bitrate in kbps
+
Video recording with specific quality setting
+
+
+
false or off
+
Disable recording feature
+
Prevents user from recording
+
+
+
+
+
+
+ Recorded file format is WebM with VP8/H264 video and OPUS/PCM audio
+ Default bitrate is approximately 4000 kbps if not specified
+ The director of a room will be notified when a user is recording
+ The director can trigger recording remotely
+ Video/audio is saved in real-time to the local download folder
+ Recording should be stopped manually before closing the browser
+
+
+
+
+
+
Recording Codec Selection (&recordcodec)
+
Set the specific codec used when recording to disk.
+
+
+
+
https://vdo.ninja/?push=abc123&record=2000&recordcodec=h264
+
+
+
+
+
+
h264
+
Record using H.264 codec
+
+
+
vp8
+
Record using VP8 codec (default fallback)
+
+
+
vp9
+
Record using VP9 codec
+
+
+
av1
+
Record using AV1 codec
+
+
+
+
+
+
+ The container format is always WebM regardless of codec, unless using Safari, and maybe then its MP4
+ If a codec is not supported, it will fall back to VP8
+ Especially useful for Chrome on Android where VP8 performance can be poor
+ Remember to add &record to enable the recording function
+ Use &rc as a shorter alias for &recordcodec
+
+
+
+
+
+
Advanced WebRTC Features
+
WebRTC supports additional features like SVC (Scalable Video Coding) and WHiP.
+
+
+
+
SVC allows a single bitstream to have multiple resolutions, frame rates, or quality layers.
+
+ Improves streaming adaptability for viewers with different connection qualities
+ Support varies by browser and codec
+ Common modes include L1T1 (single layer), L2T1 (2 spatial layers), S2T1 (2 temporal layers)
+ The detection tool above shows supported SVC modes for your browser
+
+
+
+
+
+
WHiP is a standardized protocol for sending WebRTC streams to a server.
+
+ Enables WebRTC streaming to CDNs and streaming platforms
+ Simpler to implement than custom signaling servers
+ Useful for broadcast scenarios with many viewers
+ Check your streaming platform documentation for WHiP support details
+
+
+
+
+
H.265 (HEVC) offers excellent compression efficiency but has limited browser support. Visit our H.265 guide for detailed instructions on:
+
+ Enabling H.265 in Chrome using command-line flags
+ Browser compatibility information
+ Fallback strategies using comma-separated codec preferences
+ Testing your browser's H.265 support
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/electron.html b/electron.html
index 8c1fffd..489364b 100644
--- a/electron.html
+++ b/electron.html
@@ -278,7 +278,36 @@
position: absolute;
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;
+ }
+}
@@ -321,28 +350,96 @@
* 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(){
document.querySelector("#changeText").value = document.querySelector("#lastUrls").value;
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(){
localStorage.clear();
document.querySelector('#lastUrls').innerHTML = '';
@@ -526,7 +623,6 @@ function createSpecialDeviceLink(deviceLabel) {
link.style.transition = 'opacity 2s ease-in-out';
link.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
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.";
document.body.appendChild(link);
@@ -656,7 +752,7 @@ function addUrlToHistory(url){
}
if ( lastUrls[0] != url ) {
lastUrls.unshift(url);
- if (lastUrls.length == 6) {
+ if (lastUrls.length == 100) {
lastUrls.pop();
}
}
@@ -696,6 +792,25 @@ function gohere(){
}
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();
diff --git a/ip.html b/ip.html
new file mode 100644
index 0000000..24c9cda
--- /dev/null
+++ b/ip.html
@@ -0,0 +1,157 @@
+
+
+
+
+
+ IP Address Decoder
+
+
+
+
+
IP Address Decoder
+
+
+ Obfuscated Username:
+
+
+
+
Decode IP Address
+
+
+ Decoded IP Address:
+
+
+
+
+
+ IP Address Decoder Tool
+
+
+
+
+
\ No newline at end of file
diff --git a/results.html b/results.html
index 2764de7..490d319 100644
--- a/results.html
+++ b/results.html
@@ -1,63 +1,202 @@
-
+
+
+
VDON Speed Test
@@ -233,36 +372,47 @@
var PAK = 0;
var PAKCCC = 0;
+ var packetLossSpikes = 0;
+
function process(arr) {
- var maxResolution = "0 x 0";
- console.log(arr);
- arr.forEach(data=>{
- if ("bitrate" in data){
- updateData("bitrate", data.bitrate);
- if (data.bitrate!==null){
- BBB += data.bitrate;
- counter += 1;
+ var maxResolution = "0 x 0";
+ console.log(arr);
+
+ // Clear container
+ document.getElementById("container").innerHTML = "Quality Test Results ";
+
+ arr.forEach(data => {
+ 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 ("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 += "Test start time: "+timeConverter(data.timestart)+" ";
+
+ if (data.timestart) {
+ document.getElementById("details").innerHTML += "Test start time: " + timeConverter(data.timestart) + " ";
}
if ("peakhour" in data){
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 += "Supported video codecs: " + videoCodecs + " ";
+ }
+ if (audioCodecs) {
+ document.getElementById("details").innerHTML += "Supported audio codecs: " + audioCodecs + " ";
+ }
+
+ // Check for optimal codecs
+ if (data.codecs.video.h264 || data.codecs.video.vp9) {
+ document.getElementById("details").innerHTML += "(Good codec support for streaming) ";
+ } else if (!data.codecs.video.h264) {
+ document.getElementById("details").innerHTML += "(H.264 codec support not detected, which may limit compatibility) ";
+ }
+ }
+
+ if (data.userRegion) {
+ document.getElementById("details").innerHTML += "User region: " + data.userRegion + " ";
+ }
+
if (("hostMode" in data) && ("srflxMode" in data)){
if (!data.hostMode && !data.srflxMode){
@@ -355,66 +528,72 @@
}
- var total = QLR_1 + QLR_2 + QLR_3;
- if (QLR_2/total>0.5){
- document.getElementById("container").innerHTML += "Serious CPU overload issues. Consider reducing the capture resolution. ";
- } else if (QLR_2/total>0.1){
- document.getElementById("container").innerHTML += "Occassional CPU overload issues. Consider reducing the capture resolution. ";
- }
- if (QLR_3/total>0.5){
- document.getElementById("container").innerHTML += "The network quality or bandwidth limited the performance. ";
- } else if (QLR_3/total>0.1){
- document.getElementById("container").innerHTML += "The network quality or bandwidth may have limited the performance. ";
- }
-
- document.getElementById("container").innerHTML += "The average video bitrate was: "+parseInt(BBB/counter)+"-kbps ";
-
- if (BBB/counter<500){
- document.getElementById("container").innerHTML += "Did they select an active camera? Bitrate is really bad ";
- }
- else if (BBB/counter<1000){
+ // Apply styling to the results messages
+ var total = QLR_1 + QLR_2 + QLR_3;
+ if (QLR_2/total > 0.5) {
+ document.getElementById("container").innerHTML += "Serious CPU overload issues. Consider reducing the capture resolution.
";
+ } else if (QLR_2/total > 0.1) {
+ document.getElementById("container").innerHTML += "Occasional CPU overload issues. Consider reducing the capture resolution.
";
+ }
+
+ if (QLR_3/total > 0.5) {
+ document.getElementById("container").innerHTML += "The network quality or bandwidth limited the performance.
";
+ } else if (QLR_3/total > 0.1) {
+ document.getElementById("container").innerHTML += "The network quality or bandwidth may have limited the performance.
";
+ }
+
+ document.getElementById("container").innerHTML += "Average video bitrate: " + parseInt(BBB/counter) + "-kbps
";
+
+ if (BBB/counter < 500) {
+ document.getElementById("container").innerHTML += "Did they select an active camera? Bitrate is really bad ";
+ } else if (BBB/counter < 1000) {
document.getElementById("container").innerHTML += "Bitrate is poor ";
- }
- else if (BBB/counter<2000){
+ } else if (BBB/counter < 2000) {
document.getElementById("container").innerHTML += "Bitrate a bit low ";
- }
- else {
- document.getElementById("container").innerHTML += "Bitrate is good ";
- }
-
- document.getElementById("container").innerHTML += " The average video buffer length was: "+parseInt(BUFF/BUFFCCC)+"-ms ";
-
- if (BUFF/BUFFCCC>500){
- document.getElementById("container").innerHTML += "Video delay is really bad ";
- }
- else if (BUFF/BUFFCCC>200){
- document.getElementById("container").innerHTML += "Video delay is poor ";
- }
- else if (BUFF/BUFFCCC>100){
- document.getElementById("container").innerHTML += "Video delay is sub-optimal ";
- }
- else {
- document.getElementById("container").innerHTML += "Video delay is good ";
- }
-
- document.getElementById("container").innerHTML += "The average video packet loss was: "+(parseInt(PAK*1000/PAKCCC)/1000.0)+"% ";
-
- if (PAK/PAKCCC>3){
- document.getElementById("container").innerHTML += "Packet loss is extremely bad; Must Fix This ";
- }
- else if (PAK/PAKCCC>0.8){
- document.getElementById("container").innerHTML += "Packet loss is quite bad; expect problems with audio and video ";
- }
- else if (PAK/PAKCCC>.15){
+ } else {
+ document.getElementById("container").innerHTML += "Bitrate is good ";
+ }
+
+ document.getElementById("container").innerHTML += "Average video buffer length: " + parseInt(BUFF/BUFFCCC) + "-ms
";
+
+ if (BUFF/BUFFCCC > 500) {
+ document.getElementById("container").innerHTML += "Video delay is really bad ";
+ } else if (BUFF/BUFFCCC > 200) {
+ document.getElementById("container").innerHTML += "Video delay is poor ";
+ } else if (BUFF/BUFFCCC > 100) {
+ document.getElementById("container").innerHTML += "Video delay is sub-optimal ";
+ } else {
+ document.getElementById("container").innerHTML += "Video delay is good ";
+ }
+
+ document.getElementById("container").innerHTML += "Average video packet loss: " + (parseInt(PAK*1000/PAKCCC)/1000.0) + "%
";
+
+ if (PAK/PAKCCC > 3) {
+ document.getElementById("container").innerHTML += "Packet loss is extremely bad ; Must Fix This ";
+ } else if (PAK/PAKCCC > 0.8) {
+ document.getElementById("container").innerHTML += "Packet loss is quite bad ; expect problems with audio and video ";
+ } else if (PAK/PAKCCC > 0.15) {
document.getElementById("container").innerHTML += "Packet loss is a bit high; might be a testing-server issue though ";
- }
- else {
- document.getElementById("container").innerHTML += "Packet loss is good ";
+ } else {
+ document.getElementById("container").innerHTML += "Packet loss is good ";
+ }
+
+ if (packetLossSpikes > 0) {
+ document.getElementById("details").innerHTML += "Packet loss spikes detected: " + packetLossSpikes +
+ " occurrences of packet loss > 2%(Spikes in packet loss can cause video freezes or audio dropouts even if average packet loss is low)
";
+ }
+
+ // Render the codec details button (original code)
+ if (arr.some(data => data.codecs || data.detectedCodecs)) {
+ document.getElementById("details").innerHTML += `
+ View Codec Support Details
+ `;
+
+ window.viewCodecDetails = function(resultsId) {
+ window.open(`./codecs.html?results=${resultsId}`, '_blank');
+ };
+ }
}
-
- console.log(QLR_1, QLR_2, QLR_3);
-
- }
var xmlhttp = new XMLHttpRequest();
var url = "https://record.vdo.workers.dev/?name="+streamID;
diff --git a/speedtest.html b/speedtest.html
index e42ec29..af02711 100644
--- a/speedtest.html
+++ b/speedtest.html
@@ -1,8 +1,14 @@
+
+
+
+
+
+
VDON Speed Test
@@ -230,7 +236,7 @@
iframe.allowfullscreen ="true";
//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")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
@@ -274,7 +280,7 @@
iframe.allow = "autoplay";
// 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")) {
srcString = srcString + "&turn=" + urlParams.get("turn");
diff --git a/teleprompter.html b/teleprompter.html
index fd1d6d8..f17e201 100644
--- a/teleprompter.html
+++ b/teleprompter.html
@@ -293,7 +293,14 @@
Website URL
Twitch
-
+
+ 100% (Normal)
+ 75%
+ 50%
+ 125%
+ 150%
+ 200%
+
No Rotation
âŠī¸90° CW
@@ -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")){
document.querySelector("#transform").value = urlParams.get("flip") || 0;
@@ -677,97 +689,99 @@ async function loadPage(){
document.body.appendChild(iframe);
}
-
function rotatePage(){
-
- var rotate = document.querySelector("#rotation").value || 0;
- var flip = document.querySelector("#transform").value;
-
- updateURL("rotate="+rotate, true);
- updateURL("flip="+flip, true);
-
- localStorage.setItem('iframeURL', document.getElementById('iframeURL').value);
- localStorage.setItem('rotation', document.getElementById('rotation').value);
- //localStorage.setItem('backgroundColor', document.getElementById('backgroundColor').value);
- localStorage.setItem('transform', document.getElementById('transform').value);
-
- if (rotate==180){
- iframe.style.transform = "rotate(180deg)";
- iframe.style.width = "100vw";
- iframe.style.height = "calc(100vh - "+menuOffset+")";
- iframe.style.transformOrigin = "0 0;";
- iframe.style.position = "rotate(180deg)";
- iframe.style.left = "100vw";
- iframe.style.top = "calc(100vh)";
-
- if (flip==1){
- iframe.style.transform += " scaleX(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" + )";
- iframe.style.left = "calc(" + (iframe.style.left) +" - 100vw)";
- } else if (flip==2){
- iframe.style.transform += " scaleY(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" + "+menuOffset+" - 100vh)";
- iframe.style.left = "calc(" + (iframe.style.left) +" )";
- }
-
- } else if (rotate==270){
- iframe.style.transform = "rotate(270deg)";
- iframe.style.left = "0";
- iframe.style.top = "100vh";
- iframe.style.transformOrigin = "0 0;";
- iframe.style.width = "calc(100vh - "+menuOffset+")";
- iframe.style.height = "100vw";
-
- if (flip==1){
- iframe.style.transform += " scaleX(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" + "+menuOffset+" - 100vh)";
- iframe.style.left = "calc(" + (iframe.style.left) +" )";
- } else if (flip==2){
- iframe.style.transform += " scaleY(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" )";
- iframe.style.left = "calc(" + (iframe.style.left) +" + 100vw)";
- }
-
- } else if (rotate==90){
- iframe.style.transform = "rotate(90deg)";
- iframe.style.width = "calc(100vh - "+menuOffset+")";
- iframe.style.height = "100vw";
- iframe.style.transformOrigin = "0 0;";
- iframe.style.left = "calc(100vw";
- iframe.style.top = menuOffset;
-
- if (flip==1){
- iframe.style.transform += " scaleX(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" - "+menuOffset+" + 100vh)";
- iframe.style.left = "calc(" + (iframe.style.left) +" )";
- } else if (flip==2){
- iframe.style.transform += " scaleY(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" )";
- iframe.style.left = "calc(" + (iframe.style.left) +" - 100vw)";
- }
-
- } else {
- iframe.style.transform = "rotate(0deg)";
- iframe.style.width = "100vw";
- iframe.style.height = "calc(100vh - "+menuOffset+")";
- iframe.style.transformOrigin = "0 0;";
- iframe.style.position = "rotate(0deg)";
- iframe.style.left = "0";
- iframe.style.top = menuOffset;
-
- if (flip==1){
- iframe.style.transform += " scaleX(-1)";
- iframe.style.top = "calc(" + (iframe.style.top) +" )";
- iframe.style.left = "calc(" + (iframe.style.left) +" + 100vw)";
- } else if (flip==2){
- iframe.style.transform += " scaleY(-1)";
- iframe.style.top = "calc( 100vh)";
-
- //iframe.style.left = "calc(" + (iframe.style.left) +" + 100vw)";
- }
- }
+ var rotate = document.querySelector("#rotation").value || 0;
+ var flip = document.querySelector("#transform").value;
+ var zoomLevel = parseFloat(document.querySelector("#zoomLevel").value || 1);
+
+ updateURL("rotate="+rotate, 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('transform', document.getElementById('transform').value);
+ localStorage.setItem('zoomLevel', document.getElementById('zoomLevel').value);
+
+ // Remove existing iframe if it exists
+ if (iframe.parentNode) {
+ iframe.parentNode.removeChild(iframe);
+ }
+
+ // Create container for iframe if it doesn't exist
+ var container = document.getElementById('iframe-container');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'iframe-container';
+ container.style.overflow = 'hidden';
+ container.style.position = 'absolute';
+ container.style.top = menuOffset;
+ container.style.left = '0';
+ container.style.width = '100vw';
+ container.style.height = 'calc(100vh - ' + menuOffset + ')';
+ document.body.appendChild(container);
+ }
+
+ // Reset iframe styles
+ iframe.style.position = 'absolute';
+ iframe.style.border = 'none';
+ iframe.style.margin = '0';
+ iframe.style.padding = '0';
+
+ // Calculate dimensions based on zoom
+ if (rotate == '90' || rotate == '270') {
+ // For rotated views, swap width and height
+ iframe.style.width = (100 / zoomLevel) + 'vh';
+ iframe.style.height = (100 / zoomLevel) + 'vw';
+ } else {
+ iframe.style.width = (100 / zoomLevel) + 'vw';
+ iframe.style.height = 'calc(' + (100 / zoomLevel) + 'vh - ' + (parseInt(menuOffset) / zoomLevel) + 'px)';
+ }
+
+ // Set transform origin
+ iframe.style.transformOrigin = '0 0';
+
+ // Apply transformations
+ var transforms = [];
+
+ // Add scale if not 1
+ if (zoomLevel !== 1) {
+ transforms.push('scale(' + zoomLevel + ')');
+ }
+
+ // Add rotation
+ if (rotate != '0') {
+ transforms.push('rotate(' + rotate + 'deg)');
+ }
+
+ // Add flipping
+ if (flip == '1') {
+ transforms.push('scaleX(-1)');
+ } else if (flip == '2') {
+ transforms.push('scaleY(-1)');
+ }
+
+ // Set the transform
+ iframe.style.transform = transforms.join(' ');
+
+ // Position the iframe based on rotation and scale
+ if (rotate == '180') {
+ iframe.style.top = 'calc(100vh - ' + menuOffset + ')';
+ iframe.style.left = '100vw';
+ } else if (rotate == '270') {
+ iframe.style.top = '100vh';
+ iframe.style.left = '0';
+ } else if (rotate == '90') {
+ iframe.style.top = menuOffset;
+ iframe.style.left = '100vw';
+ } else {
+ iframe.style.top = menuOffset;
+ iframe.style.left = '0';
+ }
+
+ // Add the iframe to the container
+ container.appendChild(iframe);
}
-
\ No newline at end of file
diff --git a/tts.html b/tts.html
new file mode 100644
index 0000000..1b9df20
--- /dev/null
+++ b/tts.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+ Speech Synthesis Languages
+
+
+
+
+
Available TTS Language Options
+
+
+
+ Name
+ Language
+ Local Service
+ Default
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/whip.html b/whip.html
index 4ea3e7a..9618f66 100644
--- a/whip.html
+++ b/whip.html
@@ -705,12 +705,36 @@
+
+
-
-
-
Can't publish WHIP via OBS outside your LAN?
-
Download our patched OBS version
-
[source]
+
+
+
đ Recommended OBS WHIP Settings
+
+
+
+ Rate Control: CRF
+ CRF: 23
+ Keyframe Interval: 1s
+ Preset: Veryfast
+
+
+
+
+ Profile: High
+ Tune: Fastdecode (required)
+ x264 Options: bframes=0 (required)
+
+
+
+
+ â ī¸ Important: Using &buffer=2500 in your view link can help reduce skipped frames at the cost of increased latency.
+
@@ -927,6 +951,22 @@ document.querySelector("#changeText1a").value = localStorage.getItem('changeText
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){
document.getElementById('changeText3').value = localStorage.getItem('changeText3');
}
@@ -1132,7 +1172,7 @@ function gohere1t(){
function gohere2(){
if (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;
- 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'))
.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) {
const isHidden = !section.classList.contains('visible');
section.classList.toggle('visible');
@@ -1437,7 +1487,12 @@ function generateNewToken() {
}
// Initialize UI when page loads
-document.addEventListener('DOMContentLoaded', initializeUI);
+document.addEventListener('DOMContentLoaded', function() {
+ initializeUI();
+ checkHashAndSelectTab();
+});
+
+window.addEventListener('hashchange', checkHashAndSelectTab);