Search code examples
javascriptreactjsreact-bootstrap

How to play audio in REACT app while browser tab is inactive?


I am trying to make React play a sound when a timer gets to 0. The timer runs in a component built with reacts bootstrap modal component.

If the browser tab in which the app is running is inactive, the sound sometimes doesn't get played even if counter reaches 0 until browser tab is clicked. When tab is clicked sound plays properly. (tested in Chrome)

The state.timeLeft variable is being properly updated, I wrote the component to display % of time left as the tabs title and it's obvious sound doesn't get played even if counter hits 0. Any thoughts on what might trigger this behavior ? Your help is appreciated :)

import React, {Component} from 'react'
import {Button, Modal, Image} from 'react-bootstrap'
import doneWav from '../sounds/sunny.wav'

class Pomodoro extends Component {
    constructor(props) {
      super(props)
      this.state = {
        timeLeft: this.props.time*60, //time left stored in seconds 
      }
      this.targetDate = Date.parse(new Date()) + this.props.time*60*1000 
    }

    componentDidMount() {
      this.intervalId = setInterval( () => {
        let timeLeft = ( this.targetDate - Date.parse(new Date()) )/1000
        this.setState({timeLeft})
        document.title = `${Math.round((timeLeft/(this.props.time*60))*100)}%`
        if(timeLeft < 1) {
          clearInterval(this.intervalId) 
        }
      }, 500) 
    }

    componentWillUnmount() {clearInterval(this.intervalId)}

    render() {
      const pomodoroIsRunning = this.state.timeLeft > 1
      const playAudio = !pomodoroIsRunning ? <audio src={doneWav} autoPlay/> : null
      const title = pomodoroIsRunning ? 
        `${this.props.activity.title} - ${this.state.timeLeft} sec left` : 
        `Hurray, job done !` 
      const button = pomodoroIsRunning ? 
        <Button variant="secondary" onClick={this.props.stop} className="">Stop</Button> : 
        <Button variant="primary" onClick={this.props.finish}>Claim Reward</Button>

      return (
        <>
          { playAudio }
          <Modal

Solution

  • I think this issue is correlated with this question - the gist is, the audio won't play the first time it is loaded when the tab is unfocused as a browser performance optimization mechanic. I've managed to find a way to solve it in your case though. You can just play the audio while muted when the user first enters the application. As an example, I play the muted audio when the component initially mounts. This way, subsequent plays of this audio while the tab is unfocused should still play.

    componentDidMount() {
      var audio = new Audio(doneWav);
      audio.muted = true;
      audio.play();
    }
    

    Edit React HTML5 Audio Unfocused Tab


    Old Answer:

    If you are testing this on Chrome, I think this has something to do with the Autoplay Policy Changes - the gist is, autoplay (i.e., <audio src={doneWav} autoPlay />) fails because the user didn't interact with the document first . In fact, this will also occur if you attempt to create a new audio & execute .play() on it.

    enter image description here

    Take a look at this CodeSandBox: https://codesandbox.io/s/wizardly-bash-9gswo?file=/src/App.js - do not interact with the browser tab, it will not play and cause errors. But if you, for example, click anywhere on window before the timer expires, it should play.

    In my perspective, there could be a workaround somewhere but eventually the browser developers will look into patching it because of this new policy - you had best not find an exploit to avoid constant source code maintenance for this feature and I instead recommend to look for other User Experience alternatives.