Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d211bce
Add TemporalSpan model
horatiualmasan Jul 7, 2026
0c66a38
update
horatiualmasan Jul 7, 2026
3e04899
fix
horatiualmasan Jul 7, 2026
bd48016
try eager load
horatiualmasan Jul 7, 2026
f668678
fix warning
horatiualmasan Jul 7, 2026
f3956d1
Add video annotation from activitynet
horatiualmasan Jul 8, 2026
07ddf00
remove not needed joins
horatiualmasan Jul 8, 2026
37441f5
Merge branch 'horatiu-lig-9805-import-activitynet-style-event-annotat…
horatiualmasan Jul 8, 2026
ebe86db
simplify
horatiualmasan Jul 8, 2026
84a4063
Merge branch 'main' into horatiu-lig-9805-import-activitynet-style-ev…
horatiualmasan Jul 15, 2026
47ec651
fix merge
horatiualmasan Jul 15, 2026
9f97a84
add example sctipt
horatiualmasan Jul 15, 2026
d95200c
format
horatiualmasan Jul 15, 2026
6a243bd
update
horatiualmasan Jul 15, 2026
1a24532
Merge branch 'main' into horatiu-lig-9805-import-activitynet-style-ev…
horatiualmasan Jul 15, 2026
d32cbe4
Consider also own annotations for video filter
horatiualmasan Jul 16, 2026
3acc186
Merge branch 'horatiu-lig-9805-import-activitynet-style-event-annotat…
horatiualmasan Jul 16, 2026
bdc02d2
fix merge error
horatiualmasan Jul 16, 2026
a9d31fd
Add useVideoPlayback hook with tests
horatiualmasan Jul 16, 2026
5a07a5b
Wire custom control bar into VideoPlayer
horatiualmasan Jul 16, 2026
ebad55a
Use custom player in VideoDetails with deep-link start time
horatiualmasan Jul 16, 2026
1d6d5ae
Add presentation-only VideoControls component
horatiualmasan Jul 16, 2026
2cdba41
format
horatiualmasan Jul 16, 2026
9f287db
Add Storybook stories for VideoControls
horatiualmasan Jul 16, 2026
2dc2a47
Add useVideoPlayback hook with tests
horatiualmasan Jul 16, 2026
7dede03
Wire custom control bar into VideoPlayer
horatiualmasan Jul 16, 2026
ecef795
Use custom player in VideoDetails with deep-link start time
horatiualmasan Jul 16, 2026
e2a18ce
format
horatiualmasan Jul 16, 2026
8395952
fix error in test stub
horatiualmasan Jul 16, 2026
f8ef40d
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline.d' of …
horatiualmasan Jul 16, 2026
0f2b999
Add video event model (toVideoEvents, assignEventLanes)
horatiualmasan Jul 16, 2026
8e2426e
Add VideoEventTimeline component
horatiualmasan Jul 16, 2026
cdbf3e5
Wire event bar into VideoPlayer, VideoControls and VideoDetails
horatiualmasan Jul 16, 2026
717ebf2
add AnnotationType.CLASSIFICATION
horatiualmasan Jul 17, 2026
b8ada23
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline-3-even…
horatiualmasan Jul 17, 2026
becf8d5
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline-3-even…
horatiualmasan Jul 17, 2026
21d02b9
update
horatiualmasan Jul 17, 2026
c56018e
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline-3-even…
horatiualmasan Jul 17, 2026
f212218
Merge branch 'main' into horatiu-lig-9807-show-event-bars-on-the-time…
LeonardoRosaa Jul 21, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* buttons. Full width so timeline overlays can share its coordinate system.
* Owns no playback state; calls back on user intent (see {@link VideoPlayer}).
*/
import type { Snippet } from 'svelte';
import { Play, Pause, Volume2, VolumeX, Maximize, Minimize } from '@lucide/svelte';
import { cn } from '$lib/utils/shadcn.js';
import { formatTime, clampPercent, timeFromClientX } from './VideoControls.helpers';
Expand All @@ -18,6 +19,12 @@
onSeek: (timeS: number) => void;
onToggleMute: () => void;
onToggleFullscreen: () => void;
/**
* Optional content (e.g. an event bar) rendered below the transport
* buttons. It sits in the same padded, full-width column as the scrubber,
* so overlays share the scrubber's coordinate system and line up exactly.
*/
children?: Snippet;
class?: string;
}

Expand All @@ -31,6 +38,7 @@
onSeek,
onToggleMute,
onToggleFullscreen,
children,
class: className
}: VideoControlsProps = $props();

Expand Down Expand Up @@ -162,4 +170,5 @@
{/if}
</button>
</div>
{@render children?.()}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
VideoPlayer
} from '$lib/components';
import { type FrameView, type SampleView, type VideoView } from '$lib/api/lightly_studio_local';
import { getVideoURLById } from '$lib/utils';
import { getVideoURLById, toVideoEvents } from '$lib/utils';
import VideoSampleMetadata from '../VideoSampleMetadata/VideoSampleMetadata.svelte';
import SampleDetailsCaptionSegment from '../SampleDetails/SampleDetailsCaptionsSegment/SampleDetailsCaptionSegment.svelte';
import { useVideoFrames } from '$lib/hooks/useVideoFrames/useVideoFrames';
Expand All @@ -31,6 +31,9 @@
let videoEl: HTMLVideoElement | null = $state(null);
let frameRequestId: number | null = $state(null);

// Imported events: classification annotations on the video carrying a time span.
const videoEvents = $derived(toVideoEvents(video.sample.annotations ?? []));

const {
currentFrame,
frames: videoFrames,
Expand Down Expand Up @@ -163,6 +166,8 @@
src={getVideoURLById(video.sample_id)}
bind:videoEl
{startTimeS}
events={videoEvents}
durationS={video.duration_s ?? undefined}
videoProps={{
muted: true,
class: 'object-contain',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import VideoPlayer from './VideoPlayer.svelte';
import { toVideoEvents } from '$lib/utils';

const { Story } = defineMeta({
title: 'Components/VideoPlayer',
Expand All @@ -11,6 +12,23 @@
videoProps: { control: 'object' }
}
});

const sampleEvents = toVideoEvents(
[
['Run-up', 0, 3],
['Long Jump', 2, 9],
['Landing', 8.5, 11],
['Celebration', 12, 16]
].map(([label, start, end], index) => ({
sample_id: `evt-${index}`,
parent_sample_id: 'video-1',
annotation_collection_id: 'coll-1',
annotation_type: 'classification',
annotation_label: { annotation_label_name: label },
created_at: new Date(),
temporal_span_details: { start_time_s: start, end_time_s: end }
}))
);
</script>

<Story name="Default" asChild>
Expand All @@ -22,6 +40,17 @@
</div>
</Story>

<Story name="With Events" asChild>
<div class="h-96 w-full max-w-2xl bg-black">
<VideoPlayer
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
videoProps={{ class: 'h-full w-full object-contain' }}
events={sampleEvents}
durationS={20}
/>
</div>
</Story>

<Story name="Different Sizes" asChild>
<div class="flex flex-col gap-4">
<div class="h-32 w-full max-w-md bg-black">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -54,14 +56,29 @@
* @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 {
src,
videoEl = $bindable(null),
videoProps = {},
hoverClass = 'outline outline-2 outline-blue-500',
startTimeS = 0
startTimeS = 0,
events = [],
durationS
}: VideoPlayerProps = $props();

const defaultVideoProps: HTMLVideoAttributes = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -171,13 +193,24 @@
<VideoControls
class="shrink-0"
currentTimeS={playback.currentTimeS}
durationS={playback.durationS}
durationS={effectiveDurationS}
isPlaying={playback.isPlaying}
isMuted={playback.isMuted}
isFullscreen={playback.isFullscreen}
onPlayPause={playback.togglePlay}
onSeek={playback.seekTo}
onToggleMute={playback.toggleMute}
onToggleFullscreen={playback.toggleFullscreen}
/>
>
{#if showEvents}
<VideoEventTimeline
class="w-full"
{events}
durationS={effectiveDurationS}
currentTimeS={playback.currentTimeS}
onSeek={playback.seekTo}
showHeader={false}
/>
{/if}
</VideoControls>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Loading