You need to enable JavaScript to run this app.
视频点播

视频点播

复制全文
进阶功能
视频进度条缩略图预览
复制全文
视频进度条缩略图预览

视频进度条缩略图预览功能允许用户在拖动播放进度条时,实时预览对应时间点的视频画面。这能帮助用户快速定位视频内容,准确跳转,从而显著提升播放体验。


前提条件

在开始集成前,请确保你已满足以下条件:

服务端准备

注意

**iOS 平台:**受苹果的应用传输安全 (ATS) 策略限制,雪碧图 URL 必须使用 HTTPS 协议。请在签发 PlayAuthToken 时额外指定 Ssl=1,并确保你的点播域名已配置有效的 SSL 证书。

客户端准备

已阅读集成 SDK 成功初始化播放器 SDK,请确保 SDK 版本不低于 1.3.0

实现步骤

实现缩略图预览的核心步骤如下图所示:
Image

步骤 1:获取并保存雪碧图配置

播放器加载视频信息后,你需要从回调中获取雪碧图的配置数据,并将其保存在组件的 state 中。

  • Vid 模式:在 onFetchedVideoInfo 事件回调中,雪碧图信息位于 videoModel.ThumbInfoList[0]
  • DirectURL 模式:你需要在播放前,自行调用服务端 GetPlayInfo 接口获取包含雪碧图信息的完整播放数据,其结构与 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();

步骤 2:处理进度条交互与 Seek 跳转

你需要监听用户在进度条上的手势(拖动、点击),根据手势位置计算出目标播放时间,并触发预览和跳转。
Image
示例代码如下:

// 进度条拖拽处理
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]);

步骤 3:计算缩略图位置信息

previewTime 发生变化时,你需要计算出该时间点对应的缩略图在雪碧图中的具体位置,并将其渲染出来。以下算法将一个时间点转换为雪碧图中的 (URL, x, y) 坐标。

  1. 计算时间索引:根据截图间隔 Interval,计算出当前时间点对应第几张缩略图。
  2. 定位雪碧图文件:一张雪碧图包含 ImgXLen * ImgYLen 张小图。根据时间索引计算出需要使用 StoreUrls 数组中的哪一张大图。
  3. 计算网格位置:计算出小图在该雪碧图的二维网格中的行(row)和列(col)。
  4. 转换像素坐标:将行列位置乘以小图的宽高(CellWidth, CellHeight),得到最终的 (x, y) 偏移量。

计算示例

假设有以下配置:

  • Interval: 10(每10秒一个缩略图)
  • ImgXLen: 5, ImgYLen: 4(5 列 4 行网格)
  • CellWidth: 160, CellHeight: 90(每张缩略图的大小)

对于播放时间点 65 秒:

  1. 索引计算: Math.floor(65/10) = 6
  2. 雪碧图定位: 6 ÷ (5×4) = 0 (第0个雪碧图), 6 % 20 = 6 (本地索引)
  3. 网格位置: row = Math.floor(6/5) = 1, col = 6%5 = 1
  4. 像素坐标: x = 1×160 = 160, y = 1×90 = 90

假设您获取到以下雪碧图信息:

  • Interval: 10:每 10 秒截取一帧缩略图。
  • ImgXLen: 5, ImgYLen: 4:单张雪碧图采用 5 列 × 4 行 的网格布局,可容纳 20 张缩略图。
  • CellWidth: 160, CellHeight: 90:每张缩略图的尺寸为 160px × 90px

现在以播放到 65 秒的场景为例,计算缩略图位置:

  1. 索引计算Math.floor(65/10) = 6。计算结果表明,65 秒对应的是第 6 张缩略图(索引从 0 开始计数)。
  2. 雪碧图定位6 ÷ (5×4) = 0(对应第 0 张雪碧图),6 % 20 =6(在当前雪碧图中的本地索引为 6)
  3. 网格位置row = Math.floor(6/5) =1(位于第 1 行),col =6%5=1(位于第 1 列)
  4. 像素坐标x=1×160=160y=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],
);

步骤 4:渲染预览窗口

使用上述算法的结果,通过一个“视窗”(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>
);
最近更新时间:2026.03.20 16:24:51
这个页面对您有帮助吗?
有用
有用
无用
无用