@pose-tracker/pose-estimation-web 0.1.0 → 0.2.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/README.md +21 -4
- package/dist/index.cjs +536 -129
- package/dist/index.d.cts +100 -11
- package/dist/index.d.ts +100 -11
- package/dist/index.js +536 -130
- package/dist/pose-tracker.global.js +6 -5
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var jsSha256 = require('js-sha256');
|
|
4
4
|
|
|
5
5
|
// src/version.ts
|
|
6
|
-
var SDK_VERSION = "0.
|
|
6
|
+
var SDK_VERSION = "0.2.0";
|
|
7
7
|
var SDK_NAME = "posetracker-web";
|
|
8
8
|
|
|
9
9
|
// src/api/configure.ts
|
|
@@ -461,6 +461,131 @@ function stopMediaStream(stream) {
|
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
// src/media/resolveMedia.ts
|
|
465
|
+
function isVideoElement(v) {
|
|
466
|
+
return typeof HTMLVideoElement !== "undefined" && v instanceof HTMLVideoElement;
|
|
467
|
+
}
|
|
468
|
+
function isImageElement(v) {
|
|
469
|
+
return typeof HTMLImageElement !== "undefined" && v instanceof HTMLImageElement;
|
|
470
|
+
}
|
|
471
|
+
function isImageBitmap(v) {
|
|
472
|
+
return typeof ImageBitmap !== "undefined" && v instanceof ImageBitmap;
|
|
473
|
+
}
|
|
474
|
+
function mediaSize(el) {
|
|
475
|
+
if (isImageBitmap(el)) {
|
|
476
|
+
return { width: el.width || 0, height: el.height || 0 };
|
|
477
|
+
}
|
|
478
|
+
if (isVideoElement(el)) {
|
|
479
|
+
return { width: el.videoWidth || 0, height: el.videoHeight || 0 };
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
width: el.naturalWidth || el.width || 0,
|
|
483
|
+
height: el.naturalHeight || el.height || 0
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
async function resolveObjectUrl(src) {
|
|
487
|
+
if (typeof src === "string") {
|
|
488
|
+
return { url: src, revoke: () => void 0 };
|
|
489
|
+
}
|
|
490
|
+
const url = URL.createObjectURL(src);
|
|
491
|
+
return {
|
|
492
|
+
url,
|
|
493
|
+
revoke: () => {
|
|
494
|
+
try {
|
|
495
|
+
URL.revokeObjectURL(url);
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function waitForVideoReady(video, timeoutMs = 15e3) {
|
|
502
|
+
return new Promise((resolve, reject) => {
|
|
503
|
+
if (video.readyState >= 2 && video.videoWidth > 0) {
|
|
504
|
+
resolve();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const onReady = () => {
|
|
508
|
+
cleanup();
|
|
509
|
+
resolve();
|
|
510
|
+
};
|
|
511
|
+
const onError = () => {
|
|
512
|
+
cleanup();
|
|
513
|
+
reject(new Error("Failed to load video source"));
|
|
514
|
+
};
|
|
515
|
+
const timer = setTimeout(() => {
|
|
516
|
+
cleanup();
|
|
517
|
+
reject(new Error("Timed out waiting for video metadata"));
|
|
518
|
+
}, timeoutMs);
|
|
519
|
+
const cleanup = () => {
|
|
520
|
+
clearTimeout(timer);
|
|
521
|
+
video.removeEventListener("loadeddata", onReady);
|
|
522
|
+
video.removeEventListener("error", onError);
|
|
523
|
+
};
|
|
524
|
+
video.addEventListener("loadeddata", onReady);
|
|
525
|
+
video.addEventListener("error", onError);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function waitForImageReady(img, timeoutMs = 15e3) {
|
|
529
|
+
return new Promise((resolve, reject) => {
|
|
530
|
+
if (img.complete && img.naturalWidth > 0) {
|
|
531
|
+
resolve();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const onReady = () => {
|
|
535
|
+
cleanup();
|
|
536
|
+
resolve();
|
|
537
|
+
};
|
|
538
|
+
const onError = () => {
|
|
539
|
+
cleanup();
|
|
540
|
+
reject(new Error("Failed to load image source"));
|
|
541
|
+
};
|
|
542
|
+
const timer = setTimeout(() => {
|
|
543
|
+
cleanup();
|
|
544
|
+
reject(new Error("Timed out waiting for image"));
|
|
545
|
+
}, timeoutMs);
|
|
546
|
+
const cleanup = () => {
|
|
547
|
+
clearTimeout(timer);
|
|
548
|
+
img.removeEventListener("load", onReady);
|
|
549
|
+
img.removeEventListener("error", onError);
|
|
550
|
+
};
|
|
551
|
+
img.addEventListener("load", onReady);
|
|
552
|
+
img.addEventListener("error", onError);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
async function imageBitmapToObjectUrl(bitmap) {
|
|
556
|
+
const canvas = document.createElement("canvas");
|
|
557
|
+
canvas.width = bitmap.width;
|
|
558
|
+
canvas.height = bitmap.height;
|
|
559
|
+
const ctx = canvas.getContext("2d");
|
|
560
|
+
if (!ctx) throw new Error("2d context unavailable");
|
|
561
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
562
|
+
const blob = await new Promise((resolve, reject) => {
|
|
563
|
+
canvas.toBlob((b) => b ? resolve(b) : reject(new Error("toBlob failed")), "image/png");
|
|
564
|
+
});
|
|
565
|
+
return resolveObjectUrl(blob);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/types/source.ts
|
|
569
|
+
function normalizePoseSource(source, extras) {
|
|
570
|
+
if (source && typeof source === "object" && "type" in source) {
|
|
571
|
+
return source;
|
|
572
|
+
}
|
|
573
|
+
const kind = source === "video" || source === "image" || source === "camera" ? source : "camera";
|
|
574
|
+
if (kind === "camera") {
|
|
575
|
+
return { type: "camera", facingMode: extras?.facingMode };
|
|
576
|
+
}
|
|
577
|
+
const src = extras?.sourceFile ?? extras?.sourceUrl;
|
|
578
|
+
if (!src) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`PoseSource type "${kind}" requires sourceUrl or sourceFile (or pass a full PoseSource object)`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (kind === "video") {
|
|
584
|
+
return { type: "video", src };
|
|
585
|
+
}
|
|
586
|
+
return { type: "image", src };
|
|
587
|
+
}
|
|
588
|
+
|
|
464
589
|
// src/exercises/aliases.ts
|
|
465
590
|
var EXERCISE_ALIASES = {
|
|
466
591
|
face_squat: "squat",
|
|
@@ -913,6 +1038,18 @@ async function resolvePoseDetection(options) {
|
|
|
913
1038
|
}
|
|
914
1039
|
|
|
915
1040
|
// src/models/adapters/blazepose.ts
|
|
1041
|
+
function blazeInputSize(input) {
|
|
1042
|
+
if (typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement) {
|
|
1043
|
+
return { vw: input.videoWidth || 0, vh: input.videoHeight || 0 };
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement) {
|
|
1046
|
+
return { vw: input.naturalWidth || input.width || 0, vh: input.naturalHeight || input.height || 0 };
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
|
|
1049
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1050
|
+
}
|
|
1051
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1052
|
+
}
|
|
916
1053
|
var BLAZE_TO_COCO = {
|
|
917
1054
|
nose: "nose",
|
|
918
1055
|
left_eye: "left_eye",
|
|
@@ -956,11 +1093,12 @@ var BlazePoseAdapter = class {
|
|
|
956
1093
|
}
|
|
957
1094
|
);
|
|
958
1095
|
}
|
|
959
|
-
async estimate(
|
|
1096
|
+
async estimate(input, options) {
|
|
960
1097
|
if (!this.detector) throw new Error("BlazePose adapter not loaded");
|
|
961
|
-
|
|
1098
|
+
const size = blazeInputSize(input);
|
|
1099
|
+
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
962
1100
|
const t0 = performance.now();
|
|
963
|
-
const poses = await this.detector.estimatePoses(
|
|
1101
|
+
const poses = await this.detector.estimatePoses(input, {
|
|
964
1102
|
flipHorizontal: false,
|
|
965
1103
|
maxPoses: 1
|
|
966
1104
|
});
|
|
@@ -980,15 +1118,15 @@ var BlazePoseAdapter = class {
|
|
|
980
1118
|
letterbox: {
|
|
981
1119
|
offsetX: 0,
|
|
982
1120
|
offsetY: 0,
|
|
983
|
-
drawW:
|
|
984
|
-
drawH:
|
|
985
|
-
vw:
|
|
986
|
-
vh:
|
|
1121
|
+
drawW: size.vw,
|
|
1122
|
+
drawH: size.vh,
|
|
1123
|
+
vw: size.vw,
|
|
1124
|
+
vh: size.vh
|
|
987
1125
|
}
|
|
988
1126
|
};
|
|
989
1127
|
}
|
|
990
|
-
const vw =
|
|
991
|
-
const vh =
|
|
1128
|
+
const vw = size.vw || 1;
|
|
1129
|
+
const vh = size.vh || 1;
|
|
992
1130
|
const dispW = options.displayWidth || 1;
|
|
993
1131
|
const dispH = options.displayHeight || 1;
|
|
994
1132
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -1054,6 +1192,18 @@ var BlazePoseAdapter = class {
|
|
|
1054
1192
|
};
|
|
1055
1193
|
|
|
1056
1194
|
// src/models/adapters/movenet.ts
|
|
1195
|
+
function inputSize(input) {
|
|
1196
|
+
if (typeof HTMLVideoElement !== "undefined" && input instanceof HTMLVideoElement) {
|
|
1197
|
+
return { vw: input.videoWidth || 0, vh: input.videoHeight || 0 };
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof HTMLImageElement !== "undefined" && input instanceof HTMLImageElement) {
|
|
1200
|
+
return { vw: input.naturalWidth || input.width || 0, vh: input.naturalHeight || input.height || 0 };
|
|
1201
|
+
}
|
|
1202
|
+
if (typeof HTMLCanvasElement !== "undefined" && input instanceof HTMLCanvasElement) {
|
|
1203
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1204
|
+
}
|
|
1205
|
+
return { vw: input.width || 0, vh: input.height || 0 };
|
|
1206
|
+
}
|
|
1057
1207
|
var INPUT_SIZE = 192;
|
|
1058
1208
|
var SMOOTH_ALPHA = 0.5;
|
|
1059
1209
|
function modelNormToVideo(xNorm, yNorm, lb) {
|
|
@@ -1117,9 +1267,10 @@ var MoveNetGraphAdapter = class {
|
|
|
1117
1267
|
if (!this.offCtx) throw new Error("2d context unavailable");
|
|
1118
1268
|
return this.offCtx;
|
|
1119
1269
|
}
|
|
1120
|
-
prepareInput(
|
|
1121
|
-
const vw
|
|
1122
|
-
const
|
|
1270
|
+
prepareInput(input) {
|
|
1271
|
+
const { vw: rawW, vh: rawH } = inputSize(input);
|
|
1272
|
+
const vw = rawW || 1;
|
|
1273
|
+
const vh = rawH || 1;
|
|
1123
1274
|
const scale = Math.min(INPUT_SIZE / vw, INPUT_SIZE / vh);
|
|
1124
1275
|
const drawW = vw * scale;
|
|
1125
1276
|
const drawH = vh * scale;
|
|
@@ -1129,7 +1280,7 @@ var MoveNetGraphAdapter = class {
|
|
|
1129
1280
|
const c2d = this.poseCanvasCtx();
|
|
1130
1281
|
c2d.fillStyle = "#000";
|
|
1131
1282
|
c2d.fillRect(0, 0, INPUT_SIZE, INPUT_SIZE);
|
|
1132
|
-
c2d.drawImage(
|
|
1283
|
+
c2d.drawImage(input, 0, 0, vw, vh, offsetX, offsetY, drawW, drawH);
|
|
1133
1284
|
return this.offscreen;
|
|
1134
1285
|
}
|
|
1135
1286
|
smooth(raw) {
|
|
@@ -1145,16 +1296,19 @@ var MoveNetGraphAdapter = class {
|
|
|
1145
1296
|
}
|
|
1146
1297
|
return this.smoothed;
|
|
1147
1298
|
}
|
|
1148
|
-
async estimate(
|
|
1299
|
+
async estimate(input, options) {
|
|
1149
1300
|
if (!this.tf || !this.model) throw new Error("MoveNet adapter not loaded");
|
|
1150
|
-
|
|
1301
|
+
const size = inputSize(input);
|
|
1302
|
+
if (!(size.vw > 0 && size.vh > 0)) return null;
|
|
1151
1303
|
const t0 = performance.now();
|
|
1152
|
-
const canvas = this.prepareInput(
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1304
|
+
const canvas = this.prepareInput(input);
|
|
1305
|
+
const tensorIn = this.tf.tidy(
|
|
1306
|
+
() => this.tf.expandDims(this.tf.browser.fromPixels(canvas), 0)
|
|
1307
|
+
);
|
|
1308
|
+
const out = this.model.execute(tensorIn);
|
|
1155
1309
|
const tensor = Array.isArray(out) ? out[0] : out;
|
|
1156
1310
|
const data = tensor.dataSync();
|
|
1157
|
-
|
|
1311
|
+
tensorIn.dispose();
|
|
1158
1312
|
if (Array.isArray(out)) out.forEach((t) => t.dispose());
|
|
1159
1313
|
else out.dispose();
|
|
1160
1314
|
const inferenceMs = performance.now() - t0;
|
|
@@ -1283,11 +1437,11 @@ var SHELL_CSS = `
|
|
|
1283
1437
|
position: relative; width: 100%; height: 100%; min-height: 240px;
|
|
1284
1438
|
background: #000; overflow: hidden; isolation: isolate;
|
|
1285
1439
|
}
|
|
1286
|
-
.pt-root video, .pt-root canvas.pt-overlay {
|
|
1440
|
+
.pt-root video, .pt-root img.pt-still, .pt-root canvas.pt-overlay {
|
|
1287
1441
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
1288
1442
|
object-fit: cover; opacity: 0; transition: opacity 180ms ease-out;
|
|
1289
1443
|
}
|
|
1290
|
-
.pt-root video { z-index: 1; background: #000; }
|
|
1444
|
+
.pt-root video, .pt-root img.pt-still { z-index: 1; background: #000; }
|
|
1291
1445
|
.pt-root canvas.pt-overlay { z-index: 2; pointer-events: none; }
|
|
1292
1446
|
.pt-boot {
|
|
1293
1447
|
position: absolute; inset: 0; z-index: 4;
|
|
@@ -1361,6 +1515,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1361
1515
|
root.className = "pt-root";
|
|
1362
1516
|
root.innerHTML = `
|
|
1363
1517
|
<video playsinline muted autoplay></video>
|
|
1518
|
+
<img class="pt-still" alt="" draggable="false" />
|
|
1364
1519
|
<canvas class="pt-overlay"></canvas>
|
|
1365
1520
|
<div class="pt-boot">
|
|
1366
1521
|
<div class="pt-boot-powered">Powered by</div>
|
|
@@ -1377,6 +1532,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1377
1532
|
`;
|
|
1378
1533
|
host.appendChild(root);
|
|
1379
1534
|
const video = root.querySelector("video");
|
|
1535
|
+
const image = root.querySelector("img.pt-still");
|
|
1380
1536
|
const canvas = root.querySelector("canvas.pt-overlay");
|
|
1381
1537
|
const boot = root.querySelector(".pt-boot");
|
|
1382
1538
|
const bootMsg = root.querySelector(".pt-boot-msg");
|
|
@@ -1386,6 +1542,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1386
1542
|
return {
|
|
1387
1543
|
root,
|
|
1388
1544
|
video,
|
|
1545
|
+
image,
|
|
1389
1546
|
canvas,
|
|
1390
1547
|
boot,
|
|
1391
1548
|
bootMsg,
|
|
@@ -1397,10 +1554,21 @@ function mountDomShell(host, options = {}) {
|
|
|
1397
1554
|
bootMsg.classList.toggle("is-error", isError);
|
|
1398
1555
|
},
|
|
1399
1556
|
setCameraVisible(visible) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1557
|
+
if (visible) {
|
|
1558
|
+
this.setMediaMode(image.src ? "image" : "video");
|
|
1559
|
+
} else {
|
|
1560
|
+
this.setMediaMode("none");
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
setMediaMode(mode) {
|
|
1564
|
+
const showVideo = mode === "video";
|
|
1565
|
+
const showImage = mode === "image";
|
|
1566
|
+
const showMedia = showVideo || showImage;
|
|
1567
|
+
video.style.opacity = showVideo ? "1" : "0";
|
|
1568
|
+
image.style.opacity = showImage ? "1" : "0";
|
|
1569
|
+
canvas.style.opacity = showMedia ? "1" : "0";
|
|
1570
|
+
boot.classList.toggle("hide", showMedia);
|
|
1571
|
+
if (!showMedia) {
|
|
1404
1572
|
bootMsg.textContent = loadingText;
|
|
1405
1573
|
bootMsg.classList.remove("is-error");
|
|
1406
1574
|
}
|
|
@@ -1425,6 +1593,7 @@ function mountDomShell(host, options = {}) {
|
|
|
1425
1593
|
applyMirror(facingMode) {
|
|
1426
1594
|
const t = facingMode === "user" ? "scaleX(-1)" : "none";
|
|
1427
1595
|
video.style.transform = t;
|
|
1596
|
+
image.style.transform = "none";
|
|
1428
1597
|
canvas.style.transform = t;
|
|
1429
1598
|
},
|
|
1430
1599
|
destroy() {
|
|
@@ -1628,6 +1797,10 @@ var PoseTrackerClient = class {
|
|
|
1628
1797
|
this.keypointsSuppressionLogged = false;
|
|
1629
1798
|
this.shell = null;
|
|
1630
1799
|
this.stream = null;
|
|
1800
|
+
this.poseSource = { type: "camera" };
|
|
1801
|
+
this.objectUrlRevoke = null;
|
|
1802
|
+
this.estimateInput = null;
|
|
1803
|
+
this.imageShotPending = false;
|
|
1631
1804
|
this.adapter = null;
|
|
1632
1805
|
this.running = false;
|
|
1633
1806
|
this.busy = false;
|
|
@@ -1654,6 +1827,15 @@ var PoseTrackerClient = class {
|
|
|
1654
1827
|
this.resolved = resolvePoseModel({ model: options.model, modelUrl: options.modelUrl });
|
|
1655
1828
|
this.skeletonDef = options.skeletonDef ?? null;
|
|
1656
1829
|
this.coldStartMode = options.coldStart === "full" ? "full" : "basic";
|
|
1830
|
+
try {
|
|
1831
|
+
this.poseSource = normalizePoseSource(options.source, {
|
|
1832
|
+
sourceUrl: options.sourceUrl,
|
|
1833
|
+
sourceFile: options.sourceFile,
|
|
1834
|
+
facingMode: facingFromOptions(options)
|
|
1835
|
+
});
|
|
1836
|
+
} catch {
|
|
1837
|
+
this.poseSource = { type: "camera", facingMode: facingFromOptions(options) };
|
|
1838
|
+
}
|
|
1657
1839
|
this.files = options.fileStore !== void 0 ? options.fileStore : createWebFileStore();
|
|
1658
1840
|
this.engineLoader = options.engineLoader ?? new EngineLoader({
|
|
1659
1841
|
fileStore: this.files,
|
|
@@ -1700,6 +1882,10 @@ var PoseTrackerClient = class {
|
|
|
1700
1882
|
getColdStartMode() {
|
|
1701
1883
|
return this.coldStartMode;
|
|
1702
1884
|
}
|
|
1885
|
+
/** Active pose input source (`camera` | `video` | `image`). */
|
|
1886
|
+
getSource() {
|
|
1887
|
+
return this.poseSource;
|
|
1888
|
+
}
|
|
1703
1889
|
addEventListener(listener) {
|
|
1704
1890
|
this.listeners.add(listener);
|
|
1705
1891
|
return () => this.listeners.delete(listener);
|
|
@@ -1763,31 +1949,98 @@ var PoseTrackerClient = class {
|
|
|
1763
1949
|
warmup(options) {
|
|
1764
1950
|
return this.preload(options);
|
|
1765
1951
|
}
|
|
1766
|
-
/**
|
|
1952
|
+
/**
|
|
1953
|
+
* Mount + load model + open source + start inference.
|
|
1954
|
+
* Camera / video stream continuously; image runs a single shot (re-run with {@link analyze}).
|
|
1955
|
+
*/
|
|
1767
1956
|
async start() {
|
|
1768
1957
|
if (this.disposed) throw new Error("PoseTracker disposed");
|
|
1769
1958
|
if (!this.shell) throw new Error("Call mount() before start()");
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1959
|
+
const needsMedia = this.poseSource.type === "camera" || this.poseSource.type === "video" || this.poseSource.type === "image";
|
|
1960
|
+
await this.preload({
|
|
1961
|
+
coldStart: this.opts.coldStart === "basic" && this.poseSource.type === "camera" ? "basic" : needsMedia ? "full" : "basic"
|
|
1962
|
+
});
|
|
1963
|
+
if (this.coldStartMode === "basic" || !this.estimateInput) {
|
|
1964
|
+
await this.ensureSource();
|
|
1965
|
+
}
|
|
1966
|
+
if (this.poseSource.type === "image") {
|
|
1967
|
+
this.imageShotPending = true;
|
|
1773
1968
|
}
|
|
1774
1969
|
this.beginLoop();
|
|
1775
1970
|
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Switch input source. Stops the current media, attaches the new source, and
|
|
1973
|
+
* restarts the loop when previously running.
|
|
1974
|
+
*/
|
|
1975
|
+
async setSource(source, extras) {
|
|
1976
|
+
if (this.disposed) throw new Error("PoseTracker disposed");
|
|
1977
|
+
const wasRunning = this.running;
|
|
1978
|
+
this.stopMediaOnly();
|
|
1979
|
+
this.poseSource = normalizePoseSource(source, {
|
|
1980
|
+
sourceUrl: extras?.sourceUrl ?? this.opts.sourceUrl,
|
|
1981
|
+
sourceFile: extras?.sourceFile ?? this.opts.sourceFile,
|
|
1982
|
+
facingMode: extras?.facingMode ?? facingFromOptions(this.opts)
|
|
1983
|
+
});
|
|
1984
|
+
this.opts.source = this.poseSource;
|
|
1985
|
+
if (this.poseSource.type === "camera" && this.poseSource.facingMode) {
|
|
1986
|
+
this.opts.facingMode = this.poseSource.facingMode;
|
|
1987
|
+
}
|
|
1988
|
+
if (!this.shell) return;
|
|
1989
|
+
await this.ensureSource();
|
|
1990
|
+
if (wasRunning || this.poseSource.type === "image") {
|
|
1991
|
+
if (this.poseSource.type === "image") this.imageShotPending = true;
|
|
1992
|
+
this.beginLoop();
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Re-run pose estimation on the current still image (no-op for camera/video
|
|
1997
|
+
* while the loop is already running — for those, call {@link start}).
|
|
1998
|
+
*/
|
|
1999
|
+
async analyze() {
|
|
2000
|
+
if (this.disposed) throw new Error("PoseTracker disposed");
|
|
2001
|
+
if (!this.shell) throw new Error("Call mount() before analyze()");
|
|
2002
|
+
await this.ensureAdapter();
|
|
2003
|
+
if (!this.estimateInput) await this.ensureSource();
|
|
2004
|
+
if (this.poseSource.type === "image") {
|
|
2005
|
+
this.imageShotPending = true;
|
|
2006
|
+
if (!this.running) this.beginLoop();
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
return this.runOneFrame();
|
|
2010
|
+
}
|
|
1776
2011
|
stop() {
|
|
1777
2012
|
this.running = false;
|
|
1778
2013
|
this.loopToken += 1;
|
|
1779
2014
|
this.busy = false;
|
|
1780
|
-
|
|
1781
|
-
this.
|
|
2015
|
+
this.imageShotPending = false;
|
|
2016
|
+
this.stopMediaOnly();
|
|
1782
2017
|
if (this.shell) {
|
|
1783
|
-
this.shell.
|
|
1784
|
-
this.shell.setCameraVisible(false);
|
|
2018
|
+
this.shell.setMediaMode("none");
|
|
1785
2019
|
this.shell.setPlacementVisible(false);
|
|
1786
2020
|
this.cameraRevealed = false;
|
|
1787
2021
|
this.applyWatermark();
|
|
1788
2022
|
}
|
|
1789
2023
|
if (this.status !== "error") this.setStatus("idle");
|
|
1790
2024
|
}
|
|
2025
|
+
stopMediaOnly() {
|
|
2026
|
+
stopMediaStream(this.stream);
|
|
2027
|
+
this.stream = null;
|
|
2028
|
+
this.estimateInput = null;
|
|
2029
|
+
if (this.objectUrlRevoke) {
|
|
2030
|
+
this.objectUrlRevoke();
|
|
2031
|
+
this.objectUrlRevoke = null;
|
|
2032
|
+
}
|
|
2033
|
+
if (this.shell) {
|
|
2034
|
+
try {
|
|
2035
|
+
this.shell.video.pause();
|
|
2036
|
+
} catch {
|
|
2037
|
+
}
|
|
2038
|
+
this.shell.video.removeAttribute("src");
|
|
2039
|
+
this.shell.video.srcObject = null;
|
|
2040
|
+
this.shell.video.load();
|
|
2041
|
+
this.shell.image.removeAttribute("src");
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
1791
2044
|
async dispose() {
|
|
1792
2045
|
this.stopExercise();
|
|
1793
2046
|
this.stop();
|
|
@@ -2056,7 +2309,7 @@ var PoseTrackerClient = class {
|
|
|
2056
2309
|
async ensureColdStartMode(mode) {
|
|
2057
2310
|
if (mode !== "full") return;
|
|
2058
2311
|
if (!this.shell) return;
|
|
2059
|
-
await this.
|
|
2312
|
+
await this.ensureSource();
|
|
2060
2313
|
}
|
|
2061
2314
|
async ensureAdapter() {
|
|
2062
2315
|
if (this.adapter) return;
|
|
@@ -2073,48 +2326,102 @@ var PoseTrackerClient = class {
|
|
|
2073
2326
|
this.adapter = createPoseAdapter({ resolved: this.resolved });
|
|
2074
2327
|
await this.adapter.load();
|
|
2075
2328
|
}
|
|
2076
|
-
async
|
|
2077
|
-
if (!this.shell) throw new Error("Call mount() before opening
|
|
2078
|
-
|
|
2329
|
+
async ensureSource() {
|
|
2330
|
+
if (!this.shell) throw new Error("Call mount() before opening a pose source");
|
|
2331
|
+
const src = this.poseSource;
|
|
2332
|
+
if (src.type === "camera") {
|
|
2333
|
+
if (this.stream && this.estimateInput) return;
|
|
2334
|
+
this.emit({
|
|
2335
|
+
type: "initialization",
|
|
2336
|
+
step: "accessing_webcam",
|
|
2337
|
+
message: "accessing webcam (source=camera)",
|
|
2338
|
+
ready: false
|
|
2339
|
+
});
|
|
2340
|
+
try {
|
|
2341
|
+
this.stream = await openCameraStream({
|
|
2342
|
+
facingMode: src.facingMode ?? facingFromOptions(this.opts),
|
|
2343
|
+
idealWidth: this.opts.idealWidth,
|
|
2344
|
+
idealHeight: this.opts.idealHeight
|
|
2345
|
+
});
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2348
|
+
const denied = /NotAllowed|Permission|denied/i.test(msg);
|
|
2349
|
+
this.reportError({
|
|
2350
|
+
type: "error",
|
|
2351
|
+
code: denied ? "camera_denied" : "camera_unavailable",
|
|
2352
|
+
message: msg
|
|
2353
|
+
});
|
|
2354
|
+
this.shell.setBootMessage(
|
|
2355
|
+
denied ? "Camera permission denied \u2014 allow access in the browser" : "Camera unavailable",
|
|
2356
|
+
true
|
|
2357
|
+
);
|
|
2358
|
+
throw err;
|
|
2359
|
+
}
|
|
2360
|
+
this.shell.video.srcObject = this.stream;
|
|
2361
|
+
this.shell.applyMirror(src.facingMode ?? facingFromOptions(this.opts));
|
|
2362
|
+
this.shell.setMediaMode("video");
|
|
2363
|
+
try {
|
|
2364
|
+
await this.shell.video.play();
|
|
2365
|
+
} catch {
|
|
2366
|
+
}
|
|
2367
|
+
this.estimateInput = this.shell.video;
|
|
2368
|
+
this.scheduleReveal("camera");
|
|
2369
|
+
void this.handleCameraStart({
|
|
2370
|
+
backend: this.acceleration.backend ?? "webgl",
|
|
2371
|
+
profileId: null
|
|
2372
|
+
});
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
if (src.type === "video") {
|
|
2376
|
+
this.emit({
|
|
2377
|
+
type: "initialization",
|
|
2378
|
+
step: "loading_media",
|
|
2379
|
+
message: "loading video (source=video)",
|
|
2380
|
+
ready: false
|
|
2381
|
+
});
|
|
2382
|
+
try {
|
|
2383
|
+
await this.attachVideoSource(src.src, {
|
|
2384
|
+
autoplay: src.autoplay !== false,
|
|
2385
|
+
loop: src.loop !== false,
|
|
2386
|
+
muted: src.muted !== false
|
|
2387
|
+
});
|
|
2388
|
+
} catch (err) {
|
|
2389
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2390
|
+
this.reportError({ type: "error", code: "media_load_failed", message: msg });
|
|
2391
|
+
this.shell.setBootMessage("Could not load video source", true);
|
|
2392
|
+
throw err;
|
|
2393
|
+
}
|
|
2394
|
+
this.shell.applyMirror("none");
|
|
2395
|
+
this.shell.setMediaMode("video");
|
|
2396
|
+
this.scheduleReveal("video");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2079
2399
|
this.emit({
|
|
2080
2400
|
type: "initialization",
|
|
2081
|
-
step: "
|
|
2082
|
-
message: "
|
|
2401
|
+
step: "loading_media",
|
|
2402
|
+
message: "loading image (source=image)",
|
|
2083
2403
|
ready: false
|
|
2084
2404
|
});
|
|
2085
2405
|
try {
|
|
2086
|
-
this.
|
|
2087
|
-
facingMode: facingFromOptions(this.opts),
|
|
2088
|
-
idealWidth: this.opts.idealWidth,
|
|
2089
|
-
idealHeight: this.opts.idealHeight
|
|
2090
|
-
});
|
|
2406
|
+
await this.attachImageSource(src.src);
|
|
2091
2407
|
} catch (err) {
|
|
2092
2408
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2093
|
-
|
|
2094
|
-
this.
|
|
2095
|
-
type: "error",
|
|
2096
|
-
code: denied ? "camera_denied" : "camera_unavailable",
|
|
2097
|
-
message: msg
|
|
2098
|
-
});
|
|
2099
|
-
this.shell.setBootMessage(
|
|
2100
|
-
denied ? "Camera permission denied \u2014 allow access in the browser" : "Camera unavailable",
|
|
2101
|
-
true
|
|
2102
|
-
);
|
|
2409
|
+
this.reportError({ type: "error", code: "media_load_failed", message: msg });
|
|
2410
|
+
this.shell.setBootMessage("Could not load image source", true);
|
|
2103
2411
|
throw err;
|
|
2104
2412
|
}
|
|
2105
|
-
this.shell.
|
|
2106
|
-
this.shell.
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
}
|
|
2413
|
+
this.shell.applyMirror("none");
|
|
2414
|
+
this.shell.setMediaMode("image");
|
|
2415
|
+
this.scheduleReveal("image");
|
|
2416
|
+
}
|
|
2417
|
+
scheduleReveal(kind) {
|
|
2111
2418
|
const started = Date.now();
|
|
2112
2419
|
const tick = () => {
|
|
2113
2420
|
this.revealCamera();
|
|
2114
2421
|
if (this.cameraRevealed) return;
|
|
2115
2422
|
if (Date.now() - started > 8e3) {
|
|
2116
2423
|
this.shell?.setBootMessage(
|
|
2117
|
-
|
|
2424
|
+
`${kind} loaded but preview is empty \u2014 check the file or CORS`,
|
|
2118
2425
|
true
|
|
2119
2426
|
);
|
|
2120
2427
|
return;
|
|
@@ -2122,88 +2429,182 @@ var PoseTrackerClient = class {
|
|
|
2122
2429
|
setTimeout(tick, 120);
|
|
2123
2430
|
};
|
|
2124
2431
|
setTimeout(tick, 60);
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2432
|
+
}
|
|
2433
|
+
async attachVideoSource(src, opts) {
|
|
2434
|
+
if (!this.shell) throw new Error("no shell");
|
|
2435
|
+
const video = this.shell.video;
|
|
2436
|
+
if (isVideoElement(src)) {
|
|
2437
|
+
if (src.parentElement && src !== video) {
|
|
2438
|
+
this.estimateInput = src;
|
|
2439
|
+
if (opts.autoplay) {
|
|
2440
|
+
try {
|
|
2441
|
+
await src.play();
|
|
2442
|
+
} catch {
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
await waitForVideoReady(src);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (src.srcObject) {
|
|
2449
|
+
video.srcObject = src.srcObject;
|
|
2450
|
+
} else if (src.currentSrc || src.src) {
|
|
2451
|
+
video.src = src.currentSrc || src.src;
|
|
2452
|
+
}
|
|
2453
|
+
} else {
|
|
2454
|
+
const resolved = await resolveObjectUrl(src);
|
|
2455
|
+
this.objectUrlRevoke = resolved.revoke;
|
|
2456
|
+
video.src = resolved.url;
|
|
2457
|
+
}
|
|
2458
|
+
video.loop = opts.loop;
|
|
2459
|
+
video.muted = opts.muted;
|
|
2460
|
+
video.playsInline = true;
|
|
2461
|
+
await waitForVideoReady(video);
|
|
2462
|
+
if (opts.autoplay) {
|
|
2463
|
+
try {
|
|
2464
|
+
await video.play();
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
this.estimateInput = video;
|
|
2469
|
+
}
|
|
2470
|
+
async attachImageSource(src) {
|
|
2471
|
+
if (!this.shell) throw new Error("no shell");
|
|
2472
|
+
if (isImageBitmap(src)) {
|
|
2473
|
+
const resolved2 = await imageBitmapToObjectUrl(src);
|
|
2474
|
+
this.objectUrlRevoke = resolved2.revoke;
|
|
2475
|
+
this.shell.image.src = resolved2.url;
|
|
2476
|
+
await waitForImageReady(this.shell.image);
|
|
2477
|
+
this.estimateInput = src;
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
if (isImageElement(src)) {
|
|
2481
|
+
if (src.complete && src.naturalWidth > 0) {
|
|
2482
|
+
this.shell.image.src = src.currentSrc || src.src;
|
|
2483
|
+
this.estimateInput = src;
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
await waitForImageReady(src);
|
|
2487
|
+
this.shell.image.src = src.currentSrc || src.src;
|
|
2488
|
+
this.estimateInput = src;
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
const resolved = await resolveObjectUrl(src);
|
|
2492
|
+
this.objectUrlRevoke = resolved.revoke;
|
|
2493
|
+
this.shell.image.src = resolved.url;
|
|
2494
|
+
await waitForImageReady(this.shell.image);
|
|
2495
|
+
this.estimateInput = this.shell.image;
|
|
2129
2496
|
}
|
|
2130
2497
|
beginLoop() {
|
|
2131
2498
|
this.running = true;
|
|
2132
2499
|
this.fpsTimer = performance.now();
|
|
2133
2500
|
void this.loop(++this.loopToken);
|
|
2134
2501
|
}
|
|
2502
|
+
frameReady(input) {
|
|
2503
|
+
const size = mediaSize(
|
|
2504
|
+
input
|
|
2505
|
+
);
|
|
2506
|
+
if (!(size.width > 0 && size.height > 0)) return false;
|
|
2507
|
+
if (isVideoElement(input)) {
|
|
2508
|
+
if (input.readyState < 2) return false;
|
|
2509
|
+
if (this.poseSource.type === "video" && (input.paused || input.ended)) return false;
|
|
2510
|
+
}
|
|
2511
|
+
return true;
|
|
2512
|
+
}
|
|
2513
|
+
async runOneFrame() {
|
|
2514
|
+
if (!this.adapter || !this.shell || !this.estimateInput) return null;
|
|
2515
|
+
if (!this.frameReady(this.estimateInput)) return null;
|
|
2516
|
+
const input = this.estimateInput;
|
|
2517
|
+
const dispW = this.shell.canvas.clientWidth || this.shell.root.clientWidth || 1;
|
|
2518
|
+
const dispH = this.shell.canvas.clientHeight || this.shell.root.clientHeight || 1;
|
|
2519
|
+
const facing = this.poseSource.type === "camera" ? facingFromOptions(this.opts) : "environment";
|
|
2520
|
+
const result = await this.adapter.estimate(input, {
|
|
2521
|
+
facingMode: facing,
|
|
2522
|
+
displayWidth: dispW,
|
|
2523
|
+
displayHeight: dispH
|
|
2524
|
+
});
|
|
2525
|
+
if (!result) return null;
|
|
2526
|
+
const ctx = this.shell.canvas.getContext("2d");
|
|
2527
|
+
if (ctx && this.opts.drawSkeleton !== false) {
|
|
2528
|
+
const mapped = mapVideoKeypointsToDisplay(
|
|
2529
|
+
result.videoKeypoints,
|
|
2530
|
+
result.letterbox,
|
|
2531
|
+
dispW,
|
|
2532
|
+
dispH
|
|
2533
|
+
);
|
|
2534
|
+
drawSkeleton(ctx, mapped, dispW, dispH, true, this.skeletonDef);
|
|
2535
|
+
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2536
|
+
ctx.clearRect(0, 0, dispW, dispH);
|
|
2537
|
+
}
|
|
2538
|
+
const pose = {
|
|
2539
|
+
keypoints: result.keypoints,
|
|
2540
|
+
score: result.score,
|
|
2541
|
+
timestampMs: Date.now()
|
|
2542
|
+
};
|
|
2543
|
+
this.ingestPose(pose);
|
|
2544
|
+
this.inferMs.push(result.inferenceMs);
|
|
2545
|
+
if (this.inferMs.length > 30) this.inferMs.shift();
|
|
2546
|
+
this.fpsWindow += 1;
|
|
2547
|
+
const now = performance.now();
|
|
2548
|
+
if (now - this.fpsTimer >= 1e3) {
|
|
2549
|
+
const fps = this.fpsWindow;
|
|
2550
|
+
this.fpsWindow = 0;
|
|
2551
|
+
this.fpsTimer = now;
|
|
2552
|
+
const med = median(this.inferMs);
|
|
2553
|
+
this.acceleration.medianInferenceMs = med;
|
|
2554
|
+
const above = result.keypoints.filter((k) => k.score >= 0.3).length;
|
|
2555
|
+
const backend = "getBackend" in this.adapter && typeof this.adapter.getBackend === "function" ? this.adapter.getBackend() : null;
|
|
2556
|
+
this.acceleration.backend = backend;
|
|
2557
|
+
const size = mediaSize(input);
|
|
2558
|
+
if (this.opts.debugHud) {
|
|
2559
|
+
this.shell.setHud(
|
|
2560
|
+
`${this.resolved.modelId} \xB7 ${this.poseSource.type} \xB7 ${backend ?? "?"} \xB7 ${fps} fps \xB7 ${med != null ? Math.round(med) + " ms" : "?"} \xB7 kp\u22650.3=${above}/17`,
|
|
2561
|
+
true
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
this.emit({
|
|
2565
|
+
type: "stats",
|
|
2566
|
+
fps,
|
|
2567
|
+
medianInferenceMs: med,
|
|
2568
|
+
backend,
|
|
2569
|
+
keypointsAbove03: above,
|
|
2570
|
+
meanScore: result.score,
|
|
2571
|
+
videoSize: `${size.width}x${size.height}`,
|
|
2572
|
+
timestampMs: Date.now()
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
this.revealCamera();
|
|
2576
|
+
return pose;
|
|
2577
|
+
}
|
|
2135
2578
|
async loop(token) {
|
|
2136
2579
|
if (token !== this.loopToken || !this.running || !this.adapter || !this.shell || this.disposed) {
|
|
2137
2580
|
return;
|
|
2138
2581
|
}
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2582
|
+
const input = this.estimateInput;
|
|
2583
|
+
const shouldInfer = !!input && !this.busy && this.frameReady(input) && (this.poseSource.type !== "image" || this.imageShotPending);
|
|
2584
|
+
if (shouldInfer) {
|
|
2141
2585
|
this.busy = true;
|
|
2142
2586
|
try {
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
});
|
|
2150
|
-
if (result) {
|
|
2151
|
-
const ctx = this.shell.canvas.getContext("2d");
|
|
2152
|
-
if (ctx && this.opts.drawSkeleton !== false) {
|
|
2153
|
-
const mapped = mapVideoKeypointsToDisplay(
|
|
2154
|
-
result.videoKeypoints,
|
|
2155
|
-
result.letterbox,
|
|
2156
|
-
dispW,
|
|
2157
|
-
dispH
|
|
2158
|
-
);
|
|
2159
|
-
drawSkeleton(ctx, mapped, dispW, dispH, true, this.skeletonDef);
|
|
2160
|
-
} else if (ctx && this.opts.drawSkeleton === false) {
|
|
2161
|
-
ctx.clearRect(0, 0, dispW, dispH);
|
|
2162
|
-
}
|
|
2163
|
-
const pose = {
|
|
2164
|
-
keypoints: result.keypoints,
|
|
2165
|
-
score: result.score,
|
|
2166
|
-
timestampMs: Date.now()
|
|
2167
|
-
};
|
|
2168
|
-
this.ingestPose(pose);
|
|
2169
|
-
this.inferMs.push(result.inferenceMs);
|
|
2170
|
-
if (this.inferMs.length > 30) this.inferMs.shift();
|
|
2171
|
-
this.fpsWindow += 1;
|
|
2172
|
-
const now = performance.now();
|
|
2173
|
-
if (now - this.fpsTimer >= 1e3) {
|
|
2174
|
-
const fps = this.fpsWindow;
|
|
2175
|
-
this.fpsWindow = 0;
|
|
2176
|
-
this.fpsTimer = now;
|
|
2177
|
-
const med = median(this.inferMs);
|
|
2178
|
-
this.acceleration.medianInferenceMs = med;
|
|
2179
|
-
const above = result.keypoints.filter((k) => k.score >= 0.3).length;
|
|
2180
|
-
const backend = "getBackend" in this.adapter && typeof this.adapter.getBackend === "function" ? this.adapter.getBackend() : null;
|
|
2181
|
-
this.acceleration.backend = backend;
|
|
2182
|
-
if (this.opts.debugHud) {
|
|
2183
|
-
this.shell.setHud(
|
|
2184
|
-
`${this.resolved.modelId} \xB7 ${backend ?? "?"} \xB7 ${fps} fps \xB7 ${med != null ? Math.round(med) + " ms" : "?"} \xB7 kp\u22650.3=${above}/17`,
|
|
2185
|
-
true
|
|
2186
|
-
);
|
|
2187
|
-
}
|
|
2188
|
-
this.emit({
|
|
2189
|
-
type: "stats",
|
|
2190
|
-
fps,
|
|
2191
|
-
medianInferenceMs: med,
|
|
2192
|
-
backend,
|
|
2193
|
-
keypointsAbove03: above,
|
|
2194
|
-
meanScore: result.score,
|
|
2195
|
-
videoSize: `${video.videoWidth}x${video.videoHeight}`,
|
|
2196
|
-
timestampMs: Date.now()
|
|
2197
|
-
});
|
|
2198
|
-
}
|
|
2587
|
+
await this.runOneFrame();
|
|
2588
|
+
if (this.poseSource.type === "image") {
|
|
2589
|
+
this.imageShotPending = false;
|
|
2590
|
+
this.running = false;
|
|
2591
|
+
this.busy = false;
|
|
2592
|
+
return;
|
|
2199
2593
|
}
|
|
2200
|
-
this.revealCamera();
|
|
2201
2594
|
} catch (err) {
|
|
2202
2595
|
const message = err instanceof Error ? err.message : String(err);
|
|
2203
2596
|
this.emit({ type: "error", code: "internal", message: `inference: ${message}` });
|
|
2204
2597
|
}
|
|
2205
2598
|
this.busy = false;
|
|
2206
2599
|
}
|
|
2600
|
+
if (token !== this.loopToken || !this.running) return;
|
|
2601
|
+
const videoEl = input && isVideoElement(input) ? input : this.shell.video;
|
|
2602
|
+
if (this.poseSource.type !== "image" && typeof videoEl.requestVideoFrameCallback === "function" && (this.poseSource.type === "video" || this.poseSource.type === "camera")) {
|
|
2603
|
+
videoEl.requestVideoFrameCallback(() => {
|
|
2604
|
+
void this.loop(token);
|
|
2605
|
+
});
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2207
2608
|
requestAnimationFrame(() => {
|
|
2208
2609
|
void this.loop(token);
|
|
2209
2610
|
});
|
|
@@ -2460,14 +2861,19 @@ var PoseTrackerClient = class {
|
|
|
2460
2861
|
}
|
|
2461
2862
|
revealCamera() {
|
|
2462
2863
|
if (!this.shell || this.cameraRevealed) return;
|
|
2463
|
-
const
|
|
2464
|
-
if (!
|
|
2864
|
+
const input = this.estimateInput;
|
|
2865
|
+
if (!input) return;
|
|
2866
|
+
const size = mediaSize(input);
|
|
2867
|
+
if (!(size.width > 0 && size.height > 0)) return;
|
|
2465
2868
|
requestAnimationFrame(() => {
|
|
2466
2869
|
requestAnimationFrame(() => {
|
|
2467
2870
|
if (this.cameraRevealed || !this.shell) return;
|
|
2468
|
-
if (!(this.shell.video.videoWidth > 0)) return;
|
|
2469
2871
|
this.cameraRevealed = true;
|
|
2470
|
-
this.
|
|
2872
|
+
if (this.poseSource.type === "image") {
|
|
2873
|
+
this.shell.setMediaMode("image");
|
|
2874
|
+
} else {
|
|
2875
|
+
this.shell.setMediaMode("video");
|
|
2876
|
+
}
|
|
2471
2877
|
this.applyWatermark();
|
|
2472
2878
|
});
|
|
2473
2879
|
});
|
|
@@ -2594,6 +3000,7 @@ exports.initWebGlBackend = initWebGlBackend;
|
|
|
2594
3000
|
exports.isPaidPlan = isPaidPlan;
|
|
2595
3001
|
exports.mapVideoKeypointsToDisplay = mapVideoKeypointsToDisplay;
|
|
2596
3002
|
exports.mountDomShell = mountDomShell;
|
|
3003
|
+
exports.normalizePoseSource = normalizePoseSource;
|
|
2597
3004
|
exports.openCameraStream = openCameraStream;
|
|
2598
3005
|
exports.resolveExerciseName = resolveExerciseName;
|
|
2599
3006
|
exports.resolveFeatures = resolveFeatures;
|