Skip to content

Commit 56b7182

Browse files
authored
feat(VideoTexture): onVideoFrame (#2238)
1 parent 0534eb8 commit 56b7182

File tree

2 files changed

+88
-15
lines changed

2 files changed

+88
-15
lines changed

docs/loaders/video-texture-use-video-texture.mdx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ export function useVideoTexture(
2424
{
2525
unsuspend = 'loadedmetadata',
2626
start = true,
27-
hls: hlsConfig = {},
27+
hls = {},
2828
crossOrigin = 'anonymous',
2929
muted = true,
3030
loop = true,
3131
playsInline = true,
32+
onVideoFrame,
3233
...videoProps
3334
}: {
3435
unsuspend?: keyof HTMLVideoElementEventMap
3536
start?: boolean
3637
hls?: Parameters<typeof getHls>[0]
38+
onVideoFrame: VideoFrameRequestCallback
3739
} & Partial<Omit<HTMLVideoElement, 'children' | 'src' | 'srcObject'>> = {}
3840
)
3941
```
@@ -84,3 +86,36 @@ const texture = useVideoTexture('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3
8486
hls: { abrEwmaFastLive: 1.0, abrEwmaSlowLive: 3.0, enableWorker: true },
8587
})
8688
```
89+
90+
## `requestVideoFrameCallback` (rVFC)
91+
92+
`useVideoTexture` supports [`requestVideoFrameCallback`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestVideoFrameCallback):
93+
94+
```jsx
95+
useVideoTexture(src, {
96+
onVideoFrame: (now, metadata) => {}
97+
})
98+
```
99+
100+
## `<VideoTexture>` Component
101+
102+
```tsx
103+
export type VideoTextureProps = {
104+
children?: (texture: THREE.VideoTexture) => React.ReactNode
105+
src: UseVideoTextureParams[0]
106+
} & UseVideoTextureParams[1]
107+
```
108+
109+
You can access the texture via children's render prop:
110+
111+
```jsx
112+
<VideoTexture src="/video.mp4">
113+
{(texture) => <meshBasicMaterial map={texture} />}
114+
```
115+
116+
or exposed via `ref`:
117+
118+
```jsx
119+
const textureRef = useRef()
120+
<VideoTexture ref={textureRef} src="/video.mp4" />
121+
```

src/core/VideoTexture.tsx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
/* eslint react-hooks/exhaustive-deps: 1 */
12
import * as React from 'react'
23
import * as THREE from 'three'
3-
import { useEffect, useRef } from 'react'
4+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
45
import { useThree } from '@react-three/fiber'
56
import { suspend } from 'suspend-react'
67
import { type default as Hls, Events } from 'hls.js'
@@ -31,11 +32,22 @@ export function useVideoTexture(
3132
muted = true,
3233
loop = true,
3334
playsInline = true,
35+
onVideoFrame,
3436
...videoProps
3537
}: {
38+
/** Event name that will unsuspend the video */
3639
unsuspend?: keyof HTMLVideoElementEventMap
40+
/** Auto start the video once unsuspended */
3741
start?: boolean
42+
/** HLS config */
3843
hls?: Parameters<typeof getHls>[0]
44+
/**
45+
* request Video Frame Callback (rVFC)
46+
*
47+
* @see https://web.dev/requestvideoframecallback-rvfc/
48+
* @see https://www.remotion.dev/docs/video-manipulation
49+
* */
50+
onVideoFrame?: VideoFrameRequestCallback
3951
} & Partial<Omit<HTMLVideoElement, 'children' | 'src' | 'srcObject'>> = {}
4052
) {
4153
const gl = useThree((state) => state.gl)
@@ -81,6 +93,9 @@ export function useVideoTexture(
8193
[srcOrSrcObject]
8294
)
8395

96+
const video = texture.source.data as HTMLVideoElement
97+
useVideoFrame(video, onVideoFrame)
98+
8499
useEffect(() => {
85100
start && texture.image.play()
86101

@@ -96,22 +111,45 @@ export function useVideoTexture(
96111
}
97112

98113
//
114+
// VideoTexture
115+
//
116+
117+
type UseVideoTextureParams = Parameters<typeof useVideoTexture>
118+
type VideoTexture = ReturnType<typeof useVideoTexture>
119+
120+
export type VideoTextureProps = {
121+
children?: (texture: VideoTexture) => React.ReactNode
122+
src: UseVideoTextureParams[0]
123+
} & UseVideoTextureParams[1]
124+
125+
export const VideoTexture = /* @__PURE__ */ forwardRef<VideoTexture, VideoTextureProps>(
126+
({ children, src, ...config }, fref) => {
127+
const texture = useVideoTexture(src, config)
99128

100-
type UseVideoTexture = Parameters<typeof useVideoTexture>
129+
useEffect(() => {
130+
return () => void texture.dispose()
131+
}, [texture])
101132

102-
export const VideoTexture = ({
103-
children,
104-
src,
105-
...config
106-
}: {
107-
children?: (texture: ReturnType<typeof useVideoTexture>) => React.ReactNode
108-
src: UseVideoTexture[0]
109-
} & UseVideoTexture[1]) => {
110-
const ret = useVideoTexture(src, config)
133+
useImperativeHandle(fref, () => texture, [texture]) // expose texture through ref
111134

135+
return <>{children?.(texture)}</>
136+
}
137+
)
138+
139+
// rVFC hook
140+
141+
const useVideoFrame = (video: HTMLVideoElement, f?: VideoFrameRequestCallback) => {
112142
useEffect(() => {
113-
return () => void ret.dispose()
114-
}, [ret])
143+
if (!f) return
144+
if (!video.requestVideoFrameCallback) return
145+
146+
let handle: ReturnType<(typeof video)['requestVideoFrameCallback']>
147+
const callback: VideoFrameRequestCallback = (...args) => {
148+
f(...args)
149+
handle = video.requestVideoFrameCallback(callback)
150+
}
151+
video.requestVideoFrameCallback(callback)
115152

116-
return <>{children?.(ret)}</>
153+
return () => video.cancelVideoFrameCallback(handle)
154+
}, [video, f])
117155
}

0 commit comments

Comments
 (0)