Files
securebit-chat/node_modules/html5-qrcode/src/camera/core-impl.ts
lockbitchat 34094956b7 feat(core): update session, security system and QR exchange
- Removed session creation and Lightning payment logic
- Refactored security system:
  * no more restrictions
  * all systems enabled on session creation
- Improved QR code exchange for mobile devices
2025-09-23 20:01:02 -04:00

341 lines
9.6 KiB
TypeScript

/**
* @fileoverview
* Core camera library implementations.
*
* @author mebjas <minhazav@gmail.com>
*/
import {
Camera,
CameraCapabilities,
CameraCapability,
RangeCameraCapability,
CameraRenderingOptions,
RenderedCamera,
RenderingCallbacks,
BooleanCameraCapability
} from "./core";
/** Interface for a range value. */
interface RangeValue {
min: number;
max: number;
step: number;
}
/** Abstract camera capability class. */
abstract class AbstractCameraCapability<T> implements CameraCapability<T> {
protected readonly name: string;
protected readonly track: MediaStreamTrack;
constructor(name: string, track: MediaStreamTrack) {
this.name = name;
this.track = track;
}
public isSupported(): boolean {
// TODO(minhazav): Figure out fallback for getCapabilities()
// in firefox.
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Constraints
if (!this.track.getCapabilities) {
return false;
}
return this.name in this.track.getCapabilities();
}
public apply(value: T): Promise<void> {
let constraint: any = {};
constraint[this.name] = value;
let constraints = { advanced: [ constraint ] };
return this.track.applyConstraints(constraints);
}
public value(): T | null {
let settings: any = this.track.getSettings();
if (this.name in settings) {
let settingValue = settings[this.name];
return settingValue;
}
return null;
}
}
abstract class AbstractRangeCameraCapability extends AbstractCameraCapability<number> {
constructor(name: string, track: MediaStreamTrack) {
super(name, track);
}
public min(): number {
return this.getCapabilities().min;
}
public max(): number {
return this.getCapabilities().max;
}
public step(): number {
return this.getCapabilities().step;
}
public apply(value: number): Promise<void> {
let constraint: any = {};
constraint[this.name] = value;
let constraints = {advanced: [ constraint ]};
return this.track.applyConstraints(constraints);
}
private getCapabilities(): RangeValue {
this.failIfNotSupported();
let capabilities: any = this.track.getCapabilities();
let capability: any = capabilities[this.name];
return {
min: capability.min,
max: capability.max,
step: capability.step,
};
}
private failIfNotSupported() {
if (!this.isSupported()) {
throw new Error(`${this.name} capability not supported`);
}
}
}
/** Zoom feature. */
class ZoomFeatureImpl extends AbstractRangeCameraCapability {
constructor(track: MediaStreamTrack) {
super("zoom", track);
}
}
/** Torch feature. */
class TorchFeatureImpl extends AbstractCameraCapability<boolean> {
constructor(track: MediaStreamTrack) {
super("torch", track);
}
}
/** Implementation of {@link CameraCapabilities}. */
class CameraCapabilitiesImpl implements CameraCapabilities {
private readonly track: MediaStreamTrack;
constructor(track: MediaStreamTrack) {
this.track = track;
}
zoomFeature(): RangeCameraCapability {
return new ZoomFeatureImpl(this.track);
}
torchFeature(): BooleanCameraCapability {
return new TorchFeatureImpl(this.track);
}
}
/** Implementation of {@link RenderedCamera}. */
class RenderedCameraImpl implements RenderedCamera {
private readonly parentElement: HTMLElement;
private readonly mediaStream: MediaStream;
private readonly surface: HTMLVideoElement;
private readonly callbacks: RenderingCallbacks;
private isClosed: boolean = false;
private constructor(
parentElement: HTMLElement,
mediaStream: MediaStream,
callbacks: RenderingCallbacks) {
this.parentElement = parentElement;
this.mediaStream = mediaStream;
this.callbacks = callbacks;
this.surface = this.createVideoElement(this.parentElement.clientWidth);
// Setup
parentElement.append(this.surface);
}
private createVideoElement(width: number): HTMLVideoElement {
const videoElement = document.createElement("video");
videoElement.style.width = `${width}px`;
videoElement.style.display = "block";
videoElement.muted = true;
videoElement.setAttribute("muted", "true");
(<any>videoElement).playsInline = true;
return videoElement;
}
private setupSurface() {
this.surface.onabort = () => {
throw "RenderedCameraImpl video surface onabort() called";
};
this.surface.onerror = () => {
throw "RenderedCameraImpl video surface onerror() called";
};
let onVideoStart = () => {
const videoWidth = this.surface.clientWidth;
const videoHeight = this.surface.clientHeight;
this.callbacks.onRenderSurfaceReady(videoWidth, videoHeight);
this.surface.removeEventListener("playing", onVideoStart);
};
this.surface.addEventListener("playing", onVideoStart);
this.surface.srcObject = this.mediaStream;
this.surface.play();
}
static async create(
parentElement: HTMLElement,
mediaStream: MediaStream,
options: CameraRenderingOptions,
callbacks: RenderingCallbacks)
: Promise<RenderedCamera> {
let renderedCamera = new RenderedCameraImpl(
parentElement, mediaStream, callbacks);
if (options.aspectRatio) {
let aspectRatioConstraint = {
aspectRatio: options.aspectRatio!
};
await renderedCamera.getFirstTrackOrFail().applyConstraints(
aspectRatioConstraint);
}
renderedCamera.setupSurface();
return renderedCamera;
}
private failIfClosed() {
if (this.isClosed) {
throw "The RenderedCamera has already been closed.";
}
}
private getFirstTrackOrFail(): MediaStreamTrack {
this.failIfClosed();
if (this.mediaStream.getVideoTracks().length === 0) {
throw "No video tracks found";
}
return this.mediaStream.getVideoTracks()[0];
}
//#region Public APIs.
public pause(): void {
this.failIfClosed();
this.surface.pause();
}
public resume(onResumeCallback: () => void): void {
this.failIfClosed();
let $this = this;
const onVideoResume = () => {
// Transition after 200ms to avoid the previous canvas frame being
// re-scanned.
setTimeout(onResumeCallback, 200);
$this.surface.removeEventListener("playing", onVideoResume);
};
this.surface.addEventListener("playing", onVideoResume);
this.surface.play();
}
public isPaused(): boolean {
this.failIfClosed();
return this.surface.paused;
}
public getSurface(): HTMLVideoElement {
this.failIfClosed();
return this.surface;
}
public getRunningTrackCapabilities(): MediaTrackCapabilities {
return this.getFirstTrackOrFail().getCapabilities();
}
public getRunningTrackSettings(): MediaTrackSettings {
return this.getFirstTrackOrFail().getSettings();
}
public async applyVideoConstraints(constraints: MediaTrackConstraints)
: Promise<void> {
if ("aspectRatio" in constraints) {
throw "Changing 'aspectRatio' in run-time is not yet supported.";
}
return this.getFirstTrackOrFail().applyConstraints(constraints);
}
public close(): Promise<void> {
if (this.isClosed) {
// Already closed.
return Promise.resolve();
}
let $this = this;
return new Promise((resolve, _) => {
let tracks = $this.mediaStream.getVideoTracks();
const tracksToClose = tracks.length;
var tracksClosed = 0;
$this.mediaStream.getVideoTracks().forEach((videoTrack) => {
$this.mediaStream.removeTrack(videoTrack);
videoTrack.stop();
++tracksClosed;
if (tracksClosed >= tracksToClose) {
$this.isClosed = true;
$this.parentElement.removeChild($this.surface);
resolve();
}
});
});
}
getCapabilities(): CameraCapabilities {
return new CameraCapabilitiesImpl(this.getFirstTrackOrFail());
}
//#endregion
}
/** Default implementation of {@link Camera} interface. */
export class CameraImpl implements Camera {
private readonly mediaStream: MediaStream;
private constructor(mediaStream: MediaStream) {
this.mediaStream = mediaStream;
}
async render(
parentElement: HTMLElement,
options: CameraRenderingOptions,
callbacks: RenderingCallbacks)
: Promise<RenderedCamera> {
return RenderedCameraImpl.create(
parentElement, this.mediaStream, options, callbacks);
}
static async create(videoConstraints: MediaTrackConstraints)
: Promise<Camera> {
if (!navigator.mediaDevices) {
throw "navigator.mediaDevices not supported";
}
let constraints: MediaStreamConstraints = {
audio: false,
video: videoConstraints
};
let mediaStream = await navigator.mediaDevices.getUserMedia(
constraints);
return new CameraImpl(mediaStream);
}
}