Extracting all camera functionality out of mob core into this plugin (Wave 2),
following the mob_location template. Staged across turns.
Status
- [x] Stage 1a — package + Elixir + manifest (this commit):
mix.exs,lib/mob_camera.ex(fullMobCameraAPI: capture_photo/video, preview/1 (later descoped — see below; the shipped API is start_preview/stop_preview with the view staying in core), start/stop_preview, start/stop_frame_stream, frame_stream_opts — parity with the oldMob.Camera+Mob.UI.camera_preview),src/mob_camera_nif.erl(6 NIF stubs),priv/mob_plugin.exs(manifest spec). - [~] Stage 1b — native sources moved (in progress):
- [x] iOS NIF
priv/native/ios/mob_camera_nif.m(609 lines, self-contained: capture + shared session + preview control + vImage frame stream +:camerapermission via registry;mob_send2→cam_send2,mob_root_vc→cam_root_vc,g_preview_sessionleft non-static for Swift). Brace-balanced, no residual core-helper refs. - [x] iOS Swift preview
priv/native/ios/MobCameraPreviewView.swift+mob_camera_shim.h(theextern g_preview_sessionbridging decl). - [x] Android zig
priv/native/jni/mob_camera_nif.zig(self-contained, location pattern:MobCameraBridgejclass + 6 method IDs +nativeDeliverCameraFrame- 6 NIFs + entry).
- [x] Android Kotlin
priv/native/android/MobCameraBridge.kt— drafted self-contained (MobPermissionProvider:camera→CAMERA + MobActivityAware): capture via a headlessCameraResultFragment(TakePicture/CaptureVideo) so it needs no MainActivity changes; frame stream state +deliverFrame+ the ARGB→RGB-f32 / BGRA conversion helpers; nativeRegister + the deliver thunks. - [ ] zig: add 2 capture deliver thunks the Kotlin calls —
nativeDeliverCameraFile(pid, kind, path)→{:camera, :photo|:video, %{path}}andnativeDeliverCameraCancelled(pid)→{:camera, :cancelled}. Both read jstrings (use jniGetStringUTFChars; core wraps it in mob_zig.zig ~585). location's thunks passed only primitives, so this is new there.
- [x] iOS NIF
Net-new plugin-system capabilities this extraction needs (the bt-style wall)
The pure-NIF parts (capture, frame stream, :camera permission) extract cleanly
like location. The live preview does not — it needs capabilities that don't
exist yet, exactly like the bt extraction needed two:
- iOS: plugin-swift bridging header so
MobCameraPreviewView.swiftsees the.m'sg_preview_session(mob_camera_shim.h), AND converting the core-builtin:camera_previewrenderer node (MobNode.h.cameraPreviewcameraFacing; MobRootView.swift:442) into a plugin native-view component.
- Android: a plugin Compose native-view for the preview (the
MobCameraPreviewcomposable at MobBridge.kt ~2657) bound to this bridge's observable state (it owns the CameraX binding + the frame analyzer). The tier-2 component path (signature_pad) is the seam, but the preview view also drives the analyzer off plugin-owned state, which is a new shape.
Recommendation: ship a first verifiable mob_camera WITHOUT the live-preview
component — capture_* + start/stop_frame_stream are pure NIFs (+ the Fragment)
and extract cleanly; defer preview/1 + the camera_preview component to a
follow-up once the plugin native-view-bound-to-state capability lands. Then the
Stage-2 strip removes the camera NIFs + capture + the :camera permission half
(keeping :microphone), but LEAVES :camera_preview (MobNode node + renderer +
MobCameraPreview) in core until the component capability exists.
- [ ] Stage 2 — strip core + mob_new templates.
- [ ] Stage 3 — device-verify iPhone + Moto G (capture, preview, frame
stream,
:camerapermission via registry), parity before/after. - [ ] Stage 4 — contract tests, docs, CHANGELOG, mob_new wizard opt-in.
Native extraction checklist (Stage 1b) — source: core survey
iOS — priv/native/ios/mob_camera_nif.m (extract, make self-contained)
From mob/ios/mob_nif.m:
- Permission branch (lines 2357–2365) — the camera half only (
AVMediaTypeVideo). MobCameraDelegate+ capture (2424–2526): UIImagePickerController photo/video.- Shared session + preview (2528–2643):
g_preview_session,mob_camera_ensure_session, start/stop_preview. - Frame stream (2645–2948):
MobFrameDelegate, vImage resize + BGRA→RGB f32, start/stop_frame_stream. - NIF table entries (6716–6721).
Self-contained adaptations (core helpers are private statics — not linkable):
mob_send2/mob_send3→ ownenif_sendhelpers (cf.mob_location_nif.msend_permission).mob_root_vc()→UIApplication.sharedApplication.keyWindow.rootViewController(or the connected-scene equivalent) directly.extern void mob_register_permission_handler(const char *, void (*)(ErlNifPid));— register"camera"in theloadcallback (core exports it; same static binary).ERL_NIF_INIT(mob_camera_nif, …, load, …).
iOS — priv/native/ios/MobCameraPreviewView.swift
From mob/ios/MobRootView.swift (899–975): CameraPreviewUIView + MobCameraPreviewView (UIViewRepresentable) + the MobCameraSessionChanged observer. Register it under the native view key "MobCamera_PreviewView" (the ui_components entry). It references the .m's g_preview_session global — declare extern AVCaptureSession *g_preview_session; (the .m must export it, not static).
Android — priv/native/jni/mob_camera_nif.zig
From mob/android/jni/mob_nif.zig: bridge method IDs (248–253), mob_deliver_camera_frame (2328–2365), 6 NIF impls (2607–2691), method caching (3548–3553), NIF table (3683–3688). Self-register the Kotlin bridge class via MobCameraBridge (cf. MobLocationBridge).
Android — priv/native/android/MobCameraBridge.kt
The CameraX impl (TakePicture/CaptureVideo activity contracts, ImageAnalysis + ARGB→BGRA repack, FileProvider URI). Currently lives in the mob_new host templates / host MainActivity, not core — move it here. Implements MobPermissionProvider (maps :camera → Manifest.permission.CAMERA) and registers via MobPluginBootstrap.
Entanglement decisions
- Microphone stays in core. The iOS permission branch handles camera + mic
together; mic is needed by
Mob.Audio(recording, stays in core). Split: core keeps the:microphone→AVMediaTypeAudiobranch; this plugin owns:cameraonly (and declares theRECORD_AUDIO/NSMicrophone…manifest entries for video — harmless set-union with core/mob_audio). - Scanner/CameraX coupling deferred.
mob_scanner(core, Wave 3) shares the CameraX gradle deps. This plugin declares its own CameraX deps; gradle dedups, so the in-core scanner keeps working. Extract scanner later, pointing atmob_camera. - FileProvider +
<uses-feature>(Android). These are AndroidManifest fragments the plugin manifest can't yet contribute (only<uses-permission>- gradle deps). Stage-2 decision: add a manifest-fragment capability to the
plugin system, or keep them in the host template gated on
mob_camera.
- gradle deps). Stage-2 decision: add a manifest-fragment capability to the
plugin system, or keep them in the host template gated on
Precedent
Breaking, no compatibility shim — matches the mob_location extraction
(core provided no location shim). Apps using Mob.Camera.* add
{:mob_camera, "~> 0.1"} and call MobCamera.*.