- commit
- 8e8749fe97c262665e0b603432e706a26c04424f
- parent
- 76ec14f51f05fd48e3e01c656915c1a18f1dcd8e
- author
- Dami <dami.etoile@gmail.com>
| M | portal/ | 7 | ++ |
| M | portal/ | 1 | + |
| M | portal/ | 6 | + |
| M | portal/ | 10 | ++- |
| M | portal/ | 34 | ++++++++- |
| M | scripts/ | 16 | ++++ |
| M | src-tauri/ | 126 | ++++++++++++++++++++++++++++-- |
@@ -35,3 +35,10 @@ test('raw invitation bytes stay in native commands instead of the DOM', () => {
3535 assert.match(main, /portal_import_invitation_file/);3636 assert.doesNotMatch(main, /readAsText|FileReader/);3737 });38+39+test('forced JPEG development mode is visibly labeled without bypassing enrollment', () => {40+ assert.match(html, /id="dev-jpeg-badge"[^>]*>dev JPEG forced</);41+ assert.match(main, /if \(mode\.force_jpeg\)/);42+ assert.match(main, /getElementById\('dev-jpeg-badge'\)/);43+ assert.match(main, /if \(!mode\.enabled\) return;/);44+});@@ -135,6 +135,7 @@
135135 <div class="topbar">136136 <span class="brand">portal</span>137137 <span class="dev-badge hidden" id="dev-connect-badge" title="Skips passkey identity lookup. This is not client authorization.">dev direct-node · not auth</span>138+ <span class="dev-badge hidden" id="dev-jpeg-badge" title="Development JPEG compatibility mode is forced.">dev JPEG forced</span>138139 <span class="sep">·</span>139140 <div class="status-line" id="status-line">140141 <span class="dot" id="status-dot"></span>@@ -261,6 +261,12 @@ async function cancelInvitation() {
261261 async function checkDevelopmentConnectionMode() {262262 const mode = await invoke('development_connection_mode');263263 connectionState.setDevelopmentMode(mode.enabled);264+ if (mode.force_jpeg) {265+ const jpegBadge = document.getElementById('dev-jpeg-badge');266+ jpegBadge.classList.remove('hidden');267+ jpegBadge.title = mode.jpeg_warning;268+ console.warn('[development JPEG compatibility mode]', mode.jpeg_warning);269+ }264270 if (!mode.enabled) return;265271 266272 const badge = document.getElementById('dev-connect-badge');@@ -38,8 +38,14 @@ export async function detectAndPublishVideoDeliveryMode({
3838 3939 const available = await probeH264WebCodecsSupport({ VideoDecoder, logger });4040 try {41- await invokeCommand('set_webcodecs_available', { available });42- return available;41+ const effectiveAvailable = await invokeCommand('set_webcodecs_available', { available });42+ if (typeof effectiveAvailable !== 'boolean') {43+ throw new TypeError('native video delivery publication returned an invalid mode');44+ }45+ if (available && !effectiveAvailable) {46+ logger.warn('development JPEG compatibility mode forced; WebCodecs probe passed but is not selected');47+ }48+ return effectiveAvailable;4349 } catch (error) {4450 logger.error('could not publish WebCodecs capability; using JPEG fallback:', error);4551 return false;@@ -61,7 +61,10 @@ test('awaits the probe before publishing the exact raw delivery mode', async ()
6161 }6262 const pending = detectAndPublishVideoDeliveryMode({6363 VideoDecoder: DelayedDecoder,64- invokeCommand: async (...args) => { calls.push(args); },64+ invokeCommand: async (...args) => {65+ calls.push(args);66+ return true;67+ },6568 });6669 6770 await Promise.resolve();@@ -74,6 +77,35 @@ test('awaits the probe before publishing the exact raw delivery mode', async ()
7477 ]);7578 });7679 80+test('keeps the real probe but obeys native forced-JPEG publication', async () => {81+ const calls = [];82+ const warnings = [];83+ class SupportedDecoder {84+ static async isConfigSupported(config) {85+ calls.push(['probe', config]);86+ return { supported: true, config };87+ }88+ }89+90+ assert.equal(await detectAndPublishVideoDeliveryMode({91+ VideoDecoder: SupportedDecoder,92+ invokeCommand: async (...args) => {93+ calls.push(args);94+ return false;95+ },96+ logger: { warn: (...args) => warnings.push(args), error() {} },97+ }), false);98+ assert.deepEqual(calls, [99+ ['probe', {100+ codec: 'avc1.64001f',101+ optimizeForLatency: true,102+ }],103+ ['set_webcodecs_available', { available: true }],104+ ]);105+ assert.equal(warnings.length, 1);106+ assert.match(warnings[0][0], /JPEG compatibility mode forced/);107+});108+77109 test('keeps frontend on JPEG when capability publication fails', async () => {78110 const errors = [];79111 class SupportedDecoder {@@ -124,6 +124,22 @@ cargo test --locked -p portal --release \
124124 commands::state::tests::ordinary_release_excludes_direct_node_bypass \125125 -- --exact126126 127+cargo test --locked -p portal --release \128+ commands::state::tests::rejects_force_jpeg_when_debug_mode_is_disabled \129+ -- --exact130+131+cargo test --locked -p portal --release \132+ commands::state::tests::ordinary_release_excludes_force_jpeg_override \133+ -- --exact134+135+cargo test --locked -p portal --release \136+ commands::state::tests::app_state_accepts_force_jpeg_only_in_debug_builds \137+ -- --exact138+139+cargo test --locked -p portal --release --features demo-direct-node \140+ commands::state::tests::app_state_accepts_force_jpeg_only_in_debug_builds \141+ -- --exact142+127143 cargo test --locked -p portal --release --features demo-direct-node \128144 commands::state::tests::app_state_accepts_direct_node_only_in_debug_builds \129145 -- --exact@@ -264,12 +264,21 @@ pub const fn development_direct_node_available() -> bool {
264264 cfg!(any(debug_assertions, feature = "demo-direct-node"))265265 }266266 267+pub const fn development_force_jpeg_available() -> bool {268+ cfg!(debug_assertions)269+}270+267271 268272 pub struct LaunchOptions {269273 pub dev_connect_node_id: Option<iroh::PublicKey>,274+ pub dev_force_jpeg: bool,270275 }271276 272-pub fn parse_launch_options<I, S>(args: I, allow_dev_connect: bool) -> Result<LaunchOptions, String>277+pub fn parse_launch_options<I, S>(278+ args: I,279+ allow_dev_connect: bool,280+ allow_force_jpeg: bool,281+) -> Result<LaunchOptions, String>273282 where274283 I: IntoIterator<Item = S>,275284 S: Into<OsString>,@@ -291,6 +300,19 @@ where
291300 let arg = arg292301 .into_string()293302 .map_err(|_| "Portal command-line arguments must be valid UTF-8".to_string())?;303+ if arg == "--dev-force-jpeg" {304+ if !allow_force_jpeg {305+ return Err(306+ "--dev-force-jpeg requires a debug build; ordinary release builds use the normal WebCodecs capability probe"307+ .to_string(),308+ );309+ }310+ if options.dev_force_jpeg {311+ return Err("--dev-force-jpeg may be specified only once".to_string());312+ }313+ options.dev_force_jpeg = true;314+ continue;315+ }294316 let node_id = if arg == "--dev-connect" {295317 Some(296318 args.next()@@ -344,6 +366,7 @@ pub struct AppState {
344366 pub client_connection_active: Arc<AtomicBool>,345367 pub webcodecs: AtomicBool,346368 pub dev_connect_node_id: Option<iroh::PublicKey>,369+ pub dev_force_jpeg: bool,347370 }348371 349372 pub type MediaControlRequestSender =@@ -445,7 +468,12 @@ impl AppState {
445468 I: IntoIterator<Item = S>,446469 S: Into<OsString>,447470 {448- parse_launch_options(args, development_direct_node_available()).map(Self::new)471+ parse_launch_options(472+ args,473+ development_direct_node_available(),474+ development_force_jpeg_available(),475+ )476+ .map(Self::new)449477 }450478 451479 fn new(options: LaunchOptions) -> Self {@@ -466,6 +494,7 @@ impl AppState {
466494 client_connection_active: Arc::new(AtomicBool::new(false)),467495 webcodecs: AtomicBool::new(false),468496 dev_connect_node_id: options.dev_connect_node_id,497+ dev_force_jpeg: options.dev_force_jpeg,469498 }470499 }471500 }@@ -477,6 +506,8 @@ pub struct DevelopmentConnectionMode {
477506 pub enabled: bool,478507 pub host_node_id: Option<String>,479508 pub warning: &'static str,509+ pub force_jpeg: bool,510+ pub jpeg_warning: &'static str,480511 }481512 482513 @@ -485,6 +516,8 @@ pub fn development_connection_mode(state: tauri::State<'_, AppState>) -> Develop
485516 enabled: state.dev_connect_node_id.is_some(),486517 host_node_id: state.dev_connect_node_id.map(|node_id| node_id.to_string()),487518 warning: "Development direct-node routing skips passkey identity lookup; it is not client authorization.",519+ force_jpeg: state.dev_force_jpeg,520+ jpeg_warning: "Development JPEG compatibility mode is forced; WebCodecs remains probed but is not selected.",488521 }489522 }490523 @@ -569,9 +602,15 @@ pub fn set_client_cursor_grab(window: tauri::WebviewWindow, grab: bool) -> Resul
569602 .map(|()| !cfg!(target_os = "macos"))570603 }571604 605+const fn effective_webcodecs_available(available: bool, force_jpeg: bool) -> bool {606+ available && !force_jpeg607+}608+572609 573-pub fn set_webcodecs_available(state: tauri::State<'_, AppState>, available: bool) {574- state.webcodecs.store(available, Ordering::SeqCst);610+pub fn set_webcodecs_available(state: tauri::State<'_, AppState>, available: bool) -> bool {611+ let effective = effective_webcodecs_available(available, state.dev_force_jpeg);612+ state.webcodecs.store(effective, Ordering::SeqCst);613+ effective575614 }576615 577616 @@ -693,14 +732,41 @@ mod tests {
693732 694733 fn parses_debug_direct_node_flag() {695734 let node_id = test_node_id();696- let options = parse_launch_options(["portal", "--dev-connect", &node_id], true).unwrap();735+ let options =736+ parse_launch_options(["portal", "--dev-connect", &node_id], true, true).unwrap();697737 698738 assert_eq!(options.dev_connect_node_id.unwrap().to_string(), node_id);699739 }700740 741+ 742+ fn parses_debug_force_jpeg_flag() {743+ let options = parse_launch_options(["portal", "--dev-force-jpeg"], true, true).unwrap();744+745+ assert!(options.dev_force_jpeg);746+ }747+748+ 749+ fn parses_combined_debug_hardware_proof_flags() {750+ let node_id = test_node_id();751+ let options = parse_launch_options(752+ [753+ "portal",754+ "--dev-connect",755+ node_id.as_str(),756+ "--dev-force-jpeg",757+ ],758+ true,759+ true,760+ )761+ .unwrap();762+763+ assert_eq!(options.dev_connect_node_id.unwrap().to_string(), node_id);764+ assert!(options.dev_force_jpeg);765+ }766+701767 702768 fn rejects_legacy_daemon_mode() {703- let error = parse_launch_options(["portal", "--daemon"], true).unwrap_err();769+ let error = parse_launch_options(["portal", "--daemon"], true, true).unwrap_err();704770 assert!(error.contains("separate sigil executable"));705771 }706772 @@ -708,7 +774,7 @@ mod tests {
708774 fn parses_equals_form() {709775 let node_id = test_node_id();710776 let flag = format!("--dev-connect={node_id}");711- let options = parse_launch_options(["portal", &flag], true).unwrap();777+ let options = parse_launch_options(["portal", &flag], true, true).unwrap();712778 713779 assert_eq!(options.dev_connect_node_id.unwrap().to_string(), node_id);714780 }@@ -716,11 +782,19 @@ mod tests {
716782 717783 fn rejects_direct_node_when_debug_mode_is_disabled() {718784 let node_id = test_node_id();719- let error = parse_launch_options(["portal", "--dev-connect", &node_id], false).unwrap_err();785+ let error =786+ parse_launch_options(["portal", "--dev-connect", &node_id], false, true).unwrap_err();720787 721788 assert!(error.contains("debug build or the explicit demo-direct-node feature"));722789 }723790 791+ 792+ fn rejects_force_jpeg_when_debug_mode_is_disabled() {793+ let error = parse_launch_options(["portal", "--dev-force-jpeg"], true, false).unwrap_err();794+795+ assert!(error.contains("requires a debug build"));796+ }797+724798 725799 fn ordinary_release_excludes_direct_node_bypass() {726800 if cfg!(not(debug_assertions)) && cfg!(not(feature = "demo-direct-node")) {@@ -736,6 +810,28 @@ mod tests {
736810 assert_eq!(result.is_ok(), development_direct_node_available());737811 }738812 813+ 814+ fn ordinary_release_excludes_force_jpeg_override() {815+ if cfg!(not(debug_assertions)) {816+ assert!(!development_force_jpeg_available());817+ }818+ }819+820+ 821+ fn app_state_accepts_force_jpeg_only_in_debug_builds() {822+ let result = AppState::from_args(["portal", "--dev-force-jpeg"]);823+824+ assert_eq!(result.is_ok(), development_force_jpeg_available());825+ }826+827+ 828+ fn forced_jpeg_publication_keeps_native_and_frontend_mode_aligned() {829+ assert!(effective_webcodecs_available(true, false));830+ assert!(!effective_webcodecs_available(true, true));831+ assert!(!effective_webcodecs_available(false, false));832+ assert!(!effective_webcodecs_available(false, true));833+ }834+739835 740836 fn app_state_starts_without_owned_transport_connections() {741837 let state = AppState::default();@@ -750,12 +846,12 @@ mod tests {
750846 751847 fn rejects_missing_invalid_and_duplicate_node_ids() {752848 assert!(753- parse_launch_options(["portal", "--dev-connect"], true)849+ parse_launch_options(["portal", "--dev-connect"], true, true)754850 .unwrap_err()755851 .contains("requires an iroh node ID")756852 );757853 assert!(758- parse_launch_options(["portal", "--dev-connect", "not-a-node-id"], true)854+ parse_launch_options(["portal", "--dev-connect", "not-a-node-id"], true, true)759855 .unwrap_err()760856 .contains("Invalid iroh node ID")761857 );@@ -771,6 +867,16 @@ mod tests {
771867 &node_id,772868 ],773869 true,870+ true,871+ )872+ .unwrap_err()873+ .contains("only once")874+ );875+ assert!(876+ parse_launch_options(877+ ["portal", "--dev-force-jpeg", "--dev-force-jpeg"],878+ true,879+ true,774880 )775881 .unwrap_err()776882 .contains("only once")