Skip to content

fix: FIT-31: Ensure media buffers are syncable events that keep audio and video in sync #7633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
731fab2
fix: FIT-31: Ensure media buffers are syncable events that keep audio…
bmartel May 26, 2025
eb01cba
put all changes behind FF
bmartel May 27, 2025
cd9a9e4
fixing FF usage, aligning buffer styling with all media types, fix co…
bmartel May 27, 2025
55b67f0
tweaking the media buffering animations
bmartel May 27, 2025
c427c49
tweaking the media buffering animations
bmartel May 27, 2025
6184cd5
update Audio loader and error colors to use design tokens
bmartel May 27, 2025
e437ef4
debounce updates to smooth out the handling of buffering
bmartel May 27, 2025
bcc319f
Merge remote-tracking branch 'origin/develop' into fb-fit-31
bmartel May 27, 2025
a9b22e5
Sync Follow Merge dependencies
bmartel May 28, 2025
502df27
Merge branch 'develop' into 'fb-fit-31'
bmartel May 28, 2025
0719edd
video should not progress until buffering is over
bmartel May 29, 2025
9f431b8
Merge remote-tracking branch 'origin/develop' into fb-fit-31
bmartel May 29, 2025
5d7d248
use synced buffering
bmartel May 30, 2025
2f3ba48
don't over report buffering, only report when all buffering elements …
bmartel May 30, 2025
7756fbf
Merge remote-tracking branch 'origin/develop' into fb-fit-31
bmartel May 30, 2025
50a7d02
cleanup and waveform buffer sync to player instance
bmartel May 30, 2025
c7e3fd5
fix syncing A/V + Paragraphs
bmartel May 30, 2025
a06a9c4
don't allow video to seek while buffering it causes a loop
bmartel May 30, 2025
0987411
linting
bmartel May 30, 2025
4c47690
debounce the audio player buffering updates so it does not preemptive…
bmartel May 30, 2025
a193b81
linting
bmartel May 30, 2025
0800f74
ff all the audio synced buffering changes
bmartel May 30, 2025
69f0855
linting
bmartel May 30, 2025
b4972a6
Apply suggestions from code review
bmartel May 30, 2025
046fd93
linting
bmartel May 30, 2025
5b2f918
Merge remote-tracking branch 'origin/develop' into fb-fit-31
bmartel May 30, 2025
f2a0631
improve animation of buffering loader
bmartel May 30, 2025
174a8c4
Merge branch 'develop' into fb-fit-31
bmartel May 30, 2025
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
5 changes: 5 additions & 0 deletions web/libs/core/src/lib/utils/feature-flags/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ export const FF_ADJUSTABLE_SPANS = "fflag_feat_front_leap_1973_adjustable_spans_
* Enables the theme toggle in the UI
*/
export const FF_THEME_TOGGLE = "fflag_feat_front_optic_1217_theme_toggle_short";

/**
* Fixes synced audio/video buffering
*/
export const FF_SYNCED_BUFFERING = "fflag_fix_front_fit_31_synced_media_buffering";
35 changes: 34 additions & 1 deletion web/libs/editor/src/components/Timeline/Controls.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
.timeline-controls {
position: relative;
background: var(--color-neutral-background-bold);
padding: 4px;

&__buffering {
left: 0;
right: 0;
top: 0;
height: 4px;
position: absolute;
background-color: var(--color-primary-content);
overflow: hidden;

&::after {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
will-change: left;
background-color: var(--color-primary-content-subtle);
animation: buffering-media 2.4s ease-out infinite;
}
}

&__counter {
height: 36px;
min-width: 115px;
Expand Down Expand Up @@ -85,7 +108,7 @@
}
}

& + & {
&+& {
border-left: 1px solid var(--color-neutral-border);
}
}
Expand All @@ -110,3 +133,13 @@
background: var(--color-neutral-surface);
}
}

@keyframes buffering-media {
0% {
left: -100%;
}

100% {
left: 200%;
}
}
2 changes: 2 additions & 0 deletions web/libs/editor/src/components/Timeline/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const Controls: FC<TimelineControlsProps> = memo(
position,
frameRate = 1024,
playing,
buffering = false,
collapsed,
duration,
extraControls,
Expand Down Expand Up @@ -171,6 +172,7 @@ export const Controls: FC<TimelineControlsProps> = memo(

return (
<Block name="timeline-controls" tag={Space} spread style={{ gridAutoColumns: "auto" }}>
{buffering && <Elem name="buffering" aria-label="Buffering Media Source" />}
{isFF(FF_DEV_2715) && mediaType === "audio" ? (
renderControls()
) : (
Expand Down
4 changes: 1 addition & 3 deletions web/libs/editor/src/components/Timeline/Timeline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
background-color: var(--color-neutral-background);

&__topbar {
padding-bottom: var(--spacing-tight);
min-height: 48px;
padding: 6px;
display: grid;
grid-row-gap: 8px;
align-items: center;
grid-auto-rows: min-content;
grid-template-rows: 1fr;
border-top: 1px solid var(--color-neutral-border);
border-bottom: 1px solid var(--color-neutral-border);
}
}

Expand Down
2 changes: 2 additions & 0 deletions web/libs/editor/src/components/Timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const TimelineComponent: FC<TimelineProps> = ({
framerate = 24,
hopSize = 1,
playing = false,
buffering = false,
fullscreen = false,
disableView = false,
defaultStepSize = 10,
Expand Down Expand Up @@ -126,6 +127,7 @@ const TimelineComponent: FC<TimelineProps> = ({
position={currentPosition}
frameRate={framerate}
playing={playing}
buffering={buffering}
volume={props.volume}
controls={props.controls}
altHopSize={props.altHopSize}
Expand Down
2 changes: 2 additions & 0 deletions web/libs/editor/src/components/Timeline/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface TimelineProps<D extends ViewTypes = "frames"> {
mode: D;
framerate: number;
playing: boolean;
buffering?: boolean;
zoom?: number;
volume?: number;
speed?: number;
Expand Down Expand Up @@ -172,6 +173,7 @@ export interface TimelineControlsProps {
playing: boolean;
collapsed: boolean;
fullscreen: boolean;
buffering?: boolean;
volume?: number;
speed?: number;
zoom?: number;
Expand Down
4 changes: 2 additions & 2 deletions web/libs/editor/src/components/VideoCanvas/VideoCanvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
padding: 2px 7px;
text-align: center;
background-color: var(--color-neutral-background);
background-image: repeating-linear-gradient(90deg, var(--grape_500) 0, var(--grape_400) 120px, var(--grape_500) 240px);
background-image: repeating-linear-gradient(90deg, var(--color-primary-content) 0, var(--color-primary-content-subtle) 120px, var(--color-primary-content) 240px);
animation: buffering 1.2s linear infinite;
}
}
Expand Down Expand Up @@ -72,4 +72,4 @@
100% {
background-position: 240px 0;
}
}
}
92 changes: 70 additions & 22 deletions web/libs/editor/src/components/VideoCanvas/VideoCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import "./VideoCanvas.scss";
import { MAX_ZOOM, MIN_ZOOM } from "./VideoConstants";
import { VirtualCanvas } from "./VirtualCanvas";
import { VirtualVideo } from "./VirtualVideo";
import { ff } from "@humansignal/core";
import { useSyncedBuffering } from "../../hooks/useSyncedBuffering";

const isSyncedBuffering = ff.isActive(ff.FF_SYNCED_BUFFERING);

type VideoProps = {
src: string;
Expand All @@ -14,6 +18,7 @@ type VideoProps = {
position?: number;
currentTime?: number;
playing?: boolean;
buffering?: boolean;
framerate?: number;
muted?: boolean;
zoom?: number;
Expand All @@ -37,6 +42,7 @@ type VideoProps = {
onEnded?: () => void;
onResize?: (dimensions: VideoDimentions) => void;
onError?: (error: any) => void;
onBuffering?: (isBuffering: boolean) => void;
};

type PanOptions = {
Expand Down Expand Up @@ -88,6 +94,12 @@ export interface VideoRef {
adjustPan: (x: number, y: number) => PanOptions;
}

const useLocalBuffering = (props: VideoProps) => {
return useState(false);
};

const useBuffering = isSyncedBuffering ? useSyncedBuffering : useLocalBuffering;

export const VideoCanvas = memo(
forwardRef<VideoRef, VideoProps>((props, ref) => {
const raf = useRef<number>();
Expand All @@ -106,7 +118,7 @@ export const VideoCanvas = memo(
const [length, setLength] = useState(0);
const [currentFrame, setCurrentFrame] = useState(props.position ?? 1);
const [playing, setPlaying] = useState(false);
const [buffering, setBuffering] = useState(false);
const [buffering, setBuffering] = useBuffering(props);
const [zoom, setZoom] = useState(props.zoom ?? 1);
const [pan, setPan] = useState<PanOptions>(props.pan ?? { x: 0, y: 0 });

Expand All @@ -115,6 +127,8 @@ export const VideoCanvas = memo(
const [contrast, setContrast] = useState(1);
const [brightness, setBrightness] = useState(1);
const [saturation, setSaturation] = useState(1);
const bufferingRef = useRef(buffering);
bufferingRef.current = buffering;

const filters = useMemo(() => {
const result: string[] = [];
Expand Down Expand Up @@ -170,6 +184,7 @@ export const VideoCanvas = memo(
const updateFrame = useCallback(
(force = false) => {
if (!contextRef.current) return;
if (isSyncedBuffering && bufferingRef.current) return;

const currentTime = videoRef.current?.currentTime ?? 0;
const frameNumber = isFF(FF_VIDEO_FRAME_SEEK_PRECISION)
Expand All @@ -187,6 +202,19 @@ export const VideoCanvas = memo(
[framerate, currentFrame, drawVideo, props.onFrameChange, length],
);

const updateBuffering = useCallback(() => {
if (!videoRef.current) return;
if (!contextRef.current) return;

const video = videoRef.current;
if (video.networkState === video.NETWORK_IDLE) {
hasLoadedRef.current = true;
setBuffering(false);
} else {
setBuffering(true);
}
}, []);

const delayedUpdate = useCallback(() => {
if (!videoRef.current) return;
if (!contextRef.current) return;
Expand All @@ -196,40 +224,51 @@ export const VideoCanvas = memo(
if (video) {
if (!playing) updateFrame(true);

if (video.networkState === video.NETWORK_IDLE) {
hasLoadedRef.current = true;
setBuffering(false);
} else {
setBuffering(true);
}
updateBuffering();
}
}, [playing, updateFrame]);

// VIDEO EVENTS'
const handleVideoPlay = useCallback(() => {
setPlaying(true);
setBuffering(false);
if (!isSyncedBuffering) {
setBuffering(false);
} else {
updateBuffering();
}
props.onPlay?.();
}, [props.onPlay]);

const handleVideoPause = useCallback(() => {
setPlaying(false);
setBuffering(false);
if (!isSyncedBuffering) {
setBuffering(false);
} else {
updateBuffering();
}
props.onPause?.();
}, [props.onPause]);

const handleVideoPlaying = useCallback(() => {
setBuffering(false);
if (!isSyncedBuffering) {
setBuffering(false);
}
delayedUpdate();
}, [delayedUpdate]);

const handleVideoWaiting = useCallback(() => {
setBuffering(true);
if (!isSyncedBuffering) {
setBuffering(true);
} else {
updateBuffering();
}
}, []);

const handleVideoEnded = useCallback(() => {
setPlaying(false);
setBuffering(false);
if (!isSyncedBuffering) {
setBuffering(false);
}
props.onSeeked?.();
props.onEnded?.();
props.onPause?.();
Expand Down Expand Up @@ -290,10 +329,13 @@ export const VideoCanvas = memo(

// Handle extrnal state change [current time]
useEffect(() => {
if (videoRef.current && props.currentTime) {
videoRef.current.currentTime = props.currentTime;
}
}, [props.currentTime]);
const updateId = requestAnimationFrame(() => {
if (isSyncedBuffering && bufferingRef.current) return;
if (videoRef.current && props.currentTime) videoRef.current.currentTime = props.currentTime;
});

return () => cancelAnimationFrame(updateId);
}, [props.currentTime, bufferingRef]);

// Handle extrnal state change [play/pause]
useEffect(() => {
Expand Down Expand Up @@ -469,8 +511,8 @@ export const VideoCanvas = memo(

useEffect(() => {
let isLoaded = false;
let loadTimeout: NodeJS.Timeout | undefined = undefined;
let timeout: NodeJS.Timeout | undefined = undefined;
let loadTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
let timeout: ReturnType<typeof setTimeout> | undefined = undefined;

const checkVideoLoaded = () => {
if (isLoaded) return;
Expand Down Expand Up @@ -566,7 +608,7 @@ export const VideoCanvas = memo(
width={canvasWidth}
height={canvasHeight}
/>
{!loading && buffering && <Elem name="buffering" />}
{!loading && buffering && !isSyncedBuffering && <Elem name="buffering" aria-label="Buffering Media Source" />}
</Elem>

<VirtualVideo
Expand All @@ -581,15 +623,21 @@ export const VideoCanvas = memo(
onLoadedData={delayedUpdate}
onCanPlay={delayedUpdate}
onSeeked={(event) => {
delayedUpdate();
if (!isSyncedBuffering) {
delayedUpdate();
}
props.onSeeked?.(event);
}}
onSeeking={(event) => {
delayedUpdate();
if (!isSyncedBuffering) {
delayedUpdate();
}
props.onSeeked?.(event);
}}
onTimeUpdate={(event) => {
delayedUpdate();
if (!isSyncedBuffering) {
delayedUpdate();
}
props.onTimeUpdate?.(event);
}}
onProgress={delayedUpdate}
Expand Down
Loading
Loading