Next.js中Plyr启用withCredentials时崩溃报n未定义错误
问题根源
- 执行时机错误:所有依赖浏览器API(
Hls/Plyr/document/window)的初始化逻辑写在了组件主渲染流程中,Next.js服务端渲染+客户端hydration阶段会出现DOM结构不匹配、实例重复初始化问题,直接导致播放器样式错乱,压缩后的JS变量未定义抛出Uncaught ReferenceError: n is not defined错误。 - 配置传递错误:你定义了带
withCredentials的XHR配置,但初始化Hls实例时未传入该配置;同时video标签未配置跨域属性,Safari原生HLS播放场景下不会携带Cookie。 - CORS配置不匹配:开启
withCredentials的跨域请求禁止使用*作为Access-Control-Allow-Origin的返回值,必须明确指定允许的前端域名,同时返回Access-Control-Allow-Credentials: true头,否则XHR请求会被浏览器拦截,触发hls.js内部异常。 - 全局变量污染:将hls、player实例挂载到
window对象上,多实例/多次渲染时会出现变量覆盖,导致逻辑错乱。
修复步骤
- 将所有浏览器端专属的初始化逻辑全部移入
useEffect钩子执行,避免SSR阶段操作DOM/浏览器API;使用ref获取video元素、存储第三方实例,替代全局window挂载。 - 初始化Hls实例时正确传入带
withCredentials的配置,video标签添加跨域属性。 - 调整AWS Cloudfront响应头配置:
Access-Control-Allow-Origin配置为你的前端实际域名,禁止使用*通配符- 新增
Access-Control-Allow-Credentials: true响应头 - 配置
Access-Control-Allow-Methods: GET, OPTIONS - Cloudfront缓存策略放行Cookie头,避免签名Cookie被缓存拦截
- 正确导入Plyr样式文件,避免样式丢失。
修复后完整组件代码
如果使用Next.js App Router,需要在组件顶部添加'use client'指令标记为客户端组件。
'use client'; import { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; // 不要在顶层直接导入hls.js/plyr,避免SSR打包报错 const Player4 = ({options}) => { const dispatch = useDispatch(); const {activeChapter} = useSelector(state => state.product.viewData); // 统一用ref存储DOM、实例、状态,避免全局污染 const videoRef = useRef(null); const hlsRef = useRef(null); const playerRef = useRef(null); const componentMountedRef = useRef(false); const activeChapterRef = useRef(activeChapter); const updateDurationIntervalRef = useRef(null); activeChapterRef.current = activeChapter; // 清晰度更新逻辑 const updateQuality = newQuality => { const hls = hlsRef.current; if (!hls) return; hls.levels.forEach((level, levelIndex) => { if (level.height === newQuality) { hls.currentLevel = levelIndex; } }); } // 实例销毁逻辑 const destroyHLS = () => { if (updateDurationIntervalRef.current) { clearInterval(updateDurationIntervalRef.current); updateDurationIntervalRef.current = null; } const hls = hlsRef.current; const player = playerRef.current; if (hls) { hls.stopLoad(); hls.detachMedia(); hls.destroy(); hlsRef.current = null; } if (player) { player.destroy(); playerRef.current = null; } }; // 所有客户端初始化逻辑放入useEffect,仅在浏览器端执行 useEffect(() => { componentMountedRef.current = true; const initPlayer = async () => { // 动态导入浏览器专属依赖 const Hls = (await import('hls.js')).default; const Plyr = (await import('plyr')).default; // 导入Plyr样式,若全局已导入可删除此行 await import('plyr/dist/plyr.css'); const video = videoRef.current; if (!video) return; // 绑定播放器事件 const bindPlayerEvents = (player) => { player.on('ready', () => { updateDurationIntervalRef.current = setInterval(() => { if (!player.paused && componentMountedRef.current) { dispatch(updateWatchedDuration(activeChapterRef.current?.id, player.currentTime)); } }, 4000); }); const rebindDurationTimer = () => { if (componentMountedRef.current && !updateDurationIntervalRef.current) { updateDurationIntervalRef.current = setInterval(() => { dispatch(updateWatchedDuration(activeChapterRef.current?.id, player.currentTime)); }, 4000); } } player.on('emptied', rebindDurationTimer); player.on('ended', rebindDurationTimer); player.on('loadedmetadata', () => { dispatch(updateWatchedDuration(activeChapterRef.current?.id, activeChapterRef.current?.watched_duration)); player.poster = activeChapterRef.current?.thumbnail?.url; player.currentTime = activeChapterRef.current?.watched_duration || 0; // 捕获自动播放被浏览器拦截的错误 if (player.paused) player.play().catch(() => {}); }); } const defaultOptions = { debug: false }; // Safari原生HLS支持场景 if (!Hls.isSupported()) { if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = options.source.src; video.crossOrigin = 'use-credentials'; const player = new Plyr(video, defaultOptions); playerRef.current = player; bindPlayerEvents(player); } return; } // 先销毁已存在的实例 if (hlsRef.current) destroyHLS(); // 正确传入带withCredentials的配置 const hlsConfig = { xhrSetup: (xhr) => { xhr.withCredentials = true; }, }; const hls = new Hls(hlsConfig); hlsRef.current = hls; hls.loadSource(options.source.src); hls.on(Hls.Events.MANIFEST_PARSED, () => { const availableQualities = hls.levels.map((l) => l.height); defaultOptions.quality = { default: availableQualities[0], options: availableQualities, forced: true, onChange: updateQuality, } const player = new Plyr(video, defaultOptions) playerRef.current = player; bindPlayerEvents(player); }); hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => { const span = document.querySelector(".plyr__menu__container [data-plyr='quality'][value='0'] span") if (span) { span.innerHTML = hls.autoLevelEnabled ? `AUTO (${hls.levels[data.level].height}p)` : `AUTO`; } }) hls.attachMedia(video); } initPlayer(); // 组件卸载时清理所有实例和定时器 return () => { componentMountedRef.current = false; destroyHLS(); } }, [options.source.src, dispatch]) return ( <video ref={videoRef} id="player" autoPlay playsInline controls crossOrigin="use-credentials" /> ); }; export default Player4;
额外注意事项
- 若使用Next.js Pages Router,无需添加
'use client'指令,只要保证所有浏览器逻辑在useEffect中执行即可。 - 自动播放逻辑必须加catch捕获,现代浏览器默认禁止未经用户交互的音视频自动播放,未捕获的错误会打断后续逻辑执行。
- 若项目已在全局入口导入Plyr样式,可删除代码中动态导入CSS的逻辑,避免重复加载。
内容的提问来源于stack exchange,提问作者fahad shaikh
相关产品推荐
相关产品推荐

