diff --git a/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.svelte b/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.svelte
index 4f7eed91e..29826c21c 100644
--- a/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.svelte
+++ b/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.svelte
@@ -22,7 +22,9 @@
import { cn } from '$lib/utils/shadcn.js';
import { MEDIA_ERROR_MESSAGES } from './errors';
import VideoControls from '../VideoControls/VideoControls.svelte';
+ import VideoEventTimeline from '../VideoEventTimeline/VideoEventTimeline.svelte';
import { useVideoPlayback } from './useVideoPlayback.svelte';
+ import type { VideoEvent } from '$lib/utils';
interface VideoPlayerProps {
/**
@@ -54,6 +56,19 @@
* @default 0
*/
startTimeS?: number | null;
+
+ /**
+ * Time-bounded events rendered as a clickable bar aligned with the
+ * scrubber. When empty, no event bar is shown.
+ */
+ events?: VideoEvent[];
+
+ /**
+ * Total video duration in seconds, used to position the event bars and
+ * the scrubber. Falls back to the element's own duration once metadata
+ * loads.
+ */
+ durationS?: number;
}
let {
@@ -61,7 +76,9 @@
videoEl = $bindable(null),
videoProps = {},
hoverClass = 'outline outline-2 outline-blue-500',
- startTimeS = 0
+ startTimeS = 0,
+ events = [],
+ durationS
}: VideoPlayerProps = $props();
const defaultVideoProps: HTMLVideoAttributes = {
@@ -102,6 +119,11 @@
initialMuted: defaultVideoProps.muted ?? true
});
+ // Prefer an explicit duration (known before metadata loads) for the scrubber
+ // and the event bar, so both share the same coordinate system.
+ const effectiveDurationS = $derived(durationS ?? playback.durationS);
+ const showEvents = $derived(events.length > 0);
+
function handleVideoError() {
const errorCode = videoEl?.error?.code;
sourceLoadError = errorCode
@@ -171,7 +193,7 @@
+ >
+ {#if showEvents}
+
+ {/if}
+
diff --git a/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.test.ts b/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.test.ts
index f809fc1dc..3f565f29a 100644
--- a/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.test.ts
+++ b/lightly_studio_view/src/lib/components/VideoPlayer/VideoPlayer.test.ts
@@ -125,4 +125,32 @@ describe('VideoPlayer', () => {
expect(getByRole('slider')).toBeTruthy();
expect(getByLabelText('Play')).toBeTruthy();
});
+
+ it('should not render an event bar when no events are given', () => {
+ const { queryByTestId } = render(VideoPlayer, { props: { src: 'test-video.mp4' } });
+ expect(queryByTestId('video-event-timeline')).toBeFalsy();
+ });
+
+ it('should render an event bar in the controls when events are given', () => {
+ const { getByTestId, getByText } = render(VideoPlayer, {
+ props: {
+ src: 'test-video.mp4',
+ durationS: 10,
+ events: [
+ {
+ id: 'e1',
+ annotationCollectionId: 'coll-1',
+ label: 'Jump',
+ startTimeS: 2,
+ endTimeS: 4,
+ color: 'rgba(10, 20, 30, 0.7)',
+ contrastColor: 'rgba(245, 235, 225, 0.7)'
+ }
+ ]
+ }
+ });
+
+ expect(getByTestId('video-event-timeline')).toBeTruthy();
+ expect(getByText('Jump')).toBeTruthy();
+ });
});