Tldraw/packages/editor/src/lib/editor/shapeutils/VideoShapeUtil/VideoShapeUtil.tsx

207 wiersze
5.4 KiB
TypeScript

import { toDomPrecision } from '@tldraw/primitives'
import { TLVideoShape } from '@tldraw/tlschema'
import * as React from 'react'
import { track } from 'signia-react'
import { DefaultSpinner } from '../../../components/DefaultSpinner'
import { HTMLContainer } from '../../../components/HTMLContainer'
import { useIsEditing } from '../../../hooks/useIsEditing'
import { usePrefersReducedMotion } from '../../../utils/dom'
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */
export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
static override type = 'video'
override canEdit = () => true
override isAspectRatioLocked = () => true
override defaultProps(): TLVideoShape['props'] {
return {
w: 100,
h: 100,
assetId: null,
time: 0,
playing: true,
url: '',
}
}
render(shape: TLVideoShape) {
return <TLVideoUtilComponent shape={shape} videoUtil={this} />
}
indicator(shape: TLVideoShape) {
return <rect width={toDomPrecision(shape.props.w)} height={toDomPrecision(shape.props.h)} />
}
toSvg(shape: TLVideoShape) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image')
image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', serializeVideo(shape.id))
image.setAttribute('width', shape.props.w.toString())
image.setAttribute('height', shape.props.h.toString())
g.appendChild(image)
return g
}
}
// Function from v1, could be improved bu explicitly using this.model.time (?)
function serializeVideo(id: string): string {
const splitId = id.split(':')[1]
const video = document.querySelector(`.tl-video-shape-${splitId}`) as HTMLVideoElement
if (video) {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
canvas.getContext('2d')!.drawImage(video, 0, 0)
return canvas.toDataURL('image/png')
} else throw new Error('Video with id ' + splitId + ' not found')
}
const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
shape: TLVideoShape
videoUtil: VideoShapeUtil
}) {
const { shape, videoUtil } = props
const showControls = videoUtil.editor.getBounds(shape).w * videoUtil.editor.zoomLevel >= 110
const asset = shape.props.assetId ? videoUtil.editor.getAssetById(shape.props.assetId) : null
const { w, h, time, playing } = shape.props
const isEditing = useIsEditing(shape.id)
const prefersReducedMotion = usePrefersReducedMotion()
const rVideo = React.useRef<HTMLVideoElement>(null!)
const handlePlay = React.useCallback<React.ReactEventHandler<HTMLVideoElement>>(
(e) => {
const video = e.currentTarget
videoUtil.editor.updateShapes([
{
type: 'video',
id: shape.id,
props: {
playing: true,
time: video.currentTime,
},
},
])
},
[shape.id, videoUtil.editor]
)
const handlePause = React.useCallback<React.ReactEventHandler<HTMLVideoElement>>(
(e) => {
const video = e.currentTarget
videoUtil.editor.updateShapes([
{
type: 'video',
id: shape.id,
props: {
playing: false,
time: video.currentTime,
},
},
])
},
[shape.id, videoUtil.editor]
)
const handleSetCurrentTime = React.useCallback<React.ReactEventHandler<HTMLVideoElement>>(
(e) => {
const video = e.currentTarget
if (isEditing) {
videoUtil.editor.updateShapes([
{
type: 'video',
id: shape.id,
props: {
time: video.currentTime,
},
},
])
}
},
[isEditing, shape.id, videoUtil.editor]
)
const [isLoaded, setIsLoaded] = React.useState(false)
const handleLoadedData = React.useCallback<React.ReactEventHandler<HTMLVideoElement>>(
(e) => {
const video = e.currentTarget
if (time !== video.currentTime) {
video.currentTime = time
}
if (!playing) {
video.pause()
}
setIsLoaded(true)
},
[playing, time]
)
// If the current time changes and we're not editing the video, update the video time
React.useEffect(() => {
const video = rVideo.current
if (!video) return
if (isLoaded && !isEditing && time !== video.currentTime) {
video.currentTime = time
}
}, [isEditing, isLoaded, time])
React.useEffect(() => {
if (prefersReducedMotion) {
const video = rVideo.current
video.pause()
video.currentTime = 0
}
}, [rVideo, prefersReducedMotion])
return (
<>
<HTMLContainer id={shape.id}>
<div className="tl-counter-scaled">
{asset?.props.src ? (
<video
ref={rVideo}
className={`tl-video tl-video-shape-${shape.id.split(':')[1]} tl-hitarea-stroke`}
width="100%"
height="100%"
draggable={false}
playsInline
autoPlay
muted
loop
disableRemotePlayback
disablePictureInPicture
controls={isEditing && showControls}
onPlay={handlePlay}
onPause={handlePause}
onTimeUpdate={handleSetCurrentTime}
onLoadedData={handleLoadedData}
hidden={!isLoaded}
>
<source src={asset.props.src} />
</video>
) : (
<g transform={`translate(${(w - 38) / 2}, ${(h - 38) / 2})`}>
<DefaultSpinner />
</g>
)}
</div>
</HTMLContainer>
{'url' in shape.props && shape.props.url && (
<HyperlinkButton url={shape.props.url} zoomLevel={videoUtil.editor.zoomLevel} />
)}
</>
)
})