diff --git a/apps/stage-tamagotchi/src/renderer/composables/use-vision-screen-capture.ts b/apps/stage-tamagotchi/src/renderer/composables/use-vision-screen-capture.ts index 5f7c6ec39..3df35bdec 100644 --- a/apps/stage-tamagotchi/src/renderer/composables/use-vision-screen-capture.ts +++ b/apps/stage-tamagotchi/src/renderer/composables/use-vision-screen-capture.ts @@ -36,6 +36,33 @@ export function useVisionScreenCapture(sourcesOptions: MaybeRefOrGetter sources.value.find(source => source.id === activeSourceId.value) || null) + function isActiveStream(stream: MediaStream | null | undefined) { + if (!stream) + return false + + return stream.getVideoTracks().some(track => track.readyState === 'live') + } + + function clearActiveStream() { + const stream = activeStream.value + if (!stream) { + activeStream.value = null + return + } + + stream.getTracks().forEach(track => track.stop()) + activeStream.value = null + } + + function attachStreamLifecycle(stream: MediaStream) { + stream.getTracks().forEach((track) => { + track.addEventListener('ended', () => { + if (activeStream.value === stream) + activeStream.value = null + }, { once: true }) + }) + } + async function refetchSources() { try { isRefetching.value = true @@ -61,9 +88,9 @@ export function useVisionScreenCapture(sourcesOptions: MaybeRefOrGetter 0 ? toObjectUrl(source.thumbnail, 'image/jpeg') : undefined, })) - if (!activeSourceId.value && sources.value.length > 0) { + const hasActiveSource = sources.value.some(source => source.id === activeSourceId.value) + if (!hasActiveSource) activeSourceId.value = sources.value[0]?.id || '' - } } finally { isRefetching.value = false @@ -75,9 +102,10 @@ export function useVisionScreenCapture(sourcesOptions: MaybeRefOrGetter track.stop()) - } + if (isActiveStream(activeStream.value)) + return activeStream.value! + + clearActiveStream() const handle = await setSource({ options: toRaw(toValue(sourcesOptions)), @@ -87,18 +115,20 @@ export function useVisionScreenCapture(sourcesOptions: MaybeRefOrGetter track.stop()) - } - activeStream.value = null + clearActiveStream() } function cleanup() { diff --git a/apps/stage-tamagotchi/src/renderer/pages/devtools/vision.vue b/apps/stage-tamagotchi/src/renderer/pages/devtools/vision.vue index 1f5e1bb60..6e6bf8915 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/devtools/vision.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/devtools/vision.vue @@ -130,6 +130,13 @@ const expectedRateMax = computed(() => { return Math.max(60, Math.ceil(60000 / interval)) }) +function hasLiveVideoStream(stream: MediaStream | null) { + if (!stream) + return false + + return stream.getVideoTracks().some(track => track.readyState === 'live') +} + async function ensureVideoStream() { if (!activeSourceId.value) return @@ -156,8 +163,10 @@ async function handleVisionTick() { return try { - if (!activeStream.value) + if (!hasLiveVideoStream(activeStream.value)) { + activeStream.value = null await ensureVideoStream() + } const video = videoRef.value if (!video) diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue index 42231bacf..ce2ff2a93 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/system/developer.vue @@ -43,12 +43,6 @@ const menu = computed(() => [ icon: 'i-solar:chat-square-call-bold-duotone', to: '/devtools/context-flow', }, - { - title: 'Context Observer', - description: 'Track how active context entries merge over time', - icon: 'i-solar:chat-square-bold-duotone', - to: '/devtools/context-observer', - }, { title: 'Relative Mouse', description: 'Get mouse position relative to the window', @@ -100,42 +94,27 @@ const openDevtoolsWindow = useElectronEventaInvoke(electronOpenDevtoolsWindow)