/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/elements/Video/Video.tsx
284 строки
8 KB
Ken McGrady
Update copyright header to 2026 (#13491)
02 янв 2026, 16:21
Не верифицирован
02 янв 2026, 16:21
374540a
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { memo, ReactElement, useEffect, useMemo, useRef } from "react" import { getLogger } from "loglevel" import { ISubtitleTrack, Video as VideoProto } from "@streamlit/protobuf" import { useCrossOriginAttribute } from "~lib/hooks/useCrossOriginAttribute" import { StreamlitEndpoints } from "~lib/StreamlitEndpoints" import { WidgetStateManager as ElementStateManager } from "~lib/WidgetStateManager" import { StyledVideoIframe } from "./styled-components" const LOG = getLogger("Video") export interface VideoProps { endpoints: StreamlitEndpoints element: VideoProto elementMgr: ElementStateManager } export interface Subtitle { label: string url: string } const VIDEO_STYLE = { width: "100%" } function Video({ element, endpoints, elementMgr, }: Readonly<VideoProps>): ReactElement { const videoRef = useRef<HTMLVideoElement>(null) /* Element may contain "url" or "data" property. */ const { type, url, startTime, subtitles, endTime, loop, autoplay, muted } = element let crossOrigin = useCrossOriginAttribute(url) const preventAutoplay = useMemo<boolean>(() => { if (!element.id) { // Elements without an ID should never autoplay return true } // Recover the state in case this component got unmounted // and mounted again for the same element. const preventAutoplayState = elementMgr.getElementState( element.id, "preventAutoplay" ) if (!preventAutoplayState) { // Set the state to prevent autoplay in case there is an unmount + mount // for the same element. elementMgr.setElementState(element.id, "preventAutoplay", true) } return preventAutoplayState ?? false }, [element.id, elementMgr]) // Create a stable dependency for checking subtitle source urls const subtitleSrcArrString = useMemo(() => { if (!subtitles) { return JSON.stringify([]) } return JSON.stringify( subtitles.map(subtitle => endpoints.buildMediaURL(`${subtitle.url}`)) ) }, [subtitles, endpoints]) // Check the video's subtitles for load errors useEffect(() => { const subtitleSrcArr: string[] = JSON.parse(subtitleSrcArrString) if (subtitleSrcArr.length === 0) return // Since there is no onerror event for track elements, we can't use the onerror event // to catch src url load errors. Catch with direct check instead. subtitleSrcArr.forEach(subtitleSrc => { // eslint-disable-next-line @typescript-eslint/no-floating-promises -- TODO: Fix this endpoints.checkSourceUrlResponse(subtitleSrc, "Video Subtitle") }) }, [subtitleSrcArrString, endpoints]) // Handle startTime changes useEffect(() => { if (videoRef.current) { videoRef.current.currentTime = startTime } }, [startTime]) useEffect(() => { const videoNode = videoRef.current const setStartTime: () => void = () => { if (videoNode) { videoNode.currentTime = element.startTime } } if (videoNode) { videoNode.addEventListener("loadedmetadata", setStartTime) } return () => { if (videoNode) { videoNode.removeEventListener("loadedmetadata", setStartTime) } } }, [element]) // Stop the video at 'endTime' and handle loop useEffect(() => { const videoNode = videoRef.current if (!videoNode) { return } // Flag to avoid calling 'videoNode.pause()' multiple times let stoppedByEndTime = false const handleTimeUpdate = (): void => { if (endTime > 0 && videoNode.currentTime >= endTime) { if (loop) { // If loop is true and we reached 'endTime', reset to 'startTime' videoNode.currentTime = startTime || 0 // eslint-disable-next-line @typescript-eslint/no-floating-promises -- TODO: Fix this videoNode.play() } else if (!stoppedByEndTime) { stoppedByEndTime = true videoNode.pause() } } } if (endTime > 0) { videoNode.addEventListener("timeupdate", handleTimeUpdate) } return () => { if (videoNode && endTime > 0) { videoNode.removeEventListener("timeupdate", handleTimeUpdate) } } }, [endTime, loop, startTime]) // Handle looping the video useEffect(() => { const videoNode = videoRef.current if (!videoNode) { return } // Loop the video when it has ended const handleVideoEnd = (): void => { if (loop) { videoNode.currentTime = startTime || 0 // Reset to startTime or to the start if not specified // eslint-disable-next-line @typescript-eslint/no-floating-promises -- TODO: Fix this videoNode.play() } } videoNode.addEventListener("ended", handleVideoEnd) return () => { if (videoNode) { videoNode.removeEventListener("ended", handleVideoEnd) } } }, [loop, startTime]) const getYoutubeSrc = (urlArg: string): string => { const youtubeUrl = new URL(urlArg) if (startTime && !isNaN(startTime)) { youtubeUrl.searchParams.append("start", startTime.toString()) } if (endTime && !isNaN(endTime)) { youtubeUrl.searchParams.append("end", endTime.toString()) } if (loop) { youtubeUrl.searchParams.append("loop", "1") // When using the loop parameter, YouTube requires the playlist parameter to be set to the same video ID const videoId = youtubeUrl.pathname.split("/").pop() if (videoId) { youtubeUrl.searchParams.append("playlist", videoId) } } if (autoplay) { youtubeUrl.searchParams.append("autoplay", "1") } if (muted) { youtubeUrl.searchParams.append("mute", "1") } return youtubeUrl.toString() } // Is this a YouTube link? If so we need a fancier tag. // NOTE: This part assumes the URL is already an "embed" link. if (type === VideoProto.Type.YOUTUBE_IFRAME) { return ( <StyledVideoIframe className="stVideo" data-testid="stVideo" title={url} src={getYoutubeSrc(url)} allow="autoplay; encrypted-media" allowFullScreen /> ) } const handleVideoError = ( e: React.SyntheticEvent<HTMLVideoElement> ): void => { const videoUrl = e.currentTarget.src LOG.error(`Client Error: Video source error - ${videoUrl}`) endpoints.sendClientErrorToHost( "Video", "Video source failed to load", "onerror triggered", videoUrl ) } // When in dev mode we set crossOrigin to "anonymous" to avoid CORS issues // when streamlit frontend and backend are running on different ports if (process.env.NODE_ENV === "development" && subtitles.length > 0) { crossOrigin = "anonymous" } return ( // eslint-disable-next-line jsx-a11y/media-has-caption <video className="stVideo" data-testid="stVideo" ref={videoRef} controls muted={muted} autoPlay={autoplay && !preventAutoplay} src={endpoints.buildMediaURL(url)} style={VIDEO_STYLE} crossOrigin={crossOrigin} onError={handleVideoError} > {subtitles?.map((subtitle: ISubtitleTrack, idx: number) => ( <track // TODO: Update to match React best practices // eslint-disable-next-line @eslint-react/no-array-index-key key={idx} kind="captions" src={endpoints.buildMediaURL(`${subtitle.url}`)} label={`${subtitle.label}`} default={idx === 0} data-testid="stVideoSubtitle" /> ))} </video> ) } export default memo(Video)