Skip to content

Commit c82af28

Browse files
committed
refactor: apply changes to modern.js variant
1 parent 6eee4cb commit c82af28

File tree

4 files changed

+388
-29
lines changed

4 files changed

+388
-29
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { formatDuration, formatFileSize } from '@nmit-coursition/utils'
2+
import type { MediaMetadata } from '@nmit-coursition/utils'
3+
4+
interface MediaFileDetailsProps {
5+
metadata: MediaMetadata
6+
}
7+
8+
export function MediaFileDetails({ metadata }: MediaFileDetailsProps) {
9+
return (
10+
<div className='grid grid-cols-2 gap-x-6 gap-y-3 pb-3'>
11+
{metadata.dimensions && (
12+
<div>
13+
<div className='font-medium text-gray-700'>Dimensions</div>
14+
<div>
15+
{metadata.dimensions.width}x{metadata.dimensions.height}
16+
</div>
17+
</div>
18+
)}
19+
20+
{metadata.durationInSeconds != null && (
21+
<div>
22+
<div className='font-medium text-gray-700'>Duration</div>
23+
<div>{formatDuration(metadata.durationInSeconds)}</div>
24+
</div>
25+
)}
26+
27+
{metadata.fps != null && (
28+
<div>
29+
<div className='font-medium text-gray-700'>FPS</div>
30+
<div>{metadata.fps.toFixed(2)}</div>
31+
</div>
32+
)}
33+
34+
{metadata.videoCodec && (
35+
<div>
36+
<div className='font-medium text-gray-700'>Video Codec</div>
37+
<div>{metadata.videoCodec}</div>
38+
</div>
39+
)}
40+
41+
{metadata.audioCodec && (
42+
<div>
43+
<div className='font-medium text-gray-700'>Audio Codec</div>
44+
<div>{metadata.audioCodec}</div>
45+
</div>
46+
)}
47+
48+
{metadata.size != null && (
49+
<div>
50+
<div className='font-medium text-gray-700'>File Size</div>
51+
<div>{formatFileSize(metadata.size)}</div>
52+
</div>
53+
)}
54+
55+
{metadata.container && (
56+
<div>
57+
<div className='font-medium text-gray-700'>Container</div>
58+
<div>{metadata.container}</div>
59+
</div>
60+
)}
61+
62+
{metadata.isHdr && (
63+
<div>
64+
<div className='font-medium text-gray-700'>HDR</div>
65+
<div>{metadata.isHdr ? 'Yes' : 'No'}</div>
66+
</div>
67+
)}
68+
69+
{metadata.sampleRate != null && (
70+
<div>
71+
<div className='font-medium text-gray-700'>Audio Sample Rate</div>
72+
<div>{metadata.sampleRate} Hz</div>
73+
</div>
74+
)}
75+
76+
{metadata.numberOfAudioChannels != null && (
77+
<div>
78+
<div className='font-medium text-gray-700'>Audio Channels</div>
79+
<div>{metadata.numberOfAudioChannels}</div>
80+
</div>
81+
)}
82+
83+
{metadata.mimeType && (
84+
<div>
85+
<div className='font-medium text-gray-700'>MIME Type</div>
86+
<div>{metadata.mimeType}</div>
87+
</div>
88+
)}
89+
</div>
90+
)
91+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.vds-captions {
2+
z-index: 10;
3+
bottom: 0;
4+
transition: bottom 0.15s linear;
5+
6+
/* Base caption styling */
7+
--media-captions-padding: 1%;
8+
--media-cue-backdrop: none;
9+
--media-cue-bg: rgba(0, 0, 0, 0.25);
10+
--media-cue-color: white;
11+
--media-cue-font-size: calc(var(--overlay-height) / 100 * 4.5);
12+
--media-cue-line-height: calc(var(--media-cue-font-size) * 1.2);
13+
--media-cue-padding-x: calc(var(--media-cue-font-size) * 0.4);
14+
--media-cue-padding-y: calc(var(--media-cue-font-size) * 0.6);
15+
--media-cue-text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
16+
font-weight: 800;
17+
text-transform: uppercase;
18+
}
19+
20+
/* Highlighted word style */
21+
.vds-captions .highlight {
22+
background: rgba(0, 0, 0, 0.45);
23+
color: #FFD700;
24+
border-radius: 8px;
25+
padding: 0.3em 0.6em;
26+
text-shadow: 0 0 10px rgba(255, 215, 0, 0.25);
27+
display: inline-block;
28+
}
29+
30+
/* Ensure captions are positioned correctly */
31+
.vds-captions [data-part='cue-display'] {
32+
--cue-top: 80% !important;
33+
}
34+
35+
:root {
36+
--subtitle-bg-opacity: 0.85;
37+
--subtitle-highlight-color: #FFD700;
38+
--subtitle-font-size: 1rem;
39+
}
Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,92 @@
11
import { languages } from '@nmit-coursition/utils'
2+
import { transformToHighlightedVTT } from '@nmit-coursition/utils'
23
import { MediaPlayer, type MediaPlayerInstance, MediaProvider, type MediaSrc, Track } from '@vidstack/react'
3-
import { DefaultVideoLayout, defaultLayoutIcons } from '@vidstack/react/player/layouts/default'
4-
import { useEffect, useRef } from 'react'
4+
import {
5+
DefaultMenuCheckbox,
6+
DefaultMenuItem,
7+
DefaultVideoLayout,
8+
defaultLayoutIcons,
9+
} from '@vidstack/react/player/layouts/default'
10+
import { useEffect, useRef, useState } from 'react'
511

612
import '@vidstack/react/player/styles/default/theme.css'
713
import '@vidstack/react/player/styles/default/layouts/video.css'
14+
import './video-player.css'
815

916
export type { MediaSrc } from '@vidstack/react'
1017

1118
interface VideoPlayerProps {
1219
source: MediaSrc
1320
subtitles?: string
1421
subtitlesLang?: keyof typeof languages
22+
aspectRatio?: string
1523
}
1624

17-
export function VideoPlayer({ source, subtitles, subtitlesLang = 'en-gb' }: VideoPlayerProps) {
25+
function HighlightSettings({
26+
highlightEnabled,
27+
handleHighlightToggle,
28+
}: { highlightEnabled: boolean; handleHighlightToggle: (checked: boolean) => void }) {
29+
return (
30+
<DefaultMenuItem label='Short popup style'>
31+
<DefaultMenuCheckbox
32+
label='Short popup style'
33+
checked={highlightEnabled}
34+
onChange={(checked, trigger) => trigger?.isTrusted && handleHighlightToggle(checked)}
35+
/>
36+
</DefaultMenuItem>
37+
)
38+
}
39+
40+
export function VideoPlayer({ source, subtitles, subtitlesLang = 'en-gb', aspectRatio }: VideoPlayerProps) {
1841
const player = useRef<MediaPlayerInstance>(null)
42+
const [highlightingEnabled, setHighlightingEnabled] = useState(false)
43+
const [highlightedSubtitles, setHighlightedSubtitles] = useState<string>()
44+
45+
useEffect(() => {
46+
if (!subtitles) return
47+
48+
fetch(subtitles)
49+
.then((response) => response.text())
50+
.then((content) => {
51+
const highlighted = transformToHighlightedVTT(content, 'vtt')
52+
const blob = new Blob([highlighted], { type: 'text/vtt' })
53+
const url = URL.createObjectURL(blob)
54+
setHighlightedSubtitles(url)
55+
return () => URL.revokeObjectURL(url)
56+
})
57+
.catch((error) => {
58+
console.error('Failed to transform subtitles:', error)
59+
})
60+
}, [subtitles])
1961

2062
useEffect(() => {
2163
if (!player.current) return
22-
player.current.startLoading()
23-
}, [source])
64+
const textTracks = player.current.textTracks
65+
if (textTracks[0]) {
66+
textTracks[0].mode = 'showing'
67+
}
68+
}, [highlightingEnabled])
2469

2570
return (
26-
<MediaPlayer ref={player} src={source} className='w-full aspect-video' load='visible'>
71+
<MediaPlayer ref={player} src={source} load='visible' className='h-full w-full' aspectRatio={aspectRatio}>
2772
<MediaProvider>
28-
{subtitles && (
29-
<Track src={subtitles} kind='subtitles' label={languages[subtitlesLang]} lang={subtitlesLang} default />
30-
)}
73+
<Track
74+
src={highlightingEnabled ? highlightedSubtitles : subtitles}
75+
kind='subtitles'
76+
label={languages[subtitlesLang]}
77+
lang={subtitlesLang}
78+
default
79+
/>
3180
</MediaProvider>
32-
<DefaultVideoLayout icons={defaultLayoutIcons} thumbnails='' />
81+
<DefaultVideoLayout
82+
icons={defaultLayoutIcons}
83+
menuGroup='bottom'
84+
slots={{
85+
captionsMenuItemsStart: (
86+
<HighlightSettings highlightEnabled={highlightingEnabled} handleHighlightToggle={setHighlightingEnabled} />
87+
),
88+
}}
89+
></DefaultVideoLayout>
3390
</MediaPlayer>
3491
)
3592
}

0 commit comments

Comments
 (0)