Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 27 additions & 74 deletions src/components/DriveVideo/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Colors from '../../colors';
import { ErrorOutline } from '../../icons';
import { currentOffset } from '../../timeline';
import { seek, bufferVideo } from '../../timeline/playback';
import { isIos, isFirefox } from '../../utils/browser.js';
import { isIos } from '../../utils/browser.js';

const VideoOverlay = ({ loading, error }) => {
let content;
Expand All @@ -37,30 +37,12 @@ const VideoOverlay = ({ loading, error }) => {
);
};

const getVideoState = (videoPlayer) => {
const currentTime = videoPlayer.getCurrentTime();
const { buffered } = videoPlayer.getInternalPlayer();

let bufferRemaining = -1;
for (let i = 0; i < buffered.length; i++) {
const end = buffered.end(i);
if (currentTime >= buffered.start(i) && currentTime <= end) {
bufferRemaining = end - currentTime;
break;
}
}

return {
bufferRemaining,
hasLoaded: bufferRemaining > 0,
};
};

class DriveVideo extends Component {
constructor(props) {
super(props);

this.onVideoBuffering = this.onVideoBuffering.bind(this);
this.onVideoBufferEnd = this.onVideoBufferEnd.bind(this);
this.onHlsError = this.onHlsError.bind(this);
this.onVideoError = this.onVideoError.bind(this);
this.onVideoResume = this.onVideoResume.bind(this);
Expand Down Expand Up @@ -98,22 +80,20 @@ class DriveVideo extends Component {
}

onVideoBuffering() {
const { dispatch, currentRoute } = this.props;
const { dispatch } = this.props;
const videoPlayer = this.videoPlayer.current;
if (!videoPlayer || !currentRoute || !videoPlayer.getDuration()) {
dispatch(bufferVideo(true));
}

if (this.firstSeek) {
if (this.firstSeek && videoPlayer) {
this.firstSeek = false;
videoPlayer.seekTo(this.currentVideoTime(), 'seconds');
}
dispatch(bufferVideo(true));
}

const { hasLoaded } = getVideoState(videoPlayer);
const { readyState } = videoPlayer.getInternalPlayer();
if (!hasLoaded || readyState < 2) {
dispatch(bufferVideo(true));
}
onVideoBufferEnd() {
const { dispatch, isBufferingVideo } = this.props;
const { videoError } = this.state;
if (videoError) this.setState({ videoError: null });
if (isBufferingVideo) dispatch(bufferVideo(false));
}

/**
Expand Down Expand Up @@ -201,57 +181,30 @@ class DriveVideo extends Component {
}

syncVideo() {
const { dispatch, isBufferingVideo, isMuted } = this.props;
const { dispatch, isBufferingVideo, desiredPlaySpeed, currentRoute } = this.props;
const videoPlayer = this.videoPlayer.current;
if (!videoPlayer || !videoPlayer.getInternalPlayer() || !videoPlayer.getDuration()) {
return;
}

let { desiredPlaySpeed: newPlaybackRate } = this.props;
const desiredVideoTime = this.currentVideoTime();
const curVideoTime = videoPlayer.getCurrentTime();
const timeDiff = desiredVideoTime - curVideoTime;

if (Math.abs(timeDiff) <= Math.max(0.1, 0.5 * newPlaybackRate)) { // newPlaybackRate = 0 when paused, set minimum 0.1 to prevent seeking when paused
if (!isIos()) {
newPlaybackRate = Math.max(0, newPlaybackRate + Math.round(timeDiff * 10) / 10);
const videoStartOffset = currentRoute?.videoStartOffset || 0;

if (Math.abs(timeDiff) > 0.5) {
if (desiredVideoTime === 0 && timeDiff < 0 && curVideoTime !== videoPlayer.getDuration()) {
// logs start earlier than the video, snap timeline forward to where video begins
dispatch(seek(currentOffset() - (timeDiff * 1000)));
} else {
// user seek, loop wrap, or initial sync: push to video
videoPlayer.seekTo(desiredVideoTime, 'seconds');
}
} else if (desiredVideoTime === 0 && timeDiff < 0 && curVideoTime !== videoPlayer.getDuration()) {
// logs start earlier than video, so skip to video ts 0
dispatch(seek(currentOffset() - (timeDiff * 1000)));
} else {
videoPlayer.seekTo(desiredVideoTime, 'seconds');
}
// most browsers don't support more than 16x playback rate, firefox mutes audio above 8x causing audio to cut in and out with timeDiff rate shifts
newPlaybackRate = Math.max(0, Math.min((isFirefox() && !isMuted) ? 8 : 16, newPlaybackRate));

const internalPlayer = videoPlayer.getInternalPlayer();

const { hasLoaded } = getVideoState(videoPlayer);
if (isBufferingVideo && internalPlayer.readyState >= 4) {
dispatch(bufferVideo(false));
} else if (isBufferingVideo || !hasLoaded || internalPlayer.readyState < 2) {
if (!isBufferingVideo) {
dispatch(bufferVideo(true));
}
newPlaybackRate = 0; // in some circumstances, iOS won't update readyState unless temporarily paused
}

if (videoPlayer.getInternalPlayer('hls')) {
if (!internalPlayer.paused && newPlaybackRate === 0) {
internalPlayer.pause();
} else if (internalPlayer.playbackRate !== newPlaybackRate && newPlaybackRate !== 0) {
internalPlayer.playbackRate = newPlaybackRate;
}
if (internalPlayer.paused && newPlaybackRate !== 0) {
const playRes = internalPlayer.play();
if (playRes) {
playRes.catch(() => console.debug('[DriveVideo] play interrupted by pause'));
}
}
} else {
// TODO: fix iOS bug where video doesn't stop buffering while paused
internalPlayer.playbackRate = newPlaybackRate;
} else if (Math.abs(timeDiff) > 0.05 && desiredPlaySpeed > 0 && !isBufferingVideo) {
// let the video play freely and pull the timeline to match, instead of fudging the playback rate.
// this is the core of the fix — fudging the rate every tick is what crackled audio on iOS,
// CarPlay, and macOS audio routes.
dispatch(seek(curVideoTime * 1000 + videoStartOffset));
}
}

Expand Down Expand Up @@ -314,7 +267,7 @@ class DriveVideo extends Component {
}}
playbackRate={desiredPlaySpeed}
onBuffer={this.onVideoBuffering}
onBufferEnd={this.onVideoResume}
onBufferEnd={this.onVideoBufferEnd}
onPlay={this.onVideoResume}
onError={this.onVideoError}
/>
Expand Down
47 changes: 22 additions & 25 deletions src/components/TimeDisplay/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { DownArrow, Forward10, Pause, PlayArrow, Replay10, UpArrow } from '../..
import { currentOffset } from '../../timeline';
import { seek, play, pause } from '../../timeline/playback';
import { getSegmentNumber } from '../../utils';
import { isIos } from '../../utils/browser.js';

const timerSteps = [
0.1,
Expand Down Expand Up @@ -259,30 +258,28 @@ class TimeDisplay extends Component {
<Typography variant="body1" align="center" className={classes.currentTime}>
<span ref={this.textHolder}>{ displayTime }</span>
</Typography>
{!isIos() && (
<div className={ classes.desiredPlaySpeedContainer }>
<IconButton
className={classes.tinyArrowIcon}
onClick={this.increaseSpeed}
disabled={!this.canIncreaseSpeed()}
aria-label="Increase play speed by 1 step"
>
<UpArrow className={classes.tinyArrowIcon} />
</IconButton>
<Typography variant="body2" align="center">
{desiredPlaySpeed}
×
</Typography>
<IconButton
className={classes.tinyArrowIcon}
onClick={this.decreaseSpeed}
disabled={!this.canDecreaseSpeed()}
aria-label="Decrease play speed by 1 step"
>
<DownArrow className={classes.tinyArrowIcon} />
</IconButton>
</div>
)}
<div className={ classes.desiredPlaySpeedContainer }>
<IconButton
className={classes.tinyArrowIcon}
onClick={this.increaseSpeed}
disabled={!this.canIncreaseSpeed()}
aria-label="Increase play speed by 1 step"
>
<UpArrow className={classes.tinyArrowIcon} />
</IconButton>
<Typography variant="body2" align="center">
{desiredPlaySpeed}
×
</Typography>
<IconButton
className={classes.tinyArrowIcon}
onClick={this.decreaseSpeed}
disabled={!this.canDecreaseSpeed()}
aria-label="Decrease play speed by 1 step"
>
<DownArrow className={classes.tinyArrowIcon} />
</IconButton>
</div>
<div className={ classes.leftBorderBox }>
<Tooltip title={ !this.props.hasAudio ? "Enable audio recording through the \"Record and Upload Microphone Audio\" toggle on your device" : '' }>
<div>
Expand Down
4 changes: 0 additions & 4 deletions src/utils/browser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export function isIos() {
return /iphone|ipad|ipod/i.test(navigator.userAgent);
}

export function isFirefox() {
return navigator.userAgent.toLowerCase().includes('firefox');
}
Loading