kopia lustrzana https://github.com/Tldraw/Tldraw
207 wiersze
5.4 KiB
TypeScript
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} />
|
|
)}
|
|
</>
|
|
)
|
|
})
|