Skip to content

Commit

Permalink
Merge pull request #40 from pmndrs/feature/video
Browse files Browse the repository at this point in the history
Feature/video
  • Loading branch information
bbohlender authored Apr 11, 2024
2 parents d8f8ded + ce2c316 commit a789caa
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 2 deletions.
Binary file added examples/market/public/example.mp4
Binary file not shown.
3 changes: 2 additions & 1 deletion examples/market/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Environment, OrbitControls } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import { EffectComposer, TiltShift2 } from '@react-three/postprocessing'
import { Root, Container, Image, Text, Fullscreen, DefaultProperties } from '@react-three/uikit'
import { Root, Container, Image, Text, Fullscreen, DefaultProperties, VideoContainer } from '@react-three/uikit'
import { PlusCircle } from '@react-three/uikit-lucide'
import { Defaults, colors } from '@/theme.js'
import { DialogAnchor } from '@/dialog.js'
Expand Down Expand Up @@ -108,6 +108,7 @@ export function MarketPage() {
</Container>
<Separator marginY={16} />
<Container flexShrink={1} flexDirection="row" overflow="scroll" gap={16} paddingBottom={16}>
<VideoContainer autoplay muted borderRadius={6} flexShrink={0} src="example.mp4" />
{madeForYouAlbums.map((album) => (
<AlbumArtwork key={album.name} album={album} aspectRatio="square" width={150} height={150} />
))}
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export * from './content.js'
export * from './fullscreen.js'
export * from './suspending.js'
export * from './portal.js'
export * from './video.js'
91 changes: 91 additions & 0 deletions packages/react/src/video.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
ComponentPropsWithoutRef,
ReactNode,
RefAttributes,
createContext,
forwardRef,
useContext,
useEffect,
useImperativeHandle,
useMemo,
} from 'react'
import { Image } from './image.js'
import { Texture, VideoTexture } from 'three'
import { signal } from '@preact/signals-core'

const VideoContainerContext = createContext<HTMLVideoElement | undefined>(undefined)

export function useVideoElement(): HTMLVideoElement {
const element = useContext(VideoContainerContext)
if (element == null) {
throw new Error(`useVideoElement can only be executed inside a VideoContainer`)
}
return element
}

export const VideoContainer: (
props: Omit<ComponentPropsWithoutRef<typeof Image>, 'src'> & {
src: string | MediaStream
volume?: number
preservesPitch?: boolean
playbackRate?: number
muted?: boolean
loop?: boolean
autoplay?: boolean
} & RefAttributes<HTMLVideoElement>,
) => ReactNode = forwardRef(
(
props: Omit<ComponentPropsWithoutRef<typeof Image>, 'src'> & {
src: string | MediaStream
volume?: number
preservesPitch?: boolean
playbackRate?: number
muted?: boolean
loop?: boolean
autoplay?: boolean
},
ref,
) => {
const texture = useMemo(() => signal<Texture | undefined>(undefined), [])
const aspectRatio = useMemo(() => signal<number>(1), [])
const video = useMemo(() => document.createElement('video'), [])
useEffect(() => {
if (!props.autoplay) {
return
}
video.style.position = 'absolute'
video.style.width = '1px'
video.style.zIndex = '-1000'
video.style.top = '0px'
video.style.left = '0px'
document.body.append(video)
return () => video.remove()
}, [props.autoplay, video])
video.playsInline = true
video.volume = props.volume ?? 1
video.preservesPitch = props.preservesPitch ?? true
video.playbackRate = props.playbackRate ?? 1
video.muted = props.muted ?? false
video.loop = props.loop ?? false
video.autoplay = props.autoplay ?? false
useEffect(() => {
if (typeof props.src === 'string') {
video.src = props.src
} else {
video.srcObject = props.src
}
const updateAspectRatio = () => (aspectRatio.value = video.videoWidth / video.videoHeight)
updateAspectRatio()
video.addEventListener('resize', updateAspectRatio)
return () => video.removeEventListener('resize', updateAspectRatio)
}, [aspectRatio, props.src, video])
useEffect(() => {
const videoTexture = new VideoTexture(video)
texture.value = videoTexture
return () => videoTexture.dispose()
}, [texture, video])

useImperativeHandle(ref, () => video, [video])
return <Image aspectRatio={aspectRatio} {...props} src={texture} />
},
)
6 changes: 5 additions & 1 deletion packages/uikit/src/vanilla/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export class Image extends Parent {
private readonly parentContextSignal = createParentContextSignal()
private readonly unsubscribe: () => void

constructor(src: string | Signal<string>, properties?: ImageProperties, defaultProperties?: AllOptionalProperties) {
constructor(
src: Signal<string | undefined> | string | Texture | Signal<Texture | undefined> | undefined,
properties?: ImageProperties,
defaultProperties?: AllOptionalProperties,
) {
super()
setupParentContextSignal(this.parentContextSignal, this)
this.matrixAutoUpdate = false
Expand Down
1 change: 1 addition & 0 deletions packages/uikit/src/vanilla/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './input.js'
export * from './custom.js'
export * from './content.js'
export * from './fullscreen.js'
export * from './video.js'
68 changes: 68 additions & 0 deletions packages/uikit/src/vanilla/video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Image } from './image.js'
import { VideoTexture } from 'three'
import { ImageProperties } from '../index.js'
import { Signal, signal } from '@preact/signals-core'

export class VideoContainer extends Image {
public readonly element: HTMLVideoElement
private readonly texture: VideoTexture
private readonly aspectRatio: Signal<number>
private readonly updateAspectRatio: () => void

constructor(
src: string | MediaStream,
volume?: number,
preservesPitch?: boolean,
playbackRate?: number,
muted?: boolean,
loop?: boolean,
autoplay?: boolean,
properties?: ImageProperties,
defaultProperties?: ImageProperties,
) {
const element = document.createElement('video')
const texture = new VideoTexture(element)
const aspectRatio = signal<number>(1)
super(texture, { aspectRatio, ...properties }, defaultProperties)
this.element = element
this.texture = texture
this.aspectRatio = aspectRatio
if (autoplay) {
this.element.style.position = 'absolute'
this.element.style.width = '1px'
this.element.style.zIndex = '-1000'
this.element.style.top = '0px'
this.element.style.left = '0px'
document.body.append(this.element)
}
this.element.playsInline = true
this.element.volume = volume ?? 1
this.element.preservesPitch = preservesPitch ?? true
this.element.playbackRate = playbackRate ?? 1
this.element.muted = muted ?? false
this.element.loop = loop ?? false
this.element.autoplay = autoplay ?? false
if (typeof src === 'string') {
this.element.src = src
} else {
this.element.srcObject = src
}
this.updateAspectRatio = () => (aspectRatio.value = this.element.videoWidth / this.element.videoHeight)
this.updateAspectRatio()
this.element.addEventListener('resize', this.updateAspectRatio)
}

setProperties(properties?: ImageProperties): void {
super.setProperties({
aspectRatio: this.aspectRatio,
...properties,
})
}

destroy(): void {
super.destroy()
this.texture.dispose()
this.element.remove()
this.element.removeEventListener('resize', this.updateAspectRatio)
}
}

0 comments on commit a789caa

Please sign in to comment.