1- const { app, BrowserWindow, ipcMain, shell, systemPreferences } = require ( "electron" ) ;
1+ const { app, BrowserWindow, ipcMain, shell, systemPreferences, session } = require ( "electron" ) ;
22const path = require ( "path" ) ;
33const fs = require ( "fs" ) ;
44const os = require ( "os" ) ;
5+ const crypto = require ( "crypto" ) ;
6+ const http = require ( "http" ) ;
57const childProcess = require ( "child_process" ) ;
68const pty = require ( "node-pty" ) ;
79
810const isDev = ! app . isPackaged ;
911const repoRoot = path . resolve ( __dirname , ".." ) ;
1012const activeSessions = new Map ( ) ;
13+ const cameraBridge = {
14+ server : null ,
15+ url : "" ,
16+ token : crypto . randomBytes ( 24 ) . toString ( "hex" ) ,
17+ latestFrame : "" ,
18+ updatedAt : 0 ,
19+ status : "starting" ,
20+ error : ""
21+ } ;
1122
1223function corePath ( ) {
1324 if ( isDev ) return path . join ( repoRoot , process . platform === "win32" ? "yukishell.exe" : "yukishell" ) ;
@@ -158,6 +169,67 @@ function createWindow() {
158169 }
159170}
160171
172+ function sendJson ( res , statusCode , payload ) {
173+ const body = JSON . stringify ( payload ) ;
174+ res . writeHead ( statusCode , {
175+ "Content-Type" : "application/json" ,
176+ "Content-Length" : Buffer . byteLength ( body ) ,
177+ "Cache-Control" : "no-store"
178+ } ) ;
179+ res . end ( body ) ;
180+ }
181+
182+ function startCameraBridge ( ) {
183+ if ( cameraBridge . server ) return Promise . resolve ( ) ;
184+
185+ return new Promise ( ( resolve ) => {
186+ cameraBridge . server = http . createServer ( ( req , res ) => {
187+ const requestUrl = new URL ( req . url , "http://127.0.0.1" ) ;
188+
189+ if ( requestUrl . searchParams . get ( "token" ) !== cameraBridge . token ) {
190+ sendJson ( res , 403 , { ok : false , error : "Camera bridge token rejected." } ) ;
191+ return ;
192+ }
193+
194+ if ( requestUrl . pathname === "/health" ) {
195+ sendJson ( res , 200 , {
196+ ok : cameraBridge . status === "ready" ,
197+ status : cameraBridge . status ,
198+ error : cameraBridge . error ,
199+ updatedAt : cameraBridge . updatedAt
200+ } ) ;
201+ return ;
202+ }
203+
204+ if ( requestUrl . pathname === "/frame" ) {
205+ if ( ! cameraBridge . latestFrame ) {
206+ sendJson ( res , 503 , {
207+ ok : false ,
208+ status : cameraBridge . status ,
209+ error : cameraBridge . error || "Camera frame is not ready yet."
210+ } ) ;
211+ return ;
212+ }
213+
214+ sendJson ( res , 200 , {
215+ ok : true ,
216+ data : cameraBridge . latestFrame ,
217+ updatedAt : cameraBridge . updatedAt
218+ } ) ;
219+ return ;
220+ }
221+
222+ sendJson ( res , 404 , { ok : false , error : "Unknown camera bridge endpoint." } ) ;
223+ } ) ;
224+
225+ cameraBridge . server . listen ( 0 , "127.0.0.1" , ( ) => {
226+ const address = cameraBridge . server . address ( ) ;
227+ cameraBridge . url = `http://127.0.0.1:${ address . port } /frame?token=${ cameraBridge . token } ` ;
228+ resolve ( ) ;
229+ } ) ;
230+ } ) ;
231+ }
232+
161233function requestMediaAccess ( kind ) {
162234 if ( process . platform !== "darwin" ) return ;
163235
@@ -171,7 +243,11 @@ function requestMediaAccess(kind) {
171243 }
172244}
173245
174- app . whenReady ( ) . then ( ( ) => {
246+ app . whenReady ( ) . then ( async ( ) => {
247+ await startCameraBridge ( ) ;
248+ session . defaultSession . setPermissionRequestHandler ( ( _webContents , permission , callback ) => {
249+ callback ( permission === "media" ) ;
250+ } ) ;
175251 requestMediaAccess ( "camera" ) ;
176252 requestMediaAccess ( "microphone" ) ;
177253 createWindow ( ) ;
@@ -190,6 +266,30 @@ app.on("window-all-closed", () => {
190266 if ( process . platform !== "darwin" ) app . quit ( ) ;
191267} ) ;
192268
269+ app . on ( "before-quit" , ( ) => {
270+ if ( cameraBridge . server ) {
271+ cameraBridge . server . close ( ) ;
272+ cameraBridge . server = null ;
273+ }
274+ } ) ;
275+
276+ ipcMain . on ( "camera:frame" , ( _event , { data } ) => {
277+ if ( typeof data !== "string" || data . length === 0 ) return ;
278+ cameraBridge . latestFrame = data ;
279+ cameraBridge . updatedAt = Date . now ( ) ;
280+ cameraBridge . status = "ready" ;
281+ cameraBridge . error = "" ;
282+ } ) ;
283+
284+ ipcMain . on ( "camera:status" , ( _event , payload = { } ) => {
285+ cameraBridge . status = payload . status || "unknown" ;
286+ cameraBridge . error = payload . error || "" ;
287+ if ( cameraBridge . status !== "ready" ) {
288+ cameraBridge . latestFrame = "" ;
289+ cameraBridge . updatedAt = 0 ;
290+ }
291+ } ) ;
292+
193293ipcMain . handle ( "terminal:create" , ( event , options = { } ) => {
194294 const id = `${ Date . now ( ) } -${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ;
195295 const backend = resolveBackend ( ) ;
@@ -211,6 +311,7 @@ ipcMain.handle("terminal:create", (event, options = {}) => {
211311 YUKISHELL_ENV_FILE : envFilePath ( ) ,
212312 YUKISHELL_PYTHON_BIN : pythonBinaryPath ( ) ,
213313 YUKISHELL_PYTHONPATH : bundledPythonPath ( ) ,
314+ YUKISHELL_CAMERA_BRIDGE_URL : cameraBridge . url ,
214315 OPENCV_AVFOUNDATION_SKIP_AUTH : "0" ,
215316 PYTHONDONTWRITEBYTECODE : "1" ,
216317 PYTHONPATH : [ bundledPythonPath ( ) , process . env . PYTHONPATH ] . filter ( Boolean ) . join ( path . delimiter )
@@ -261,6 +362,7 @@ ipcMain.handle("app:paths", () => ({
261362 pythonBin : pythonBinaryPath ( ) ,
262363 pythonBundleExists : fs . existsSync ( bundledPythonPath ( ) ) ,
263364 envFileExists : fs . existsSync ( envFilePath ( ) ) ,
365+ cameraBridgeReady : Boolean ( cameraBridge . url ) ,
264366 platform : process . platform ,
265367 arch : process . arch
266368} ) ) ;
0 commit comments