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

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对象上,多实例/多次渲染时会出现变量覆盖,导致逻辑错乱。
修复步骤
  1. 将所有浏览器端专属的初始化逻辑全部移入useEffect钩子执行,避免SSR阶段操作DOM/浏览器API;使用ref获取video元素、存储第三方实例,替代全局window挂载。
  2. 初始化Hls实例时正确传入带withCredentials的配置,video标签添加跨域属性。
  3. 调整AWS Cloudfront响应头配置:
    • Access-Control-Allow-Origin配置为你的前端实际域名,禁止使用*通配符
    • 新增Access-Control-Allow-Credentials: true响应头
    • 配置Access-Control-Allow-Methods: GET, OPTIONS
    • Cloudfront缓存策略放行Cookie头,避免签名Cookie被缓存拦截
  4. 正确导入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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 03:49:12