diff --git a/client/src/components/DrmVideo.jsx b/client/src/components/DrmVideo.jsx index b607f08..2b94bf3 100644 --- a/client/src/components/DrmVideo.jsx +++ b/client/src/components/DrmVideo.jsx @@ -1,7 +1,44 @@ import { useEffect, useRef, useState } from 'react' import shaka from 'shaka-player' +import ServerDrmVideo from './ServerDrmVideo' -export default function DrmVideo({ dashSrc, cookies, mediaId, entityId, entityType, poster, className = '' }) { +function isWidevineSupported() { + const ua = navigator.userAgent + // Safari (desktop & mobile) and all iOS browsers (WebKit-only) lack Widevine + if (/Safari/i.test(ua) && !/Chrome/i.test(ua)) return false + if (/iPad|iPhone|iPod/i.test(ua)) return false + return true +} + +export default function DrmVideo({ dashSrc, mpdUrl, cookies, mediaId, entityId, entityType, poster, className = '' }) { + if (!isWidevineSupported()) { + return ( + + ) + } + + return ( + + ) +} + +function ShakaPlayer({ dashSrc, cookies, mediaId, entityId, entityType, poster, className = '' }) { const videoRef = useRef(null) const playerRef = useRef(null) const [error, setError] = useState(null) diff --git a/client/src/components/MediaGrid.jsx b/client/src/components/MediaGrid.jsx index fe48f4c..5fb8e37 100644 --- a/client/src/components/MediaGrid.jsx +++ b/client/src/components/MediaGrid.jsx @@ -52,6 +52,7 @@ function getDrmDashInfo(item, { entityId, entityType } = {}) { return { dashSrc: `/api/drm-hls?url=${encodeURIComponent(drm.manifest.dash)}&cp=${encodeURIComponent(sig['CloudFront-Policy'])}&cs=${encodeURIComponent(sig['CloudFront-Signature'])}&ck=${encodeURIComponent(sig['CloudFront-Key-Pair-Id'])}`, + mpdUrl: drm.manifest.dash, cookies: { cp: sig['CloudFront-Policy'], cs: sig['CloudFront-Signature'], @@ -128,6 +129,7 @@ function MediaItem({ item, className = '', entityId, entityType }) { > { + setState('processing') + try { + const res = await fetch('/api/drm-stream/decrypt', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mpdUrl, cfCookies: cookies, mediaId, entityType, entityId }), + }) + const data = await res.json() + if (!mountedRef.current) return + + if (!res.ok) { + setState('error') + setError(data.error || `Server error ${res.status}`) + return + } + + if (data.status === 'ready') { + setState('ready') + } + // else 'processing' — polling will pick it up + } catch (err) { + if (!mountedRef.current) return + setState('error') + setError(err.message) + } + }, [mpdUrl, cookies, mediaId, entityType, entityId]) + + // Start decrypt on mount + useEffect(() => { + mountedRef.current = true + triggerDecrypt() + return () => { mountedRef.current = false } + }, [triggerDecrypt]) + + // Poll for status while processing + useEffect(() => { + if (state !== 'processing') return + + pollRef.current = setInterval(async () => { + try { + const res = await fetch(`/api/drm-stream/${mediaId}/status`) + const data = await res.json() + if (!mountedRef.current) return + + if (data.status === 'ready') { + setState('ready') + } else if (data.status === 'error') { + setState('error') + setError(data.error || 'Decryption failed') + } else if (data.status === 'none') { + // Server restarted, re-trigger + triggerDecrypt() + } + } catch {} + }, 2000) + + return () => { + if (pollRef.current) clearInterval(pollRef.current) + } + }, [state, mediaId, triggerDecrypt]) + + if (state === 'ready') { + return ( +