mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
1781 lines
59 KiB
HTML
1781 lines
59 KiB
HTML
<!-- codecs.html !-->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>VDO.Ninja Codec Support Detector</title>
|
|
<meta id="metaTitle" name="title" content="VDO.Ninja Codec Support Detector">
|
|
<meta name="description" content="Check browser codec support for VDO.Ninja. Detect hardware acceleration for video encoding and decoding.">
|
|
<meta name="author" content="Steve Seguin">
|
|
<meta name="copyright" content="© 2024 Steve Seguin">
|
|
<meta name="license" content="https://github.com/steveseguin/vdo.ninja/LICENSE.md">
|
|
<meta name="sourcecode" content="https://github.com/steveseguin/vdo.ninja">
|
|
<meta name="robots" content="index, follow">
|
|
<link rel="canonical" href="https://vdo.ninja/codec">
|
|
<link rel="author" href="/about">
|
|
|
|
<meta name="msapplication-TileColor" content="#da532c">
|
|
<meta name="theme-color" content="#0f131d">
|
|
|
|
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon">
|
|
<link id="favicon1" rel="icon" type="image/png" sizes="32x32" href="./media/favicon-32x32.png">
|
|
<link id="favicon2" rel="icon" type="image/png" sizes="16x16" href="./media/favicon-16x16.png">
|
|
<link id="favicon3" rel="icon" href="./media/favicon.ico">
|
|
|
|
<!-- X (Twitter) Card Tags -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:creator" content="@SteveSeguin">
|
|
<meta name="twitter:title" content="VDO.Ninja Codec Support Detector">
|
|
<meta name="twitter:description" content="Check browser codec support for VDO.Ninja. Detect hardware acceleration for video encoding and decoding.">
|
|
<meta name="twitter:image" content="https://vdo.ninja/media/vdoNinja_logo_full.png">
|
|
|
|
<!-- Open Graph Tags -->
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://vdo.ninja/codec">
|
|
<meta property="og:title" content="VDO.Ninja Codec Support Detector">
|
|
<meta property="og:description" content="Check browser codec support for VDO.Ninja. Detect hardware acceleration for video encoding and decoding.">
|
|
<meta property="og:image" content="https://vdo.ninja/media/vdoNinja_logo_full.png">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:site_name" content="VDO.Ninja">
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
line-height: 1.6;
|
|
color: #e0e0e0;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background-color: #121212;
|
|
}
|
|
|
|
h1, h2, h3 {
|
|
color: #4296f5;
|
|
}
|
|
|
|
a {
|
|
color: #4296f5;
|
|
}
|
|
.container {
|
|
background-color: #1e1e1e;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.codec-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.codec-item {
|
|
background-color: #2a2a2a;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
border-left: 4px solid #4296f5;
|
|
position: relative;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
margin-right: 5px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.badge-hw {
|
|
background-color: #34a853;
|
|
color: white;
|
|
}
|
|
|
|
.badge-sw {
|
|
background-color: #fbbc04;
|
|
color: white;
|
|
}
|
|
|
|
.badge-recorder {
|
|
background-color: #4296f5;
|
|
color: white;
|
|
}
|
|
|
|
.badge-webcodec {
|
|
background-color: #ea4335;
|
|
color: white;
|
|
}
|
|
|
|
.badge-webrtc {
|
|
background-color: #8f44eb;
|
|
color: white;
|
|
}
|
|
|
|
.badge-mediacapabilities {
|
|
background-color: #ff6d01;
|
|
color: white;
|
|
}
|
|
|
|
.badge-encoder {
|
|
background-color: #01c0ff;
|
|
color: white;
|
|
}
|
|
|
|
.badge-decoder {
|
|
background-color: #ff01c0;
|
|
color: white;
|
|
}
|
|
|
|
.codec-name {
|
|
font-weight: bold;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.details {
|
|
font-size: 14px;
|
|
color: #aaa;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.status-bar {
|
|
height: 4px;
|
|
width: 100%;
|
|
background-color: #333;
|
|
position: relative;
|
|
margin-bottom: 20px;
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.status-progress {
|
|
position: absolute;
|
|
height: 100%;
|
|
background-color: #4296f5;
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px solid #444;
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
cursor: pointer;
|
|
border-bottom: 3px solid transparent;
|
|
color: #aaa;
|
|
}
|
|
|
|
.tab.active {
|
|
border-bottom: 3px solid #4296f5;
|
|
font-weight: bold;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
button {
|
|
background-color: #4296f5;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #2979e2;
|
|
}
|
|
|
|
button:disabled {
|
|
background-color: #555;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
#error-message {
|
|
color: #ff5252;
|
|
font-weight: bold;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.toggle-container {
|
|
display: flex;
|
|
margin: 15px 0;
|
|
align-items: center;
|
|
}
|
|
|
|
.toggle-label {
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 60px;
|
|
height: 28px;
|
|
}
|
|
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #333;
|
|
transition: .4s;
|
|
border-radius: 34px;
|
|
}
|
|
|
|
.toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 20px;
|
|
width: 20px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background-color: #fff;
|
|
transition: .4s;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
input:checked + .toggle-slider {
|
|
background-color: #4296f5;
|
|
}
|
|
|
|
input:checked + .toggle-slider:before {
|
|
transform: translateX(32px);
|
|
}
|
|
|
|
.toggle-text {
|
|
margin-left: 10px;
|
|
}
|
|
|
|
pre {
|
|
background: #2a2a2a;
|
|
padding: 15px;
|
|
overflow: auto;
|
|
max-height: 400px;
|
|
color: #e0e0e0;
|
|
border-radius: 4px;
|
|
}
|
|
.codec-guide-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 30px;
|
|
}
|
|
|
|
.codec-section {
|
|
background-color: #2a2a2a;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.codec-section h4 {
|
|
color: #4296f5;
|
|
margin-top: 0;
|
|
margin-bottom: 10px;
|
|
border-bottom: 1px solid #444;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.usage-block, .tips-block, .note-block, .feature-block {
|
|
background-color: #333;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.usage-header, .tip-header, .note-header, .feature-header {
|
|
font-weight: bold;
|
|
color: #4296f5;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.usage-example {
|
|
font-family: monospace;
|
|
background-color: #222;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
margin-bottom: 8px;
|
|
white-space: nowrap;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.options-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.option-row {
|
|
display: flex;
|
|
border-bottom: 1px solid #444;
|
|
}
|
|
|
|
.option-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.header-row {
|
|
background-color: #333;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.option-cell {
|
|
padding: 10px;
|
|
flex: 1;
|
|
}
|
|
|
|
.option-cell:first-child {
|
|
flex: 0 0 120px;
|
|
}
|
|
|
|
.option-cell code {
|
|
background-color: #222;
|
|
padding: 2px 5px;
|
|
border-radius: 3px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.tips-list, .note-list, .feature-list {
|
|
margin: 0;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.tips-list li, .note-list li, .feature-list li {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.option-row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.option-cell {
|
|
padding: 8px;
|
|
}
|
|
|
|
.option-cell:first-child {
|
|
flex: 1;
|
|
background-color: #333;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.header-row {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Video Codec Support Detector</h1>
|
|
|
|
<div class="container">
|
|
<h2>Codec Detection</h2>
|
|
<p>This tool detects video and audio codecs supported by your browser and identifies hardware acceleration capabilities for both encoding and decoding.</p>
|
|
|
|
<div class="toggle-container">
|
|
<span class="toggle-label">Detection Mode:</span>
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" id="toggle-mode" checked>
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<span class="toggle-text" id="mode-text">Testing Both Encode/Decode</span>
|
|
</div>
|
|
|
|
<div class="status-bar">
|
|
<div id="status-progress" class="status-progress" style="width: 0%"></div>
|
|
</div>
|
|
|
|
<button id="start-detection">Start Comprehensive Detection</button>
|
|
<div id="error-message"></div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="tabs">
|
|
<div class="tab active" data-tab="video-codecs">Video Codecs</div>
|
|
<div class="tab" data-tab="audio-codecs">Audio Codecs</div>
|
|
<div class="tab" data-tab="raw-data">Raw Data</div>
|
|
<div class="tab" data-tab="codec-usage">Codec Usage Guide</div>
|
|
</div>
|
|
|
|
<div id="video-codecs" class="tab-content active">
|
|
<h3>Video Codec Support</h3>
|
|
<div id="video-codec-list" class="codec-list">
|
|
<p>Click "Start Comprehensive Detection" to begin...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="audio-codecs" class="tab-content">
|
|
<h3>Audio Codec Support</h3>
|
|
<div id="audio-codec-list" class="codec-list">
|
|
<p>Click "Start Comprehensive Detection" to begin...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="raw-data" class="tab-content">
|
|
<h3>Raw Detection Data</h3>
|
|
<pre id="output" style="background: #2a2a2a; padding: 15px; overflow: auto; max-height: 400px;"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="codec-usage" class="tab-content">
|
|
<h3>How to Use Codecs in VDO.Ninja</h3>
|
|
|
|
<div class="codec-guide-container">
|
|
<div class="codec-section">
|
|
<h4>Video Codec Selection (&codec)</h4>
|
|
<p>Control which codec is used to encode and transmit video.</p>
|
|
|
|
<div class="usage-block">
|
|
<div class="usage-header">Basic Usage</div>
|
|
<div class="usage-example">https://vdo.ninja/?view=abc123&codec=h264</div>
|
|
<div class="usage-example">https://vdo.ninja/?room=xxx7654&scene&bitrate=2000&codec=vp9</div>
|
|
</div>
|
|
|
|
<div class="options-table">
|
|
<div class="option-row header-row">
|
|
<div class="option-cell">Option</div>
|
|
<div class="option-cell">Description</div>
|
|
<div class="option-cell">Use Case</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>h264</code></div>
|
|
<div class="option-cell">Request H.264 codec</div>
|
|
<div class="option-cell">Better battery life on mobile devices, hardware acceleration on many devices</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>vp8</code></div>
|
|
<div class="option-cell">Request VP8 codec</div>
|
|
<div class="option-cell">Default codec, works on most devices</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>vp9</code></div>
|
|
<div class="option-cell">Request VP9 codec</div>
|
|
<div class="option-cell">Better compression, cleaner image for screen sharing</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>av1</code></div>
|
|
<div class="option-cell">Request AV1 codec</div>
|
|
<div class="option-cell">Most advanced compression, requires Chrome v90+</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>h265</code></div>
|
|
<div class="option-cell">Request H.265/HEVC codec</div>
|
|
<div class="option-cell">High efficiency codec, <a href="https://vdo.ninja/h265" target="_blank">limited browser support</a></div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>webp</code></div>
|
|
<div class="option-cell">Request WebP codec</div>
|
|
<div class="option-cell">Alternative image-based codec</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>hardware</code></div>
|
|
<div class="option-cell">Android-specific option for hardware encoding</div>
|
|
<div class="option-cell">Android devices struggling with video quality</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>av1,h264</code></div>
|
|
<div class="option-cell">Comma-separated fallback options</div>
|
|
<div class="option-cell">Try AV1 first, fall back to H264 if not supported</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tips-block">
|
|
<div class="tip-header">Selection Tips</div>
|
|
<ul class="tips-list">
|
|
<li><strong>H.264:</strong> Hardware accelerated on many devices, good for mobile battery life</li>
|
|
<li><strong>VP8:</strong> Default choice, software-encoded but compatible with most devices</li>
|
|
<li><strong>VP9:</strong> Better quality at same bitrate vs VP8, but more CPU intensive</li>
|
|
<li><strong>AV1:</strong> Best compression ratio but highest CPU usage, limited device support</li>
|
|
<li><strong>H.265:</strong> Limited browser support, needs <a href="https://vdo.ninja/h265" target="_blank">command-line flags in Chrome</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="note-block">
|
|
<div class="note-header">Important Notes</div>
|
|
<ul class="note-list">
|
|
<li>The <code>&codec</code> parameter is added to the viewer-side (use with <code>&view</code> or <code>&scene</code>)</li>
|
|
<li>Hardware encoding capabilities vary by device, OS, and browser.</li>
|
|
<li>H.264 hardware encoding might use less battery but sometimes has compatibility issues</li>
|
|
<li>AV1 and VP9 tends to look better for screen sharing but uses more CPU</li>
|
|
<li>AV1 tends to offer more accurate colours; useful for chroma green screening</li>
|
|
<li>Use the detection tool above to see which codecs have hardware acceleration on your device</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="codec-section">
|
|
<h4>Recording Options (&record)</h4>
|
|
<p>Configure how video and audio are recorded to disk.</p>
|
|
|
|
<div class="usage-block">
|
|
<div class="usage-header">Basic Usage</div>
|
|
<div class="usage-example">https://vdo.ninja/?push=abc123&record=2000</div>
|
|
<div class="usage-example">https://vdo.ninja/?push=xxx7654&record=0</div>
|
|
</div>
|
|
|
|
<div class="options-table">
|
|
<div class="option-row header-row">
|
|
<div class="option-cell">Option</div>
|
|
<div class="option-cell">Description</div>
|
|
<div class="option-cell">Use Case</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>0</code></div>
|
|
<div class="option-cell">No video, audio recorded as 32bit PCM lossless</div>
|
|
<div class="option-cell">High-quality audio only recording</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>-120</code> (negative)</div>
|
|
<div class="option-cell">No video, audio at specified kbps (OPUS)</div>
|
|
<div class="option-cell">Audio-only recording with specific bitrate</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>2000</code> (positive)</div>
|
|
<div class="option-cell">Video bitrate in kbps</div>
|
|
<div class="option-cell">Video recording with specific quality setting</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>false</code> or <code>off</code></div>
|
|
<div class="option-cell">Disable recording feature</div>
|
|
<div class="option-cell">Prevents user from recording</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="note-block">
|
|
<div class="note-header">Recording Notes</div>
|
|
<ul class="note-list">
|
|
<li>Recorded file format is WebM with VP8/H264 video and OPUS/PCM audio</li>
|
|
<li>Default bitrate is approximately 4000 kbps if not specified</li>
|
|
<li>The director of a room will be notified when a user is recording</li>
|
|
<li>The director can trigger recording remotely</li>
|
|
<li>Video/audio is saved in real-time to the local download folder</li>
|
|
<li>Recording should be stopped manually before closing the browser</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="codec-section">
|
|
<h4>Recording Codec Selection (&recordcodec)</h4>
|
|
<p>Set the specific codec used when recording to disk.</p>
|
|
|
|
<div class="usage-block">
|
|
<div class="usage-header">Basic Usage</div>
|
|
<div class="usage-example">https://vdo.ninja/?push=abc123&record=2000&recordcodec=h264</div>
|
|
</div>
|
|
|
|
<div class="options-table">
|
|
<div class="option-row header-row">
|
|
<div class="option-cell">Option</div>
|
|
<div class="option-cell">Description</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>h264</code></div>
|
|
<div class="option-cell">Record using H.264 codec</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>vp8</code></div>
|
|
<div class="option-cell">Record using VP8 codec (default fallback)</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>vp9</code></div>
|
|
<div class="option-cell">Record using VP9 codec</div>
|
|
</div>
|
|
<div class="option-row">
|
|
<div class="option-cell"><code>av1</code></div>
|
|
<div class="option-cell">Record using AV1 codec</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="note-block">
|
|
<div class="note-header">Important Notes</div>
|
|
<ul class="note-list">
|
|
<li>The container format is always WebM regardless of codec, unless using Safari, and maybe then its MP4</li>
|
|
<li>If a codec is not supported, it will fall back to VP8</li>
|
|
<li>Especially useful for Chrome on Android where VP8 performance can be poor</li>
|
|
<li>Remember to add <code>&record</code> to enable the recording function</li>
|
|
<li>Use <code>&rc</code> as a shorter alias for <code>&recordcodec</code></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="codec-section">
|
|
<h4>Advanced WebRTC Features</h4>
|
|
<p>WebRTC supports additional features like SVC (Scalable Video Coding) and WHiP.</p>
|
|
|
|
<div class="feature-block">
|
|
<div class="feature-header">SVC (Scalable Video Coding)</div>
|
|
<p>SVC allows a single bitstream to have multiple resolutions, frame rates, or quality layers.</p>
|
|
<ul class="feature-list">
|
|
<li>Improves streaming adaptability for viewers with different connection qualities</li>
|
|
<li>Support varies by browser and codec</li>
|
|
<li>Common modes include L1T1 (single layer), L2T1 (2 spatial layers), S2T1 (2 temporal layers)</li>
|
|
<li>The detection tool above shows supported SVC modes for your browser</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="feature-block">
|
|
<div class="feature-header">WHiP (WebRTC HTTP ingest Protocol)</div>
|
|
<p>WHiP is a standardized protocol for sending WebRTC streams to a server.</p>
|
|
<ul class="feature-list">
|
|
<li>Enables WebRTC streaming to CDNs and streaming platforms</li>
|
|
<li>Simpler to implement than custom signaling servers</li>
|
|
<li>Useful for broadcast scenarios with many viewers</li>
|
|
<li>Check your streaming platform documentation for WHiP support details</li>
|
|
</ul>
|
|
</div>
|
|
<div class="note-block">
|
|
<div class="note-header">H.265/HEVC Support</div>
|
|
<p>H.265 (HEVC) offers excellent compression efficiency but has limited browser support. <a href="https://vdo.ninja/h265" target="_blank">Visit our H.265 guide</a> for detailed instructions on:</p>
|
|
<ul class="note-list">
|
|
<li>Enabling H.265 in Chrome using command-line flags</li>
|
|
<li>Browser compatibility information</li>
|
|
<li>Fallback strategies using comma-separated codec preferences</li>
|
|
<li>Testing your browser's H.265 support</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Main data object to store all detection results
|
|
const codecData = {
|
|
video: {}, // Will store video codec info
|
|
audio: {}, // Will store audio codec info
|
|
webrtc: {}, // Will store WebRTC related info
|
|
webcodec: {}, // Will store WebCodec related info
|
|
mediaCapabilities: {} // Will store MediaCapabilities info
|
|
};
|
|
|
|
// Basic DOM manipulation helpers
|
|
const dom = {
|
|
get: id => document.getElementById(id),
|
|
create: tag => document.createElement(tag),
|
|
append: (parent, child) => parent.appendChild(child)
|
|
};
|
|
|
|
// Detection mode
|
|
let detectionMode = {
|
|
encode: true,
|
|
decode: true
|
|
};
|
|
|
|
// Toggle switch for detection mode
|
|
dom.get('toggle-mode').addEventListener('change', function() {
|
|
if (this.checked) {
|
|
detectionMode = { encode: true, decode: true };
|
|
dom.get('mode-text').textContent = 'Testing Both Encode/Decode';
|
|
} else {
|
|
detectionMode = { encode: false, decode: true };
|
|
dom.get('mode-text').textContent = 'Testing Decode Only';
|
|
}
|
|
});
|
|
|
|
// Tab functionality
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
// Remove active class from all tabs and contents
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
|
|
// Add active class to clicked tab and corresponding content
|
|
tab.classList.add('active');
|
|
dom.get(tab.dataset.tab).classList.add('active');
|
|
});
|
|
});
|
|
|
|
// Add this to the codecs.html file, inside the script tag, near the top
|
|
|
|
// Check for results parameter
|
|
function checkForResultsParam() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const resultsId = urlParams.get('results');
|
|
|
|
if (resultsId) {
|
|
loadResultsData(resultsId);
|
|
|
|
// Update the page header
|
|
const pageHeader = document.querySelector('h1');
|
|
if (pageHeader) {
|
|
pageHeader.textContent = 'Loading Remote User Codec Support...';
|
|
}
|
|
|
|
// Disable the start button
|
|
const startButton = dom.get('start-detection');
|
|
if (startButton) {
|
|
startButton.disabled = true;
|
|
startButton.textContent = 'Loading Remote Results...';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load results data from the server
|
|
async function loadResultsData(resultsId) {
|
|
try {
|
|
// Show progress indicator
|
|
setProgress(30);
|
|
|
|
// Fetch the results data
|
|
const response = await fetch(`https://record.vdo.workers.dev/?name=${resultsId}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load results (${response.status})`);
|
|
}
|
|
|
|
const results = await response.json();
|
|
setProgress(60);
|
|
|
|
// Process the results
|
|
processResultsData(results);
|
|
|
|
// Update the UI with the loaded data
|
|
updateUI();
|
|
setProgress(100);
|
|
|
|
// Update the page header
|
|
const pageHeader = document.querySelector('h1');
|
|
if (pageHeader) {
|
|
pageHeader.textContent = 'Remote User Codec Support';
|
|
}
|
|
|
|
// Update the button text
|
|
const startButton = dom.get('start-detection');
|
|
if (startButton) {
|
|
startButton.textContent = 'Run Local Detection Instead';
|
|
startButton.disabled = false;
|
|
}
|
|
|
|
// Add a "back to results" link
|
|
const container = document.querySelector('.container');
|
|
if (container) {
|
|
const backLink = dom.create('div');
|
|
backLink.className = 'back-link';
|
|
backLink.innerHTML = `<a href="./results?id=${resultsId}">← Back to Test Results</a>`;
|
|
container.insertBefore(backLink, container.firstChild);
|
|
|
|
// Add notification that we're viewing remote data
|
|
const remoteNotice = dom.create('div');
|
|
remoteNotice.className = 'remote-notice';
|
|
remoteNotice.textContent = 'You are viewing codec information from a remote user\'s test results';
|
|
container.insertBefore(remoteNotice, container.firstChild);
|
|
|
|
// Add styles
|
|
const style = dom.create('style');
|
|
style.textContent = `
|
|
.back-link {
|
|
margin-bottom: 15px;
|
|
}
|
|
.back-link a {
|
|
color: #4296f5;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
.back-link a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.remote-notice {
|
|
background-color: #2a2a2a;
|
|
border-left: 4px solid #4296f5;
|
|
padding: 10px 15px;
|
|
margin-bottom: 15px;
|
|
font-weight: bold;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading results:', error);
|
|
showError(`Failed to load results: ${error.message}`);
|
|
|
|
// Re-enable the start button on error
|
|
const startButton = dom.get('start-detection');
|
|
if (startButton) {
|
|
startButton.disabled = false;
|
|
startButton.textContent = 'Start Comprehensive Detection';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process the loaded results data
|
|
function processResultsData(results) {
|
|
// Clear existing codec data
|
|
codecData.video = {};
|
|
codecData.audio = {};
|
|
|
|
// Look for codec information in the results
|
|
for (const item of results) {
|
|
// Look for detectedCodecs data (if it exists in the logged data)
|
|
if (item.detectedCodecs) {
|
|
// Copy the codec information
|
|
Object.assign(codecData.video, item.detectedCodecs.video || {});
|
|
Object.assign(codecData.audio, item.detectedCodecs.audio || {});
|
|
|
|
// Log info about the remote browser
|
|
if (item.detectedCodecs.browserInfo) {
|
|
addLog(`Remote Browser: ${item.detectedCodecs.browserInfo.userAgent || "Unknown"}`);
|
|
}
|
|
}
|
|
|
|
// Look for codecs data - the format used in the test data
|
|
if (item.codecs) {
|
|
// Process video codecs
|
|
if (item.codecs.video) {
|
|
Object.keys(item.codecs.video).forEach(codec => {
|
|
codecData.video[codec] = codecData.video[codec] || {};
|
|
codecData.video[codec].webrtc = true;
|
|
codecData.video[codec].canDecode = true;
|
|
|
|
// Add a badge or icon for these values in the UI
|
|
if (codec === 'h264' || codec === 'vp9') {
|
|
codecData.video[codec].details = 'Common codec for streaming';
|
|
}
|
|
});
|
|
|
|
addLog(`Remote Video Codecs: ${Object.keys(item.codecs.video).join(', ')}`);
|
|
}
|
|
|
|
// Process audio codecs
|
|
if (item.codecs.audio) {
|
|
Object.keys(item.codecs.audio).forEach(codec => {
|
|
codecData.audio[codec] = codecData.audio[codec] || {};
|
|
codecData.audio[codec].webrtc = true;
|
|
codecData.audio[codec].canDecode = true;
|
|
});
|
|
|
|
addLog(`Remote Audio Codecs: ${Object.keys(item.codecs.audio).join(', ')}`);
|
|
}
|
|
}
|
|
|
|
// Look for browser/platform info
|
|
if (item.info) {
|
|
if (item.info.Browser) {
|
|
addLog(`Browser: ${item.info.Browser}`);
|
|
}
|
|
if (item.info.platform) {
|
|
addLog(`Platform: ${item.info.platform}`);
|
|
}
|
|
if (item.info.gpGPU) {
|
|
addLog(`GPU: ${item.info.gpGPU}`);
|
|
|
|
// Set additional details for hardware acceleration based on GPU
|
|
if (codecData.video.h264) {
|
|
codecData.video.h264.details = `GPU: ${item.info.gpGPU}`;
|
|
}
|
|
}
|
|
if (item.info.CPU) {
|
|
addLog(`CPU: ${item.info.CPU}`);
|
|
}
|
|
}
|
|
|
|
// Look for encoder used
|
|
if (item.encoder) {
|
|
const encoderName = item.encoder.toLowerCase().replace('libvpx', 'vp8');
|
|
addLog(`Encoder used in test: ${encoderName}`);
|
|
|
|
// Mark this codec as used in the test
|
|
if (codecData.video[encoderName]) {
|
|
codecData.video[encoderName].usedInTest = true;
|
|
codecData.video[encoderName].details = `Used during the test`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call this function when the page loads
|
|
document.addEventListener('DOMContentLoaded', checkForResultsParam);
|
|
|
|
// Set progress bar
|
|
function setProgress(percent) {
|
|
dom.get('status-progress').style.width = `${percent}%`;
|
|
}
|
|
|
|
// Show error message
|
|
function showError(message) {
|
|
dom.get('error-message').textContent = message;
|
|
}
|
|
|
|
// Add log to raw data tab
|
|
function addLog(message, isError = false) {
|
|
const output = dom.get('output');
|
|
const logEntry = document.createElement('div');
|
|
logEntry.textContent = message;
|
|
if (isError) logEntry.style.color = '#ff5252';
|
|
output.appendChild(logEntry);
|
|
output.scrollTop = output.scrollHeight;
|
|
}
|
|
|
|
// Create a codec item element with more detailed information
|
|
function createCodecItem(codec, info) {
|
|
const item = dom.create('div');
|
|
item.className = 'codec-item';
|
|
|
|
const codecName = dom.create('div');
|
|
codecName.className = 'codec-name';
|
|
codecName.textContent = codec;
|
|
|
|
const badgesContainer = dom.create('div');
|
|
|
|
// Add API badges
|
|
if (info.mediaRecorder) {
|
|
const badge = dom.create('span');
|
|
badge.className = 'badge badge-recorder';
|
|
badge.textContent = 'MediaRecorder';
|
|
badgesContainer.appendChild(badge);
|
|
}
|
|
|
|
if (info.webcodec) {
|
|
const badge = dom.create('span');
|
|
badge.className = 'badge badge-webcodec';
|
|
badge.textContent = 'WebCodec';
|
|
badgesContainer.appendChild(badge);
|
|
}
|
|
|
|
if (info.webrtc) {
|
|
const badge = dom.create('span');
|
|
badge.className = 'badge badge-webrtc';
|
|
badge.textContent = 'WebRTC';
|
|
badgesContainer.appendChild(badge);
|
|
}
|
|
|
|
if (info.mediaCapabilities) {
|
|
const badge = dom.create('span');
|
|
badge.className = 'badge badge-mediacapabilities';
|
|
badge.textContent = 'MediaCapabilities';
|
|
badgesContainer.appendChild(badge);
|
|
}
|
|
|
|
// Add encoder/decoder badges with specific API info
|
|
if (info.canEncode) {
|
|
const badge = dom.create('span');
|
|
badge.className = 'badge badge-encoder';
|
|
badge.textContent = 'Encoder';
|
|
badgesContainer.appendChild(badge);
|
|
}
|
|
|
|
if (info.canDecode) {
|
|
const badge = dom.create('span');
|
|
badge.className = 'badge badge-decoder';
|
|
badge.textContent = 'Decoder';
|
|
badgesContainer.appendChild(badge);
|
|
}
|
|
|
|
// Create the details container for more specific information
|
|
const details = dom.create('div');
|
|
details.className = 'details';
|
|
|
|
// Add more specific hardware acceleration info
|
|
if (info.webcodecConfig && info.webcodecConfig.encoder && info.webcodecConfig.encoder.hardwareAcceleration === 'prefer-hardware') {
|
|
const hwEncBadge = dom.create('span');
|
|
hwEncBadge.className = 'badge badge-hw';
|
|
hwEncBadge.textContent = 'HW Encoder';
|
|
details.appendChild(hwEncBadge);
|
|
}
|
|
|
|
if (info.webcodecConfig && info.webcodecConfig.decoder && info.webcodecConfig.decoder.hardwareAcceleration === 'prefer-hardware') {
|
|
const hwDecBadge = dom.create('span');
|
|
hwDecBadge.className = 'badge badge-hw';
|
|
hwDecBadge.textContent = 'HW Decoder';
|
|
details.appendChild(hwDecBadge);
|
|
}
|
|
|
|
if (info.webrtcHwEncoding) {
|
|
const hwWrtcEncBadge = dom.create('span');
|
|
hwWrtcEncBadge.className = 'badge badge-hw';
|
|
hwWrtcEncBadge.textContent = 'WebRTC HW Encoder';
|
|
details.appendChild(hwWrtcEncBadge);
|
|
}
|
|
|
|
// Add special details if available
|
|
if (info.profile) {
|
|
details.innerHTML += `<div>Profile: ${info.profile}</div>`;
|
|
}
|
|
|
|
if (info.resolutionLimit) {
|
|
details.innerHTML += `<div>Resolution Limit: ${info.resolutionLimit}</div>`;
|
|
}
|
|
|
|
if (info.details) {
|
|
details.innerHTML += `<div>${info.details}</div>`;
|
|
}
|
|
|
|
if (info.scalabilityModes && info.scalabilityModes.length) {
|
|
details.innerHTML += `<div>SVC Modes: ${info.scalabilityModes.join(', ')}</div>`;
|
|
}
|
|
|
|
item.appendChild(codecName);
|
|
item.appendChild(badgesContainer);
|
|
item.appendChild(details);
|
|
|
|
return item;
|
|
}
|
|
|
|
// Get all supported MIME types via MediaRecorder
|
|
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);
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Check hardware acceleration via WebRTC
|
|
async function checkWebRTCHardwareAcceleration(codec) {
|
|
return new Promise(async (resolve) => {
|
|
try {
|
|
// Create peer connection with reasonable config (no TURN servers needed)
|
|
const config = {
|
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
iceCandidatePoolSize: 0
|
|
};
|
|
|
|
const pc1 = new RTCPeerConnection(config);
|
|
const pc2 = new RTCPeerConnection(config);
|
|
|
|
// Create data channel to keep connection alive
|
|
pc1.createDataChannel('test', {ordered: true});
|
|
|
|
// Connect the peers
|
|
pc1.onicecandidate = e => e.candidate && pc2.addIceCandidate(e.candidate);
|
|
pc2.onicecandidate = e => e.candidate && pc1.addIceCandidate(e.candidate);
|
|
|
|
// Create a video stream from canvas to avoid permission prompts
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 1280;
|
|
canvas.height = 720;
|
|
|
|
// Draw something on the canvas to ensure there's content
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#3498db';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Add some animation to ensure proper encoding
|
|
const startTime = Date.now();
|
|
function animateCanvas() {
|
|
const elapsed = Date.now() - startTime;
|
|
ctx.fillStyle = '#3498db';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw a moving circle
|
|
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);
|
|
|
|
// Add tracks to the connection
|
|
stream.getVideoTracks().forEach(track => {
|
|
pc1.addTrack(track, stream);
|
|
});
|
|
|
|
// Create and set the offer with specific codec
|
|
const transceiver = pc1.getTransceivers()[0];
|
|
const codecs = RTCRtpSender.getCapabilities('video').codecs;
|
|
|
|
// Find matching codec
|
|
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);
|
|
|
|
// Wait a bit for stats to be available
|
|
setTimeout(async () => {
|
|
let hardwareAccelerated = false;
|
|
let profile = null;
|
|
|
|
try {
|
|
const stats = await pc1.getStats();
|
|
stats.forEach(stat => {
|
|
// Check for hardware encoding
|
|
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');
|
|
}
|
|
|
|
// Try to get the profile ID
|
|
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) {
|
|
addLog(`Error getting WebRTC stats: ${e.message}`, true);
|
|
}
|
|
|
|
// Clean up
|
|
pc1.close();
|
|
pc2.close();
|
|
stream.getTracks().forEach(track => track.stop());
|
|
|
|
resolve({
|
|
hardwareAccelerated,
|
|
profile,
|
|
webrtc: true,
|
|
canEncode: true,
|
|
canDecode: true,
|
|
webrtcHwEncoding: hardwareAccelerated // specific flag for WebRTC encoding
|
|
});
|
|
}, 2000);
|
|
} catch (error) {
|
|
addLog(`WebRTC check failed for ${codec}: ${error.message}`, true);
|
|
resolve({ webrtc: false });
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check hardware acceleration via WebCodec API for encoding
|
|
async function checkWebCodecEncoder(codec, width = 1280, height = 720, framerate = 30) {
|
|
if (!('VideoEncoder' in window) || !detectionMode.encode) {
|
|
return { webcodec: false };
|
|
}
|
|
|
|
let result = { webcodec: false };
|
|
|
|
try {
|
|
// Map common codec names to WebCodec format
|
|
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) {
|
|
addLog(`WebCodec encoder check failed for ${codec}: ${error.message}`, true);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Check hardware acceleration via WebCodec API for decoding
|
|
async function checkWebCodecDecoder(codec, width = 1280, height = 720, framerate = 30) {
|
|
if (!('VideoDecoder' in window) || !detectionMode.decode) {
|
|
return { webcodec: false };
|
|
}
|
|
|
|
let result = { webcodec: false };
|
|
|
|
try {
|
|
// Map common codec names to WebCodec format
|
|
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) // Dummy data
|
|
};
|
|
|
|
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) // Dummy data
|
|
};
|
|
|
|
const swSupport = await VideoDecoder.isConfigSupported(swConfig);
|
|
|
|
if (swSupport && swSupport.supported) {
|
|
result = {
|
|
webcodec: true,
|
|
hardwareAccelerated: false,
|
|
config: swSupport.config,
|
|
canDecode: true
|
|
};
|
|
}
|
|
}
|
|
} catch (error) {
|
|
addLog(`WebCodec decoder check failed for ${codec}: ${error.message}`, true);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Comprehensive codec detection
|
|
async function detectCodecs() {
|
|
setProgress(0);
|
|
showError('');
|
|
dom.get('output').innerHTML = '';
|
|
dom.get('video-codec-list').innerHTML = '<p>Detection in progress...</p>';
|
|
dom.get('audio-codec-list').innerHTML = '<p>Detection in progress...</p>';
|
|
|
|
// Disable the start button during detection
|
|
const startButton = dom.get('start-detection');
|
|
startButton.disabled = true;
|
|
startButton.textContent = 'Detection in progress...';
|
|
|
|
try {
|
|
// 1. Define codec lists
|
|
addLog('Starting codec detection...');
|
|
addLog(`Mode: ${detectionMode.encode ? 'Encoding + ' : ''}${detectionMode.decode ? 'Decoding' : ''}`);
|
|
|
|
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"
|
|
];
|
|
|
|
setProgress(10);
|
|
|
|
// 2. Detect MediaRecorder support
|
|
addLog('Checking MediaRecorder support...');
|
|
|
|
const supportedVideoTypes = getSupportedMimeTypes("video", videoTypes, videoCodecs);
|
|
const supportedAudioTypes = getSupportedMimeTypes("audio", audioTypes, audioCodecs);
|
|
|
|
supportedVideoTypes.forEach(type => {
|
|
const codecMatch = type.match(/codecs=([^,]+)/);
|
|
const codec = codecMatch ? codecMatch[1] : type.split('/')[1];
|
|
|
|
codecData.video[codec] = codecData.video[codec] || {};
|
|
codecData.video[codec].mediaRecorder = true;
|
|
codecData.video[codec].canEncode = true;
|
|
codecData.video[codec].mimeTypes = codecData.video[codec].mimeTypes || [];
|
|
codecData.video[codec].mimeTypes.push(type);
|
|
});
|
|
|
|
supportedAudioTypes.forEach(type => {
|
|
const codecMatch = type.match(/codecs=([^,]+)/);
|
|
const codec = codecMatch ? codecMatch[1] : type.split('/')[1];
|
|
|
|
codecData.audio[codec] = codecData.audio[codec] || {};
|
|
codecData.audio[codec].mediaRecorder = true;
|
|
codecData.audio[codec].canEncode = true;
|
|
codecData.audio[codec].mimeTypes = codecData.audio[codec].mimeTypes || [];
|
|
codecData.audio[codec].mimeTypes.push(type);
|
|
});
|
|
|
|
addLog(`Found ${supportedVideoTypes.length} supported video MediaRecorder formats`);
|
|
addLog(`Found ${supportedAudioTypes.length} supported audio MediaRecorder formats`);
|
|
|
|
setProgress(30);
|
|
|
|
// 3. Detect WebCodec support
|
|
if ('VideoEncoder' in window || 'VideoDecoder' in window) {
|
|
addLog('Checking WebCodec support...');
|
|
|
|
// Check at multiple resolutions to detect resolution-dependent hardware acceleration
|
|
const resolutions = [
|
|
{ width: 640, height: 360 },
|
|
{ width: 1280, height: 720 },
|
|
{ width: 1920, height: 1080 }
|
|
];
|
|
|
|
for (const codec of ['vp8', 'vp9', 'h264', 'av1', 'h265']) {
|
|
for (const res of resolutions) {
|
|
// Check encoder if enabled
|
|
if (detectionMode.encode) {
|
|
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 if enabled
|
|
if (detectionMode.decode) {
|
|
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 {
|
|
addLog('WebCodec API not supported by this browser');
|
|
}
|
|
|
|
setProgress(60);
|
|
|
|
// 4. Detect WebRTC support and hardware acceleration
|
|
if ('RTCPeerConnection' in window) {
|
|
addLog('Checking WebRTC support and hardware acceleration...');
|
|
|
|
// Codecs most commonly used in WebRTC
|
|
const webrtcCodecs = ['VP8', 'VP9', 'AV1', 'H264'];
|
|
|
|
if (detectionMode.encode || detectionMode.decode) {
|
|
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;
|
|
|
|
if (detectionMode.encode) {
|
|
codecData.video[normalizedCodec].canEncode = true;
|
|
}
|
|
|
|
if (detectionMode.decode) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
addLog('WebRTC not supported by this browser');
|
|
}
|
|
|
|
setProgress(90);
|
|
|
|
// 5. Update the UI with all collected data
|
|
updateUI();
|
|
|
|
// 6. Check MediaCapabilities API
|
|
if ('mediaCapabilities' in navigator) {
|
|
addLog('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",
|
|
"L1T2h", "L1T3h",
|
|
"L2T1h", "L2T2h", "L2T3h",
|
|
"L3T1h", "L3T2h", "L3T3h"
|
|
];
|
|
|
|
// 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
|
|
});
|
|
}
|
|
});
|
|
|
|
addLog(`Found ${webrtcCodecs.length} supported WebRTC codecs`);
|
|
} catch (e) {
|
|
addLog(`Error getting RTCRtpSender.getCapabilities: ${e.message}`, true);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
if (detectionMode.decode) {
|
|
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, suggesting hardware acceleration. ';
|
|
}
|
|
|
|
// 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) {
|
|
addLog(`MediaCapabilities check failed for ${codec.name}: ${error.message}`, true);
|
|
}
|
|
}
|
|
} else {
|
|
addLog('MediaCapabilities API not supported by this browser');
|
|
}
|
|
|
|
addLog('Detection complete!');
|
|
setProgress(100);
|
|
|
|
// Re-enable the start button
|
|
const startButton = dom.get('start-detection');
|
|
startButton.disabled = false;
|
|
startButton.textContent = 'Start Comprehensive Detection';
|
|
} catch (error) {
|
|
showError(`Detection failed: ${error.message}`);
|
|
addLog(`Detection error: ${error.message}`, true);
|
|
console.error(error);
|
|
|
|
// Re-enable the start button on error
|
|
const startButton = dom.get('start-detection');
|
|
startButton.disabled = false;
|
|
startButton.textContent = 'Start Comprehensive Detection';
|
|
}
|
|
}
|
|
|
|
|
|
// Update the UI with detected codec information
|
|
function updateUI() {
|
|
// Clear existing content
|
|
dom.get('video-codec-list').innerHTML = '';
|
|
dom.get('audio-codec-list').innerHTML = '';
|
|
|
|
// Add video codecs
|
|
const videoCodecs = Object.keys(codecData.video);
|
|
if (videoCodecs.length === 0) {
|
|
dom.get('video-codec-list').innerHTML = '<p>No supported video codecs detected</p>';
|
|
} else {
|
|
videoCodecs.forEach(codec => {
|
|
const info = codecData.video[codec];
|
|
const item = createCodecItem(codec, info);
|
|
dom.get('video-codec-list').appendChild(item);
|
|
});
|
|
}
|
|
|
|
// Add audio codecs
|
|
const audioCodecs = Object.keys(codecData.audio);
|
|
if (audioCodecs.length === 0) {
|
|
dom.get('audio-codec-list').innerHTML = '<p>No supported audio codecs detected</p>';
|
|
} else {
|
|
audioCodecs.forEach(codec => {
|
|
const info = codecData.audio[codec];
|
|
const item = createCodecItem(codec, info);
|
|
dom.get('audio-codec-list').appendChild(item);
|
|
});
|
|
}
|
|
|
|
dom.get('output').innerHTML += '\n--- DETECTION SUMMARY ---\n';
|
|
dom.get('output').innerHTML += `Video Codecs: ${videoCodecs.join(', ')}\n`;
|
|
dom.get('output').innerHTML += `Audio Codecs: ${audioCodecs.join(', ')}\n`;
|
|
|
|
// Log hardware accelerated codecs
|
|
const hwCodecs = videoCodecs.filter(codec => codecData.video[codec].hardwareAccelerated);
|
|
dom.get('output').innerHTML += `Hardware Accelerated: ${hwCodecs.join(', ')}\n`;
|
|
|
|
// Add H.264 profile information
|
|
const h264Codecs = videoCodecs.filter(codec =>
|
|
codec.toLowerCase().includes('h264') || codec.toLowerCase().includes('avc'));
|
|
if (h264Codecs.length > 0) {
|
|
dom.get('output').innerHTML += '\n--- H.264 PROFILES ---\n';
|
|
h264Codecs.forEach(codec => {
|
|
const profile = codecData.video[codec].profile || 'Unknown';
|
|
dom.get('output').innerHTML += `${codec}: profile-level-id=${profile}\n`;
|
|
});
|
|
}
|
|
}
|
|
|
|
let detectionStarted = false;
|
|
|
|
setTimeout(function() {
|
|
if (!detectionStarted) {
|
|
dom.get('start-detection').click();
|
|
addLog('Auto-starting detection...');
|
|
}
|
|
}, 5000);
|
|
|
|
// Start detection when button is clicked
|
|
dom.get('start-detection').addEventListener('click', function() {
|
|
detectionStarted = true;
|
|
detectCodecs();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |