Await WebCodecs H.264 support before publishing the delivery mode to Rust. Fail closed to the existing JPEG path when support is absent, rejected, or cannot be published.
- commit
- 2cb5380110896da0b8f085a01b0d337a051f4c90
- parent
- 4f77397944a19a8f3853296095a173126b368a4d
- author
- FelineStateMachine <dami.etoile@gmail.com>
| M | portal/ | 10 | ++-- |
| A | portal/ | 47 | +++++++++++++++ |
| A | portal/ | 94 | ++++++++++++++++++++++++++++++ |
@@ -29,6 +29,7 @@ import { createWindowRuntime } from './window-runtime.mjs';
2929 import {3030 createVideoPipelineSession,3131 } from './video-pipeline.mjs';32+import { detectAndPublishVideoDeliveryMode } from './video-capability.mjs';3233 import {3334 formatAdaptiveDecision,3435 } from './adaptive-feedback.mjs';@@ -572,11 +573,10 @@ function togglePanel(force) {
572573 }573574 574575 // ─── WebCodecs detection ──────────────────────────────────────────────────────575-let hasWebCodecs = ('VideoDecoder' in window);576-577-(async function detectWebCodecs() {578- await invoke('set_webcodecs_available', { available: hasWebCodecs });579-})();576+// Finish the codec probe and publish the same delivery mode to Rust before577+// exposing any connect handlers. This keeps raw WebCodecs frames and the Rust578+// JPEG compatibility path from disagreeing during startup.579+const hasWebCodecs = await detectAndPublishVideoDeliveryMode({ invokeCommand: invoke });580580 581581 // ─── Frame decoding ───────────────────────────────────────────────────────────582582 const canvas = document.getElementById('frame-canvas');@@ -0,0 +1,47 @@
1+const H264_PROBE_CONFIG = Object.freeze({2+ // Sigil's proven encoder path currently emits High Profile, Level 3.1 AVC.3+ // Deliberately omit coded dimensions: this tests the emitted codec contract4+ // without pinning Portal's capability decision to one host resolution.5+ codec: 'avc1.64001f',6+ optimizeForLatency: true,7+});8+9+/**10+ * Report WebCodecs only when this webview can decode Sigil's current H.26411+ * format. API presence alone is insufficient on codec-stripped webviews.12+ */13+export async function probeH264WebCodecsSupport({14+ VideoDecoder = globalThis.VideoDecoder,15+ logger = console,16+} = {}) {17+ if (typeof VideoDecoder !== 'function'18+ || typeof VideoDecoder.isConfigSupported !== 'function') return false;19+20+ try {21+ const support = await VideoDecoder.isConfigSupported({ ...H264_PROBE_CONFIG });22+ return support?.supported === true;23+ } catch (error) {24+ logger.warn('WebCodecs H.264 capability probe failed:', error);25+ return false;26+ }27+}28+29+/** Publish one delivery mode to both halves of Portal before a connection. */30+export async function detectAndPublishVideoDeliveryMode({31+ invokeCommand,32+ VideoDecoder = globalThis.VideoDecoder,33+ logger = console,34+} = {}) {35+ if (typeof invokeCommand !== 'function') {36+ throw new TypeError('invokeCommand must be a function');37+ }38+39+ const available = await probeH264WebCodecsSupport({ VideoDecoder, logger });40+ try {41+ await invokeCommand('set_webcodecs_available', { available });42+ return available;43+ } catch (error) {44+ logger.error('could not publish WebCodecs capability; using JPEG fallback:', error);45+ return false;46+ }47+}@@ -0,0 +1,94 @@
1+import assert from 'node:assert/strict';2+import test from 'node:test';3+4+import {5+ detectAndPublishVideoDeliveryMode,6+ probeH264WebCodecsSupport,7+} from './video-capability.mjs';8+9+test('requires VideoDecoder and its asynchronous support probe', async () => {10+ assert.equal(await probeH264WebCodecsSupport({ VideoDecoder: undefined }), false);11+ assert.equal(await probeH264WebCodecsSupport({ VideoDecoder: class {} }), false);12+});13+14+test('reports only explicit support for Sigil current H.264 format', async () => {15+ let probedConfig = null;16+ class SupportedDecoder {17+ static async isConfigSupported(config) {18+ probedConfig = config;19+ return { supported: true, config };20+ }21+ }22+23+ assert.equal(await probeH264WebCodecsSupport({ VideoDecoder: SupportedDecoder }), true);24+ assert.deepEqual(probedConfig, {25+ codec: 'avc1.64001f',26+ optimizeForLatency: true,27+ });28+29+ class UnsupportedDecoder {30+ static async isConfigSupported(config) {31+ return { supported: false, config };32+ }33+ }34+ assert.equal(await probeH264WebCodecsSupport({ VideoDecoder: UnsupportedDecoder }), false);35+});36+37+test('fails closed when the webview rejects its H.264 support probe', async () => {38+ const warnings = [];39+ class RejectingDecoder {40+ static async isConfigSupported() {41+ throw new Error('codec support unavailable');42+ }43+ }44+45+ assert.equal(await probeH264WebCodecsSupport({46+ VideoDecoder: RejectingDecoder,47+ logger: { warn: (...args) => warnings.push(args) },48+ }), false);49+ assert.equal(warnings.length, 1);50+ assert.match(warnings[0][0], /capability probe failed/);51+});52+53+test('awaits the probe before publishing the exact raw delivery mode', async () => {54+ const calls = [];55+ let finishProbe;56+ class DelayedDecoder {57+ static isConfigSupported() {58+ calls.push('probe');59+ return new Promise((resolve) => { finishProbe = resolve; });60+ }61+ }62+ const pending = detectAndPublishVideoDeliveryMode({63+ VideoDecoder: DelayedDecoder,64+ invokeCommand: async (...args) => { calls.push(args); },65+ });66+67+ await Promise.resolve();68+ assert.deepEqual(calls, ['probe']);69+ finishProbe({ supported: true });70+ assert.equal(await pending, true);71+ assert.deepEqual(calls, [72+ 'probe',73+ ['set_webcodecs_available', { available: true }],74+ ]);75+});76+77+test('keeps frontend on JPEG when capability publication fails', async () => {78+ const errors = [];79+ class SupportedDecoder {80+ static async isConfigSupported() { return { supported: true }; }81+ }82+83+ assert.equal(await detectAndPublishVideoDeliveryMode({84+ VideoDecoder: SupportedDecoder,85+ invokeCommand: async () => { throw new Error('native command failed'); },86+ logger: { warn() {}, error: (...args) => errors.push(args) },87+ }), false);88+ assert.equal(errors.length, 1);89+ assert.match(errors[0][0], /using JPEG fallback/);90+});91+92+test('rejects a missing native publication boundary', async () => {93+ await assert.rejects(detectAndPublishVideoDeliveryMode(), TypeError);94+});