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

ASP.NET MVC中Agora悬浮视频窗页面切换后无画面问题求助

解决方案:ASP.NET MVC中跨页面保持Agora视频通话悬浮窗播放

问题根源

ASP.NET MVC默认是整页刷新模式,切换页面时会重置整个JS上下文和DOM环境:

  • 原Agora客户端实例、媒体轨道会被销毁
  • 悬浮窗DOM虽因布局保留,但无有效视频轨道绑定,导致无画面
  • sessionStorage仅保存"已加入"状态,无法恢复已销毁的媒体资源

核心修复方案

1. 全局挂载悬浮窗与Agora核心逻辑

将悬浮视频窗局部视图放到_Layout.cshtml的body末尾,确保所有页面都包含该DOM结构;同时将Agora的JS代码抽离为全局脚本,避免页面切换后重复初始化。

修改_Layout.cshtml:

<body>
    <!-- 原有布局内容 -->
    <div id="main-content">@RenderBody()</div>
    
    <!-- 全局悬浮视频窗 -->
    <partial name="_FloatingVideoView" />
    
    <!-- 引入Agora SDK和全局视频通话脚本 -->
    <script src="https://cdn.agora.io/sdk/release/AgoraRTC_N-4.17.0.js"></script>
    <script src="~/js/agora-global-call.js"></script>
</body>

2. 重构Agora代码为全局状态管理

使用全局对象存储Agora客户端、媒体轨道和通话状态,确保页面切换(无刷新)时状态不丢失:

~/js/agora-global-call.js代码:

// 全局通话状态,跨页面保持
window.videoCallState = {
    client: null,
    localTracks: { videoTrack: null, audioTrack: null },
    remoteUsers: {},
    isJoined: false,
    options: {
        appid: "your-app-id",
        channel: "your-channel-name",
        token: "your-token",
        uid: null
    }
};

// 初始化Agora客户端(仅执行一次)
function initAgoraClient() {
    if (!window.videoCallState.client) {
        window.videoCallState.client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
        
        // 绑定全局事件监听
        window.videoCallState.client.on("user-published", handleUserPublished);
        window.videoCallState.client.on("user-unpublished", handleUserUnpublished);
    }
}

// 加入或恢复通话
async function joinOrResumeCall() {
    const state = window.videoCallState;
    
    if (!state.client) initAgoraClient();
    
    // 已在通话中,仅重新绑定视频轨道到当前DOM
    if (state.isJoined) {
        if (state.localTracks.videoTrack) {
            state.localTracks.videoTrack.play("local-player");
        }
        // 恢复远端用户视频
        Object.values(state.remoteUsers).forEach(user => {
            if (user.videoTrack && !$(player-${user.uid}).length) {
                const player = $(`
                    <div id="player-wrapper-${user.uid}">
                        <p class="player-name">remoteUser(${user.uid})</p>
                        <div id="player-${user.uid}" class="player"></div>
                    </div>
                `);
                $("#remote-player").append(player);
                user.videoTrack.play(`player-${user.uid}`);
            }
        });
        $("#floating-video-window").show();
        return;
    }

    // 首次加入通话
    [state.options.uid, state.localTracks.audioTrack, state.localTracks.videoTrack] = await Promise.all([
        state.client.join(state.options.appid, state.options.channel, state.options.token || null),
        AgoraRTC.createMicrophoneAudioTrack(),
        AgoraRTC.createCameraVideoTrack()
    ]);

    state.localTracks.videoTrack.play("local-player");
    $("#local-player-name").text(`localVideo(${state.options.uid})`);
    
    await state.client.publish(Object.values(state.localTracks));
    state.isJoined = true;
    sessionStorage.setItem('videoCallJoined', 'true');
    $("#floating-video-window").show();
}

// 离开通话
async function leaveCall() {
    const state = window.videoCallState;
    
    for (let trackName in state.localTracks) {
        const track = state.localTracks[trackName];
        if (track) {
            track.stop();
            track.close();
            state.localTracks[trackName] = null;
        }
    }

    state.remoteUsers = {};
    $("#remote-player").html("");
    await state.client.leave();
    
    $("#local-player-name").text("");
    $("#floating-video-window").hide();
    
    state.isJoined = false;
    sessionStorage.setItem('videoCallJoined', 'false');
}

// 订阅远端用户
async function subscribe(user, mediaType) {
    const state = window.videoCallState;
    const uid = user.uid;
    await state.client.subscribe(user, mediaType);
    
    if (mediaType === 'video') {
        const player = $(`
            <div id="player-wrapper-${uid}">
                <p class="player-name">remoteUser(${uid})</p>
                <div id="player-${uid}" class="player"></div>
            </div>
        `);
        $("#remote-player").append(player);
        user.videoTrack.play(`player-${uid}`);
        state.remoteUsers[uid] = user;
    }
    if (mediaType === 'audio') {
        user.audioTrack.play();
    }
}

// 远端用户发布处理
function handleUserPublished(user, mediaType) {
    subscribe(user, mediaType);
}

// 远端用户取消发布处理
function handleUserUnpublished(user) {
    const uid = user.uid;
    delete window.videoCallState.remoteUsers[uid];
    $(`#player-wrapper-${uid}`).remove();
}

// 页面初始化事件
$(document).ready(function() {
    // 从sessionStorage恢复状态
    window.videoCallState.isJoined = sessionStorage.getItem('videoCallJoined') === 'true';
    
    // 绑定VideoCall视图的Join/Leave按钮事件(仅当按钮存在时)
    if ($("#join").length) {
        $("#join").click(async function() {
            $(this).attr("disabled", true);
            try {
                await joinOrResumeCall();
                // 保留原有成功提示逻辑
            } catch (err) {
                console.error(err);
                $(this).attr("disabled", false);
            } finally {
                $("#leave").attr("disabled", false);
            }
        });
    }
    
    if ($("#leave").length) {
        $("#leave").click(async function() {
            await leaveCall();
            $(this).attr("disabled", true);
            $("#join").attr("disabled", false);
        });
    }
    
    // 悬浮窗关闭按钮事件
    $("#close-floating-window").click(async function() {
        await leaveCall();
    });
    
    // 页面加载时恢复通话
    if (window.videoCallState.isJoined) {
        joinOrResumeCall();
    }
});

3. 实现无刷新页面切换(关键)

整页刷新会销毁全局JS状态,因此需要拦截导航链接,用AJAX加载页面内容:

在全局JS中添加以下代码:

// 拦截站内导航链接,实现无刷新跳转
$(document).on('click', 'a[href]:not([href^="http"]):not([href^="#"]):not([target="_blank"])', function(e) {
    e.preventDefault();
    const targetUrl = $(this).attr('href');
    
    // 加载目标视图内容到主容器
    $('#main-content').load(targetUrl, function() {
        // 内容加载完成后触发自定义事件,支持后续初始化
        $(document).trigger('pageContentLoaded');
    });
    
    // 更新浏览器地址栏
    history.pushState(null, document.title, targetUrl);
});

// 监听浏览器前进后退事件
window.addEventListener('popstate', function() {
    $('#main-content').load(location.pathname);
});

验证要点

  1. 点击VideoCall视图的Join按钮,确认悬浮窗正常播放视频
  2. 切换到Index、Privacy等其他视图,悬浮窗应持续显示视频画面
  3. 点击悬浮窗关闭按钮,确认通话正常断开,悬浮窗隐藏

内容的提问来源于stack exchange,提问作者Sarmad Ali

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 08:34:55