fix(gif-player): 循环回到首帧重置缓冲区修复冻结 + 模块级双缓存避免重复下载; bump MEDIA_CACHE_BUST 以拉取压缩后动图(1.8G->856M)
This commit is contained in:
@@ -1,8 +1,105 @@
|
|||||||
// gif-player:微信 <image> 组件不播放 GIF 动画(只渲染首帧),
|
// gif-player:微信 <image> 组件不播放 GIF 动画(只渲染首帧),
|
||||||
// 这里用 Canvas 2D + 纯 JS 解码器(omggif)逐帧绘制,实现真正的动图播放。
|
// 这里用 Canvas 2D + 纯 JS 解码器(omggif)逐帧绘制,实现真正的动图播放。
|
||||||
|
//
|
||||||
|
// 两个关键修复:
|
||||||
|
// 1) 循环:循环回到首帧时重置累积缓冲区,避免上一轮 disposal 残留导致画面冻结/错乱。
|
||||||
|
// 2) 缓存:同一 GIF 下载/解码一次后,同会话用内存缓存秒开,跨会话用本地文件缓存,
|
||||||
|
// 不再每次进入详情页都重新下载。
|
||||||
const { GifReader } = require('../../utils/vendor/omggif.js');
|
const { GifReader } = require('../../utils/vendor/omggif.js');
|
||||||
const config = require('../../config.js');
|
const config = require('../../config.js');
|
||||||
|
|
||||||
|
// ---------- 模块级 GIF 缓存 ----------
|
||||||
|
// 内存缓存(同会话秒开) + 本地文件缓存(跨会话持久) 双保险。
|
||||||
|
const _mem = new Map(); // url -> { buf, reader } 按插入顺序充当 LRU
|
||||||
|
const _inflight = new Map(); // url -> Promise,去重并发请求,防止同一 GIF 重复下载
|
||||||
|
const _MAX_MEM = 30; // 内存最多保留 30 个解码结果
|
||||||
|
const _MAX_LOCAL = 12; // 本地文件最多保留 12 个(约数十 MB,避免超配额)
|
||||||
|
const _localIndex = []; // 本地文件访问顺序(用于 LRU 淘汰)
|
||||||
|
|
||||||
|
function _hash(s) {
|
||||||
|
let h = 0;
|
||||||
|
for (let i = 0; i < s.length; i++) h = (Math.imul(h, 31) + s.charCodeAt(i)) >>> 0;
|
||||||
|
return h.toString(36);
|
||||||
|
}
|
||||||
|
function _localPath(url) {
|
||||||
|
// 含 cb 参数天然区分媒体版本,换超分动图后旧缓存不会误命中
|
||||||
|
return `${wx.env.USER_DATA_PATH}/gifcache_${_hash(url)}.bin`;
|
||||||
|
}
|
||||||
|
function _readLocal(url) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
wx.getFileSystemManager().readFile({
|
||||||
|
filePath: _localPath(url),
|
||||||
|
success: (r) => resolve(r.data),
|
||||||
|
fail: () => resolve(null)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function _writeLocal(url) {
|
||||||
|
const entry = _mem.get(url);
|
||||||
|
if (!entry) return;
|
||||||
|
try {
|
||||||
|
wx.getFileSystemManager().writeFileSync(_localPath(url), entry.buf);
|
||||||
|
const p = _localPath(url);
|
||||||
|
const i = _localIndex.indexOf(p);
|
||||||
|
if (i >= 0) _localIndex.splice(i, 1);
|
||||||
|
_localIndex.push(p);
|
||||||
|
while (_localIndex.length > _MAX_LOCAL) {
|
||||||
|
const old = _localIndex.shift();
|
||||||
|
try { wx.getFileSystemManager().unlinkSync(old); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
} catch (e) { /* 配额不足等写入失败,忽略:内存缓存仍生效 */ }
|
||||||
|
}
|
||||||
|
function _touchMem(url) {
|
||||||
|
const v = _mem.get(url);
|
||||||
|
if (v) { _mem.delete(url); _mem.set(url, v); } // 移到末尾 = 最近使用
|
||||||
|
}
|
||||||
|
function _evictMem() {
|
||||||
|
while (_mem.size > _MAX_MEM) {
|
||||||
|
const k = _mem.keys().next().value;
|
||||||
|
_mem.delete(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function loadGif(url) {
|
||||||
|
if (_mem.has(url)) {
|
||||||
|
_touchMem(url);
|
||||||
|
return Promise.resolve(_mem.get(url));
|
||||||
|
}
|
||||||
|
if (_inflight.has(url)) return _inflight.get(url);
|
||||||
|
const p = (async () => {
|
||||||
|
// 先试本地文件缓存(跨会话)
|
||||||
|
const local = await _readLocal(url);
|
||||||
|
if (local) {
|
||||||
|
const reader = new GifReader(new Uint8Array(local));
|
||||||
|
const entry = { buf: local, reader };
|
||||||
|
_mem.set(url, entry);
|
||||||
|
_touchMem(url);
|
||||||
|
_evictMem();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
// 未命中则下载
|
||||||
|
const buf = await new Promise((resolve, reject) => {
|
||||||
|
wx.request({
|
||||||
|
url,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
success: (res) => (res.statusCode >= 200 && res.statusCode < 300
|
||||||
|
? resolve(res.data)
|
||||||
|
: reject(new Error('HTTP ' + res.statusCode))),
|
||||||
|
fail: reject
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const reader = new GifReader(new Uint8Array(buf));
|
||||||
|
const entry = { buf, reader };
|
||||||
|
_mem.set(url, entry);
|
||||||
|
_touchMem(url);
|
||||||
|
_evictMem();
|
||||||
|
_writeLocal(url); // 落本地,下次跨会话可直接读
|
||||||
|
return entry;
|
||||||
|
})();
|
||||||
|
_inflight.set(url, p);
|
||||||
|
p.finally(() => _inflight.delete(url));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
Component({
|
Component({
|
||||||
properties: {
|
properties: {
|
||||||
// GIF 地址(通常是 ex.gifUrl,超分后的清晰动图)。
|
// GIF 地址(通常是 ex.gifUrl,超分后的清晰动图)。
|
||||||
@@ -53,22 +150,16 @@ Component({
|
|||||||
this._loadedSrc = src;
|
this._loadedSrc = src;
|
||||||
this._clearTimer();
|
this._clearTimer();
|
||||||
this.setData({ loading: true, failed: false });
|
this.setData({ loading: true, failed: false });
|
||||||
wx.request({
|
loadGif(src).then((entry) => {
|
||||||
url: src,
|
// detached 或 src 已变化则放弃
|
||||||
responseType: 'arraybuffer',
|
if (this._aborted || this._loadedSrc !== src) return;
|
||||||
success: (res) => {
|
this._reader = entry.reader;
|
||||||
// detached 之后才允许放弃
|
this._frame = 0;
|
||||||
if (this._aborted) return;
|
this._start();
|
||||||
try {
|
}).catch((e) => {
|
||||||
this._reader = new GifReader(new Uint8Array(res.data));
|
if (!this._aborted) {
|
||||||
this._start();
|
console.error('[gif-player] load fail', e);
|
||||||
} catch (e) {
|
this.setData({ failed: true, loading: false });
|
||||||
console.error('[gif-player] decode fail', e);
|
|
||||||
this.setData({ failed: true, loading: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: () => {
|
|
||||||
if (!this._aborted) this.setData({ failed: true, loading: false });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -101,6 +192,11 @@ Component({
|
|||||||
const n = r.numFrames();
|
const n = r.numFrames();
|
||||||
if (!n) { this.setData({ failed: true, loading: false }); return; }
|
if (!n) { this.setData({ failed: true, loading: false }); return; }
|
||||||
try {
|
try {
|
||||||
|
// 循环回到首帧时重置累积缓冲区,清除上一轮 disposal 残留,
|
||||||
|
// 否则画面会在播放一轮后冻结/错乱。
|
||||||
|
if (this._frame === 0) {
|
||||||
|
this._rgba = new Uint8ClampedArray(r.width * r.height * 4);
|
||||||
|
}
|
||||||
// decodeAndBlitFrameRGBA 按 GIF 的 disposal 规则在 rgba 上累积绘制
|
// decodeAndBlitFrameRGBA 按 GIF 的 disposal 规则在 rgba 上累积绘制
|
||||||
r.decodeAndBlitFrameRGBA(this._frame, this._rgba);
|
r.decodeAndBlitFrameRGBA(this._frame, this._rgba);
|
||||||
const img = this._ctx.createImageData(r.width, r.height);
|
const img = this._ctx.createImageData(r.width, r.height);
|
||||||
@@ -117,7 +213,7 @@ Component({
|
|||||||
// omggif 的 delay 单位是 1/100 秒,需 ×10 转毫秒;缺失时给 100ms
|
// omggif 的 delay 单位是 1/100 秒,需 ×10 转毫秒;缺失时给 100ms
|
||||||
const delay = (info && info.delay) ? info.delay * 10 : 100;
|
const delay = (info && info.delay) ? info.delay * 10 : 100;
|
||||||
this._frame = (this._frame + 1) % n;
|
this._frame = (this._frame + 1) % n;
|
||||||
// 单帧 GIF 只画一次;多帧才循环。最小 40ms 避免极端快帧
|
// 单帧 GIF 只画一次;多帧无限循环(健身演示需要持续播放)。最小 40ms 避免极端快帧
|
||||||
if (n > 1) this._timer = setTimeout(() => this._render(), Math.max(40, delay));
|
if (n > 1) this._timer = setTimeout(() => this._render(), Math.max(40, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ module.exports = {
|
|||||||
BASE_URL: 'http://192.227.237.8:3001',
|
BASE_URL: 'http://192.227.237.8:3001',
|
||||||
// 媒体资源缓存击穿:服务器静态文件(动图/图片)被覆盖新版本后,旧版可能被微信请求缓存,
|
// 媒体资源缓存击穿:服务器静态文件(动图/图片)被覆盖新版本后,旧版可能被微信请求缓存,
|
||||||
// 导致客户端仍显示旧图。换媒体(如重做超分动图)后修改此值即可强制全量客户端重新下载。
|
// 导致客户端仍显示旧图。换媒体(如重做超分动图)后修改此值即可强制全量客户端重新下载。
|
||||||
MEDIA_CACHE_BUST: '20260724'
|
// 2026-08-02:服务器端 1324 个动图经 gifsicle -O3 --lossy=80 压缩(1.8G→856M,分辨率/帧数不变),
|
||||||
|
// 改此值让客户端重新拉取压缩后的小体积版本。
|
||||||
|
MEDIA_CACHE_BUST: '20260802'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user