Play video smoothly with setInterval in React - javascript

I want to play a video with the setInterval function which should set the currentTime every Frame to play the video at the usual speed. My video has 25 frames per second. And to make it smoother I thought of using requestAnimationFrame, as without it it's lagging a bit. But I'm lost in the React useEffect Architecture on how to unmount all different life cycles in the right way. So how to simultaneously use setInterval with requestAnimationFrame to set the currentTime correctly.
The comments show
import React, { useState, useEffect, useRef } from "react";
export default function VideoInterval() {
const videoRef = useRef(null);
const [currentFrame, setCurrentFrame] = useState(0);
useEffect(() => {
// Set Video Framerate
const framerate = 25;
// Calculate interval duration for fluid playback
const intervalDuration = 1000 / framerate;
const interval = setInterval(() => {
// Not sure if it's a bad idea to store it in a let variable or this will cause problems
// let currentTime = videoRef.current.currentTime;
// Use requestAnimationFrame for smooth playback
const intervalPlay = () => {
videoRef.current.currentTime =
videoRef.current.currentTime + intervalDuration / 1000; // add interval in seconds
// window.requestAnimationFrame(intervalPlay);
};
window.requestAnimationFrame(intervalPlay);
// Tried it over useState to see if working with refs made it slower that useState
// setCurrentFrame((i) => i + intervalDuration / 1000);
// Not sure where or if to cancel the AnimationFrame
// cancelAnimationFrame(videoRef.current.currentTime);
}, intervalDuration); // every 40 milliseconds == 25 frames per second
return () => {
clearInterval(interval);
};
}, []);
return (
<>
<video
ref={videoRef}
// autoPlay
muted
loop
>
<source src="video_with_25_frames_per_second.mp4" type="video/mp4" />
</video>
</>
);
}

Related

React's useEffect unstable delay

I'm working on a Pomodoro clock. To build the countdowns I'm using useEffect and setTimeout. Everything seemed to be fine until I realized there's a 30ms to 50ms delay between every second of the clock. How would I set it to pricesily update the clock at every 1000ms?
I'm using useState to handle the clock's time and controls to stop, pause and reset. They're all working properly. It's just the second's timing that is delaying more that it should.
function App() {
const [workTarget, setWorkTarget] = useState(25 * 60);
const [breakTarget, setBreakTarget] = useState(5 * 60);
const [time, setTime] = useState(workTarget); //time in seconds
const [counting, setCounting] = useState(false);
const [working, setWorking] = useState(true);
const [clockTarget, setClockTarget] = useState(workTarget);
const [combo, setCombo] = useState(0);
const [config, setConfig] = useState(false);
const [playWork] = useSound(workSfx);
const [playBreak] = useSound(breakSfx);
const [playPause] = useSound(pauseSfx);
let tick = 1000;
let timeout;
let timenow = Date.now();
// Handle pause and stop of countdown
useEffect(() => {
if (time > 0 && counting === true) {
timeout = setTimeout(() => {
setTime(time - 1);
console.log(timenow);
}, tick);
} else if (time === 0 && counting === true) {
setWorking(!working);
if (working === true) {
playBreak();
setTime(breakTarget);
setClockTarget(breakTarget);
} else {
playWork();
setCombo(combo + 1);
setTime(workTarget);
setClockTarget(workTarget);
}
}
if (!counting || config) {
clearTimeout(timeout);
}
});
}
export default App;
This is not the complete code. I cut off other components for buttons and stuff that don't relate to this.
We can't ensure its EXACTLY 1000 ms. Behind the scenes, react state updates use setTimeout, and to change the timer, you need to use either setTimeout or setInterval. setTimeout and setInterval only ensure that the code inside will not run for a delay period, and then executes when the main thread is not busy.
Therefore, it's impossible to ensure that every update is EXACTLY 1000ms. There will usually be 30-50ms delay.
However, that doesn't mean your timer will be inaccurate or unreliable. It just depends how you initialize it.
Below is how I would improve the code you provided, as right now clearing the timeout adds some extra overhead, and batching the state updates would lead to improved performance in this case.
let tick = 1000;
//countdown modifying behavior every 1000ms. Active clock
useEffect(() => {
if (time > 0 && counting === true) {
setTimeout(() => {
setTime((state) => state - 1);
console.log(timenow);
}, tick);
}
}, [time, counting]);
//pausing, stopping and resuming
useEffect(() => {
if (time === 0 && counting === true) {
setWorking(!working);
if (working === true) {
playBreak();
unstable_batchedUpdates(() =>{
setTime(breakTarget);
setClockTarget(breakTarget);
}
} else {
playWork();
//update all at once
unstable_batchedUpdates(() =>{
setCombo(combo + 1);
setTime(workTarget);
setClockTarget(workTarget);
}
}
}
}, [time, counting]);
Further improvements to keep the clock in sync
On component mount, grab the current time for start time, and calculate the end time and store both
At every setTimeout in your useEffect, using your stored start time and current time, calculate how much time there is left and set state to that.
//countdown time modifying behavior every 1000ms
const current = new Date()
const [start, setStart] = useState(current)
//this example uses a 25 minute timer as an example
const [end, setEnd] = useState(new Date(start.getTime() + 25 * 60000))
const tick = 1000
useEffect(() => {
if (time > 0 && counting === true) {
setTimeout(() => {
const current = new Date()
const diffTime = current.getDate() - end.getDate()
const timeRemainingInMins = Math.ceil(diffTime / (1000*60));
setTime(timeRemainingInMins);
}, tick);
}
}, [time, counting]);
I may have an idea.
Currently your useEffect hook is set up to run on every render which means every time you change the Time you render the page and rerun the useEffect hook, however you seem to have quite a bit of logic inside the hook, plus react may not render consistently which means that there may be some inconsistencies with your timing.
I suggest using setInterval instead and running the useEffect hook only once at the start to set up the interval using useEffect(() => {...}, []) (notice [] as the second argument)
For the rest of your logic you can always create another useEffect hook that updates when the components change.
With all that in mind your timing logic should look something like this:
useEffect(() => {
interval = setInterval(() => {
setTime(time-1);
console.log(timenow);
}, tick); //runs once every tick
return(() => clearInterval(interval)); //Once the component unmounts run some code to clear the interval
}, [])
Now in another useEffect hook you can watch for changes to time and update the rest of your app accordingly.
This approach should be much more reliable because it doesn't depend on react to update the time.
#The_solution
const component =() => {
const [val, setval]= useState(globalState.get());
useEffect(() => {
setVal(globalState.get());
globalState.subscribe(newVal=> setVal(newVal));
});
return <span>{val}</span>
}

When I run AudioBufferSourceNode.start() when I have multiple tracks, I sometimes get a delay

I am making an application that reads and plays two audio files.
CodeSnadBox
The above CodeSandBox has the following specifications.
Press the "play" button to play the audio.
The volume of each of the two audio tracks can be changed.
Problem
When playing audio, there is sometimes a delay.
However, there is not always an audio delay, and there are times when two tracks can be played back at exactly the same time.
Although not implemented in the CodeSandBox above, the application I am currently working on implements a seek bar to indicate the current playback position.
By moving the seek bar to indicate the current playback position, the audio is reloaded and the resulting delay may be cured.
On the other hand, moving the seek bar may cause a delay even though the audio was playing at exactly the same timing.
Anyway, is there a way to play multiple audio tracks at the same time in a stable and consistent manner?
Code
let ctx,
tr1,
tr2,
tr1gain = 0,
tr2gain = 0,
start = false;
const trackList = ["track1", "track2"];
const App = () => {
useEffect(() => {
ctx = new AudioContext();
tr1 = ctx.createBufferSource();
tr2 = ctx.createBufferSource();
tr1gain = ctx.createGain();
tr2gain = ctx.createGain();
trackList.forEach(async (item) => {
const res = await fetch("/" + item + ".mp3");
const arrayBuffer = await res.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
item === "track1"
? (tr1.buffer = audioBuffer)
: (tr2.buffer = audioBuffer);
});
tr1.connect(tr1gain);
tr1gain.connect(ctx.destination);
tr2.connect(tr2gain);
tr2gain.connect(ctx.destination);
return () => ctx.close();
}, []);
const [playing, setPlaying] = useState(false);
const playAudio = () => {
if (!start) {
tr1.start();
tr2.start();
start = true;
}
ctx.resume();
setPlaying(true);
};
const pauseAudio = () => {
ctx.suspend();
setPlaying(false);
};
const changeVolume = (e) => {
const target = e.target.ariaLabel;
target === "track1"
? (tr1gain.gain.value = e.target.value)
: (tr2gain.gain.value = e.target.value);
};
const Inputs = trackList.map((item, index) => (
<div key={index}>
<span>{item}</span>
<input
type="range"
onChange={changeVolume}
step="any"
max="1"
aria-label={item}
/>
</div>
));
return (
<>
<button
onClick={playing ? pauseAudio : playAudio}
style={{ display: "block" }}
>
{playing ? "pause" : "play"}
</button>
{Inputs}
</>
);
};
When calling start() without a parameter it's the same as calling start with currentTime of the AudioContext as the first parameter. In your example that would look like this:
tr1.start(tr1.context.currentTime);
tr2.start(tr2.context.currentTime);
By definition the currentTime of an AudioContext increases over time. It's totally possible that this happens between the two calls. Therefore a first attempt to fix the problem could be to make sure both function calls use the same value.
const currentTime = tr1.context.currentTime;
tr1.start(currentTime);
tr2.start(currentTime);
Since currentTime usually increases by the time of a render quantum you could add an extra safety net by adding a little delay.
const currentTime = tr1.context.currentTime + 128 / tr1.context.sampleRate;
tr1.start(currentTime);
tr2.start(currentTime);
If this doesn't help you could also use an OfflineAudioContext to render your mix upfront into a single AudioBuffer.

Is it possible to keep setInterval on multiple devices 'synced' by compensating for drift, or do I need server synchronisation?

I am building a React Native app where my entire back end is provided for by services like Firebase etc.
The app requires clocks on multiple devices to start and end at the same time which can run for up to an hour.
Given a shared starting point in time between devices I have observed drift in the accuracy of setInterval in this 20 seconds of data:
I am attempting to compensate for this deviation in clock timing by measuring it and then compensating for it - here is a code sandbox with my solution.
useTimer hook:
import { useState, useEffect, useRef } from "react";
import moment from "moment";
export const convertMsToMinsAndSecs = (countDown) => {
const seconds = moment
.duration(countDown)
.seconds()
.toString()
.padStart(2, "0");
const minutes = moment
.duration(countDown)
.minutes()
.toString()
.padStart(2, "0");
const minsAndSecs = `${minutes.toString()}:${seconds.toString()}`;
return countDown > 0 ? minsAndSecs : "00:00";
};
const roundTimeStamp = (timeStamp) =>
timeStamp === 0 ? 0 : timeStamp + (1000 - (timeStamp % 1000));
export const useTimer = (
started,
startTime,
length,
resetClock,
clockIntialState
) => {
const initialTimerState = {
start: 0,
end: 0,
timeNow: 0,
remaining: length,
clock: convertMsToMinsAndSecs(length),
internalClockDeviation: 0
};
const [timeData, setTimeData] = useState(initialTimerState);
const intervalId = useRef(null);
const deviation = useRef(null);
useEffect(() => {
setTimeData((prevState) => ({
...prevState,
start: roundTimeStamp(startTime),
end: roundTimeStamp(startTime) + length
}));
if (started) {
intervalId.current = setInterval(() => {
const intervalTime = moment().valueOf();
setTimeData((prevState) => {
return {
...prevState,
timeNow: intervalTime,
remaining: prevState.remaining - 1000,
clock: convertMsToMinsAndSecs(prevState.remaining - 1000),
internalClockDeviation:
prevState.timeNow === 0
? 0
: intervalTime - prevState.timeNow - 1000
};
});
}, 1000 - deviation.current);
}
}, [started]);
useEffect(() => {
deviation.current = timeData.internalClockDeviation;
}, [timeData.internalClockDeviation]);
if (timeData.remaining <= 0 && started) {
resetClock(clockIntialState);
clearTimeout(intervalId.current);
setTimeData(initialTimerState);
}
const compensatedLength = 1000 - deviation.current;
return {
timeData,
intervalId,
compensatedLength,
setTimeData,
initialTimerState
};
};
As I am not running my own server application I would prefer to handle this on the client side if possible. It also means that I do not need to rely on network connections or the availability of a timing server.
Will my approach work across multiple devices, and if so can it be improved, or do I need to build a server side application to effectively handle this? TIA.
When you determine time diff you can not rely on intervals being accurate. Gets worse when tab is in background/not in focus.
Typically you rely on timestamps to get the offset in time, you do not subtract a fix number.
function countDown(totalTime, onComplete, onUpdate, delay = 1000) {
let timer;
const startTime = new Date().getTime();
function next() {
const runningTime = new Date().getTime() - startTime;
let remaining = Math.max(totalTime - runningTime, 0);
onUpdate && onUpdate(remaining);
!remaining && onComplete && onComplete();
var ms = Math.min(delay, remaining);
timer = remaining && window.setTimeout(next, ms);
}
next()
return function () {
timer && window.clearTimeout(timer);
}
}
countDown(5000, function(){ console.log('done1'); }, function(x){ console.log('update1 ', x); });
const out = document.getElementById("out");
const cd = countDown(
60000,
function(){ out.textContent = 'done'; },
function(x){ out.textContent = (x/1000).toFixed(3); },
20
);
document.getElementById("btn").addEventListener('click', cd);
<div id="out"></div>
<button id="btn">stop</button>
This will fail if user changes clock, not much you can do on that. You could ping the server for time, but that also has latency with how long the call takes.
The only way you can synchronize all the clocks together is having all of them observe one single source of truth.
If you create the timestamp on one client. You need to make sure all the other clients are in sync with that data.
What you can do is have a server + client architecture where the server is the single point of truth. But if you are trying to completely synchronize without a single point of truth, you are doomed to fail because of the problems that you can not control like latency of communication between all the client applications in your case clock.

Slick slider - pause on hover and play remaining seconds on mouse leave

In Slick slider i'm using the below configuration
https://kenwheeler.github.io/slick/
autoplay: true,
autoplaySpeed: 6000,
pauseOnHover: true,
pauseOnFocus: true,
pauseOnDotsHover: true,
Here, when we hover on the banner its pausing and when mouse leave again the timer is running 6000ms again and moving to next slide.
I need like,
if we mouse hover on 3rd sec and keep some more seconds and leave then,
the remaining 3 seconds only should stop and play the next slide.
How to achieve this in slick slider please.
Finally done in React slick slide
We need to consider the mouse in and out time and calculate the remaining time (even in multiple time if the user did the mouse in and out)
In slick slide there is a option, afterChange and init
init: () => {
setSliderInitialized(true)
addTimeStamp()
},
afterChange: index => {
setCurrentSlide(index)
setAutoplaySpeed(parseInt(props.timer)) // props.timer is pause time which is from JSON
//setAutoplaySpeed(6000) - 6 sec
setTimeStamps([new Date()])
},
and have to consider the mouse in and out time, for this from React can import useEffect
import React, { useEffect, useRef, useState } from "react"
const slider = useRef()
const [hovered, setHovered] = useState(false)
const previousHovered = usePrevious(hovered)
const [currentSlide, setCurrentSlide] = useState(-1)
const [autoplaySpeed, setAutoplaySpeed] = useState(parseInt(props.timer))
const [timeStamps, setTimeStamps] = useState([])
const [sliderInitialized, setSliderInitialized] = useState(false)
const addTimeStamp = () => setTimeStamps([...timeStamps, new Date()])
need to use the time stamp
const getElapsedTime = () => {
let elapsedTime = 0
for (let i = 0; i < timeStamps.length; i += 2) {
const start = timeStamps[i]
const stop = timeStamps[i + 1]
elapsedTime += stop - start
}
return elapsedTime
}
useEffect(() => {
if (previousHovered === false && hovered === true) {
addTimeStamp()
}
if (previousHovered === true && hovered === false) {
addTimeStamp()
const elapsedTime = getElapsedTime()
let remainingTime = parseInt(props.timer) - elapsedTime // props.timer - 6000ms
setAutoplaySpeed(remainingTime ? remainingTime : parseInt(props.timer))
}
}, [hovered])
When the slider initialized (init) and changed afterChange - pass the timer.
When mouse hover store the current time and and mouse out store the time in **array**
By iterating the array and subtract we can get the elapsedTime. And we need to change the pause time from 6000ms to elapsed Time. and when slider change again we need to set the 6000ms.

Why does my setting of the audio currentTime always emit audio ended event?

I am setting an Audio element's currentTime = 0, but it always emit the audio ended event, and currentTime is always equal to the duration.
audio.addEventListener('loadedmetadata', (e) => {
const that = this;
const audio = this.oAudio;
const duration = audio.duration;
if (duration === Infinity) {
audio.currentTime = 1e101;
audio.ontimeupdate = function() {
audio.ontimeupdate = () => {
};
audio.currentTime = 0;
that.duration();
};
}
}, false);
Because your Media lasts less than 1e+101 seconds.
I'm not entirely sure what it is you are trying to do here, nor why it is a problem that this ended event fires, but it sounds like you are trying to apply this workaround to get the correct duration of some medias.
If it is the case, then you would have to wait until you received that duration before attaching the ended event (and even before doing anything else with that MediaElement).
as pseudo-code that would be
// getMediaDuration is an asynchronous task
const duration = await getMediaDuration(audio);
// now that the async part is done we can add our listeners
audio.addEventListener('ended', dosomething);

Categories