From 239a3a6d05f00f54d3d77754ea463cd2b1c51d03 Mon Sep 17 00:00:00 2001 From: Steve Seguin Date: Fri, 9 May 2025 03:01:56 -0400 Subject: [PATCH] Add files via upload --- check.html | 654 ++++++++++++++++- codecs.html | 1769 +++++++++++++++++++++++++++++++++++++++++++++ electron.html | 151 +++- ip.html | 157 ++++ results.html | 437 +++++++---- speedtest.html | 10 +- teleprompter.html | 194 ++--- tts.html | 88 +++ whip.html | 71 +- 9 files changed, 3274 insertions(+), 257 deletions(-) create mode 100644 codecs.html create mode 100644 ip.html create mode 100644 tts.html 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 +
+ +
+
+
+ + +
+
+ +
+
+
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...

+
+
+ +
+

Raw Detection Data

+

+    
+
+ +
+

How to Use Codecs in VDO.Ninja

+ +
+
+

Video Codec Selection (&codec)

+

Control which codec is used to encode and transmit video.

+ +
+
Basic Usage
+
https://vdo.ninja/?view=abc123&codec=h264
+
https://vdo.ninja/?room=xxx7654&scene&bitrate=2000&codec=vp9
+
+ +
+
+
Option
+
Description
+
Use Case
+
+
+
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
+
High efficiency codec, limited browser support
+
+
+
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
+
+
+ +
+
Selection Tips
+
    +
  • 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
  • +
+
+ +
+
Important Notes
+
    +
  • 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.

+ +
+
Basic Usage
+
https://vdo.ninja/?push=abc123&record=2000
+
https://vdo.ninja/?push=xxx7654&record=0
+
+ +
+
+
Option
+
Description
+
Use Case
+
+
+
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
+
+
+ +
+
Recording Notes
+
    +
  • 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.

+ +
+
Basic Usage
+
https://vdo.ninja/?push=abc123&record=2000&recordcodec=h264
+
+ +
+
+
Option
+
Description
+
+
+
h264
+
Record using H.264 codec
+
+
+
vp8
+
Record using VP8 codec (default fallback)
+
+
+
vp9
+
Record using VP9 codec
+
+
+
av1
+
Record using AV1 codec
+
+
+ +
+
Important Notes
+
    +
  • 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 (Scalable Video Coding)
+

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 (WebRTC HTTP ingest Protocol)
+

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 Support
+

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

+ +
+ + +
+ + + +
+ + +
+
+ + + + + + \ 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 += `
+ + `; + + 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 @@ - + + + + + + + + + +
NameLanguageLocal ServiceDefault
+ + + + \ 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:[Windows][macOS][source] +
-
- - 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); \ No newline at end of file