@wcstack/camera 4.7.0 → 4.9.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.d.ts CHANGED
@@ -347,6 +347,9 @@ declare class WcsCamera extends HTMLElement {
347
347
  private _core;
348
348
  private _video;
349
349
  private _connectedCallbackPromise;
350
+ private _connectResolve;
351
+ private _acquirePending;
352
+ private _autostartCancelled;
350
353
  private _connected;
351
354
  private _batchingAttrs;
352
355
  private _internals;
@@ -396,6 +399,7 @@ declare class WcsCamera extends HTMLElement {
396
399
  private _constraints;
397
400
  private _onStreamReady;
398
401
  private _onActiveChanged;
402
+ private _scheduleAcquire;
399
403
  connectedCallback(): void;
400
404
  disconnectedCallback(): void;
401
405
  attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null): void;
@@ -445,9 +449,11 @@ declare class RecorderCore extends EventTarget {
445
449
  private _closing;
446
450
  private _ready;
447
451
  /**
448
- * Whether `[wcs/command-not-ready]` was said for this element: a `start()` refused because the
452
+ * Whether `[wcs/command-not-ready]` was said in this outage: a `start()` refused because the
449
453
  * element is not ready loses what it carried, and `error` alone told only a page that binds it.
450
- * Said once per element (docs/v4-direction-2026-10.md, the I/O command policy).
454
+ * Said once per outage (docs/v4-direction-2026-10.md, the I/O command policy): a stream arriving
455
+ * resets it. Once per element was once per page life — a route puts the same element back on
456
+ * every entry, so the next outage's lost recording went unsaid (docs/seam-apps.md, shift-desk).
451
457
  */
452
458
  private _toldNotReady;
453
459
  constructor(target?: EventTarget);
package/dist/index.esm.js CHANGED
@@ -647,6 +647,12 @@ class CameraCore extends EventTarget {
647
647
  if (this._constraints.audio) {
648
648
  tasks.push(this._watchMicrophone());
649
649
  }
650
+ else {
651
+ // A re-observe after dispose() without audio: the microphone state the last
652
+ // subscription left is not what this one asks about (null = audio not requested),
653
+ // as _reconcileAudioWatcher clears it on a live Core.
654
+ this._setAudioPermission(null);
655
+ }
650
656
  // Track hot-plug: refresh the device list when a camera is added/removed.
651
657
  if (typeof navigator.mediaDevices.addEventListener === "function") {
652
658
  navigator.mediaDevices.addEventListener("devicechange", this._onDeviceChange);
@@ -951,14 +957,23 @@ class WcsCamera extends HTMLElement {
951
957
  commands: CameraCore.wcBindable.commands,
952
958
  };
953
959
  // `autostart` and `keep-alive` are intentionally NOT observed: `autostart` is a
954
- // connect-time-only acquire trigger (read once in connectedCallback; flipping it
955
- // later is meaningless), and `keep-alive` is read fresh on every visibilitychange
956
- // (_onVisibilityChange), so it never needs to drive a re-acquire. The observed set
957
- // is exactly the constraints that reshape the requested track.
960
+ // connect-time-only acquire trigger (read once by the connect-time decision in
961
+ // _scheduleAcquire's microtask; flipping it later is meaningless), and `keep-alive`
962
+ // is read fresh on every visibilitychange (_onVisibilityChange), so it never needs
963
+ // to drive a re-acquire. The observed set is exactly the constraints that reshape
964
+ // the requested track.
958
965
  static observedAttributes = ["facing-mode", "device-id", "audio", "width", "height"];
959
966
  _core;
960
967
  _video;
961
968
  _connectedCallbackPromise = Promise.resolve();
969
+ // connectedCallback が張った connectedCallbackPromise を解く関数。接続時の判断を下す
970
+ // _scheduleAcquire のマイクロタスクが(その前に外れたときは disconnectedCallback が)呼ぶ。
971
+ // null でないあいだは「接続時の判断待ち」でもある。
972
+ _connectResolve = null;
973
+ // _scheduleAcquire のマイクロタスクが待っている間 true
974
+ _acquirePending = false;
975
+ // 接続時の判断待ちに stop() が呼ばれたら true — その判断は autostart でも取得しない
976
+ _autostartCancelled = false;
962
977
  _connected = false;
963
978
  // True while switchCamera() rewrites several attributes at once. Each setAttribute /
964
979
  // removeAttribute fires its own attributeChangedCallback synchronously; without this
@@ -1081,8 +1096,21 @@ class WcsCamera extends HTMLElement {
1081
1096
  get errorInfo() { return this._core.errorInfo; }
1082
1097
  get connectedCallbackPromise() { return this._connectedCallbackPromise; }
1083
1098
  // --- Commands ---
1084
- start() { this._core.start(); }
1085
- stop() { this._core.stop(); }
1099
+ start() {
1100
+ // 制約は属性が正本。接続時の判断・属性の変化はマイクロタスクで Core に渡るので
1101
+ // (_scheduleAcquire)、それより先の start() でも、いまの属性の制約で取得するよう渡しておく
1102
+ if (this._connected)
1103
+ this._core.observe(this._constraints());
1104
+ this._core.start();
1105
+ }
1106
+ stop() {
1107
+ // 接続時の判断待ちなら、その autostart を取り消す — 同じタスクの append → stop() で、後の
1108
+ // マイクロタスクが取得を始めないように(判断が同期だった頃は、始めた取得を stop() が止めて
1109
+ // いた)。権限の監視(observe)は取り消さない。入れ直せば、次の判断は autostart に従う
1110
+ if (this._connectResolve !== null)
1111
+ this._autostartCancelled = true;
1112
+ this._core.stop();
1113
+ }
1086
1114
  /**
1087
1115
  * Toggle the front/back camera by updating the DOM attributes (the single source
1088
1116
  * of truth), not just the Core's internal constraints. Deliberately does NOT call
@@ -1142,18 +1170,63 @@ class WcsCamera extends HTMLElement {
1142
1170
  }
1143
1171
  };
1144
1172
  // --- Lifecycle ---
1173
+ // 取得の判断はマイクロタスクで、そのときの属性(制約と autostart)で 1 回だけ行う
1174
+ // (timing-and-firing-contract §23)。router は遷移で同じノードを入れ直し、新しいルートの
1175
+ // params から state が device-id などを書くのはマイクロタスク — その場で取得すると、前の
1176
+ // 制約で getUserMedia を起こし、取得中は active が偽なので新しい制約での取り直しも起きず、
1177
+ // 前の制約のストリームが残っていた(docs/seam-apps.md、shift-desk)。同じタスクの制約の
1178
+ // 変化もここへまとまる。
1179
+ _scheduleAcquire() {
1180
+ if (this._acquirePending)
1181
+ return;
1182
+ this._acquirePending = true;
1183
+ queueMicrotask(() => {
1184
+ this._acquirePending = false;
1185
+ const resolveConnect = this._connectResolve;
1186
+ this._connectResolve = null;
1187
+ // 判断の前に外れた要素は何もしない(disconnectedCallback が dispose 済み)
1188
+ if (!this._connected)
1189
+ return;
1190
+ // Track the constraints (and reconcile the microphone watcher).
1191
+ const ready = this._core.observe(this._constraints());
1192
+ if (resolveConnect !== null) {
1193
+ // 接続時の判断: autostart なら取得する(判断待ちに stop() が呼ばれていなければ)
1194
+ if (this.autostart && !this._autostartCancelled) {
1195
+ this._core.start();
1196
+ }
1197
+ this._autostartCancelled = false;
1198
+ resolveConnect(ready);
1199
+ }
1200
+ else if (this._core.active) {
1201
+ // A constraints change re-acquires (switchMap-style restart) only when a stream
1202
+ // is actually live. `active` is the deliberate guard (not `desired`): `active`
1203
+ // implies `desired` (a stream only goes live under desired), so re-acquiring
1204
+ // cannot spuriously re-`desired` a stopped camera. Guarding on `active` rather
1205
+ // than `desired` also avoids force-acquiring while suspended/hidden (desired but
1206
+ // not active) — the visibility handler owns resume there.
1207
+ this._core.start();
1208
+ }
1209
+ });
1210
+ }
1145
1211
  connectedCallback() {
1146
1212
  // upgrade 前に代入された input を取り込み直す(doc 13 §1.2 / Phase A1)
1147
1213
  upgradeProperties(this);
1148
1214
  this._connected = true;
1149
- this._connectedCallbackPromise = this._core.observe(this._constraints());
1150
- if (this.autostart) {
1151
- this._core.start();
1152
- }
1215
+ // connectedCallbackPromise は接続時の observe(権限の初期状態)が済むまでを表す。
1216
+ // observe は判断と同じマイクロタスクで走るので、ここで張ってそこで解く
1217
+ this._connectedCallbackPromise = new Promise((resolve) => {
1218
+ this._connectResolve = resolve;
1219
+ });
1220
+ this._scheduleAcquire();
1153
1221
  document.addEventListener("visibilitychange", this._onVisibilityChange);
1154
1222
  }
1155
1223
  disconnectedCallback() {
1156
1224
  this._connected = false;
1225
+ // 判断の前に外れたら、張った connectedCallbackPromise をここで解く — 同じタスクで
1226
+ // 入れ直されると次の connectedCallback が張り替えるので、待つ側が取り残されないように
1227
+ this._connectResolve?.();
1228
+ this._connectResolve = null;
1229
+ this._autostartCancelled = false;
1157
1230
  document.removeEventListener("visibilitychange", this._onVisibilityChange);
1158
1231
  this._core.dispose();
1159
1232
  }
@@ -1165,17 +1238,9 @@ class WcsCamera extends HTMLElement {
1165
1238
  // constraints (see `_batchingAttrs`).
1166
1239
  if (this._batchingAttrs)
1167
1240
  return;
1168
- // Track the new constraints (and reconcile the microphone watcher).
1169
- this._core.observe(this._constraints());
1170
- // A constraints change re-acquires (switchMap-style restart) only when a stream
1171
- // is actually live. `active` is the deliberate guard (not `desired`): `active`
1172
- // implies `desired` (a stream only goes live under desired), so re-acquiring
1173
- // cannot spuriously re-`desired` a stopped camera. Guarding on `active` rather
1174
- // than `desired` also avoids force-acquiring while suspended/hidden (desired but
1175
- // not active) — the visibility handler owns resume there.
1176
- if (this._core.active) {
1177
- this._core.start();
1178
- }
1241
+ // 制約の変化は取得の判断(_scheduleAcquire)へまとめる: 接続時の判断待ちならそれが
1242
+ // 新しい制約を読み、その後ならストリームが生きているときだけ取り直す
1243
+ this._scheduleAcquire();
1179
1244
  }
1180
1245
  _onVisibilityChange = () => {
1181
1246
  if (this.keepAlive)
@@ -1270,9 +1335,11 @@ class RecorderCore extends EventTarget {
1270
1335
  // asynchronous probe to await — readiness is immediate.
1271
1336
  _ready = Promise.resolve();
1272
1337
  /**
1273
- * Whether `[wcs/command-not-ready]` was said for this element: a `start()` refused because the
1338
+ * Whether `[wcs/command-not-ready]` was said in this outage: a `start()` refused because the
1274
1339
  * element is not ready loses what it carried, and `error` alone told only a page that binds it.
1275
- * Said once per element (docs/v4-direction-2026-10.md, the I/O command policy).
1340
+ * Said once per outage (docs/v4-direction-2026-10.md, the I/O command policy): a stream arriving
1341
+ * resets it. Once per element was once per page life — a route puts the same element back on
1342
+ * every entry, so the next outage's lost recording went unsaid (docs/seam-apps.md, shift-desk).
1276
1343
  */
1277
1344
  _toldNotReady = false;
1278
1345
  constructor(target) {
@@ -1358,6 +1425,9 @@ class RecorderCore extends EventTarget {
1358
1425
  */
1359
1426
  attachStream(stream) {
1360
1427
  this._stream = stream;
1428
+ // ready again: a start refused in the next outage is said again
1429
+ if (stream)
1430
+ this._toldNotReady = false;
1361
1431
  }
1362
1432
  /** Start recording the borrowed stream. Never throws — surfaces `error`. */
1363
1433
  start(options = {}) {
@@ -1470,7 +1540,8 @@ class RecorderCore extends EventTarget {
1470
1540
  return;
1471
1541
  this._toldNotReady = true;
1472
1542
  console.warn("[@wcstack/camera] [wcs/command-not-ready] start() was refused: no stream is attached, so nothing is recorded. " +
1473
- "It goes to `error` too. Wire a camera's stream to the recorder first. (Said once per element.)", this._target);
1543
+ "It goes to `error` too. Wire a camera's stream to the recorder first. " +
1544
+ "(Said once until a stream is attached again.)", this._target);
1474
1545
  }
1475
1546
  /** Stop recording; the assembled Blob is published from the recorder's onstop. */
1476
1547
  stop() {