feat: device pinning — stream from a chosen pair (SpecificDeviceSelector)#13
Conversation
…nning) startStreamSession(deviceId) now binds the session to a specific paired pair — a WearableDevice.id from getDevices() — via the SDK's SpecificDeviceSelector, instead of always using the shared AutoDeviceSelector. Pass null to auto-select. - Dart: rename the positional deviceUUID -> deviceId on startStreamSession / stopStreamSession / capturePhoto (+ the wire key); positional, non-breaking. - The pin is state (pinnedDeviceId) applied at startStreamSession through a single pin-aware selector factory used at EVERY construction site, so the blind-rebuild recovery path can't silently drop it. - Requesting a different deviceId while a stream is active returns STREAM_ACTIVE (PlatformException) — stop first; same id returns the existing texture. - iOS: selector widened to `any DeviceSelector`; on a pin change tear down the session, rebuild the selector, relaunch the availability watchdog, restart both event handlers (force: true), bump lastSelectorRebuild. - Android: deferred nullable selector holder + accessor (never constructed before Wearables.initialize); NOT_INITIALIZED guard on startStreamSession; both handlers gained restartMonitoring(force) to rebind after a swap. - Example: tap a pair (or "Automatic") in the Paired-devices sheet to choose which one streams next. Selection is the sole source of truth (null = Automatic); the mock is pinned explicitly on pair and cleared on unpair only when the selection still points at it. - Tests: plugin wire (deviceId forwarding / STREAM_ACTIVE) + example provider selection policy. Docs: CHANGELOG 0.6.0, README, AGENTS, agent rules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…zation, iOS selector monitoring) - [P1] Conflict guard now checks the actual Stream state: a non-nil but STOPPED/STOPPING stream counts as inactive (torn down and recreated), so restarting the same device no longer returns a stale texture and switching no longer false-positives STREAM_ACTIVE. Both platforms. - [P1] Serialize concurrent starts: reject a start while another is in flight (isStartingSession / startInProgress). Android ensureSessionStarted also bails if the session reaches STOPPED instead of STARTED, so a session torn down mid-start can't hang the awaiter. - [P2] iOS pins via a filtered AutoDeviceSelector (filter: identifier == id) instead of SpecificDeviceSelector — the latter's iOS activeDeviceStream() emits once then completes, which would stop the availability watchdog from observing the pinned device disconnecting. The filtered AutoDeviceSelector keeps a continuous stream (emits nil on disconnect). iOS selector field + handler provider types reverted from `any DeviceSelector` to concrete AutoDeviceSelector. Android keeps SpecificDeviceSelector (its Device exposes no id to filter on). Verified: dart analyze + flutter test (root + example) green; flutter build apk + ios --no-codesign green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed all three review findings in
Re-verified: |
… selector doc - [P1] Gate the example Start button on the *selected* pair's connectivity (new `canStartSelected`), not the shared selector's activeDevice. With a pin to a disconnected pair, `hasActiveDevice` stays false so Start was disabled — and since selecting a different (connected) pair only updates the Dart selection until the next startStreamSession, that switching call could never fire (deadlock). `canStartSelected` reflects the selected pair's link state from the last getDevices snapshot (falling back to hasActiveDevice in Automatic mode), so a connected pair can be picked to switch to — the native selector re-pins on start. Added a provider test. - [P3] dat-streaming.mdc no longer claims pinning always uses SpecificDeviceSelector; now notes SpecificDeviceSelector on Android and a filtered AutoDeviceSelector on iOS, matching the CHANGELOG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Both remaining findings addressed in
No native changes this round, so the prior green |
…Session
On a device-pin change the shared selector is rebuilt, and a freshly-built
selector resolves its active device asynchronously — so createSession ran
against an unresolved selector and failed with noEligibleDevice. (Seen on
hardware: the pinned device's `activeDevice -> true` arrived a beat *after*
the failure, confirming the device was found, just too late.)
startStreamSession now waits — after a pin change to a specific device — for
the selector's activeDevice to resolve (up to ~8s) before creating the
session. iOS polls deviceSelector.activeDevice; Android awaits
activeDeviceFlow().first { it != null } under withTimeoutOrNull. Automatic
(no pin) is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ces, dep hints - README: streaming snippets pass null (auto) with a pinning comment instead of an undefined `deviceId` variable. - Copilot instructions: add getDevices() / pinning / STREAM_ACTIVE and rename the streaming param deviceUUID -> deviceId (it was behind on both 0.6.0 features). - Bump core + mock dependency hints from ^0.5.x to ^0.6.0 across README, AGENTS, and the agent rules/skills (^0.5.x would exclude the 0.6.0 packages). - Mock-device examples keep `deviceUUID` (the mock's own pair UUID) — passing it now correctly pins the mock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stacked on #12 (
feat/get-devices-api) — review/merge that first; this PR's diff is pinning-only.Summary
Step 2 of the device-selection work:
startStreamSession(deviceId)now streams from a specific paired pair instead of whatever the sharedAutoDeviceSelectorhappens to pick. Pass aWearableDevice.id(from thegetDevices()added in #12) to pin it via the SDK'sSpecificDeviceSelector; passnullto auto-select. This fixes the "connects to a random pair" problem with two pairs.How it works
pinnedDeviceId) applied atstartStreamSession, routed through a single pin-aware selector factory used at every construction site — so the load-bearing blind-rebuild recovery path keeps the pin instead of silently dropping it.SpecificDeviceSelectorwhen pinned), so the existing availability-watchdog / thermal /getDevices().isActivestack stays correct — we never run a specific selector beside the auto one.deviceIdwhile a stream is already active returnsSTREAM_ACTIVE(PlatformException) rather than silently returning the old device's texture — stop first.Notable details
Deviceexposes no identifier, so anAutoDeviceSelectorfilter can't match by id —SpecificDeviceSelector(DeviceIdentifier)is the only id-based option, hence the uniform approach.Wearables.initialize()), andstartStreamSessionguards withNOT_INITIALIZEDbefore any selector/texture work.restartMonitoring(force: true)on both event handlers (added aforcepath on Android) so nothing keeps observing the old selector.deviceUUIDparam onstartStreamSession/stopStreamSession/capturePhotois renamed todeviceId(positional → non-breaking).Example app
Tap a pair (or Automatic) in the Paired-devices sheet to choose which one streams next (takes effect on the next Start, per the chosen UX). Selection is the sole source of truth (
null= Automatic); the mock is pinned explicitly on pair and cleared on unpair only when the selection still points at it.Verification
dart analyzeclean (root + example);flutter testgreen in both (startStreamSessionwire forwarding /STREAM_ACTIVE; provider selection policy incl. unpair-preserves-real-pair).flutter build apk✓ andflutter build ios --no-codesign✓.STREAM_ACTIVEon switch-while-streaming; pinned-device disconnect/reconnect recovery.🤖 Generated with Claude Code