Created
September 9, 2020 00:41
-
-
Save kwindla/753a02ccc7391a1526bc2f17f586ffbd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// accumulated "wisdom" to handle as many play()-related issues as possible, going back | |
// to browser versions of the distant past. | |
// | |
// currently we use separate <video> and <audio> elements as part of ensuring that play() | |
// works as expected for both video and sound. new browser versions *definitely do* | |
// change this behavior, though, so the code below will change over time, too. | |
// we set the elements up like this in our render() function: | |
return ( | |
<VideoWrapper isActiveSpeaker={this.isActiveSpeaker()}> | |
<video | |
id={this.props.id} | |
className={'daily-video-element'} | |
style={style} | |
ref={(video) => { | |
if (video && video !== this.video) { | |
this.video = video; | |
// while we don't rely on autoplay, it is best | |
// practice to set the muted attribute on the | |
// video element to avoid potential issues when | |
// when we go to play() the video | |
this.video.setAttribute('muted', true); | |
if (this.isSafariOrIOS()) { | |
// 'playsinline' is required to avoid the video expanding | |
// to full screen. common examples in the wild set this | |
// for all browsers should we? | |
this.video.setAttribute('playsinline', true); | |
} | |
} | |
}} | |
/> | |
<audio | |
ref={(audio) => { | |
if (audio && audio !== this.audio) { | |
this.audio = audio; | |
if (this.isSafariOrIOS()) { | |
this.audio.setAttribute('playsinline', true); | |
this.audio.setAttribute('autoplay', true); | |
} | |
} | |
}} | |
/> | |
</VideoWrapper> | |
); | |
// and the function that we call to play the video and audio when either or both | |
// tracks are ready to play | |
playTracks() { | |
if ( | |
!(this.props.source || this.props.audioSource || this.props.videoSource) | |
) { | |
// not playing because nothing to play | |
return; | |
} | |
try { | |
// video | |
if ( | |
this.video && | |
this.video.paused && | |
this.video.srcObject && | |
this.video.srcObject.getVideoTracks().length | |
) { | |
this.video | |
.play() | |
.then(() => { | |
// video started | |
this.endLoading(); | |
}) | |
.catch((e) => { | |
if (e.message.match(/interact with the document first/)) { | |
this.meiClickTimeout = window.setTimeout(() => { | |
if ( | |
!this.props.isMEIModalShowing && | |
this.video && | |
this.video.paused | |
) { | |
this.props.showMEIModal(this.props.type); | |
} | |
}, 1500); | |
} | |
}); | |
} | |
// Safari video autoplay works because our videos don't have | |
// audio tracks. Audio autoplay works unless the user hasn't | |
// either 1) allowed media capture, or 2) triggered an audio | |
// play in response to a user gesture | |
if ( | |
!this.props.isLocal && | |
this.audio && | |
this.audio.srcObject && | |
getBrowserName() === 'Safari' | |
) { | |
if (this._safariAutoplayWatchdogTimer) { | |
return; | |
} | |
this._safariAutoplayWatchdogTimer = window.setTimeout(async () => { | |
if (!this.props.isMEIModalShowing && this.audio.paused) { | |
// autoplay failed or was very slow (very slow happens on | |
// the iphone quite a bit, so the code below is kind of | |
// messy and we could end up hiding an MEI modal that we | |
// actually need, if multiple videos load and events fire | |
// in the wrong order. feh. | |
// but anyway, first, try once, quickly, to play again | |
// may be a bit much to log here but I'm leaning toward more info | |
this.audio | |
.play() | |
.then() | |
.catch((e) => console.log('minor audio play fail', e)); | |
await aTimeout(500); | |
if (!this.audio.paused) { | |
this._safariAutoplayWatchdogTimer = null; | |
return; | |
} | |
// well, we need to get user input to autoplay | |
this.props.showMEIModal('safari-audio'); | |
// okay, need to set up periodic play attempt | |
this.meiClickInterval = window.setInterval(() => { | |
if (!this.audio.paused) { | |
this.props.hideMEIModal(); | |
window.clearInterval(this.meiClickInterval); | |
this.meiClickInterval = null; | |
this.props.setOutputDeviceId(this.audio); | |
} else { | |
this.audio | |
.play() | |
.then(() => { | |
this.props.hideMEIModal(); | |
this.props.setOutputDeviceId(this.audio); | |
}) | |
.catch((e) => { | |
console.error(e); | |
}) | |
.finally(() => { | |
this._safariAutoplayWatchdogTimer = null; | |
window.clearInterval(this.meiClickInterval); | |
this.meiClickInterval = null; | |
}); | |
} | |
}, 250); | |
} else { | |
// autoplay succeeded. make sure device output is correct | |
this._safariAutoplayWatchdogTimer = null; | |
this.props.setOutputDeviceId(this.audio); | |
} | |
}, 2000); | |
} | |
// non-autoplay audio logic (browsers other than Safari) | |
if ( | |
this.audio && | |
this.audio.srcObject && | |
this.audio.paused && | |
!(this.props.isLocal || this.isSafariOrIOS()) | |
) { | |
let src = this.audio.srcObject; | |
// throttle how often we change audio source and then call | |
// play, for Edge. If we don't do this, Edge audio playing | |
// silently fails. There is definitely a cleaner way to do | |
// this, but I can't think of one that doesn't involve | |
// refactoring that could break audio play reliability on | |
// other browsers. come back to this when we refactor stream | |
// handling, maybe, and Edge improves its support for | |
// WebRTC/audio | |
if (getBrowserName() === 'Edge') { | |
// what time did we play, most recently? | |
let lastPlay = this._edgeLastPlay || 0; | |
if (Date.now() - lastPlay < 3 * 1000) { | |
setTimeout(() => { | |
let x = this.audio.srcObject; | |
this.audio.srcObject = null; | |
this.audio.srcObject = x; | |
this.playTracks(); | |
}, 3.1 * 1000); | |
return; | |
} | |
this._edgeLastPlay = Date.now(); | |
} | |
this.audio | |
.play() | |
.then(() => { | |
// audio started. trigger our end-loading callback here if | |
// we don't have a video track. (we don't bother to try to | |
// set the output device id for MEI-delayed elements, | |
// because we probably have to use the default speakers, | |
// anyway, in contexts in which MEI is triggered.) | |
this.props.setOutputDeviceId(this.audio); | |
if (!(this.video && this.video.srcObject)) { | |
this.endLoading(); | |
} | |
}) | |
.catch((e) => { | |
// todo: fix this to work for Firefox, which now | |
// blocks audio playing in a Safari-like way. Firefox enters | |
// this catch block, but the e.message string is different | |
// than what we look for below, and if we have a video | |
// element with a srcObject we don't try to show the MEI | |
// overlay. Additionally, an attempt at a quick fix here | |
// resulted in an MEI overlay that blinked briefly into | |
// existence and then disappeared. So, more digging to do ... | |
// | |
// console.warn(' audio play error', src, e); | |
// trigger our MEI foolishness callback here if we don't | |
// have a video track | |
if (!(this.video && this.video.srcObject)) { | |
if (e.message.match(/interact with the document first/)) { | |
this.meiClickTimeout = window.setTimeout(() => { | |
if ( | |
!this.isMEIModalShowing && | |
this.video && | |
this.video.paused | |
) { | |
this.props.showMEIModal(this.props.type); | |
} | |
}, 1500); | |
} | |
return; | |
} | |
if (e.message.match(/request is not allowed by the user agent/)) { | |
// sanity check to catch audio issues in safari. should | |
// not need this because we are autoplay'ing audio | |
console.log('safari audio autoplay issue'); | |
} else { | |
console.warn('audio play error: ', e.message); | |
} | |
}); | |
} | |
} catch (e) { | |
// maybe avoid our occasional ReactCompositeComponent infinite | |
// recursion bug? we think we see no promise returned from | |
// play(), occationally. anyway, good to just end the loading | |
// attempt if we got an unexpected error | |
console.error('video play error, forcing end to loading sequence', e); | |
window.setTimeout(() => this.endLoading(), 25); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment