Skip to content

macOS: ask for camera access instead of hanging; custom apps keep score as main executable, settings in Info.plist - #2354

Open
jcelerier wants to merge 2 commits into
masterfrom
fix/macos-camera-permission
Open

jcelerier wants to merge 2 commits into
masterfrom
fix/macos-camera-permission

Conversation

@jcelerier

@jcelerier jcelerier commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Custom apps built with tools/create-app.sh, LivePose among them, hang on macOS as soon as they start a camera, and the user is never asked for camera access. Two commits fix the two causes.

1. camera: ask for camera access on macOS instead of hanging

Score.play() → DocumentPlugin::reload() → start_execution() → CameraInput::start() → avformat_open_input("avfoundation"), on the GUI thread. Without camera access, libavdevice waits in get_video_config() for a first frame that AVFoundation never delivers. That loop ignores the interrupt callback, so CameraInput's 2 s timeout can't fire. (FFmpeg side: ossia/sdk#50, merged.)

CameraInput::start() now checks AVCaptureDevice authorizationStatusForMediaType: first (Video/CameraPermission.{hpp,mm}, with a stub elsewhere):

  • authorized: opens as before;
  • denied / restricted: fails right away, with a warning pointing to System Settings › Privacy & Security › Camera;
  • not determined: calls requestAccessForMediaType: and returns. If the user agrees, the answer (delivered on the main queue) starts the camera. A stop() or the input's destruction while the prompt is up cancels that start.

If the bundle has no NSCameraUsageDescription, the old behaviour is kept: asking would get the process killed.

2. macOS custom apps: keep score as the main executable, settings in Info.plist

Why custom apps never got the prompt. create-app-macos.sh renamed score to app-bin and made CFBundleExecutable a bash script that exec'd it. A bundle's signature, with its identifier and Info.plist, belongs to its main executable, here the script. app-bin was signed as a separate helper (ad-hoc identifier app-bin-<hash>). When it asked for the camera, TCC logged:

kTCCServiceCamera requires entitlement com.apple.security.device.camera but it is missing
  for requesting={identifier=app-bin-5555494441a1…, binary_path=…/Contents/MacOS/app-bin}
Policy disallows prompt for Sub:{ca.qc.sat.camtestold} Resp:{identifier=app-bin-5555494441a1…}
  access to kTCCServiceCamera denied

app-bin's signature does contain that entitlement. Access is refused silently, and then the wait from part 1 never ends.

Now: score itself is the bundle's main executable, renamed to the app's name. What the script did comes from the bundle's Info.plist, read at the very start of main() (custom_app::setup):

  • LSEnvironment, the standard key for a bundle's environment. macOS applies it only when the app is opened through Launch Services (Finder, Dock, open), and caches it. So score sets the variables again in every case, replacing $RESOURCES_DIR / ${RESOURCES_DIR} with Contents/Resources. It holds the SCORE_CUSTOM_APP_* variables, QML2_IMPORT_PATH and the app's environment file.
  • ScoreArguments, an array put before the command-line arguments: [--autoplay] --ui $RESOURCES_DIR/qml/<main>.qml [$RESOURCES_DIR/<doc>.score]. --debug on the command line still turns --ui into --ui-debug.

create-app-macos.sh:

  • Refuses a score whose Info.plist lacks the new ScoreCustomAppVersion = 1 (added to src/app/Info.plist.in), instead of producing an app that starts as plain score. All our apps package continuous or a fresh master build.
  • Writes both keys with plutil. The environment file is converted at packaging time, in the syntax the Windows launcher already reads: [export] KEY=VALUE, optional quotes, #/; comments. Nothing runs it through a shell any more, so anything a shell would have expanded ($HOME, backticks), or a line that isn't an assignment, is an error with its line number. LivePose's, DomeportPro's, koaia's and Spatial Protocol Mapper's files all convert unchanged.
  • Signs the renamed main executable with the entitlements, instead of app-bin.

New option --app-info-plist FILE (input app-info-plist in ossia/actions#32): the app's own Info.plist entries, e.g. a camera description worded for the app.

  • They replace score's, except LSEnvironment, which is merged variable by variable.
  • Refused: keys that describe the binary (CFBundleExecutable, LSMinimumSystemVersion, NSPrincipalClass…) or that other options set (name, identifier, versions, icon). Also refused: emptying any usage description, since macOS kills an app that asks for a permission without one.

Tested on macmini-m1 (macOS 26.4.1, Apple Silicon)

score built from this branch against /opt/ossia-sdk-aarch64. Test apps packaged with create-app.sh --local-installer, using LivePose's environment files plus a $RESOURCES_DIR variable, signed ad hoc through the script (MAC_CODESIGN_IDENTITY=-). The script ran under the system /bin/bash 3.2. The app name contains spaces ("Cam Test Plist"). The test UI does what LivePose's Start button does: Score.createDevice() on the OBS Virtual Camera, then Score.play(), with a 500 ms heartbeat. It also logs Qt.application.arguments and imports a QML module found only through QML2_IMPORT_PATH.

launch TCC arguments received environment
open AUTHREQ_PROMPTING … subject=Sub:{ca.qc.sat.camtestplist} Resp:{identifier=ca.qc.sat.camtestplist}: prompt shown --ui …/Contents/Resources/qml/Main.qml module found; app name, organization, domain from SCORE_CUSTOM_APP_*
open --args --debug prompt shown --ui-debug …/Main.qml same
open, app with a fragment prompt shown same fragment's variables added and overriding
binary run from a terminal (macOS applies no LSEnvironment) n/a: attributed to the ssh session same module found, names from SCORE_CUSTOM_APP_*: set by score itself

In every case play() returns ("Camera: waiting for the user to allow camera access") and the heartbeat continues.

Packaging:

  • The bundle has one executable, named after the app. codesign -dv shows Identifier=ca.qc.sat.camtestplist and codesign --verify --strict passes.
  • With --score … --autoplay: ["--autoplay","--ui","$RESOURCES_DIR/qml/Main.qml","$RESOURCES_DIR/app.score"].
  • Each of these stops with a clear message: a score without ScoreCustomAppVersion (master's Info.plist.in), export PATH=$HOME/bin:$PATH in the environment file, a fragment setting CFBundleIdentifier, a fragment emptying NSMicrophoneUsageDescription.

Not tested:

  • clicking "Allow", which needs someone at the Mac, so the "start once granted" path is reviewed but not exercised;
  • a real signing identity and notarization;
  • Intel Macs.

🤖 Generated with Claude Code

@coveralls

coveralls commented Oct 19, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 48.86% (-0.3%) from 49.148% — fix/macos-camera-permission into master

jcelerier and others added 2 commits October 21, 2026 14:47
Opening an avfoundation camera the user has not allowed blocks forever in
libavdevice, which waits for a first frame that AVFoundation never delivers --
on the GUI thread, since devices start when playback starts. CameraInput now
checks the authorization first: denied fails right away with a hint, and
undetermined asks the user and opens the device once they agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o.plist

create-app-macos.sh replaced the bundle's main executable with a bash script
exec'ing a renamed score. macOS only shows privacy prompts (camera, microphone)
for the bundle's main executable, so custom apps had camera access refused
without a prompt, and LivePose hung waiting for frames.

score now stays the main executable. What the script set comes from the bundle's
Info.plist, read at the start of main():
- LSEnvironment, the standard key for a bundle's environment. macOS applies it
  only when the app is opened through Launch Services and caches it, so score
  sets it again, expanding $RESOURCES_DIR to Contents/Resources;
- ScoreArguments, inserted before the command-line arguments; --debug still
  turns --ui into --ui-debug.

The environment file is converted at packaging time, in the syntax the Windows
launcher already reads (KEY=VALUE, export, quotes, comments); anything a shell
would have expanded is an error. --app-info-plist merges the app's own entries,
such as a usage description worded for it, over score's: binary-related and
option-set keys are refused, and so is emptying a usage description.

score's Info.plist declares ScoreCustomAppVersion = 1; the script refuses a score
without it instead of producing an app that starts as plain score.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jcelerier
jcelerier force-pushed the fix/macos-camera-permission branch from 8b3fad6 to e0d2120 Compare October 21, 2026 19:54
@jcelerier jcelerier changed the title macOS: ask for camera access instead of hanging; keep score as custom apps' main executable macOS: ask for camera access instead of hanging; custom apps keep score as main executable, settings in Info.plist Oct 21, 2026

This branch has not been deployed

No deployments
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.

2 participants