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 { isDev } from '@/Utils'
|
||||||
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
import { FileItem, PrefKey, sleep, SNTag } from '@standardnotes/snjs'
|
||||||
import { FilesController } from '../FilesController'
|
import { FilesController } from '../FilesController'
|
||||||
import { preparePhotoOperation, takePhoto, stopCameraStream } from './CameraUtils'
|
|
||||||
import { action, makeObservable, observable } from 'mobx'
|
import { action, makeObservable, observable } from 'mobx'
|
||||||
import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController'
|
import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
||||||
|
import { PhotoRecorder } from './PhotoRecorder'
|
||||||
|
|
||||||
const EVERY_HALF_HOUR = 1000 * 60 * 30
|
const EVERY_HALF_HOUR = 1000 * 60 * 30
|
||||||
const EVERY_TEN_SECONDS = 1000 * 10
|
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 filename = `Moment ${dateToStringStyle1(new Date())}.png`
|
||||||
|
const camera = new PhotoRecorder(filename)
|
||||||
|
await camera.initialize()
|
||||||
|
|
||||||
if (this.application.isMobileDevice) {
|
if (this.application.isMobileDevice) {
|
||||||
await sleep(DELAY_AFTER_STARTING_CAMERA_TO_ALLOW_MOBILE_AUTOFOCUS)
|
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) {
|
if (!file) {
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
file = await takePhoto(filename, canvas, video, width, height)
|
file = await camera.takePhoto()
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ export class MomentsService extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopCameraStream(canvas, video, stream)
|
camera.finish()
|
||||||
|
|
||||||
return uploadedFile
|
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