Files
lean-sdl3/WebcamApp.lean
Greg ShuflinandClaude Fable 5.1 8104955c71 Make the webcam demo run on macOS
Lean runs main on a helper thread, which Cocoa rejects for video init;
export LEAN_MAIN_USE_THREAD=0 from the justfile on macOS. Wait for the
camera permission event before reading the camera format, since SDL
grants access asynchronously.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 15:26:20 -07:00

129 lines
3.7 KiB
Lean4

import SDL
def SCREEN_WIDTH : Int32 := 1280
def SCREEN_HEIGHT : Int32 := 720
def isEscapeKeyDown : IO Bool := SDL.getKeyState SDL.SDL_SCANCODE_ESCAPE
-- SDL reports camera permission asynchronously (macOS shows a system prompt); the format and
-- frames are unavailable until the approved event arrives.
partial def waitForCameraPermission : IO Bool := do
let eventType SDL.pollEvent
if eventType == SDL.SDL_EVENT_CAMERA_DEVICE_APPROVED then
return true
if eventType == SDL.SDL_EVENT_CAMERA_DEVICE_DENIED || eventType == SDL.SDL_QUIT then
return false
if eventType == 0 then
IO.sleep 10
waitForCameraPermission
structure WebcamState where
window : SDL.SDLWindow
renderer : SDL.SDLRenderer
camera : SDL.SDLCamera
texture : Option SDL.SDLTexture
running : Bool
partial def webcamLoop (stateRef: IO.Ref WebcamState): IO Unit := do
let state stateRef.get
-- Check for quit event
let eventType SDL.pollEvent
if eventType == SDL.SDL_QUIT || ( isEscapeKeyDown) then
stateRef.modify fun s => { s with running := false }
return
-- Try to acquire a camera frame (may return none if no frame ready)
let maybeFrame SDL.acquireCameraFrame state.camera
match maybeFrame with
| none =>
-- No frame available yet, just continue
pure ()
| some cameraFrame =>
let w := cameraFrame.w.toUInt32
let h := cameraFrame.h.toUInt32
match state.texture with
| none =>
-- First frame: create the texture
let cameraTexture SDL.createTexture state.renderer cameraFrame.format SDL.SDL_TEXTUREACCESS_STREAMING w h
stateRef.modify fun s => { s with texture := some cameraTexture }
| some tx =>
-- Subsequent frames: update the texture directly from the camera frame
let _ SDL.updateTextureFromSurface tx cameraFrame
SDL.releaseCameraFrame state.camera cameraFrame
-- Clear the renderer
let _ SDL.setRenderDrawColorFloat state.renderer 0.4 0.6 1.0 SDL.SDL_ALPHA_OPAQUE_FLOAT
let _ SDL.renderClear state.renderer
-- Render the texture if we have one
let state stateRef.get
match state.texture with
| none => pure ()
| some tx =>
let _ SDL.renderTextureFullscreen state.renderer tx
pure ()
-- Present the frame
SDL.renderPresent state.renderer
-- Continue the loop if still running
let state stateRef.get
if state.running then
webcamLoop stateRef
def main : IO Unit := do
unless ( SDL.init (SDL.SDL_INIT_VIDEO ||| SDL.SDL_INIT_CAMERA)) == 1 do
IO.println "Failed to initialize SDL"
return
let (window, renderer) try
SDL.createWindowAndRenderer "WebcamTest" SCREEN_WIDTH SCREEN_HEIGHT SDL.SDL_WINDOW_SHOWN
catch sdlError =>
IO.println sdlError
SDL.quit
return
let cameraCount SDL.getCameras
IO.println s!"Camera count: {cameraCount}"
if cameraCount.isEmpty then
IO.println "No cameras found!"
SDL.quit
return
let idx := cameraCount[0]!
let camera SDL.openCamera idx
IO.println "Waiting for camera permission..."
unless ( waitForCameraPermission) do
IO.println "Camera permission denied"
SDL.quit
return
let spec SDL.getCameraFormat camera
let msg :=
let width := spec.width
let height := spec.height
let n := spec.framerateNumerator
let d := spec.framerateDenominator
s!"Framerate: {n}/{d} FPS width: {width}, height: {height}"
IO.println msg
let initialState : WebcamState := {
window := window
renderer := renderer
camera := camera
texture := none
running := true
}
let stateRef IO.mkRef initialState
IO.println "Starting webcam loop..."
webcamLoop stateRef
SDL.quit