Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,269 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Bounded libhybris preview worker. Frames stay in memory; no microphone access.
The parent owns stdin. EOF or five seconds without a request closes the sensor.
A pbuffer and external texture are required even for the software NV21 callback.
"""
import argparse
import struct
import ctypes as C
import json
import os
import select
import sys
import time
def function(lib, name, args, result=None):
value = getattr(lib, name)
value.argtypes, value.restype = args, result
return value
class Listener(C.Structure):
_fields_ = [(name, C.c_void_p) for name in
('error', 'shutter', 'focus', 'zoom', 'raw', 'jpeg', 'texture', 'context', 'preview')]
class Camera:
width, height = 640, 480
def __init__(self, index):
self.camera = None
self.display = self.surface = self.context = None
self.frame = None
self.jpeg = None
self.focus_done = False
self.count = self.errors = 0
self.texture_pending = False
os.environ['HYBRIS_EGLPLATFORM'] = 'null'
os.environ['__EGL_VENDOR_LIBRARY_FILENAMES'] = '/usr/share/glvnd/egl_vendor.d/10_libhybris.json'
self.egl = C.CDLL('libEGL.so.1')
self.gl = C.CDLL('libGLESv2.so.2')
self.lib = C.CDLL('libcamera.so.1')
P, I, U = C.c_void_p, C.c_int, C.c_uint
self.display = function(self.egl, 'eglGetDisplay', [P], P)(None)
if not function(self.egl, 'eglInitialize', [P, P, P], U)(self.display, None, None):
raise RuntimeError('CAMERA_EGL_INIT_FAILED')
attrs = (I * 13)(0x3033, 1, 0x3040, 4, 0x3024, 8, 0x3023, 8, 0x3022, 8, 0x3021, 8, 0x3038)
config, count = P(), I()
function(self.egl, 'eglChooseConfig', [P, P, P, I, P], U)(self.display, attrs, C.byref(config), 1, C.byref(count))
if not count.value:
raise RuntimeError('CAMERA_EGL_CONFIG_FAILED')
function(self.egl, 'eglBindAPI', [U], U)(0x30A0)
self.context = function(self.egl, 'eglCreateContext', [P, P, P, P], P)(self.display, config, None, (I * 3)(0x3098, 2, 0x3038))
self.surface = function(self.egl, 'eglCreatePbufferSurface', [P, P, P], P)(self.display, config, (I * 5)(0x3057, 16, 0x3056, 16, 0x3038))
if not function(self.egl, 'eglMakeCurrent', [P, P, P, P], U)(self.display, self.surface, self.surface, self.context):
raise RuntimeError('CAMERA_EGL_CONTEXT_FAILED')
texture = U()
function(self.gl, 'glGenTextures', [I, P])(1, C.byref(texture))
@C.CFUNCTYPE(None, P, U, P)
def preview(data, size, _context):
if size == self.width * self.height * 3 // 2:
self.frame = C.string_at(data, size)
self.count += 1
@C.CFUNCTYPE(None, P)
def error(_context):
self.errors += 1
@C.CFUNCTYPE(None, P)
def available(_context):
self.texture_pending = True
@C.CFUNCTYPE(None, P)
def focused(_context):
self.focus_done = True
@C.CFUNCTYPE(None, P, U, P)
def jpeg(data, size, _context):
if 0 < size <= 32 * 1024 * 1024:
self.jpeg = C.string_at(data, size)
self.callbacks = (preview, error, available, focused, jpeg)
self.listener = Listener()
for name, callback in [('preview', preview), ('error', error), ('texture', available), ('focus', focused), ('jpeg', jpeg)]:
setattr(self.listener, name, C.cast(callback, P).value)
self.camera = function(self.lib, 'android_camera_connect_by_id', [I, P], P)(index, C.byref(self.listener))
if not self.camera:
raise RuntimeError('CAMERA_CONNECT_FAILED')
facing, orientation = I(), I()
function(self.lib, 'android_camera_get_device_info', [I, P, P], I)(index, C.byref(facing), C.byref(orientation))
self.orientation = orientation.value
function(self.lib, 'android_camera_set_preview_size', [P, I, I])(self.camera, self.width, self.height)
function(self.lib, 'android_camera_set_preview_format', [P, I])(self.camera, 1) # YUV420SP / NV21
function(self.lib, 'android_camera_set_preview_texture', [P, I])(self.camera, texture.value)
function(self.lib, 'android_camera_set_preview_callback_mode', [P, I], I)(self.camera, 1)
function(self.lib, 'android_camera_start_preview', [P])(self.camera)
self.parameters = self.read_parameters()
self.picture_sizes = [tuple(map(int, size.split('x'))) for size in self.parameters.get('picture-size-values', '').split(',') if 'x' in size]
self.picture_size = max(self.picture_sizes, key=lambda size:size[0]*size[1])
self.zoom_ratios = [int(n)/100 for n in self.parameters.get('zoom-ratios', '100').split(',')]
def read_parameters(self):
# libhybris exposes the flattened parameters through a stdout dump.
# Capture it in RAM so it cannot corrupt the worker's binary protocol.
libc = C.CDLL(None)
fd = os.memfd_create('camera-parameters')
saved = os.dup(1)
try:
libc.fflush(None)
os.dup2(fd, 1)
function(self.lib, 'android_camera_dump_parameters', [C.c_void_p])(self.camera)
libc.fflush(None)
finally:
os.dup2(saved, 1)
os.close(saved)
try:
os.lseek(fd, 0, 0)
raw = os.read(fd, 65536).decode()
return dict(item.split('=', 1) for item in raw.strip().split(';') if '=' in item)
finally:
os.close(fd)
def metadata(self):
return {'ready': True, 'width': self.width, 'height': self.height,
'orientation': self.orientation, 'format': 'nv21',
'pictureSizes': [{'width':w,'height':h} for w,h in self.picture_sizes],
'focusModes': self.parameters.get('focus-mode-values', 'fixed').split(','),
'maxFocusAreas': min(1,int(self.parameters.get('max-num-focus-areas', 0))),
'zoomRatios': self.zoom_ratios}
def control(self, command):
P, I = C.c_void_p, C.c_int
action = command.get('action')
if action == 'zoom':
ratio = command.get('zoom')
if not isinstance(ratio, (int,float)) or not self.zoom_ratios[0] <= ratio <= self.zoom_ratios[-1]:
raise ValueError('CAMERA_INVALID_ZOOM')
index = min(range(len(self.zoom_ratios)), key=lambda i:abs(self.zoom_ratios[i]-ratio))
function(self.lib,'android_camera_set_zoom',[P,I])(self.camera,index)
actual = int(self.read_parameters().get('zoom',index))
if not 0 <= actual < len(self.zoom_ratios): raise RuntimeError('CAMERA_INVALID_ZOOM_REPLY')
return {'zoom':self.zoom_ratios[actual]}
if action == 'focus':
if 'auto' not in self.metadata()['focusModes']:
raise ValueError('CAMERA_FOCUS_UNAVAILABLE')
function(self.lib,'android_camera_stop_autofocus',[P])(self.camera)
function(self.lib,'android_camera_set_auto_focus_mode',[P,I])(self.camera,2)
area = command.get('area')
if area is not None:
values = [area.get(k) for k in ('top','left','bottom','right')]
if any(not isinstance(n,int) or not -1000 <= n <= 1000 for n in values) or values[0]>=values[2] or values[1]>=values[3]:
raise ValueError('CAMERA_INVALID_FOCUS_AREA')
if not self.metadata()['maxFocusAreas']:
raise ValueError('CAMERA_FOCUS_AREA_UNAVAILABLE')
region = (I*5)(*values,1000)
function(self.lib,'android_camera_set_focus_region',[P,P])(self.camera,region)
else:
function(self.lib,'android_camera_reset_focus_region',[P])(self.camera)
self.focus_done = False
function(self.lib,'android_camera_start_autofocus',[P])(self.camera)
deadline = time.monotonic()+4
while not self.focus_done and time.monotonic()<deadline:
self.tick(); time.sleep(.01)
if not self.focus_done:
raise RuntimeError('CAMERA_FOCUS_TIMEOUT')
return {'completed':True}
if action == 'picture':
size = command.get('size') or dict(zip(('width','height'),self.picture_size))
pair = (size.get('width'),size.get('height'))
if pair not in self.picture_sizes:
raise ValueError('CAMERA_INVALID_PICTURE_SIZE')
function(self.lib,'android_camera_set_picture_size',[P,I,I])(self.camera,*pair)
function(self.lib,'android_camera_set_jpeg_quality',[P,I])(self.camera,95)
function(self.lib,'android_camera_set_rotation',[P,I])(self.camera,self.orientation)
self.jpeg = None
try:
function(self.lib,'android_camera_take_snapshot',[P])(self.camera)
deadline = time.monotonic()+12
while self.jpeg is None and time.monotonic()<deadline:
self.tick(); time.sleep(.01)
if self.jpeg is None:
raise RuntimeError('CAMERA_CAPTURE_TIMEOUT')
return self.jpeg
finally:
function(self.lib,'android_camera_start_preview',[P])(self.camera)
raise ValueError('CAMERA_INVALID_COMMAND')
def tick(self):
if self.texture_pending:
self.texture_pending = False
function(self.lib, 'android_camera_update_preview_texture', [C.c_void_p])(self.camera)
def close(self):
P = C.c_void_p
if self.camera:
function(self.lib, 'android_camera_stop_preview', [P])(self.camera)
function(self.lib, 'android_camera_disconnect', [P])(self.camera)
self.camera = None
if self.display:
function(self.egl, 'eglMakeCurrent', [P, P, P, P], C.c_uint)(self.display, None, None, None)
if self.surface:
function(self.egl, 'eglDestroySurface', [P, P], C.c_uint)(self.display, self.surface)
if self.context:
function(self.egl, 'eglDestroyContext', [P, P], C.c_uint)(self.display, self.context)
function(self.egl, 'eglTerminate', [P], C.c_uint)(self.display)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--camera', type=int, choices=(0, 1), default=0)
parser.add_argument('--probe', action='store_true', help='Count frames for five seconds, retain no images')
args = parser.parse_args()
output = os.fdopen(os.dup(1), 'wb', buffering=0)
os.dup2(2, 1) # HAL callbacks can print; keep stdout protocol uncontaminated.
camera = None
try:
camera = Camera(args.camera)
started = last_request = time.monotonic()
if not args.probe:
metadata = json.dumps(camera.metadata()).encode()
output.write(struct.pack('<I', len(metadata)) + metadata)
output.flush()
while time.monotonic() - last_request < 5:
camera.tick()
if args.probe:
time.sleep(.01)
continue
if not select.select([sys.stdin], [], [], .01)[0]:
continue
command = sys.stdin.readline(4096)
if not command or len(command)>=4096:
break
last_request = time.monotonic()
if command == 'frame\n':
data = camera.frame or b''
output.write(struct.pack('<II', camera.count & 0xffffffff, len(data)))
output.write(data)
else:
try:
result = camera.control(json.loads(command))
if isinstance(result,bytes):
data = json.dumps({'ok':True,'jpegLength':len(result)}).encode()
else:
data = json.dumps({'ok':True,'result':result}).encode()
except (ValueError,RuntimeError,TypeError,AttributeError) as error:
result = None
data = json.dumps({'ok':False,'error':str(error)}).encode()
output.write(struct.pack('<I',len(data))+data)
if isinstance(result,bytes):
output.write(result)
output.flush()
last_request = time.monotonic()
if args.probe:
output.write((json.dumps({'camera': args.camera, 'frames': camera.count, 'errors': camera.errors,
'width': camera.width, 'height': camera.height, 'orientation': camera.orientation,
'seconds': round(time.monotonic() - started, 2), 'imagesRetained': False})+'\n').encode())
if not camera.count or camera.errors:
return 1
return 0
finally:
if camera:
camera.close()
if __name__ == '__main__':
sys.exit(main())