@eyepop.ai/eyepop 0.10.0 → 0.11.0

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.
package/dist/index.mjs CHANGED
@@ -9035,6 +9035,39 @@ var LoadFromJob = class extends AbstractJob {
9035
9035
  return response;
9036
9036
  }
9037
9037
  };
9038
+ var LoadLiveIngressJob = class extends AbstractJob {
9039
+ constructor(ingressId, getSession, client, requestLogger) {
9040
+ super(getSession, client, requestLogger);
9041
+ this._ingressId = ingressId;
9042
+ }
9043
+ get [Symbol.toStringTag]() {
9044
+ return "loadLiveIngressJob";
9045
+ }
9046
+ async startJob() {
9047
+ const body = {
9048
+ "sourceType": "LIVE_INGRESS",
9049
+ "liveIngressId": this._ingressId
9050
+ };
9051
+ const session = await this._getSession();
9052
+ const headers = {
9053
+ "Authorization": `Bearer ${session.accessToken}`,
9054
+ "Accept": "application/jsonl",
9055
+ "Content-Type": "application/json"
9056
+ };
9057
+ const patchUrl = `${session.baseUrl.replace(/\/+$/, "")}/pipelines/${session.pipelineId}/source?mode=queue&processing=sync`;
9058
+ this._requestLogger.debug("before PATCH %s with url %s as source", patchUrl, this._ingressId);
9059
+ const response = await this._client.fetch(patchUrl, {
9060
+ headers,
9061
+ method: "PATCH",
9062
+ body: JSON.stringify(body),
9063
+ signal: this._controller.signal,
9064
+ // @ts-ignore
9065
+ dispatcher: this._dispatcher
9066
+ });
9067
+ this._requestLogger.debug("after PATCH %s with url %s as source", patchUrl, this._ingressId);
9068
+ return response;
9069
+ }
9070
+ };
9038
9071
 
9039
9072
  // src/shims/http_client.ts
9040
9073
  var createHttpClient;
@@ -9203,6 +9236,374 @@ var Semaphore = class {
9203
9236
  }
9204
9237
  };
9205
9238
 
9239
+ // node_modules/uuid/dist/esm-node/rng.js
9240
+ import crypto from "crypto";
9241
+ var rnds8Pool = new Uint8Array(256);
9242
+ var poolPtr = rnds8Pool.length;
9243
+ function rng() {
9244
+ if (poolPtr > rnds8Pool.length - 16) {
9245
+ crypto.randomFillSync(rnds8Pool);
9246
+ poolPtr = 0;
9247
+ }
9248
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
9249
+ }
9250
+
9251
+ // node_modules/uuid/dist/esm-node/stringify.js
9252
+ var byteToHex = [];
9253
+ for (let i = 0; i < 256; ++i) {
9254
+ byteToHex.push((i + 256).toString(16).slice(1));
9255
+ }
9256
+ function unsafeStringify(arr, offset = 0) {
9257
+ return byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]];
9258
+ }
9259
+
9260
+ // node_modules/uuid/dist/esm-node/native.js
9261
+ import crypto2 from "crypto";
9262
+ var native_default = {
9263
+ randomUUID: crypto2.randomUUID
9264
+ };
9265
+
9266
+ // node_modules/uuid/dist/esm-node/v4.js
9267
+ function v4(options, buf, offset) {
9268
+ if (native_default.randomUUID && !buf && !options) {
9269
+ return native_default.randomUUID();
9270
+ }
9271
+ options = options || {};
9272
+ const rnds = options.random || (options.rng || rng)();
9273
+ rnds[6] = rnds[6] & 15 | 64;
9274
+ rnds[8] = rnds[8] & 63 | 128;
9275
+ if (buf) {
9276
+ offset = offset || 0;
9277
+ for (let i = 0; i < 16; ++i) {
9278
+ buf[offset + i] = rnds[i];
9279
+ }
9280
+ return buf;
9281
+ }
9282
+ return unsafeStringify(rnds);
9283
+ }
9284
+ var v4_default = v4;
9285
+
9286
+ // src/web_rtc_base.ts
9287
+ var WebRtcBase = class {
9288
+ constructor(getSession, client, requestLogger) {
9289
+ this._pc = null;
9290
+ this._getSession = getSession;
9291
+ this._client = client;
9292
+ this._requestLogger = requestLogger;
9293
+ this._eTag = "";
9294
+ this._queuedCandidates = [];
9295
+ this._offerData = null;
9296
+ }
9297
+ static linkToIceServers(links) {
9298
+ const servers = links !== null ? links.split(", ").map((link) => {
9299
+ const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);
9300
+ if (!m) {
9301
+ return null;
9302
+ }
9303
+ const iceServer = {
9304
+ urls: [m[1]],
9305
+ username: Whip.unquoteCredential(m[3]),
9306
+ credential: Whip.unquoteCredential(m[4])
9307
+ // credentialType: m[3]? "password" : undefined
9308
+ };
9309
+ return iceServer;
9310
+ }) : [];
9311
+ return servers.filter(function(el) {
9312
+ return el != null;
9313
+ });
9314
+ }
9315
+ static editAnswer(answer, videoCodec, audioCodec, videoBitrate, audioBitrate, audioVoice) {
9316
+ const sections = answer.split("m=");
9317
+ for (let i = 0; i < sections.length; i++) {
9318
+ const section = sections[i];
9319
+ if (section.startsWith("video")) {
9320
+ sections[i] = Whip.setVideoBitrate(Whip.setCodec(section, videoCodec), videoBitrate);
9321
+ } else if (section.startsWith("audio")) {
9322
+ sections[i] = Whip.setAudioBitrate(Whip.setCodec(section, audioCodec), audioBitrate, audioVoice);
9323
+ }
9324
+ }
9325
+ return sections.join("m=");
9326
+ }
9327
+ static setCodec(section, codec) {
9328
+ const lines = section.split("\r\n");
9329
+ const lines2 = [];
9330
+ const payloadFormats = [];
9331
+ for (const line of lines) {
9332
+ if (!line.startsWith("a=rtpmap:")) {
9333
+ lines2.push(line);
9334
+ } else {
9335
+ if (codec && line.toLowerCase().includes(codec)) {
9336
+ payloadFormats.push(line.slice("a=rtpmap:".length).split(" ")[0]);
9337
+ lines2.push(line);
9338
+ }
9339
+ }
9340
+ }
9341
+ const lines3 = [];
9342
+ for (const line of lines2) {
9343
+ if (line.startsWith("a=fmtp:")) {
9344
+ if (payloadFormats.includes(line.slice("a=fmtp:".length).split(" ")[0])) {
9345
+ lines3.push(line);
9346
+ }
9347
+ } else if (line.startsWith("a=rtcp-fb:")) {
9348
+ if (payloadFormats.includes(line.slice("a=rtcp-fb:".length).split(" ")[0])) {
9349
+ lines3.push(line);
9350
+ }
9351
+ } else {
9352
+ lines3.push(line);
9353
+ }
9354
+ }
9355
+ return lines3.join("\r\n");
9356
+ }
9357
+ static setVideoBitrate(section, bitrate) {
9358
+ let lines = section.split("\r\n");
9359
+ for (let i = 0; i < lines.length; i++) {
9360
+ if (lines[i].startsWith("c=")) {
9361
+ lines = [...lines.slice(0, i + 1), "b=TIAS:" + (bitrate * 1024).toString(), ...lines.slice(i + 1)];
9362
+ break;
9363
+ }
9364
+ }
9365
+ return lines.join("\r\n");
9366
+ }
9367
+ static setAudioBitrate(section, bitrate, voice) {
9368
+ let opusPayloadFormat = "";
9369
+ let lines = section.split("\r\n");
9370
+ for (let i = 0; i < lines.length; i++) {
9371
+ if (lines[i].startsWith("a=rtpmap:") && lines[i].toLowerCase().includes("opus/")) {
9372
+ opusPayloadFormat = lines[i].slice("a=rtpmap:".length).split(" ")[0];
9373
+ break;
9374
+ }
9375
+ }
9376
+ if (opusPayloadFormat === "") {
9377
+ return section;
9378
+ }
9379
+ for (let i = 0; i < lines.length; i++) {
9380
+ if (lines[i].startsWith("a=fmtp:" + opusPayloadFormat + " ")) {
9381
+ if (voice) {
9382
+ lines[i] = "a=fmtp:" + opusPayloadFormat + " minptime=10;useinbandfec=1;maxaveragebitrate=" + ((bitrate ?? 0) * 1024).toString();
9383
+ } else {
9384
+ lines[i] = "a=fmtp:" + opusPayloadFormat + " maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate" + ((bitrate ?? 0) * 1024).toString();
9385
+ }
9386
+ }
9387
+ }
9388
+ return lines.join("\r\n");
9389
+ }
9390
+ static parseOffer(offer) {
9391
+ let medias = [];
9392
+ let iceUfrag = "";
9393
+ let icePwd = "";
9394
+ for (const line of offer.split("\r\n")) {
9395
+ if (line.startsWith("m=")) {
9396
+ medias.push(line.slice("m=".length));
9397
+ } else if (iceUfrag === "" && line.startsWith("a=ice-ufrag:")) {
9398
+ iceUfrag = line.slice("a=ice-ufrag:".length);
9399
+ } else if (icePwd === "" && line.startsWith("a=ice-pwd:")) {
9400
+ icePwd = line.slice("a=ice-pwd:".length);
9401
+ }
9402
+ }
9403
+ return {
9404
+ iceUfrag,
9405
+ icePwd,
9406
+ medias
9407
+ };
9408
+ }
9409
+ static generateSdpFragment(offerData, candidates) {
9410
+ const candidatesByMedia = /* @__PURE__ */ new Map();
9411
+ for (const candidate of candidates) {
9412
+ if (candidate.sdpMLineIndex !== null) {
9413
+ const mid2 = candidate.sdpMLineIndex;
9414
+ let matched = candidatesByMedia.get(mid2);
9415
+ if (matched === void 0) {
9416
+ matched = [];
9417
+ candidatesByMedia.set(mid2, matched);
9418
+ }
9419
+ matched.push(candidate);
9420
+ }
9421
+ }
9422
+ let frag = "a=ice-ufrag:" + offerData.iceUfrag + "\r\na=ice-pwd:" + offerData.icePwd + "\r\n";
9423
+ let mid = 0;
9424
+ for (const media of offerData.medias) {
9425
+ let matched = candidatesByMedia.get(mid);
9426
+ if (matched !== void 0) {
9427
+ frag += "m=" + media + "\r\na=mid:" + mid + "\r\n";
9428
+ for (const candidate of matched) {
9429
+ frag += "a=" + candidate.candidate + "\r\n";
9430
+ }
9431
+ }
9432
+ mid++;
9433
+ }
9434
+ return frag;
9435
+ }
9436
+ static unquoteCredential(v) {
9437
+ if (!v) {
9438
+ return "";
9439
+ }
9440
+ return JSON.parse(`"${v}"`);
9441
+ }
9442
+ };
9443
+
9444
+ // src/whip.ts
9445
+ var Whip = class _Whip extends WebRtcBase {
9446
+ constructor(stream, getSession, client, requestLogger) {
9447
+ super(getSession, client, requestLogger);
9448
+ this._ingressId = v4_default().toString();
9449
+ this._stream = stream;
9450
+ }
9451
+ ingressId() {
9452
+ if (!this._ingressId) {
9453
+ throw new Error("not initialized");
9454
+ }
9455
+ return this._ingressId;
9456
+ }
9457
+ async start() {
9458
+ const session = await this._getSession();
9459
+ const ingressUrl = new URL("/liveIngress/whip/" + this._ingressId, session.baseUrl);
9460
+ this._requestLogger.debug("before GET: %s", ingressUrl);
9461
+ const headers = {
9462
+ "Authorization": `Bearer ${session.accessToken}`
9463
+ };
9464
+ const response = await this._client.fetch(ingressUrl, {
9465
+ headers,
9466
+ method: "GET"
9467
+ });
9468
+ if (response.status >= 300) {
9469
+ return Promise.reject(`unknown status code for GET '${ingressUrl}': ${response.status} (${response.statusText})`);
9470
+ }
9471
+ return this.onIceServers(response);
9472
+ }
9473
+ async onIceServers(response) {
9474
+ if (!this._stream) {
9475
+ throw new Error("onIceServers no stream");
9476
+ }
9477
+ const stream = this._stream;
9478
+ const pc = new RTCPeerConnection({
9479
+ iceServers: _Whip.linkToIceServers(response.headers.get("Link"))
9480
+ });
9481
+ this._pc = pc;
9482
+ pc.onicecandidate = (evt) => this.onLocalCandidate(evt);
9483
+ return new Promise(async (resolve2, reject) => {
9484
+ pc.oniceconnectionstatechange = () => {
9485
+ switch (pc.iceConnectionState) {
9486
+ case "failed":
9487
+ case "disconnected":
9488
+ reject(`peer connection: ${pc.iceConnectionState}`);
9489
+ break;
9490
+ case "connected":
9491
+ resolve2(this);
9492
+ break;
9493
+ }
9494
+ };
9495
+ stream.getTracks().forEach((track) => {
9496
+ this._requestLogger.debug("onIceServers our stream has track: %s", track);
9497
+ pc.addTrack(track, stream);
9498
+ });
9499
+ stream.addEventListener("addtrack", (trackEvent) => {
9500
+ this._requestLogger.debug("onIceServers got lazy track: ", trackEvent.track);
9501
+ pc.addTrack(trackEvent.track, stream);
9502
+ });
9503
+ await this.onLocalOffer(await pc.createOffer());
9504
+ });
9505
+ }
9506
+ async onLocalCandidate(evt) {
9507
+ if (evt.candidate !== null) {
9508
+ if (this._eTag === "") {
9509
+ this._queuedCandidates.push(evt.candidate);
9510
+ } else {
9511
+ await this.sendLocalCandidates([evt.candidate]);
9512
+ }
9513
+ }
9514
+ return this;
9515
+ }
9516
+ async onLocalOffer(offer) {
9517
+ if (!this._pc) {
9518
+ throw new Error("onLocalOffer no peer connection");
9519
+ }
9520
+ if (!offer.sdp) {
9521
+ throw new Error("onLocalOffer no sdp");
9522
+ }
9523
+ this._offerData = _Whip.parseOffer(offer.sdp);
9524
+ await this._pc.setLocalDescription(offer);
9525
+ const session = await this._getSession();
9526
+ const ingressUrl = new URL("/liveIngress/whip/" + this._ingressId, session.baseUrl);
9527
+ this._requestLogger.debug("before GET: %s", ingressUrl);
9528
+ const headers = {
9529
+ "Authorization": `Bearer ${session.accessToken}`,
9530
+ "Content-Type": "application/sdp"
9531
+ };
9532
+ const response = await this._client.fetch(ingressUrl, {
9533
+ headers,
9534
+ method: "POST",
9535
+ body: offer.sdp
9536
+ });
9537
+ if (response.status != 201) {
9538
+ return Promise.reject(`unknown status code for POST '${ingressUrl}': ${response.status} (${response.statusText})`);
9539
+ }
9540
+ this._eTag = response.headers.get("ETag") ?? "";
9541
+ return this.onRemoteAnswer(new RTCSessionDescription({
9542
+ type: "answer",
9543
+ sdp: await response.text()
9544
+ }));
9545
+ }
9546
+ async onRemoteAnswer(answer) {
9547
+ if (!this._pc) {
9548
+ throw new Error("onLocalOffer no peer connection");
9549
+ }
9550
+ answer = new RTCSessionDescription({
9551
+ type: "answer",
9552
+ sdp: _Whip.editAnswer(
9553
+ answer.sdp,
9554
+ "h264/90000",
9555
+ void 0,
9556
+ 5e3,
9557
+ void 0,
9558
+ true
9559
+ )
9560
+ });
9561
+ await this._pc.setRemoteDescription(new RTCSessionDescription(answer));
9562
+ if (this._queuedCandidates.length !== 0) {
9563
+ await this.sendLocalCandidates(this._queuedCandidates);
9564
+ this._queuedCandidates = [];
9565
+ }
9566
+ return this;
9567
+ }
9568
+ async sendLocalCandidates(candidates) {
9569
+ if (!this._offerData) {
9570
+ throw new Error("sendLocalCandidates no offerData");
9571
+ }
9572
+ const session = await this._getSession();
9573
+ const ingressUrl = new URL("/liveIngress/whip/" + this._ingressId, session.baseUrl);
9574
+ this._requestLogger.debug("before PATCH: %s", ingressUrl);
9575
+ const headers = {
9576
+ "Authorization": `Bearer ${session.accessToken}`,
9577
+ "Content-Type": "application/trickle-ice-sdpfrag",
9578
+ "If-Match": this._eTag
9579
+ };
9580
+ const response = await this._client.fetch(ingressUrl, {
9581
+ headers,
9582
+ method: "PATCH",
9583
+ body: _Whip.generateSdpFragment(this._offerData, candidates)
9584
+ });
9585
+ if (response.status != 204) {
9586
+ return Promise.reject(`unknown status code for PATCH '${ingressUrl}': ${response.status} (${response.statusText})`);
9587
+ }
9588
+ return this;
9589
+ }
9590
+ async close() {
9591
+ const pc = this._pc;
9592
+ const stream = this._stream;
9593
+ this._stream = null;
9594
+ this._pc = null;
9595
+ if (stream) {
9596
+ const tracks = stream.getTracks();
9597
+ tracks.forEach((track) => {
9598
+ track.stop();
9599
+ });
9600
+ }
9601
+ if (pc) {
9602
+ return pc.close();
9603
+ }
9604
+ }
9605
+ };
9606
+
9206
9607
  // src/shims/local_file.ts
9207
9608
  var resolve;
9208
9609
  if ("document" in globalThis && "implementation" in globalThis.document) {
@@ -9258,6 +9659,8 @@ var Endpoint = class {
9258
9659
  this._client = null;
9259
9660
  this._state = "Idle" /* Idle */;
9260
9661
  this._stateChangeHandler = null;
9662
+ this._ingressEventHandler = null;
9663
+ this._ingressEventWs = null;
9261
9664
  this._limit = new Semaphore(this._options.jobQueueLength ?? 1024);
9262
9665
  let rootLogger;
9263
9666
  if (options.logger) {
@@ -9272,6 +9675,51 @@ var Endpoint = class {
9272
9675
  this._stateChangeHandler = handler;
9273
9676
  return this;
9274
9677
  }
9678
+ async startIngressWs() {
9679
+ if (this._ingressEventWs || !this._baseUrl || !this._client) {
9680
+ return;
9681
+ }
9682
+ try {
9683
+ const getTokenUrl = `${this._baseUrl}/liveIngress/events/token`;
9684
+ const headers = {
9685
+ "Authorization": await this.authorizationHeader()
9686
+ };
9687
+ this._requestLogger.debug("before GET %s", getTokenUrl);
9688
+ let response = await this._client.fetch(getTokenUrl, { headers });
9689
+ if (response.status == 401) {
9690
+ this._requestLogger.debug("after GET %s: 401, about to retry with fresh access token", getTokenUrl);
9691
+ this._token = null;
9692
+ const headers2 = {
9693
+ "Authorization": await this.authorizationHeader()
9694
+ };
9695
+ this.updateState("FetchConfig" /* FetchConfig */);
9696
+ response = await this._client.fetch(getTokenUrl, { headers: headers2 });
9697
+ }
9698
+ if (response.status != 200) {
9699
+ const message = await response.text();
9700
+ return Promise.reject(`Unexpected status ${response.status}: ${message}`);
9701
+ }
9702
+ const wsAuthToken = await response.json();
9703
+ const wsUrl = this._baseUrl.startsWith("https://") ? `${this._baseUrl.replace("https://", "wss://")}/liveIngress/events/${wsAuthToken.token}` : `${this._baseUrl.replace("http://", "ws://")}/liveIngress/events/${wsAuthToken.token}`;
9704
+ this._ingressEventWs = new WebSocket(wsUrl);
9705
+ this._ingressEventWs.addEventListener("message", (event) => {
9706
+ const ingressEvent = JSON.parse(event.data);
9707
+ if (this._ingressEventHandler) {
9708
+ this._ingressEventHandler(ingressEvent);
9709
+ }
9710
+ });
9711
+ } finally {
9712
+ this.updateState();
9713
+ }
9714
+ }
9715
+ onIngressEvent(handler) {
9716
+ this._ingressEventHandler = handler;
9717
+ this.startIngressWs().then(() => {
9718
+ }).catch((reason) => {
9719
+ this._logger.warn("unexpected error starting the ingress event handler: %s", reason);
9720
+ });
9721
+ return this;
9722
+ }
9275
9723
  state() {
9276
9724
  return this._state;
9277
9725
  }
@@ -9304,9 +9752,18 @@ var Endpoint = class {
9304
9752
  pipelineId: this._pipelineId
9305
9753
  };
9306
9754
  }
9755
+ async liveIngress(stream) {
9756
+ if (!this._baseUrl || !this._client) {
9757
+ return Promise.reject("endpoint not connected, use connect() before ingress()");
9758
+ }
9759
+ const job = new Whip(stream, async () => {
9760
+ return this.session();
9761
+ }, this._client, this._requestLogger);
9762
+ return job.start();
9763
+ }
9307
9764
  async upload(params) {
9308
9765
  if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
9309
- return Promise.reject("endpoint not connected, use open() before upload()");
9766
+ return Promise.reject("endpoint not connected, use connect() before upload()");
9310
9767
  }
9311
9768
  params = await resolve(params);
9312
9769
  if (!params.mimeType) {
@@ -9325,7 +9782,7 @@ var Endpoint = class {
9325
9782
  }
9326
9783
  async loadFrom(location) {
9327
9784
  if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
9328
- throw new Error("endpoint not connected, use open() before loadFrom()");
9785
+ throw new Error("endpoint not connected, use connect() before loadFrom()");
9329
9786
  }
9330
9787
  await this._limit.acquire();
9331
9788
  this.updateState();
@@ -9338,6 +9795,21 @@ var Endpoint = class {
9338
9795
  this.jobStatus(job, statusCode);
9339
9796
  });
9340
9797
  }
9798
+ async loadLiveIngress(ingressId) {
9799
+ if (!this._baseUrl || !this._pipelineId || !this._client || !this._limit) {
9800
+ throw new Error("endpoint not connected, use connect() before loadLiveIngress()");
9801
+ }
9802
+ await this._limit.acquire();
9803
+ this.updateState();
9804
+ const job = new LoadLiveIngressJob(ingressId, async () => {
9805
+ return this.session();
9806
+ }, this._client, this._requestLogger);
9807
+ return job.start(() => {
9808
+ this.jobDone(job);
9809
+ }, (statusCode) => {
9810
+ this.jobStatus(job, statusCode);
9811
+ });
9812
+ }
9341
9813
  // noinspection TypeScriptValidateTypes
9342
9814
  async connect() {
9343
9815
  if (this._client) {
@@ -9375,6 +9847,9 @@ var Endpoint = class {
9375
9847
  }
9376
9848
  this._requestLogger.debug("after PATCH %s", stop_url);
9377
9849
  }
9850
+ if (!this._ingressEventWs && this._ingressEventHandler) {
9851
+ await this.startIngressWs();
9852
+ }
9378
9853
  return result;
9379
9854
  }
9380
9855
  jobDone(job) {
@@ -9537,7 +10012,7 @@ var _EyePopPlot = class _EyePopPlot {
9537
10012
  }
9538
10013
  }
9539
10014
  }
9540
- object(obj, x_scale, y_scale) {
10015
+ object(obj, x_scale = 1, y_scale = 1) {
9541
10016
  const context = this._context;
9542
10017
  const x = obj.x * x_scale;
9543
10018
  const y = obj.y * y_scale;