@kubohiroya/turbowarp-teachable-machine 3.3.0 → 3.4.1

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/tm.js CHANGED
@@ -553,7 +553,7 @@
553
553
  }
554
554
  var package_default = {
555
555
  name: "@kubohiroya/turbowarp-teachable-machine",
556
- version: "3.3.0",
556
+ version: "3.4.1",
557
557
  description: "A TurboWarp extension for recognition using Teachable Machine pose, image, and audio models.",
558
558
  author: "Hiroya Kubo <hiroya@cuc.ac.jp>",
559
559
  license: "MPL-2.0",
@@ -621,6 +621,7 @@
621
621
  "prepack": "npm run build && npm run release:consistency:check"
622
622
  },
623
623
  devDependencies: {
624
+ "@kubohiroya/turbowarp-camera-source": "0.13.0",
624
625
  "@kubohiroya/vite-plugin-turbowarp-extension": "0.4.0",
625
626
  "@teachablemachine/image": "0.8.5",
626
627
  "@teachablemachine/pose": "0.8.6",
@@ -647,6 +648,23 @@
647
648
  };
648
649
  //#endregion
649
650
  //#region src/extension.ts
651
+ /**
652
+ * Where Camera Source puts itself on the VM runtime.
653
+ *
654
+ * Declared here rather than imported as a value, because this file is bundled into a single-file
655
+ * extension and no dependency's code has ever been inlined into it. The key and the member read
656
+ * below are pinned against Camera Source's own published constant and narrowing function in the
657
+ * tests, which import both and are not shipped, so a rename upstream fails here rather than
658
+ * silently reading an absent extension as "not loaded".
659
+ */
660
+ var CAMERA_SOURCE_RUNTIME_KEY = "ext_kubohiroyacamerasource";
661
+ function readCameraSource(runtime) {
662
+ if (typeof runtime !== "object" || runtime === null) return void 0;
663
+ const candidate = runtime[CAMERA_SOURCE_RUNTIME_KEY];
664
+ if (typeof candidate !== "object" || candidate === null) return void 0;
665
+ const { acquireCamera } = candidate;
666
+ return typeof acquireCamera === "function" ? candidate : void 0;
667
+ }
650
668
  var EXTENSION_ID = "kubohiroyatm";
651
669
  var VERSION = `${package_default.version}-typescript`;
652
670
  var DOCS_URI = "https://kubohiroya.github.io/turbowarp-teachable-machine/";
@@ -929,6 +947,7 @@
929
947
  this.modelURL = "";
930
948
  this.model = null;
931
949
  this.webcam = null;
950
+ this.cameraLease = null;
932
951
  this.cameraRunning = false;
933
952
  this.cameraSelection = "default";
934
953
  this.cameraSelectionIsDeviceId = false;
@@ -1150,8 +1169,11 @@
1150
1169
  await this.ensureComputeBackend();
1151
1170
  }
1152
1171
  cleanupCameraResources() {
1172
+ const lease = this.cameraLease;
1173
+ this.cameraLease = null;
1153
1174
  const video = this.webcam?.webcam;
1154
- if (video?.srcObject) {
1175
+ if (lease) lease.release();
1176
+ else if (video?.srcObject) {
1155
1177
  video.srcObject.getTracks().forEach((track) => track.stop());
1156
1178
  video.srcObject = null;
1157
1179
  }
@@ -1184,11 +1206,17 @@
1184
1206
  if (!runtime) throw new Error("Teachable Machine: Runtime is unavailable.");
1185
1207
  if (!("Webcam" in runtime)) throw new Error("Teachable Machine: Camera runtime is unavailable in the current mode.");
1186
1208
  this.webcam = new runtime.Webcam(320, 240, true);
1187
- const constraints = cameraConstraints(this.resolvedCameraSelection());
1209
+ const lease = await this.acquireSharedCamera();
1210
+ if (lease) {
1211
+ this.cameraLease = lease;
1212
+ this.webcam.webcam = lease.getFrameSource().element;
1213
+ }
1214
+ const constraints = lease ? void 0 : cameraConstraints(this.resolvedCameraSelection());
1188
1215
  if (constraints) await this.webcam.setup(constraints);
1189
1216
  else await this.webcam.setup();
1217
+ this.ensureCameraCanvas();
1190
1218
  initializeCameraReadbackContext(this.webcam.canvas);
1191
- await this.webcam.play();
1219
+ if (!lease) await this.webcam.play();
1192
1220
  this.attachPreviewToStage();
1193
1221
  this.cameraRunning = true;
1194
1222
  try {
@@ -1203,6 +1231,46 @@
1203
1231
  throw error;
1204
1232
  }
1205
1233
  }
1234
+ /**
1235
+ * Give the camera the readback canvas `setup()` did not build.
1236
+ *
1237
+ * Upstream creates that canvas inside the same branch that opens the camera, so an injected
1238
+ * element skips the canvas with the `getUserMedia` call. 3.4.0 shipped assuming the two were
1239
+ * separate, and every leased start then failed at the first context request with "Webcam canvas
1240
+ * does not provide a 2D context" -- which is to say pose recognition did not start at all
1241
+ * wherever Camera Source was present. The canvas built here is the one upstream would have
1242
+ * built, so the mirrored centre crop every downstream reader expects is unchanged.
1243
+ */
1244
+ ensureCameraCanvas() {
1245
+ if (!this.webcam || this.webcam.canvas) return;
1246
+ const canvas = document.createElement("canvas");
1247
+ canvas.width = this.webcam.width ?? 320;
1248
+ canvas.height = this.webcam.height ?? 240;
1249
+ this.webcam.canvas = canvas;
1250
+ }
1251
+ /**
1252
+ * A lease on the shared camera, or null when Camera Source is not loaded.
1253
+ *
1254
+ * Camera Source owns the device and decides when a stream stops, which is what lets one camera
1255
+ * serve pose recognition and anything else reading frames at the same time. It is a separate
1256
+ * TurboWarp extension, and this one is also distributed for standalone URL loading, so its
1257
+ * absence is ordinary rather than a failure: the self-acquired path stays for it.
1258
+ *
1259
+ * One camera id for every visual recognition mode. Opening a second device for a work that
1260
+ * recognises both poses and images is not what sharing is for, and `pose` is the role name
1261
+ * Camera Source's own documentation uses for this consumer.
1262
+ */
1263
+ async acquireSharedCamera() {
1264
+ const cameraSource = readCameraSource(Scratch.vm?.runtime);
1265
+ if (!cameraSource) return null;
1266
+ const selection = this.resolvedCameraSelection();
1267
+ const options = {
1268
+ owner: package_default.name,
1269
+ cameraId: "pose"
1270
+ };
1271
+ if (selection.kind === "device") options.deviceId = selection.value;
1272
+ return cameraSource.acquireCamera(options);
1273
+ }
1206
1274
  stopCamera() {
1207
1275
  try {
1208
1276
  this.stopRecognition();
@@ -5,6 +5,9 @@ export declare const EXTENSION_ID = "kubohiroyatm";
5
5
  export declare const VERSION: string;
6
6
  export declare const DOCS_URI = "https://kubohiroya.github.io/turbowarp-teachable-machine/";
7
7
  export declare const ACCUMULATED_POSE_CHANGED_EVENT = "TM_ACCUMULATED_POSE_CHANGED";
8
+ /** The readback canvas size every downstream frame reader is written against. */
9
+ export declare const CAMERA_FRAME_WIDTH = 320;
10
+ export declare const CAMERA_FRAME_HEIGHT = 240;
8
11
  export declare const BLOCK_ICON_URI: string;
9
12
  export type RecognitionMode = 'pose' | 'image' | 'audio';
10
13
  export interface TeachableMachineRuntime {
@@ -152,6 +155,30 @@ export declare class TMExtension {
152
155
  ensureLibrariesLoaded(): Promise<void>;
153
156
  cleanupCameraResources(): void;
154
157
  startCamera(): Promise<void>;
158
+ /**
159
+ * Give the camera the readback canvas `setup()` did not build.
160
+ *
161
+ * Upstream creates that canvas inside the same branch that opens the camera, so an injected
162
+ * element skips the canvas with the `getUserMedia` call. 3.4.0 shipped assuming the two were
163
+ * separate, and every leased start then failed at the first context request with "Webcam canvas
164
+ * does not provide a 2D context" -- which is to say pose recognition did not start at all
165
+ * wherever Camera Source was present. The canvas built here is the one upstream would have
166
+ * built, so the mirrored centre crop every downstream reader expects is unchanged.
167
+ */
168
+ ensureCameraCanvas(): void;
169
+ /**
170
+ * A lease on the shared camera, or null when Camera Source is not loaded.
171
+ *
172
+ * Camera Source owns the device and decides when a stream stops, which is what lets one camera
173
+ * serve pose recognition and anything else reading frames at the same time. It is a separate
174
+ * TurboWarp extension, and this one is also distributed for standalone URL loading, so its
175
+ * absence is ordinary rather than a failure: the self-acquired path stays for it.
176
+ *
177
+ * One camera id for every visual recognition mode. Opening a second device for a work that
178
+ * recognises both poses and images is not what sharing is for, and `pose` is the role name
179
+ * Camera Source's own documentation uses for this consumer.
180
+ */
181
+ private acquireSharedCamera;
155
182
  stopCamera(): void;
156
183
  isCameraRunning(): any;
157
184
  getCameraMenuItems(): {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-teachable-machine",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "description": "A TurboWarp extension for recognition using Teachable Machine pose, image, and audio models.",
5
5
  "author": "Hiroya Kubo <hiroya@cuc.ac.jp>",
6
6
  "license": "MPL-2.0",
@@ -72,6 +72,7 @@
72
72
  "prepack": "npm run build && npm run release:consistency:check"
73
73
  },
74
74
  "devDependencies": {
75
+ "@kubohiroya/turbowarp-camera-source": "0.13.0",
75
76
  "@kubohiroya/vite-plugin-turbowarp-extension": "0.4.0",
76
77
  "@teachablemachine/image": "0.8.5",
77
78
  "@teachablemachine/pose": "0.8.6",