cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix: WebCodecs decoder failure — wrong codec level + missing keyframe flag

dami etoile committed 944dccf
Root causes:
1. Hardcoded 'avc1.42001e' = Baseline Level 3.0 (max 720x576).
   1920x1080 needs Level 4.0+. Decoder rejects → EncodingError.
2. Keyframe detection was guessing (pendingKeyframe var), not using
   the actual keyframe flag from the backend.

Fixes:
- Parse SPS NAL from first H.264 packet → extract real profile/constraints/level
  → build correct codec string (e.g. 'avc1.42e028' for Baseline L4.0)
- Fallback codec: 'avc1.42e028' (Baseline L4.0, supports 1080p)
- Backend sends keyframe flag in FramePayload
- Frontend uses payload.keyframe for EncodedVideoChunk type
- Added optimizeForRealtimeUse: true
- Console logs codec string + dimensions on init
commit
944dccf22df891b58c74988c7847e175c08946d6
parent
f3bb53ff444cee450cec22957400913887f1d842
author
dami etoile <dami.etoile@gmail.com>
2 files changed, +37 -11
Msrc-tauri/src/commands.rs5++-
Msrc/ui/index.html43++++++++++++++++++++++++------
M src-tauri/src/commands.rs +3 -2
M src/ui/index.html +34 -9
@@ -813,7 +813,6 @@
813813 let hasWebCodecs = ('VideoDecoder' in window);814814 let videoDecoder = null;815815 let decoderConfigured = false;816- let pendingKeyframe = true;817816 818817 (async function detectWebCodecs() {819818 const el = document.getElementById('codec-webcodecs');
@@ -831,7 +830,31 @@
831830 }832831 })();833832 834- function initWebCodecsDecoder(width, height) {833+ function parseSpsCodec(h264Data) {834+ // Scan for SPS NAL (type 7) and extract profile/level835+ let i = 0;836+ while (i < h264Data.length - 4) {837+ if (h264Data[i] === 0 && h264Data[i+1] === 0 && h264Data[i+2] === 0 && h264Data[i+3] === 1) {838+ const nalType = h264Data[i+4] & 0x1f;839+ if (nalType === 7) { // SPS840+ const profileIdc = h264Data[i+5];841+ const constraintFlags = h264Data[i+6];842+ const levelIdc = h264Data[i+7];843+ const profileHex = profileIdc.toString(16).padStart(2, '0');844+ const constraintHex = constraintFlags.toString(16).padStart(2, '0');845+ const levelHex = levelIdc.toString(16).padStart(2, '0');846+ return `avc1.${profileHex}${constraintHex}${levelHex}`;847+ }848+ i += 4;849+ } else {850+ i++;851+ }852+ }853+ // Fallback: Baseline, Level 4.0 (supports 1080p)854+ return 'avc1.42e028';855+ }856+857+ function initWebCodecsDecoder(width, height, codecStr) {835858 if (videoDecoder) {836859 try { videoDecoder.close(); } catch(e) {}837860 }
@@ -842,16 +865,15 @@
842865 },843866 error: (e) => {844867 console.error('VideoDecoder error:', e);845- pendingKeyframe = true;846868 }847869 });848870 videoDecoder.configure({849- codec: 'avc1.42001e',871+ codec: codecStr,850872 codedWidth: width,851873 codedHeight: height,874+ optimizeForRealtimeUse: true,852875 });853876 decoderConfigured = true;854- pendingKeyframe = true;855877 }856878 857879 // ─── Frames — dual path: WebCodecs or openh264→JPEG ─────────────────
@@ -868,17 +890,20 @@
868890 if (hasWebCodecs) {869891 // Path 1: WebCodecs hardware decode870892 const h264 = Uint8Array.from(atob(data), c => c.charCodeAt(0));871- if (!decoderConfigured) initWebCodecsDecoder(width, height);893+ if (!decoderConfigured) {894+ const codecStr = parseSpsCodec(h264);895+ console.log('WebCodecs codec:', codecStr, 'w:', width, 'h:', height);896+ initWebCodecsDecoder(width, height, codecStr);897+ }872898 const chunk = new EncodedVideoChunk({873- type: pendingKeyframe ? 'key' : 'delta',899+ type: event.payload.keyframe ? 'key' : 'delta',874900 timestamp: performance.now() * 1000,875901 data: h264,876902 });877903 try {878904 videoDecoder.decode(chunk);879- pendingKeyframe = false;880905 } catch (e) {881- pendingKeyframe = true;906+ console.warn('decode failed:', e);882907 }883908 } else {884909 // Path 2: JPEG from backend (openh264 decode → JPEG encode in Rust)