Skip to content

feat(native): add useVideoStream hook — native camera streaming for Android & iOS#239

Open
mayankmahavar1mg wants to merge 13 commits into
mainfrom
feature/video_hook_android_ios
Open

feat(native): add useVideoStream hook — native camera streaming for Android & iOS#239
mayankmahavar1mg wants to merge 13 commits into
mainfrom
feature/video_hook_android_ios

Conversation

@mayankmahavar1mg
Copy link
Copy Markdown
Collaborator

Summary

Adds useVideoStream, a native camera streaming hook for Catalyst-Core. Native CameraX (Android) and AVCaptureSession (iOS) render behind a transparent WebView — native owns all hardware and detection, web owns all UI and business logic. The bridge is a minimal event/command contract.

What's included

Android

  • NativeCameraManager — orchestrates CameraX session lifecycle, overlay, and viewfinder
  • CameraSessionManager — binds use cases (preview + image analysis), handles FPS, flip, format changes via cleanupForRebind pattern
  • BarcodeDetector — ML Kit QR/barcode scanning with auto-zoom, format filter (qr/barcode/all), and close-before-rebuild resource safety
  • ZoomController — smooth ratio-based zoom (multiplier API: 1x/2x), emits ON_ZOOM_CHANGED with {zoomLevel, minZoom, maxZoom}
  • TorchControllerListenableFuture-based torch toggle, fires onTorchChanged only on hardware confirmation
  • HoldController — QR hold state machine with AtomicReference TOCTOU fix
  • VideoStreamStateMachine@Synchronized state transitions with out-of-lock listener notification (deadlock safe)
  • BridgeUtils — JSON detection heuristic to prevent double-encoding; U+2028/U+2029 escaping for V8 safety
  • NativeBridge.kt/.jsstartVideoStream/stopVideoStream, setZoom, setTorch, flip, sendCommand
  • 1080p ResolutionSelector, debug viewfinder drawable, activity_main.xml overlay layout

iOS

  • CameraSessionManager — AVCaptureSession management with paramsLock for cross-thread property safety, beginConfiguration/commitConfiguration FPS batching, previewLayer race fix
  • BarcodeDetector — AVFoundation barcode scanning with .qr fallback safety
  • ZoomController — KVO-based zoom with #keyPath, deinit observer cleanup, duplicate-attach guard, main-thread dispatch
  • TorchControllerlockForConfiguration torch with onTorchChanged(false) on failure
  • HoldController — serial DispatchQueue for thread-safe hold state
  • NativeCameraManagerObservableObject conformance, @StateObject ownership in ContentView
  • NativeBridge.swift — full command bridge with jsonStringToDict() for Android-compatible param encoding
  • CameraPreviewView — synchronous updateUIView (no async dispatch)
  • WebView / WebViewNavigationDelegate — navigation auto-stop, user-scalable=no viewport (blocks WebKit pinch-zoom)
  • VideoStreamState + VideoStreamStateMachine — shared state enum with SwiftUI-safe transitions

JS Bridge

  • hooks.jsuseVideoStream hook: start()/stop()/sendCommand(), streamState, event subscriptions (onQrDetected, onZoomChanged, onTorchChanged)
  • NativeBridge.jssetZoom input validation, setTorch boolean coerce
  • NativeInterfaces.jsVIDEO_STREAM interface constants

Other

  • mcp_v2: removed dead syncCatalystDocs + 5 helper functions; fixed 20 pre-existing ESLint errors across 9 files

Key design decisions

Decision Choice
Layer model Native renders behind transparent WebView
Zoom API Multiplier float (1x, 2x) not percentage
Bridge encoding JS always JSON.stringify params; Swift parses string; Android reads string
iOS ownership cameraManager as @StateObject, not weak var
Torch timing Fire onTorchChanged inside ListenableFuture callback, not speculatively
Deadlock prevention VideoStreamStateMachine notifies listeners outside @Synchronized lock
Double-encode fix BridgeUtils.notifyWeb detects JSON strings, injects raw

Test coverage

  • Tested on real Android device (all CR + CR2 cycles)
  • Tested on real iOS device — iPhone X (A11) — all CR + CR2 cycles
  • Lint: zero ESLint errors

Commits

Hash Message
db0d12c feat(native): add useVideoStream hook — Android POC
b3b7d56 feat(native): Phase 2 + refactor — streamState/sendCommand, zoom multiplier, camera/ package
f4879b6 feat(native): nav auto-stop, format filter, FPS, showQrDetected overlay
3960084 feat(native/android): smooth auto-zoom + ResolutionFilter
b3ae81d fix(native/android): restore plain string passing in notifyWeb
41b5cbd feat(native/ios): add useVideoStream iOS implementation with CR fixes
587ccf1 fix(native/android): address all CodeRabbit CR findings
1bc37b4 fix(native/android): address all CodeRabbit CR2 findings
7dd49d5 fix(native/ios): address all CodeRabbit CR2 findings
ba73bc6 fix(lint): resolve all pre-existing ESLint errors

mayankmahavar1mg and others added 11 commits April 6, 2026 14:55
…n (Android POC)

Implements a native video streaming hook for Catalyst-Core that renders a CameraX
preview behind a transparent WebView — no getUserMedia required.

Native (Android):
- NativeCameraManager: CameraX session lifecycle, ML Kit barcode scanning,
  pinch-to-zoom via ScaleGestureDetector, FILL_CENTER coordinate mapping for
  accurate QR-to-screen projection, viewfinder region filtering (full box containment)
- startVideoStream / stopVideoStream bridge commands wired into NativeBridge
- Camera permission flow handled inside start(), result delegated via onPermissionResult
- WebView zoom disabled to prevent conflict with native pinch gesture
- Debug overlays: red viewfinder border + green barcode box + inside/outside status label
- PreviewView added to activity_main.xml (INVISIBLE by default, shown on stream start)

JS Bridge:
- START_VIDEO_STREAM / STOP_VIDEO_STREAM commands and ON_VIDEO_STREAM_READY /
  ON_VIDEO_STREAM_STOPPED / ON_QR_DETECTED callbacks added to NativeInterfaces.js
- nativeBridge.videoStream.start/stop added to NativeBridge.js util
- useVideoStream hook: accepts { onQRDetected } callback, exposes isStreaming,
  viewfinderRef (auto-computes physical px rect with dpr scaling), start, stop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…multiplier, camera/ package

- Refactored NativeCameraManager into camera/ package (8 files):
  VideoStreamStateMachine, CameraSessionManager, BarcodeDetector,
  HoldController, ZoomController, TorchController, ViewfinderMapper,
  VideoStreamState
- Added 1080p ResolutionSelector (ImageAnalysis only), ML Kit auto-zoom
  with zoom-only-up guard
- Fixed BridgeUtils transport layer (notifyWeb/notifyWebJson)
- Implemented QR hold state machine (suppress flag, 200ms, no flicker)
- Replaced setZoom/setTorch/onTorchChanged with streamState + sendCommand API
- Migrated zoom API from percentage (0-100) to multiplier (1.0x/2.0x);
  ON_ZOOM_CHANGED payload: { zoomLevel, minZoom, maxZoom }
- Added setTorch/flip/notifyTorchChanged bridge commands
- Updated hooks.js useVideoStream and NativeInterfaces.js constants

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ay, flicker fix

- Navigation auto-stop: CustomWebview onPageStarted lambda → cameraManager.stop()
- Format filter: qr/barcode/all wired through start() options JS→NativeBridge→NativeCameraManager
- FPS via sendCommand('fps', {min, max}) — Camera2Interop on Preview.Builder, triggers rebind
- Flip/reveal animations removed — CameraSessionManager stripped to bare bind/unbind
- viewfinderRef disabled (useRef declared, logic commented out pending re-enable)
- showQrDetected: debugBarcodeOverlay repurposed — green=QR in frame, orange=ON_QR_DETECTED
  200ms debounce hide, state machine resets on HOLD→STREAMING/STOPPING/IDLE
- Overlay flicker fix: overlayVisible flag skips redundant layout updates on normal frames,
  overlayPaintedOrange tracks painted color for green→orange repaint, lastBarcodeScreenRect
  cached for zoom-triggered repaints, WebView offset subtraction removed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…view resolution

ZoomController: ValueAnimator 300ms DecelerateInterpolator, 2-frame debounce
(SUGGESTION_CONFIRM_FRAMES=2, SUGGESTION_TOLERANCE=0.1f) before committing to
a zoom target. Cancel-and-restart on new mid-animation suggestion.
cancelZoomAnimation() called on detachCamera().

CameraSessionManager: switched Preview from ResolutionStrategy to ResolutionFilter
sorting by descending pixel count — HAL picks true maximum (1920x1080 on OnePlus 11
with dual-stream, vs 1640x1232 with ResolutionStrategy). Log.d reports negotiated
resolution on every bind.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… JSON.parse

JSON.parse("GRANTED") fails with SyntaxError because the outer double-quotes
are JS string delimiters, leaving the parser with bare `GRANTED` (not valid JSON).
JSONObject.quote() already produces a valid JS string literal — pass it directly.

Fixes requestNotificationPermission, checkNotificationPermissionStatus, and any
other plain-string bridge callback (CAMERA_PERMISSION_STATUS, etc.).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 9 Camera/ Swift files: VideoStreamState/StateMachine, HoldController,
  TorchController, ZoomController, BarcodeDetector, CameraSessionManager,
  NativeCameraManager, CameraPreviewView
- Wire NativeBridge.swift with full command dispatch (start/stop/setZoom/
  setTorch/flip), JSON param parsing via jsonStringToDict()
- ZStack layout in ContentView: CameraPreviewView behind WebViewContainer
- WebViewNavigationDelegate auto-stops camera on navigation
- CatalystConstants: add all video stream event/command constants
- Fix isActive data race: reads _state under NSLock in VideoStreamStateMachine
- Fix WeakListenerBox: weak listener array + removeListener() + nil-compact
- Fix EventBox/ErrorBox: NSLock + call()/replace() thread-safe API
- Fix rewireEvents/rewireError: use .replace() instead of direct assignment
- Fix current* data races in CameraSessionManager: paramsLock NSLock
- Fix ZoomController: KVO on videoZoomFactor fires onZoomChanged during ramp
- Fix pinch-to-zoom: user-scalable=no in viewport meta (NativeBridge.js)
- Fix FPS change: wrap in beginConfiguration/commitConfiguration to avoid
  session graph rebuild
- Fix bridge params: jsonStringToDict() parses JSON.stringify'd string from JS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread-safety: CopyOnWriteArrayList + @synchronized transition() in
VideoStreamStateMachine; @volatile on HoldController.lastDetectedValue,
resumeRunnable, BarcodeDetector.suppressResults; NativeCameraManager
overlay/viewfinder fields all @volatile; runOnUiThread for
setZoom/setTorch/flip bridge calls; stateMachine.transition + onStopped
moved into runOnUiThread in stop().

Resource/memory safety: BarcodeDetector closes existing scanner before
rebuild + addOnFailureListener; TorchController fires onTorchChanged via
ListenableFuture callback (not speculatively); NativeCameraManager
cancels overlayHideHandler in cleanup(); CameraSessionManager guards
start() against double-call while cameraProvider != null.

Null/encoding safety: NativeBridge saves and restores original WebView
background color instead of hardcoding Color.WHITE; BridgeUtils escapes
U+2028/U+2029 in JSON before evaluateJavascript.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VideoStreamStateMachine: notify listeners outside lock (deadlock fix) + duplicate listener guard
- BridgeUtils: fix double-JSON encoding — detect JSON strings, inject raw (with U+2028/U+2029 escaping)
- ViewfinderMapper: guard imageWidth/imageHeight == 0 + fix KDoc
- TorchController: use getMainExecutor via Context, fire onTorchChanged only on Future success
- BarcodeDetector: fix stale activeScanner capture using local newScanner var
- HoldController: fix TOCTOU race with AtomicReference on lastDetectedValue
- CameraSessionManager: null bindToLifecycle guard + cleanupForRebind helper + fpsMin<=fpsMax validation
- NativeCameraManager: safe-cast layoutParams
- NativeBridge.kt: null nativeCameraManager guard in permission handler
- NativeBridge.js: setTorch !!on coerce + setZoom input validation
- hooks.js: fps min<=max validation in sendCommand

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ZoomController: deinit KVO observer + duplicate attachDevice guard + #keyPath + super.observeValue + main-thread dispatch for onZoomChanged
- CameraSessionManager: clarify FPS assignment comments + fix previewLayer race (capture to oldPreview local) + fix misleading nil-session log
- HoldController: serial DispatchQueue for thread safety + rename HOLD_DURATION to holdDuration
- ContentView: cameraManager as @StateObject (NativeCameraManager now ObservableObject)
- CameraPreviewView: remove async dispatch from updateUIView
- NativeBridge: nil cameraManager guard logs for stop/flip/handleVideoStreamCommand
- BarcodeDetector: .qr fallback safety check against supported formats list
- NativeCameraManager: fix optional-chaining side-effect in detectionHandler
- TorchController: call onTorchChanged(false) on lockForConfiguration failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mcp_v2/setup.js: remove dead syncCatalystDocs function and its unused helpers (fetchUrl, parseSitemapUrls, stripHtml, contentHash, SITEMAP_URL, https, crypto imports)
- mcp_v2/mcp.js: convert inner function declaration to const arrow to fix no-inner-declarations
- mcp_v2/tools/config.js: remove unused config param from _buildResult destructure
- mcp_v2/tools/debug.js: fix _score destructure rename in fallback map
- mcp_v2/tools/knowledge.js: suppress intentional github_files omit-destructure, fix empty catch blocks
- mcp_v2/tools/tasks.js: remove unused fetcherFiles assignment, suppress projectRoot param on deriveFilesTouched
- mcp_v2/tools/conversion.js: add /* ignore */ to intentional empty catch blocks
- mcp_v2/lib/helpers.js: add /* ignore */ to intentional empty catch block
- src/native/bridge/useBaseHook.js: add isNative to useCallback dependency array

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@deputydev-agent
Copy link
Copy Markdown

DeputyDev will no longer review pull requests automatically.To request a review, simply comment #review on your pull request—this will trigger an on-demand review whenever you need it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant