From 7129f5d8b856bd0d4d18f908e4380b16c1079408 Mon Sep 17 00:00:00 2001 From: Ze-Zheng Wu Date: Wed, 17 Jan 2024 23:24:38 +0800 Subject: [PATCH] chore: update userMediaStream logic --- main.ts | 74 +++++++++++---- package-lock.json | 14 ++- package.json | 3 +- src/scanner/userMediaStream.ts | 0 src/scanner/videoScanner.ts | 57 +++++++---- src/stream/index.ts | 1 + src/{scanner => stream}/queuefy.ts | 25 ++++- src/stream/userMediaStream.ts | 146 +++++++++++++++++++++++++++++ tsconfig.json | 4 +- tsconfig.node.json | 2 - 10 files changed, 275 insertions(+), 51 deletions(-) delete mode 100644 src/scanner/userMediaStream.ts create mode 100644 src/stream/index.ts rename src/{scanner => stream}/queuefy.ts (84%) create mode 100644 src/stream/userMediaStream.ts diff --git a/main.ts b/main.ts index 4d224145..2f658201 100644 --- a/main.ts +++ b/main.ts @@ -1,11 +1,11 @@ /// -import { initMediaStream } from "user-media-stream"; import { createVideoScanner } from "./src/scanner/videoScanner.js"; +import { createUserMediaStream } from "./src/stream/userMediaStream.js"; const videoElement = document.querySelector("video"); if (videoElement) { - const stream = await initMediaStream(videoElement, { + const userMediaStream = createUserMediaStream(videoElement, { initConstraints(supportedConstraints) { console.log("supported constraints", supportedConstraints); return { @@ -28,34 +28,24 @@ if (videoElement) { console.log("audio capabilities", capabilities); return {}; }, + onStart: () => console.log("stream start"), + onStop: () => console.log("stream stop"), + onUpdate: () => console.log("stream update"), }); - const videoTracks = stream.getVideoTracks(); - const audioTracks = stream.getAudioTracks(); - - console.log("video track", videoTracks); - console.log("audio track", audioTracks); - - console.log( - "video track settings", - videoTracks.map((videoTrack) => videoTrack.getSettings()), - ); - console.log( - "audio track settings", - audioTracks.map((audioTrack) => audioTrack.getSettings()), - ); - const videoScanner = createVideoScanner(videoElement, { minInterval: 0, readerOptions: { formats: [], }, onDetect: console.log, - onScanStart: () => console.log("scan start"), - onScanStop: () => console.log("scan stop"), + onStart: () => console.log("scan start"), + onStop: () => console.log("scan stop"), onClose: () => console.log("close"), }); + userMediaStream.start(); + videoScanner.start(); // await new Promise((resolve) => { @@ -64,6 +54,52 @@ if (videoElement) { // }, 5000); // }); + userMediaStream.setOptions({ + videoConstraints: { + advanced: [ + { + exposureMode: "manual", + exposureTime: 1000, + }, + ], + }, + }); + + userMediaStream.setOptions({ + videoConstraints: { + advanced: [ + { + exposureMode: "manual", + exposureTime: 10, + }, + ], + }, + }); + + // userMediaStream.setOptions({ + // videoConstraints: (capabilities) => { + // const { min, step } = capabilities.exposureTime ?? {}; + // if (typeof min === "number" && typeof step === "number") { + // return { + // advanced: [ + // { + // exposureMode: "manual", + // exposureTime: min + 3 * step, + // }, + // ], + // }; + // } else { + // return {}; + // } + // }, + // }); + + // await new Promise((resolve) => { + // setTimeout(() => { + // resolve(); + // }, 5000); + // }); + // videoScanner.stop(); // await new Promise((resolve) => { diff --git a/package-lock.json b/package-lock.json index c0e9216a..b0feb654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "license": "MIT", "dependencies": { "@types/emscripten": "^1.39.10", + "just-compare": "^2.3.0", "sha1-uint8array": "^0.10.7", - "user-media-stream": "^0.1.0-beta.4", + "user-media-stream": "^0.1.0-rc.1", "zustand": "^4.4.7" }, "devDependencies": { @@ -4327,6 +4328,11 @@ "node >= 0.2.0" ] }, + "node_modules/just-compare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", + "integrity": "sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7403,9 +7409,9 @@ } }, "node_modules/user-media-stream": { - "version": "0.1.0-beta.4", - "resolved": "https://registry.npmjs.org/user-media-stream/-/user-media-stream-0.1.0-beta.4.tgz", - "integrity": "sha512-IB0QMvK7mSKHUiTMkj54iP6nzcto+c2lSxL0yyDHbumEJBULMyh+624USu3ScOUC77z5XFW3w4VblLtF5LpUGg==", + "version": "0.1.0-rc.1", + "resolved": "https://registry.npmjs.org/user-media-stream/-/user-media-stream-0.1.0-rc.1.tgz", + "integrity": "sha512-7C6UUmgVg8E3ec1cQILqyPQDXEemVTgjMeM7cb4Bva8It5kWtdmhX/nO+x4ddYucVEpq3D0t6CyVaPjiJCWmgA==", "dependencies": { "webrtc-adapter": "^8.2.3" } diff --git a/package.json b/package.json index 9c3d6ddc..94236d70 100644 --- a/package.json +++ b/package.json @@ -155,8 +155,9 @@ }, "dependencies": { "@types/emscripten": "^1.39.10", + "just-compare": "^2.3.0", "sha1-uint8array": "^0.10.7", - "user-media-stream": "^0.1.0-beta.4", + "user-media-stream": "^0.1.0-rc.1", "zustand": "^4.4.7" } } diff --git a/src/scanner/userMediaStream.ts b/src/scanner/userMediaStream.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/scanner/videoScanner.ts b/src/scanner/videoScanner.ts index 5fe340b0..b51f087a 100644 --- a/src/scanner/videoScanner.ts +++ b/src/scanner/videoScanner.ts @@ -22,6 +22,10 @@ const MIN_SCAN_INTERVAL = 40; const scanSymbol = Symbol("scan"); const closeSymbol = Symbol("close"); +const startActionSymbol = Symbol("start"); +const stopActionSymbol = Symbol("stop"); +const closeActionSymbol = Symbol("close"); + /** * Options for configuring the VideoScanner. */ @@ -53,11 +57,11 @@ export interface VideoScannerOptions { /** * Callback function that is triggered when the scanning process starts. */ - onScanStart?: () => void; + onStart?: () => void; /** * Callback function that is triggered when the scanning process stops. */ - onScanStop?: () => void; + onStop?: () => void; /** * Callback function that is triggered when the scanning process is closed. */ @@ -85,15 +89,15 @@ interface VideoScannerAction { /** * Starts the scanning process. */ - start: () => void; + [startActionSymbol]: () => void; /** * Stops the scanning process. */ - stop: () => void; + [stopActionSymbol]: () => void; /** * Closes the scanner and performs cleanup operations. */ - close: () => void; + [closeActionSymbol]: () => void; } /** @@ -116,7 +120,19 @@ const defaultVideoScannerOptions: RequiredVideoScannerOptions = { /** * Interface combining actions and a method to update options for the VideoScanner. */ -export interface VideoScanner extends VideoScannerAction { +export interface VideoScanner { + /** + * Starts the scanning process. + */ + start: VideoScannerAction[typeof startActionSymbol]; + /** + * Stops the scanning process. + */ + stop: VideoScannerAction[typeof stopActionSymbol]; + /** + * Closes the scanner and performs cleanup operations. + */ + close: VideoScannerAction[typeof closeActionSymbol]; /** * Update the configuration options of the VideoScanner. * @@ -144,8 +160,8 @@ export function createVideoScanner( minInterval = defaultVideoScannerOptions.minInterval, onDetect = defaultVideoScannerOptions.onDetect, onUpdate = defaultVideoScannerOptions.onUpdate, - onScanStart = defaultVideoScannerOptions.onScanStart, - onScanStop = defaultVideoScannerOptions.onScanStop, + onStart: onStart = defaultVideoScannerOptions.onStart, + onStop: onStop = defaultVideoScannerOptions.onStop, onClose = defaultVideoScannerOptions.onClose, }: VideoScannerOptions = defaultVideoScannerOptions, ): VideoScanner { @@ -163,21 +179,22 @@ export function createVideoScanner( wasmLocation, readerOptions, minInterval, + onDetect, onUpdate, - onScanStart, - onScanStop, + onStart, + onStop, onClose, - start: () => { + [startActionSymbol]: () => { set({ [scanSymbol]: true }); }, - stop: () => { + [stopActionSymbol]: () => { set({ [scanSymbol]: false }); }, - close: () => { + [closeActionSymbol]: () => { set({ [closeSymbol]: true }); }, })), @@ -189,9 +206,9 @@ export function createVideoScanner( (options) => options[scanSymbol], (scan) => { if (scan) { - videoScannerStore.getState().onScanStart?.(); + videoScannerStore.getState().onStart?.(); } else { - videoScannerStore.getState().onScanStop?.(); + videoScannerStore.getState().onStop?.(); } }, ); @@ -203,7 +220,7 @@ export function createVideoScanner( (close) => { if (close) { globalThis.cancelAnimationFrame(requestAnimationFrameId); - videoScannerStore.getState().stop(); + videoScannerStore.getState()[stopActionSymbol](); unsubScan(); unsubClose(); unsubWasmLocation(); @@ -326,10 +343,10 @@ export function createVideoScanner( globalThis.requestAnimationFrame(frameRequestCallback); return { - start: videoScannerStore.getState().start, - stop: videoScannerStore.getState().stop, - close: videoScannerStore.getState().close, - setOptions: (videoScannerOptions: VideoScannerOptions) => + start: videoScannerStore.getState()[startActionSymbol], + stop: videoScannerStore.getState()[stopActionSymbol], + close: videoScannerStore.getState()[closeActionSymbol], + setOptions: (videoScannerOptions) => videoScannerStore.setState(videoScannerOptions), } as VideoScanner; } diff --git a/src/stream/index.ts b/src/stream/index.ts new file mode 100644 index 00000000..21a83690 --- /dev/null +++ b/src/stream/index.ts @@ -0,0 +1 @@ +export * from "./userMediaStream"; diff --git a/src/scanner/queuefy.ts b/src/stream/queuefy.ts similarity index 84% rename from src/scanner/queuefy.ts rename to src/stream/queuefy.ts index ab24f62a..d32de9d6 100644 --- a/src/scanner/queuefy.ts +++ b/src/stream/queuefy.ts @@ -1,3 +1,7 @@ +// TODO: rewrite + +import compare from "just-compare"; + /** * Represent a record of a function invocation, storing the function and its arguments. * @@ -25,6 +29,7 @@ interface QueuefyOptions { curr: InvokeRecord, prev: InvokeRecord, ) => boolean; + cleanUp?: () => Promise; } /** @@ -42,9 +47,13 @@ interface CreateQueuefyOptions extends QueuefyOptions { /** * Default options for the createQueuefy function, including fail-fast behavior. */ -const defaultCreateQueuefyOptions: Required = { +const defaultCreateQueuefyOptions: Required< + Omit +> & + CreateQueuefyOptions = { failFast: true, - skipStrategy: (curr, prev) => Object.is(curr.f, prev.f), + skipStrategy: (curr, prev) => + Object.is(curr.f, prev.f) && compare(curr.args, prev.args), }; /** @@ -58,6 +67,7 @@ const defaultCreateQueuefyOptions: Required = { export function createQueuefy({ failFast = defaultCreateQueuefyOptions.failFast, skipStrategy: _skipStrategy = defaultCreateQueuefyOptions.skipStrategy, + cleanUp: _cleanUp = defaultCreateQueuefyOptions.cleanUp, }: CreateQueuefyOptions = {}) { return (() => { // Internal queue to manage the execution order. @@ -79,7 +89,10 @@ export function createQueuefy({ */ return ( f: (...args: I) => Promise, - { skipStrategy = _skipStrategy }: QueuefyOptions = {}, + { + skipStrategy = _skipStrategy, + cleanUp = _cleanUp, + }: QueuefyOptions = {}, ) => (...args: I) => { const currInvokeRecord: InvokeRecord = { f, args }; @@ -87,6 +100,12 @@ export function createQueuefy({ return queue as Promise; } prevInvokeRecord = currInvokeRecord as unknown as InvokeRecord; + if (cleanUp) { + queue = queue.then( + () => cleanUp(), + failFast ? undefined : () => cleanUp(), + ); + } // Enqueue the function execution, ensuring sequential execution. If `failFast` is true, any // error will halt the queue. Otherwise, the queue continues despite errors. return (queue = queue.then( diff --git a/src/stream/userMediaStream.ts b/src/stream/userMediaStream.ts new file mode 100644 index 00000000..f082ad20 --- /dev/null +++ b/src/stream/userMediaStream.ts @@ -0,0 +1,146 @@ +import { createStore } from "zustand/vanilla"; +import { subscribeWithSelector } from "zustand/middleware"; +import { shallow } from "zustand/shallow"; +import { + type InitConstraints, + type VideoConstraints, + type AudioConstraints, + initMediaStream as _initMediaStream, + constrainMediaStream as _constrainMediaStream, + stopMediaStream as _stopMediaStream, +} from "user-media-stream"; +import { createQueuefy } from "./queuefy.js"; + +const streamSymbol = Symbol("stream"); +const startActionSymbol = Symbol("start"); +const stopActionSymbol = Symbol("stop"); + +export interface UserMediaStreamOptions { + initConstraints?: InitConstraints; + videoConstraints?: VideoConstraints; + audioConstraints?: AudioConstraints; + onStart?: (stream: MediaStream) => void; + onStop?: () => void; + onUpdate?: (stream: MediaStream) => void; +} + +interface UserMediaStreamState extends UserMediaStreamOptions { + [streamSymbol]: MediaStream | null; +} + +interface UserMediaStreamAction { + [startActionSymbol]: () => Promise; + [stopActionSymbol]: () => Promise; +} + +interface DefaultUserMediaStreamOptions extends UserMediaStreamOptions {} + +const defaultUserMediaStreamOptions: DefaultUserMediaStreamOptions = { + initConstraints: { + video: true, + audio: false, + }, +}; + +export interface UserMediaStream { + start: UserMediaStreamAction[typeof startActionSymbol]; + stop: UserMediaStreamAction[typeof stopActionSymbol]; + setOptions: (userMediaStreamOptions: UserMediaStreamOptions) => void; +} + +export function createUserMediaStream( + videoElement: HTMLVideoElement, + { + initConstraints = defaultUserMediaStreamOptions.initConstraints, + videoConstraints = defaultUserMediaStreamOptions.videoConstraints, + audioConstraints = defaultUserMediaStreamOptions.audioConstraints, + onStart = defaultUserMediaStreamOptions.onStart, + onStop = defaultUserMediaStreamOptions.onStop, + onUpdate = defaultUserMediaStreamOptions.onUpdate, + }: UserMediaStreamOptions = {}, +): UserMediaStream { + const queuefy = createQueuefy({ failFast: false }); + const initMediaStream = queuefy(_initMediaStream, { + cleanUp: async () => { + const stream = userMediaStreamStore.getState()[streamSymbol]; + if (stream) { + await _stopMediaStream(videoElement, stream); + } + }, + }); + const constrainMediaStream = queuefy(_constrainMediaStream); + const stopMediaStream = queuefy(_stopMediaStream); + + const userMediaStreamStore = createStore< + UserMediaStreamState & UserMediaStreamAction + >()( + subscribeWithSelector( + (set, get) => ({ + [streamSymbol]: null, + + initConstraints, + videoConstraints, + audioConstraints, + + onStart, + onStop, + onUpdate, + + [startActionSymbol]: async () => { + const { + onStart, + initConstraints, + videoConstraints, + audioConstraints, + } = get(); + const stream = await initMediaStream(videoElement, { + initConstraints, + videoConstraints, + audioConstraints, + }); + set({ + [streamSymbol]: stream, + }); + onStart?.(stream); + return stream; + }, + + [stopActionSymbol]: async () => { + const { [streamSymbol]: stream, onStop } = get(); + if (stream === null) { + return; + } + await stopMediaStream(videoElement, stream); + set({ + [streamSymbol]: null, + }); + onStop?.(); + }, + }), + ), + ); + + userMediaStreamStore.subscribe( + (options) => [options.videoConstraints, options.audioConstraints], + async ([videoConstraints, audioConstraints]) => { + const { [streamSymbol]: stream, onUpdate } = + userMediaStreamStore.getState(); + if (stream === null) { + return; + } + await constrainMediaStream(stream, { + videoConstraints, + audioConstraints, + }); + onUpdate?.(stream); + }, + { equalityFn: shallow }, + ); + + return { + start: userMediaStreamStore.getState()[startActionSymbol], + stop: userMediaStreamStore.getState()[stopActionSymbol], + setOptions: (userMediaStreamOptions) => + userMediaStreamStore.setState(userMediaStreamOptions), + }; +} diff --git a/tsconfig.json b/tsconfig.json index 224ab907..6fb29764 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,11 @@ "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, - "module": "NodeNext", + "module": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["emscripten"], "skipLibCheck": true, - "moduleResolution": "NodeNext", + "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, "declaration": true, diff --git a/tsconfig.node.json b/tsconfig.node.json index 73ea2be7..8946667f 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,8 +3,6 @@ "compilerOptions": { "composite": true, "types": ["node"], - "module": "ESNext", - "moduleResolution": "Bundler", "resolveJsonModule": true, "allowJs": true },