Closed st4l1nR closed 1 year ago
Who it's facing the same problem i solved it by setting onProgress={onLoadedData} instead of onLoadedData={onLoadedData} in the video handler here my code:
import Image from "next/image";
import { useInView } from "react-intersection-observer";
import { useState, useEffect, useRef, BaseSyntheticEvent } from "react";
import { useMainContext } from "../MainContext";
import { BsPlay, BsPause, BsVolumeMute, BsVolumeUp } from "react-icons/bs";
import { BiVolumeMute, BiVolumeFull, BiPlay, BiPause } from "react-icons/bi";
import { secondsToHMS } from "../../lib/utils";
import { useKeyPress } from "../hooks/KeyPress";
import { ImSpinner2 } from "react-icons/im";
import React from "react";
import useGlobalState from "../hooks/useGlobalState";
const VideoHandler = ({
name,
curPostName = undefined,
thumbnail,
placeholder,
videoInfo,
maxHeight = {},
maxHeightNum = -1,
fullMediaMode = false,
imgFull = false,
postMode = false,
hide = false,
audio,
containerDims = undefined,
uniformMediaMode = false,
}) => {
const context: any = useMainContext();
const [vodInfo, setVodInfo] = useState(() => videoInfo);
const { getGlobalData, setGlobalData, clearGlobalState } = useGlobalState([
"videoLoadData",
]);
const video: any = useRef();
const volControls = useRef<HTMLDivElement>(null);
const audioRef: any = useRef();
const fullWidthRef: any = useRef();
const seekRef: any = useRef();
const [hasAudio, sethasAudio] = useState(videoInfo?.hasAudio ?? false);
const [videoLoaded, setVideoLoaded] = useState(false);
const [useFallback, setUseFallback] = useState(false);
const [audioPlaying, setAudioPlaying] = useState(false);
const [videoPlaying, setVideoPlaying] = useState(false);
const [preventPlay, setPreventPlay] = useState(hide);
const [manualPlay, setmanualPlay] = useState(false);
const [manualPause, setManualPause] = useState(false);
const [muted, setMuted] = useState(
!(context?.audioOnHover && postMode) || preventPlay
);
const { volume, setVolume } = context;
const [prevMuted, setPrevMuted] = useState(true);
const [manualAudio, setManualAudio] = useState(false);
const [currentTime, setCurrentTime] = useState(0.0);
const [videoDuration, setVideoDuration] = useState(0.0);
const [buffering, setBuffering] = useState(false);
const [mouseIn, setMouseIn] = useState(false);
const [focused, setFocused] = useState(postMode);
const [show, setShow] = useState(postMode ? true : false);
const [left, setLeft] = useState(false);
const [heightStyle, setHeightStyle] = useState({});
const { ref } = useInView({
threshold: [0, 0.7, 0.8, 0.9, 1],
onChange: (inView, entry) => {
if (!postMode && !preventPlay && !context?.mediaMode) {
entry.intersectionRatio == 0
? setShow(false)
: entry.intersectionRatio >= 0.8 && setShow(true);
show && !postMode && entry.intersectionRatio < 1
? setLeft(true)
: entry.intersectionRatio == 1 && setLeft(false);
}
},
});
useEffect(() => {
if (name === curPostName && fullMediaMode) {
setPreventPlay(false);
} else if (fullMediaMode && name !== curPostName) {
setPreventPlay(true);
}
}, [curPostName]);
useEffect(() => {
if (videoLoaded && show && !manualPause && !preventPlay) {
playVideo();
} else if (!show && videoPlaying) {
pauseVideo();
} else if (fullMediaMode && preventPlay) {
pauseVideo();
}
}, [videoLoaded, show, preventPlay]);
const onLoadedData = () => {
setVideoDuration(video?.current?.duration);
setVideoLoaded(true);
};
const onAudioLoaded = () => {
sethasAudio(true);
};
const [vidHeight, setVidHeight] = useState(videoInfo?.height);
const [vidWidth, setVidWidth] = useState(videoInfo?.width);
useEffect(() => {
const maximizeWidth = () => {
let r = fullWidthRef?.current?.clientWidth / videoInfo.width;
setVidWidth(Math.floor(r * videoInfo.width));
setVidHeight(Math.floor(r * videoInfo.height));
};
//if imgFull maximize the video to take full width
if (imgFull && !containerDims) {
maximizeWidth();
}
//if postMode with portrait container (containerDims) fill it
else if (postMode && containerDims) {
let ry = containerDims?.[1] / videoInfo?.height;
let rx = containerDims?.[0] / videoInfo?.width;
if (Math.abs(ry - rx) < 0.05) {
//if minimal cropping just fill the area
setVidHeight(Math.floor(containerDims?.[1]));
setVidWidth(Math.floor(containerDims?.[0]));
} else if (ry <= rx) {
setVidHeight(containerDims?.[1]);
setVidWidth(
Math.floor(videoInfo.width * (containerDims?.[1] / videoInfo?.height))
);
} else {
setVidWidth(containerDims?.[0]);
setVidHeight(
Math.floor(videoInfo.height * (containerDims?.[0] / videoInfo?.width))
);
}
}
//
//in single column or in a post we will scale video so height is within window. By default maxHeightNum is x * windowHeight (set in Media component)
else if ((context.columns == 1 || postMode) && maxHeightNum > 0) {
if (videoInfo.height > maxHeightNum) {
let r2 = maxHeightNum / videoInfo.height;
setVidHeight(Math.floor(maxHeightNum));
setVidWidth(Math.floor(videoInfo.width * r2));
}
}
//otherwise fill the available width to minimize letterboxing
else {
maximizeWidth();
}
return () => {
//by default native video height/width
setVidHeight(videoInfo.height);
setVidWidth(videoInfo.width);
};
}, [
imgFull,
maxHeightNum,
fullWidthRef?.current?.clientWidth,
videoInfo,
context.columnOverride,
postMode,
containerDims,
]);
useEffect(() => {
if (videoInfo.hasAudio) {
audioRef.current = video.current;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//unify overall heights to avoid weird sizing issues
useEffect(() => {
let h = (video?.current?.clientWidth / vidWidth) * vidHeight;
if (h > 0) {
setHeightStyle({
height: `${h}px`,
});
}
}, [video?.current?.clientWidth, vidHeight, vidWidth]);
useEffect(() => {
if (
((context?.autoplay && !fullMediaMode) ||
(context?.autoPlayMode && fullMediaMode && !preventPlay)) &&
!videoPlaying &&
(!context.pauseAll || fullMediaMode) &&
show
) {
video?.current?.play().catch((e) => console.log(e));
} else if (!context?.autoplay && videoPlaying && !fullMediaMode) {
video?.current?.pause()?.catch((e) => console.log(e));
}
}, [context.autoplay, context.pauseAll, context?.autoPlayMode]);
useEffect(() => {
if (context.pauseAll) {
pauseAll();
}
}, [context.pauseAll]);
//main control for play/pause button
const playControl = (e?, manual = false) => {
setShow(true);
e?.preventDefault();
e?.stopPropagation();
if (true) {
//when video is paused/stopped
if ((video?.current?.paused || video?.current?.ended) && !videoPlaying) {
//play the video
manual && setManualPause(false);
setBuffering(true);
video?.current
?.play()
.then(() => {
//setVideoPlaying(true); this will be handled directly from the video element
manual && setmanualPlay(true);
if (hasAudio && !videoInfo.hasAudio) {
audioRef.current.currentTime = video.current.currentTime;
audioRef?.current
?.play()
?.then(() => setAudioPlaying(true))
.catch((e) => console.log(e));
}
})
.catch((e) => {
//setVideoPlaying(false); this will be handled directly from the video element
manual && setmanualPlay(false);
});
}
//pause the video
else if (!video?.current?.paused && videoPlaying) {
manual && setManualPause(true);
setVideoPlaying(false);
setmanualPlay(false);
video?.current?.pause();
if (!audioRef?.current?.paused && !videoInfo.hasAudio) {
audioRef.current?.pause();
setAudioPlaying(false);
}
}
}
};
//main controls for audio
const audioControl = (e?, manual = false) => {
e?.preventDefault();
e?.stopPropagation();
//if audio exists
if (audioRef?.current?.muted !== undefined && hasAudio && !preventPlay) {
if (!videoInfo.hasAudio) {
// sync audio
audioRef.current.currentTime = video.current.currentTime;
//if audio is not playing and video is playing, play audio
if (!audioPlaying && videoPlaying) {
audioRef?.current?.play().catch((e) => console.log(e));
}
}
manual && setManualAudio(true);
setMuted((m) => !m);
}
};
//for pausing media when another post is opened
const pauseAll = () => {
videoLoaded && video?.current?.pause();
hasAudio && audioRef.current.pause();
};
const pauseVideo = () => {
pauseAudio();
videoLoaded && video?.current?.pause();
};
//pause Audio while video is loading
const pauseAudio = () => {
if (hasAudio) {
audioRef.current?.pause();
}
};
//play Audio..
const playAudio = () => {
if (hasAudio) {
audioRef.current.currentTime = video.current.currentTime;
audioRef.current.play();
}
};
//play Video..
const playVideo = () => {
videoLoaded &&
video?.current
?.play()
.then(() => playAudio())
.catch((e) => console.log(e));
console.log("playing video", video.current.src);
};
const handleMouseIn = (e) => {
setShow(true);
setMouseIn(true);
if (context.hoverplay && context.cardStyle !== "row1") {
if (
(!manualPlay || video?.current?.paused) &&
!context.autoplay &&
!postMode
) {
playControl(e);
}
}
if (!postMode && !manualAudio && context.audioOnHover && !preventPlay) {
setMuted(false);
}
};
const handleMouseOut = () => {
setMouseIn(false);
//not in manual play, then pause audio and video
if (context.hoverplay && context.cardStyle !== "row1") {
if (!manualPlay && !context.autoplay && !postMode) {
// pauseAudio();
pauseVideo();
}
}
//also mute if not manually unmuted manipulation
if (
!manualAudio &&
!postMode &&
context.audioOnHover &&
context.columns !== 1
) {
setMuted(true);
}
};
const [showVol, setShowVol] = useState(false);
const [seekTime, setSeekTime] = useState("");
const [seekLeftOfset, setSeekLeftOffset] = useState(0);
const [seekTargetLength, setSeekTargetLenght] = useState(0);
const showSeek = (e) => {
let r = e.nativeEvent.offsetX / e.nativeEvent.target.clientWidth;
setSeekLeftOffset(e.nativeEvent.offsetX);
setSeekTargetLenght(e.nativeEvent.target.clientWidth);
//console.log(secondsToHMS(r * videoDuration));
setSeekTime(secondsToHMS(r * videoDuration));
};
const updateSeek = (e) => {
e.stopPropagation();
e.preventDefault();
//console.log(e, e.nativeEvent.offsetX, e.nativeEvent.target.clientWidth);
//console.log(video.current);
let r = e.nativeEvent.offsetX / e.nativeEvent.target.clientWidth;
//console.log(r, videoDuration, r * videoDuration);
video.current.currentTime = r * videoDuration;
if (!videoPlaying) {
setProgressPerc(video?.current?.currentTime / video?.current?.duration);
}
};
const [volMouseDown, setVolMouseDown] = useState(false);
const [showVolSlider, setShowVolSlider] = useState(false);
const updateVolume = (e) => {
e.stopPropagation();
e.preventDefault();
let r = Math.abs(
1 - e.nativeEvent.offsetY / e.nativeEvent.target.clientHeight
);
if (r >= 1) r = 1;
if (r <= 0.1) r = 0;
r > 0 ? setMuted(false) : setMuted(true);
setManualAudio(true);
setVolume(r);
};
const updateVolumeDrag = (e) => {
if (volMouseDown) {
updateVolume(e);
}
};
useEffect(() => {
if (
(!show || !context?.audioOnHover || left) &&
(!postMode || fullMediaMode)
) {
setMuted(true);
} else if (
context?.audioOnHover &&
(postMode || context?.columns == 1) &&
!left
) {
setMuted(false);
}
}, [show, context?.audioOnHover, context?.columns, left]);
useEffect(() => {
if (audioRef?.current) {
audioRef.current.muted = muted;
}
}, [muted]);
useEffect(() => {
if (audioRef?.current) {
audioRef.current.volume = volume ?? 0.5;
}
return () => {
//
};
}, [volume]);
//smoother progress bar
const [progressPerc, setProgressPerc] = useState(0);
const [intervalID, setIntervalID] = useState<any>();
useEffect(() => {
if (
videoPlaying &&
video?.current &&
(mouseIn || fullMediaMode) &&
!preventPlay
) {
let initial = Date.now();
let timepassed = 0;
let duration = video?.current?.duration * 1000;
let initialTime = video?.current?.currentTime * 1000;
let delta = initialTime / duration;
let updateSeekRange = () => {
if (videoPlaying) {
timepassed = Date.now() - initial;
delta = (initialTime + timepassed) / duration;
if (delta > 1) delta = 1;
setCurrentTime((initialTime + timepassed) / 1000);
setProgressPerc(delta);
//handle out of range..
if (
(initialTime + timepassed) / 1000 > duration &&
video?.current?.currentTime >= 0
) {
video.current.currentTime = 0;
}
}
};
setIntervalID((id) => {
clearInterval(id);
return setInterval(updateSeekRange, 10);
});
} else {
clearInterval(intervalID);
}
return () => {};
}, [videoPlaying, mouseIn, fullMediaMode, preventPlay]);
const setData = useRef<any>();
useEffect(() => {
//console.log(progressPerc, context?.autoPlayMode);
if (
progressPerc >= 0.9 &&
fullMediaMode &&
!setData.current &&
!preventPlay
) {
//console.log("ended!", progressPerc);
setData.current = true;
setTimeout(
() => {
//console.log('set', name)
setGlobalData(name, true);
},
videoDuration > 0 ? videoDuration * 0.1 * 1000 : 100
);
} else if (progressPerc < 0.9) {
setData.current = false;
}
}, [progressPerc, fullMediaMode, name, videoDuration, preventPlay]);
const kPress = useKeyPress("k");
const mPress = useKeyPress("m");
useEffect(() => {
if (focused && !context?.replyFocus && !preventPlay) {
kPress && playControl();
mPress && audioControl();
}
return () => {};
}, [kPress, mPress, context.replyFocus, focused]);
const timeoutRef = useRef<any>(null);
return (
<div
className={
" min-w-full group hover:cursor-pointer overflow-hidden relative" +
(uniformMediaMode
? "object-cover object-center aspect-[9/16] "
: " flex items-center justify-center")
}
onClick={(e) => {
if (fullMediaMode) {
if (timeoutRef.current === null) {
timeoutRef.current = setTimeout(() => {
playControl(e, true);
timeoutRef.current = null;
}, 300);
}
} else if (!uniformMediaMode) {
playControl(e, true);
}
}}
onDoubleClick={(e) => {
if (fullMediaMode) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}}
onMouseEnter={(e) => {
setFocused(true);
handleMouseIn(e);
}}
onMouseLeave={(e) => {
!postMode && setFocused(false);
handleMouseOut();
}}
ref={ref}
style={
containerDims?.[1]
? { height: `${containerDims[1]}px` }
: uniformMediaMode
? { minHeight: "100%" }
: heightStyle
}
>
{((!videoLoaded && (context?.autoPlay || postMode)) || buffering) && (
<div className="absolute z-10 text-white -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2">
<ImSpinner2 className="w-8 h-8 animate-spin" />
</div>
)}
{/* Background Span Image */}
<div
className="absolute z-0 min-w-full min-h-full overflow-hidden brightness-[0.2]"
ref={fullWidthRef}
>
<Image
className={"scale-110 blur-md "}
src={thumbnail.url}
alt=""
layout="fill"
unoptimized={true}
priority={imgFull}
onError={() => {
setUseFallback(true);
}}
/>
</div>
{/* Controls */}
<div className="absolute bottom-0 z-10 flex flex-row min-w-full p-1 pb-2 text-white">
<div
className={
(fullMediaMode ? "hidden md:flex" : "flex") +
" items-center space-x-2"
}
>
<button
aria-label="play"
onClick={(e) => {
playControl(e, true);
}}
className={
(uniformMediaMode ? (mouseIn ? " block " : " hidden ") : "") +
(context?.autoplay
? `${mouseIn ? " flex " : " hidden "}`
: " flex ") +
"items-center justify-center w-8 h-8 bg-black rounded-md bg-opacity-20 hover:bg-opacity-40 "
}
>
<div className="">
{videoPlaying ? (
<BiPause className="flex-none w-6 h-6 " />
) : (
<BiPlay className="flex-none w-6 h-6 ml-0.5" />
)}
</div>
</button>
<div className={" text-sm " + (mouseIn ? " block " : " hidden ")}>
{secondsToHMS(currentTime) + "/" + secondsToHMS(videoDuration)}
</div>
</div>
{hasAudio && (
// vol positioner
<div
className="relative ml-auto"
onMouseEnter={() => setShowVolSlider(true)}
onMouseLeave={() => setShowVolSlider(false)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<div
//vol container
className="absolute bottom-0 left-0 w-full h-[170px] bg-transparent"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<div
//slider container
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onMouseDown={(e) => setVolMouseDown(true)}
onMouseUp={(e) => setVolMouseDown(false)}
onMouseLeave={() => setVolMouseDown(false)}
className={
"relative bottom-0 left-0 justify-center w-full h-32 bg-black bg-opacity-40 rounded-md " +
(showVolSlider ? " hidden md:flex " : " hidden ")
}
>
<div
//slide range
className="absolute bottom-0 flex justify-center w-full mb-2 rounded-full h-28"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<div
//range controls
className="absolute z-30 w-full h-full"
ref={volControls}
onClick={(e) => updateVolume(e)}
onMouseMove={(e) => {
updateVolumeDrag(e);
}}
></div>
<div className="absolute bottom-0 w-2 h-full">
<div
//control circle
className="absolute z-20 w-4 h-4 bg-white border rounded-full -left-1 "
style={
muted || volume === 0
? { bottom: 0 }
: {
bottom: `${(volume ?? 0.5) * 100 - 10}%`,
}
}
onMouseDown={() => setVolMouseDown(true)}
onMouseUp={() => setVolMouseDown(false)}
></div>
<div
//vol container
className="absolute bottom-0 z-10 w-full origin-bottom rounded-full bg-th-scrollbar"
style={{
height: `${muted ? 0 : (volume ?? 0.5) * 100}%`,
}}
></div>
<div className="absolute bottom-0 z-0 w-full h-full rounded-full bg-white/10"></div>
</div>
</div>
</div>
</div>
<button
aria-label="toggle mute"
//mute unmute button
onClick={(e) => audioControl(e, true)}
onMouseEnter={() => setShowVol(true)}
onMouseLeave={() => setShowVol(false)}
className={
(fullMediaMode || uniformMediaMode
? " hidden md:flex"
: " flex ") +
" w-8 h-8 relative items-center justify-center ml-auto bg-black rounded-md bg-opacity-20 hover:bg-opacity-40"
}
>
{muted ? (
<BiVolumeMute className="flex-none w-4 h-4" />
) : (
<BiVolumeFull className="flex-none w-4 h-4 " />
)}
</button>
</div>
)}
<div
// Progress Bar Container
id={"progressBarConainer"}
ref={seekRef}
className={
"absolute bottom-0 left-0 z-10 w-full h-5 " +
(mouseIn || fullMediaMode ? " block " : " hidden ")
}
>
{seekTime !== "" && (
<div
className="absolute flex items-center justify-center w-12 py-1 text-sm transition-transform bg-black rounded-lg bg-opacity-40 bottom-4 border-th-border"
style={{
left: `${
seekLeftOfset <= 24
? 0
: seekLeftOfset >= seekTargetLength - 24
? seekTargetLength - 48
: seekLeftOfset - 24
}px`,
}}
>
{seekTime}
</div>
)}
<div
//video duration
className="absolute bottom-0 left-0 h-1 origin-left bg-th-scrollbar "
style={{ width: `${progressPerc * 100}%` }}
></div>
<div
className="absolute left-0 w-full h-full "
onMouseMove={(e) => showSeek(e)}
onMouseLeave={() => setSeekTime("")}
onClick={(e: any) => {
updateSeek(e);
}}
></div>
</div>
</div>
{/* Video */}
<div
className={
uniformMediaMode
? "min-h-full min-w-full object-cover object-center "
: "relative overflow-hidden flex object-fill"
}
style={
uniformMediaMode
? { height: `100%` }
: containerDims?.[1]
? { height: `${containerDims[1]}px` }
: heightStyle
}
>
<div
//high res placeholder image
className={
` ` +
`${
!videoLoaded
? " opacity-100 "
: containerDims?.[1]
? " hidden "
: " opacity-0 "
}` +
(!videoLoaded && containerDims?.[1]
? " absolute top-1/2 -translate-y-1/2 "
: "")
}
>
<Image
className={
(!containerDims?.[1] ? "absolute bottom-0 left-0" : " ") +
(placeholder?.url?.includes("http") ? " " : " blur-2xl ")
} //!postMode ?
src={
placeholder?.url?.includes("http")
? placeholder.url
: thumbnail.url
}
height={vidHeight}
width={vidWidth}
layout={uniformMediaMode ? "fill" : undefined}
objectFit={uniformMediaMode ? "cover" : undefined}
alt="placeholder"
unoptimized={true}
priority={imgFull}
onError={() => {
setUseFallback(true);
}}
draggable={false}
/>
</div>
<video
ref={video}
className={
(videoLoaded ? "opacity-100 " : " opacity-0") +
(!containerDims?.[1] ? " absolute top-0 left-0 " : " ") +
(uniformMediaMode
? " min-w-full min-h-full max-w-full max-h-full m-auto block absolute inset-0 w-0 h-0 "
: "absolute left-1/2 transform -translate-x-1/2")
}
width={`${vidWidth}`}
height={`${vidHeight}`}
muted
loop={true}
preload={context?.autoplay || postMode ? "auto" : "none"}
onWaiting={() => {
if (!muted) setPrevMuted(false);
//pauseAll();
setBuffering(true);
setVideoPlaying(false);
!vodInfo.hasAudio && pauseAudio();
}}
onPlaying={() => {
setBuffering(false);
setVideoPlaying(true);
!vodInfo.hasAudio && playAudio();
}}
onPause={() => {
setVideoPlaying(false);
}}
controls={false}
// onLoadedData={onLoadedData}
onProgress={onLoadedData}
playsInline
onCanPlay={() => {
vodInfo.hasAudio && onAudioLoaded();
}}
draggable={false}
>
<source data-src={vodInfo.url} src={vodInfo.url} type="video/mp4" />
</video>
{/* if video doesn't have its own audio (v.reddits) */}
{!videoInfo?.hasAudio && audio && (
<video
controls={false}
ref={!videoInfo?.hasAudio && audioRef}
muted={!muted}
loop={false}
className="hidden"
playsInline
onCanPlay={onAudioLoaded}
onPlaying={() => setAudioPlaying(true)}
onPause={() => setAudioPlaying(false)}
onWaiting={() => setAudioPlaying(false)}
onError={(err) => null}
>
<source data-src={audio} src={audio} type="video/mp4" />
</video>
)}
</div>
</div>
);
};
export default VideoHandler;
Thanks for the suggestion. This works but introduces other issues (videos autoplay when autoplay is not toggled on). I'm going to be rewriting this component to support a HLS player for better cross browser support and do away with the current 'hack' for reddit audio support since this does not work well on iOS either.
Troddit autoplay functionality it's not working in iphone safari