MediaSession Play/Pause button does not work in Chrome - reactjs

I'm building a media controller application that controls a media player running in a remote process, and trying to use the MediaSession API to facilitate media key control. An audio element that is nearly silent is used to establish the media session, and after a few seconds it is paused indefinitely.
This works well in Firefox, but in Chrome-based browsers (desktop and mobile) the Play/Pause button does not change state and ultimately stops working after a few seconds. The Next/Previous track buttons work as expected.
What do I need to do to make the Play/Pause media session controls work in Chrome-based browsers?
React app to reproduce the issue:
import "./styles.css";
import React from "react";
export default function App() {
return (
<div className="App">
<h1>MediaSession demo</h1>
<Player />
</div>
);
}
function Player() {
const playctl = usePlayer();
if (playctl.state === "stopped") {
return <button onClick={playctl.playPause}>Begin</button>;
}
return (
<>
<p>Use media session notification to control player state.</p>
<MediaSession playctl={playctl} />
<p>Player state: {playctl.state}</p>
<p>Track: {playctl.track}</p>
</>
);
}
function usePlayer() {
const [state, setState] = React.useState("stopped");
const [track, setTrack] = React.useState(1);
let playing = state === "playing";
return {
playPause: () => {
playing = !playing;
setState(playing ? "playing" : "paused");
},
nextTrack: () => {
setTrack(track < 5 ? track + 1 : 1);
},
prevTrack: () => {
setTrack(track > 1 ? track - 1 : 5);
},
state,
nextState: playing ? "Pause" : "Play",
playing,
track
};
}
const MediaSession = ({ playctl }) => {
const controls = useMediaControls();
React.useEffect(() => controls.update(playctl), [controls, playctl]);
return controls.audio;
};
function useMediaControls() {
const audiofile = require("./near-silence.mp3");
const hasSession = window.navigator && "mediaSession" in window.navigator;
const ref = React.useRef();
let shouldShow = true;
function showControls(audio) {
shouldShow = false;
audio.volume = 0.00001; // very low volume level
audio.play();
audio.currentTime = 0;
// pause before track ends so controls remain visible
setTimeout(() => audio.pause(), 5000);
}
function updateSession(playctl) {
const session = window.navigator.mediaSession;
session.playbackState = playctl.playing ? "playing" : "paused";
session.setActionHandler("pause", playctl.playPause);
session.setActionHandler("play", playctl.playPause);
session.setActionHandler("nexttrack", playctl.nextTrack);
session.setActionHandler("previoustrack", playctl.prevTrack);
}
function createApi() {
return {
audio: hasSession && <audio ref={ref} src={audiofile} />,
update: (playctl) => {
if (hasSession) {
const audio = ref.current;
shouldShow && audio && showControls(audio);
updateSession(playctl);
}
}
};
}
return React.useState(createApi)[0];
}
Code sandbox: https://codesandbox.io/s/mediasession-demo-r773i

Related

How to install audio in React?

I am using a package called npm i --save react-audio-player and I am trying to use to have music auto play when the page is loaded. At this time, I am able to get the audio player to display, so the package is installed properly, however, the music does not show up at all.
I have had my audio directory both inside, and outside of the the project directory and I have copied the path directly from vscode, but it does not show to be active. Im not exactly sure what could possibly be causing this error. At first I thought maybe it was just chrome blocking the autoplay, but that would not explain why the audio file is displaying at all.
Below is both my code and a photo of my files
import React, {useState, useEffect} from 'react'
import ReactAudioPlayer from 'react-audio-player'
const App = () => {
// ======================================
// HOOKS
// ======================================
const [score, setScore] = useState(0)
const [showEZBake, setShowEZBake] = useState(false)
const [showToasterOven, setShowToasterOven] = useState(false)
const [showConvectionOven, setShowConvectionOven] = useState(false)
const [showSmallFactory, setShowSmallFactory] = useState(false)
// const [counter, setCounter] = useState(score)
// ======================================
// FUNCTIONS
// ======================================
const winCondition = () => {
if (score >= 100000) {
return (
<h1>YOURE A WINNER!!</h1>
)
}
}
// EARN REVENUE FUNCTIONS
const earn1 = () => {
setScore(score + 1)
winCondition()
}
const earn5 = () => {
setScore(score + 5)
winCondition()
}
const earn25 = () => {
setScore(score + 25)
winCondition()
}
const earn50 = () => {
setScore(score + 50)
winCondition()
}
const earn250 = () => {
setScore(score + 250)
winCondition()
}
// PURCHASE ITEMS FUNCTIONS
const buyEZOven = () => {
setScore(score - 25)
}
const buyToasterOven = () => {
setScore(score - 250)
}
const buyConvectionOven = () => {
setScore(score - 1000)
}
const buySmallFactory = () => {
setScore(score - 15000)
}
// THIS IS AN EXAMPLE OF HOW TO FORCE TEXT ONTO A PAGE
// const reveal = () => {
// // If the score is greater than or equal to 5, return the <h1> element
// if (score >= 5) {
// return (
// <h1>TEST</h1>
// )
// } else {
// // Otherwise, return null
// return null
// }
// }
const upgradeEZOven = () => {
if (score >= 25) {
setShowEZBake(true)
buyEZOven()
}
}
const upgradeToasterOven = () => {
if (score >= 250 ) {
setShowToasterOven(true)
buyToasterOven()
}
}
const upgradeConvectionOven = () => {
if (score >= 1000) {
setShowConvectionOven(true)
buyConvectionOven()
}
}
const upgradeSmallFactory = () => {
if (score >= 15000) {
setShowSmallFactory(true)
buySmallFactory()
}
}
// useEffect(() => {
// const timer = setInterval(() => {
// setCounter((prevCounter) => prevCounter + 1)
// }, 1000)
// return () => clearTimeout(timer);
// }, [counter, setCounter])
// ======================================
// DISPLAY
// ======================================
return (
<div>
<ReactAudioPlayer
src='../audio/mainMusic.mp3'
autoPlay
controls
/>
<h1>Bakery</h1>
<div className='header-grid-container'>
<h2>Revenue ${score}</h2>
<h2>Goal: $100,000</h2>
</div>
<div className='grid-container'>
{/* EZ BAKE OVEN */}
<img src="https://i.imgur.com/gDIbzJa.png" onClick={earn1}></img>
{showEZBake ? (
<img src="https://i.imgur.com/NQ0vFjF.png" onClick={earn5}></img>
) : (
<img onClick={upgradeEZOven} src="https://i.imgur.com/mwp9tL5.png"></img>
)}
{/* TOASTER OVEN */}
{showToasterOven ? (
<img src='https://i.imgur.com/k5m7lCM.png' onClick={earn25}></img>
) : (
<img src='https://i.imgur.com/hg12R4H.png' onClick={upgradeToasterOven}></img>
)}
{/* CONVECTION OVEN */}
{showConvectionOven ? (
<img src='https://i.imgur.com/JEzQkHL.png' onClick={earn50}></img>
) : (
<img src='https://i.imgur.com/x7i3ZeE.png' onClick={upgradeConvectionOven}></img>
)}
</div>
<div className='grid-container2'>
{showSmallFactory ? (
<img className='factory' src='https://i.imgur.com/HugCDVu.png' onClick={earn250}></img>
) : (
<img className='factory' src='https://i.imgur.com/HLsiH2r.png' onClick={upgradeSmallFactory}></img>
)}
{/* WIN CONDITION */}
{
winCondition()
}
{/* THIS IS AN EXAMPLE OF HOW TO CALL A FUNCTION
{
reveal()
} */}
</div>
</div>
)
}
export default App
Browsers does not allow pages to play audio unless there was a interaction done on the domain playing the sound. For Chrome this is the case since 2018.
Shown here: https://developer.chrome.com/blog/autoplay/
I am not familiar with package you are using, but I doubt it would change it. This means that you should not be able to play audio as soon as page is loaded and will have to force user to make any sort of interaction.
Aside from that I feel like the path is wrong. If you use those sort of relative paths and do not use import to get the resource they are in 90% instances resolved relative to public folder an project structure. So I'd try placing your audio folder into public and change src so that you are grabbing it relative to what browser sees. I sadly do not remember if that's public/audio/mainMusic.mp3 or audio/mainMusic.mp3or something else, but the simple test is, if you paste it into browser, it will either open, or start downloading the file.

Audio is not working properly after playing multiple time in ReactJS

I'm new to React and trying to add some audio to a tenzies game.
The audio is getting weirder every time I click the roll button. And after clicking for a while, the sound is fading away. There is also a warning in the console: 'The AudioContext was not allowed to start.'
I can't find out what causing this issue. Please help!
I don't know how to run react code in StackOverflow. So I'm adding a link to the github repository and live site URL of this game.
Here is the full App.js component's code:
import React from 'react';
import Die from './Components/Die';
import { nanoid } from 'nanoid';
import Confetti from 'react-confetti';
import {Howl} from 'howler';
import winSound from './audio/win.mp3';
import rollSound from './audio/roll.mp3';
import holdSound from './audio/hold.mp3';
export default function App(){
const [dice, setDice] = React.useState(generateNewDice());
const [tenzies, setTenzies] = React.useState(false);
const [audio, setAudio] = React.useState(true);
const [rollCount, setRollCount] = React.useState(0);
const [timer, setTimer] = React.useState(0);
const [timerRunning, setTimerRunning] = React.useState(false);
function holdDieObject(){
return {
value: Math.floor(Math.random()*6) + 1,
isHeld: false,
id: nanoid()
}
}
function generateNewDice(){
let newDice= [];
for(let i=0; i<10; i++){
newDice.push(holdDieObject());
}
return newDice;
}
React.useEffect(()=>{ //Count time per 10 milliseconds when timer is running
let interval;
if(timerRunning){
interval = setInterval(() => {
setTimer((prevTime) => prevTime + 10)
}, 10)
}else{
clearInterval(interval);
}
return () => clearInterval(interval);
},[timerRunning])
React.useEffect(()=>{ //Check all the dice are matched or not
const someDiceHeld = dice.some(die => die.isHeld);
const allDiceHeld = dice.every(die => die.isHeld);
const firstDiceValue = dice[0].value;
const allSameValue = dice.every(die=> die.value === firstDiceValue);
if(someDiceHeld){
setTimerRunning(true);
}
if(allDiceHeld && allSameValue){
setTenzies(true);
// audio && victorySound.play(); // This brings up dependency warning. So moved it to the bottom
setTimerRunning(false)
}
},[dice])
const victorySound = new Howl({
src: [winSound]
})
if(tenzies){
audio && victorySound.play(); // Here
}
const rollDieSound = new Howl({
src: [rollSound]
})
const holdDieSound = new Howl({
src: [holdSound]
})
function holdDice(id){
audio && holdDieSound.play();
setDice(oldDice => oldDice.map(die =>{
return die.id===id ?
{
...die,
isHeld: !die.isHeld
} :
die;
}))
}
function rollDice(){
if(!tenzies){
setDice(oldDice => oldDice.map(die=>{
audio && rollDieSound.play();
return die.isHeld ? die : holdDieObject();
}))
setRollCount(prevCount => prevCount + 1);
}else{
setTenzies(false);
setDice(generateNewDice());
setRollCount(0);
setTimer(0);
}
}
function toggleMute(){
setAudio(prevState => !prevState);
}
function startNewGame(){
setTenzies(false);
setDice(generateNewDice());
}
const minutes = <span>{("0" + Math.floor((timer / 60000) % 60)).slice(-2)}</span>
const seconds = <span>{("0" + Math.floor((timer / 1000) % 60)).slice(-2)}</span>
const milliseconds = <span>{("0" + ((timer / 10) % 100)).slice(-2)}</span>
const dieElements = dice.map((die) => {
return <Die key={die.id}
value={die.value}
isHeld={die.isHeld}
holdDice={()=> holdDice(die.id)}
/>
})
return(
<div>
<main className="board">
<button onClick={toggleMute} className="mute-btn">{audio ? "πŸ”‰" : "πŸ”‡"}</button>
<h1>Tenzies</h1>
<p>Roll untill the dice are the same. Click each die to freeze it at its current value between rolls.</p>
<div className="die-container">
{dieElements}
</div>
<button onClick={rollDice}>Roll</button>
{tenzies && <div className="scoreboard">
<h2>Congratulations!</h2>
<p className='rollCount'>Rolled: {rollCount}</p>
<p className="rolltime">Time Taken: {minutes}:{seconds}:{milliseconds}</p>
<h3>Your Score: 4500</h3>
<button className='close' onClick={startNewGame}>New Game</button>
</div>}
</main>
{tenzies && <Confetti className="confetti" recycle={false} />}
</div>
)
}

continuous speech to text in react

I am trying to make a speech to text component. it is working but when i stop talking it stops also , but i want a loop until i press stop button .
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
This is the code i have tried - https://codesandbox.io/s/clever-clarke-4z7eqb?file=/src/App.js
Don't know what you want but here is how to continuously play the text.
import React, { useEffect } from "react";
function App() {
const [voice, setVoice] = React.useState(false);
const speek = () => {
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
if (voice) recognition.start();
else recognition.stop();
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
console.log(transcript);
};
recognition.onspeechend = () => {
recognition.stop();
if (voice === true) speek(voice); // loop
};
recognition.onerror = (event) => {
console.log("error");
};
};
useEffect(() => {
speek(voice);
}, [voice]);
return (
<>
<button onClick={() => setVoice(!voice)}>
{voice ? "stop" : "start"}
</button>
</>
);
}
export default App;
and here is codebase https://codesandbox.io/s/flamboyant-cray-igzrb7?file=/src/App.js:0-886

React Typewriter effect doesn't reset

I have created a typewriting effect with React and it works perfectly fine. However, when I change the language with i18n both texts don't have the same length and it keeps writing until both texts have the same length and then it changes the language and starts the effect again.
How can I reset the input when the language has changed? How can I reset the input when the component has been destroyed?
I have recorded a video
I have the same issue when I change from one page to another, as both pages have different texts and they don't have the same length.
Here code of my component
export const ConsoleText = ({text, complete = false}) => {
const [currentText, setCurrentText] = useState("");
const translatedText = i18n.t(text);
const index = useRef(0);
useEffect(() => {
if (!complete && currentText.length !== translatedText.length) {
const timeOut = setTimeout(() => {
setCurrentText((value) => value + translatedText.charAt(index.current));
index.current++;
}, 20);
return () => {
clearTimeout(timeOut);
}
} else {
setCurrentText(translatedText);
}
}, [translatedText, currentText, complete]);
return (
<p className="console-text">
{currentText}
</p>
);
};
You are telling react to do setCurrentText(translatedText) only when it is complete or when the compared text lengths are equal, so yes it continues to write until this moment.
To reset your text when text changes, try creating another useEffect that will reset your states :
useEffect(() => {
index.current = 0;
setCurrentText('');
}, [text]);
Now, I actually did this exact same feature few days ago, here is my component if it can help you :
import React from 'react';
import DOMPurify from 'dompurify';
import './text-writer.scss';
interface ITextWriterState {
writtenText: string,
index: number;
}
const TextWriter = ({ text, speed }: { text: string, speed: number }) => {
const initialState = { writtenText: '', index: 0 };
const sanitizer = DOMPurify.sanitize;
const [state, setState] = React.useState<ITextWriterState>(initialState);
React.useEffect(() => {
if (state.index < text.length - 1) {
const animKey = setInterval(() => {
setState(state => {
if (state.index > text.length - 1) {
clearInterval(animKey);
return { ...state };
}
return {
writtenText: state.writtenText + text[state.index],
index: state.index + 1
};
});
}, speed);
return () => clearInterval(animKey);
}
}, []);
// Reset the state when the text is changed (Language change)
React.useEffect(() => {
if (text.length > 0) {
setState(initialState);
}
}, [text])
return <div className="text-writer-component"><span className="text" dangerouslySetInnerHTML={{ __html: sanitizer(state.writtenText) }} /></div>
}
export default TextWriter;
The translation is made outside of the component so you can pass any kind of text to the component.

react 25+5 clock for freecodecamp test fail

I’m making the 25 + 5 clock for the freecodecamp certification but 2 test failled.
The test 10 and 11 for the #Timer are wrong.
" 25 + 5 clock has paused but time continued elapsing: expected β€˜58’ to equal β€˜59’ "
On my side, it’s working and you can test it yourself link to the deployed project here 1.
You can click the play and pause button as fast as you can, it work.
But the test not.
It’s for 2 days that I’m checking on stackoverflow, freecodecamp forum, google about this issue.
I did a lot of change but not possible to find the issue.
body component
import React from 'react';
import Compteur from './Compteur';
import Config from './Config';
import { useState, useEffect } from 'react';
const Body = () => {
const [sessionCounter, setSessionCounter] = useState(1500);
const [breakCounter, setBreakCounter] = useState(300);
const [counterScreenSession, setCounterScreenSession] = useState(sessionCounter);
const [play, setPlay] = useState(false);
const [session, setSession] = useState(true);
const handleSessionCounter = (e) => {
let number = e.currentTarget.dataset.session
if(number === "up"){
if(sessionCounter<3600){
return setSessionCounter(sessionCounter+60);
}else{
return sessionCounter;
}
}
else{
if(sessionCounter >= 120){
return setSessionCounter(sessionCounter-60);
}else{
return sessionCounter;
}
}
}
const handleBreakCounter = (e) => {
let number = e.currentTarget.dataset.breaker
if(number === "up"){
if(breakCounter<3600){
return setBreakCounter(breakCounter+60);
}else{
return breakCounter;
}
}
else{
if(breakCounter >= 120){
return setBreakCounter(breakCounter-60);
}else{
return breakCounter;
}
}
}
const handleClear = () => {
setPlay(false);
setSession(true);
setBreakCounter(300);
setSessionCounter(1500)
document.getElementById("beep").pause();
document.getElementById("beep").currentTime=0;
return setCounterScreenSession(1500);
}
const handleCounterScreen = () => {
setPlay(play=>!play);
}
useEffect(() => {
if(play && counterScreenSession>0){
const timer = window.setInterval(()=>{
setCounterScreenSession(counterScreenSession => counterScreenSession-1);
}, 1000);
return ()=>{
clearInterval(timer)
}
}
}, [play, counterScreenSession])
useEffect(() => {
if(counterScreenSession===0 && session){
document.getElementById("beep").play();
setCounterScreenSession(breakCounter);
setSession(!session);
}
if(counterScreenSession===0 && !session){
setCounterScreenSession(sessionCounter);
setSession(!session);
}
}, [counterScreenSession, session, breakCounter, sessionCounter])
useEffect(()=>{
return setCounterScreenSession(sessionCounter);
}, [sessionCounter, breakCounter])
const timeCounter = () =>{
let minutes = Math.floor(counterScreenSession/60);
let seconds = counterScreenSession%60;
if(minutes<10){
minutes = "0"+minutes;
}
if(seconds<10){
seconds = "0"+seconds;
}
return `${minutes}:${seconds}`;
}
return (
<div className="body">
<Config handleBreakCounter={handleBreakCounter} handleSessionCounter={handleSessionCounter}
sessionCounter={sessionCounter} breakCounter={breakCounter}/>
<Compteur counterScreenSession={counterScreenSession} play={play} handleCounterScreen=
{handleCounterScreen} handleClear={handleClear} session={session} sessionCounter={sessionCounter}
timeCounter={timeCounter} breakCounter={breakCounter}/>
</div>
);
};
export default Body;
Compteur component
import React from 'react';
import { AiFillPauseCircle, AiFillPlayCircle } from "react-icons/ai";
import {VscDebugRestart} from "react-icons/vsc";
import { CircularProgressbar } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
const Compteur = ({counterScreenSession, play, handleCounterScreen, handleClear, session,
timeCounter,breakCounter,sessionCounter}) => {
return (
<div className={"compteur"} >
<div className="compteur__name" id="timer-label">{session? "Session" : "Break"}</div>
<CircularProgressbar
className="compteur__animation"
value={counterScreenSession}
minValue={0}
maxValue={session? sessionCounter:breakCounter }
counterClockwise="true"
styles={{
path:{
stroke: "#005479"
},
trail:{
stroke:"#A8223A"
}}
}
/>
<div className="compteur__time"
className={counterScreenSession<600 && counterScreenSession%60<5?"compteur__time
compteur__name--red" : "compteur__time" }id="time-left">
{
/*
counterScreenSession<600 && counterScreenSession%60<10 ?
"0"+Math.floor(counterScreenSession/60)+":0"+counterScreenSession%60:
counterScreenSession>599 && counterScreenSession%60<10 ?
Math.floor(counterScreenSession/60)+":0"+counterScreenSession%60:
counterScreenSession<600 && counterScreenSession%60>10 ?
"0"+Math.floor(counterScreenSession/60)+":"+counterScreenSession%60:
Math.floor(counterScreenSession/60)+":"+counterScreenSession%60
*/
timeCounter()
}
</div>
<audio id="beep" src="./sound/duke-game-over.mp3"></audio>
<div className="compteur__controler">
{
play === false ?<button className="compteur__controler__play" id="start_stop" onClick=
{handleCounterScreen} ><AiFillPlayCircle/></button>:<button className="compteur__controler__break"
onClick={handleCounterScreen}><AiFillPauseCircle/></button>
}
<button className="compteur__controler__clear" id="reset" onClick={handleClear}>
<VscDebugRestart/></button>
</div>
</div>
);
};
export default Compteur;
Link to the repo on github here.
I had the same problem and solved it using a class-based solution.
What I had was 53 - 55. It means there's a 2000ms (in your case is 1000ms) difference between the time the test case was paused and the time the test case replayed. The problem was because I chained the beep sound play, then setting the state (switching session-break), and clearing the interval (so it was serially firing three different functions). It was solved when I moved the beep sound play and switching session-break, all together within the function that invokes clearInterval (so they're all "fired together" in that function).
That chaining might've happened here:
if(counterScreenSession===0 && session){
document.getElementById("beep").play();
setCounterScreenSession(breakCounter);
setSession(!session);
}
if(counterScreenSession===0 && !session){
setCounterScreenSession(sessionCounter);
setSession(!session);
}
So if I were you, I probably would try to incorporate those beep play and setsession within the return function in the else statement here, together with the clearInterval. Not sure if it'll work out though, I'm not even sure it could be played that way, but it might be worth it to toy around with that idea.
if(play && counterScreenSession>0){
const timer = window.setInterval(()=>{
setCounterScreenSession(counterScreenSession => counterScreenSession-1);
}, 1000);
return ()=>{
clearInterval(timer)
}
}

Resources