Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
98 lines
4 KiB
JavaScript
98 lines
4 KiB
JavaScript
// 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() { 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);
|
|
}
|
|
if(length!==0 && length!==640*480*3/2) 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();
|
|
const process = await Subprocess.call({command:'/usr/bin/python3',
|
|
arguments:['-u', '/etc/tundra/camera-preview.py', '--camera', params.camera === 'front' ? '1' : '0'],
|
|
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 !== 640 || metadata.height !== 480)
|
|
throw Error('CAMERA_INVALID_FORMAT');
|
|
return {...metadata, session: this.session.id};
|
|
} 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'].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();
|
|
}
|
|
}
|