fix camera startup fallbacks
这个提交包含在:
@@ -9,6 +9,13 @@ export type CameraZoomState = {
|
||||
focusMode: string;
|
||||
};
|
||||
|
||||
export type CameraRequestResult = {
|
||||
stream: MediaStream;
|
||||
appliedFacingMode: "user" | "environment";
|
||||
audioEnabled: boolean;
|
||||
usedFallback: boolean;
|
||||
};
|
||||
|
||||
type NumericRange = {
|
||||
min: number;
|
||||
max: number;
|
||||
@@ -66,6 +73,98 @@ export function getCameraVideoConstraints(
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeVideoConstraintCandidate(candidate: MediaTrackConstraints | true) {
|
||||
if (candidate === true) {
|
||||
return { label: "camera-any", video: true as const };
|
||||
}
|
||||
|
||||
return {
|
||||
label: JSON.stringify(candidate),
|
||||
video: candidate,
|
||||
};
|
||||
}
|
||||
|
||||
function createFallbackVideoCandidates(
|
||||
facingMode: "user" | "environment",
|
||||
isMobile: boolean,
|
||||
preset: CameraQualityPreset,
|
||||
) {
|
||||
const base = getCameraVideoConstraints(facingMode, isMobile, preset);
|
||||
const alternateFacing = facingMode === "environment" ? "user" : "environment";
|
||||
const lowRes = {
|
||||
facingMode,
|
||||
width: { ideal: isMobile ? 640 : 960 },
|
||||
height: { ideal: isMobile ? 360 : 540 },
|
||||
} satisfies MediaTrackConstraints;
|
||||
const lowResAlternate = {
|
||||
facingMode: alternateFacing,
|
||||
width: { ideal: isMobile ? 640 : 960 },
|
||||
height: { ideal: isMobile ? 360 : 540 },
|
||||
} satisfies MediaTrackConstraints;
|
||||
const anyCamera = {
|
||||
width: { ideal: isMobile ? 640 : 960 },
|
||||
height: { ideal: isMobile ? 360 : 540 },
|
||||
} satisfies MediaTrackConstraints;
|
||||
|
||||
const candidates = [
|
||||
normalizeVideoConstraintCandidate(base),
|
||||
normalizeVideoConstraintCandidate({
|
||||
...base,
|
||||
frameRate: undefined,
|
||||
}),
|
||||
normalizeVideoConstraintCandidate(lowRes),
|
||||
normalizeVideoConstraintCandidate(lowResAlternate),
|
||||
normalizeVideoConstraintCandidate(anyCamera),
|
||||
normalizeVideoConstraintCandidate(true),
|
||||
];
|
||||
|
||||
const deduped = new Map<string, { video: MediaTrackConstraints | true }>();
|
||||
candidates.forEach((candidate) => {
|
||||
if (!deduped.has(candidate.label)) {
|
||||
deduped.set(candidate.label, { video: candidate.video });
|
||||
}
|
||||
});
|
||||
return Array.from(deduped.values());
|
||||
}
|
||||
|
||||
export async function requestCameraStream(options: {
|
||||
facingMode: "user" | "environment";
|
||||
isMobile: boolean;
|
||||
preset: CameraQualityPreset;
|
||||
audio?: false | MediaTrackConstraints;
|
||||
}) {
|
||||
const videoCandidates = createFallbackVideoCandidates(options.facingMode, options.isMobile, options.preset);
|
||||
const audioCandidates = options.audio ? [options.audio, false] : [false];
|
||||
let lastError: unknown = null;
|
||||
|
||||
for (const audio of audioCandidates) {
|
||||
for (let index = 0; index < videoCandidates.length; index += 1) {
|
||||
const video = videoCandidates[index]?.video ?? true;
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video, audio });
|
||||
const videoTrack = stream.getVideoTracks()[0] || null;
|
||||
const settings = (
|
||||
videoTrack && typeof (videoTrack as MediaStreamTrack & { getSettings?: () => unknown }).getSettings === "function"
|
||||
? (videoTrack as MediaStreamTrack & { getSettings: () => unknown }).getSettings()
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const appliedFacingMode = settings.facingMode === "user" ? "user" : settings.facingMode === "environment" ? "environment" : options.facingMode;
|
||||
|
||||
return {
|
||||
stream,
|
||||
appliedFacingMode,
|
||||
audioEnabled: stream.getAudioTracks().length > 0,
|
||||
usedFallback: index > 0 || audio === false && Boolean(options.audio),
|
||||
} satisfies CameraRequestResult;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error("无法访问摄像头");
|
||||
}
|
||||
|
||||
export function getLiveAnalysisBitrate(preset: CameraQualityPreset, isMobile: boolean) {
|
||||
switch (preset) {
|
||||
case "economy":
|
||||
|
||||
在新工单中引用
屏蔽一个用户