当前位置: 首页 > news >正文

Web 点播播放器如何加载缩略图?

在前面的文章说,说明了服务端如何生成雪碧图/WebVTT,那如何在 Web 点播播放器上加载缩略图呢?

无论使用西瓜播放器,Video.js,Shaka Player,Plyr,DPlayer 等,以下方法都可以参考,有些播放器内置了支持雪碧图,有些播放器通过插件支持 WebVTT,但都无法达到我们想要的效果,那就…自己实现吧!

以下方案以 DPlayer 为例。

在视频元数据加载完成时,加载缩略图

initializeThumbnailPreview 用于初始化缩略图,有 4 个参数,分别是

  • webvtt 文件路径
  • 播放器的 video 标签
  • 父级容器( 可以对 div 设置 ref,通过 ref 获取 )
  • 错误回调
    // 播放器基础事件player.on('loadedmetadata', async () => {logger.info('🍒 视频元数据加载完成', {duration: player.video.duration.toFixed(2),videoWidth: player.video.videoWidth,videoHeight: player.video.videoHeight});// 初始化缩略图预览功能if (vttPath && player.video && container) {try {const thumbnailState = await initializeThumbnailPreview({vttPath,videoElement: player.video,videoContainer: container,onError});resolve(thumbnailState);} catch (error) {logger.error('🍒 缩略图预览初始化失败', error);resolve(null);}} else {resolve(null);}});

重点要关注的是 setupThumbnailPreviewupdateThumbnailContent 函数,简单来说做了以下几个步骤

  1. 计算鼠标在进度条上的位置
  2. 在该位置上方显示缩略图
  3. 在缩略图里面显示进度

那么如何确定显示雪碧图中的哪个 tile 呢?

遍历 webvtt 文件,找到对应播放时间的缩略图,缩略图的后缀有 #xywh,之前的文章中有提到这个用于定位。图片元素显示是固定的 160*90,通过设置像素的位置对小图定位显示。

/*** 初始化缩略图预览功能* 这是主要的导出函数,供外部组件调用*/
export const initializeThumbnailPreview = async (config: ThumbnailPreviewConfig): Promise<ThumbnailPreviewState> => {const { vttPath, videoElement, videoContainer, onError } = config;try {logger.info('🍒 开始初始化缩略图预览功能', {vttPath,videoElementReady: !!videoElement,containerReady: !!videoContainer});// 设置容器为相对定位videoContainer.style.position = 'relative';// 创建缩略图预览元素const { thumbnailElement, timeElement } = createThumbnailElements(videoContainer);// 加载 WebVTT 轨道await loadWebVTTTrack(vttPath, videoElement);// 设置缩略图预览功能await setupThumbnailPreview(videoElement, thumbnailElement, timeElement, videoContainer);// 创建清理函数const cleanup = () => {if (thumbnailElement && thumbnailElement.parentNode) {thumbnailElement.parentNode.removeChild(thumbnailElement);logger.info('🍒 缩略图预览元素已清理');}};logger.info('🍒 缩略图预览功能初始化完成');return {thumbnailElement,timeElement,cleanup};} catch (error) {const errorMessage = `缩略图预览功能初始化失败: ${error}`;logger.error('🍒 缩略图预览功能初始化失败', { error, vttPath });onError?.(errorMessage);throw error;}
};

loadWebVTTTrack 在 video 标签中添加 dom 元素,用于存储 webvtt 信息,方便后续读取与显示。

/*** 加载 WebVTT 缩略图轨道*/
const loadWebVTTTrack = async (vttUrl: string, videoElement: HTMLVideoElement): Promise<void> => {try {logger.info('🍒 开始加载 WebVTT 缩略图', { vttUrl });// 使用 fetch 加载 WebVTT 内容const response = await fetch(vttUrl, {mode: 'cors',credentials: 'omit'});if (!response.ok) {throw new Error(`HTTP ${response.status}: ${response.statusText}`);}const vttContent = await response.text();logger.info('🍒 WebVTT 内容加载成功', {size: vttContent.length,firstLine: vttContent.split('\n')[0]});// 创建 Blob URLconst blob = new Blob([vttContent], { type: 'text/vtt' });const blobUrl = URL.createObjectURL(blob);// 创建轨道元素const track = document.createElement('track');track.kind = 'metadata';track.label = 'thumbnails';track.default = true;track.src = blobUrl;// 添加轨道到视频元素videoElement.appendChild(track);// 监听轨道加载事件return new Promise((resolve, reject) => {track.addEventListener('load', () => {logger.info('🍒 WebVTT 轨道加载完成', { vttUrl });URL.revokeObjectURL(blobUrl);resolve();});track.addEventListener('error', (e) => {logger.error('🍒 WebVTT 轨道加载失败', { vttUrl, error: e });URL.revokeObjectURL(blobUrl);reject(new Error('WebVTT 轨道加载失败'));});});} catch (error) {logger.error('🍒 加载 WebVTT 失败', { vttUrl, error });throw error;}
};

setupThumbnailPreview 用于设置缩略图预览功能,主要是操作 dom 元素,修改 css 等。

为保证代码的完整性,我将在此处贴出完整函数,并通过注释的方式讲解内部设计。

/*** 设置缩略图预览功能(带重试机制)*/
const setupThumbnailPreview = (videoElement: HTMLVideoElement,thumbnailElement: HTMLDivElement,timeElement: HTMLDivElement,videoContainer: HTMLElement
): Promise<void> => {return new Promise((resolve, reject) => {const attemptSetup = (attempt: number = 1): void => {const progressBar = findProgressBar();if (progressBar) {setupProgressBarEvents(progressBar, videoElement, thumbnailElement, timeElement, videoContainer);logger.info('🍒 缩略图预览功能设置成功', { attempt });resolve();return;}if (attempt <= RETRY_CONFIG.maxAttempts) {const delay = attempt * RETRY_CONFIG.baseDelay;logger.info('🍒 进度条元素未找到,延迟重试', {attempt,delay,maxAttempts: RETRY_CONFIG.maxAttempts});setTimeout(() => {// 调试信息:打印当前 DOM 中的相关元素const debugInfo = {dplayerElements: Array.from(document.querySelectorAll('[class*="dplayer"]')).map(el => ({tagName: el.tagName,className: el.className,visible: (el as HTMLElement).offsetParent !== null})),barElements: Array.from(document.querySelectorAll('[class*="bar"]')).map(el => ({tagName: el.tagName,className: el.className,visible: (el as HTMLElement).offsetParent !== null}))};logger.info('🍒 DOM 调试信息', { attempt, ...debugInfo });attemptSetup(attempt + 1);}, delay);} else {const error = new Error(`已达到最大重试次数 ${RETRY_CONFIG.maxAttempts},缩略图功能无法正常工作`);logger.warn('🍒 缩略图功能设置失败', { maxAttempts: RETRY_CONFIG.maxAttempts });reject(error);}};attemptSetup();});
};

更新缩略图的内容

/*** 更新缩略图内容和位置*/
const updateThumbnailContent = (videoElement: HTMLVideoElement,thumbnailElement: HTMLDivElement,timeElement: HTMLDivElement,seekTime: number,thumbnailX: number
): void => {// 获取视频的文本轨道const tracks = videoElement.textTracks;if (!tracks || tracks.length === 0) {logger.warn('🍒 没有找到文本轨道', { tracksLength: tracks?.length });return;}const track = tracks[0];if (track.mode !== 'showing') {track.mode = 'showing';}if (!track.cues) {logger.warn('🍒 文本轨道没有 cues', {trackMode: track.mode,trackKind: track.kind});return;}// 查找当前时间对应的缩略图信息let activeCue = null;for (let i = 0; i < track.cues.length; i++) {const cue = track.cues[i];if (seekTime >= cue.startTime && seekTime <= cue.endTime) {activeCue = cue;break;}}if (!activeCue) {logger.warn('🍒 未找到对应时间的缩略图', { seekTime: seekTime.toFixed(2) });return;}// @ts-expect-error - VTTCue.text 包含缩略图信息const cueText = activeCue.text;// 解析 WebVTT 缩略图信息// 格式:image.jpg#xywh=160,90,160,90 或 url(image.jpg)let imageUrl = '';let xywh = '';if (cueText.includes('#xywh=')) {const parts = cueText.split('#xywh=');imageUrl = parts[0];xywh = parts[1];} else {const match = cueText.match(/url\(([^)]+)\)/);if (match) {imageUrl = match[1];} else {imageUrl = cueText.trim();}}if (!imageUrl) {logger.warn('🍒 无法解析缩略图URL', { cueText });return;}// 设置缩略图样式和位置if (xywh) {// 雪碧图模式:使用坐标信息const [x, y, w, h] = xywh.split(',').map(Number);thumbnailElement.style.backgroundImage = `url(${imageUrl})`;thumbnailElement.style.backgroundPosition = `-${x}px -${y}px`;thumbnailElement.style.backgroundSize = 'auto';thumbnailElement.style.width = `${w}px`;thumbnailElement.style.height = `${h}px`;logger.info('🍒 缩略图更新(雪碧图模式)', {seekTime: seekTime.toFixed(2),imageUrl: imageUrl.substring(imageUrl.lastIndexOf('/') + 1),spritePosition: `${x},${y}`,spriteSize: `${w}x${h}`,thumbnailX: thumbnailX.toFixed(1)});} else {// 单图模式thumbnailElement.style.backgroundImage = `url(${imageUrl})`;thumbnailElement.style.backgroundSize = 'cover';thumbnailElement.style.backgroundPosition = 'center';thumbnailElement.style.width = '160px';thumbnailElement.style.height = '90px';logger.info('🍒 缩略图更新(单图模式)', {seekTime: seekTime.toFixed(2),imageUrl: imageUrl.substring(imageUrl.lastIndexOf('/') + 1),thumbnailX: thumbnailX.toFixed(1)});}// 设置位置和时间thumbnailElement.style.left = `${thumbnailX}px`;timeElement.textContent = formatTime(seekTime);
};

希望你会喜欢 EasyDSS 点播模块

上面贴了很多代码,作为用户可以直接嵌入 iframe, 接入我们实现的播放器,开发者可以使用 EasyPlayer 。

点击查看 easydss 论坛

欢迎来到论坛一起讨论

相关文章:

  • 医院关于申请网站建设的请示标题seo是什么意思
  • 武昌网站建设高端营销型网站
  • 微信小程序是免费的吗广州网站快速排名优化
  • PHP+MySQL网站开发全程实例百度西安分公司地址
  • 中国商标注册申请官网天津百度整站优化服务
  • 网站301跳转怎么做营销推广策略
  • SQL关键字三分钟入门:DELETE —— 删除数据
  • js 组装树形结构
  • Mac安装Apache CXF的时候报错:/Library/Internet: No such file or directory
  • Windows下Zookeeper客户端启动缓慢问题分析与解决方案
  • Python训练营-Day33
  • 在ASP.NET Core WebApi中使用标识框架(Identity)
  • 对象实例化内存布局与访问定位
  • spring项目启动sheel脚本
  • SpringBoot 数据库连接池与 ManticoreSearch 兼容性测试
  • 本地如何安装midscene.js运行环境
  • Liunx操作系统笔记2
  • 【AI论文】从跨领域视角重新审视强化学习在大型语言模型推理中的应用
  • 【实时Linux实战系列】基于实时Linux的音频处理应用开发
  • BGP边界网关协议
  • 深度图聚类DGC—Paper Notes
  • Windows所有系统自带.NET Framework版本win7,win10,win11预装.NET版本
  • CommunityToolkit.Mvvm 重构激光直写控制软件
  • Jenkins 常用定时构建脚本
  • 电商导购app平台的缓存策略与性能优化方案:架构师的实践经验
  • 将Python Tkinter程序转换为手机可运行的Web应用 - 详细教程