/* eslint-disable react-hooks/rules-of-hooks */ import { BaseBoxShapeUtil, HTMLContainer, TLVideoShape, toDomPrecision, useIsEditing, videoShapeMigrations, videoShapeProps, } from '@tldraw/editor' import { ReactEventHandler, useCallback, useEffect, useRef, useState } from 'react' import { BrokenAssetIcon } from '../shared/BrokenAssetIcon' import { HyperlinkButton } from '../shared/HyperlinkButton' import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion' /** @public */ export class VideoShapeUtil extends BaseBoxShapeUtil { static override type = 'video' as const static override props = videoShapeProps static override migrations = videoShapeMigrations override canEdit = () => true override isAspectRatioLocked = () => true override getDefaultProps(): TLVideoShape['props'] { return { w: 100, h: 100, assetId: null, time: 0, playing: true, url: '', } } component(shape: TLVideoShape) { const { editor } = this const showControls = editor.getShapeGeometry(shape).bounds.w * editor.getZoomLevel() >= 110 const asset = shape.props.assetId ? editor.getAsset(shape.props.assetId) : null const { time, playing } = shape.props const isEditing = useIsEditing(shape.id) const prefersReducedMotion = usePrefersReducedMotion() const rVideo = useRef(null!) const handlePlay = useCallback>( (e) => { const video = e.currentTarget if (!video) return editor.updateShapes([ { type: 'video', id: shape.id, props: { playing: true, time: video.currentTime, }, }, ]) }, [shape.id, editor] ) const handlePause = useCallback>( (e) => { const video = e.currentTarget if (!video) return editor.updateShapes([ { type: 'video', id: shape.id, props: { playing: false, time: video.currentTime, }, }, ]) }, [shape.id, editor] ) const handleSetCurrentTime = useCallback>( (e) => { const video = e.currentTarget if (!video) return if (isEditing) { editor.updateShapes([ { type: 'video', id: shape.id, props: { time: video.currentTime, }, }, ]) } }, [isEditing, shape.id, editor] ) const [isLoaded, setIsLoaded] = useState(false) const handleLoadedData = useCallback>( (e) => { const video = e.currentTarget if (!video) return 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 useEffect(() => { const video = rVideo.current if (!video) return if (isLoaded && !isEditing && time !== video.currentTime) { video.currentTime = time } if (isEditing) { if (document.activeElement !== video) { video.focus() } } }, [isEditing, isLoaded, time]) useEffect(() => { if (prefersReducedMotion) { const video = rVideo.current if (!video) return video.pause() video.currentTime = 0 } }, [rVideo, prefersReducedMotion]) return ( <>
{asset?.props.src ? ( ) : ( )}
{'url' in shape.props && shape.props.url && ( )} ) } indicator(shape: TLVideoShape) { return } override 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 but 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 not found when attempting serialization.') }