| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- /**
- * 竖滑视频资源缓存(App 端落盘 + 全端预取调度)
- * - App:downloadFile → saveFile,LRU 淘汰
- * - H5/小程序:依赖 video http-cache + 仅做预连接(不强制落盘)
- */
- const INDEX_KEY = 'hl88app:feed_video_cache_index'
- const MAX_ENTRIES = 12
- const MAX_BYTES = 120 * 1024 * 1024
- const MAX_CONCURRENT = 2
- /** @type {Map<string, { path: string, size: number, atime: number }>} */
- const memory = new Map()
- /** @type {Map<string, Promise<string>>} */
- const inflight = new Map()
- /** @type {string[]} */
- const waitQueue = []
- let activeCount = 0
- let indexLoaded = false
- function canPersistFiles() {
- // #ifdef APP-PLUS
- return true
- // #endif
- return false
- }
- function hashUrl(url) {
- let h = 0
- const s = String(url)
- for (let i = 0; i < s.length; i++) {
- h = (h << 5) - h + s.charCodeAt(i)
- h |= 0
- }
- return `v${Math.abs(h)}`
- }
- function loadIndex() {
- if (indexLoaded) return
- indexLoaded = true
- try {
- const raw = uni.getStorageSync(INDEX_KEY)
- if (!raw || typeof raw !== 'object') return
- Object.keys(raw).forEach((url) => {
- const row = raw[url]
- if (row && row.path) memory.set(url, row)
- })
- } catch (_) {}
- }
- function persistIndex() {
- try {
- const payload = {}
- memory.forEach((row, url) => {
- payload[url] = row
- })
- uni.setStorageSync(INDEX_KEY, payload)
- } catch (_) {}
- }
- function touch(url) {
- const row = memory.get(url)
- if (row) row.atime = Date.now()
- }
- function totalBytes() {
- let sum = 0
- memory.forEach((row) => {
- sum += Number(row.size) || 0
- })
- return sum
- }
- function evictLru() {
- while (memory.size >= MAX_ENTRIES || totalBytes() > MAX_BYTES) {
- let oldestUrl = ''
- let oldestAt = Infinity
- memory.forEach((row, url) => {
- if (row.atime < oldestAt) {
- oldestAt = row.atime
- oldestUrl = url
- }
- })
- if (!oldestUrl) break
- const victim = memory.get(oldestUrl)
- memory.delete(oldestUrl)
- if (victim && victim.path) {
- // #ifdef APP-PLUS
- uni.removeSavedFile({ filePath: victim.path, complete: () => {} })
- // #endif
- }
- }
- persistIndex()
- }
- function pumpQueue() {
- if (activeCount >= MAX_CONCURRENT || !waitQueue.length) return
- const url = waitQueue.shift()
- if (!url || memory.has(url) || inflight.has(url)) {
- pumpQueue()
- return
- }
- activeCount += 1
- const job = downloadAndSave(url)
- .then((local) => {
- if (local) {
- memory.set(url, { path: local, size: 0, atime: Date.now() })
- evictLru()
- }
- return local || url
- })
- .finally(() => {
- activeCount -= 1
- inflight.delete(url)
- pumpQueue()
- })
- inflight.set(url, job)
- }
- function downloadAndSave(url) {
- return new Promise((resolve) => {
- // #ifdef APP-PLUS
- uni.downloadFile({
- url,
- success: (res) => {
- if (res.statusCode !== 200 || !res.tempFilePath) {
- resolve('')
- return
- }
- uni.saveFile({
- tempFilePath: res.tempFilePath,
- success: (saved) => resolve(saved.savedFilePath || ''),
- fail: () => resolve('')
- })
- },
- fail: () => resolve('')
- })
- // #endif
- // #ifndef APP-PLUS
- resolve('')
- // #endif
- })
- }
- /**
- * 解析可播放地址:有本地缓存则返回 file:// 路径
- * @param {string} remoteUrl
- */
- export async function resolvePlaybackUrl(remoteUrl) {
- if (!remoteUrl) return ''
- loadIndex()
- if (!canPersistFiles()) return remoteUrl
- const cached = memory.get(remoteUrl)
- if (cached && cached.path) {
- touch(remoteUrl)
- return cached.path
- }
- if (inflight.has(remoteUrl)) {
- return inflight.get(remoteUrl)
- }
- const job = downloadAndSave(remoteUrl)
- .then((local) => {
- if (local) {
- uni.getSavedFileInfo({
- filePath: local,
- success: (info) => {
- memory.set(remoteUrl, {
- path: local,
- size: info.size || 0,
- atime: Date.now()
- })
- evictLru()
- },
- fail: () => {
- memory.set(remoteUrl, { path: local, size: 0, atime: Date.now() })
- evictLru()
- }
- })
- return local
- }
- return remoteUrl
- })
- .finally(() => inflight.delete(remoteUrl))
- inflight.set(remoteUrl, job)
- return job
- }
- /**
- * 后台预取(不阻塞当前播放)
- * @param {string} remoteUrl
- * @param {'high'|'low'} [priority]
- */
- export function prefetchPlaybackUrl(remoteUrl, priority = 'low') {
- if (!remoteUrl || !canPersistFiles()) return
- loadIndex()
- if (memory.has(remoteUrl) || inflight.has(remoteUrl)) return
- if (waitQueue.includes(remoteUrl)) return
- if (priority === 'high') waitQueue.unshift(remoteUrl)
- else waitQueue.push(remoteUrl)
- pumpQueue()
- }
- /** 按当前索引预取相邻视频 */
- export function prefetchNeighbors(clips, centerIndex, toRemoteUrl) {
- if (!Array.isArray(clips) || !clips.length) return
- const order = [centerIndex + 1, centerIndex - 1, centerIndex + 2]
- order.forEach((idx, i) => {
- const clip = clips[idx]
- if (!clip) return
- const remote = toRemoteUrl(clip)
- if (!remote) return
- prefetchPlaybackUrl(remote, i === 0 ? 'high' : 'low')
- })
- }
- export function clearVideoAssetCache() {
- memory.forEach((row) => {
- if (row.path) {
- // #ifdef APP-PLUS
- uni.removeSavedFile({ filePath: row.path, complete: () => {} })
- // #endif
- }
- })
- memory.clear()
- inflight.clear()
- waitQueue.length = 0
- activeCount = 0
- try {
- uni.removeStorageSync(INDEX_KEY)
- } catch (_) {}
- }
- export function readCachedPlaybackUrl(remoteUrl) {
- loadIndex()
- const row = memory.get(remoteUrl)
- return row && row.path ? row.path : ''
- }
- /** 本地文件损坏时剔除并回退远程播放 */
- export function dropCachedPlaybackUrl(remoteUrl) {
- if (!remoteUrl) return
- const row = memory.get(remoteUrl)
- memory.delete(remoteUrl)
- inflight.delete(remoteUrl)
- const idx = waitQueue.indexOf(remoteUrl)
- if (idx >= 0) waitQueue.splice(idx, 1)
- if (row && row.path) {
- // #ifdef APP-PLUS
- uni.removeSavedFile({ filePath: row.path, complete: () => {} })
- // #endif
- }
- persistIndex()
- }
|