Skip to content

Instantly share code, notes, and snippets.

@kwindla
Created September 9, 2020 00:41
Show Gist options
  • Save kwindla/753a02ccc7391a1526bc2f17f586ffbd to your computer and use it in GitHub Desktop.
Save kwindla/753a02ccc7391a1526bc2f17f586ffbd to your computer and use it in GitHub Desktop.
// 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