diff --git a/package-lock.json b/package-lock.json index fcb5e74..bb3c226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.1", "dependencies": { "@atproto/api": "^0.17.2", - "@lucide/svelte": "^0.545.0" + "@lucide/svelte": "^0.545.0", + "hls.js": "^1.5.19" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", @@ -1571,6 +1572,12 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "license": "MIT" }, + "node_modules/hls.js": { + "version": "1.6.14", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz", + "integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==", + "license": "Apache-2.0" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", diff --git a/package.json b/package.json index efc4a84..6ae00a7 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@atproto/api": "^0.17.2", - "@lucide/svelte": "^0.545.0" + "@lucide/svelte": "^0.545.0", + "hls.js": "^1.5.19" } } diff --git a/src/lib/components/layout/main/card/BlueskyPostCard.svelte b/src/lib/components/layout/main/card/BlueskyPostCard.svelte index 40d916e..83bed7a 100644 --- a/src/lib/components/layout/main/card/BlueskyPostCard.svelte +++ b/src/lib/components/layout/main/card/BlueskyPostCard.svelte @@ -5,12 +5,14 @@ import { formatRelativeTime } from '$lib/utils/formatDate'; import { formatCompactNumber } from '$lib/utils/formatNumber'; import { Heart, Repeat2, MessageCircle, ExternalLink, X } from '@lucide/svelte'; + import Hls from 'hls.js'; let post: BlueskyPost | null = null; let loading = true; let error: string | null = null; let lightboxImage: { url: string; alt: string } | null = null; let pollInterval: ReturnType | null = null; + let videoElements = new Map(); // Detect system locale, fallback to en-GB const locale = typeof navigator !== 'undefined' ? navigator.language || 'en-GB' : 'en-GB'; @@ -51,6 +53,13 @@ clearInterval(pollInterval); pollInterval = null; } + // Clean up all HLS instances + videoElements.forEach(({ hls }) => { + if (hls) { + hls.destroy(); + } + }); + videoElements.clear(); }); function getPostUrl(uri: string): string { @@ -113,6 +122,57 @@ div.textContent = text; return div.innerHTML; } + + function setupVideo(videoElement: HTMLVideoElement, videoUrl: string) { + if (!videoElement || !videoUrl) return; + + // Clean up existing HLS instance for this video + const existing = videoElements.get(videoUrl); + if (existing?.hls) { + existing.hls.destroy(); + } + + let hls: Hls | null = null; + + // Check if HLS is supported + if (videoUrl.includes('.m3u8')) { + if (Hls.isSupported()) { + hls = new Hls({ + enableSoftwareAES: true, + maxBufferLength: 30, + maxMaxBufferLength: 600 + }); + hls.loadSource(videoUrl); + hls.attachMedia(videoElement); + hls.on(Hls.Events.MANIFEST_PARSED, () => { + console.log('[HLS] Video ready to play'); + }); + hls.on(Hls.Events.ERROR, (event, data) => { + if (data.fatal) { + console.error('[HLS] Fatal error:', data); + } + }); + videoElements.set(videoUrl, { element: videoElement, hls }); + } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) { + // Native HLS support (Safari) + videoElement.src = videoUrl; + videoElements.set(videoUrl, { element: videoElement, hls: null }); + } + } else { + // Regular video file + videoElement.src = videoUrl; + videoElements.set(videoUrl, { element: videoElement, hls: null }); + } + + return { + destroy() { + if (hls) { + hls.destroy(); + } + videoElements.delete(videoUrl); + } + }; + } {#snippet postContent(postData: BlueskyPost, depth: number = 0, isReplyParent: boolean = false)} @@ -199,7 +259,7 @@ {#if postData.hasVideo && postData.videoUrl}