refactor: class-based photo recorder for Moments

This commit is contained in:
Mo
2022-12-07 21:09:23 -06:00
parent d3609853c6
commit 2694c2f1a9
4 changed files with 163 additions and 71 deletions

View File

@@ -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<File | undefined> {
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 }
}

View File

@@ -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
}

View File

@@ -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<File | undefined> {
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)
})
})
}
}

View File

@@ -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<File>()
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<void> {
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<File> {
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)
})
})
}
}