Web 点播播放器如何加载缩略图?
Web播放器加载缩略图的实现方案 本文介绍了在各类Web播放器(如DPlayer)中实现缩略图预览功能的技术方案。主要步骤包括: 在视频元数据加载完成后初始化缩略图功能 通过WebVTT文件获取缩略图元数据 使用雪碧图技术显示对应时间点的缩略图 核心实现包含两个关键函数: setupThumbnailPreview:设置进度条事件监听,计算鼠标位置并显示缩略图 updateThumbnailCon
在前面的文章说,说明了服务端如何生成雪碧图/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);
}
});
重点要关注的是 setupThumbnailPreview
和 updateThumbnailContent
函数,简单来说做了以下几个步骤
- 计算鼠标在进度条上的位置
- 在该位置上方显示缩略图
- 在缩略图里面显示进度
那么如何确定显示雪碧图中的哪个 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 URL
const 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 。
欢迎来到论坛一起讨论
更多推荐
所有评论(0)