#!/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()=4096: break last_request = time.monotonic() if command == 'frame\n': data = camera.frame or b'' output.write(struct.pack('