feat: Added browser codec support checker

This commit is contained in:
Boofdev 2025-03-12 12:06:53 +01:00
parent e71323f02f
commit 8308e02d1f
6 changed files with 544 additions and 6 deletions

View File

@ -36,6 +36,7 @@
"flowbite": "^3.1.2",
"flowbite-svelte": "^0.47.4",
"fuse.js": "^7.1.0",
"iconify-icon": "^2.3.0",
"mode-watcher": "^0.5.0",
"msgpackr": "^1.11.2",
"pako": "^2.1.0",

15
pnpm-lock.yaml generated
View File

@ -26,6 +26,9 @@ importers:
fuse.js:
specifier: ^7.1.0
version: 7.1.0
iconify-icon:
specifier: ^2.3.0
version: 2.3.0
mode-watcher:
specifier: ^0.5.0
version: 0.5.0(svelte@5.2.11)
@ -273,6 +276,9 @@ packages:
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -852,6 +858,9 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
iconify-icon@2.3.0:
resolution: {integrity: sha512-C0beI9oTDxQz6voI5CKl7MiJf0Lw4UU8K4G4t6pcUDClLmCvuMOpcvd8MAztQ2SfoH0iv7WHdxBFjekKPFKH2Q==}
import-meta-resolve@4.1.0:
resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
@ -1724,6 +1733,8 @@ snapshots:
'@floating-ui/utils@0.2.9': {}
'@iconify/types@2.0.0': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@ -2290,6 +2301,10 @@ snapshots:
html-void-elements@3.0.0: {}
iconify-icon@2.3.0:
dependencies:
'@iconify/types': 2.0.0
import-meta-resolve@4.1.0: {}
is-binary-path@2.1.0:

View File

@ -1,4 +1,5 @@
<script lang="ts">
import 'iconify-icon';
let { children } = $props();
</script>

View File

@ -17,7 +17,21 @@
import remarkGfm from 'remark-gfm';
import { unified } from 'unified';
let alerts = [];
let alerts: {
text: string;
color:
| 'dark'
| 'red'
| 'yellow'
| 'green'
| 'indigo'
| 'purple'
| 'pink'
| 'blue'
| 'primary'
| 'none';
icon: string;
}[] = $state([]);
let conversionOptions = $state({
gfm: true,
@ -39,14 +53,15 @@
outputValue = u.processSync(inputValue).toString();
alerts.push({ text: 'Conversion successful', color: 'green', icon: 'nrk:check' });
} catch (e) {
outputValue = e
alerts.push({ text: e, color: 'red' });
outputValue = e;
alerts.push({ text: e, color: 'red', icon: 'nrk:close' });
}
}
</script>
<div class="flex min-h-screen items-center justify-between gap-4 px-4">
<div class="flex min-h-screen flex-col items-center justify-between gap-4 px-4 md:flex-row">
<!-- Left Section -->
<div class="flex flex-1 flex-col items-center justify-center space-y-2">
<Label for="markdown-input">Markdown</Label>
@ -85,9 +100,21 @@
bind:value={outputValue}
/>
</div>
</div>
<div class="toast-container">
{#each alerts as alert}
<Toast transition={fly} params={{ x: 200 }} color={alert.color} class="mb-4">
<iconify-icon icon={alert.icon} slot="icon"></iconify-icon>
{alert.text}
</Toast>
{/each}
</div>
<style>
.toast-container {
position: fixed;
right: 1rem;
bottom: 1rem;
}
</style>

View File

@ -0,0 +1,211 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getAllAV1Codecs, getAllAVCCodecs, getAllHEVCCodecs, getAllVP9Codecs } from './codecs';
type ResultType = {
name: string;
// The result type. If it is a boolean it will be rendered as a tick or cross. If it is a number it will be a percentage of the 'total' value.
type?: 'boolean' | 'string' | 'number';
supported: boolean | string | number;
// Has no effect if the result type is not a number
total?: number;
};
type SectionType = {
id: string;
name: string;
description: string;
note: string;
results?: ResultType[];
};
class Results {
sections: SectionType[] = [];
addSection(id: string, name: string, description: string, note?: string) {
this.sections.push({
id: id,
name: name,
description: description,
note: note,
results: []
});
}
addFormat(sectionId: string, resultData: ResultType) {
// Find the section
const section = this.sections.find((section) => section.id === sectionId);
if (section) {
// Add the result
section.results.push(resultData);
}
}
}
let results = new Results();
let isChecking = true;
const imageFormats: { name: string; mime: string }[] = [
{ name: 'AVIF', mime: 'avif' },
{ name: 'WEBP', mime: 'webp' },
{ name: 'JPEG', mime: 'jpeg' },
{ name: 'PNG', mime: 'png' },
{ name: 'GIF', mime: 'gif' },
{ name: 'BMP', mime: 'bmp' },
{ name: 'ICO', mime: 'vnd.microsoft.icon' },
{ name: 'SVG', mime: 'svg+xml' },
{ name: 'HEIC', mime: 'heic' },
{ name: 'HEIF', mime: 'heif' },
{ name: 'TIFF', mime: 'tiff' },
{ name: 'JPEG XL', mime: 'jxl' }
];
async function checkImageFormat(format: string): Promise<boolean> {
return new Promise((resolve) => {
const img = new Image();
img.src = `data:image/${format};base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=`;
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
});
}
onMount(async () => {
// Check image formats
const imageResults = await Promise.all(
imageFormats.map(async (format) => checkImageFormat(format.mime))
);
results.addSection(
'image',
'Image Formats',
'Image formats detected using data URIs',
'This tends to be pretty inaccurate'
);
imageFormats.forEach((format, i) => {
results.addFormat('image', {
name: format.name,
type: 'boolean',
supported: imageResults[i]
});
});
results.addSection('video.codecs', 'Video Codecs', 'Video codecs detected using MediaSource.isTypeSupported(). The larger the number displayed the more variations of the codec are supported.');
const av1Codecs = getAllAV1Codecs();
const avcCodecs = getAllAVCCodecs();
const hevcCodecs = getAllHEVCCodecs();
const vp9Codecs = getAllVP9Codecs();
let av1CodecsSupported = 0;
let avcCodecsSupported = 0;
let hevcCodecsSupported = 0;
let vp9CodecsSupported = 0;
av1Codecs.forEach((codec) => {
const supported = MediaSource.isTypeSupported(`video/mp4; codecs="${codec.codec}"`);
if (supported) {
av1CodecsSupported++;
}
});
avcCodecs.forEach((codec) => {
const supported = MediaSource.isTypeSupported(`video/mp4; codecs="${codec.codec}"`);
if (supported) {
avcCodecsSupported++;
}
});
hevcCodecs.forEach((codec) => {
const supported = MediaSource.isTypeSupported(`video/mp4; codecs="${codec.codec}"`);
if (supported) {
hevcCodecsSupported++;
}
});
vp9Codecs.forEach((codec) => {
const supported = MediaSource.isTypeSupported(`video/mp4; codecs="${codec.codec}"`);
if (supported) {
vp9CodecsSupported++;
}
});
results.addFormat('video.codecs', {
name: 'AV1',
type: 'number',
supported: av1CodecsSupported,
total: av1Codecs.length
});
results.addFormat('video.codecs', {
name: 'AVC',
type: 'number',
supported: avcCodecsSupported,
total: avcCodecs.length
});
results.addFormat('video.codecs', {
name: 'HEVC',
type: 'number',
supported: hevcCodecsSupported,
total: hevcCodecs.length
});
results.addFormat('video.codecs', {
name: 'VP9',
type: 'number',
supported: vp9CodecsSupported,
total: vp9Codecs.length
});
isChecking = false;
});
</script>
<div class="min-h-screen bg-gray-100 px-4 py-8">
<div class="mx-auto max-w-2xl space-y-6">
<h1 class="text-center text-3xl font-bold text-gray-900">Browser Format Support Checker</h1>
{#if isChecking}
<div class="text-center text-gray-600">Checking supported formats...</div>
{:else}
{#each results.sections as section}
<div class="rounded-lg bg-white p-6 shadow">
<h2 class="mb-4 text-xl font-semibold">{section.name}</h2>
<p class="mb-2 text-center text-gray-600">{section.description}</p>
{#if section.note}
<p class="mb-2 text-center text-yellow-600">{section.note}</p>
{/if}
<ul class="space-y-2">
{#each section.results as format}
{#if format.type === 'boolean'}
<li
class="flex items-center justify-between rounded-lg p-3 {format.supported
? 'bg-green-50'
: 'bg-red-50'}"
>
<span class="font-medium">{format.name}</span>
<div class="flex items-center space-x-2">
<div
class={`h-2 w-2 rounded-full ${format.supported ? 'bg-green-500' : 'bg-red-500'}`}
/>
<span class={format.supported ? 'text-green-700' : 'text-red-700'}>
{format.supported ? 'Supported' : 'Not Supported'}
</span>
</div>
</li>
{:else if format.type === 'number' && typeof format.supported === 'number'}
<li
class="flex items-center justify-between rounded-lg p-3"
style="background-color: hsl({(format.supported / format.total) * 120}, 100%, 90%)"
>
<span class="font-medium">{format.name}</span>
<div class="flex items-center space-x-2">
<div
style="background-color: hsl({(format.supported / format.total) * 120}, 100%, 50%)"
class="h-2 w-2 rounded-full"
/>
<span class="text-gray-700">
{format.supported}/{format.total}
</span>
</div>
</li>
{/if}
{/each}
</ul>
</div>
{/each}
{/if}
</div>
</div>

View File

@ -0,0 +1,283 @@
export function getAllAVCCodecs()
{
var AVC_PROFILES_DESC = [
//{ constrained_set0_flag: true },
//{ constrained_set1_flag: true },
//{ constrained_set2_flag: true },
{ profile_idc: 66, description: "Baseline" },
{ profile_idc: 66, description: "Constrained Baseline", constrained_set1_flag: true},
{ profile_idc: 77, description: "Main" },
{ profile_idc: 77, description: "Constrained Main", constrained_set1_flag: true},
{ profile_idc: 88, description: "Extended" },
{ profile_idc: 100, description: "High", constrained_set4_flag: false },
{ profile_idc: 100, description: "High Progressive", constrained_set4_flag: true },
{ profile_idc: 100, description: "Constrained High", constrained_set4_flag: true, constrained_set5_flag: true },
{ profile_idc: 110, description: "High 10" },
{ profile_idc: 110, description: "High 10 Intra", constrained_set3_flag: true },
{ profile_idc: 122, description: "High 4:2:2" },
{ profile_idc: 122, description: "High 4:2:2 Intra", constrained_set3_flag: true },
{ profile_idc: 244, description: "High 4:4:4 Predictive" },
{ profile_idc: 244, description: "High 4:4:4 Intra", constrained_set3_flag: true },
{ profile_idc: 44, description: "CAVLC 4:4:4 Intra" }
];
var AVC_PROFILES_IDC = [ 66, 77, 88, 100, 110, 122, 244, 44];
var AVC_CONSTRAINTS = [ 0, 4, 8, 16, 32, 64, 128 ];
var AVC_LEVELS = [ 10, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 52];
var sj, sk, sl;
var mimes = [];
for (var j in AVC_PROFILES_IDC) {
sj = AVC_PROFILES_IDC[j].toString(16);
if (sj.length == 1) sj = "0"+sj;
for (var k in AVC_CONSTRAINTS) {
sk = AVC_CONSTRAINTS[k].toString(16);
if (sk.length == 1) sk = "0"+sk;
var desc = "";
for (let i in AVC_PROFILES_DESC) {
if (AVC_PROFILES_IDC[j] == AVC_PROFILES_DESC[i].profile_idc) {
var c = ((AVC_PROFILES_DESC[i].constrained_set0_flag ? 1 : 0) << 7) |
((AVC_PROFILES_DESC[i].constrained_set1_flag ? 1 : 0) << 6) |
((AVC_PROFILES_DESC[i].constrained_set2_flag ? 1 : 0) << 5) |
((AVC_PROFILES_DESC[i].constrained_set3_flag ? 1 : 0) << 4) |
((AVC_PROFILES_DESC[i].constrained_set4_flag ? 1 : 0) << 3) |
((AVC_PROFILES_DESC[i].constrained_set5_flag ? 1 : 0) << 2);
if (c === AVC_CONSTRAINTS[k]) {
desc = AVC_PROFILES_DESC[i].description;
break;
}
}
}
if (desc.length > 0) {
for (var l in AVC_LEVELS) {
sl = AVC_LEVELS[l].toString(16);
if (sl.length == 1) sl = "0"+sl;
mimes.push({
codec: 'avc1.'+sj+sk+sl,
description: "AVC "+desc+" Level "+ AVC_LEVELS[l]/10
});
}
}
}
}
return mimes;
}
export function getAllAV1Codecs()
{
var PROFILES_VALUES = [ 0, 1, 2 ];
var PROFILES_NAMES = [ 'Main', 'High', 'Professional' ];
var LEVEL_VALUES = [ 0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
12, 13, 14, 15,
16, 17, 18, 19,
20, 21, 22, 23,
31];
var LEVEL_NAMES = [ '2.0', '2.1', '2.2', '2.3',
'3.0', '3.1', '3.2', '3.3',
'4.0', '4.1', '4.2', '4.3',
'5.0', '5.1', '5.2', '5.3',
'6.0', '6.1', '6.2', '6.3',
'7.0', '6.1', '7.2', '7.3',
'Max' ];
var TIER_VALUES = [ 'M', 'H' ];
var TIER_NAMES = [ 'Main', 'High' ];
var DEPTH_VALUES = [ 8, 10, 12];
var MONOCHROME_VALUES = [ null ];//, 0, 1 ];
var CHROMA_SUBSAMPLING_VALUES = [ '000', '001', '010', '011', '100', '101', '110', '111' ];
var COLOR_PRIMARIES_VALUES = [ 0, 1, 2];//, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ];
var TRANSER_CHARACTERISTICS_VALUES = [ 0, 1, 2];//, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ];
var MATRIX_COEFFICIENT_VALUES = [ 0, 1, 2];//, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ];
var VIDEO_FULL_RANGE_FLAG_VALUES = [ 0, 1 ];
var allValues = [];
for (var profile in PROFILES_VALUES) {
for (var level in LEVEL_VALUES) {
var levelString = ''+LEVEL_VALUES[level];
if (levelString.length == 1) levelString = "0"+levelString;
for (var tier in TIER_VALUES) {
for (var depth in DEPTH_VALUES) {
var depthString = ''+DEPTH_VALUES[depth];
if (depthString.length == 1) depthString = "0"+depthString;
for (var mono in MONOCHROME_VALUES) {
if (MONOCHROME_VALUES[mono]!= null) {
for (var chroma in CHROMA_SUBSAMPLING_VALUES) {
for (var colorPrimary in COLOR_PRIMARIES_VALUES) {
for (var transfer in TRANSER_CHARACTERISTICS_VALUES) {
for (var matrix in MATRIX_COEFFICIENT_VALUES) {
for (var range in VIDEO_FULL_RANGE_FLAG_VALUES) {
allValues.push({
codec: 'av01.'+PROFILES_VALUES[profile]+
'.'+levelString+
''+TIER_VALUES[tier]+
'.'+depthString+
'.'+MONOCHROME_VALUES[mono]+
'.'+CHROMA_SUBSAMPLING_VALUES[chroma]+
'.'+COLOR_PRIMARIES_VALUES[colorPrimary]+
'.'+TRANSER_CHARACTERISTICS_VALUES[transfer]+
'.'+MATRIX_COEFFICIENT_VALUES[matrix]+
'.'+VIDEO_FULL_RANGE_FLAG_VALUES[range],
description: ''
});
}
}
}
}
}
} else {
allValues.push({
codec: 'av01.'+PROFILES_VALUES[profile]+
'.'+levelString+
''+TIER_VALUES[tier]+
'.'+depthString,
description: 'AV1 '+PROFILES_NAMES[profile]+' Profile, level '+LEVEL_NAMES[level]+', '+TIER_NAMES[tier]+ ' tier, '+DEPTH_VALUES[depth]+' bits'
});
}
}
}
}
}
}
return allValues;
}
export function getAllHEVCCodecs() {
const HEVC_PROFILES_DESC = [
{ profile_idc: 1, description: "Main" },
{ profile_idc: 2, description: "Main 10" },
{ profile_idc: 3, description: "Main Still Picture" },
{ profile_idc: 4, description: "Range Extensions" },
{ profile_idc: 5, description: "High Throughput" },
{ profile_idc: 6, description: "Multiview Main" },
{ profile_idc: 7, description: "Scalable Main" },
{ profile_idc: 8, description: "3D Main" },
{ profile_idc: 9, description: "Screen Content Coding Extensions" },
{ profile_idc: 10, description: "Scalable Format Range Extensions" },
];
const HEVC_TIERS = [
{ tier: 0, tier_name: 'Main' },
{ tier: 1, tier_name: 'High' }
];
const HEVC_LEVELS = [30, 60, 63, 90, 93, 120, 123, 150, 153, 156, 180, 183, 186];
const HEVC_CONSTRAINTS = ['B0']; // Placeholder for constraints
const mimes = [];
for (const profile of HEVC_PROFILES_DESC) {
for (const tier of HEVC_TIERS) {
for (const level of HEVC_LEVELS) {
for (const constraint of HEVC_CONSTRAINTS) {
const tierCompatibility = tier.tier === 0 ? 6 : 4; // Example-based
const levelHex = level.toString(16).toUpperCase().padStart(2, '0');
const codec = `hev1.${profile.profile_idc}.${tierCompatibility}.L${levelHex}.${constraint}`;
const levelName = (level / 30).toFixed(1);
const description = `HEVC ${profile.description}, ${tier.tier_name} Tier, Level ${levelName}`;
mimes.push({ codec, description });
}
}
}
}
return mimes;
}
export function getAllVP9Codecs() {
const VP9_PROFILES = [0, 1, 2, 3];
const VP9_PROFILE_NAMES = ['Profile 0', 'Profile 1', 'Profile 2', 'Profile 3'];
const VP9_LEVELS = [10, 11, 20, 21, 30, 31, 40, 41, 50, 51];
const VP9_LEVEL_NAMES = {
10: '1.0', 11: '1.1', 20: '2.0', 21: '2.1',
30: '3.0', 31: '3.1', 40: '4.0', 41: '4.1',
50: '5.0', 51: '5.1'
};
const VP9_BIT_DEPTHS = [8, 10, 12];
const VP9_CHROMA_SUBSAMPLING = [0, 1, 2];
const VP9_COLOR_PRIMARIES = [1, 9]; // 1=BT.709, 9=BT.2020
const VP9_TRANSFER_CHARACTERISTICS = [1, 15]; // 1=BT.709, 15=HLG
const VP9_MATRIX_COEFFICIENTS = [1, 9]; // 1=BT.709, 9=BT.2020
const VP9_VIDEO_FULL_RANGE_FLAG = [0, 1];
const mimes = [];
const pad = (n) => n.toString().padStart(2, '0');
const getChromaName = (cs) => {
switch (cs) {
case 0: return '4:2:0';
case 1: return '4:2:2';
case 2: return '4:4:4';
default: return 'Unknown';
}
};
const getColorPrimaryName = (cp) => {
switch (cp) {
case 1: return 'BT.709';
case 9: return 'BT.2020';
default: return 'Unknown';
}
};
const getTransferName = (tc) => {
switch (tc) {
case 1: return 'BT.709';
case 15: return 'HLG';
default: return 'Unknown';
}
};
const getMatrixName = (mc) => {
switch (mc) {
case 1: return 'BT.709';
case 9: return 'BT.2020';
default: return 'Unknown';
}
};
for (const profile of VP9_PROFILES) {
for (const level of VP9_LEVELS) {
const levelName = VP9_LEVEL_NAMES[level] || 'Unknown';
for (const bitDepth of VP9_BIT_DEPTHS) {
// Minimal codec string
const minimalCodec = `vp09.${pad(profile)}.${pad(level)}.${pad(bitDepth)}`;
const minimalDesc = `VP9 ${VP9_PROFILE_NAMES[profile]}, Level ${levelName}, ${bitDepth}-bit`;
mimes.push({ codec: minimalCodec, description: minimalDesc });
// Full codec strings with all parameters
for (const chroma of VP9_CHROMA_SUBSAMPLING) {
for (const colorPrimary of VP9_COLOR_PRIMARIES) {
for (const transfer of VP9_TRANSFER_CHARACTERISTICS) {
for (const matrix of VP9_MATRIX_COEFFICIENTS) {
for (const range of VP9_VIDEO_FULL_RANGE_FLAG) {
const codec = [
`vp09.${pad(profile)}`,
pad(level),
pad(bitDepth),
pad(chroma),
pad(colorPrimary),
pad(transfer),
pad(matrix),
pad(range)
].join('.');
const desc = [
minimalDesc,
`Chroma ${getChromaName(chroma)}`,
`Color ${getColorPrimaryName(colorPrimary)}`,
`Transfer ${getTransferName(transfer)}`,
`Matrix ${getMatrixName(matrix)}`,
`${range ? 'Full' : 'Limited'} Range`
].join(', ');
mimes.push({ codec, description: desc });
}
}
}
}
}
}
}
}
return mimes;
}