第08章:视频全景与移动端交互
静止的全景图能展示某一个瞬间,但 360° 视频才能真正”让场景活起来”。从 VR 音乐会现场到房地产虚拟看房,从赛事直播回放到旅游宣传片——360° 视频正成为沉浸式内容的主力形态。与此同时,越来越多的用户通过手机陀螺仪”转动身体”来探索全景场景,而不是拖动鼠标。本章聚焦这两大主题:如何用 PSV 播放和控制 360° 视频,以及如何在移动端打造流畅、自然的全景交互体验。
8.1 视频全景概述
8.1.1 360° 视频的应用场景
360° 全景视频(又称球形视频、VR 视频)已经在多个领域得到了实际应用:
| 领域 | 典型场景 |
|---|---|
| VR 内容 | 全景音乐会、体育赛事直播回放、沉浸式纪录片 |
| 房地产 | VR 样板间视频导览,动态展示空间格局和光线变化 |
| 旅游 | 景区全景宣传片、酒店房间动态预览 |
| 教育 | 虚拟实地考察、历史事件全景重现 |
| 电商 | 产品 360° 动态展示带讲解 |
| 活动直播 | 婚礼、发布会、演出的 VR 直播回放 |
与静态全景图不同,360° 视频自带”时间维度”——用户在观看全景画面的同时,还有一个时间轴在推进。这意味着视频全景需要处理播放控制、音画同步、多分辨率适配等视频特有的问题。
8.1.2 视频格式要求
要在全景球面上正确显示视频,视频文件必须满足以下技术要求:
投影方式:等距柱状投影(Equirectangular Projection)。这是一种将球面展开为 2:1 矩形平面的映射方式,整段视频的每一帧都是 equirectangular 格式——上方对应天顶,下方对应天底,水平方向覆盖 360°,垂直方向覆盖 180°。如果用普通相机拍摄的视频直接贴到全景球面上,画面会被严重拉伸扭曲。
宽高比:严格 2:1(宽是高的两倍)。例如 3840×1920(4K)、1920×960(1080p)、1280×640(720p)。
编码格式:推荐 H.264(MP4 封装),兼容性最好。具体地:
| 格式 | 编码 | 封装 | 浏览器支持 |
|---|---|---|---|
| MP4 | H.264 (AVC) | .mp4 |
所有现代浏览器 |
| WebM | VP8 / VP9 | .webm |
Chrome, Firefox, Edge(Safari 不支持) |
| Ogg | Theora | .ogv |
Firefox, Chrome(不推荐,压缩率低) |
推荐做法:同时提供 MP4(H.264)和 WebM(VP9)两个版本,让浏览器自行选择支持的格式:
<video>
<source src="panorama.mp4" type="video/mp4">
<source src="panorama.webm" type="video/webm">
</video>
8.1.3 视频文件 vs 视频流
两者的选择取决于业务场景:
| 对比维度 | 视频文件 | 视频流 (HLS/DASH) |
|---|---|---|
| 加载方式 | 完整下载(或 HTTP Range 请求部分下载) | 分片加载,自适应码率 |
| 实现复杂度 | 低,<video> 标签直接支持 |
需要 HLS.js / dash.js 等第三方库 |
| 适用场景 | 短视频(< 3 分钟)、演示原型 | 长视频、直播、需要自适应码率的场景 |
| PSV 支持 | 原生支持,VideoPlugin 基于 <video> 元素 |
通过 <video> 元素的 srcObject 间接支持 |
对于大多数全景展示场景,直接使用 MP4/WebM 文件即可。如果确实需要 HLS 流播放,可以借助 hls.js 将流赋给 <video> 元素后再传入 PSV:
import Hls from 'hls.js';
const video = document.createElement('video');
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource('https://example.com/panorama.m3u8');
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生 HLS 支持
video.src = 'https://example.com/panorama.m3u8';
}
const viewer = new Viewer({
adapter: EquirectangularVideoAdapter,
panorama: { source: video },
plugins: [VideoPlugin],
});
注意:HLS 流需要 HTTPS 和正确的 CORS 配置。
8.2 Equirectangular Video Adapter
8.2.1 适配器的作用
在 PSV v5 中,全景图格式由适配器(Adapter)统一抽象。视频全景的适配器是 EquirectangularVideoAdapter,它与静态图片的 EquirectangularAdapter 工作方式类似,区别在于纹理的来源是一帧一帧的视频帧而非一次性加载的图片。适配器内部会从 <video> 元素中持续读取当前帧,并通过 Three.js 的 VideoTexture 将其映射到全景球面上。
8.2.2 安装与导入
npm install @photo-sphere-viewer/equirectangular-video-adapter
import { EquirectangularVideoAdapter } from '@photo-sphere-viewer/equirectangular-video-adapter';
与静态图适配器不同,视频适配器必须搭配 VideoPlugin 使用,因为它本身不提供播放控制——它只负责”把视频纹理贴到球上”,VideoPlugin 才是负责”播放/暂停/进度”的控制器。
8.2.3 两种传入方式
方式一:传入 <video> 元素(推荐,灵活性最高)
自己创建和管理 <video> 元素,可以精确控制视频的属性和行为:
import { Viewer } from '@photo-sphere-viewer/core';
import { EquirectangularVideoAdapter } from '@photo-sphere-viewer/equirectangular-video-adapter';
import { VideoPlugin } from '@photo-sphere-viewer/video-plugin';
// 手动创建 video 元素
const video = document.createElement('video');
video.src = 'https://example.com/panorama.mp4';
video.crossOrigin = 'anonymous'; // 关键:必须设置为 anonymous
video.playsInline = true; // iOS 内联播放(不强制全屏)
video.muted = true; // 大多数浏览器要求自动播放须静音
video.loop = true;
const viewer = new Viewer({
container: document.getElementById('viewer'),
adapter: EquirectangularVideoAdapter,
panorama: { source: video },
plugins: [
[VideoPlugin, { autoplay: true }],
],
});
方式二:直接传入 URL
PSV 内部会自动创建 <video> 元素。这种方式更简洁但不能自定义 video 元素的属性:
const viewer = new Viewer({
container: document.getElementById('viewer'),
adapter: EquirectangularVideoAdapter,
panorama: { source: 'https://example.com/panorama.mp4' },
plugins: [
[VideoPlugin, { autoplay: true, muted: true }],
],
});
8.2.4 crossOrigin 的重要性
全景视频文件往往托管在 CDN 或独立文件服务器上。如果视频 URL 的域名与网页不同,必须在 <video> 元素上设置 crossOrigin = 'anonymous',否则 Three.js 的 VideoTexture 无法从视频帧中读取像素数据,全景球面上只会显示一片黑色。
另外,视频服务器必须返回正确的 CORS 响应头:
Access-Control-Allow-Origin: *
或指定具体域名:
Access-Control-Allow-Origin: https://your-site.com
缺少任一端(客户端没设 crossOrigin 或服务端没返回 CORS 头),视频全景都将无法渲染。
8.3 VideoPlugin 详细配置
8.3.1 安装与导入
npm install @photo-sphere-viewer/video-plugin
import { VideoPlugin } from '@photo-sphere-viewer/video-plugin';
import '@photo-sphere-viewer/video-plugin/index.css';
务必导入 CSS 文件,它包含了播放器工具栏按钮(播放/暂停图标、进度条、音量滑块)的样式,不导入会导致工具栏上只有空白按钮。
8.3.2 完整配置选项
import { VideoPlugin } from '@photo-sphere-viewer/video-plugin';
import '@photo-sphere-viewer/video-plugin/index.css';
const viewer = new Viewer({
container: document.getElementById('viewer'),
adapter: EquirectangularVideoAdapter,
panorama: { source: video },
plugins: [
[VideoPlugin, {
// 自动播放
autoplay: true,
// 静音(大多数浏览器要求自动播放必须静音)
muted: true,
// 循环播放
loop: false,
// 初始音量 0-1
volume: 0.8,
// 本地化文本(覆盖播放器上的英文文字)
lang: {
play: '播放',
pause: '暂停',
volume: '音量',
autoPlay: '自动播放',
},
}],
],
navbar: [
'zoom',
'videoPlay', // 播放/暂停按钮
'videoTime', // 时间显示
'videoProgress',// 进度条
'videoVolume', // 音量滑块
'fullscreen',
],
});
各配置项的含义:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
autoplay |
boolean |
false |
初始化后自动播放 |
muted |
boolean |
false |
是否静音。设为 true 可绕过浏览器的自动播放限制 |
loop |
boolean |
false |
是否循环播放 |
volume |
number |
1.0 |
初始音量,范围 0-1 |
lang |
object |
英文 | 播放器 UI 文本的本地化 |
导航栏按钮——VideoPlugin 注册了四个导航栏按钮,按需添加到 navbar 数组:
| 按钮 ID | 显示内容 | 说明 |
|---|---|---|
videoPlay |
播放/暂停图标 | 点击切换播放状态 |
videoTime |
00:00 / 00:00 |
当前时间 / 总时长 |
videoProgress |
可拖拽进度条 | 点击或拖拽跳转到指定位置 |
videoVolume |
音量图标 + 滑块 | 点击静音切换,悬停显示音量滑块 |
8.3.3 VideoPlugin 提供的方法
VideoPlugin 实例提供了程序化控制视频播放的 API:
const videoPlugin = viewer.getPlugin(VideoPlugin);
// 播放控制
videoPlugin.play();
videoPlugin.pause();
videoPlugin.toggle(); // 播放/暂停切换
// 音量控制
videoPlugin.setVolume(0.5); // 设置音量 0-1
const vol = videoPlugin.getVolume();
// 进度控制
videoPlugin.setCurrentTime(30); // 跳转到第 30 秒
const current = videoPlugin.getCurrentTime(); // 当前播放时间(秒)
const duration = videoPlugin.getDuration(); // 视频总时长(秒)
这些方法让你可以通过自定义按钮或外部逻辑来控制全景视频的播放。例如,制作一个”快进 10 秒”按钮:
document.getElementById('skip-forward').addEventListener('click', () => {
const plugin = viewer.getPlugin(VideoPlugin);
const newTime = Math.min(plugin.getCurrentTime() + 10, plugin.getDuration());
plugin.setCurrentTime(newTime);
});
8.3.4 VideoPlugin 事件
VideoPlugin 在播放状态变化时触发自定义事件,可以通过 Viewer 的标准 addEventListener 监听:
// 播放/暂停
viewer.addEventListener('play', () => {
console.log('视频开始播放');
});
viewer.addEventListener('pause', () => {
console.log('视频已暂停');
});
// 音量变化
viewer.addEventListener('volume-change', (e) => {
console.log('当前音量:', e.detail.volume);
});
// 播放进度(频繁触发,每帧一次)
viewer.addEventListener('progress', (e) => {
const { currentTime, duration, progress } = e.detail;
// progress 是 0-1 的比例值
document.getElementById('custom-progress').style.width = `${progress * 100}%`;
});
// 视频播放结束
viewer.addEventListener('ended', () => {
console.log('视频播放完毕');
// 可以在这里做"自动切换到下一个全景场景"等逻辑
});
注意:
progress事件触发频率很高(随 requestAnimationFrame),不要在回调中执行 DOM 读/写之外的重计算操作,需要节流处理。
应用:自定义外部进度条:
const progressBar = document.getElementById('external-progress-bar');
const timeLabel = document.getElementById('external-time-label');
function formatTime(seconds) {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}
viewer.addEventListener('progress', (e) => {
const { currentTime, duration, progress } = e.detail;
progressBar.value = progress * 100;
timeLabel.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
});
progressBar.addEventListener('input', (e) => {
const plugin = viewer.getPlugin(VideoPlugin);
plugin.setCurrentTime((e.target.value / 100) * plugin.getDuration());
});
8.4 视频多分辨率
不同用户在带宽和屏幕分辨率上差异很大。4G 用户无法流畅播放 4K 视频,而 5G/WiFi 用户又不想看模糊的 720p。多分辨率(Multi-Resolution / Adaptive Bitrate)是解决这一问题的关键。
PSV 通过 VideoPlugin + ResolutionPlugin + SettingsPlugin 三者的组合来实现手动/自动切换视频分辨率。
8.4.1 安装
npm install @photo-sphere-viewer/resolution-plugin @photo-sphere-viewer/settings-plugin
8.4.2 配置多分辨率
import { Viewer } from '@photo-sphere-viewer/core';
import { EquirectangularVideoAdapter } from '@photo-sphere-viewer/equirectangular-video-adapter';
import { VideoPlugin } from '@photo-sphere-viewer/video-plugin';
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
const viewer = new Viewer({
container: document.getElementById('viewer'),
adapter: EquirectangularVideoAdapter,
panorama: { source: 'https://example.com/video-1080p.mp4' },
plugins: [
[VideoPlugin, {
autoplay: false,
muted: true,
loop: true,
}],
[ResolutionPlugin, {
resolutions: [
{
id: '4K',
label: '超清 4K',
panorama: { source: 'https://example.com/video-4k.mp4' },
},
{
id: 'HD',
label: '高清 1080p',
panorama: { source: 'https://example.com/video-1080p.mp4' },
},
{
id: 'SD',
label: '标清 720p',
panorama: { source: 'https://example.com/video-720p.mp4' },
},
],
}],
SettingsPlugin,
],
navbar: [
'zoom',
'videoPlay',
'videoTime',
'videoProgress',
'videoVolume',
'settings', // 设置面板按钮,用户在这里切换分辨率
'fullscreen',
],
});
关键注意事项:
-
切换分辨率时,视频的当前播放位置不会被保留。如果用户看了 30 秒后切换画质,视频会从头开始。要解决这个问题,需要自行在切换前记录
getCurrentTime(),切换后调用setCurrentTime()恢复。 -
每个分辨率的
panorama.source可以传入 URL 字符串,也可以传入<video>元素。如果需要精确控制每个视频的属性(如不同的预加载策略),建议为每个分辨率创建独立的<video>元素:
const videos = {
'4K': createVideoElement('https://example.com/video-4k.mp4'),
'HD': createVideoElement('https://example.com/video-1080p.mp4'),
'SD': createVideoElement('https://example.com/video-720p.mp4'),
};
function createVideoElement(src) {
const video = document.createElement('video');
video.src = src;
video.crossOrigin = 'anonymous';
video.playsInline = true;
video.preload = 'metadata'; // 只预加载元数据,不预加载整个视频
return video;
}
// 在 ResolutionPlugin 的配置中使用这些元素
resolutions: [
{ id: '4K', label: '超清 4K', panorama: { source: videos['4K'] } },
{ id: 'HD', label: '高清 1080p', panorama: { source: videos['HD'] } },
{ id: 'SD', label: '标清 720p', panorama: { source: videos['SD'] } },
]
- 也可以利用 ResolutionPlugin 的事件在切换时执行自定义逻辑:
viewer.addEventListener('resolution-change', (e) => {
const { resolution } = e.detail;
console.log(`已切换到: ${resolution}`);
// 可以在这里上报数据,用于分析用户偏好
});
8.5 GyroscopePlugin 陀螺仪详解
8.5.1 陀螺仪交互的基本原理
现代智能手机内置了陀螺仪(Gyroscope)和加速度计(Accelerometer),可以实时测量设备在三个轴上的旋转角度。PSV 的 GyroscopePlugin 通过监听浏览器的 DeviceOrientationEvent,将设备的物理旋转映射为全景视角的旋转——你拿着手机转一圈,画面就跟着转一圈,实现了”把手机当作一扇窗户看世界”的自然交互。
8.5.2 安装与导入
npm install @photo-sphere-viewer/gyroscope-plugin
import { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin';
8.5.3 重要限制
HTTPS 必须:DeviceOrientationEvent 在 HTTP 页面上被 Chrome 和 Firefox 阻止。你的网站必须部署在 HTTPS 下,陀螺仪功能才能正常工作。本地开发时,localhost 通常被浏览器视为安全上下文,可以不受此限制。
iOS 13+ 需要用户手势:从 iOS 13 开始,苹果要求 DeviceOrientationEvent 必须在用户手势(如点击按钮)之后才能触发。这意味着你不能在页面加载时自动启动陀螺仪——必须提供一个按钮让用户主动激活:
// iOS 兼容:在用户点击按钮后才请求权限
document.getElementById('enable-gyro').addEventListener('click', async () => {
if (typeof DeviceOrientationEvent !== 'undefined'
&& typeof DeviceOrientationEvent.requestPermission === 'function') {
// iOS 13+ 设备
const permission = await DeviceOrientationEvent.requestPermission();
if (permission === 'granted') {
viewer.getPlugin(GyroscopePlugin).start();
}
} else {
// Android 或旧版 iOS
viewer.getPlugin(GyroscopePlugin).start();
}
});
8.5.4 配置选项
import { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin';
const viewer = new Viewer({
container: document.getElementById('viewer'),
panorama: 'panorama.jpg',
plugins: [
[GyroscopePlugin, {
// 触摸模式:true = 陀螺仪激活时仍允许手指拖动
touchmove: true,
// 绝对定位模式:设备的绝对方向(相对地理北极)
absolutePosition: false,
// 移动模式:'smooth'(平滑过渡)或 'fast'(即时响应)
moveMode: 'smooth',
}],
],
navbar: [
'zoom',
'gyroscope', // 陀螺仪开/关按钮
'fullscreen',
],
});
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
touchmove |
boolean |
true |
陀螺仪激活时是否允许手指同时拖动画面。设为 false 则纯靠身体转动 |
absolutePosition |
boolean |
false |
是否使用设备的绝对方向。启用后,手机指向北方时画面也显示北方 |
moveMode |
string |
'smooth' |
移动模式。'smooth' 是做插值平滑,'fast' 是直接响应原始陀螺仪数据 |
8.5.5 GyroscopePlugin 方法
const gyroPlugin = viewer.getPlugin(GyroscopePlugin);
gyroPlugin.start(); // 启动陀螺仪
gyroPlugin.stop(); // 停止陀螺仪
gyroPlugin.toggle(); // 切换
const isActive = gyroPlugin.isEnabled();
start() 和 stop() 对应于导航栏上的陀螺仪按钮状态,也可以通过代码控制。
8.5.6 事件
viewer.addEventListener('gyroscope-updated', (e) => {
// e.detail 包含设备的原始姿态信息
console.log(e.detail);
});
8.6 StereoPlugin VR 立体视图详解
8.6.1 VR 立体视图的原理
StereoPlugin 将全景画面左右分屏显示,模拟人眼的视差效果。把手机放入 Google Cardboard 等廉价 VR 眼镜盒中,左右眼分别看到略微偏移的画面,大脑就会合成出立体感。配合 GyroscopePlugin,用户转动头部时视角同步变化,达到基本的 VR 沉浸体验。
8.6.2 安装与导入
npm install @photo-sphere-viewer/stereo-plugin
import { StereoPlugin } from '@photo-sphere-viewer/stereo-plugin';
8.6.3 配置与使用
import { Viewer } from '@photo-sphere-viewer/core';
import { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin';
import { StereoPlugin } from '@photo-sphere-viewer/stereo-plugin';
const viewer = new Viewer({
container: document.getElementById('viewer'),
panorama: 'panorama.jpg',
plugins: [
[GyroscopePlugin, {
// 立体模式下通常禁止触屏拖动,纯靠头部转动
touchmove: false,
moveMode: 'fast',
}],
[StereoPlugin, {
// 覆盖层提示文字
overlay: {
title: 'VR 立体模式',
message: '请将手机放入 VR 眼镜盒中',
},
}],
],
navbar: [
'zoom',
'gyroscope',
'stereo', // 进入/退出立体模式按钮
'fullscreen',
],
});
8.6.4 WakeLock 防止屏幕休眠
立体模式下用户长时间不触摸屏幕,设备可能自动熄屏。使用浏览器的 WakeLock API 可以保持屏幕常亮:
let wakeLock = null;
viewer.addEventListener('stereo-enter', async () => {
// 进入 VR 模式时请求屏幕常亮
try {
wakeLock = await navigator.wakeLock.request('screen');
console.log('屏幕常亮已激活');
wakeLock.addEventListener('release', () => {
console.log('屏幕常亮已释放');
});
} catch (err) {
console.warn('WakeLock 不可用:', err.message);
}
});
viewer.addEventListener('stereo-leave', async () => {
// 退出 VR 模式时释放
if (wakeLock) {
await wakeLock.release();
wakeLock = null;
}
});
WakeLock API 同样只在 HTTPS 下可用,且部分安卓浏览器可能不支持。
8.6.5 退出立体模式
进入立体模式后,点击全景画面任意位置即可退出。这是 StereoPlugin 的默认行为,无需额外配置。
8.7 移动端最佳实践
移动端全景体验不仅仅是”让页面在手机上能打开”。屏幕尺寸、触摸交互、网络条件、电池续航——这些因素共同决定了移动端的实际体验质量。以下是经过验证的实践建议。
8.7.1 响应式容器
全景容器应该根据设备屏幕动态适配,而不是写死尺寸:
#viewer {
width: 100%;
height: 100vh;
/* 在手机上使用 100vh 有时会因为浏览器地址栏的显示/隐藏导致抖动 */
/* 可以用 dvh(动态视口高度)作为更现代的替代 */
height: 100dvh;
}
/* 在横屏手机上减小容器高度,留出操控空间 */
@media (orientation: landscape) and (max-height: 500px) {
#viewer {
height: 85vh;
}
}
8.7.2 触摸交互优化
PSV 提供了 touchmoveTwoFingers 选项,可以根据业务需求调整移动端的手势行为:
const viewer = new Viewer({
container: document.getElementById('viewer'),
panorama: 'panorama.jpg',
// 仅允许双指拖动旋转画面(单指用于其他操作,如图层交互、滚动页面等)
touchmoveTwoFingers: true,
// 手指移动速度的缩放系数
mouseSpeed: 1.0,
// 触摸设备上手指移动的灵敏度
touchSpeed: 1.0,
// 捏合缩放的速度
zoomSpeed: 1.0,
});
如果你在一个可滚动的页面中嵌入了全景查看器,建议启用 touchmoveTwoFingers: true,这样单指滑动由页面滚动消费,只有双指操作才触发全景旋转,避免”想滚动页面却旋转了全景”的问题。
8.7.3 性能优化
移动端 GPU 性能远弱于桌面端。以下措施可以显著改善流畅度:
1. 降低全景图分辨率
移动端屏幕物理像素虽然高(2x/3x DPR),但全景纹理的分辨率不需要 1:1 映射。一般来说:
| 图片分辨率 | 适用场景 |
|---|---|
| 4096×2048 | 仅限桌面端高配机型,不建议移动端使用 |
| 2048×1024 | 移动端的理想上限 |
| 1024×512 | 低端安卓设备,快速加载优先 |
对于视频全景,同样的原则适用:4K (3840×1920) 视频在移动端意义不大,1080p 已经足够。
2. 控制渲染像素比
const viewer = new Viewer({
renderer: {
// 限制渲染器像素比,避免移动端 GPU 超负荷
maxPixelRatio: 2,
},
// 分辨率插件中提供低分辨率选项
});
3. 视频预加载策略
在移动网络下,预加载整段视频可能浪费流量。如果不需要自动播放,设置 preload: 'metadata' 可以只下载视频头信息(时长、分辨率等),等用户点击播放后再下载画面数据。如果是手动创建的 <video> 元素:
video.preload = 'none'; // 或 'metadata'
8.7.4 PWA 支持
如果你希望全景应用在移动端有”类原生 App”的体验,可以考虑将其打包为 PWA(Progressive Web App)。这不是 PSV 特有的功能,而是通用 Web 技术:
- 添加 manifest.json:定义应用名称、图标、全屏模式等
- 注册 Service Worker:实现离线缓存,让全景图/视频在第二次访问时秒开
- 全屏模式:通过
fullscreen插件或 Fullscreen API 隐藏浏览器 UI
PWA 的完整配置超出了本章范围,这里只给出最小必要配置:
// manifest.json
{
"name": "360° 全景导览",
"short_name": "全景导览",
"start_url": "/",
"display": "fullscreen",
"orientation": "any",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
<link rel="manifest" href="/manifest.json">
8.7.5 移动端完整配置示例
综合以上所有要点,这里给出一个面向移动端优化过的完整配置:
import { Viewer } from '@photo-sphere-viewer/core';
import { EquirectangularVideoAdapter } from '@photo-sphere-viewer/equirectangular-video-adapter';
import { VideoPlugin } from '@photo-sphere-viewer/video-plugin';
import { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin';
import { StereoPlugin } from '@photo-sphere-viewer/stereo-plugin';
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin';
// 根据屏幕宽度自动选择初始分辨率
const isSmallScreen = window.innerWidth < 768;
const defaultSource = isSmallScreen
? 'https://example.com/video-720p.mp4'
: 'https://example.com/video-1080p.mp4';
const viewer = new Viewer({
container: document.getElementById('viewer'),
adapter: EquirectangularVideoAdapter,
panorama: { source: defaultSource },
touchmoveTwoFingers: true,
mouseSpeed: 1.0,
touchSpeed: 1.2,
renderer: {
maxPixelRatio: 2,
},
plugins: [
[VideoPlugin, {
autoplay: false,
muted: true,
loop: true,
lang: {
play: '播放',
pause: '暂停',
volume: '音量',
},
}],
[ResolutionPlugin, {
resolutions: [
{ id: 'HD', label: '高清', panorama: { source: 'https://example.com/video-1080p.mp4' } },
{ id: 'SD', label: '流畅', panorama: { source: 'https://example.com/video-720p.mp4' } },
],
}],
[GyroscopePlugin, {
touchmove: false,
moveMode: 'fast',
}],
[StereoPlugin],
SettingsPlugin,
],
navbar: [
'zoom',
'videoPlay',
'videoTime',
'videoProgress',
'videoVolume',
'gyroscope',
'stereo',
'settings',
'fullscreen',
],
});
// iOS 陀螺仪权限请求
const gyroBtn = document.querySelector('[data-psv-button="gyroscope"]');
if (gyroBtn) {
gyroBtn.addEventListener('click', async () => {
if (typeof DeviceOrientationEvent !== 'undefined'
&& typeof DeviceOrientationEvent.requestPermission === 'function') {
const permission = await DeviceOrientationEvent.requestPermission();
if (permission === 'granted') {
viewer.getPlugin(GyroscopePlugin).toggle();
}
}
});
}
8.8 视频关键帧
8.8.1 概念
视频关键帧(Video Keypoints)允许你在视频的特定时间点上触发全景视角的自动旋转,实现”时间 + 空间”的双维导览。例如:在房地产全景视频中,当画面推进到厨房区域时,视角自动转向橱柜方向;当推到阳台时,视角自动转向窗外景色。
8.8.2 配置方式
VideoPlugin 没有内置的关键帧功能,但可以通过 VideoPlugin 的 progress 事件 + Viewer 的 rotate() 方法 来实现:
// 定义关键帧:在视频第 N 秒时将视角转到指定位置
const keyframes = [
{ seconds: 5, yaw: -0.5, pitch: 0.1, duration: 1500 }, // 第5秒,转向客厅
{ seconds: 15, yaw: 1.2, pitch: -0.2, duration: 2000 }, // 第15秒,转向厨房
{ seconds: 30, yaw: 2.8, pitch: 0.05, duration: 1800 }, // 第30秒,转向阳台
];
let lastTriggeredIndex = -1;
viewer.addEventListener('progress', (e) => {
const { currentTime } = e.detail;
// 找到当前时间应该触发但尚未触发的关键帧
for (let i = lastTriggeredIndex + 1; i < keyframes.length; i++) {
if (currentTime >= keyframes[i].seconds) {
const kf = keyframes[i];
viewer.rotate({
yaw: kf.yaw,
pitch: kf.pitch,
}, {
speed: `${kf.duration}ms`,
});
lastTriggeredIndex = i;
} else {
break;
}
}
});
// 视频结束后重置,以便循环播放时重新触发
viewer.addEventListener('ended', () => {
lastTriggeredIndex = -1;
});
8.8.3 配合 AutorotatePlugin
如果要在关键帧之间让自动旋转持续运行(给用户一种”电影运镜”的体验),可以结合 AutorotatePlugin:
import { AutorotatePlugin } from '@photo-sphere-viewer/autorotate-plugin';
const viewer = new Viewer({
// ...
plugins: [
[VideoPlugin, { autoplay: true, muted: true }],
[AutorotatePlugin, {
autostart: true,
speed: '1rpm',
}],
],
});
// 在关键帧时间点暂停自动旋转,执行定向旋转,再恢复
注意:复杂的关键帧逻辑和自动旋转之间的协调需要自己处理,PSV 没有提供”视频时间轴 + 视角动画时间线”的内置编排器。如果需要精细的运镜控制,建议在
progress事件中手动管理状态机。
8.9 Cubemap Video 适配器
8.9.1 立方体贴图视频
除了等距柱状投影的视频,PSV 还支持立方体贴图视频格式。这种格式将全景画面渲染到立方体的六个面上(前、后、左、右、上、下),通过 CubemapVideoAdapter 加载。
npm install @photo-sphere-viewer/cubemap-video-adapter
import { CubemapVideoAdapter } from '@photo-sphere-viewer/cubemap-video-adapter';
8.9.2 与 Equirectangular Video 的区别
| 对比维度 | Equirectangular Video | Cubemap Video |
|---|---|---|
| 视频文件数量 | 1 个 | 1 个(复合布局)或 6 个(每面一个) |
| 存储效率 | 高(单文件) | 中等 |
| GPU 运算 | 需要在球面上做纹理映射,两极区域像素被拉伸 | 六个平面纹理,像素分布更均匀 |
| 使用场景 | 通用全景视频,制作和分发方便 | 游戏引擎输出、对画质均匀性有要求的场景 |
| 浏览器兼容性 | <video> 元素天然支持 |
需要视频纹理 + Three.js CubeRenderTarget |
对于绝大多数开发者而言,Equirectangular Video 是默认选择——它的制作工具链更成熟(所有 360° 相机都输出 equirectangular 格式),分发也简单。Cubemap Video 主要用于专业制作管线,如从 Unity/Unreal 引擎导出的全景序列。
8.10 本章小结
本章从视频全景和移动端交互两个维度,完整覆盖了 PSV 在动态全景领域的核心能力:
-
视频全景基础:360° 视频采用 equirectangular 投影、2:1 宽高比,推荐 H.264 MP4 格式以获得最佳浏览器兼容性。跨域部署时必须设置
crossOrigin和 CORS 响应头。 -
视频适配器与插件配合:
EquirectangularVideoAdapter负责将视频纹理映射到全景球面,VideoPlugin负责播放控制(播放/暂停、进度、音量)。两者必须同时使用。 -
播放控制:VideoPlugin 提供了完整的 API(
play()/pause()/setVolume()/setCurrentTime())和事件系统(play/pause/progress/ended),可以在此基础上构建自定义的播放器界面。 -
多分辨率:通过 VideoPlugin + ResolutionPlugin + SettingsPlugin 组合,可以为不同网络环境的用户提供不同码率的视频,按钮切换清晰直观。
-
陀螺仪:
GyroscopePlugin利用设备传感器实现”转动手机 = 转动画面”,是移动端全景体验的核心。注意 HTTPS 限制和 iOS 13+ 的权限请求。 -
VR 立体视图:
StereoPlugin配合陀螺仪和 VR 眼镜盒可以快速搭建简易 VR 体验,搭配 WakeLock API 防止屏幕休眠。 -
移动端最佳实践:响应式容器、触摸手势优化、降低渲染分辨率、PWA 支持——这些工程层面的细节决定了移动端的实际体验质量。
掌握了视频全景和移动端交互之后,你已经可以制作市面上大部分常见类型的全景应用——静态全景展示、动态视频播放、移动端 VR 体验。在下一章中,我们将把全景与地理信息结合起来,探索如何通过地图投影、指南针和 GPS 定位,让全景场景”落到地图上”。