From 8308e02d1f01a48e250b8942fe042adb85562bd0 Mon Sep 17 00:00:00 2001 From: Boofdev Date: Wed, 12 Mar 2025 12:06:53 +0100 Subject: [PATCH] feat: Added browser codec support checker --- package.json | 1 + pnpm-lock.yaml | 15 + src/routes/(normal)/+layout.svelte | 5 +- .../(normal)/markdown-to-html/+page.svelte | 35 ++- src/routes/(normal)/supports/+page.svelte | 211 +++++++++++++ src/routes/(normal)/supports/codecs.js | 283 ++++++++++++++++++ 6 files changed, 544 insertions(+), 6 deletions(-) create mode 100644 src/routes/(normal)/supports/+page.svelte create mode 100644 src/routes/(normal)/supports/codecs.js diff --git a/package.json b/package.json index e818d30..91b672d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1174be..314a1fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/routes/(normal)/+layout.svelte b/src/routes/(normal)/+layout.svelte index c63a393..2a706e1 100644 --- a/src/routes/(normal)/+layout.svelte +++ b/src/routes/(normal)/+layout.svelte @@ -1,6 +1,7 @@ -{@render children()} \ No newline at end of file +{@render children()} diff --git a/src/routes/(normal)/markdown-to-html/+page.svelte b/src/routes/(normal)/markdown-to-html/+page.svelte index 0b876f0..23d50a7 100644 --- a/src/routes/(normal)/markdown-to-html/+page.svelte +++ b/src/routes/(normal)/markdown-to-html/+page.svelte @@ -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' }); } } -
+
@@ -85,9 +100,21 @@ bind:value={outputValue} />
+
+ +
{#each alerts as alert} + {alert.text} {/each}
+ + diff --git a/src/routes/(normal)/supports/+page.svelte b/src/routes/(normal)/supports/+page.svelte new file mode 100644 index 0000000..fdc798a --- /dev/null +++ b/src/routes/(normal)/supports/+page.svelte @@ -0,0 +1,211 @@ + + +
+
+

Browser Format Support Checker

+ + {#if isChecking} +
Checking supported formats...
+ {:else} + {#each results.sections as section} +
+

{section.name}

+

{section.description}

+ {#if section.note} +

{section.note}

+ {/if} +
    + {#each section.results as format} + {#if format.type === 'boolean'} +
  • + {format.name} +
    +
    + + {format.supported ? 'Supported' : 'Not Supported'} + +
    +
  • + {:else if format.type === 'number' && typeof format.supported === 'number'} +
  • + {format.name} +
    +
    + + {format.supported}/{format.total} + +
    +
  • + {/if} + {/each} +
+
+ {/each} + {/if} +
+
diff --git a/src/routes/(normal)/supports/codecs.js b/src/routes/(normal)/supports/codecs.js new file mode 100644 index 0000000..12d3340 --- /dev/null +++ b/src/routes/(normal)/supports/codecs.js @@ -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; +} \ No newline at end of file