Skip to content

Commit 05da8d8

Browse files
committed
feat(ui): add adaptive audio player
1 parent b9c4de0 commit 05da8d8

2 files changed

Lines changed: 45 additions & 7 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"name":"@stacksjs/audio","type":"module","sideEffects":false,"version":"0.70.162","description":"Native audio delivery planning for Stacks.","author":"Chris Breuer","license":"MIT","funding":"https://github.com/sponsors/chrisbbreuer","repository":{"type":"git","url":"git+https://github.com/stacksjs/stacks.git","directory":"./storage/framework/core/audio"},"exports":{".":{"types":"./dist/index.d.ts","development":"./src/index.ts","bun":"./dist/index.js","import":"./dist/index.js"}},"module":"dist/index.js","types":"dist/index.d.ts","files":["README.md","dist"],"scripts":{"build":"bun build.ts","typecheck":"bun tsc --noEmit","prepublishOnly":"bun run build"},"devDependencies":{"better-dx":"catalog:"}}
1+
{"name":"@stacksjs/audio","type":"module","sideEffects":false,"version":"0.70.162","description":"Native audio delivery planning for Stacks.","author":"Chris Breuer","license":"MIT","funding":"https://github.com/sponsors/chrisbbreuer","repository":{"type":"git","url":"git+https://github.com/stacksjs/stacks.git","directory":"./storage/framework/core/audio"},"exports":{".":{"types":"./dist/index.d.ts","development":"./src/index.ts","bun":"./dist/index.js","import":"./dist/index.js"}},"module":"dist/index.js","types":"dist/index.d.ts","files":["README.md","dist"],"scripts":{"build":"bun build.ts","typecheck":"bun tsc --noEmit","prepublishOnly":"bun run build"},"dependencies":{"ts-video-player":"^0.1.0"},"devDependencies":{"better-dx":"catalog:"}}

storage/framework/defaults/resources/components/Audio.stx

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
<script server>
22
interface 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+
}
38
interface AudioTrack { src: string, kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata', srclang?: string, label?: string, default?: boolean }
49
interface 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
2935
const preload = props.preload || 'metadata'
3036
const crossorigin = props.crossorigin || ''
3137
const 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

Comments
 (0)