11<script server>
22interface AudioSource { src: string, type?: string }
3+ interface AudioDrm {
4+ widevine?: { url: string, headers?: Record<string, string> }
5+ playready?: { url: string, headers?: Record<string, string> }
6+ fairplay?: { url: string, certificateUrl: string, headers?: Record<string, string> }
7+ }
38interface AudioTrack { src: string, kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata', srclang?: string, label?: string, default?: boolean }
49interface AudioProps {
510 src: string
611 sources?: AudioSource[]
12+ drm?: AudioDrm
713 tracks?: AudioTrack[]
814 title?: string
915 artist?: string
@@ -29,6 +35,14 @@ const controls = props.controls !== false
2935const preload = props.preload || 'metadata'
3036const crossorigin = props.crossorigin || ''
3137const className = props.class || ''
38+ const playerSources = (sources.length ? sources : src ? [{ src }] : []).map(source => props.drm && (source.type === 'application/dash+xml' || source.src.split(/[?#]/, 1)[0].toLowerCase().endsWith('.mpd')) ? { ...source, type: 'application/dash+xml', drm: props.drm } : source)
39+ const playerTracks = tracks.map(track => ({ ...track, language: track.srclang }))
40+ const playerConfig = JSON.stringify({ sources: playerSources, tracks: playerTracks, ...(props.drm ? { drm: props.drm } : {}) }).replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026')
41+ </script>
42+
43+ <script client>
44+ import { registerElements } from 'ts-video-player/elements'
45+ registerElements()
3246</script>
3347
3448<figure class="m-0 overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-stone-200 dark:bg-stone-950 dark:ring-stone-800 {{ className }}">
@@ -44,11 +58,35 @@ const className = props.class || ''
4458 @if(waveform)<img src="{{ waveform }}" alt="" loading="lazy" decoding="async" class="object-cover mt-3 h-10 w-full opacity-70 dark:opacity-80">@endif
4559 </div>
4660 </div>
47- <div class="px-3 py-2 bg-stone-50/70 dark:bg-stone-900/60 border-stone-100 border-t dark:border-stone-800">
48- <audio src="{{ src }}" @if(controls) controls @endif @if(props.autoplay) autoplay @endif @if(props.loop) loop @endif @if(props.muted) muted @endif preload="{{ preload }}" @if(crossorigin) crossorigin="{{ crossorigin }}" @endif aria-label="{{ title }}" class="block h-12 w-full accent-sky-600">
49- @foreach (sources as source)<source src="{{ source.src }}" @if(source.type) type="{{ source.type }}" @endif>@endforeach
50- @foreach (tracks as track)<track src="{{ track.src }}" kind="{{ track.kind }}" @if(track.srclang) srclang="{{ track.srclang }}" @endif @if(track.label) label="{{ track.label }}" @endif @if(track.default) default @endif>@endforeach
51- Your browser does not support audio playback.
52- </audio>
61+ <div class="p-3 bg-stone-950 border-stone-100 border-t dark:border-stone-800">
62+ <video-player src="{{ src }}" title="{{ title }}" data-media-config="{{ playerConfig }}" @if(controls) controls @endif @if(props.autoplay) autoplay @endif @if(props.loop) loop @endif @if(props.muted) muted @endif preload="{{ preload }}" @if(crossorigin) crossorigin="{{ crossorigin }}" keep-alive aria-label="{{ title }}" class="block overflow-hidden relative h-16 w-full text-white rounded-xl focus-within:ring-2 focus-within:ring-sky-500">
63+ <media-error-display></media-error-display>
64+ <audio data-native-fallback src="{{ src }}" @if(controls) controls @endif @if(props.autoplay) autoplay @endif @if(props.loop) loop @endif @if(props.muted) muted @endif preload="{{ preload }}" @if(crossorigin) crossorigin="{{ crossorigin }}" @endif aria-label="{{ title }}" class="block h-16 w-full accent-sky-500">
65+ @foreach (sources as source)<source src="{{ source.src }}" @if(source.type) type="{{ source.type }}" @endif>@endforeach
66+ @foreach (tracks as track)<track src="{{ track.src }}" kind="{{ track.kind }}" @if(track.srclang) srclang="{{ track.srclang }}" @endif @if(track.label) label="{{ track.label }}" @endif @if(track.default) default @endif>@endforeach
67+ Your browser does not support audio playback.
68+ </audio>
69+ @if(controls)
70+ <video-skin>
71+ <media-seek-button slot="left" seconds="-10" aria-label="Back 10 seconds"></media-seek-button>
72+ <media-play-button slot="left" aria-label="Play or pause"></media-play-button>
73+ <media-seek-button slot="left" seconds="10" aria-label="Forward 10 seconds"></media-seek-button>
74+ <media-time-group slot="center" class="font-mono tabular-nums text-xs"></media-time-group>
75+ <media-progress-bar slot="center" @if(waveform) waveform="{{ waveform }}" @endif aria-label="Audio progress"></media-progress-bar>
76+ <media-mute-button slot="right" aria-label="Mute or unmute"></media-mute-button>
77+ <media-volume-slider slot="right" aria-label="Volume"></media-volume-slider>
78+ <media-playback-rate-button slot="right" rates="0.5,0.75,1,1.25,1.5,2" aria-label="Playback speed"></media-playback-rate-button>
79+ <media-settings-menu slot="right" aria-label="Audio settings"></media-settings-menu>
80+ <media-airplay-button slot="right" aria-label="Play on AirPlay"></media-airplay-button>
81+ <media-remote-playback-button slot="right" aria-label="Play on another device"></media-remote-playback-button>
82+ </video-skin>
83+ @endif
84+ </video-player>
5385 </div>
5486</figure>
87+
88+ <style>
89+ video-player:not(:defined) video-skin { display: none; }
90+ video-player:defined > [data-native-fallback] { display: none; }
91+ @media (prefers-reduced-motion: reduce) { video-player, video-player * { transition-duration: 0.01ms !important; } }
92+ </style>
0 commit comments