refactor: class-based photo recorder for Moments
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user