fix: add stream diagnostics and project-ID fallback for video playback
- Stream endpoint now accepts both content ID and project ID, falling back to project lookup when content ID is not found - Added /contents/:id/stream-debug diagnostic endpoint that checks file existence in both private and public MinIO buckets - Stream endpoint now verifies raw file exists before generating presigned URL, returning a clear error if file is missing - Added comprehensive logging throughout the stream pipeline - VideoPlayer now logs stream URL, API responses, and playback errors to browser console for easier debugging - Bumped CACHEBUST for frontend (19), API (11), and ffmpeg-worker (13) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -296,71 +296,72 @@ onUnmounted(() => {
|
||||
// ── Stream fetching ─────────────────────────────────────────────────
|
||||
|
||||
async function fetchStream() {
|
||||
// Prefer the content entity ID; fall back to the project ID.
|
||||
// The backend stream endpoint now accepts both — it will look up
|
||||
// content by ID first, then try project ID if not found.
|
||||
const contentId = props.content?.contentId || props.content?.id
|
||||
if (!contentId) {
|
||||
streamError.value = 'Content ID not available.'
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[VideoPlayer] fetchStream — contentId:', contentId,
|
||||
'content.contentId:', props.content?.contentId,
|
||||
'content.id:', props.content?.id)
|
||||
|
||||
isLoadingStream.value = true
|
||||
streamError.value = null
|
||||
|
||||
try {
|
||||
// Try the backend stream endpoint first (handles DRM, presigned URLs, etc.)
|
||||
// Try the backend stream endpoint (handles DRM, presigned URLs, etc.)
|
||||
const info = await indeehubApiService.getStreamingUrl(contentId)
|
||||
console.log('[VideoPlayer] Stream API response:', JSON.stringify(info))
|
||||
|
||||
// Check if backend returned an error message inside the response
|
||||
const errorMsg = (info as any).error
|
||||
if (errorMsg) {
|
||||
console.warn('[VideoPlayer] Backend returned error:', errorMsg)
|
||||
streamError.value = errorMsg
|
||||
return
|
||||
}
|
||||
|
||||
const streamFile = (info as any).file || info.url
|
||||
if (!streamFile) {
|
||||
console.error('[VideoPlayer] No stream URL in response:', info)
|
||||
streamError.value = 'No video URL returned from server.'
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[VideoPlayer] Playing URL:', streamFile)
|
||||
hlsStreamUrl.value = streamFile
|
||||
await nextTick()
|
||||
initPlayer(streamFile)
|
||||
} catch (err: any) {
|
||||
console.warn('Stream API failed, trying direct HLS URL fallback:', err?.response?.status || err?.message)
|
||||
const status = err?.response?.status
|
||||
const responseData = err?.response?.data
|
||||
console.warn('[VideoPlayer] Stream API failed:', status, responseData || err?.message)
|
||||
|
||||
// Fallback: construct the HLS URL directly from the public S3 bucket.
|
||||
// The transcoded HLS files live in the public bucket at a predictable path.
|
||||
// This works because the user already passed the access check before the
|
||||
// player opened (rental verified in ContentDetailModal.handlePlay).
|
||||
const directUrl = buildDirectHlsUrl()
|
||||
if (directUrl) {
|
||||
console.log('Using direct HLS URL:', directUrl)
|
||||
hlsStreamUrl.value = directUrl
|
||||
await nextTick()
|
||||
initPlayer(directUrl)
|
||||
// If the error is auth-related, show a clear message
|
||||
if (status === 403) {
|
||||
streamError.value = 'You need an active subscription or rental to watch this content.'
|
||||
} else if (status === 401) {
|
||||
streamError.value = 'Please sign in to watch this content.'
|
||||
} else {
|
||||
const status = err?.response?.status
|
||||
if (status === 403) {
|
||||
streamError.value = 'You need an active subscription or rental to watch this content.'
|
||||
} else if (status === 401) {
|
||||
streamError.value = 'Please sign in to watch this content.'
|
||||
} else {
|
||||
streamError.value = 'Unable to load the stream. Please try again.'
|
||||
}
|
||||
console.error('Failed to fetch stream info:', err)
|
||||
streamError.value = `Unable to load the stream (${status || 'network error'}). Please try again.`
|
||||
}
|
||||
console.error('[VideoPlayer] Failed to fetch stream info:', err)
|
||||
} finally {
|
||||
isLoadingStream.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a direct HLS URL from the content's API data.
|
||||
* The public bucket stores transcoded HLS at:
|
||||
* {CDN_URL}/projects/{projectId}/file/transcoded/file.m3u8
|
||||
*/
|
||||
function buildDirectHlsUrl(): string | null {
|
||||
const projectId = props.content?.id
|
||||
if (!projectId) return null
|
||||
|
||||
// Use the CDN/storage URL configured for the self-hosted backend
|
||||
const cdnBase = import.meta.env.VITE_INDEEHUB_CDN_URL || 'http://localhost:9000/indeedhub-public'
|
||||
return `${cdnBase}/projects/${projectId}/file/transcoded/file.m3u8`
|
||||
}
|
||||
|
||||
function initPlayer(url: string) {
|
||||
destroyHls()
|
||||
const video = videoEl.value
|
||||
if (!video) return
|
||||
|
||||
const isHls = url.includes('.m3u8')
|
||||
console.log(`[VideoPlayer] initPlayer — isHls=${isHls}, url=${url.substring(0, 120)}...`)
|
||||
|
||||
if (isHls && Hls.isSupported()) {
|
||||
hls = new Hls({
|
||||
@@ -370,15 +371,19 @@ function initPlayer(url: string) {
|
||||
hls.loadSource(url)
|
||||
hls.attachMedia(video)
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('[VideoPlayer] HLS manifest parsed, starting playback')
|
||||
video.play().catch(() => {
|
||||
// Autoplay blocked — user needs to click play
|
||||
})
|
||||
})
|
||||
hls.on(Hls.Events.ERROR, (_event, data) => {
|
||||
console.error('[VideoPlayer] HLS error:', data.type, data.details, data.fatal ? '(FATAL)' : '')
|
||||
if (data.fatal) {
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
console.warn('[VideoPlayer] HLS network error, retrying...')
|
||||
hls?.startLoad()
|
||||
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
console.warn('[VideoPlayer] HLS media error, recovering...')
|
||||
hls?.recoverMediaError()
|
||||
} else {
|
||||
streamError.value = 'Playback error. Please try again.'
|
||||
@@ -390,16 +395,26 @@ function initPlayer(url: string) {
|
||||
// Safari native HLS
|
||||
video.src = url
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
video.play().catch(() => {})
|
||||
}, { once: true })
|
||||
} else if (!isHls) {
|
||||
// Direct video file (mp4, mov, webm) — use native <video>
|
||||
video.src = url
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
console.log('[VideoPlayer] Safari HLS loaded, starting playback')
|
||||
video.play().catch(() => {})
|
||||
}, { once: true })
|
||||
video.addEventListener('error', () => {
|
||||
streamError.value = 'Unable to play this video format. Please try again.'
|
||||
const err = video.error
|
||||
console.error('[VideoPlayer] Safari HLS error:', err?.code, err?.message)
|
||||
streamError.value = `Playback error (code ${err?.code}): ${err?.message || 'Unknown'}`
|
||||
}, { once: true })
|
||||
} else if (!isHls) {
|
||||
// Direct video file (mp4, mov, webm) — use native <video>
|
||||
console.log('[VideoPlayer] Using native video playback for direct file')
|
||||
video.src = url
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
console.log('[VideoPlayer] Video metadata loaded, duration:', video.duration)
|
||||
video.play().catch(() => {})
|
||||
}, { once: true })
|
||||
video.addEventListener('error', () => {
|
||||
const err = video.error
|
||||
console.error('[VideoPlayer] Native video error:', err?.code, err?.message)
|
||||
streamError.value = `Unable to play video (code ${err?.code}): ${err?.message || 'Unknown format or network error'}`
|
||||
}, { once: true })
|
||||
} else {
|
||||
streamError.value = 'Your browser does not support HLS video playback.'
|
||||
|
||||
Reference in New Issue
Block a user