Skip to main content

uvc_avf

A sudo-free stand-in for pyuvc (import uvc) on macOS.

  • Frames come from AVFoundation, through a small Swift helper process that writes each frame's luma plane into a shared-memory ring.
  • UVC controls (exposure, gain, auto-exposure mode, extension units, ...) are class requests sent with libusb on the camera's default control pipe, without claiming an interface.

Neither part needs root. The module mirrors the pyuvc calls a typical capture loop makes, so pyuvc code can switch backends with one import:

import os
if os.geteuid() == 0:
    import uvc                  # pyuvc: needs sudo on macOS
else:
    import uvc_avf as uvc       # this package: no sudo

Scope: verified with one camera (Arducam OV9281 USB Camera behind a Sonix bridge, 0c45:6366) on one Mac, see Measured results. Frames are luma (grayscale) only.

Why pyuvc needs sudo on macOS

libuvc (which pyuvc wraps) streams by claiming the camera's VideoStreaming interface through libusb. macOS's own UVC driver holds that interface. libusb can only take it by "capturing" the device through a re-enumeration, and IOKit allows that only for root or for a process that has Apple's VM device-access entitlement. No user-space workaround exists for streaming: without root, the frames have to come from macOS, which means AVFoundation.

Controls are a different matter. They are class requests on the default control pipe (endpoint 0), and macOS lets a normal user send those through libusb without claiming any interface, even while AVFoundation streams the same camera. uvc_avf combines the two paths.

What works without root

Verified 2026-09-26/27 with tools/nosudo_probe.py, run as a normal user, once with the camera idle and again while an AVFoundation session streamed it (setup under Measured results):

Operation (libusb, no interface claimed) Result
libusb_open succeeds
libusb_kernel_driver_active on interfaces 0 and 1 True for both
libusb_claim_interface(0) fails with LIBUSB_ERROR_ACCESS
Standard descriptor reads (e.g. the product string) work
UVC GET_LEN, GET_INFO, GET_CUR, GET_MIN, GET_MAX, SET_CUR on the camera terminal, processing unit and extension units work, idle and while streaming
Exposure write (SET_CUR) with read-back works, idle and while streaming
Vendor extension-unit register read (Sonix ASIC id) works, idle and while streaming

While macOS streamed, the streaming interface sat on alternate setting 6, and the committed mode (VS_COMMIT_CONTROL GET_CUR) was MJPEG 800x600 @ 120 fps with 3072 bytes per transfer.

Design

camera --USB--> macOS UVC driver --> AVFoundation --> uvc_avf_helper (Swift, child process)
  ^                                                     | copies the Y plane into a ring slot
  |                                                     | prints "frame SLOT SEQ" on stdout
  |                                                     v
  |                                         ring file, mmap'ed by both processes
  |                                                     |
  |                                                     v
  +-- UVC requests on endpoint 0 <-- uvc_avf (Python): Capture.controls, Capture.get_frame()
      (libusb, no interface claimed)

Streaming path

  1. Capture(uid) opens the device with libusb, parses the UVC descriptors (modes, unit ids, control bitmaps) and builds cap.controls. It does not stream.
  2. The first get_frame() builds the helper if needed (swiftc -O, next to the module, rebuilt when uvc_avf_helper.swift is newer than the binary), creates a temporary directory (uvc_avf_* under $TMPDIR) holding the ring file frames, and starts the helper. It waits up to 15 s (Capture.READY_TIMEOUT) for the helper's ready line, then maps the file read-only.
  3. The helper picks the AVFoundation device (see Device matching) and the AVFoundation format with the exact size and a frame-rate range whose maximum is within 1 fps of the request, preferring 420v formats (the MJPEG modes). It requests 420f output, falling back to 420v. It takes lockForConfiguration(), sets the format and both frame durations to the range's exact minFrameDuration, calls startRunning(), unlocks, and checks that the session kept the format.
  4. For each frame the capture callback copies the luma plane row by row into the next ring slot, tightly packed (no row padding). For 420f/420v that is plane 0; for 2vuy/yuvs it takes every other byte.
  5. Each get_frame() reads the pending frame lines, keeps at most cap.max_frame_lag of them (default 4; older ones are counted in frames_skipped), and copies the oldest remaining slot.
  6. stop_stream() / close() send quit on the helper's stdin. The helper also exits on stdin EOF, so it does not outlive the Python process.

Helper protocol

uvc_avf_helper --shm PATH --width W --height H --fps F [--slots N]
               [--unique-id ID] [--vidpid VVVVPPPP] [--device NAME] [--test-pattern]

Capture passes --slots 8, the computed AVFoundation uniqueID, the vendor and product id as 8 hex digits, and the USB product string.

stdout line Meaning
ready {json} Streaming started. Keys: width, height, fps, fourcc (the AVFoundation format's subtype, e.g. 420v), slots, slot_size, header_size, slot_header, file_size, device, unique_id, matched_by (unique id, vid/pid, name, or test), pid.
frame SLOT SEQ Frame number SEQ (1, 2, ...) is complete in slot SLOT = SEQ mod slots.
warn ... Non-fatal problem; the module logs it as a warning.
info ... Informational, e.g. info quit.
error ... Fatal. The helper exits right after, and the module raises the text as StreamError.

stdin: a quit line or EOF ends the helper.

Exit codes: 0 after quit/EOF; 1 ring-file error; 3 camera permission denied; 4 no matching AVFoundation device; 5 no matching format, or the session replaced it; 6 cannot open the device, add the input or output, or lock the configuration; 7 session runtime error or session stopped; 8 camera disconnected; 64 bad arguments (message on stderr).

--test-pattern skips AVFoundation completely, so it needs no camera and no permission. It publishes a scrolling gradient at the requested rate, alternating bright and dark frames about 100 grey levels apart (odd SEQ bright), with fourcc TEST.

Ring layout

A plain file mapped by both processes. Values are little-endian.

File header (4096 bytes):

Offset Type Field
0 u32 magic 0x55564146 (UVAF)
4 u32 layout version, 1
8 u32 width
12 u32 height
16 u32 number of slots
20 u32 slot size in bytes
24 u32 header size, 4096
28 u32 slot header size, 64
64 u64 SEQ of the latest published frame (= frames published)
72 u64 frames dropped by macOS (captureOutput(_:didDrop:from:) calls)

Slot k starts at 4096 + k * slot_size, where slot_size is 64 + width * height rounded up to a multiple of 4096 (77824 bytes at 320x240, 483328 bytes at 800x600).

Offset in slot Type Field
0 u64 seq: the frame's SEQ; 0 while the helper is writing the slot
8 f64 pts: the sample buffer's presentation time, seconds
16 f64 t_callback: time in the helper's capture callback, taken right after the copy
24 u32 width
28 u32 height
32 u32 pixel format of the delivered buffer (e.g. 420f; TEST in test-pattern mode)
64 u8[width * height] luma plane, rows tightly packed

Times are mach_absolute_time seconds, the same clock as Python's time.monotonic() on macOS (time.get_clock_info("monotonic").implementation reports mach_absolute_time(); checked on Python 3.9.6 and 3.11.12).

Write order in the helper (a seqlock): set seq to 0, memory barrier, copy the luma plane, write the other slot fields, memory barrier, set seq to SEQ, update the file header, print frame SLOT SEQ. The Python reader checks that seq equals SEQ before and after its copy. If either check fails, the slot was being overwritten: the frame is discarded and counted in frames_torn. The lag cap (4) is below the slot count (8), which keeps the reader away from slots the helper is about to reuse.

Controls

  • Requests: bmRequestType 0xA1 for GET_* and 0x21 for SET_CUR, wValue = selector << 8, wIndex = unit id << 8 | VideoControl interface number, 1000 ms timeout, on the libusb handle with no interface claimed. A lock serializes them.
  • cap.controls is pyuvc's standard_ctrl_units table (uvc_avf.standard_ctrl_units), filtered by the bmControls bitmaps of the camera terminal and the processing unit in the descriptor. Controls the device does not advertise are left out.
  • Each Control reads GET_INFO when it is created (info_bit_mask), plus GET_MIN, GET_MAX, GET_RES and GET_DEF for whichever of min_val, max_val, step and def_val the table leaves as None. refresh() re-reads GET_INFO and fills in any limit that is still None.
  • .value reads GET_CUR and writes SET_CUR. Controls that share one request buffer (Pan/Tilt, White Balance blue/red) read GET_CUR first and replace only their own field. Controls with a dict d_type also accept the names, e.g. ctrl.value = "manual mode".
  • 4-byte controls, plus Brightness, Hue, Roll, Pan and Tilt, are decoded as signed; everything else is unsigned.
  • A failed request is logged as a warning (Could not get Value of ... / Could not set Value of ...), as pyuvc does. The getter then returns None. Nothing is raised.

Device matching

The helper receives three keys and uses the first that matches an AVFoundation device:

  1. The AVFoundation uniqueID, computed from libusb data as "0x" + hex(locationID) + vendor id (4 hex digits) + product id (4 hex digits). locationID is bus << 24, with the USB port path in the following nibbles, one nibble per port, starting at bit 20. Example: bus 1, ports 1,1, 0c45:6366 gives locationID 0x01110000 and uniqueID 0x11100000c456366.
  2. A uniqueID ending in the vendor and product id (0c456366).
  3. A device name containing the USB product string.

The ready line and the INFO log (streaming 800x600 @ 120.0 (420v) from ..., matched by unique id) say which key matched.

Requirements and install

  • macOS. Tested on 26.4.1 (MacBook Air, Apple Silicon).
  • Xcode command line tools for swiftc (xcode-select --install). Tested with Swift 6.3.1.
  • Homebrew libusb: brew install libusb. Tested with 1.0.29. The module loads /opt/homebrew/lib/libusb-1.0.dylib, then /usr/local/lib/libusb-1.0.dylib, then libusb-1.0.dylib from the default library search path.
  • Python 3.9 or newer, with numpy. The measurements used Python 3.11.
  • Camera permission for the app that launches Python (see Gotchas).
pip install git+https://github.com/VimalMollyn/uvc-avf
# or, to work on it:
git clone https://github.com/VimalMollyn/uvc-avf
cd uvc-avf
pip install -e .

The helper is compiled on the first get_frame() into the installed package directory, next to uvc_avf_helper.swift, so that directory must be writable. This is the case in a virtualenv or an editable checkout. On the packaging machine swiftc -O took about 2.5 s. To build it ahead of time (this does not touch the camera):

python -c "import uvc_avf; print(uvc_avf._helper_exe())"

Usage

import uvc_avf as uvc

dev = next(d for d in uvc.device_list() if "Arducam" in d["name"])
cap = uvc.Capture(dev["uid"])
cap.frame_size = (800, 600)
cap.frame_rate = 120
ctrls = {c.display_name: c for c in cap.controls}
ctrls["Auto Exposure Mode"].value = 1          # manual; needed before writing the exposure
ctrls["Absolute Exposure Time"].value = 50     # units of 0.0001 s
try:
    first = cap.get_frame(timeout=10.0)        # the first call also starts the stream
    for _ in range(600):
        frame = cap.get_frame(timeout=1.0)     # TimeoutError, or uvc.StreamError if the helper died
        img = frame.gray                       # (600, 800) uint8
finally:
    cap.close()

pyuvc to uvc_avf

pyuvc uvc_avf Notes
uvc.device_list() same List of dicts with name, manufacturer, serialNumber, idProduct, idVendor, device_address, bus_number, uid ("bus:address"). Every USB device with a UVC VideoControl interface.
uvc.Device_List() same update() re-reads the bus; cleanup() does nothing.
uvc.Capture(uid) same Opens the device with libusb (no claim), reads descriptors, builds controls. Extra keyword test_pattern=False. Does not stream yet.
cap.available_modes, cap.all_modes same CameraMode(width, height, fps, format_native, format_name, supported) from the UVC descriptors. format_native is the UVC frame index; format_name is "MJPEG", "YUYV", ...; fps is rounded to an integer.
cap.frame_sizes, cap.frame_rates same Sizes, largest first; rates available at the current size.
cap.frame_mode same Setting it while streaming stops the stream; the next get_frame() restarts it.
cap.frame_size = (w, h) same Keeps the current rate if the size offers it. Unknown size: CaptureError.
cap.frame_rate = fps same A mode within 1 fps of the request, MJPEG preferred; otherwise the highest rate at that size, with a warning.
cap.bandwidth_factor accepted, unused macOS decides the USB bandwidth. Default 2.0.
cap.controls same See Controls.
cap.add_vendor_control(meta) same Returns the new Control and appends it to cap.controls.
cap.get_frame(timeout) same timeout 0 (the default) or negative waits forever. Raises builtin TimeoutError, or StreamError when the helper dies.
cap.get_frame_robust() same Four attempts with a 1 s timeout, then StreamError("could not get a frame").
cap.stop_stream() same Stops the helper; the next get_frame() starts a new one.
cap.close() same Stops the helper and closes the libusb handle. Also runs at interpreter exit and from __del__.
frame.gray same H x W uint8, a copy owned by the frame.
frame.bgr, frame.img gray replicated H x W x 3; no color.
frame.yuv_buffer luma bytes only Not a full YUV buffer.
frame.jpeg_buffer raises AttributeError macOS decodes the MJPEG stream.
frame.timestamp capture time The buffer's presentation time, on the time.monotonic() clock.
frame.index helper sequence number See Differences.
frame.width, frame.height, frame.data_fully_received same data_fully_received is always True.
uvc.get_time_monotonic() same time.monotonic().
uvc.is_accessible(uid) always True
uvc.InitError, OpenError, StreamError, DeviceNotFoundError, InsufficientBandwidthError same names All derive from uvc_avf.CaptureError. InsufficientBandwidthError (a StreamError) is never raised.

uvc_avf additions: cap.stats ({"frames", "dropped_by_macos", "skipped", "torn"}), cap.max_frame_lag, cap.frames_skipped, cap.frames_torn, cap.avf_unique_id, cap.name, frame.t_callback, frame.t_received, Capture.REOPEN_GAP (0.5 s), Capture.READY_TIMEOUT (15 s).

Differences from pyuvc

  • Delivery order. Frames come oldest first, with the lag capped at cap.max_frame_lag frames (default 4). libuvc always hands out the newest frame. cap.max_frame_lag = 1 returns the most recently announced frame; values below 1 are not supported.
  • Child process. The stream lives in the helper process and starts on the first get_frame(). That call first waits for the helper's ready line (up to 15 s); its timeout applies after that.
  • Restart guard. The module waits so that at least 0.5 s pass between stopping a stream and starting the next one (Capture.REOPEN_GAP), because of a quirk of the tested bridge.
  • frame.index is the helper's sequence number: frames AVFoundation delivered to the helper, counting from 1 for each stream. Consecutive frames returned by get_frame() differ by 1 unless the reader skipped frames (max_frame_lag) or discarded a torn one. Frames lost before AVFoundation delivered them, including those macOS dropped (stats["dropped_by_macos"]), leave no gap. To detect those, compare frame.timestamp differences with the frame period.
  • Signed values. 4-byte controls are signed 32-bit, like pyuvc (see Controls).
  • No MJPEG buffers, and luma only.
  • No bandwidth control. bandwidth_factor is ignored.

Vendor (extension-unit) controls

add_vendor_control takes a pyuvc-style meta dict with the keys unit_id, display_name, unit, control_id, offset, data_len, bit_mask, d_type, doc, buffer_len, min_val, max_val, step, def_val. unit_id, display_name, control_id and data_len are required. The defaults are unit="extension_unit", offset=0, bit_mask=0, buffer_len=data_len, doc="" and d_type=int. The limits default to None and are then queried from the device. Other keys raise TypeError.

meta = dict(unit_id=3, control_id=1, display_name="Vendor register", unit="extension_unit",
            offset=0, data_len=4, buffer_len=4, bit_mask=1 << 0, d_type=int, doc="",
            min_val=-(1 << 31), max_val=(1 << 31) - 1, step=1, def_val=0)
ctrl = cap.add_vendor_control(meta)
ctrl.value = request            # SET_CUR, 4 bytes little-endian; pass a signed 32-bit int
reply = ctrl.value & 0xFFFFFFFF # GET_CUR, decoded as signed 32-bit

Example: on the tested camera, the Sonix bridge exposes ASIC register access as extension unit 3, selector 1, 4 bytes. Writing the bytes [addr_lo, addr_hi, 0x00, 0xFF] (packed into a signed 32-bit int) requests a read. Byte 2 of the following GET_CUR holds the register value. The ASIC id register 0x101F reads 0x83. Extension units are vendor-specific: do not send this to another bridge.

Self-test

python -m uvc_avf --help
python -m uvc_avf --test-pattern        # synthetic frames; no camera permission needed
python -m uvc_avf --reopen              # real camera; run from Terminal.app
python -m uvc_avf --name "USB Camera" --size 640x480 --fps 100 --seconds 8

Apart from --help, every variant needs a UVC camera on the bus whose name contains --name (default Arducam). Even --test-pattern opens that camera through libusb for its descriptors, modes and controls; only the frames are synthetic. --size must be one of the camera's modes.

What it prints:

  • devices: [...], opened <uvc_avf.Capture ...> AVFoundation id 0x..., controls: [...], modes: [...], frame_mode: ....
  • first frame after X s: index 1, WxH, mean M, ipc latency L ms. X starts at the first get_frame() call, so it includes starting the helper (and compiling it the first time).
  • N frames in S s -> F fps; non-consecutive: K; mean |pair diff| D; ipc latency p50 P ms max Q ms; stats {...}. F comes from frame timestamps. K counts index gaps. D is the mean brightness difference between the two frames of each pair (about 100 with --test-pattern, whose frames alternate bright and dark). ipc latency is t_received - t_callback.
  • Real camera only: exposure E -> set E+1 -> read back E+1 (min ... max ...). The exposure and the AE mode are restored afterwards.
  • Real camera only: a vendor-control check that runs only if the sonix_i2c module of the project this package came from is importable. It is not part of this package; otherwise the line is sonix_i2c not importable: vendor-control check skipped.
  • With --reopen: closed; reopening, a second run of up to 3 s, closed.

The helper can also be run alone, with no camera, USB access or permission:

(sleep 2 | ./uvc_avf/uvc_avf_helper --shm "$TMPDIR/uvcavf_smoke" --width 320 --height 240 --fps 60 --test-pattern) | head -3
rm -f "$TMPDIR/uvcavf_smoke"
ready {"width":320,"height":240,"fps":60.0,"fourcc":"TEST","slots":8,"slot_size":77824,"header_size":4096,"slot_header":64,"file_size":626688,"device":"test-pattern","unique_id":"","matched_by":"test","pid":79624}
frame 1 1
frame 2 2

Probes

Both probes must run without sudo, from Terminal.app or another app with camera permission.

tools/avf_probe.swift: can AVFoundation stream this camera, and how fast?

swift tools/avf_probe.swift [name-substring] [width] [height] [fps] [seconds]
# defaults: Arducam 800 600 120 6; width 0 keeps the session's default mode

nosudo_probe.py compiles it to tools/avf_probe on first use. The output shows:

  • camera permission for this process: notDetermined|restricted|denied|authorized, and requestAccess -> true|false if it asked.
  • devices: [...] and using NAME (UNIQUEID). Compare UNIQUEID with Capture.avf_unique_id.
  • format FOURCC WxH fps [...] for each AVFoundation format. On the tested camera the MJPEG modes appear as 420v and the YUY2 modes as yuvs.
  • data output pixel formats: [...]. On the tested camera: 2vuy, yuvs, 420v, 420f, BGRA.
  • requested ... at VALUE/TIMESCALE: the exact advertised frame duration, e.g. 1000000/120000480.
  • startRunning returned after X s, running=true; active format .... The line !! the session replaced the requested format means the format did not survive startRunning.
  • frames N dropped D in S s -> F fps; interval p50 A ms max B ms, then either first frame: WxH FOURCC planes=P meanY M, T s after start or no frames in S s. With permission denied, AVFoundation still runs the session and this line reads no frames.

This probe requests 420v (video range), unlike the helper, so its meanY is not comparable with frame.gray.mean().

tools/nosudo_probe.py: do controls work without root, idle and while macOS streams?

python3 tools/nosudo_probe.py [--vidpid VVVV:PPPP] [avfoundation-name-substring]
UVC_AVF_VIDPID=VVVV:PPPP python3 tools/nosudo_probe.py "Camera Name"

The defaults are 0c45:6366 and Arducam; --vidpid wins over UVC_AVF_VIDPID. The probe needs only the standard library and libusb.

Phase 1, camera idle:

  • camera VVVV:PPPP at bus B addr A, running as uid U (not root, good).
  • units: ... and streaming formats (from the descriptor): ....
  • libusb_open OK. The probe sets LIBUSB_DEBUG=2. A libusb USBDeviceOpen warning printed above this line means macOS holds the device object itself; control requests still work.
  • interface N: kernel/system driver active = True for the VideoControl and VideoStreaming interfaces.
  • == control requests, camera idle ==, one OK value or FAIL LIBUSB_ERROR_... line per request: product string, extension-unit GET_LEN/GET_INFO, Sonix ASIC id (expect 0x83), AE mode, AE priority, exposure GET_CUR/GET_MIN/GET_MAX, exposure SET_CUR with read-back. The exposure and the AE mode are restored. The three extension-unit requests are Sonix-specific and run only for vendor id 0c45; other cameras print skipped.
  • VS interface N alt setting: 0 (idle) and the probe/commit GET_CUR blocks.
  • claim interface 0 without detaching: LIBUSB_ERROR_ACCESS, which is expected without root.

Phase 2, macOS streaming: avf_probe streams 800x600 @ 120 for 8 s. After 3.5 s the probe repeats the control requests (while macOS streams) and reads the streaming state. The tested camera reported alt setting: 6 (isoch pipe open, streaming) and a commit block of MJPEG 800x600 at 120 fps with 3072 B per transfer. If that attempt delivers no frames, the probe tries the session default mode and then 640x480 @ 100, each in a fresh process 3 s apart. !! this process has no camera permission means TCC denied the launching app.

The run ends with a SUMMARY block and either verdict: sudo-free path WORKS: ... or verdict: not proven yet (see the FAIL / !! lines above).

Gotchas

  • Camera permission belongs to the launching app. macOS attributes camera access (TCC) to the app that launched the process, not to Python or the helper. An app without NSCameraUsageDescription in its Info.plist is denied silently. One example is the Claude Code desktop app. AVFoundation raises no error in that case: the session runs and delivers zero frames. Run from Terminal.app, which asks once. The helper checks AVCaptureDevice.authorizationStatus before starting and exits with error camera permission denied for this process: ..., which the module raises as StreamError. On the first run the helper waits for the answer to the prompt. If the answer takes longer than 15 s, get_frame() raises StreamError("camera helper did not start streaming within 15 s"); call it again after answering.
  • Hold the configuration lock across startRunning(). The session applies its preset at startRunning() and replaces the device's activeFormat. Holding lockForConfiguration() across startRunning() keeps the requested format; ffmpeg does the same. Re-applying the format after the start is a quick stop/start, which left the tested Sonix bridge delivering 0 frames.
  • Full range, not video range. The helper requests 420f (luma 0..255, what pyuvc's JPEG decode gives) and falls back to 420v. Video range squeezes the picture into 16..235, about 14% less contrast.
  • Exact frame durations. Set the frame duration to the exact advertised CMTime, e.g. 1000000/120000480, not 1/120, or setActiveVideoMinFrameDuration throws.
  • Quick restarts. Keep at least 0.5 s between stopping and starting a stream; the module enforces this. Changing frame_mode, frame_size or frame_rate while streaming is a stop plus a restart on the next get_frame().
  • First frame delay. The first frame arrived 0.56-0.85 s after start, and the very first stream also compiles the helper. Give the first get_frame() a generous timeout (the self-test uses 10 s).
  • One Capture per camera. Each Capture starts its own helper process and AVFoundation session. Two Captures on one camera were not tested. Close one before opening another; close followed by reopen was tested and works.
  • Exposure writes need manual mode. Set Auto Exposure Mode to 1 (manual) before writing Absolute Exposure Time (units of 0.0001 s).
  • Writable package directory. The helper binary is written next to the module on first use.

Measured results

Measured 2026-09-26/27 on this setup:

Item Value
Computer MacBook Air (Apple Silicon), macOS 26.4.1
Toolchain Swift 6.3.1, Homebrew libusb 1.0.29, Python 3.11
Camera Arducam OV9281 USB Camera, Sonix bridge 0x0c45:0x6366
Connection USB 2.0 through an Apple USB-C multiport adapter
Measurement Result
Frame rate 800x600 at 118-122 fps, over runs of 5-8 s
Frames dropped by macOS 0
Frames skipped by the reader 0
Torn frames 0
First frame 0.56-0.85 s after start
Helper callback to Python (t_received - t_callback) p50 about 0.2 ms, max below 1 ms
Close and reopen works
Controls while streaming exposure write with read-back and vendor extension-unit reads work
Streaming interface while macOS streams alternate setting 6; committed MJPEG 800x600 @ 120 fps, 3072 B per transfer

Limitations

  • macOS only. Verified with one camera, one bridge, one Mac and one macOS version (above). Other cameras and bridges are untested, and the restart guard and lock-across-start behaviour were established on the Sonix bridge.
  • Measured end to end only in MJPEG 800x600 @ 120. The luma extraction for 2vuy/yuvs buffers exists but was not measured.
  • Luma only. bgr/img replicate the gray plane and any color is discarded; the tested sensor is monochrome.
  • A mode must exist in both the UVC descriptors and AVFoundation's format list (same size, maximum rate within 1 fps). Otherwise the helper fails with a list of what AVFoundation offers.
  • No bandwidth control. InsufficientBandwidthError is never raised.
  • is_accessible() always returns True. Access problems surface as OpenError or StreamError.
  • The uniqueID formula uses one nibble per port, so port numbers above 15 cannot be represented. The vendor/product and name fallbacks then apply. With two identical cameras, the vendor/product fallback takes the first AVFoundation device with that id, which may be the wrong one.
  • Only the pyuvc surface listed above is provided.

License

MIT, see LICENSE. Copyright 2026 Vimal Mollyn.

Metadata

Release files for uvc-avf 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for uvc-avf 0.1.0
File Size Uploaded Provenance
uvc_avf-0.1.0.tar.gz 41.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for uvc-avf 0.1.0
File Interpreter ABI Platform
uvc_avf-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 73.8 kB

Release files / uvc_avf-0.1.0.tar.gz

Download URL uvc_avf-0.1.0.tar.gz
Size 41.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ab2d89aa4e49d667aaa5975217eb72583f729e84fde1d650d845859e8ce213d4
BLAKE2b-256 checksum
How to use checksums
0b26a35837c3715ee431805c8814fe6b0c8223410e8663cbc4154ded44ac1c0f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / uvc_avf-0.1.0-py3-none-any.whl

Download URL uvc_avf-0.1.0-py3-none-any.whl
Size 31.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f0c3ac193597913903d605285268ce1e10076e7491c222547639eefc161ba89b
BLAKE2b-256 checksum
How to use checksums
b33217ab306e6a492a6f494304fc0eec049a81bb49cc25f68cfaff4feccddeea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for uvc-avf 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page