How do I fast-forward the audio in my react component? - javascript

I have a react component that display an audio player. I need to implement a feature where you can go back and forward in the audio 15 seconds. I have a function called skip that does this but it only updates the time and is not moving the audio track. What I am doing wrong? Here is an image of my audio player
import React, { useState, useEffect, useRef } from "react";
import "./AudioPlayer.css";
import Navbar from "../../components/Navbar/Navbar";
import { useParams } from "react-router-dom";
import axios from "axios";
import Slider from "../../components/Slider/Slider";
import ControlPanel from "../../components/Controls/ControlPanel";
import * as RiIcons from "react-icons/ri";
function AudioPlayer() {
const [episodeData, setEpisodeData] = useState([]);
const [percentage, setPercentage] = useState();
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState();
const [speed, setSpeed] = useState(1);
const audioRef = useRef();
const onChange = (e) => {
const audio = audioRef.current;
audio.currentTime = (audio.duration / 100) * e.target.value;
setPercentage(e.target.value);
};
const play = () => {
const audio = audioRef.current;
// audio.playbackRate = speed;
audio.volume = 0.1;
if (!isPlaying) {
setIsPlaying(true);
audio.play();
}
if (isPlaying) {
setIsPlaying(false);
audio.pause();
}
};
const getCurrDuration = (e) => {
const percent = (
(e.currentTarget.currentTime / e.currentTarget.duration) *
100
).toFixed(2);
const time = e.currentTarget.currentTime;
setPercentage(+percent);
setCurrentTime(time.toFixed(2));
};
const changeSpeed = () => {
if (speed >= 2) {
setSpeed(0.5);
} else setSpeed(speed + 0.5);
};
const skip = (time) => {
const audio = audioRef.current;
if (time == "back") {
console.log("15");
setCurrentTime(audio.currentTime - 15);
} else if (time == "fwd") {
console.log("15");
setCurrentTime(audio.currentTime + 15);
}
};
const { id } = useParams();
const headers = { jwt_token: localStorage.token };
useEffect(() => {
axios
.get(`/api/get/episodes/${id}`, { headers })
.then((res) => setEpisodeData(res.data));
}, []);
useEffect(() => {
const audio = audioRef.current;
audio.playbackRate = speed;
}, [speed]);
return (
<div>
<Navbar />
<div>
<div style={{ width: "60%", margin: "0 auto", paddingTop: "10rem" }}>
<div className="app-container">
<h3 style={{ color: "#fff" }}>{episodeData.podcast_title}</h3>
<h3 style={{ color: "#fff" }}>{episodeData.episode_title}</h3>
<Slider percentage={percentage} onChange={onChange} />
<audio
ref={audioRef}
onTimeUpdate={getCurrDuration}
onLoadedData={(e) => {
setDuration(e.currentTarget.duration.toFixed(2));
}}
src={episodeData.episode_audio}
></audio>
<ControlPanel
play={play}
isPlaying={isPlaying}
duration={duration}
currentTime={currentTime}
/>
<button className="speed-button" onClick={() => changeSpeed()}>
{speed}x
</button>
<button onClick={() => skip("back")}>
BACK 15 SECONDS
<RiIcons.RiArrowGoBackLine color={"white"} size={16} />
</button>
<button onClick={() => skip("fwd")}>
<RiIcons.RiArrowGoForwardLine color={"white"} size={16} />
FORWARD 15 SECONDS
</button>
</div>
</div>
</div>
</div>
);
}
export default AudioPlayer;

The problem with your code is, you're trying to change the state of component where as you should be changing the current time of the audio player.
The currentTime property sets or returns the current position (in seconds) of the audio/video playback.
When setting this property, the playback will jump to the specified position.
const skip = (time) => {
const audio = audioRef.current;
if (time == 'back') {
// changes
audio.currentTime = audio.currentTime - 15;
} else if (time == 'fwd') {
// changes
audio.currentTime = audio.currentTime + 15;
}
};
By changing the current time of the audio player using ref the audio player jumps to the specified position, so your onchange function will also be called.

Related

Typescript error : "audioPlayer.current may be null" [duplicate]

This question already has answers here:
Typescript, how to pass "Object is possibly null" error?
(11 answers)
Closed last month.
I'm trying to learn Typescript and for that I came back on some components to convert them into an errorless Typescript file.
I have here a bunch of errors that can't find the cause.I trying different options, but can't figure it out.
The first one is on refs. When I use theme with ref.current, I receive the error
"may be null"
Here is the component code :
import React, { useState, useRef, useEffect } from "react";
const AudioPlayer = () => {
// state
const [isPlaying, setIsPLaying] = useState(false);
const [duration, setDuration] = useState(0);
const [currentTime, setCurrentTime] = useState(0);
const [currentTrack, setCurrentTrack] = useState(null);
// references
let audioPlayer = useRef(null); //reference to our audioplayer
let progressBar = useRef(); //reference to our progressBar
let animationRef = useRef(); //reference the animation
The ref audioPlayer is declared here and used below to get the duration of the track
useEffect(() => {
const seconds = Math.floor(audioPlayer.current.duration);
console.log(audioPlayer);
setDuration(seconds);
progressBar.current.max = seconds;
}, [
audioPlayer?.current?.onloadedmetadata,
audioPlayer?.current?.readyState,
]);
const togglePlayPause = () => {
const prevValue = isPlaying;
setIsPLaying(!prevValue);
if (!prevValue) {
audioPlayer.current.play();
animationRef.current = requestAnimationFrame(whilePlaying);
} else {
audioPlayer.current.pause();
cancelAnimationFrame(animationRef.current);
}
};
const whilePlaying = () => {
progressBar.current.value = audioPlayer.current.currentTime;
setCurrentTime(progressBar.current.value);
animationRef.current = requestAnimationFrame(whilePlaying);
};
const calculateTime = (secs) => {
const minutes = Math.floor(secs / 60);
const returnedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(secs % 60);
const returnedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${returnedMinutes} : ${returnedSeconds}`;
};
const changeRange = () => {
audioPlayer.current.currentTime = progressBar.current.value;
setCurrentTime(progressBar.current.value);
};
const changeTrack = (e) => {
setCurrentTrack(e.target.value);
console.log(e.target.value);
togglePlayPause();
};
return (
<>
<div className="relative flex justify-center my-10 mx-4">
<img src="/sphere_3D.jpg" alt="sph" width="600" />
<p className="huit absolute">8</p>
<input
className="dots top-40"
value="/piste1.mp3"
onClick={(e) => changeTrack(e)}
></input>
<input
className="dots top-20 left-2/3"
value="/piste2.mp3"
onClick={(e) => changeTrack(e)}
></input>
</div>
<div>
<audio
ref={audioPlayer}
src={currentTrack}
preload="metadata"
onCanPlay={(e) => e.target.play()}
></audio>
<button className="mx-5" onClick={togglePlayPause}>
{isPlaying ? "Pause" : "Play"}
</button>
{/* Current time */}
<div>{calculateTime(currentTime)}</div>
{/* progress bar */}
<div>
<input
type="range"
defaultValue="0"
ref={progressBar}
onChange={changeRange}
/>
</div>
{/* duration */}
<div>{duration && !isNaN(duration) && calculateTime(duration)}</div>
</div>
</>
);
};
export default AudioPlayer;
Add a type to the ref
const audioElem = useRef<HTMLAudioElement>(null);
make sure you check for a null value before using current:
if (audioElem.current !== null) {
audioElem.current.focus();
}

How to use the latest value of the state inside a useEffect?

My component renders an image every 4 seconds. When I click on the image I want to stop rendering new images. For that I've used a useEffect hook. When I click to the image, the state hasToRefresh changes it's values, but inside useEffect it doesn't change. This is my code:
import { useEffect, useState } from "react";
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const imageRefresh = "?forcerefresh=";
const [image, setImage] = useState(imageUrl);
const [hasToRefresh, setHasToRefresh] = useState(true);
useEffect(() => {
if (hasToRefresh) {
setInterval(() => {
setImage(imageUrl + imageRefresh + Math.random());
}, 4000);
}
}, [imageUrl, imageRefresh, hasToRefresh]);
return (
<>
<img
src={image}
onClick={() => setHasToRefresh(!hasToRefresh)}
alt="scenery"
height="200"
width="200"
/>
</>
);
};
export default VariableImage;
Also in sandbox: https://codesandbox.io/s/variable-image-zxhejs
How can I do for when I click the image to not render more images?
If anyone could help me I would be very grateful. Thanks.
You're never stopping your interval. And to only trigger the useEffect() for hasToRefresh, I would move the creation of image string outside of it.
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const imageRefresh = "?forcerefresh=";
const [imageNumber, setImageNumber] = useState(Math.random());
const image = imageUrl + imageRefresh + imageNumber;
const [hasToRefresh, setHasToRefresh] = useState(true);
const intervalRef = useRef();
useEffect(() => {
if (hasToRefresh) {
intervalRef.current = setInterval(() => {
setImageNumber(Math.random());
}, 1000);
}
return () => {
intervalRef.current && clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [hasToRefresh]);
return (
<>
<img
src={image}
onClick={() => setHasToRefresh(!hasToRefresh)}
alt="scenery"
height="200"
width="200"
/>
</>
);
};
Here's the updated codesandbox: https://codesandbox.io/s/variable-image-forked-oxfgc9?file=/src/VariableImage/VariableImage.js:54-896
As Roy Schut mentioned, you never stop your timer. But the best option would be here to stop the timer when the image shouldn't be refreshed. Here's the code I would prefer.
import { useEffect, useState, useRef } from "react";
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const imageRefresh = "?forcerefresh=";
const [image, setImage] = useState(imageUrl);
const [hasToRefresh, setHasToRefresh] = useState(true);
const intervalRef = useRef(null);
useEffect(() => {
startTimer();
return () => stopTimer();
}, []);
const startTimer = () => {
intervalRef.current = setInterval(() => {
setImage(imageUrl + imageRefresh + Math.random());
}, 4000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
const toggleRefresh = () => {
if (hasToRefresh) {
stopTimer();
} else {
startTimer();
}
setHasToRefresh(state => !state);
};
return (
<>
<img
src={image}
onClick={() => toggleRefresh()}
alt="scenery"
height="200"
width="200"
/>
</>
);
};
export default VariableImage;

How to change image and background color every x seconds. React

Desired outcome = change image and bg color every x seconds.
Problem I'm running into = it is not displaying every color and image. It is going from pink -> orange -> pink -> orange, skipping blue and green.
import * as React from 'react';
// Images
import heroPink from './hero-pink.png';
import heroBlue from './hero-blue.png';
import heroOrange from './hero-orange.png';
import heroGreen from './hero-green.png';
import logo from './oddballs_logo.svg';
const colors = [
"#F9B199",
"#237E95",
"#D79446",
"#C2C138"
];
const images = [
heroPink,
heroBlue,
heroOrange,
heroGreen
];
export default function App() {
const [value, setValue] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setValue((v) => (v === 3 ? 0 : v + 1));
}, 1000);
}, []);
return (
<div className="App" style={{ backgroundColor: colors[value] }}>
<img src={images[value]}/>
</div>
);
}
You need to clear your interval with the return statement. That means, in every un-mount the interval will clear and when this component will mount, new interval will register. It works fine for me, hope your problem will solve also.
React.useEffect(() => {
const interval = setInterval(() => {
setValue((v) => {
return v === 4 ? 0 : v + 1;
});
}, 1000);
return () => clearInterval(interval);
}, []);
Remember, clearing any interval events is important. Otherwise it
occurs memory leak.
Here is the full example:
import React from "react";
const colors = ["#92c952", "#771796", "#24f355", "#d32776", "#f66b97"];
const images = [
"https://via.placeholder.com/150/92c952",
"https://via.placeholder.com/150/771796",
"https://via.placeholder.com/150/24f355",
"https://via.placeholder.com/150/d32776",
"https://via.placeholder.com/150/f66b97"
];
export default function App() {
const [value, setValue] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setValue((v) => {
return v === 4 ? 0 : v + 1;
});
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className="App" style={{ backgroundColor: colors[value] }}>
<img src={images[value]} alt="img" style={{ border: "3px solid" }} />
</div>
);
}
You can use setInterval function to achieve the behavior. Here I have used two states for color and image indexes.
const colors = ["#eb4034", "#ebdb34", "#34eb37"];
const images = [
"https://images.unsplash.com/photo-1649894158666-c10421960f13?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2071&q=50",
"https://images.unsplash.com/photo-1648737154448-ccf0cafae1c2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80",
"https://images.unsplash.com/photo-1585974738771-84483dd9f89f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1072&q=80"
];
export default function App() {
const [image, setImage] = useState(0);
const [color, setColor] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
if (image === images.length - 1 && color === colors.length - 1) {
setImage(0);
setColor(0);
} else {
setImage(image + 1);
setColor(color + 1);
}
}, 3000);
return () => {
clearInterval(interval);
};
}, [image, color]);
return (
<div style={{ backgroundColor: `${colors[color]}`, padding: "20px" }}>
<img src={images[image]} style={{ width: "100%" }} alt="img" />
</div>
);
}
Also cleanup the interval unmounting phase.

When clicking play again, it plays a new audio on top of the old audio

I am creating an audio player in React and I have the following code. It plays fine when you click on the play button once but refuses to pause when you click pause. Also if you click play again, instead of just continuing the audio it creates new audio of the same song and plays it on top of the previous song. I'm not sure how to fix this.
const AudioPlayer = () => {
let audio = new Audio(song)
const [playing, setPlaying] = useState(false)
const playHandler = () => {
if(playing){
setPlaying(false)
console.log(playing)
}
else{
setPlaying(true)
}
}
const audioPause = () => {
audio.pause()
}
const audioPlay = () => {
audio.play()
}
useEffect (() => {
(playing) ? audioPlay() : audioPause()
}, [playing])
return(
<div className="songPlaying">
<p> { (playing) ? "Now playing" : "Stopped Playing" } </p>
<button type="submit" onClick={playHandler}>
{(playing) ? "Pause" : "Play" }
</button>
</div>
)
}
Instead creating a variable of audio, you can create a state of it, and update it accordingly.
Here is the sample code, this should work in your case:
...
// let audio = new Audio(song)
const [audio] = useState(new Audio(song));
...
In your case, your code is creating a new object every time, when you hit pause and play so this is the reason why it overlapping the audio instead of pause and play.
By creating a state, it remains the same object, so whenever you're updating the state of play and pause, it can use the same audio object and do actions on it, rather than creating a new one.
Here is the minified code of yours:
const AudioPlayer = () => {
const [audio] = useState(new Audio(song));
const [playing, setPlaying] = useState(false)
const toggle = () => setPlaying(!playing);
useEffect(() => {
playing ? audio.play() : audio.pause();
},
[playing]
);
return (
<div className="songPlaying">
<p> {(playing) ? "Now playing" : "Stopped Playing"} </p>
<button type="submit" onClick={toggle}>
{(playing) ? "Pause" : "Play"}
</button>
</div>
)
}
Read this answer for more info.
import React, { useState } from "react";
// Play icon
import voicemailplay from "../../assets/voicemail_play.png";
// Pause icon
import voicemailpause from "../../assets/fax_call_icon.png";
// Global declaration audio array
let audioArray = [];
// Global declaration audio array index
let audioIndex = '';
const VoicemailSider = (props) => {
// Manage flag
const [isPlaying, setIsPlaying] = useState(false);
// Play audio
const playAudio = (audioFilePath, index) => {
if(audioIndex !== '' && audioArray[audioIndex]) {
audioArray[audioIndex].pause();
}
setIsPlaying(
index
);
audioArray = [];
audioIndex = index;
try{
if(index !== '' && event !== '') {
audioArray[index] = new Audio(audioFilePath);
audioArray[index].play();
}
} catch(error) {
console.log("Could not paly this audio - ", error);
}
}
// Pause audio
const pauseAudio = (index) => {
setIsPlaying(
false
);
audioArray[index].pause();
}
return (
<div>
<List
dataSource={voicemaillist} // Your data that comes from databse
renderItem={(item,index) => (
<List.Item key={index}>
<img src={isPlaying === index ? voicemailpause : voicemailplay } alt="voicemail_play" onClick={() => isPlaying !== index ? playAudio(item.audioFilePath, index) : pauseAudio(index) } id={"voicemail_play" + index} style={{width: 35 ,height: 35, margin: "0px 10px"}} />
</List.Item>
)}
</List>
</div>
);
};
export default VoicemailSider;

React audio Cannot set property 'volume' of undefined

I am trying to play an mp3 using the Audio element but whenever the player renders an error occurs:- Cannot set property 'volume' of undefined.
Play prop is just a boolean value.
The useRef() current property shows me the mp3 file when I console.log it.
I removed the volume property but then it displays the same error for audio.play().
why is the audio undefined?
import React, { useState, useRef } from "react";
import "../../Static/player.css";
import Nowplaying from "./Nowplaying";
import SongInfo from "./SongInfo";
import Slider from "./Slider";
import Duration from "./Duration";
import song from "../../Static/assets/song.mp3";
const Player = (props) => {
const { Play } = props;
const [percentage, setPercentage] = useState(0)
const [duration, setDuration] = useState(0)
const [currentTime, setCurrentTime] = useState(0)
const audioRef = useRef()
const onChange = (e) => {
const audio = audioRef.current
audio.currentTime = (audio.duration / 100) * e.target.value
setPercentage(e.target.value)
}
const play = () => {
const audio = audioRef.current
audio.volume = 0.1
if (!Play) {
audio.play()
}
if (Play) {
audio.pause()
}
}
const getCurrDuration = (e) => {
const percent = ((e.currentTarget.currentTime / e.currentTarget.duration) * 100).toFixed(2)
const time = e.currentTarget.currentTime
setPercentage(+percent)
setCurrentTime(time.toFixed(2))
}
if (Play) {
play();
} else {
play();
}
return (
<div className="player-screen">
<div className="play-screen">
<div className="navbar">
<Nowplaying />
</div>
<div className="song-info">
<SongInfo />
</div>
<div className="player-controls">
<Slider percentage={percentage} onChange={onChange} />
<Duration
duration={duration}
currentTime={currentTime}
/>
<audio
ref={audioRef}
onTimeUpdate={getCurrDuration}
onLoadedData={(e) => {
setDuration(e.currentTarget.duration.toFixed(2));
}}
src={song}
></audio>
</div>
</div>
</div>
);
};
export default Player;
what wrong am I doing?
This part of code:
if (Play) {
play();
} else {
play();
}
gets immediately called, before React even has the chance to set audioRef.current to the Audio element. Your function hasn't even finished rendering yet, React doesn't even know where audioRef is used.
Move that piece of code into a useEffect, or better yet, replace your audioRef with a callback function (which can still store the Audio element in another ref or state variable), as shown here.

Categories