@flighthq/webcam 0.1.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.
@@ -0,0 +1,3 @@
1
+ export * from './webcam';
2
+ export * from './webcamStream';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './webcam';
2
+ export * from './webcamStream';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { WebcamBackend, WebcamCaptureOptions, WebcamPhoto, WebcamVideo } from '@flighthq/types';
2
+ export declare function createWebWebcamBackend(): WebcamBackend;
3
+ export declare function getWebcamBackend(): WebcamBackend;
4
+ export declare function recordWebcamVideo(options?: Readonly<WebcamCaptureOptions>): Promise<WebcamVideo | null>;
5
+ export declare function requestWebcamPermission(): Promise<boolean>;
6
+ export declare function selectWebcamImage(options?: Readonly<WebcamCaptureOptions>): Promise<WebcamPhoto | null>;
7
+ export declare function setWebcamBackend(backend: WebcamBackend | null): void;
8
+ export declare function takeWebcamPhoto(options?: Readonly<WebcamCaptureOptions>): Promise<WebcamPhoto | null>;
9
+ //# sourceMappingURL=webcam.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcam.d.ts","sourceRoot":"","sources":["../src/webcam.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKrG,wBAAgB,sBAAsB,IAAI,aAAa,CAsFtD;AAGD,wBAAgB,gBAAgB,IAAI,aAAa,CAGhD;AAGD,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAEvG;AAGD,wBAAgB,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE1D;AAGD,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAEvG;AAGD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,GAAG,IAAI,CAEpE;AAGD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAErG"}
package/dist/webcam.js ADDED
@@ -0,0 +1,125 @@
1
+ // Builds the default web backend over a transient <input type="file">. capture resolves to null when
2
+ // the document is absent (jsdom), the user cancels, or the file cannot be read — capture is not
3
+ // guaranteed. Real pixel dimensions are not decoded; width/height resolve to 0.
4
+ export function createWebWebcamBackend() {
5
+ return {
6
+ capture(options) {
7
+ return new Promise((resolve) => {
8
+ if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
9
+ resolve(null);
10
+ return;
11
+ }
12
+ try {
13
+ const input = document.createElement('input');
14
+ input.type = 'file';
15
+ input.accept = 'image/*';
16
+ if (options.source === 'camera')
17
+ input.capture = 'environment';
18
+ input.onchange = () => {
19
+ const file = input.files?.[0] ?? null;
20
+ if (file === null) {
21
+ resolve(null);
22
+ return;
23
+ }
24
+ const reader = new FileReader();
25
+ reader.onload = () => {
26
+ resolve({
27
+ dataUrl: typeof reader.result === 'string' ? reader.result : '',
28
+ width: 0,
29
+ height: 0,
30
+ format: file.type,
31
+ });
32
+ };
33
+ reader.onerror = () => resolve(null);
34
+ reader.readAsDataURL(file);
35
+ };
36
+ input.click();
37
+ }
38
+ catch {
39
+ resolve(null);
40
+ }
41
+ });
42
+ },
43
+ captureVideo(options) {
44
+ return new Promise((resolve) => {
45
+ if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
46
+ resolve(null);
47
+ return;
48
+ }
49
+ try {
50
+ const input = document.createElement('input');
51
+ input.type = 'file';
52
+ input.accept = 'video/*';
53
+ if (options.source === 'camera')
54
+ input.capture = 'environment';
55
+ input.onchange = () => {
56
+ const file = input.files?.[0] ?? null;
57
+ if (file === null) {
58
+ resolve(null);
59
+ return;
60
+ }
61
+ const reader = new FileReader();
62
+ reader.onload = () => {
63
+ resolve({
64
+ dataUrl: typeof reader.result === 'string' ? reader.result : '',
65
+ // The web file input cannot decode the clip; native hosts report a real duration.
66
+ duration: 0,
67
+ format: file.type,
68
+ });
69
+ };
70
+ reader.onerror = () => resolve(null);
71
+ reader.readAsDataURL(file);
72
+ };
73
+ input.click();
74
+ }
75
+ catch {
76
+ resolve(null);
77
+ }
78
+ });
79
+ },
80
+ async requestPermission() {
81
+ // The Permissions API only reports state; it does not prompt. A native host performs a real
82
+ // permission request. Returns false when the API is absent (jsdom, older browsers) or denied.
83
+ if (typeof navigator === 'undefined')
84
+ return false;
85
+ try {
86
+ const permissions = navigator.permissions;
87
+ if (permissions === undefined || typeof permissions.query !== 'function')
88
+ return false;
89
+ const status = await permissions.query({ name: 'camera' });
90
+ return status.state === 'granted';
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ },
96
+ };
97
+ }
98
+ // The active camera backend, or a lazily-created web default. There is always a backend.
99
+ export function getWebcamBackend() {
100
+ if (_backend === null)
101
+ _backend = createWebWebcamBackend();
102
+ return _backend;
103
+ }
104
+ // Records a video from the device camera. Resolves null when cancelled, denied, or unavailable.
105
+ export function recordWebcamVideo(options) {
106
+ return getWebcamBackend().captureVideo({ ...options, source: 'camera' });
107
+ }
108
+ // Requests camera access permission. Resolves false when denied or when the host cannot prompt.
109
+ export function requestWebcamPermission() {
110
+ return getWebcamBackend().requestPermission();
111
+ }
112
+ // Picks an existing image from the photo library. Resolves null when cancelled or unavailable.
113
+ export function selectWebcamImage(options) {
114
+ return getWebcamBackend().capture({ ...options, source: 'photos' });
115
+ }
116
+ // Installs a native host camera backend; pass null to fall back to the web default.
117
+ export function setWebcamBackend(backend) {
118
+ _backend = backend;
119
+ }
120
+ // Captures a photo from the device camera. Resolves null when cancelled, denied, or unavailable.
121
+ export function takeWebcamPhoto(options) {
122
+ return getWebcamBackend().capture({ ...options, source: 'camera' });
123
+ }
124
+ let _backend = null;
125
+ //# sourceMappingURL=webcam.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcam.js","sourceRoot":"","sources":["../src/webcam.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { EntityRuntime, WebcamFacingMode, WebcamStream } from '@flighthq/types';
2
+ export interface WebcamStreamRuntime extends EntityRuntime {
3
+ binding: null;
4
+ mediaStream: MediaStream | null;
5
+ videoElement: HTMLVideoElement | null;
6
+ }
7
+ export declare function createWebcamStreamEntity(data: {
8
+ active: boolean;
9
+ deviceId: string;
10
+ facingMode: WebcamFacingMode | null;
11
+ frameRate: number;
12
+ height: number;
13
+ id: string;
14
+ width: number;
15
+ }): WebcamStream;
16
+ export declare function getWebcamStreamRuntime(stream: Readonly<WebcamStream>): WebcamStreamRuntime | null;
17
+ //# sourceMappingURL=webcamStream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcamStream.d.ts","sourceRoot":"","sources":["../src/webcamStream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGrF,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,OAAO,EAAE,IAAI,CAAC;IAEd,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACvC;AAKD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,YAAY,CAoBf;AAGD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,mBAAmB,GAAG,IAAI,CAIjG"}
@@ -0,0 +1,34 @@
1
+ import { createEntity } from '@flighthq/entity';
2
+ import { EntityRuntimeKey } from '@flighthq/types';
3
+ // Allocates a WebcamStream entity with an attached runtime slot holding the MediaStream.
4
+ // The fields in data become the entity's public fields; the runtime is initialized with a
5
+ // placeholder MediaStream that is replaced immediately by the caller.
6
+ export function createWebcamStreamEntity(data) {
7
+ const stream = createEntity({
8
+ active: data.active,
9
+ deviceId: data.deviceId,
10
+ facingMode: data.facingMode,
11
+ frameRate: data.frameRate,
12
+ height: data.height,
13
+ id: data.id,
14
+ width: data.width,
15
+ });
16
+ // Initialize with null; the caller overwrites rt.mediaStream once a live stream is acquired. Using
17
+ // null (not new MediaStream()) avoids depending on MediaStream in environments that lack it (jsdom
18
+ // without the media API).
19
+ const rt = {
20
+ binding: null,
21
+ mediaStream: null,
22
+ videoElement: null,
23
+ };
24
+ stream[EntityRuntimeKey] = rt;
25
+ return stream;
26
+ }
27
+ // Returns the WebcamStreamRuntime attached to stream, or null when stream has no runtime.
28
+ export function getWebcamStreamRuntime(stream) {
29
+ const rt = stream[EntityRuntimeKey];
30
+ if (rt === undefined || rt === null)
31
+ return null;
32
+ return rt;
33
+ }
34
+ //# sourceMappingURL=webcamStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcamStream.js","sourceRoot":"","sources":["../src/webcamStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AASnD,yFAAyF;AACzF,0FAA0F;AAC1F,sEAAsE;AACtE,MAAM,UAAU,wBAAwB,CAAC,IAQxC;IACC,MAAM,MAAM,GAAG,YAAY,CAAC;QAC1B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC,CAAC;IACH,mGAAmG;IACnG,mGAAmG;IACnG,0BAA0B;IAC1B,MAAM,EAAE,GAAwB;QAC9B,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,IAAI;QACjB,YAAY,EAAE,IAAI;KACnB,CAAC;IACF,MAAM,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAC;IAC9B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,sBAAsB,CAAC,MAA8B;IACnE,MAAM,EAAE,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACpC,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,EAAyB,CAAC;AACnC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/webcam",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/entity": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Webcam capture and photo picking over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,103 @@
1
+ import type { WebcamBackend, WebcamCaptureOptions } from '@flighthq/types';
2
+
3
+ import {
4
+ createWebWebcamBackend,
5
+ getWebcamBackend,
6
+ recordWebcamVideo,
7
+ requestWebcamPermission,
8
+ selectWebcamImage,
9
+ setWebcamBackend,
10
+ takeWebcamPhoto,
11
+ } from './webcam';
12
+
13
+ function fakeBackend(): WebcamBackend & { lastOptions: WebcamCaptureOptions | null } {
14
+ return {
15
+ lastOptions: null,
16
+ async capture(options) {
17
+ this.lastOptions = { ...options };
18
+ return { dataUrl: 'data:image/png;base64,xx', width: 0, height: 0, format: 'image/png' };
19
+ },
20
+ async captureVideo(options) {
21
+ this.lastOptions = { ...options };
22
+ return { dataUrl: 'data:video/mp4;base64,xx', duration: 0, format: 'video/mp4' };
23
+ },
24
+ async requestPermission() {
25
+ return true;
26
+ },
27
+ };
28
+ }
29
+
30
+ afterEach(() => setWebcamBackend(null));
31
+
32
+ describe('createWebWebcamBackend', () => {
33
+ it('returns a backend whose capture yields a Promise without throwing synchronously', () => {
34
+ const backend = createWebWebcamBackend();
35
+ expect(backend.capture({}) instanceof Promise).toBe(true);
36
+ });
37
+ });
38
+
39
+ describe('getWebcamBackend', () => {
40
+ it('falls back to a web backend', () => {
41
+ expect(getWebcamBackend()).not.toBeNull();
42
+ });
43
+
44
+ it('returns the registered backend', () => {
45
+ const backend = fakeBackend();
46
+ setWebcamBackend(backend);
47
+ expect(getWebcamBackend()).toBe(backend);
48
+ });
49
+ });
50
+
51
+ describe('recordWebcamVideo', () => {
52
+ it('captures video with the camera source', async () => {
53
+ const backend = fakeBackend();
54
+ setWebcamBackend(backend);
55
+ const video = await recordWebcamVideo({ maxDurationMs: 5000 });
56
+ expect(video).not.toBeNull();
57
+ expect(backend.lastOptions).toEqual({ maxDurationMs: 5000, source: 'camera' });
58
+ });
59
+
60
+ it('returns a Promise from the web backend without throwing', () => {
61
+ const backend = createWebWebcamBackend();
62
+ expect(backend.captureVideo({}) instanceof Promise).toBe(true);
63
+ });
64
+ });
65
+
66
+ describe('requestWebcamPermission', () => {
67
+ it('delegates to the active backend', async () => {
68
+ setWebcamBackend(fakeBackend());
69
+ expect(await requestWebcamPermission()).toBe(true);
70
+ });
71
+
72
+ it('returns a boolean from the web backend without throwing', async () => {
73
+ expect(typeof (await requestWebcamPermission())).toBe('boolean');
74
+ });
75
+ });
76
+
77
+ describe('selectWebcamImage', () => {
78
+ it('captures with the photos source', async () => {
79
+ const backend = fakeBackend();
80
+ setWebcamBackend(backend);
81
+ const photo = await selectWebcamImage({ quality: 0.5 });
82
+ expect(photo).not.toBeNull();
83
+ expect(backend.lastOptions).toEqual({ quality: 0.5, source: 'photos' });
84
+ });
85
+ });
86
+
87
+ describe('setWebcamBackend', () => {
88
+ it('clears back to the web fallback when passed null', () => {
89
+ setWebcamBackend(fakeBackend());
90
+ setWebcamBackend(null);
91
+ expect(getWebcamBackend()).not.toBeNull();
92
+ });
93
+ });
94
+
95
+ describe('takeWebcamPhoto', () => {
96
+ it('captures with the camera source', async () => {
97
+ const backend = fakeBackend();
98
+ setWebcamBackend(backend);
99
+ const photo = await takeWebcamPhoto({ allowEditing: true });
100
+ expect(photo).not.toBeNull();
101
+ expect(backend.lastOptions).toEqual({ allowEditing: true, source: 'camera' });
102
+ });
103
+ });
@@ -0,0 +1,86 @@
1
+ import type { WebcamStream } from '@flighthq/types';
2
+ import { EntityRuntimeKey } from '@flighthq/types';
3
+
4
+ import { createWebcamStreamEntity, getWebcamStreamRuntime } from './webcamStream';
5
+
6
+ describe('createWebcamStreamEntity', () => {
7
+ it('creates a WebcamStream with the correct public fields', () => {
8
+ const stream = createWebcamStreamEntity({
9
+ active: true,
10
+ deviceId: 'device-1',
11
+ facingMode: 'environment',
12
+ frameRate: 30,
13
+ height: 480,
14
+ id: 'stream-123',
15
+ width: 640,
16
+ });
17
+ expect(stream.active).toBe(true);
18
+ expect(stream.deviceId).toBe('device-1');
19
+ expect(stream.facingMode).toBe('environment');
20
+ expect(stream.frameRate).toBe(30);
21
+ expect(stream.height).toBe(480);
22
+ expect(stream.id).toBe('stream-123');
23
+ expect(stream.width).toBe(640);
24
+ });
25
+
26
+ it('attaches a runtime slot to the entity', () => {
27
+ const stream = createWebcamStreamEntity({
28
+ active: true,
29
+ deviceId: '',
30
+ facingMode: null,
31
+ frameRate: 0,
32
+ height: 0,
33
+ id: 'x',
34
+ width: 0,
35
+ });
36
+ expect(stream[EntityRuntimeKey]).not.toBeUndefined();
37
+ });
38
+
39
+ it('runtime slot is initialized (mediaStream placeholder is present)', () => {
40
+ const stream = createWebcamStreamEntity({
41
+ active: false,
42
+ deviceId: '',
43
+ facingMode: null,
44
+ frameRate: 0,
45
+ height: 0,
46
+ id: 'x',
47
+ width: 0,
48
+ });
49
+ const rt = getWebcamStreamRuntime(stream);
50
+ expect(rt).not.toBeNull();
51
+ // mediaStream starts null; the caller attaches a live stream before use.
52
+ expect(rt?.mediaStream).toBeNull();
53
+ expect(rt?.videoElement).toBeNull();
54
+ });
55
+ });
56
+
57
+ describe('getWebcamStreamRuntime', () => {
58
+ it('returns null for a stream without a runtime', () => {
59
+ const stream = {
60
+ active: true,
61
+ deviceId: '',
62
+ facingMode: null,
63
+ frameRate: 0,
64
+ height: 0,
65
+ id: 'x',
66
+ width: 0,
67
+ [EntityRuntimeKey]: undefined,
68
+ } as WebcamStream;
69
+ expect(getWebcamStreamRuntime(stream)).toBeNull();
70
+ });
71
+
72
+ it('returns the runtime for a stream created by createWebcamStreamEntity', () => {
73
+ const stream = createWebcamStreamEntity({
74
+ active: true,
75
+ deviceId: 'dev',
76
+ facingMode: 'user',
77
+ frameRate: 60,
78
+ height: 720,
79
+ id: 'stream-rt',
80
+ width: 1280,
81
+ });
82
+ const rt = getWebcamStreamRuntime(stream);
83
+ expect(rt).not.toBeNull();
84
+ expect(rt?.videoElement).toBeNull();
85
+ });
86
+ });