Skip to content

Commit 2450cb3

Browse files
committed
chore: restructure
1 parent b28a7b0 commit 2450cb3

File tree

11 files changed

+208
-231
lines changed

11 files changed

+208
-231
lines changed

main.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/// <reference types="./src/react/camera/media.d.ts" />
2-
import { startCamera } from "./src/react/camera/useCamera";
1+
/// <reference types="./src/camera/media.d.ts" />
2+
import { startCamera } from "./src/camera/index";
33

44
const videoElement = document.querySelector("video");
55

@@ -23,4 +23,7 @@ if (videoElement) {
2323
});
2424
console.log("video track", videoTrack);
2525
console.log("audio track", audioTrack);
26+
27+
console.log("video track settings", videoTrack.getSettings());
28+
console.log("audio track settings", audioTrack.getSettings());
2629
}

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
},
145145
"dependencies": {
146146
"@types/emscripten": "^1.39.10",
147+
"just-clone": "^6.2.0",
147148
"webrtc-adapter": "^8.2.3"
148149
}
149150
}
File renamed without changes.

src/camera/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./startCamera.js";

src/react/camera/media.d.ts renamed to src/camera/media.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// TODO: complete constraints and capabilities
2-
// TODO: export type declarations
2+
// TODO: export type declarations in package.json
33

44
declare interface MediaTrackSupportedConstraints {
55
brightness?: boolean;

src/camera/startCamera.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import clone from "just-clone";
2+
3+
const GET_CAPABILITIES_TIMEOUT = 500;
4+
const IOS_PWA_WAIT_TIMEOUT = 3000;
5+
6+
export interface Constraints {
7+
initConstraints?:
8+
| MediaStreamConstraints
9+
| ((
10+
supportedConstraints: MediaTrackSupportedConstraints,
11+
) => MediaStreamConstraints | Promise<MediaStreamConstraints>);
12+
videoConstraints?:
13+
| MediaTrackConstraints
14+
| ((
15+
capabilities: MediaTrackCapabilities,
16+
) => MediaTrackConstraints | Promise<MediaTrackConstraints>);
17+
audioConstraints?:
18+
| MediaTrackConstraints
19+
| ((
20+
capabilities: MediaTrackCapabilities,
21+
) => MediaTrackConstraints | Promise<MediaTrackConstraints>);
22+
}
23+
24+
const defaultConstraints: Required<Constraints> = {
25+
initConstraints: {
26+
video: true,
27+
audio: false,
28+
},
29+
videoConstraints: {},
30+
audioConstraints: {},
31+
};
32+
const _ = clone(defaultConstraints);
33+
export { _ as defaultConstraints };
34+
35+
export async function startCamera(
36+
videoElement: HTMLVideoElement,
37+
{
38+
initConstraints = defaultConstraints.initConstraints,
39+
videoConstraints = defaultConstraints.videoConstraints,
40+
audioConstraints = defaultConstraints.audioConstraints,
41+
}: Constraints = defaultConstraints,
42+
) {
43+
// check if we can use the getUserMedia API at first
44+
if (navigator?.mediaDevices?.getUserMedia === undefined) {
45+
throw new DOMException(
46+
"Cannot get user media." +
47+
"Is this page loaded in a secure context (HTTPS or LOCALHOST)?" +
48+
" Or is getUserMedia supported in the runtime?",
49+
"NotSupportedError",
50+
);
51+
}
52+
53+
// shim WebRTC APIs in the client runtime
54+
await import("webrtc-adapter");
55+
56+
// callback constraints
57+
if (typeof initConstraints === "function") {
58+
initConstraints = await initConstraints(
59+
navigator.mediaDevices.getSupportedConstraints(),
60+
);
61+
}
62+
63+
// apply constraints and get the media stream
64+
const stream = await navigator.mediaDevices.getUserMedia(initConstraints);
65+
66+
// attach the stream to the video element
67+
attachStreamToVideoElement(videoElement, stream);
68+
69+
// in the WeChat browser on iOS,
70+
// 'loadeddata' event won't get fired
71+
// unless the video is explictly triggered by play()
72+
videoElement.play();
73+
74+
// wait for the loadeddata event
75+
// so we can safely use the video element later
76+
await Promise.race([
77+
new Promise<void>((resolve) => {
78+
videoElement.addEventListener(
79+
"loadeddata",
80+
() => {
81+
resolve();
82+
},
83+
{ once: true },
84+
);
85+
}),
86+
new Promise<void>((_, reject) => {
87+
setTimeout(() => {
88+
reject(
89+
new DOMException("The video can not be loaded.", "InvalidStateError"),
90+
);
91+
}, IOS_PWA_WAIT_TIMEOUT);
92+
}),
93+
]);
94+
95+
// get the first video track
96+
const [videoTrack] = stream.getVideoTracks();
97+
98+
// get the first audio track
99+
const [audioTrack] = stream.getAudioTracks();
100+
101+
// apply media track constraints
102+
await Promise.all([
103+
(async () => {
104+
// apply video constraints
105+
if (videoTrack) {
106+
if (typeof videoConstraints === "function") {
107+
videoConstraints = await videoConstraints(
108+
await getCapabilities(videoTrack),
109+
);
110+
}
111+
await videoTrack.applyConstraints(videoConstraints);
112+
}
113+
})(),
114+
(async () => {
115+
// apply audio constraints
116+
if (audioTrack) {
117+
if (typeof audioConstraints === "function") {
118+
audioConstraints = await audioConstraints(
119+
await getCapabilities(audioTrack),
120+
);
121+
}
122+
await audioTrack.applyConstraints(audioConstraints);
123+
}
124+
})(),
125+
]);
126+
127+
// return tracks
128+
return {
129+
videoTrack,
130+
audioTrack,
131+
};
132+
}
133+
134+
function attachStreamToVideoElement(
135+
videoElement: HTMLVideoElement,
136+
stream: MediaStream,
137+
) {
138+
// attach the stream to the video element
139+
if (videoElement.srcObject !== undefined) {
140+
videoElement.srcObject = stream;
141+
} else if (videoElement.mozSrcObject !== undefined) {
142+
videoElement.mozSrcObject = stream;
143+
} else if (window.URL.createObjectURL) {
144+
videoElement.src = (window.URL.createObjectURL as CreateObjectURLCompat)(
145+
stream,
146+
);
147+
} else if (window.webkitURL) {
148+
videoElement.src = (
149+
window.webkitURL.createObjectURL as CreateObjectURLCompat
150+
)(stream);
151+
} else {
152+
videoElement.src = stream.id;
153+
}
154+
}
155+
156+
async function getCapabilities(
157+
track: MediaStreamTrack,
158+
timeout = GET_CAPABILITIES_TIMEOUT,
159+
): Promise<MediaTrackCapabilities> {
160+
return new Promise((resolve) => {
161+
// timeout, return empty capabilities
162+
let timeoutId: number | undefined = setTimeout(() => {
163+
resolve({});
164+
timeoutId = undefined;
165+
return;
166+
}, timeout);
167+
168+
// not supported, return empty capabilities
169+
if (!track.getCapabilities) {
170+
clearTimeout(timeoutId);
171+
resolve({});
172+
timeoutId = undefined;
173+
return;
174+
}
175+
176+
// poll to check capabilities
177+
let capabilities: MediaTrackCapabilities = {};
178+
while (Object.keys(capabilities).length === 0 && timeoutId !== undefined) {
179+
capabilities = track.getCapabilities();
180+
}
181+
clearTimeout(timeoutId);
182+
resolve(capabilities);
183+
timeoutId = undefined;
184+
return;
185+
});
186+
}

src/react/camera/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./useCamera.js";

src/react/camera/shimGetUserMedia.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)