11、前端UI框架:React/Vue集成、视频网格布局、画中画模式、响应式设计

做WebRTC多人视频会议,说白了,后端信令和媒体流搞定了,但用户看到的界面一塌糊涂,那这产品就废了。我见过太多项目,技术选型牛得不行,结果UI一打开,视频格子挤成一团,或者某个人的脸占了半个屏幕——用户直接关页面走人。

这一章,咱们就聊聊前端UI怎么落地。我会结合React和Vue两种主流框架,讲讲视频网格布局、画中画模式,还有响应式设计。嗯,这些都是我踩过坑之后总结出来的经验。

核心要点:UI框架的选择决定了开发效率,布局方案决定了用户体验,响应式设计决定了产品能不能在不同设备上活下来。

11.1 React/Vue集成:选谁?怎么搭?

我个人习惯用React做视频会议,因为它的状态管理更灵活,特别是处理多个视频流的动态增删时,React的Hooks用起来很顺手。但Vue也不差,它的模板语法对新手更友好。

先看React的集成方式。你想想看,每个视频流其实就是一个独立的组件,我们需要动态渲染它们。

// React 示例:动态渲染视频流
import React, { useRef, useEffect } from 'react';

const VideoTile = ({ stream, isLocal }) => {
  const videoRef = useRef(null);

  useEffect(() => {
    if (videoRef.current && stream) {
      videoRef.current.srcObject = stream;
    }
  }, [stream]);

  return (
    <div className={`video-tile ${isLocal ? 'local' : 'remote'}`}>
      <video ref={videoRef} autoPlay playsInline muted={isLocal} />
      <span className="label">{isLocal ? '我' : '参会者'}</span>
    </div>
  );
};

// 父组件管理多个视频流
const VideoGrid = ({ streams }) => {
  return (
    <div className="video-grid">
      {streams.map((stream, index) => (
        <VideoTile key={stream.id} stream={stream} isLocal={index === 0} />
      ))}
    </div>
  );
};

Vue这边呢?其实逻辑差不多,但写法更模板化。我在项目中遇到过一个问题:Vue的响应式系统在处理MediaStream对象时,有时候会触发不必要的重新渲染。解决办法是用shallowRef或者手动控制更新。

<!-- Vue 3 示例:视频瓦片组件 -->
<template>
  <div :class="['video-tile', { local: isLocal }]">
    <video ref="videoEl" autoplay playsinline :muted="isLocal" />
    <span class="label">{{ isLocal ? '我' : '参会者' }}</span>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue';

const props = defineProps({
  stream: Object,
  isLocal: Boolean
});

const videoEl = ref(null);

watch(() => props.stream, (newStream) => {
  if (videoEl.value && newStream) {
    videoEl.value.srcObject = newStream;
  }
}, { immediate: true });
</script>

小技巧:不管是React还是Vue,记得给每个视频元素设置playsinline属性。iOS上如果不加这个,视频会强制全屏播放,用户体验极差。我曾经因为这个被测试骂了一下午。

11.2 视频网格布局:让每个人都能被看见

多人视频会议最头疼的就是布局。2个人还好,4个人也凑合,但8个人、16个人呢?你想想看,如果每个人都是固定大小的小方块,那屏幕利用率就太低了。

我常用的方案是动态计算网格。说白了,就是根据参会人数,算出最优的行数和列数,然后让每个视频瓦片自适应填充。

// 动态网格计算函数
function calculateGrid(count, containerWidth, containerHeight) {
  // 先算理想的行列数
  let cols = Math.ceil(Math.sqrt(count));
  let rows = Math.ceil(count / cols);

  // 调整比例,避免太扁或太高
  while ((cols - 1) * rows >= count) {
    cols--;
  }

  // 计算每个瓦片的尺寸
  const tileWidth = containerWidth / cols;
  const tileHeight = containerHeight / rows;

  return { cols, rows, tileWidth, tileHeight };
}

// React中使用
const GridLayout = ({ streams, containerRef }) => {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const updateSize = () => {
      if (containerRef.current) {
        setDimensions({
          width: containerRef.current.offsetWidth,
          height: containerRef.current.offsetHeight
        });
      }
    };
    updateSize();
    window.addEventListener('resize', updateSize);
    return () => window.removeEventListener('resize', updateSize);
  }, []);

  const grid = calculateGrid(streams.length, dimensions.width, dimensions.height);

  return (
    <div ref={containerRef} className="grid-container">
      <div className="grid-inner" style={{
        gridTemplateColumns: `repeat(${grid.cols}, 1fr)`,
        gridTemplateRows: `repeat(${grid.rows}, 1fr)`
      }}>
        {streams.map(stream => (
          <VideoTile key={stream.id} stream={stream} />
        ))}
      </div>
    </div>
  );
};

注意:当参会人数超过9人时,网格布局会让每个人的画面变得很小。这时候我建议切换到「演讲者模式」——也就是只放大当前说话的人,其他人缩小到侧边栏。这个逻辑我在做企业会议系统时踩过坑,用户反馈说「根本看不清谁在说话」。

11.3 画中画模式:边开会边干别的

画中画(Picture-in-Picture,PiP)是视频会议里一个很实用的功能。用户可以在浏览器里开一个小窗口,把视频会议放在角落,然后继续看文档或者写代码。

WebRTC原生支持PiP API,但要注意兼容性。我建议用document.pictureInPictureEnabled先检测一下浏览器是否支持。

// 开启画中画
async function enablePiP(videoElement) {
  if (!document.pictureInPictureEnabled) {
    console.warn('浏览器不支持画中画');
    return;
  }

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    console.error('开启画中画失败:', error);
  }
}

// 关闭画中画
async function disablePiP() {
  if (document.pictureInPictureElement) {
    await document.exitPictureInPicture();
  }
}

// React组件中集成
const PiPButton = ({ videoRef }) => {
  const [isPiP, setIsPiP] = useState(false);

  const togglePiP = async () => {
    if (isPiP) {
      await disablePiP();
      setIsPiP(false);
    } else {
      await enablePiP(videoRef.current);
      setIsPiP(true);
    }
  };

  // 监听画中画退出事件(用户可能手动关闭)
  useEffect(() => {
    const handleLeavePiP = () => setIsPiP(false);
    videoRef.current?.addEventListener('leavepictureinpicture', handleLeavePiP);
    return () => videoRef.current?.removeEventListener('leavepictureinpicture', handleLeavePiP);
  }, []);

  return <button onClick={togglePiP}>{isPiP ? '退出画中画' : '画中画'}</button>;
};

避坑指南:我曾经遇到一个问题——画中画窗口里的视频卡住了,但主窗口是正常的。后来发现是因为PiP模式下,浏览器的渲染优先级会降低。解决办法是在开启PiP时,把视频流的帧率降低到15fps,这样能减少CPU压力。

11.4 响应式设计:适配手机、平板、电脑

做视频会议,你不能假设用户都在大屏显示器前。很多人用手机参会,甚至是在地铁上。响应式设计不是可选项,是必选项。

我的策略是:用CSS媒体查询 + JavaScript动态检测,做三档适配。

设备类型 屏幕宽度 布局策略 视频瓦片大小
手机 < 768px 单列滚动,最多显示2个视频 100%宽度,自动高度
平板 768px - 1024px 2列网格,最多显示4个视频 50%宽度,16:9比例
桌面 > 1024px 动态网格,最多显示9个视频 按网格计算
/* CSS 响应式设计 */
.video-grid {
  display: grid;
  gap: 8px;
  padding: 8px;
  width: 100%;
  height: 100%;
}

/* 手机:单列 */
@media (max-width: 767px) {
  .video-grid {
    grid-template-columns: 1fr;
    grid-template-rows: auto;
  }
  .video-tile {
    min-height: 200px;
  }
}

/* 平板:两列 */
@media (min-width: 768px) and (max-width: 1023px) {
  .video-grid {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: repeat(2, 1fr);
  }
}

/* 桌面:动态列数由JS控制 */
@media (min-width: 1024px) {
  .video-grid {
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  }
}

核心原则:响应式设计不是让所有设备显示同样的内容,而是让每个设备显示最适合它的内容。手机上就别想着同时看9个人了,专注当前说话的人才是正道。

知识体系总览

下面这张图是我自己整理的,把这一章的核心知识点串起来了。你看一眼就能明白各个模块之间的关系。

WebRTC多人视频会议 - UI框架知识体系 前端UI框架 React / Vue 集成 视频网格布局 画中画模式 响应式设计 动态渲染 状态管理 动态计算 自适应填充 PiP API 事件监听 媒体查询 三档适配 核心目标:让每个参会者在任何设备上都能获得最佳的视频体验

嗯,这一章的内容就这些。UI框架的选择、网格布局的计算、画中画的实现、响应式的适配——每一个点都是我在实际项目中反复打磨过的。你照着做,基本不会出大问题。

最后说一句:技术方案再好,最终还是要落到用户体验上。多站在用户的角度想想——他们开会时最需要什么?是清晰的画面、流畅的操作,还是能在手机上快速加入?想明白了,代码怎么写自然就清楚了。

公众号:蓝海资料掘金营,微信deep3321