diff --git a/packages/web/src/javascripts/Controllers/Moments/CameraUtils.ts b/packages/web/src/javascripts/Controllers/Moments/CameraUtils.ts deleted file mode 100644 index 45eeacecb..000000000 --- a/packages/web/src/javascripts/Controllers/Moments/CameraUtils.ts +++ /dev/null @@ -1,65 +0,0 @@ -export async function awaitVideoReady(video: HTMLVideoElement) { - return new Promise((resolve) => { - video.addEventListener('canplaythrough', () => { - resolve(null) - }) - }) -} - -export function stopCameraStream(canvas: HTMLCanvasElement, video: HTMLVideoElement, stream: MediaStream) { - video.pause() - video.parentElement?.removeChild(video) - canvas.parentElement?.removeChild(canvas) - video.remove() - canvas.remove() - - stream.getTracks().forEach((track) => { - track.stop() - }) -} - -export async function takePhoto( - filename: string, - canvas: HTMLCanvasElement, - video: HTMLVideoElement, - width: number, - height: number, -): Promise { - const context = canvas.getContext('2d') - context?.drawImage(video, 0, 0, width, height) - const dataUrl = canvas.toDataURL('image/png') - - const isFailedImage = dataUrl.length < 100000 - - if (isFailedImage) { - return undefined - } - - const res: Response = await fetch(dataUrl) - const blob: Blob = await res.blob() - const file = new File([blob], filename, { type: 'image/png' }) - return file -} - -export async function preparePhotoOperation() { - const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) - const video = document.createElement('video') - video.playsInline = true - video.style.position = 'absolute' - video.style.display = 'none' - const canvas = document.createElement('canvas') - document.body.append(video) - video.srcObject = stream - await video.play() - await awaitVideoReady(video) - - const videoTrack = stream.getVideoTracks()[0] - const settings = videoTrack.getSettings() - const width = settings.width ?? 1280 - const height = settings.height ?? 720 - - canvas.width = width - canvas.height = height - - return { canvas, video, stream, width, height } -} diff --git a/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts b/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts index 51df0c4f2..908132075 100644 --- a/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts +++ b/packages/web/src/javascripts/Controllers/Moments/MomentsService.ts @@ -3,11 +3,11 @@ import { ApplicationEvent, InternalEventBus, StorageKey } from '@standardnotes/s import { isDev } from '@/Utils' import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs' import { FilesController } from '../FilesController' -import { preparePhotoOperation, takePhoto, stopCameraStream } from './CameraUtils' import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController' import { WebApplication } from '@/Application/Application' import { dateToStringStyle1 } from '@/Utils/DateUtils' +import { PhotoRecorder } from './PhotoRecorder' const EVERY_HALF_HOUR = 1000 * 60 * 30 const EVERY_TEN_SECONDS = 1000 * 10 @@ -100,18 +100,18 @@ export class MomentsService extends AbstractViewController { } } - const { canvas, video, stream, width, height } = await preparePhotoOperation() - const filename = `Moment ${dateToStringStyle1(new Date())}.png` + const camera = new PhotoRecorder(filename) + await camera.initialize() if (this.application.isMobileDevice) { await sleep(DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS) } - let file = await takePhoto(filename, canvas, video, width, height) + let file = await camera.takePhoto() if (!file) { await sleep(1000) - file = await takePhoto(filename, canvas, video, width, height) + file = await camera.takePhoto() if (!file) { return undefined } @@ -130,7 +130,7 @@ export class MomentsService extends AbstractViewController { } } - stopCameraStream(canvas, video, stream) + camera.finish() return uploadedFile } diff --git a/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts b/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts new file mode 100644 index 000000000..4fcf6d808 --- /dev/null +++ b/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts @@ -0,0 +1,74 @@ +export class PhotoRecorder { + public video!: HTMLVideoElement + + private canvas!: HTMLCanvasElement + private width!: number + private height!: number + private stream!: MediaStream + + constructor(private fileName: string) {} + + public async initialize() { + this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false }) + + this.video = document.createElement('video') + this.video.playsInline = true + this.video.style.position = 'absolute' + this.video.style.display = 'none' + + this.canvas = document.createElement('canvas') + + document.body.append(this.video) + this.video.srcObject = this.stream + + await this.video.play() + await this.awaitVideoReady(this.video) + + const videoTrack = this.stream.getVideoTracks()[0] + const settings = videoTrack.getSettings() + this.width = settings.width ?? 1280 + this.height = settings.height ?? 720 + + this.canvas.width = this.width + this.canvas.height = this.height + } + + public async takePhoto(): Promise { + const context = this.canvas.getContext('2d') + context?.drawImage(this.video, 0, 0, this.width, this.height) + const dataUrl = this.canvas.toDataURL('image/png') + + const isFailedImage = dataUrl.length < 100000 + + if (isFailedImage) { + return undefined + } + + const res: Response = await fetch(dataUrl) + const blob: Blob = await res.blob() + const file = new File([blob], this.fileName, { type: 'image/png' }) + return file + } + + public finish() { + this.video.pause() + + this.video.parentElement?.removeChild(this.video) + this.canvas.parentElement?.removeChild(this.canvas) + + this.video.remove() + this.canvas.remove() + + this.stream.getTracks().forEach((track) => { + track.stop() + }) + } + + private async awaitVideoReady(video: HTMLVideoElement) { + return new Promise((resolve) => { + video.addEventListener('canplaythrough', () => { + resolve(null) + }) + }) + } +} diff --git a/packages/web/src/javascripts/Controllers/Moments/VideoRecorder.ts b/packages/web/src/javascripts/Controllers/Moments/VideoRecorder.ts new file mode 100644 index 000000000..bb557fc76 --- /dev/null +++ b/packages/web/src/javascripts/Controllers/Moments/VideoRecorder.ts @@ -0,0 +1,83 @@ +import { Deferred } from '@standardnotes/snjs' + +export class VideoRecorder { + public video!: HTMLVideoElement + + private canvas!: HTMLCanvasElement + private width!: number + private height!: number + private stream!: MediaStream + private recorder!: MediaRecorder + + private dataReadyPromise = Deferred() + + constructor(private fileName: string) {} + + public async initialize() { + this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) + this.recorder = new MediaRecorder(this.stream) + + this.video = document.createElement('video') + this.video.playsInline = true + this.video.style.position = 'absolute' + this.video.style.display = 'none' + this.video.volume = 0 + + this.canvas = document.createElement('canvas') + + document.body.append(this.video) + this.video.srcObject = this.stream + + await this.video.play() + await this.awaitVideoReady(this.video) + + const videoTrack = this.stream.getVideoTracks()[0] + const settings = videoTrack.getSettings() + this.width = settings.width ?? 1280 + this.height = settings.height ?? 720 + + this.canvas.width = this.width + this.canvas.height = this.height + } + + public async startRecording(): Promise { + this.recorder.start() + + this.recorder.ondataavailable = this.onData + } + + private onData = async (event: BlobEvent) => { + const blob = new Blob([event.data], { type: 'video/mp4' }) + const url = URL.createObjectURL(blob) + const res: Response = await fetch(url) + const responseBlob: Blob = await res.blob() + const file = new File([responseBlob], this.fileName, { type: 'video/mp4' }) + + this.dataReadyPromise.resolve(file) + } + + public async stop(): Promise { + this.video.pause() + this.recorder.stop() + + this.video.parentElement?.removeChild(this.video) + this.canvas.parentElement?.removeChild(this.canvas) + + this.video.remove() + this.canvas.remove() + + this.stream.getTracks().forEach((track) => { + track.stop() + }) + + return this.dataReadyPromise.promise + } + + private async awaitVideoReady(video: HTMLVideoElement) { + return new Promise((resolve) => { + video.addEventListener('canplaythrough', () => { + resolve(null) + }) + }) + } +}