summaryrefslogtreecommitdiff
path: root/src/lib/components/VideoPlayer.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/components/VideoPlayer.svelte')
-rw-r--r--src/lib/components/VideoPlayer.svelte185
1 files changed, 185 insertions, 0 deletions
diff --git a/src/lib/components/VideoPlayer.svelte b/src/lib/components/VideoPlayer.svelte
new file mode 100644
index 0000000..0829c8b
--- /dev/null
+++ b/src/lib/components/VideoPlayer.svelte
@@ -0,0 +1,185 @@
+<script lang="ts">
+ import type { VideoFormat } from '$lib/api/youtube';
+
+ interface Props {
+ formats: VideoFormat[];
+ title: string;
+ }
+
+ let { formats, title }: Props = $props();
+
+ let videoElement: HTMLVideoElement | undefined = $state();
+ let currentQuality = $state('');
+
+ const sortedFormats = $derived(
+ [...formats]
+ .filter(f => f.url && f.height)
+ .sort((a, b) => (b.height || 0) - (a.height || 0))
+ );
+
+ const selectedFormat = $derived(
+ currentQuality
+ ? sortedFormats.find(f => f.qualityLabel === currentQuality) || sortedFormats[0]
+ : sortedFormats[0]
+ );
+
+ function handleQualityChange(e: Event) {
+ const select = e.target as HTMLSelectElement;
+ const newQuality = select.value;
+ const currentTime = videoElement?.currentTime || 0;
+ const wasPlaying = videoElement && !videoElement.paused;
+
+ currentQuality = newQuality;
+
+ if (videoElement) {
+ videoElement.load();
+ videoElement.currentTime = currentTime;
+ if (wasPlaying) {
+ videoElement.play();
+ }
+ }
+ }
+
+ function handleKeydown(e: KeyboardEvent) {
+ if (!videoElement) return;
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
+
+ switch (e.key) {
+ case ' ':
+ case 'k':
+ e.preventDefault();
+ if (videoElement.paused) {
+ videoElement.play();
+ } else {
+ videoElement.pause();
+ }
+ break;
+ case 'f':
+ e.preventDefault();
+ if (document.fullscreenElement) {
+ document.exitFullscreen();
+ } else {
+ videoElement.requestFullscreen();
+ }
+ break;
+ case 'm':
+ e.preventDefault();
+ videoElement.muted = !videoElement.muted;
+ break;
+ case 'ArrowLeft':
+ e.preventDefault();
+ videoElement.currentTime -= 5;
+ break;
+ case 'ArrowRight':
+ e.preventDefault();
+ videoElement.currentTime += 5;
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ videoElement.volume = Math.min(1, videoElement.volume + 0.1);
+ break;
+ case 'ArrowDown':
+ e.preventDefault();
+ videoElement.volume = Math.max(0, videoElement.volume - 0.1);
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ e.preventDefault();
+ videoElement.currentTime = (parseInt(e.key) / 10) * videoElement.duration;
+ break;
+ }
+ }
+
+ function getMimeType(ext: string): string {
+ const types: Record<string, string> = {
+ mp4: 'video/mp4',
+ webm: 'video/webm',
+ mkv: 'video/x-matroska'
+ };
+ return types[ext] || 'video/mp4';
+ }
+</script>
+
+<svelte:window onkeydown={handleKeydown} />
+
+<div class="player-container">
+ {#if selectedFormat}
+ <video
+ bind:this={videoElement}
+ controls
+ autoplay
+ {title}
+ >
+ <source src={selectedFormat.url} type={getMimeType(selectedFormat.ext)} />
+ Your browser does not support the video tag.
+ </video>
+ {:else}
+ <div class="no-video">
+ <p>No playable video format found</p>
+ </div>
+ {/if}
+
+ {#if sortedFormats.length > 1}
+ <div class="quality-selector">
+ <label for="quality">Quality:</label>
+ <select id="quality" onchange={handleQualityChange} value={selectedFormat?.qualityLabel}>
+ {#each sortedFormats as format}
+ <option value={format.qualityLabel}>{format.qualityLabel || format.height + 'p'}</option>
+ {/each}
+ </select>
+ </div>
+ {/if}
+</div>
+
+<style>
+ .player-container {
+ width: 100%;
+ background: #000;
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ video {
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ display: block;
+ }
+
+ .no-video {
+ aspect-ratio: 16 / 9;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-muted);
+ }
+
+ .quality-selector {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background: var(--bg-secondary);
+ }
+
+ .quality-selector label {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ }
+
+ .quality-selector select {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ font-size: 0.9rem;
+ }
+</style>