serve-sim-sjchmiela 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,7 @@ final class H264Encoder {
36
36
  private var encodedCount: Int64 = 0
37
37
  private var lowLatencyEnabled = true
38
38
  private var forceKeyframeAfterReset = false
39
+ private var retiredSessions: [VTCompressionSession] = []
39
40
 
40
41
  init(fps: Int = 60, bitrate: Int = 6_000_000) {
41
42
  self.fps = Int32(fps)
@@ -44,6 +45,7 @@ final class H264Encoder {
44
45
 
45
46
  deinit {
46
47
  if let session { VTCompressionSessionInvalidate(session) }
48
+ for session in retiredSessions { VTCompressionSessionInvalidate(session) }
47
49
  }
48
50
 
49
51
  /// Submit a frame. Returns immediately; `onEncoded` fires on VT's queue.
@@ -137,6 +139,8 @@ final class H264Encoder {
137
139
  VTCompressionSessionInvalidate(session)
138
140
  self.session = nil
139
141
  }
142
+ for session in retiredSessions { VTCompressionSessionInvalidate(session) }
143
+ retiredSessions.removeAll()
140
144
  pool = nil
141
145
  }
142
146
 
@@ -265,10 +269,18 @@ final class H264Encoder {
265
269
  lock.lock()
266
270
  defer { lock.unlock() }
267
271
  guard lowLatencyEnabled else { return }
268
- streamLog("[stream:h264] low-latency encoder failed (\(reason)); rebuilding default VT session")
272
+ streamLog("[stream:h264] low-latency encoder failed (\(reason)); default VT session will be used")
269
273
  lowLatencyEnabled = false
270
274
  forceKeyframeAfterReset = true
271
- rebuildSession()
275
+ if let session {
276
+ retiredSessions.append(session)
277
+ self.session = nil
278
+ }
279
+ pool = nil
280
+ stateQueue.sync {
281
+ emittedDescription = false
282
+ encodedCount = 0
283
+ }
272
284
  }
273
285
 
274
286
  private func nextEncodedCount() -> Int64 {
Binary file
package/dist/serve-sim.js CHANGED
@@ -139,7 +139,7 @@ Usage:
139
139
  Permissions: ${E4().join(", ")}`),process.exit(1);let Y=J.device?o(J.device):Q$();if(!Y)console.error("No booted simulator. Boot one or pass -d <udid|name>."),process.exit(1);if(J.verb==="list"){let X={udid:Y,bundleId:J.bundleId??null,tcc:x7(Y,J.bundleId),location:S7(Y,J.bundleId),notifications:v7(Y,J.bundleId)};console.log(JSON.stringify(X,null,Q?0:2)),process.exit(0)}let G=J.bundleId;try{if(J.permission==="all")for(let X of E4())A5(Y,"reset",X,void 0,G);else A5(Y,J.verb,J.permission,J.value,G)}catch(X){console.error(X?.message??String(X)),process.exit(1)}if(Q)console.log(JSON.stringify({udid:Y,verb:J.verb,permission:J.permission,value:J.value??null,bundleId:G}));else{let X=J.value?` (${J.value})`:"";console.log(`\uD83D\uDD10 ${J.verb} ${J.permission}${X} for ${G} on ${Y}`)}process.exit(0)}x4();v0();import{spawn as PZ}from"child_process";import{randomBytes as IZ}from"crypto";var wZ=/https:\/\/[a-z0-9-]+\.trycloudflare\.com/,e5=30000,s5="cloudflared not found on PATH. Install it with `brew install cloudflared` (macOS) or see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/.";function $6($){return`${$.toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,40)||"serve-sim"}-${IZ(4).toString("hex")}`}function Q6($,Q){if((Q?.provider??"cloudflare")==="ngrok")return kZ($,{timeoutMs:Q?.timeoutMs,domain:Q?.domain,label:Q?.label});return xZ($,{timeoutMs:Q?.timeoutMs,protocol:Q?.protocol})}function xZ($,Q){let Z=Q?.timeoutMs??e5,J=Q?.protocol;return new Promise((Y,G)=>{let X=["tunnel","--no-autoupdate",...J?["--protocol",J]:[],"--url",`http://localhost:${$}`],V;try{V=PZ("cloudflared",X,{stdio:["ignore","pipe","pipe"]})}catch(O){G(O.code==="ENOENT"?Error(s5):O);return}let K=!1,z="",W=()=>{clearTimeout(j),V.stdout?.off("data",H),V.stderr?.off("data",H),V.off("error",L),V.off("exit",T)},H=(O)=>{if(z+=typeof O==="string"?O:O.toString(),z.length>65536)z=z.slice(-32768);let A=z.match(wZ);if(A&&!K)K=!0,W(),Y({url:A[0],pid:V.pid,child:V,stop:()=>{try{V.kill("SIGTERM")}catch{}}})},L=(O)=>{if(K)return;K=!0,W();try{V.kill()}catch{}G(O.code==="ENOENT"?Error(s5):O)},T=(O)=>{if(K)return;K=!0,W();let A=z.split(`
140
140
  `).slice(-5).join(`
141
141
  `).trim();G(Error(`cloudflared exited (code ${O}) before producing a URL`+(A?`:
142
- ${A}`:"")))},j=setTimeout(()=>{if(K)return;K=!0,W();try{V.kill()}catch{}G(Error(`cloudflared did not produce a URL within ${Z}ms`))},Z);V.stdout?.on("data",H),V.stderr?.on("data",H),V.on("error",L),V.on("exit",T)})}async function kZ($,Q){let Z=Q?.timeoutMs??e5,J=await SZ(),Y=Q?.domain?yZ(Q.domain,Q.label):void 0,G={addr:$};if(process.env.NGROK_AUTHTOKEN)G.authtoken=process.env.NGROK_AUTHTOKEN;if(Y)G.domain=Y;let X=await fZ(J(G),Z,`ngrok did not produce a URL within ${Z}ms`,(K)=>{K.close()}),V=X.url();if(!V)throw await X.close().catch(()=>{}),Error("ngrok started but did not return a URL");return{url:V,stop:()=>{X.close().catch(()=>{})}}}async function SZ(){try{let $=await import("@ngrok/ngrok"),Q=$.forward??$.default?.forward;if(typeof Q!=="function")throw Error("@ngrok/ngrok does not export forward()");return Q}catch($){let Q=$ instanceof Error?$.message:String($);throw Error(`Unable to load @ngrok/ngrok: ${Q}. Run \`bun install\` or install the package before using --tunnel-provider ngrok.`)}}function yZ($,Q){let Z=$.replace(/^https?:\/\//i,"").replace(/^\*\./,"").replace(/\/+$/,"");if(!Q)return Z;return`${Q}.${Z}`}function fZ($,Q,Z,J){let Y=!1;return new Promise((G,X)=>{let V=setTimeout(()=>{Y=!0,X(Error(Z))},Q);$.then((K)=>{if(clearTimeout(V),Y){J?.(K);return}G(K)},(K)=>{clearTimeout(V),X(K)})})}var D$=P0(import.meta.url);function L3(){return"0.1.40"}function P9($,Q){let Z=Number($);if(!Number.isInteger(Z)||Z<=0)throw new D0(`${Q} must be a positive integer.`);return Z}function R3($){let Q=Number($);if(!Number.isFinite(Q)||Q<0||Q>1)throw new D0("--stream-quality must be a number between 0 and 1.");return Q}var I9=new Set;function U3($){return I9.add($),$}function _3(){for(let $ of I9)try{$.stop()}catch{}I9.clear()}process.on("exit",_3);function u8(){if(!i(M$))k9(M$,{recursive:!0})}function e($){if($)return w9(X0($));for(let Q of A0()){let Z=w9(Q);if(Z)return Z}return null}var $4={at:0,booted:null};function N3(){let $=Date.now();if($4.booted&&$-$4.at<1000)return $4.booted;try{let Q=s("xcrun simctl list devices booted -j",{encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:3000}),Z=JSON.parse(Q),J=new Set;for(let Y of Object.values(Z.devices))for(let G of Y)if(G.state==="Booted")J.add(G.udid);return $4={at:$,booted:J},J}catch{return null}}function w9($){try{if(!i($))return U$("state file missing %s",$),null;let Q=JSON.parse(W$($,"utf-8"));try{process.kill(Q.pid,0)}catch{return U$("helper pid %d dead, removing stale state %s",Q.pid,$),s$($),null}let Z=N3();if(Z&&!Z.has(Q.device)){U$("helper pid %d bound to non-booted device %s — killing stale helper",Q.pid,Q.device),console.error(`[serve-sim] Helper pid ${Q.pid} is bound to device ${Q.device} which is no longer booted — killing stale helper.`);try{process.kill(Q.pid,"SIGTERM")}catch{}try{s$($)}catch{}return null}return U$("state ok pid=%d device=%s port=%d",Q.pid,Q.device,Q.port),Q}catch(Q){return U$("readStateFile threw for %s: %o",$,Q),null}}function F0(){let $=[];for(let Q of A0()){let Z=w9(Q);if(Z)$.push(Z)}return $}function B3($){u8(),f9(X0($.device),JSON.stringify($,null,2)),U$("wrote state pid=%d device=%s port=%d",$.pid,$.device,$.port)}function v$($){if($){U$("clearState device=%s",$);try{s$(X0($))}catch{}}else{U$("clearState (all)");for(let Q of A0())try{s$(Q)}catch{}}}function b9(){try{let $=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Q=JSON.parse($),Z=Object.keys(Q.devices).filter((J)=>/SimRuntime\.iOS-/i.test(J)).sort((J,Y)=>{let G=(J.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number),X=(Y.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number);return(X[0]??0)-(G[0]??0)||(X[1]??0)-(G[1]??0)});for(let J of Z){let G=(Q.devices[J]??[]).find((X)=>X.isAvailable!==!1&&/^iPhone\b/i.test(X.name));if(G)return{udid:G.udid,name:G.name}}}catch{}return null}function b8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.name}catch{}return null}function g8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.state==="Booted"}catch{}return!1}function Z4($){try{return process.kill($,0),!0}catch{return!1}}function l8($){try{process.kill($,"SIGTERM")}catch{return}let Q=Date.now()+500;while(Date.now()<Q)try{process.kill($,0),A$(25)}catch{return}try{process.kill($,"SIGKILL")}catch{}let Z=Date.now()+500;while(Date.now()<Z)try{process.kill($,0),A$(25)}catch{return}}function E3($){if(!g8($))try{s(`xcrun simctl boot ${$}`,{encoding:"utf-8",stdio:"pipe"})}catch(Q){let Z=(Q.stderr??Q.message??"").toLowerCase();if(!Z.includes("booted")&&!Z.includes("current state"))throw Error(`Failed to boot device ${$}: ${Q.stderr||Q.message}`)}try{s("open -ga Simulator",{encoding:"utf-8",stdio:"pipe",timeout:3000})}catch{}}function F3(){let $=H3();for(let Q of Object.values($))for(let Z of Q??[])if(Z.family==="IPv4"&&!Z.internal)return Z.address;return null}async function c8($){let Q=new Set(F0().map((Z)=>Z.port));for(let Z=$;Z<$+100;Z++){if(Q.has(Z))continue;if(await j5(Z))return Z}throw Error(`No available port found in range ${$}-${$+99}`)}async function T3($){E3($);try{s(`xcrun simctl bootstatus ${$} -b`,{encoding:"utf-8",stdio:"pipe",timeout:60000})}catch(Q){if(!g8($))console.error(`Device ${$} failed to reach booted state: ${Q.stderr||Q.message}`),process.exit(1)}}function j3($){if(process.argv[0]&&/(^|\/)serve-sim$/.test(process.argv[0]))return{command:process.argv[0],args:$};return{command:process.argv[0],args:[process.argv[1],...$]}}async function O3($,Q=150000){let Z=Date.now();while(Date.now()-Z<Q){let J=e($);if(J)return J;await new Promise((Y)=>setTimeout(Y,200))}return null}async function m8($,Q,Z){f4("startHelper udid=%s port=%d detach=%s",$,Q,Z.detach);let J="127.0.0.1";u8(),v$($),D5(Q);let Y=J$(M$,`server-${$}.log`),G=S9(Y,"w"),{command:X,args:V}=j3([$,"--port",String(Q),"--host",J]),K=v8(X,V,{detached:Z.detach,stdio:["ignore",G,G]});if(y9(G),Z.detach)K.unref();let z=await O3($);if(!z){if(K.pid)l8(K.pid);let W="";try{W=W$(Y,"utf-8").trim()}catch{}console.error(W?`Preview server failed:
142
+ ${A}`:"")))},j=setTimeout(()=>{if(K)return;K=!0,W();try{V.kill()}catch{}G(Error(`cloudflared did not produce a URL within ${Z}ms`))},Z);V.stdout?.on("data",H),V.stderr?.on("data",H),V.on("error",L),V.on("exit",T)})}async function kZ($,Q){let Z=Q?.timeoutMs??e5,J=await SZ(),Y=Q?.domain?yZ(Q.domain,Q.label):void 0,G={addr:$};if(process.env.NGROK_AUTHTOKEN)G.authtoken=process.env.NGROK_AUTHTOKEN;if(Y)G.domain=Y;let X=await fZ(J(G),Z,`ngrok did not produce a URL within ${Z}ms`,(K)=>{K.close()}),V=X.url();if(!V)throw await X.close().catch(()=>{}),Error("ngrok started but did not return a URL");return{url:V,stop:()=>{X.close().catch(()=>{})}}}async function SZ(){try{let $=await import("@ngrok/ngrok"),Q=$.forward??$.default?.forward;if(typeof Q!=="function")throw Error("@ngrok/ngrok does not export forward()");return Q}catch($){let Q=$ instanceof Error?$.message:String($);throw Error(`Unable to load @ngrok/ngrok: ${Q}. Run \`bun install\` or install the package before using --tunnel-provider ngrok.`)}}function yZ($,Q){let Z=$.replace(/^https?:\/\//i,"").replace(/^\*\./,"").replace(/\/+$/,"");if(!Q)return Z;return`${Q}.${Z}`}function fZ($,Q,Z,J){let Y=!1;return new Promise((G,X)=>{let V=setTimeout(()=>{Y=!0,X(Error(Z))},Q);$.then((K)=>{if(clearTimeout(V),Y){J?.(K);return}G(K)},(K)=>{clearTimeout(V),X(K)})})}var D$=P0(import.meta.url);function L3(){return"0.1.41"}function P9($,Q){let Z=Number($);if(!Number.isInteger(Z)||Z<=0)throw new D0(`${Q} must be a positive integer.`);return Z}function R3($){let Q=Number($);if(!Number.isFinite(Q)||Q<0||Q>1)throw new D0("--stream-quality must be a number between 0 and 1.");return Q}var I9=new Set;function U3($){return I9.add($),$}function _3(){for(let $ of I9)try{$.stop()}catch{}I9.clear()}process.on("exit",_3);function u8(){if(!i(M$))k9(M$,{recursive:!0})}function e($){if($)return w9(X0($));for(let Q of A0()){let Z=w9(Q);if(Z)return Z}return null}var $4={at:0,booted:null};function N3(){let $=Date.now();if($4.booted&&$-$4.at<1000)return $4.booted;try{let Q=s("xcrun simctl list devices booted -j",{encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:3000}),Z=JSON.parse(Q),J=new Set;for(let Y of Object.values(Z.devices))for(let G of Y)if(G.state==="Booted")J.add(G.udid);return $4={at:$,booted:J},J}catch{return null}}function w9($){try{if(!i($))return U$("state file missing %s",$),null;let Q=JSON.parse(W$($,"utf-8"));try{process.kill(Q.pid,0)}catch{return U$("helper pid %d dead, removing stale state %s",Q.pid,$),s$($),null}let Z=N3();if(Z&&!Z.has(Q.device)){U$("helper pid %d bound to non-booted device %s — killing stale helper",Q.pid,Q.device),console.error(`[serve-sim] Helper pid ${Q.pid} is bound to device ${Q.device} which is no longer booted — killing stale helper.`);try{process.kill(Q.pid,"SIGTERM")}catch{}try{s$($)}catch{}return null}return U$("state ok pid=%d device=%s port=%d",Q.pid,Q.device,Q.port),Q}catch(Q){return U$("readStateFile threw for %s: %o",$,Q),null}}function F0(){let $=[];for(let Q of A0()){let Z=w9(Q);if(Z)$.push(Z)}return $}function B3($){u8(),f9(X0($.device),JSON.stringify($,null,2)),U$("wrote state pid=%d device=%s port=%d",$.pid,$.device,$.port)}function v$($){if($){U$("clearState device=%s",$);try{s$(X0($))}catch{}}else{U$("clearState (all)");for(let Q of A0())try{s$(Q)}catch{}}}function b9(){try{let $=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Q=JSON.parse($),Z=Object.keys(Q.devices).filter((J)=>/SimRuntime\.iOS-/i.test(J)).sort((J,Y)=>{let G=(J.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number),X=(Y.match(/iOS-(\d+)-(\d+)/)??[]).slice(1).map(Number);return(X[0]??0)-(G[0]??0)||(X[1]??0)-(G[1]??0)});for(let J of Z){let G=(Q.devices[J]??[]).find((X)=>X.isAvailable!==!1&&/^iPhone\b/i.test(X.name));if(G)return{udid:G.udid,name:G.name}}}catch{}return null}function b8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.name}catch{}return null}function g8($){try{let Q=s("xcrun simctl list devices -j",{encoding:"utf-8"}),Z=JSON.parse(Q);for(let J of Object.values(Z.devices))for(let Y of J)if(Y.udid===$)return Y.state==="Booted"}catch{}return!1}function Z4($){try{return process.kill($,0),!0}catch{return!1}}function l8($){try{process.kill($,"SIGTERM")}catch{return}let Q=Date.now()+500;while(Date.now()<Q)try{process.kill($,0),A$(25)}catch{return}try{process.kill($,"SIGKILL")}catch{}let Z=Date.now()+500;while(Date.now()<Z)try{process.kill($,0),A$(25)}catch{return}}function E3($){if(!g8($))try{s(`xcrun simctl boot ${$}`,{encoding:"utf-8",stdio:"pipe"})}catch(Q){let Z=(Q.stderr??Q.message??"").toLowerCase();if(!Z.includes("booted")&&!Z.includes("current state"))throw Error(`Failed to boot device ${$}: ${Q.stderr||Q.message}`)}try{s("open -ga Simulator",{encoding:"utf-8",stdio:"pipe",timeout:3000})}catch{}}function F3(){let $=H3();for(let Q of Object.values($))for(let Z of Q??[])if(Z.family==="IPv4"&&!Z.internal)return Z.address;return null}async function c8($){let Q=new Set(F0().map((Z)=>Z.port));for(let Z=$;Z<$+100;Z++){if(Q.has(Z))continue;if(await j5(Z))return Z}throw Error(`No available port found in range ${$}-${$+99}`)}async function T3($){E3($);try{s(`xcrun simctl bootstatus ${$} -b`,{encoding:"utf-8",stdio:"pipe",timeout:60000})}catch(Q){if(!g8($))console.error(`Device ${$} failed to reach booted state: ${Q.stderr||Q.message}`),process.exit(1)}}function j3($){if(process.argv[0]&&/(^|\/)serve-sim$/.test(process.argv[0]))return{command:process.argv[0],args:$};return{command:process.argv[0],args:[process.argv[1],...$]}}async function O3($,Q=150000){let Z=Date.now();while(Date.now()-Z<Q){let J=e($);if(J)return J;await new Promise((Y)=>setTimeout(Y,200))}return null}async function m8($,Q,Z){f4("startHelper udid=%s port=%d detach=%s",$,Q,Z.detach);let J="127.0.0.1";u8(),v$($),D5(Q);let Y=J$(M$,`server-${$}.log`),G=S9(Y,"w"),{command:X,args:V}=j3([$,"--port",String(Q),"--host",J]),K=v8(X,V,{detached:Z.detach,stdio:["ignore",G,G]});if(y9(G),Z.detach)K.unref();let z=await O3($);if(!z){if(K.pid)l8(K.pid);let W="";try{W=W$(Y,"utf-8").trim()}catch{}console.error(W?`Preview server failed:
143
143
  ${W}`:"Preview server failed to start"),process.exit(1)}return Z.detach?{pid:z.pid}:{pid:z.pid,child:K}}async function D3($,Q,Z){y4("follow devices=%o startPort=%d",$,Q);let J=$.length>0?$.map(o):(()=>{let z=Q$();if(z)return[z];let W=b9();if(!W)console.error("No device specified and no available iOS simulator found."),process.exit(1);if(!Z)console.log(`No booted simulator — booting ${W.name}...`);return[W.udid]})(),Y=new Map,G=[],X=Q;for(let z of J){let W=e(z);if(W){if(!Z){let T=b8(z)??z;if(J.length>1)console.log(`
144
144
  ==> ${T} (${z}) <==`);console.log(` Already running on port ${W.port}`),console.log(` Stream: ${W.streamUrl}`),console.log(` WebSocket: ${W.wsUrl}`)}G.push(W);continue}X=await c8(X);let{child:H}=await m8(z,X,{detach:!1});if(H)Y.set(z,H);let L=e(z)??p$(z,X,"/","127.0.0.1");if(G.push(L),!Z){let T=b8(z)??z;if(J.length>1)console.log(`
145
145
  ==> ${T} (${z}) <==`);console.log(` Stream: ${L.streamUrl}`),console.log(` WebSocket: ${L.wsUrl}`),console.log(` Port: ${X}`)}X++}if(G.length===1){let z=G[0];console.log(JSON.stringify({url:z.url,streamUrl:z.streamUrl,wsUrl:z.wsUrl,port:z.port,device:z.device}))}else console.log(JSON.stringify({devices:G.map((z)=>({url:z.url,streamUrl:z.streamUrl,wsUrl:z.wsUrl,port:z.port,device:z.device}))}));if(Y.size===0)return;let V=!1,K=(z)=>{if(V)return;if(V=!0,!Z)console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serve-sim-sjchmiela",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Stanislaw Chmiela",