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:
Dorian
2026-02-13 22:36:41 +00:00
parent f715534c06
commit 0917410c9e
4 changed files with 206 additions and 53 deletions

View File

@@ -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.'