视频进度条缩略图预览功能允许用户在拖动播放进度条时,实时预览对应时间点的视频画面。这能帮助用户快速定位视频内容,准确跳转,从而显著提升播放体验。
在开始集成前,请确保你已满足以下条件:
PlayAuthToken 时,必须指定 needThumbs=1 参数,以确保客户端能获取到雪碧图信息。详见以下服务端 SDK 文档:
注意
**iOS 平台:**受苹果的应用传输安全 (ATS) 策略限制,雪碧图 URL 必须使用 HTTPS 协议。请在签发 PlayAuthToken 时额外指定 Ssl=1,并确保你的点播域名已配置有效的 SSL 证书。
已阅读集成 SDK 成功初始化播放器 SDK,请确保 SDK 版本不低于 1.3.0。
实现缩略图预览的核心步骤如下图所示:
播放器加载视频信息后,你需要从回调中获取雪碧图的配置数据,并将其保存在组件的 state 中。
onFetchedVideoInfo 事件回调中,雪碧图信息位于 videoModel.ThumbInfoList[0]。onFetchedVideoInfo 回调中的 videoModel 一致。雪碧图配置 (ThumbInfoItem) 结构说明:
interface ThumbInfoItem { CaptureNum: number; // 缩略图总数 StoreUrls: string[]; // 雪碧图大图的 URL 列表 CellWidth: number; // 单个缩略图的宽度 CellHeight: number; // 单个缩略图的高度 ImgXLen: number; // 每行有多少个缩略图 ImgYLen: number; // 每列有多少个缩略图 Interval: number; // 截图时间间隔(秒) Format: string; // 截图格式 (e.g., "jpg") }
播放页示例代码:
// 初始化播放器 const player = await initPlayer({ viewId: 'player-view-id', } as InitPlayerOptions); // 设置事件监听器 player.setListener({ onFetchedVideoInfo(videoModel: any) { const thumbInfo = videoModel?.ThumbInfoList?.[0]; if (thumbInfo && thumbInfo.StoreUrls?.length > 0) { // 雪碧图数据可用,启用预览功能 setSpriteData(thumbInfo); setHasSpriteData(true); } }, onCurrentPlaybackTimeUpdate(currentTime: number) { setCurrentTime(currentTime); }, onLoadStateChanged(_engine: TTVideoEngine, loadState: number) { setLoadState(loadState); }, onPlaybackStateChanged(_engine: TTVideoEngine, playbackState: number) { setPlaybackState(playbackState); }, // 其他事件监听器... }); // 创建视频源并开始播放 const vidSource = createVidSource({ vid: 'your-video-id', playAuthToken: 'your-play-auth-token', } as VidSourceInitProps); player.setVideoSource(vidSource); player.play();
你需要监听用户在进度条上的手势(拖动、点击),根据手势位置计算出目标播放时间,并触发预览和跳转。
示例代码如下:
// 进度条拖拽处理 const handleProgressMove = useCallback((event: any) => { if (!hasSpriteData() || !duration) return; const locationX = event.nativeEvent.locationX; // 通过进度条拖拽位置计算出对应的时间进度 const newTime = calculateTimeFromPosition(locationX); // 显示缩略图预览 showThumbnailPreview(newTime); }, [hasSpriteData, duration, calculateTimeFromPosition, showThumbnailPreview]); // 进度条点击/释放处理 const handleProgressTouch = useCallback((event: any) => { const locationX = event.nativeEvent.locationX; // 通过进度条拖拽位置计算出对应的时间进度 const newTime = calculateTimeFromPosition(locationX); // 验证时间范围有效性 if (duration <= 0 || newTime < 0 || newTime > duration) { return; } // 跳转视频 player.seek(newTime, (success: boolean) => { if (!success) { console.warn('视频跳转失败'); } }); // 隐藏预览 hideThumbnailPreview(); }, [calculateTimeFromPosition, duration, hideThumbnailPreview]);
当 previewTime 发生变化时,你需要计算出该时间点对应的缩略图在雪碧图中的具体位置,并将其渲染出来。以下算法将一个时间点转换为雪碧图中的 (URL, x, y) 坐标。
Interval,计算出当前时间点对应第几张缩略图。ImgXLen * ImgYLen 张小图。根据时间索引计算出需要使用 StoreUrls 数组中的哪一张大图。CellWidth, CellHeight),得到最终的 (x, y) 偏移量。假设有以下配置:
Interval: 10(每10秒一个缩略图)ImgXLen: 5, ImgYLen: 4(5 列 4 行网格)CellWidth: 160, CellHeight: 90(每张缩略图的大小)对于播放时间点 65 秒:
Math.floor(65/10) = 66 ÷ (5×4) = 0 (第0个雪碧图), 6 % 20 = 6 (本地索引)row = Math.floor(6/5) = 1, col = 6%5 = 1x = 1×160 = 160, y = 1×90 = 90假设您获取到以下雪碧图信息:
Interval: 10:每 10 秒截取一帧缩略图。ImgXLen: 5, ImgYLen: 4:单张雪碧图采用 5 列 × 4 行 的网格布局,可容纳 20 张缩略图。CellWidth: 160, CellHeight: 90:每张缩略图的尺寸为 160px × 90px现在以播放到 65 秒的场景为例,计算缩略图位置:
Math.floor(65/10) = 6。计算结果表明,65 秒对应的是第 6 张缩略图(索引从 0 开始计数)。6 ÷ (5×4) = 0(对应第 0 张雪碧图),6 % 20 =6(在当前雪碧图中的本地索引为 6)row = Math.floor(6/5) =1(位于第 1 行),col =6%5=1(位于第 1 列)x=1×160=160,y=1×90=90即缩略图在雪碧图中的像素偏移量为 X:160px,Y:90px。
// 缩略图位置信息接口(计算结果) interface ThumbnailInfo { url: string; // 包含缩略图的雪碧图URL x: number; // 缩略图在雪碧图中的X坐标 y: number; // 缩略图在雪碧图中的Y坐标 width: number; // 缩略图宽度 height: number; // 缩略图高度 } const calculateThumbnailInfo = useCallback( (time: number): ThumbnailInfo | null => { const spriteConfig = getSpriteConfig(); if (!spriteConfig || !spriteConfig.StoreUrls?.length) { return null; } // 步骤1: 计算缩略图索引 const thumbIndex = Math.floor(time / spriteConfig.Interval); const clampedIndex = Math.min(thumbIndex, spriteConfig.CaptureNum - 1); // 步骤2: 确定雪碧图文件 const thumbsPerSprite = spriteConfig.ImgXLen * spriteConfig.ImgYLen; const spriteImageIndex = Math.floor(clampedIndex / thumbsPerSprite); const localThumbIndex = clampedIndex % thumbsPerSprite; // 步骤3: 计算网格位置 const row = Math.floor(localThumbIndex / spriteConfig.ImgXLen); const col = localThumbIndex % spriteConfig.ImgXLen; // 步骤4: 转换为像素坐标 const x = col * spriteConfig.CellWidth; const y = row * spriteConfig.CellHeight; return { url: spriteConfig.StoreUrls[spriteImageIndex], x, y, width: spriteConfig.CellWidth, height: spriteConfig.CellHeight, }; }, [getSpriteConfig], );
使用上述算法的结果,通过一个“视窗”(View)和偏移的 Image 来实现预览效果。
const renderThumbnailPreview = () => { if (!showPreview || !previewTime) return null; const thumbnailInfo = calculateThumbnailInfo(previewTime); if (!thumbnailInfo) return null; return ( <View style={styles.previewContainer}> <View style={styles.previewWindow}> {/* 缩略图容器 - 用于裁剪显示 */} <View style={[styles.thumbnailContainer, { width: thumbnailInfo.width, height: thumbnailInfo.height, }]}> {/* 完整雪碧图 - 通过偏移显示指定缩略图 */} <Image source={{uri: thumbnailInfo.url}} style={{ width: thumbnailInfo.width * spriteData.ImgXLen, height: thumbnailInfo.height * spriteData.ImgYLen, marginLeft: -thumbnailInfo.x, marginTop: -thumbnailInfo.y, }} resizeMode="stretch" /> </View> {/* 时间显示 */} <Text style={styles.timeText}> {formatTime(previewTime)} </Text> </View> </View> ); };
为了避免用户首次拖动进度条时因加载图片而出现延迟或闪烁,建议在获取到雪碧图配置后立即进行预加载。原理是:渲染一个或多个屏幕外的、透明的 Image 组件来加载所有雪碧图。一旦加载完成,图片数据就会被系统缓存。当用户实际预览时,可以直接从缓存中读取,实现“秒开”。
// 状态管理 const [preloadUrls, setPreloadUrls] = useState<string[]>([]); // 预加载雪碧图 const preloadSpriteImages = useCallback(() => { const spriteConfig = getSpriteConfig(); if (spriteConfig?.StoreUrls?.length) { setPreloadUrls(spriteConfig.StoreUrls); } }, [getSpriteConfig]); // 监听 modelInfo 变化,触发预加载 useEffect(() => { if (modelInfo?.ThumbInfoList?.length) { preloadSpriteImages(); } }, [modelInfo, preloadSpriteImages]); // 渲染隐藏的预加载图片 const renderPreloadImages = () => { return preloadUrls.map((url, index) => ( <Image key={`preload-${index}`} source={{uri: url}} style={{ position: 'absolute', width: 1, height: 1, opacity: 0, left: -9999, }} onLoad={() => { // 预加载完成 }} onError={() => { // 预加载失败 }} /> )); }; // 在JSX中使用 return ( <View> {/* 其他组件 */} {renderPreloadImages()} </View> );