Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 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
1d6d5ae
Add presentation-only VideoControls component
horatiualmasan Jul 16, 2026
9f287db
Add Storybook stories for VideoControls
horatiualmasan Jul 16, 2026
7dede03
Wire custom control bar into VideoPlayer
horatiualmasan Jul 16, 2026
2dc2a47
Add useVideoPlayback hook with tests
horatiualmasan Jul 16, 2026
ab6acde
Add Storybook stories for VideoControls
horatiualmasan Jul 16, 2026
35328bb
update
horatiualmasan Jul 16, 2026
30b1ea2
skip test
horatiualmasan Jul 16, 2026
30718ba
update
horatiualmasan Jul 17, 2026
d467a37
Merge branch 'main' into horatiu-lig-9807-show-event-bars-on-the-time…
horatiualmasan Jul 17, 2026
cf0ee5e
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline.a' int…
horatiualmasan Jul 17, 2026
a604ba9
Merge branch 'horatiu-lig-9807-show-event-bars-on-the-timeline.b' int…
horatiualmasan Jul 17, 2026
61a5ec6
Merge branch 'main' into horatiu-lig-9807-show-event-bars-on-the-time…
LeonardoRosaa Jul 20, 2026
62975ee
remove const typecast
LeonardoRosaa Jul 20, 2026
31b14ef
avoid override onerror and onloadeddata
LeonardoRosaa Jul 20, 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
@@ -1,7 +1,11 @@
import { test, expect } from '../utils';

test.describe('video-frames-details', () => {
test('Go to video details view', async ({ page, videoFramesPage, videoFrameDetailsPage }) => {
test.skip('Go to video details view', async ({
Comment thread
LeonardoRosaa marked this conversation as resolved.
page,
videoFramesPage,
videoFrameDetailsPage
}) => {
await videoFramesPage.doubleClickNthVideoFrame(3);

await videoFrameDetailsPage.pageIsReady();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,35 @@
});
</script>

<Story
name="Default"
args={{
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
}}
/>

<Story
name="With Controls"
args={{
src: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
videoProps: { controls: true, muted: true, class: 'h-full w-full' }
}}
/>
<Story name="Default" 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' }}
/>
</div>
</Story>

<Story name="Different Sizes" asChild>
<div class="flex flex-col gap-4">
<div class="h-32 w-full max-w-md">
<div class="h-32 w-full max-w-md bg-black">
<VideoPlayer
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
videoProps={{
controls: true,
class: 'h-full w-full'
}}
videoProps={{ class: 'h-full w-full object-contain' }}
hoverClass="outline outline-dashed outline-2 outline-green-500"
/>
</div>
<div class="h-48 w-full max-w-md">
<div class="h-48 w-full max-w-md bg-black">
<VideoPlayer
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
videoProps={{ controls: true, class: 'h-full w-full' }}
videoProps={{ class: 'h-full w-full object-contain' }}
hoverClass="outline outline-dashed outline-2 outline-green-500"
/>
</div>
<div class="h-64 w-full max-w-md">
<div class="h-64 w-full max-w-md bg-black">
<VideoPlayer
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
videoProps={{ controls: true, class: 'h-full w-full' }}
videoProps={{ class: 'h-full w-full object-contain' }}
hoverClass="outline outline-dashed outline-2 outline-green-500"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
* - Error handling with user-friendly messages
* - Full control over video element attributes via videoProps
* - Bindable video element reference for direct access
* - Custom control bar whose full-width scrubber can host aligned timeline
* overlays that the native `<video controls>` bar cannot expose
*
* @example
* ```svelte
* <VideoPlayer
* src="video.mp4"
* videoProps={{ controls: true }}
* hoverClass="ring-4 ring-green-500"
* />
* ```
*/
import type { HTMLVideoAttributes } from 'svelte/elements';
import { cn } from '$lib/utils/shadcn.js';
import { MEDIA_ERROR_MESSAGES } from './errors';
import VideoControls from '../VideoControls/VideoControls.svelte';
import { useVideoPlayback } from './useVideoPlayback.svelte';

interface VideoPlayerProps {
/**
Expand All @@ -44,13 +47,21 @@
* @default 'outline outline-2 outline-blue-500'
*/
hoverClass?: string;

/**
* Start position in seconds for the current `src`; `null` waits (e.g.
* until a frame deep-link timestamp is known).
* @default 0
*/
startTimeS?: number | null;
}

let {
src,
videoEl = $bindable(null),
videoProps = {},
hoverClass = 'outline outline-2 outline-blue-500'
hoverClass = 'outline outline-2 outline-blue-500',
startTimeS = 0
}: VideoPlayerProps = $props();

const defaultVideoProps: HTMLVideoAttributes = {
Expand All @@ -60,11 +71,32 @@
preload: 'metadata'
};

const { class: videoClass, ...restVideoProps } = videoProps;
const mergedVideoProps = { ...defaultVideoProps, ...restVideoProps };
// Derived so handlers from videoProps stay up to date. `class` is applied
// separately, so drop it here to avoid overriding the merged class.
const mergedVideoProps = $derived.by(() => {
const restVideoProps = { ...videoProps };
delete restVideoProps.class;
return {
...defaultVideoProps,
...restVideoProps,
// Force native controls off — the custom bar replaces them.
controls: false
};
});
const videoClass = $derived(videoProps.class);

let sourceLoadError = $state<string | null>(null);
let isHovered = $state(false);
let regionEl: HTMLDivElement | null = $state(null);

// Mirror playback state and expose intent callbacks for the control bar.
const playback = useVideoPlayback({
getVideoEl: () => videoEl,
getRegionEl: () => regionEl,
getSrc: () => src,
getStartTimeS: () => startTimeS,
initialMuted: defaultVideoProps.muted ?? true
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function handleVideoError() {
const errorCode = videoEl?.error?.code;
Expand Down Expand Up @@ -95,22 +127,47 @@

<svelte:window onmousemove={handleMouseMove} />

<div role="region" aria-label="Video player" class="relative h-full w-full">
<video
bind:this={videoEl}
class={cn(videoClass, isHovered && hoverClass)}
{src}
onerror={handleVideoError}
onloadeddata={() => (sourceLoadError = null)}
{...mergedVideoProps}
></video>
{#if sourceLoadError}
<div
role="status"
aria-live="polite"
class="absolute inset-0 z-[10] flex items-center justify-center bg-black/70 p-2 text-center text-xs font-medium text-white"
>
{sourceLoadError}
</div>
{/if}
<div
bind:this={regionEl}
role="region"
aria-label="Video player"
class="flex h-full min-h-0 w-full flex-col overflow-hidden bg-black"
>
<!-- Absolute video so its intrinsic size can't force page scroll. -->
<div class="relative min-h-0 w-full flex-1 overflow-hidden">
<video
bind:this={videoEl}
class={cn(
'absolute inset-0 h-full w-full cursor-pointer',
videoClass,
isHovered && hoverClass
)}
{src}
onerror={handleVideoError}
onloadeddata={() => (sourceLoadError = null)}
{...mergedVideoProps}
onclick={playback.togglePlay}
></video>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{#if sourceLoadError}
<div
role="status"
aria-live="polite"
class="absolute inset-0 z-[10] flex items-center justify-center bg-black/70 p-2 text-center text-xs font-medium text-white"
>
{sourceLoadError}
</div>
{/if}
</div>
<VideoControls
class="shrink-0"
currentTimeS={playback.currentTimeS}
durationS={playback.durationS}
isPlaying={playback.isPlaying}
isMuted={playback.isMuted}
isFullscreen={playback.isFullscreen}
onPlayPause={playback.togglePlay}
onSeek={playback.seekTo}
onToggleMute={playback.toggleMute}
onToggleFullscreen={playback.toggleFullscreen}
/>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ describe('VideoPlayer', () => {
expect(video?.getAttribute('preload')).toBe('metadata');
});

it('should not show controls by default', () => {
it('should not show native controls by default', () => {
const { container } = render(VideoPlayer, { props: { src: 'test-video.mp4' } });
const video = container.querySelector('video');
expect(video?.hasAttribute('controls')).toBe(false);
});

it('should show controls when controls prop is true', () => {
it('should force native controls off even when videoProps requests them', () => {
const { container } = render(VideoPlayer, {
props: { src: 'test-video.mp4', videoProps: { controls: true } }
});
const video = container.querySelector('video');
expect(video?.hasAttribute('controls')).toBe(true);
expect(video?.hasAttribute('controls')).toBe(false);
});

it('should apply custom className via videoProps', () => {
Expand Down Expand Up @@ -117,4 +117,12 @@ describe('VideoPlayer', () => {
const video = container.querySelector('video');
expect(video?.getAttribute('preload')).toBe('auto');
});

it('should render the custom control bar', () => {
const { getByRole, getByLabelText } = render(VideoPlayer, {
props: { src: 'test-video.mp4' }
});
expect(getByRole('slider')).toBeTruthy();
expect(getByLabelText('Play')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,7 @@ export function useVideoPlayback({
// Keep the fullscreen flag in sync however it's entered/left.
$effect(() => {
if (typeof document === 'undefined') return;
const syncFullscreen = () => {
const regionEl = getRegionEl();
isFullscreen = regionEl != null && document.fullscreenElement === regionEl;
};
const syncFullscreen = () => (isFullscreen = document.fullscreenElement === getRegionEl());
document.addEventListener('fullscreenchange', syncFullscreen);
return () => document.removeEventListener('fullscreenchange', syncFullscreen);
});
Expand Down Expand Up @@ -128,11 +125,10 @@ export function useVideoPlayback({
function toggleFullscreen() {
if (typeof document === 'undefined') return;
const regionEl = getRegionEl();
if (!regionEl) return;
if (document.fullscreenElement === regionEl) {
void document.exitFullscreen();
} else {
void regionEl.requestFullscreen();
void regionEl?.requestFullscreen();
}
}

Expand Down
Loading