import React, { useRef, useEffect, RefObject, FC, useState, CSSProperties, useMemo, MutableRefObject, } from "react"; import { StreamPlayerApi, StreamProps, VideoDimensions } from "./types"; import { useStreamSDK, safelyAccessStreamSDK } from "./useStreamSDK"; import { useIframeSrc } from "./useIframeSrc"; import { validSrcUrl } from "./validSrcUrl"; /** * Hook for syncing properties to the SDK api when they change */ function useProperty<T, Key extends keyof T>( name: Key, ref: RefObject<T | undefined>, value: T[Key] ) { useEffect(() => { if (!ref.current) return; const el = ref.current; el[name] = value; }, [name, value, ref]); } /** * Hook for binding event listeners to the player. */ function useEvent( event: string, ref: MutableRefObject<StreamPlayerApi | undefined>, callback: EventListener = noop ) { useEffect(() => { if (!ref.current) return; const el = ref.current; el.addEventListener(event, callback); // clean up return () => el.removeEventListener(event, callback); }, [callback, event, ref]); } // Declaring a single noop function that will retain object // identity across renders and prevent unnecessary rebinding // when no callback is provided const noop = () => {}; export const Stream: FC<StreamProps> = (props) => { const streamSdk = useStreamSDK(); return streamSdk ? <StreamEmbed {...props} /> : null; }; const responsiveIframeStyles: CSSProperties = { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, height: "100%", width: "100%", }; const Container: FC<{ responsive: boolean; videoDimensions: VideoDimensions; className?: string; }> = ({ children, responsive, className, videoDimensions }) => { const { videoHeight, videoWidth } = videoDimensions; const responsiveStyles: CSSProperties = useMemo<CSSProperties>( () => ({ position: "relative", paddingTop: videoWidth > 0 ? `${(videoHeight / videoWidth) * 100}%` : undefined, }), [videoWidth, videoHeight] ); return ( <div className={className} style={responsive ? responsiveStyles : undefined} > {children} </div> ); }; export const StreamEmbed: FC<StreamProps> = ({ src, adUrl, controls = false, muted = false, autoplay = false, loop = false, preload = "metadata", primaryColor, letterboxColor, defaultTextTrack, height, width, poster, currentTime = 0, volume = 1, startTime, streamRef, responsive = true, className, title, onAbort, onCanPlay, onCanPlayThrough, onDurationChange, onEnded, onError, onLoadedData, onLoadedMetaData, onLoadStart, onPause, onPlay, onPlaying, onProgress, onRateChange, onResize, onSeeked, onSeeking, onStalled, onSuspend, onTimeUpdate, onVolumeChange, onWaiting, onStreamAdStart, onStreamAdEnd, onStreamAdTimeout, }) => { const internalRef = useRef<StreamPlayerApi>(); const ref = streamRef ?? internalRef; const [videoDimensions, setVideoDimensions] = useState({ videoHeight: 0, videoWidth: 0, }); const iframeRef = useRef<HTMLIFrameElement>(null); const computedSrc = useIframeSrc(src, { muted, preload, loop, autoplay, controls, poster, primaryColor, letterboxColor, adUrl, defaultTextTrack, startTime, }); // While it's easier for most consumers to simply provide the video id // or signed URL and have us compute the iframe's src for them, some // consumers may need to manually specify the iframe's src. const iframeSrc = validSrcUrl(src) ? src : computedSrc; useProperty("muted", ref, muted); useProperty("controls", ref, controls); useProperty("src", ref, src); useProperty("autoplay", ref, autoplay); useProperty("currentTime", ref, currentTime); useProperty("loop", ref, loop); useProperty("preload", ref, preload); useProperty("primaryColor", ref, primaryColor); useProperty("letterboxColor", ref, letterboxColor); useProperty("volume", ref, volume); // instantiate API after properties are bound because we want undefined // values to be set before defining the properties useEffect(() => { const Stream = safelyAccessStreamSDK(); if (iframeRef.current && Stream) { const api = Stream(iframeRef.current); ref.current = api; const { videoHeight, videoWidth } = api; if (videoHeight && videoWidth) setVideoDimensions({ videoHeight, videoWidth }); } }, []); // bind events useEvent("abort", ref, onAbort); useEvent("canplay", ref, onCanPlay); useEvent("canplaythrough", ref, onCanPlayThrough); useEvent("durationchange", ref, onDurationChange); useEvent("ended", ref, onEnded); useEvent("error", ref, onError); useEvent("loadeddata", ref, onLoadedData); useEvent("loadedmetadata", ref, onLoadedMetaData); useEvent("loadstart", ref, onLoadStart); useEvent("pause", ref, onPause); useEvent("play", ref, onPlay); useEvent("playing", ref, onPlaying); useEvent("progress", ref, onProgress); useEvent("ratechange", ref, onRateChange); useEvent("seeked", ref, onSeeked); useEvent("seeking", ref, onSeeking); useEvent("stalled", ref, onStalled); useEvent("suspend", ref, onSuspend); useEvent("timeupdate", ref, onTimeUpdate); useEvent("volumechange", ref, onVolumeChange); useEvent("waiting", ref, onWaiting); useEvent("stream-adstart", ref, onStreamAdStart); useEvent("stream-adend", ref, onStreamAdEnd); useEvent("stream-adtimeout", ref, onStreamAdTimeout); useEvent("resize", ref, (e) => { if (ref.current) { const { videoHeight, videoWidth } = ref.current; setVideoDimensions({ videoHeight, videoWidth }); onResize && onResize(e); } }); return ( <Container className={className} responsive={responsive} videoDimensions={videoDimensions} > <iframe ref={iframeRef} src={iframeSrc} title={title} style={responsive ? responsiveIframeStyles : undefined} frameBorder={0} height={height} width={width} allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;" allowFullScreen /> </Container> ); };