UNPKG

tldraw

Version:

A tiny little drawing editor.

159 lines (158 loc) 5.59 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { BaseBoxShapeUtil, HTMLContainer, MediaHelpers, WeakCache, toDomPrecision, useEditor, useEditorComponents, useIsEditing, videoShapeMigrations, videoShapeProps } from "@tldraw/editor"; import classNames from "classnames"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { BrokenAssetIcon } from "../shared/BrokenAssetIcon.mjs"; import { HyperlinkButton } from "../shared/HyperlinkButton.mjs"; import { useImageOrVideoAsset } from "../shared/useImageOrVideoAsset.mjs"; import { usePrefersReducedMotion } from "../shared/usePrefersReducedMotion.mjs"; const videoSvgExportCache = new WeakCache(); class VideoShapeUtil extends BaseBoxShapeUtil { static type = "video"; static props = videoShapeProps; static migrations = videoShapeMigrations; options = { autoplay: true }; canEdit() { return true; } isAspectRatioLocked() { return true; } getDefaultProps() { return { w: 100, h: 100, assetId: null, autoplay: this.options.autoplay, url: "", altText: "", // Not used, but once upon a time were used to sync video state between users time: 0, playing: true }; } getAriaDescriptor(shape) { return shape.props.altText; } component(shape) { return /* @__PURE__ */ jsx(VideoShape, { shape }); } indicator(shape) { return /* @__PURE__ */ jsx("rect", { width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h) }); } useLegacyIndicator() { return false; } getIndicatorPath(shape) { const path = new Path2D(); path.rect(0, 0, shape.props.w, shape.props.h); return path; } async toSvg(shape, ctx) { const props = shape.props; if (!props.assetId) return null; const asset = this.editor.getAsset(props.assetId); if (!asset) return null; const src = await videoSvgExportCache.get(asset, async () => { const assetUrl = await ctx.resolveAssetUrl(asset.id, props.w); if (!assetUrl) return null; const video = await MediaHelpers.loadVideo(assetUrl); return await MediaHelpers.getVideoFrameAsDataUrl(video, 0); }); if (!src) return null; return /* @__PURE__ */ jsx("image", { href: src, width: props.w, height: props.h, "aria-label": shape.props.altText }); } } const VideoShape = memo(function VideoShape2({ shape }) { const editor = useEditor(); const showControls = editor.getShapeGeometry(shape).bounds.w * editor.getEfficientZoomLevel() >= 110; const isEditing = useIsEditing(shape.id); const prefersReducedMotion = usePrefersReducedMotion(); const { Spinner } = useEditorComponents(); const { asset, url } = useImageOrVideoAsset({ shapeId: shape.id, assetId: shape.props.assetId, width: shape.props.w }); const rVideo = useRef(null); const [isLoaded, setIsLoaded] = useState(false); const handleLoadedData = useCallback((e) => { const video = e.currentTarget; if (!video) return; setIsLoaded(true); }, []); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const fullscreenChange = () => setIsFullscreen(document.fullscreenElement === rVideo.current); document.addEventListener("fullscreenchange", fullscreenChange); return () => document.removeEventListener("fullscreenchange", fullscreenChange); }); useEffect(() => { const video = rVideo.current; if (!video) return; if (isEditing) { if (document.activeElement !== video) { video.focus(); } } }, [isEditing, isLoaded]); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( HTMLContainer, { id: shape.id, style: { color: "var(--tl-color-text-3)", backgroundColor: asset ? "transparent" : "var(--tl-color-low)", border: asset ? "none" : "1px solid var(--tl-color-low-border)" }, children: /* @__PURE__ */ jsx("div", { className: "tl-counter-scaled", children: /* @__PURE__ */ jsx("div", { className: "tl-video-container", children: !asset ? /* @__PURE__ */ jsx(BrokenAssetIcon, {}) : Spinner && !asset.props.src ? /* @__PURE__ */ jsx(Spinner, {}) : url ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "video", { ref: rVideo, style: isEditing ? { pointerEvents: "all" } : !isLoaded ? { display: "none" } : void 0, className: classNames("tl-video", `tl-video-shape-${shape.id.split(":")[1]}`, { "tl-video-is-fullscreen": isFullscreen }), width: "100%", height: "100%", draggable: false, playsInline: true, autoPlay: shape.props.autoplay && !prefersReducedMotion, muted: true, loop: true, disableRemotePlayback: true, disablePictureInPicture: true, controls: isEditing && showControls, onLoadedData: handleLoadedData, hidden: !isLoaded, "aria-label": shape.props.altText, children: /* @__PURE__ */ jsx("source", { src: url }) }, url ), !isLoaded && Spinner && /* @__PURE__ */ jsx(Spinner, {}) ] }) : null }) }) } ), "url" in shape.props && shape.props.url && /* @__PURE__ */ jsx(HyperlinkButton, { url: shape.props.url }) ] }); }); export { VideoShapeUtil }; //# sourceMappingURL=VideoShapeUtil.mjs.map