143 lines
3.7 KiB
TypeScript
143 lines
3.7 KiB
TypeScript
import { Image, type ImageRef } from 'expo-image'
|
||
import { memo, useEffect, useRef, useState } from 'react'
|
||
import { type ViewStyle } from 'react-native'
|
||
import Video from 'react-native-video'
|
||
|
||
import { videoUrlCache } from '@/utils/storage'
|
||
|
||
type Props = {
|
||
url?: string
|
||
needWeb?: boolean
|
||
style?: ViewStyle
|
||
width?: number
|
||
autoplay?: boolean // 控制动画播放,默认 true
|
||
} & React.ComponentProps<typeof Video>
|
||
|
||
// 默认宽度256半屏宽度
|
||
const VideoBox = ({ url, needWeb = true, width = 256, style, autoplay = true, ...videoProps }: Props) => {
|
||
const [urlFinal, setUrlFinal] = useState('')
|
||
const imageRef = useRef<ImageRef>(null)
|
||
|
||
const createUrl = (url: string) => {
|
||
return `https://modal-dev.bowong.cc/api/custom/video/converter/v2?media_url=${encodeURI(url)}&options=compression_level=3,quality=70,loop=true,resolution=${width}x${width},fps=24`
|
||
}
|
||
|
||
const isImg = (url: any) => {
|
||
if (!url) return false
|
||
const lowerUrl = url.toLowerCase()
|
||
return lowerUrl?.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|svg)(\?.*)?$/i)
|
||
}
|
||
|
||
async function resolveRedirect(url: string) {
|
||
const res = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
Range: 'bytes=0-0',
|
||
},
|
||
})
|
||
return res.url
|
||
}
|
||
|
||
const setRedirectUrl = async (url?: string) => {
|
||
const isImg2 = isImg(url)
|
||
if (isImg2) {
|
||
setUrlFinal(url!)
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 先尝试从缓存获取
|
||
const cachedUrl = await videoUrlCache.get(url!, width)
|
||
// console.log('getRedirectUrl cachedUrl-----------', url, cachedUrl, width)
|
||
if (cachedUrl) {
|
||
setUrlFinal(cachedUrl)
|
||
return
|
||
}
|
||
|
||
// 缓存未命中,进行网络请求
|
||
const webpUrl = createUrl(url!)
|
||
const finalUrl = await resolveRedirect(webpUrl)
|
||
|
||
// 缓存结果
|
||
await videoUrlCache.set(url!, finalUrl, width)
|
||
|
||
// console.log('setRedirectUrl finalUrl-----------', finalUrl)
|
||
setUrlFinal(finalUrl!)
|
||
} catch (error) {
|
||
console.warn('获取视频URL失败:', error)
|
||
// 错误时尝试使用原始URL
|
||
setUrlFinal(url!)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!url) return
|
||
setRedirectUrl(url!)
|
||
// const finalUrl = createUrl(url)
|
||
return
|
||
}, [url])
|
||
|
||
// 控制动画播放/停止
|
||
useEffect(() => {
|
||
if (imageRef.current) {
|
||
if (autoplay) {
|
||
imageRef.current?.startAnimating()
|
||
} else {
|
||
imageRef.current?.stopAnimating()
|
||
}
|
||
}
|
||
}, [autoplay])
|
||
|
||
if (!url) return null
|
||
|
||
// 本地文件全部用video组件播放
|
||
const isLocal = !url.startsWith('http://') && !url.startsWith('https://')
|
||
const isImageFile = isImg(url)
|
||
|
||
if (isLocal) {
|
||
if (!isImageFile) {
|
||
return (
|
||
<Video
|
||
key={url ?? 'no-url'}
|
||
muted
|
||
repeat
|
||
controls={false}
|
||
paused={false}
|
||
poster={url}
|
||
resizeMode="cover"
|
||
source={{ uri: url }}
|
||
style={style as any}
|
||
viewType={0}
|
||
volume={0}
|
||
{...videoProps}
|
||
/>
|
||
)
|
||
}
|
||
return (
|
||
<Image
|
||
cachePolicy="memory-disk"
|
||
source={{ uri: url }}
|
||
style={style as any}
|
||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
// 移除 key 避免组件重建导致闪烁,使用 transition 实现平滑切换
|
||
<Image
|
||
ref={imageRef}
|
||
// 只使用 disk 缓存,减少内存占用
|
||
cachePolicy="disk"
|
||
// 添加 recyclingKey 帮助内存回收
|
||
recyclingKey={urlFinal}
|
||
source={{ uri: urlFinal }}
|
||
style={style as any}
|
||
autoplay={autoplay}
|
||
transition={{ duration: 200, effect: 'cross-dissolve' }}
|
||
/>
|
||
)
|
||
}
|
||
|
||
export default memo(VideoBox)
|