(Bug) Autoplay don't working in iphone
st4l1nR opened this issue · 2 comments
st4l1nR commented
Troddit autoplay functionality it's not working in iphone safari
st4l1nR commented
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;
burhan-syed commented
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.