expo-duooomi-app/@share/components/Video.tsx

188 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Image as ExpoImage, type ImageRef } from 'expo-image'
import { memo, useEffect, useRef, useState } from 'react'
import { View, type ViewStyle } from 'react-native'
import Video from 'react-native-video'
import tw from 'twrnc'
import { videoUrlCache } from '@/utils/storage'
import { mp4ToWebpUrl } from '@/utils/uploadFile'
import Img from './Img'
type Props = {
className?: string
url?: string
placeholderUrl?: string
needWeb?: boolean
style?: ViewStyle
width?: number
autoplay?: boolean // 控制动画播放,默认 true
} & React.ComponentProps<typeof Video>
// videoUrlCache.clear()
// ExpoImage.clearDiskCache()
// ExpoImage.clearMemoryCache()
// 默认宽度256半屏宽度
const VideoBox = ({
className = '',
url,
placeholderUrl = '',
width = 256,
style,
autoplay = true,
...videoProps
}: Props) => {
const [urlFinal, setUrlFinal] = useState('')
const imageRef = useRef<ImageRef | null>(null)
const isImg = (url: any) => {
if (!url) return false
const lowerUrl = url.toLowerCase()
return lowerUrl?.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|svg)(\?.*)?$/i)
}
const setRedirectUrl = async (isCancelled: () => boolean, url?: string) => {
const isImg2 = isImg(url)
if (isImg2) {
if (!isCancelled()) {
setUrlFinal(url!)
}
return
}
try {
// 先尝试从缓存获取
const cachedUrl = await videoUrlCache.get(url!, width)
// console.log('getRedirectUrl cachedUrl-----------', url, cachedUrl, width)
if (cachedUrl) {
// 只有当请求未被取消时才更新状态
if (!isCancelled()) {
setUrlFinal(cachedUrl)
}
return
}
const webpUrl = await mp4ToWebpUrl({ videoUrl: url!, width, height: width })
// 只有当请求未被取消时才缓存和更新状态
if (!isCancelled()) {
// 缓存结果
await videoUrlCache.set(url!, webpUrl, width)
// console.log('setRedirectUrl finalUrl-----------', finalUrl)
setUrlFinal(webpUrl)
}
} catch (error) {
console.warn('获取视频URL失败:', error)
// 只有当请求未被取消时才更新状态
if (!isCancelled()) {
// 错误时尝试使用原始URL
setUrlFinal(url!)
}
}
}
useEffect(() => {
// 每次url变化强制刷新
setUrlFinal('')
if (!url) {
return
}
// 用于标记当前请求是否已过期
let cancelled = false
setTimeout(() => {
if (!cancelled) {
setRedirectUrl(() => cancelled, url)
}
}, 0)
// 清理函数:当 url 变化或组件卸载时,标记当前请求为已取消
return () => {
cancelled = true
}
}, [url, width])
// 控制动画播放/停止
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 (
<ExpoImage
cachePolicy="disk"
source={{ uri: url }}
style={style as any}
transition={{ duration: 200, effect: 'cross-dissolve' }}
/>
)
}
const imageStyle = tw`${className}`
const showPlaceholder = !!placeholderUrl
return (
<View style={[style, imageStyle, { overflow: 'hidden' }]}>
{/* 占位图层 - 加载完成后隐藏 */}
{showPlaceholder && (
<Img
isCompression={false}
isWebP={true}
src={placeholderUrl}
style={{ position: 'absolute', width: '100%', height: '100%' }}
contentFit={'cover'}
transition={{ duration: 100, effect: 'curl-up' }}
/>
)}
{/* 真实图片层 */}
<ExpoImage
ref={imageRef}
// 只使用 disk 缓存减少内存占用
cachePolicy="disk"
// 添加 recyclingKey 帮助内存回收
contentFit={'cover'}
source={{ uri: urlFinal }}
style={style as any}
autoplay={autoplay}
transition={{ duration: 200, effect: 'cross-dissolve' }}
/>
</View>
)
}
export default memo(VideoBox)