You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

视频流加载失败:NotSupportedError问题排查求助

视频流加载失败排查求助

我遇到了视频流加载失败的问题,具体流程如下:

  1. 前端有一个1080p视频源,先请求后端获取seriesName,拼接出视频完整路径后再次发送给后端;
  2. 后端通过FFmpeg将视频转码为720p和480p,保存到临时存储后返回两个视频路径;
  3. 前端根据选择的画质获取对应路径,发起带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中转:
    // 替换原Blob生成逻辑
    const videoURL = `/videoFormat/getVideo?path=${encodeURIComponent(videoPath)}`;
    return videoURL;
    
    浏览器会自动处理Range请求,符合标准流媒体加载流程。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 07:02:34