tethr
Version:
Controlls USB-connected cameras, webcam, and smartphone camera from browser
191 lines • 6.33 kB
JavaScript
import { BiMap } from 'bim';
import { Tethr } from './Tethr';
import { readonlyConfigDesc, UnsupportedConfigDesc } from './util';
export class TethrWebcam extends Tethr {
#media = null;
#captureHandler = null;
#facingModeDict = new BiMap();
constructor() {
super();
}
async open() {
try {
this.#media = await navigator.mediaDevices.getUserMedia({
video: true,
});
}
catch {
throw new Error('No available webcam is connected');
}
// Setup CaptureHandler
if ('ImageCapture' in globalThis) {
const videoTrack = this.#media.getVideoTracks()[0];
this.#captureHandler = {
type: 'imageCapture',
imageCapture: new ImageCapture(videoTrack),
};
}
else {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const video = document.createElement('video');
video.autoplay = true;
video.muted = true;
video.style.display = 'none';
video.playsInline = true;
document.body.appendChild(video);
video.srcObject = this.#media;
if (context) {
this.#captureHandler = {
type: 'canvas',
canvas,
context,
video,
};
}
}
// Retrieve the camera's available facingModes
const devices = await navigator.mediaDevices.enumerateDevices();
const facingModeEntries = devices
.filter(d => d.kind === 'videoinput')
.map(d => [d.deviceId, d.label]);
this.#facingModeDict = new BiMap(facingModeEntries);
}
async close() {
this.#media?.getTracks().forEach(t => t.stop());
this.#media = null;
this.emit('liveviewChange', readonlyConfigDesc(null));
}
get opened() {
return !!this.#media;
}
get type() {
return 'webcam';
}
get name() {
return 'Webcam';
}
// Configs
async getCanStartLiveviewDesc() {
return readonlyConfigDesc(true);
}
async getCanTakePhotoDesc() {
return readonlyConfigDesc(this.#captureHandler !== null);
}
async setFacingMode(value) {
if (!this.#media || !this.#captureHandler) {
return { status: 'unsupported' };
}
const desc = await this.getFacingModeDesc();
if (desc.value === null) {
return { status: 'unsupported' };
}
const deviceId = this.#facingModeDict.getKey(value);
if (!deviceId) {
return { status: 'invalid parameter' };
}
// Stop all tracks at first
this.#media.getTracks().forEach(t => t.stop());
// Then get a new media stream and notify the change
this.#media = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } },
});
this.emit('liveviewChange', readonlyConfigDesc(this.#media));
// Setup other variables
const videoTrack = this.#media.getVideoTracks()[0];
if (this.#captureHandler.type === 'imageCapture') {
this.#captureHandler.imageCapture = new ImageCapture(videoTrack);
}
else {
const { video } = this.#captureHandler;
video.srcObject = this.#media;
}
return { status: 'ok' };
}
async getFacingModeDesc() {
if (!this.#media) {
return UnsupportedConfigDesc;
}
const videoTrack = this.#media.getVideoTracks()[0];
const currentId = videoTrack.getSettings().deviceId ?? '';
const value = this.#facingModeDict.get(currentId) ?? null;
return {
writable: this.#facingModeDict.size > 0,
value,
option: {
type: 'enum',
values: [...this.#facingModeDict.values()],
},
};
}
async getModelDesc() {
return readonlyConfigDesc('Webcam');
}
async getLiveviewDesc() {
return this.#media ? readonlyConfigDesc(this.#media) : UnsupportedConfigDesc;
}
// Actions
async takePhoto({ doDownload = true } = {}) {
if (!this.#media || !this.#captureHandler) {
return { status: 'unsupported' };
}
if (!doDownload)
return { status: 'ok', value: [] };
let blob;
if (this.#captureHandler.type === 'imageCapture') {
blob = await this.#captureHandler.imageCapture.takePhoto();
}
else {
const videoTrack = this.#media.getVideoTracks()[0];
const { width, height } = {
width: 640,
height: 480,
...videoTrack.getSettings(),
};
const { canvas, context, video } = this.#captureHandler;
canvas.width = width;
canvas.height = height;
video.play();
context.drawImage(video, 0, 0, width, height);
const blobOrNull = await new Promise(resolve => {
canvas.toBlob(resolve);
});
if (!blobOrNull)
return { status: 'general error' };
blob = blobOrNull;
}
const now = new Date();
const tethrObject = {
id: 0,
storageID: 0,
format: 'jpeg',
byteLength: blob.size,
// protectionStatus: 0,
image: {
width: 0,
height: 0,
bitDepth: 8,
},
filename: 'capture.jpeg',
sequenceNumber: 0,
captureDate: now,
modificationDate: now,
blob,
};
return { status: 'ok', value: [tethrObject] };
}
async startLiveview() {
if (!this.#media) {
return { status: 'general error' };
}
this.emit('liveviewChange', readonlyConfigDesc(this.#media));
return {
status: 'ok',
value: this.#media,
};
}
async stopLiveview() {
return { status: 'ok' };
}
}
//# sourceMappingURL=TethrWebcam.js.map