@xmaxai/sdk 1.0.0 → 1.0.2

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/README.md +9 -5
  3. package/dist/{chunk-M5QA6LWS.js → chunk-F567ONBH.js} +32 -190
  4. package/dist/chunk-F567ONBH.js.map +1 -0
  5. package/dist/{chunk-PLU2YVAS.js → chunk-TFGYBN3Q.js} +493 -136
  6. package/dist/chunk-TFGYBN3Q.js.map +1 -0
  7. package/dist/index.cjs +76 -294
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +376 -10
  10. package/dist/index.d.ts +376 -10
  11. package/dist/index.js +1003 -9
  12. package/dist/index.js.map +1 -1
  13. package/dist/react/index.cjs +34 -277
  14. package/dist/react/index.cjs.map +1 -1
  15. package/dist/react/index.d.cts +1 -3
  16. package/dist/react/index.d.ts +1 -3
  17. package/dist/react/index.js +6 -26
  18. package/dist/react/index.js.map +1 -1
  19. package/dist/{rtc-manager-gYDkADGw.d.cts → rtc-manager-DnQoznIg.d.cts} +2 -17
  20. package/dist/{rtc-manager-gYDkADGw.d.ts → rtc-manager-DnQoznIg.d.ts} +2 -17
  21. package/dist/video-file-stream-4TAGKL2Q.js +3 -0
  22. package/dist/{video-file-stream-UXJ6TNIB.js.map → video-file-stream-4TAGKL2Q.js.map} +1 -1
  23. package/package.json +1 -6
  24. package/dist/chunk-7X7QCO3O.js +0 -992
  25. package/dist/chunk-7X7QCO3O.js.map +0 -1
  26. package/dist/chunk-EGNRAZ5U.js +0 -432
  27. package/dist/chunk-EGNRAZ5U.js.map +0 -1
  28. package/dist/chunk-M5QA6LWS.js.map +0 -1
  29. package/dist/chunk-PLU2YVAS.js.map +0 -1
  30. package/dist/experimental/index.cjs +0 -3873
  31. package/dist/experimental/index.cjs.map +0 -1
  32. package/dist/experimental/index.d.cts +0 -2
  33. package/dist/experimental/index.d.ts +0 -2
  34. package/dist/experimental/index.js +0 -5
  35. package/dist/experimental/index.js.map +0 -1
  36. package/dist/index-BEgtCLkJ.d.cts +0 -346
  37. package/dist/index-D9wwNTfP.d.ts +0 -346
  38. package/dist/video-file-stream-UXJ6TNIB.js +0 -4
@@ -280,148 +280,13 @@ function createSDKI18n(input) {
280
280
  };
281
281
  }
282
282
 
283
- // src/shared/browser-toast.ts
284
- var TOAST_ROOT_ID = "Xmax-sdk-toast-root";
285
- var TOAST_DURATION_MS = 3e3;
286
- var toastSeed = 0;
287
- var timeoutMap = /* @__PURE__ */ new Map();
288
- var elementMap = /* @__PURE__ */ new Map();
289
- function ensureToastRoot() {
290
- if (typeof document === "undefined") {
291
- return null;
292
- }
293
- let root = document.getElementById(TOAST_ROOT_ID);
294
- if (root) {
295
- return root;
296
- }
297
- root = document.createElement("div");
298
- root.id = TOAST_ROOT_ID;
299
- root.setAttribute("aria-live", "polite");
300
- Object.assign(root.style, {
301
- position: "fixed",
302
- left: "50%",
303
- top: "24px",
304
- transform: "translateX(-50%)",
305
- zIndex: "3000",
306
- display: "flex",
307
- flexDirection: "column",
308
- alignItems: "center",
309
- gap: "12px",
310
- pointerEvents: "none"
311
- });
312
- document.body.appendChild(root);
313
- return root;
314
- }
315
- function createIcon(type) {
316
- const icon = document.createElement("span");
317
- icon.setAttribute("aria-hidden", "true");
318
- icon.innerHTML = type === "success" ? '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 3L4.5 8.5 2 6" stroke="#4ade80" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="5" stroke="#f87171" stroke-width="1.5"/><path d="M4 4l4 4M8 4L4 8" stroke="#f87171" stroke-width="1.5" stroke-linecap="round"/></svg>';
319
- Object.assign(icon.style, {
320
- display: "inline-flex",
321
- width: "12px",
322
- height: "12px",
323
- flexShrink: "0"
324
- });
325
- return icon;
326
- }
327
- function clearToastTimer(id) {
328
- const timer = timeoutMap.get(id);
329
- if (timer) {
330
- window.clearTimeout(timer);
331
- timeoutMap.delete(id);
332
- }
333
- }
334
- function destroyToast(id) {
335
- if (!id) {
336
- timeoutMap.forEach((timer) => window.clearTimeout(timer));
337
- timeoutMap.clear();
338
- elementMap.forEach((element2) => element2.remove());
339
- elementMap.clear();
340
- const root2 = typeof document !== "undefined" ? document.getElementById(TOAST_ROOT_ID) : null;
341
- root2?.remove();
342
- return;
343
- }
344
- clearToastTimer(id);
345
- const element = elementMap.get(id);
346
- element?.remove();
347
- elementMap.delete(id);
348
- const root = typeof document !== "undefined" ? document.getElementById(TOAST_ROOT_ID) : null;
349
- if (root && !root.childElementCount) {
350
- root.remove();
351
- }
352
- }
353
- function showToast(type, message, options) {
354
- const root = ensureToastRoot();
355
- if (!root) {
356
- return "";
357
- }
358
- const id = options?.key || `toast-${Date.now()}-${++toastSeed}`;
359
- clearToastTimer(id);
360
- elementMap.get(id)?.remove();
361
- const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches;
362
- const textSize = isMobile ? "12px" : "14px";
363
- const toast = document.createElement("div");
364
- toast.setAttribute("role", "status");
365
- Object.assign(toast.style, {
366
- pointerEvents: "auto",
367
- display: "flex",
368
- alignItems: "center",
369
- gap: "12px",
370
- minWidth: "240px",
371
- padding: "16px",
372
- borderRadius: "12px",
373
- border: "1px solid rgba(255, 255, 255, 0.20)",
374
- background: "rgba(0, 0, 0, 0.80)",
375
- boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
376
- fontFamily: "Rubik, ui-sans-serif, system-ui, sans-serif",
377
- fontSize: textSize,
378
- fontWeight: "400",
379
- lineHeight: "14px"
380
- });
381
- const content = document.createElement("span");
382
- content.textContent = message;
383
- Object.assign(content.style, {
384
- paddingRight: "0px",
385
- textAlign: "center",
386
- color: "#ffffff",
387
- fontFamily: "Rubik, ui-sans-serif, system-ui, sans-serif",
388
- fontSize: textSize,
389
- fontWeight: "400",
390
- lineHeight: "14px"
391
- });
392
- toast.appendChild(createIcon(type));
393
- toast.appendChild(content);
394
- root.appendChild(toast);
395
- elementMap.set(id, toast);
396
- const timer = window.setTimeout(() => {
397
- destroyToast(id);
398
- }, options?.duration ?? TOAST_DURATION_MS);
399
- timeoutMap.set(id, timer);
400
- return id;
401
- }
402
- var browserToast = {
403
- success(message, options) {
404
- return showToast("success", message, options);
405
- },
406
- error(message, options) {
407
- return showToast("error", message, options);
408
- },
409
- destroy(id) {
410
- destroyToast(id);
411
- }
412
- };
413
-
414
283
  // src/shared/error-utils.ts
415
284
  function notifyError(error, notifier, fallbackMessage) {
416
285
  const resolved = error instanceof Error ? error : Object.assign(new Error(fallbackMessage), { cause: error });
417
- if (!resolved.__XmaxNotified) {
286
+ if (!resolved.__XmaxNotified && notifier) {
418
287
  resolved.__XmaxNotified = true;
419
288
  const message = resolved.message || fallbackMessage;
420
- if (notifier) {
421
- notifier(message, error);
422
- } else if (typeof window !== "undefined" && typeof document !== "undefined") {
423
- browserToast.error(message);
424
- }
289
+ notifier(message, error);
425
290
  }
426
291
  return resolved;
427
292
  }
@@ -483,7 +348,7 @@ var XmaxOpenClient = class {
483
348
  }
484
349
  throw notifyError(error, this.onError, message);
485
350
  }
486
- /** Stop heartbeat, abort in-flight API requests, and suppress error toasts during teardown. */
351
+ /** Stop heartbeat, abort in-flight API requests, and suppress error notifications during teardown. */
487
352
  beginTeardown() {
488
353
  this.suppressErrorNotification = true;
489
354
  this.stopHeartbeat();
@@ -539,7 +404,8 @@ var XmaxOpenClient = class {
539
404
  return p.data;
540
405
  }
541
406
  const mappedMessage = resolveApiErrorMessage(p.code, this.i18n.locale);
542
- const errorMessage = mappedMessage ?? this.i18n.t("errors.requestRetry");
407
+ const backendMessage = typeof p.message === "string" && p.message.trim() ? p.message.trim() : void 0;
408
+ const errorMessage = mappedMessage ?? backendMessage ?? this.i18n.t("errors.requestRetry");
543
409
  throw new Error(errorMessage);
544
410
  } catch (error) {
545
411
  if (this.isAbortError(error)) {
@@ -691,6 +557,7 @@ function createStartRtcRoomEvent(input) {
691
557
  const resolvedPrompt = input.prompt?.trim() ?? "";
692
558
  const refImagePath = input.refImagePath?.trim() || input.refImage?.trim() || void 0;
693
559
  const useRefImage = !!refImagePath;
560
+ const staticImagePath = input.staticImagePath?.trim() || void 0;
694
561
  return {
695
562
  event: "start",
696
563
  user_id: input.userId?.trim() || void 0,
@@ -702,7 +569,9 @@ function createStartRtcRoomEvent(input) {
702
569
  prompt: resolvedPrompt,
703
570
  ref_image_path: useRefImage ? refImagePath : void 0,
704
571
  ref_image: useRefImage ? refImagePath : void 0,
705
- static_generate: input.staticGenerate === void 0 ? void 0 : input.staticGenerate,
572
+ // Instant-only omit when false/undefined so non-instant starts never carry leftover flags.
573
+ static_generate: input.staticGenerate === true ? true : void 0,
574
+ static_image_path: staticImagePath,
706
575
  ...input.extraParams ?? {}
707
576
  }
708
577
  };
@@ -1103,7 +972,7 @@ var _RtcManager = class _RtcManager {
1103
972
  this.lastRemoteSei = null;
1104
973
  this.lastRemoteSeiUserId = null;
1105
974
  this.hasLoggedSeiSendFailure = false;
1106
- if (previousExpectedRemoteSei !== this.expectedRemoteSei && this.remoteVideoUserId) {
975
+ if (previousExpectedRemoteSei !== this.expectedRemoteSei && this.remoteVideoUserId && !this.preserveRemoteDom) {
1107
976
  this.pendingRemoteVideoUserId = this.remoteVideoUserId;
1108
977
  this.remoteVideoUserId = null;
1109
978
  }
@@ -1451,45 +1320,6 @@ var _RtcManager = class _RtcManager {
1451
1320
  sentFrameRate: this.lastObservedSentFrameRate
1452
1321
  });
1453
1322
  }
1454
- /** @deprecated Use {@link ensurePublishedInputBeforeStart}. */
1455
- async waitForRtcOutboundBeforeStart(options) {
1456
- const timeoutMs = options?.timeoutMs ?? PUBLISH_READY_TIMEOUT_MS;
1457
- const publishAgeMs = this.publishStartedAtMs > 0 ? performance.now() - this.publishStartedAtMs : 0;
1458
- const localFramesPromise = publishAgeMs >= PUBLISH_WARM_SKIP_MS ? Promise.resolve({ ready: true, frameCount: 0, elapsedMs: 0 }) : this.waitForPublishedInputFrames({
1459
- minFrames: PUBLISH_START_MIN_FRAMES,
1460
- timeoutMs: PUBLISH_START_FRAME_TIMEOUT_MS
1461
- });
1462
- const [, rtcReady] = await Promise.all([
1463
- localFramesPromise,
1464
- this.waitForLocalPublishReady({ timeoutMs })
1465
- ]);
1466
- return rtcReady;
1467
- }
1468
- /** @deprecated Use {@link waitForRtcOutboundBeforeStart}. */
1469
- async waitForPublishedInputFramesBeforeStart() {
1470
- const waitOptions = this.resolveInputFrameWaitOptions();
1471
- const waitTasks = [
1472
- this.waitForLocalPublishReady({ timeoutMs: PUBLISH_READY_TIMEOUT_MS })
1473
- ];
1474
- if (waitOptions) {
1475
- waitTasks.push(
1476
- this.waitForMinPublishDuration(waitOptions.minPublishMs),
1477
- this.waitForPublishedInputFrames({
1478
- minFrames: waitOptions.minFrames,
1479
- timeoutMs: waitOptions.timeoutMs
1480
- })
1481
- );
1482
- }
1483
- const [rtcResult, ...rest] = await Promise.all(waitTasks);
1484
- const rtcSend = rtcResult;
1485
- const frameResult = waitOptions ? rest[1] : { ready: true, frameCount: 0, elapsedMs: 0 };
1486
- return {
1487
- ...frameResult,
1488
- rtcSendReady: rtcSend.ready,
1489
- rtcSendReason: rtcSend.reason,
1490
- rtcSendElapsedMs: rtcSend.elapsedSincePublishMs
1491
- };
1492
- }
1493
1323
  async waitForMinPublishDuration(minMs) {
1494
1324
  if (minMs <= 0 || this.publishStartedAtMs <= 0) {
1495
1325
  return;
@@ -2453,17 +2283,6 @@ function shouldSampleAtMediaTime(lastMediaTimeS, mediaTimeS) {
2453
2283
  }
2454
2284
  return mediaTimeS > lastMediaTimeS + MEDIA_TIME_EPSILON_S;
2455
2285
  }
2456
- function sampleCanvasFingerprint(ctx, width, height) {
2457
- const points = [
2458
- [0, 0],
2459
- [Math.max(0, (width >> 1) - 1), Math.max(0, (height >> 1) - 1)],
2460
- [Math.max(0, width - 1), Math.max(0, height - 1)]
2461
- ];
2462
- return points.map(([x, y]) => {
2463
- const data = ctx.getImageData(x, y, 1, 1).data;
2464
- return `${data[0]},${data[1]},${data[2]}`;
2465
- }).join("|");
2466
- }
2467
2286
  async function applyPublishTrackConstraints(track, width, height) {
2468
2287
  if (typeof track.applyConstraints !== "function") {
2469
2288
  return;
@@ -2536,12 +2355,6 @@ async function createVideoFileStream(fileOrUrl, options = {}) {
2536
2355
  let lastDrawMediaTimeS = -1;
2537
2356
  let videoFrameCallbackId = null;
2538
2357
  let rafId = null;
2539
- const logPublishFrames = options.logPublishFrames ?? false;
2540
- let publishFrameIndex = 0;
2541
- let lastPublishWallMs = 0;
2542
- let lastPublishMediaTimeS = -1;
2543
- let publishFramesInWindow = 0;
2544
- let publishWindowStartMs = 0;
2545
2358
  const drawFrame = () => {
2546
2359
  if (destroyed) {
2547
2360
  return;
@@ -2555,52 +2368,13 @@ async function createVideoFileStream(fileOrUrl, options = {}) {
2555
2368
  sourceHeight
2556
2369
  );
2557
2370
  };
2558
- const logPublishedFrame = (mediaTimeS, presentedFrames) => {
2559
- if (!logPublishFrames) {
2560
- return;
2561
- }
2562
- publishFrameIndex += 1;
2563
- const wallNowMs = performance.now();
2564
- const wallDeltaMs = lastPublishWallMs > 0 ? wallNowMs - lastPublishWallMs : 0;
2565
- const mediaDeltaS = lastPublishMediaTimeS >= 0 ? mediaTimeS - lastPublishMediaTimeS : 0;
2566
- lastPublishWallMs = wallNowMs;
2567
- lastPublishMediaTimeS = mediaTimeS;
2568
- publishFramesInWindow += 1;
2569
- if (publishWindowStartMs === 0) {
2570
- publishWindowStartMs = wallNowMs;
2571
- }
2572
- const fingerprint = sampleCanvasFingerprint(ctx, outputWidth, outputHeight);
2573
- console.warn("[upload-video \u2192 publishFrame]", {
2574
- frameIndex: publishFrameIndex,
2575
- mediaTimeS: Number(mediaTimeS.toFixed(4)),
2576
- mediaDeltaS: Number(mediaDeltaS.toFixed(4)),
2577
- wallDeltaMs: Number(wallDeltaMs.toFixed(1)),
2578
- effectiveFps: wallDeltaMs > 0 ? Number((1e3 / wallDeltaMs).toFixed(1)) : null,
2579
- presentedFrames,
2580
- size: `${outputWidth}\xD7${outputHeight}`,
2581
- fingerprint
2582
- });
2583
- const windowElapsedMs = wallNowMs - publishWindowStartMs;
2584
- if (windowElapsedMs >= 1e3) {
2585
- console.warn("[upload-video \u2192 publishFps]", {
2586
- framesInLast1s: publishFramesInWindow,
2587
- avgFps: Number((publishFramesInWindow / (windowElapsedMs / 1e3)).toFixed(2)),
2588
- videoCurrentTime: Number(videoEl.currentTime.toFixed(4)),
2589
- videoPaused: videoEl.paused,
2590
- videoReadyState: videoEl.readyState
2591
- });
2592
- publishFramesInWindow = 0;
2593
- publishWindowStartMs = wallNowMs;
2594
- }
2595
- };
2596
- const onPlaybackSample = (mediaTimeS, presentedFrames) => {
2371
+ const onPlaybackSample = (mediaTimeS) => {
2597
2372
  if (destroyed || !shouldSampleAtMediaTime(lastDrawMediaTimeS, mediaTimeS)) {
2598
2373
  return;
2599
2374
  }
2600
2375
  lastDrawMediaTimeS = mediaTimeS;
2601
2376
  lastAdvanceAt = Date.now();
2602
2377
  drawFrame();
2603
- logPublishedFrame(mediaTimeS, presentedFrames);
2604
2378
  };
2605
2379
  const schedulePlaybackWatch = () => {
2606
2380
  if (destroyed) {
@@ -2609,7 +2383,7 @@ async function createVideoFileStream(fileOrUrl, options = {}) {
2609
2383
  const video = videoEl;
2610
2384
  if (video.requestVideoFrameCallback) {
2611
2385
  videoFrameCallbackId = video.requestVideoFrameCallback((_now, metadata) => {
2612
- onPlaybackSample(metadata.mediaTime, metadata.presentedFrames);
2386
+ onPlaybackSample(metadata.mediaTime);
2613
2387
  schedulePlaybackWatch();
2614
2388
  });
2615
2389
  return;
@@ -2620,7 +2394,6 @@ async function createVideoFileStream(fileOrUrl, options = {}) {
2620
2394
  });
2621
2395
  };
2622
2396
  drawFrame();
2623
- logPublishedFrame(videoEl.currentTime);
2624
2397
  schedulePlaybackWatch();
2625
2398
  const ensurePlaying = () => {
2626
2399
  if (destroyed) {
@@ -2642,18 +2415,6 @@ async function createVideoFileStream(fileOrUrl, options = {}) {
2642
2415
  videoEl.addEventListener("ended", ensurePlaying);
2643
2416
  videoEl.addEventListener("stalled", ensurePlaying);
2644
2417
  videoEl.addEventListener("waiting", ensurePlaying);
2645
- if (logPublishFrames) {
2646
- const logPlaybackEvent = (eventName) => {
2647
- console.warn("[upload-video \u2192 playbackEvent]", {
2648
- event: eventName,
2649
- currentTime: Number(videoEl.currentTime.toFixed(4)),
2650
- readyState: videoEl.readyState,
2651
- paused: videoEl.paused
2652
- });
2653
- };
2654
- videoEl.addEventListener("waiting", () => logPlaybackEvent("waiting"));
2655
- videoEl.addEventListener("stalled", () => logPlaybackEvent("stalled"));
2656
- }
2657
2418
  const healthIntervalMs = options.healthIntervalMs ?? 3e3;
2658
2419
  let healthTimer = null;
2659
2420
  if (options.onHealthReport) {
@@ -3151,18 +2912,9 @@ function XmaxProvider({ config, children }) {
3151
2912
  const detachRemoteContainer = react.useCallback(() => {
3152
2913
  rtcManagerRef.current?.setRemoteVideoContainer(null);
3153
2914
  }, []);
3154
- const notifyUserError = react.useCallback((message, error) => {
3155
- if (externalOnError) {
3156
- externalOnError(message, error);
3157
- return;
3158
- }
3159
- if (typeof window !== "undefined" && typeof document !== "undefined") {
3160
- browserToast.error(message);
3161
- }
3162
- }, [externalOnError]);
3163
2915
  const reportError = react.useCallback((error, fallbackMessage) => {
3164
- return notifyError(error, notifyUserError, fallbackMessage ?? sdkI18n.t("errors.requestFailed"));
3165
- }, [notifyUserError, sdkI18n]);
2916
+ return notifyError(error, externalOnError, fallbackMessage ?? sdkI18n.t("errors.requestFailed"));
2917
+ }, [externalOnError, sdkI18n]);
3166
2918
  const rtcManagerRef = react.useRef(null);
3167
2919
  if (!rtcManagerRef.current) {
3168
2920
  rtcManagerRef.current = new RtcManager({
@@ -3191,11 +2943,11 @@ function XmaxProvider({ config, children }) {
3191
2943
  baseUrl: config.baseUrl,
3192
2944
  locale: config.locale,
3193
2945
  i18n: sdkI18n,
3194
- onError: notifyUserError
2946
+ onError: externalOnError
3195
2947
  });
3196
2948
  clientRef.current = created;
3197
2949
  return created;
3198
- }, [config.apiKey, config.authToken, config.baseUrl, config.locale, notifyUserError, sdkI18n]);
2950
+ }, [config.apiKey, config.authToken, config.baseUrl, config.locale, externalOnError, sdkI18n]);
3199
2951
  const ensureConnected = react.useCallback(async () => {
3200
2952
  const client = getClient();
3201
2953
  const rtc = rtcManagerRef.current;
@@ -3605,11 +3357,6 @@ function XmaxProvider({ config, children }) {
3605
3357
  if (!activeTaskUid) {
3606
3358
  return;
3607
3359
  }
3608
- console.log("[Xmax] sendTracks", {
3609
- taskUid: activeTaskUid,
3610
- targetSize: sessionTargetSize,
3611
- tracks: options.tracks
3612
- });
3613
3360
  try {
3614
3361
  const event = createTracksRtcRoomEvent({
3615
3362
  userId: rtc.snapshot.userId ?? void 0,
@@ -3662,9 +3409,6 @@ function XmaxProvider({ config, children }) {
3662
3409
  debugLogRef.current("lifecycle", "warn", "remote output stalled", info);
3663
3410
  };
3664
3411
  }, []);
3665
- const changeModel = react.useCallback(async () => {
3666
- return;
3667
- }, []);
3668
3412
  const bindLocalView = react.useCallback((el) => {
3669
3413
  localContainerRef.current = el;
3670
3414
  rtcManagerRef.current?.setLocalVideoContainer(el);
@@ -3707,7 +3451,6 @@ function XmaxProvider({ config, children }) {
3707
3451
  start,
3708
3452
  sendTracks,
3709
3453
  stop,
3710
- changeModel,
3711
3454
  bindLocalView,
3712
3455
  bindRemoteView
3713
3456
  }), [
@@ -3732,7 +3475,6 @@ function XmaxProvider({ config, children }) {
3732
3475
  start,
3733
3476
  sendTracks,
3734
3477
  stop,
3735
- changeModel,
3736
3478
  bindLocalView,
3737
3479
  bindRemoteView
3738
3480
  ]);
@@ -3935,6 +3677,7 @@ function drawNeon(ctx, segs, container, targetSize, dpr, fitMode, mirrored) {
3935
3677
  }
3936
3678
  var DragTrackController = class {
3937
3679
  constructor(container, options) {
3680
+ this.strokeActive = false;
3938
3681
  this.activePointers = /* @__PURE__ */ new Map();
3939
3682
  this.lastTargetPoints = /* @__PURE__ */ new Map();
3940
3683
  this.segQueue = [];
@@ -3944,6 +3687,8 @@ var DragTrackController = class {
3944
3687
  this.resizeObserver = null;
3945
3688
  this.container = container;
3946
3689
  this.onTracks = options.onTracks;
3690
+ this.onStrokeStart = options.onStrokeStart;
3691
+ this.onStrokeEnd = options.onStrokeEnd;
3947
3692
  this.targetSize = options.targetSize;
3948
3693
  this.fitMode = options.fitMode ?? "cover";
3949
3694
  this.mirrored = options.mirrored ?? false;
@@ -4114,11 +3859,17 @@ var DragTrackController = class {
4114
3859
  this.flushTracks();
4115
3860
  }, 30);
4116
3861
  }
4117
- finishStroke() {
3862
+ finishStroke(emitEnd = false) {
3863
+ const wasActive = this.strokeActive;
3864
+ this.strokeActive = false;
4118
3865
  this.stopSampling();
4119
3866
  this.stopDrawLoop();
4120
3867
  this.segQueue.length = 0;
4121
3868
  this.clearCanvas();
3869
+ if (emitEnd && wasActive && this.onStrokeEnd) {
3870
+ Promise.resolve(this.onStrokeEnd()).catch(() => {
3871
+ });
3872
+ }
4122
3873
  }
4123
3874
  handlePointerDown(e) {
4124
3875
  if (!this.enabled) {
@@ -4142,6 +3893,12 @@ var DragTrackController = class {
4142
3893
  if (targetPoint) {
4143
3894
  this.lastTargetPoints.set(e.pointerId, targetPoint);
4144
3895
  }
3896
+ const strokeWasActive = this.strokeActive;
3897
+ this.strokeActive = true;
3898
+ if (!strokeWasActive && this.onStrokeStart) {
3899
+ Promise.resolve(this.onStrokeStart()).catch(() => {
3900
+ });
3901
+ }
4145
3902
  this.startDrawLoop();
4146
3903
  this.startSampling();
4147
3904
  }
@@ -4174,7 +3931,7 @@ var DragTrackController = class {
4174
3931
  } catch {
4175
3932
  }
4176
3933
  if (this.activePointers.size === 0) {
4177
- this.finishStroke();
3934
+ this.finishStroke(true);
4178
3935
  }
4179
3936
  }
4180
3937
  handlePointerCancel(e) {
@@ -4184,7 +3941,7 @@ var DragTrackController = class {
4184
3941
  this.activePointers.delete(e.pointerId);
4185
3942
  this.lastTargetPoints.delete(e.pointerId);
4186
3943
  if (this.activePointers.size === 0) {
4187
- this.finishStroke();
3944
+ this.finishStroke(true);
4188
3945
  }
4189
3946
  }
4190
3947
  };