/* * Copyright 2021 Google LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ import { h, Component, createRef } from 'preact'; import { rethrowIfCritical } from 'web-gphoto2'; export const isDebug = new URLSearchParams(location.search).has('debug'); const Stats = isDebug ? await import('stats.js').then( res => /** @type {typeof import('stats.js')} */ (res['default']) ) : null; /** @param {number} ms */ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); /** * How long to let the camera settle before asking for live view again after a * pause. A still capture drops the EOS out of live view entirely, and asking * too soon just earns a "device busy". */ const RESUME_DELAY_MS = 500; /** * @extends Component<{ * getPreview?: () => Promise, * paused?: boolean, * pausedMessage?: string * }, { restoring?: boolean }> */ export class Preview extends Component { canvasHolderRef = createRef(); canvasRef = createRef(); /** @type {ResizeObserver} */ resizeObserver; stats = isDebug ? new Stats() : null; state = { restoring: false }; render( /** @type {Preview['props']} */ props, /** @type {Preview['state']} */ state ) { let overlay = props.paused ? props.pausedMessage || '⏸ Live view paused' : state.restoring ? '⌛ Restoring live view…' : undefined; return h( 'div', { class: 'center-parent', ref: this.canvasHolderRef }, !props.getPreview ? h('div', { class: 'center' }, `Preview is unsupported`) : h('canvas', { class: 'center', ref: this.canvasRef }), overlay ? h('div', { class: 'preview-overlay' }, overlay) : undefined ); } async componentDidMount() { if (!this.props.getPreview) return; let canvas = /** @type {HTMLCanvasElement} */ (this.canvasRef.current); let canvasHolder = this.canvasHolderRef.current; if (isDebug) { canvasHolder.appendChild(this.stats.dom); } let canvasCtx = canvas.getContext('bitmaprenderer'); let ratio = 0; let throttled = 0; function updateCanvasSize() { if (throttled) { cancelAnimationFrame(throttled); } throttled = requestAnimationFrame(() => { throttled = 0; let width = canvasHolder.offsetWidth - 10; let height = canvasHolder.offsetHeight; if (height * ratio > width) { height = width / ratio; } else { width = height * ratio; } Object.assign(canvas, { width, height }); }); } (this.resizeObserver = new ResizeObserver(updateCanvasSize)).observe( canvasHolder ); // I have no idea why, but if we connect too soon, it just hangs... await new Promise(resolve => setTimeout(resolve, 1500)); let failures = 0; let resuming = false; while (this.canvasRef.current) { // Paused while the shutter actually fires - live view and capture share // one USB link, and the camera drops out of live view to take the shot. if (this.props.paused) { resuming = true; await sleep(200); continue; } if (resuming) { resuming = false; await sleep(RESUME_DELAY_MS); } try { let blob = await this.props.getPreview(); // If ratio is known; decode resized image right away - it's a bit faster. // If it isn't known, retrieve entire image to calculate ratio from its dimensions. let img = await createImageBitmap( blob, ratio ? { resizeWidth: canvas.width, resizeHeight: canvas.height } : {} ); if (!ratio) { ratio = img.width / img.height; updateCanvasSize(); } await new Promise(resolve => requestAnimationFrame(resolve)); canvasCtx.transferFromImageBitmap(img); if (failures) { failures = 0; this.setState({ restoring: false }); } } catch (err) { rethrowIfCritical(err); // Right after a capture the camera reports busy for a beat while the // driver spins live view back up, so back off instead of hammering it - // retrying flat out here is what keeps the feed down. if (!failures) console.warn('Could not refresh preview:', err); failures++; if (failures === 3) this.setState({ restoring: true }); await sleep(Math.min(1500, 150 * failures)); } this.stats?.update(); } } componentWillUnmount() { this.resizeObserver?.disconnect(); } }