视频流加载失败:NotSupportedError问题排查求助
视频流加载失败排查求助
我遇到了视频流加载失败的问题,具体流程如下:
- 前端有一个1080p视频源,先请求后端获取seriesName,拼接出视频完整路径后再次发送给后端;
- 后端通过FFmpeg将视频转码为720p和480p,保存到临时存储后返回两个视频路径;
- 前端根据选择的画质获取对应路径,发起带Range头的GET请求获取视频流,将返回数据转为Blob并生成Blob URL,赋值给video标签的src属性。
最终出现错误:Vinland-Saga:1 Uncaught (in promise) NotSupportedError: Failed to load because no supported source was found,尝试多种方法仍无法定位原因,求帮忙排查。
相关代码
前端获取视频路径与发送请求代码
const req = async()=>{try{const res = await axios.get('/catalog/item',{params:{SeriesName:seriesName}});return {data:res.data};}catch(err){console.log(err);return false;}}const fetchedData = await req(); const videoUrl = 'C:/Users/arMori/Desktop/RedditClone/reddit/public/videos';console.log('IT VideoURL',videoUrl); const selectedFile = `${videoUrl}/${fetchedData.data.VideoSource}/${seriesName}-1080p.mp4` console.log(`ITS'S SELECTED FILE: ${selectedFile}`); const response = await axios.post('/videoFormat', {videoUrl:selectedFile})console.log('Это консоль лог путей: ',response.data);const videoPaths = response.data;
后端转码服务代码
async videoUpload(videoUrl:string){try{const tempDir = 'C:/Users/arMori/Desktop/RedditClone/reddit_back/src/video/temp';const inputFile = videoUrl;console.log('VIDEOURL: ',videoUrl); const outputFiles = []; await this.createDirectories(tempDir); outputFiles.push(await this.convertVideo(inputFile, '1280x720', '720p.mp4')); outputFiles.push(await this.convertVideo(inputFile, '854x480', '480p.mp4')); console.log('OUTUPT FILES SERVICE: ',outputFiles); return outputFiles; }catch(err){ console.error('VideoFormatterService Error: ',err); } } private convertVideo(inputPath:string,resolution:string,outputFileName:string):Promise<string>{ const temp = `C:/Users/arMori/Desktop/RedditClone/reddit_back/src/video/temp`; return new Promise(async(resolve,reject)=>{ const height = resolution.split('x')[1]; console.log('HIEGHT: ',height); const outputDir = `C:/Users/arMori/Desktop/RedditClone/reddit_back/src/video/temp/${height}p`; const outputPath = join(outputDir, outputFileName); const isExists = await fs.access(outputPath).then(() => true).catch(() => false); if(isExists){ console.log(`File already exists: ${outputPath}`); return resolve(outputPath) }; ffmpeg(inputPath) .size(`${resolution}`) .videoCodec('libx264') // Кодек H.264 .audioCodec('aac') .output(outputPath) .on('end',()=>resolve(outputPath)) .on('error',(err)=>reject(err)) .run() }) } private async createDirectories(temp:string){ try{ const dir720p = `${temp}/720p`; const dir480p = `${temp}/480p`; const dir720pExists = await fs.access(dir720p).then(() => true).catch(() => false); const dir480pExists = await fs.access(dir480p).then(() => true).catch(() => false); if(dir720pExists && dir480pExists){ console.log('FILES ALIVE'); return; } if (!dir720pExists) { await fs.mkdir(dir720p, { recursive: true }); console.log('Папка 720p создана'); } if (!dir480pExists) { await fs.mkdir(dir480p, { recursive: true }); console.log('Папка 480p создана'); } } catch (err) { console.error('Ошибка при создании директорий:', err); } }
前端获取视频流与赋值代码
let videoPath; if (quality === '720p') { videoPath = videoPaths[0]; } else if (quality === '480p') { videoPath = videoPaths[1]; } if (!videoPath) { console.error('Video path not found!'); return; } console.log('VIDEOPATH LOG: ',videoPath); try { const videoRes = await axios.get('/videoFormat/getVideo', { params: { path: videoPath } , headers: { Range: 'bytes=0-' }, responseType: 'blob' }); console.log('Video fetched: ', videoRes); const videoBlob = new Blob([videoRes.data], { type: 'video/mp4' }); const videoURL = URL.createObjectURL(videoBlob); return videoURL; } catch (error) { console.error('Ошибка при загрузке видео:', error); } } useVideo(seriesName,quality).then(src => { if (src) { console.log('ITS VIDEOLOGISC GOIDA!'); if(!playRef.current) return; const oldURL = playRef.current.src; if (oldURL && oldURL.startsWith('blob:')) { URL.revokeObjectURL(oldURL); } playRef.current.pause(); playRef.current.src = ''; setQuality(quality); console.log('SRC: ',src); playRef.current.src = src; playRef.current.load(); console.log('ITS VIDEOURL GOIDA!'); togglePlayPause(); } }) .catch(err => console.error('Failed to fetch video', err));
后端视频流接口代码
@Public() @Get('/getVideo') async getVideo(@Query('path') videoPath:string,@Req() req:Request,@Res() res:Response){ try { console.log('PATH ARGUMENT: ',videoPath); console.log('VIDEOPATH IN SERVICE: ',videoPath); const videoSize = (await fs.stat(videoPath)).size; const CHUNK_SIZE = 10 ** 6; const range = req.headers['range'] as string | undefined; if (!range) { return new ForbiddenException('Range не найденно'); } const start = Number(range.replace(/\D/g,"")); const end = Math.min(start + CHUNK_SIZE,videoSize - 1); const contentLength = end - start + 1; const videoStream = fsSync.createReadStream(videoPath, { start, end }); const headers = { 'Content-Range':`bytes ${start}-${end}/${videoSize}`, 'Accept-Ranges':'bytes', 'Content-Length':contentLength, 'Content-Type':'video/mp4' } res.writeHead(206,headers); videoStream.pipe(res); videoStream.on('error', (error) => { console.error('Ошибка при чтении видео:', error); res.status(500).send('Ошибка при чтении видео'); }); } catch (error) { console.error('Ошибка при обработке запросов:', error); return res.status(400).json({ message: 'Ошибка при обработке getVideo запросов' }); } }
排查方向建议
1. 转码视频格式兼容性问题
浏览器对MP4的编码有严格要求,必须是H.264视频编码+AAC音频编码,且文件结构需要是moov原子前置(索引信息在文件开头,方便流媒体加载)。
- 转码时添加FFmpeg参数
-movflags +faststart,将moov原子移到文件头部:ffmpeg(inputPath) .size(`${resolution}`) .videoCodec('libx264') .audioCodec('aac') .outputOptions('-movflags +faststart') // 新增该行 .output(outputPath) .on('end',()=>resolve(outputPath)) .on('error',(err)=>reject(err)) .run() - 直接检查转码后的视频:用本地播放器确认能正常播放,用
ffprobe命令查看编码信息是否符合要求。
2. Blob中转导致的流媒体识别问题
前端通过axios获取Range片段后转成Blob,再生成Blob URL,video标签可能无法正确识别这种拼接后的片段(缺少完整文件索引)。
- 直接将后端接口地址赋值给video标签src,跳过Blob中转:
浏览器会自动处理Range请求,符合标准流媒体加载流程。// 替换原Blob生成逻辑 const videoURL = `/videoFormat/getVideo?path=${encodeURIComponent(videoPath)}`; return videoURL;
3. 文件路径与权限问题
- 后端返回的
videoPath是本地绝对路径,前端传参时要确保路径无转义错误(如空格、特殊字符),且后端服务有读取该文件的权限。 - 在
getVideo接口中打印完整videoPath,用fs.existsSync确认文件存在,避免路径拼写错误。
4. Range请求处理逻辑验证
- 测试直接访问
/videoFormat/getVideo?path=xxx(带Range头),查看响应头是否符合规范,将响应二进制数据保存为MP4,确认能正常播放。 - 检查
Content-Range格式是否正确,确保浏览器能解析片段范围。
5. 前端video标签加载逻辑优化
- 替换为直接接口地址后,可去掉
playRef.current.load()调用,设置src后浏览器会自动加载;若保留需确保调用时机正确。 - 给video标签添加
type="video/mp4"属性,明确告知浏览器视频格式。
内容的提问来源于stack exchange,提问作者Aizen
相关产品推荐
相关产品推荐

