背景
项目中需要对采集的数据做大屏展示,其中一部分数据是图片形式。每个点位一张图片,一系列点位按照顺序构成视频流。需要对图片依次播放实现定格动画。
原理
假设,定格动画播放速度是 5 fps,在播放时需要提前缓存 1s 的图片数据。据此,我们需要 6 个格子来存储预缓存的数据。同时,我们需要一个“指针”(为方便文书,后直接称为指针,指针所指的格子称指针格)来记录当前播放到了第几个格子。基础准备工作完成,下面开始介绍原理。
首先初始化,指针指向 0 号格子,根据接口获取到下一秒的播放数据,并依次缓存到格子中。如下图第一列所示。
开始播放,指针指向 1 号格子,0 号格子失效,如下图第二列所示。同时根据当前播放数据查询后面 5 条数据,即第 2 ~ 6 条的数据,从接口中取出最新一条数据,填入第 0 号格子,如下图第三列所示。
随着播放进行,指针会走到第 5 号格子,继续播放,需要将指针重置到第 0 位。以此循环。
原理图如下。
方案设计
实际考虑到除了正向播放外,还要支持倒向播放。为了播放的顺畅,于是在指针格的左侧同样需要 5 个缓存帧。除此之外,暂停/播放两个功能也需要支持。
前端功能设计完成,需要考虑接口内容该如何设计了,具体的接口如何设计这里不做讨论,这里主要讨论站在前端视角有哪些可行的方案。好的,首先我们的接口内容需要包含至少以下两个信息。
- 图片的序列号。
- 图片的访问地址。
除此之外,还期望接口返回给前端的是一个list,接口请求时将当前查询的 图片ID/序列号 给后端。后端返回给前端的数据是 该帧的 前 5 条数据 + 该帧的数据 + 该帧的 后 5 条数据。如果没有携带 图片ID/序列号 给后端,后端则返回 起始位置 以及后面 5 条数据。
基础准备工作有了,具体如何实施呢?其实也很简单。当我们开始播放时,指针就会开始移动,当他移动完后,获取到当前帧图片的数据,以此帧的数据查询接口,最后返回数据的最后(或第 0 条)一条一定是与缓存数据的最新一帧。再将这一帧数据填补到失效的那一格即可。
方案实现
下面以 react 为例,写一份实现方案。
import { useCallback, useEffect, useState } from 'react';
import { isEmpty } from 'lodash';
import useEffectInterval from './useEffectInterval';
export type PlayDirective = 'none' | 'forward' | 'backward';
export interface UseFrameCacheProps<F extends object = any, R extends Partial<F> = any> {
cacheLen: number;
frameInterval: number;
fetchList: (search?: R) => F[] | Promise<F[]>;
playDirective: PlayDirective;
}
export const useFrameCache = <F extends object = any, R extends Partial<F> = any>({
cacheLen = 5,
frameInterval = 200,
fetchList,
playDirective = 'forward',
}: UseFrameCacheProps) => {
const totalLen = cacheLen * 2 + 1;
const [played, setPlayed] = useState(false);
const [playIndex, setPlayIndex] = useState(0);
const [cacheList, setCacheList] = useState<F[]>([]);
const init = useCallback(async () => {
const data = await fetchList();
setPlayIndex(0);
setCacheList(data ?? []);
}, [fetchList]);
const handleStop =() => setPlayed(false);
const handleResume =() => setPlayed(true);
const handlePlay = useCallback(async () => {
const nextIndex = ((playDirective === 'forward' ? playIndex + 1 : playIndex - 1) + totalLen) % totalLen;
const nextInfo = cacheList?.[nextIndex];
try {
if (isEmpty(nextInfo)) {
throw new Error();
}
const data = await fetchList({
...nextInfo,
} as unknown as R) ?? [];
const replaceData = playDirective === 'forward' ? data?.[totalLen - 1] : data?.[data.length - totalLen];
const insertIndex = ((playDirective === 'forward' ? playIndex + 1 + 5 : playIndex - 1 - 5) + totalLen) % totalLen;
setCacheList((prev) => {
const newList = [...prev];
newList.splice(insertIndex, 1, replaceData);
return newList;
});
} catch {
setPlayed(false);
}
}, [cacheLen, fetchList, playDirective, playIndex, totalLen, cacheList]);
useEffectInterval({
fn: handlePlay,
delay: frameInterval,
active: played,
options: {
immediate: true,
}
});
useEffect(() => {
init();
}, []);
return {
playIndex,
cacheList,
currFrame: cacheList?.[playIndex],
handleStop,
handleResume,
};
};
export default useFrameCache;
上面例子中有使用 useEffectInterval,有不清楚的小伙伴可以查看我的 个人笔记:React Hook设计,如何更轻松使用Interval与Timeout