diff --git a/src/renderer/src/components/emulator-pane/use-emulator-video-stream.test.tsx b/src/renderer/src/components/emulator-pane/use-emulator-video-stream.test.tsx new file mode 100644 index 00000000000..fadaf1abf07 --- /dev/null +++ b/src/renderer/src/components/emulator-pane/use-emulator-video-stream.test.tsx @@ -0,0 +1,98 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useEmulatorVideoStream } from './use-emulator-video-stream' +import type { StreamSize } from './emulator-device-frame-layout' + +type Frame = { displayWidth: number; displayHeight: number; close: () => void } +type Meta = { + streamId: string + deviceId: string + meta: { codecId: string; width: number; height: number } +} + +let output: (frame: Frame) => void +let metaListener: (meta: Meta) => void +let streamId: string +let reports: StreamSize[] +let root: Root +let contextDescriptor: PropertyDescriptor | undefined + +function Screen() { + const { canvasRef } = useEmulatorVideoStream('device', 'stream', true, (size) => + reports.push(size) + ) + return +} + +beforeEach(() => { + Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { configurable: true, value: true }) + reports = [] + vi.stubGlobal( + 'VideoDecoder', + class { + state = 'unconfigured' + constructor(init: { output: (frame: Frame) => void }) { + output = init.output + } + close(): void {} + } + ) + vi.stubGlobal('EncodedVideoChunk', class {}) + contextDescriptor = Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype, 'getContext') + Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { + configurable: true, + value: () => ({ clearRect: () => {}, drawImage: () => {} }) + }) + Object.defineProperty(window, 'api', { + configurable: true, + value: { + emulator: { + startVideoStream: async (args: { streamId: string }) => { + streamId = args.streamId + return args + }, + stopVideoStream: async () => {}, + onVideoStreamMeta: (cb: (meta: Meta) => void) => { + metaListener = cb + return () => {} + } + } + } + }) + root = createRoot(document.createElement('div')) +}) + +afterEach(() => { + act(() => root.unmount()) + if (contextDescriptor) { + Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', contextDescriptor) + } else { + Reflect.deleteProperty(HTMLCanvasElement.prototype, 'getContext') + } + Reflect.deleteProperty(window, 'api') + vi.unstubAllGlobals() +}) + +describe('useEmulatorVideoStream decoded size', () => { + it('reports decoded frame size changes once after codec metadata', async () => { + await act(async () => root.render()) + const frame = (): void => output({ displayWidth: 480, displayHeight: 240, close: () => {} }) + act(() => { + metaListener({ + streamId, + deviceId: 'device', + meta: { codecId: 'h264', width: 240, height: 480 } + }) + frame() + frame() + frame() + }) + expect(reports).toEqual([ + { width: 240, height: 480 }, + { width: 480, height: 240 } + ]) + }) +}) diff --git a/src/renderer/src/components/emulator-pane/use-emulator-video-stream.ts b/src/renderer/src/components/emulator-pane/use-emulator-video-stream.ts index b0fdf11c961..5ba4fe622d6 100644 --- a/src/renderer/src/components/emulator-pane/use-emulator-video-stream.ts +++ b/src/renderer/src/components/emulator-pane/use-emulator-video-stream.ts @@ -43,6 +43,7 @@ export function useEmulatorVideoStream( const onSizeRef = useRef(onSize) onSizeRef.current = onSize + // oxlint-disable-next-line react-doctor/effect-needs-cleanup -- cleanup() clears the first-frame timeout. useEffect(() => { const api = (window as { api?: { emulator?: EmulatorVideoApi } }).api?.emulator if (!enabled || !deviceId) { @@ -65,6 +66,14 @@ export function useEmulatorVideoStream( let configured = false let timestamp = 0 let configBytes: Uint8Array | null = null + let reportedSize: StreamSize | null = null + const reportSize = (width: number, height: number): void => { + if (reportedSize?.width === width && reportedSize.height === height) { + return + } + reportedSize = { width, height } + onSizeRef.current?.(reportedSize) + } const currentStreamId = newVideoStreamId() let streamId: string | null = currentStreamId let unsubMeta: (() => void) | undefined @@ -84,6 +93,7 @@ export function useEmulatorVideoStream( canvas.width = frame.displayWidth canvas.height = frame.displayHeight } + reportSize(frame.displayWidth, frame.displayHeight) ctx.drawImage(frame, 0, 0) } frame.close() @@ -135,7 +145,7 @@ export function useEmulatorVideoStream( unsubMeta = api.onVideoStreamMeta?.((msg) => { if (!disposed && msg.streamId === streamId && msg.deviceId === deviceId) { - onSizeRef.current?.({ width: msg.meta.width, height: msg.meta.height }) + reportSize(msg.meta.width, msg.meta.height) } })