// Only Platform's authorized camera application may own this worker. import {Subprocess} from 'resource://gre/modules/Subprocess.sys.mjs'; import {setTimeout, clearTimeout} from 'resource://gre/modules/Timer.sys.mjs'; export class NativeCamera { constructor(device = '') { this.device = device; this.previewSize = device === 'beatles' ? {width:320,height:240} : {width:640,height:480}; this.session = null; this.queue = Promise.resolve(); this.closing = Promise.resolve(); this.nextSession = 0; } request(actor, params) { const work = this.queue.then(() => this.perform(actor, params)); this.queue = work.catch(() => {}); return work; } async packet(session, metadata = false) { let timer; try { return await Promise.race([ (async () => { const pipe=session.process.stdout; const header=new DataView(await pipe.read(metadata ? 4 : 8)); const sequence=metadata ? null : header.getUint32(0,true); const length=header.getUint32(metadata ? 0 : 4,true); if(metadata) { if(length>65536) throw Error('CAMERA_INVALID_FORMAT'); return pipe.readJSON(length); } const expected = this.session.width * this.session.height * 3 / 2; if(length!==0 && length!==expected) throw Error('CAMERA_INVALID_FRAME'); return {sequence,data:length ? await pipe.read(length) : null}; })(), new Promise((_, reject) => { timer=setTimeout(()=>reject(Error('CAMERA_TIMEOUT')),15000); }), ]); } finally { clearTimeout(timer); } } async perform(actor, params) { if (params.action === 'stop') { if (this.session?.actor === actor && this.session.id === params.session) await this.close(); return null; } if (params.action === 'start') { if (this.session && this.session.actor !== actor) throw Error('CAMERA_BUSY'); await this.close(); if (this.device === 'beatles' && params.camera === 'front') throw Error('CAMERA_UNAVAILABLE'); const beatles = this.device === 'beatles'; const process = await Subprocess.call({command:beatles?'/usr/bin/python3.11':'/usr/bin/python3', arguments:beatles ? ['-u','/opt/vulpes/tundra/runtime/beatles-camera-client.py'] : ['-u', '/etc/tundra/camera-preview.py', '--camera', params.camera === 'front' ? '1' : '0', '--width',String(this.previewSize.width),'--height',String(this.previewSize.height)], stderr:'ignore'}); this.session = {actor, process, id: ++this.nextSession}; try { const metadata = await this.packet(this.session, true); if (!metadata.ready || metadata.format !== 'nv21' || metadata.width !== this.previewSize.width || metadata.height !== this.previewSize.height) throw Error('CAMERA_INVALID_FORMAT'); this.session.width = metadata.width; this.session.height = metadata.height; return {...metadata, session: this.session.id, // The 2720 Flip has 400 MB of RAM and copies each frame through // CameraService, Gecko IPC and a software canvas. Polling at the // sensor's nominal rate exhausts its zram; keep its preview bounded. frameRate: beatles ? 5 : 30, cameras: beatles ? ['back'] : ['back','front']}; } catch (error) { this.close(); throw error; } } if (!this.session || this.session.actor !== actor || this.session.id !== params.session) throw Error('CAMERA_NOT_OPEN'); if (!['frame','focus','zoom','picture','flash'].includes(params.action)) throw Error('CAMERA_INVALID_COMMAND'); let controlRejected = false; try { if (params.action === 'frame') { await this.session.process.stdin.write('frame\n'); return await this.packet(this.session); } const command = JSON.stringify(params); if (command.length>2048) throw Error('CAMERA_INVALID_COMMAND'); await this.session.process.stdin.write(command+'\n'); const reply = await this.packet(this.session, true); if (!reply.ok) { controlRejected = true; throw Error(reply.error || 'CAMERA_CONTROL_FAILED'); } if (params.action === 'picture') { if (!Number.isInteger(reply.jpegLength) || reply.jpegLength<4 || reply.jpegLength>32*1024*1024) throw Error('CAMERA_INVALID_JPEG'); return {data:await this.session.process.stdout.read(reply.jpegLength)}; } return reply.result; } catch (error) { // A complete error reply leaves the protocol synchronized. A focus // timeout must not discard a camera whose preview is still running. if (!controlRejected) this.close(); throw error; } } close() { const session = this.session; this.session = null; if (!session) return this.closing; session.process.stdin.close().catch(() => {}); const timer = setTimeout(() => session.process.kill(), 1500); this.closing = session.process.wait().finally(() => clearTimeout(timer)); return this.closing; } cleanup(actor) { if (this.session?.actor === actor) this.close(); } }