Skip to content

Commit

Permalink
Canvas, useRequestAnimationFrame, useUserMedia, Video
Browse files Browse the repository at this point in the history
  • Loading branch information
yoiang committed Mar 10, 2022
1 parent a7cf9cf commit 24d46ce
Show file tree
Hide file tree
Showing 15 changed files with 694 additions and 3 deletions.
67 changes: 67 additions & 0 deletions src/components/Canvas/Canvas.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react'
import { useCallback, useRef } from 'react'

import { Canvas, Methods as CanvasMethods } from './Canvas'

const displayConfig = {
title: 'Canvas'
}
export default displayConfig

interface Props {
width: number
height: number

flip: boolean
}

export const Default = ({ width, height, flip }: Props) => {
const canvas = useRef<CanvasMethods | null>()

const draw = useCallback(() => {
if (!canvas || !canvas.current) {
return
}
const context = canvas.current.getContext()
if (!context) {
return
}
let gradient = context.createLinearGradient(
0,
0,
canvas.current.width(),
canvas.current.height()
)
gradient.addColorStop(0, 'green')
gradient.addColorStop(0.5, 'cyan')
gradient.addColorStop(1, 'red')

context.fillStyle = gradient
context.fillRect(0, 0, canvas.current.width(), canvas.current.height())
}, [])

// const drawIntoVideo = useCallback(() => {
// }, [])

return (
<div>
<Canvas
ref={(ref) => (canvas.current = ref)}
width={width}
height={height}
flip={flip}
/>
<br />
Width: {canvas.current?.width()} Height: {canvas.current?.height()}
<br />
<button onClick={draw}>Draw</button>
<button onClick={() => canvas.current?.clearContext()}>Clear</button>
</div>
)
}
Default.args = {
width: 640,
height: 480,

flip: false
}
126 changes: 126 additions & 0 deletions src/components/Canvas/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as React from 'react'

import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'

export interface Methods {
drawContext: (video: HTMLVideoElement) => void
clearContext: () => void
getContext: () => CanvasRenderingContext2D | null | undefined

width: () => number
height: () => number
}

interface Props {
width: number
height: number

flip?: boolean

style?: React.CSSProperties
className?: string
}

export const Canvas = forwardRef<Methods, Props>(
({ width, height, flip = false, style, className }: Props, forwardedRef) => {
const ref = useRef<HTMLCanvasElement | null>()
const contextRef = useRef<CanvasRenderingContext2D | null>()

useEffect(() => {
if (!ref.current) {
return
}
ref.current.width = width
}, [ref, width])

useEffect(() => {
if (!ref.current) {
return
}
ref.current.height = height
}, [ref, height])

useEffect(() => {
if (!contextRef.current) {
return
}
if (flip) {
contextRef.current.translate(width, 0) //result.video.videoWidth, 0)
contextRef.current.scale(-1, 1)
}
return () => {
if (!contextRef.current) {
return
}
if (flip) {
contextRef.current.scale(1, 1)
contextRef.current.translate(-width, 0) //result.video.videoWidth, 0)
}
}
}, [ref, width, flip])

useEffect(() => {
if (!ref.current) {
return
}
// TODO: support configurable context
contextRef.current = ref.current.getContext('2d')

return () => {
contextRef.current = null
}
}, [ref])

useImperativeHandle(forwardedRef, () => ({
drawContext: (video: HTMLVideoElement) => {
if (!contextRef.current) {
return
}
contextRef.current.drawImage(
video,
0,
0,
video.videoWidth,
video.videoHeight
)
},
clearContext: () => {
if (!ref.current || !contextRef.current) {
return
}
contextRef.current.clearRect(
0,
0,
ref.current.width,
ref.current.height
)
},
getContext: () => {
return contextRef.current
},
width: () => {
if (!ref.current) {
return 0
}
return ref.current.width
},
height: () => {
if (!ref.current) {
return 0
}
return ref.current.height
}
}))

return (
<canvas
ref={(newRef) => {
ref.current = newRef
}}
style={style}
className={className}
/>
)
}
)
Canvas.displayName = 'Canvas'
1 change: 1 addition & 0 deletions src/components/Canvas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Canvas'
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Meta } from '@storybook/react/types-6-0'
import { useLocalStorageItem } from './useLocalStorageItem'

export default {
title: 'LocalStorageItem/useLocalStorageItem'
title: 'useLocalStorageItem'
} as Meta

interface Props {
Expand Down
1 change: 1 addition & 0 deletions src/components/RequestAnimationFrame/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useRequestAnimationFrame'
43 changes: 43 additions & 0 deletions src/components/RequestAnimationFrame/useRequestAnimationFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback, useEffect } from 'react'

const InvalidRequestId = -1

// TODO: properly scope
let requestId: number = InvalidRequestId
export const useRequestAnimationFrame = (perform: (time: number) => void) => {
// const [requestId, setRequestId] = useState<number>(InvalidRequestId)

const animateFrame = useCallback(
(time: number) => {
if (requestId === InvalidRequestId) {
return
}

perform(time)

if (requestAnimationFrame) {
requestId = requestAnimationFrame(animateFrame)
} else {
throw new Error('requestAnimationFrame is not supported')
}
},
[perform]
)

useEffect(() => {
if (requestAnimationFrame) {
requestId = requestAnimationFrame(animateFrame)
} else {
throw new Error('requestAnimationFrame is not supported')
}

return () => {
if (requestId !== InvalidRequestId) {
cancelAnimationFrame(requestId)
requestId = InvalidRequestId
}
}
}, [animateFrame])

return {}
}
5 changes: 5 additions & 0 deletions src/components/UserMedia/Utility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const stopMediaStream = (stream: MediaStream) => {
stream.getTracks().forEach((track) => {
track.stop()
})
}
1 change: 1 addition & 0 deletions src/components/UserMedia/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useUserMedia'
55 changes: 55 additions & 0 deletions src/components/UserMedia/useUserMedia.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react'

import { useUserMedia } from './useUserMedia'

const displayConfig = {
title: 'useUserMedia'
}
export default displayConfig

interface Props {
audio: boolean
facingMode: string
width: number
height: number
idealFrameRate: number
}

export const Default = ({
audio,
facingMode,
width,
height,
idealFrameRate
}: Props) => {
const { stream } = useUserMedia({
constraints: {
audio,
video: {
facingMode,
width,
height,
frameRate: {
ideal: idealFrameRate
}
}
}
})

return (
<div>
Id: {stream?.id}
<br />
Active: {stream?.active}
<br />
Video Track Count: {stream?.getVideoTracks()?.length}
</div>
)
}
Default.args = {
audio: false,
facingMode: 'user',
width: 360,
height: 270,
idealFrameRate: 60
}
49 changes: 49 additions & 0 deletions src/components/UserMedia/useUserMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useEffect, useRef, useState } from 'react'
import { stopMediaStream } from './Utility'

export interface Props {
constraints?: MediaStreamConstraints | undefined
}

export const useUserMedia = ({ constraints }: Props) => {
const stream = useRef<MediaStream>()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isStreamSet, setIsStreamSet] = useState<boolean>(false) // Hack to force rerender with new reference returned
const [error, setError] = useState<Error>()

console.log(stream)
useEffect(() => {
let cancelEnable = false

navigator.mediaDevices
.getUserMedia(constraints)
.then((result) => {
if (!cancelEnable) {
stream.current = result
setIsStreamSet(true)
}
})
.catch((reason) => {
if (!cancelEnable) {
setError(
new Error(`Error enabling video stream: ${JSON.stringify(reason)}}`)
)
}
})

return () => {
cancelEnable = true
}
}, [constraints, setIsStreamSet, setError])

useEffect(() => {
return () => {
if (stream.current) {
console.log('stopping stream', stream.current)
stopMediaStream(stream.current)
}
}
}, [stream])

return { stream: stream.current, error }
}
Loading

0 comments on commit 24d46ce

Please sign in to comment.