import moment from 'moment'
import { useEffect, useRef } from 'react'
import { ScrollShotUrls, seekVideo } from '..'
export enum ShotStatus {
INIT,
SUCCESS,
FAIL,
INITFAIL,
}
export interface VideoShotProps {
file?: File
shotUrlName?: string
shotUrl?: ScrollShotUrls[]
setFieldValue?: (field: string, value: any, shouldValidate?: boolean | undefined) => void
getShotStatus?: (status: ShotStatus) => void
}
export const VideoShot: React.FC<VideoShotProps> = (props) => {
const {
setFieldValue,
file,
shotUrlName,
shotUrl,
getShotStatus
} = props
const videoRef = useRef<HTMLVideoElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const slideWidth = 750
const slideHeight = 44
useEffect(() => {
if (!file || shotUrl?.length !== 0) {
return
}
console.log('start', file)
videoRef.current = document.createElement('video')
canvasRef.current = document.createElement('canvas')
const video: HTMLVideoElement | null = videoRef.current as HTMLVideoElement
let canvas: HTMLCanvasElement | null = canvasRef.current as HTMLCanvasElement
const url = window.URL.createObjectURL(file)
video.src = url
if (!(typeof video.canPlayType === 'function')) {
handleChangeStatus(ShotStatus.INITFAIL)
}
console.log('startTime:', moment())
video?.addEventListener('loadeddata', async () => {
const { duration, videoWidth, videoHeight } = video
if (!duration || !videoWidth || !videoHeight || !canvas) {
console.error('视频文件存在问题')
handleChangeStatus(ShotStatus.INITFAIL)
return
}
const toobarHeight = slideHeight
const toobarWidth = (toobarHeight * videoWidth) / videoHeight
const toobarCount = Math.ceil(slideWidth / toobarWidth)
const loopcount = duration / (toobarCount - 1)
const drawWidth = toobarWidth * 2
const drawHeight = toobarHeight * 2
video.width = drawWidth
video.height = drawHeight
video.style.height = `${drawWidth}px`
video.style.width = `${drawHeight}px`
canvas.width = drawWidth
canvas.height = drawHeight
const shotUrl: ScrollShotUrls[] = []
for (let i = 0
try {
const temp = (await seekVideo(video, canvas, Math.min(loopcount * i, duration), drawWidth, drawHeight, true)) as {
time: number
img: string
}
shotUrl.push({ ...temp, toobarWidth, toobarHeight, duration })
} catch (error) {
console.error(error)
}
}
console.log('endTime: ', moment())
if (setFieldValue && shotUrlName) {
setFieldValue(shotUrlName, shotUrl)
handleChangeStatus(ShotStatus.SUCCESS)
canvas = null
}
})
video.addEventListener('error', (err) => {
console.error(err, 'listenError')
handleChangeStatus(ShotStatus.INITFAIL)
})
}, [file])
const handleChangeStatus = (status: ShotStatus) => {
if (getShotStatus && typeof getShotStatus === 'function') {
getShotStatus(status)
}
}
return null
}
import { useEffect, useRef, useState } from 'react'
import { SliderBar } from './sliderBar'
import styles from './index.less'
import ImageLoading from '@/pages/upload/ImageLoading'
import { ShotStatus } from './shot/shot'
import { XtButton } from '@shared/components/XtButton'
import CcLoading from '@/components/core/CcLoading'
export interface VideoScreenShotProps {
file?: File
shotUrl?: ScrollShotUrls[]
setFieldValue?: (field: string, value: any, shouldValidate?: boolean | undefined) => void
shotUrlName?: string
shotStatus?: ShotStatus
onSave: (url: string) => void
}
export interface ScrollShotUrls {
time: number
img: string
toobarWidth?: number
toobarHeight?: number
shotReady?: boolean
duration: number
}
export function seekVideo(
video: HTMLVideoElement,
canvas: HTMLCanvasElement,
time: number,
drawWidth: number,
drawHeight: number,
qutZero?: boolean,
) {
let resolve: (arg0: { time: number
const onSeeked = () => {
video?.removeEventListener('seeked', onSeeked)
const context = canvas.getContext('2d')
context?.drawImage(video, 0, 0, drawWidth, drawHeight)
const img = canvas.toDataURL()
resolve?.({ time, img })
}
return new Promise((r) => {
resolve = r
video?.addEventListener('seeked', onSeeked)
let t = time
// 很多视频起始帧是无意义的黑屏,默认取 0.1s 开始
if ((t <= 0 && qutZero) || isNaN(t)) {
t = 0.1
}
// 末尾的帧向下取整到小数点后 1 位,否则有可能指定 currentTime 无效
if (video?.duration - t < 0.1) {
t = Math.floor(t)
}
video.currentTime = parseInt(`${t * 10}`, 10) / 10
})
}
export const VideoScreenShot: React.FC<VideoScreenShotProps> = (props) => {
const { file, shotUrl: propsShotUrls = [], shotStatus, onSave } = props
const videoRef = useRef<HTMLVideoElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const [error, setError] = useState(false)
const [videoSrc, setVideoSrc] = useState('')
const [shotUrls, setShotUrls] = useState<ScrollShotUrls[]>([])
const [shotIng, setShotIng] = useState(false)
useEffect(() => {
setShotUrls(propsShotUrls)
}, [propsShotUrls])
const [sliderValue, setSliderValue] = useState(0)
const [shotInfo, setShotInfo] = useState({
duration: 0,
})
useEffect(() => {
if (!file || !canvasRef?.current || !videoRef?.current) {
return
}
const video: HTMLVideoElement = videoRef?.current as HTMLVideoElement
const url = window.URL.createObjectURL(file)
setVideoSrc(url)
video.src = url
const canvas: HTMLCanvasElement = canvasRef?.current as HTMLCanvasElement
video.addEventListener('loadeddata', async () => {
const { duration, videoWidth, videoHeight } = videoRef.current as HTMLVideoElement
if (!duration || !videoWidth || !videoHeight) {
setError(true)
}
setShotInfo({
duration,
})
canvas.width = videoWidth
canvas.height = videoHeight
video.addEventListener('error', () => {
setError(true)
})
})
}, [file, videoRef?.current, canvasRef?.current])
const handleShotScreen = async (time: number) => {
setShotIng(true)
if (videoRef.current && canvasRef.current) {
const temp = (await seekVideo(
videoRef.current,
canvasRef.current,
time,
videoRef.current.videoWidth,
videoRef.current.videoHeight,
)) as ScrollShotUrls
setShotIng(false)
onSave(temp.img)
}
}
const handleSliderChange = (value: number | number[]) => {
setSliderValue(value as number)
if (typeof value === 'number' && videoRef.current) {
videoRef.current.currentTime = value
}
}
const handleClick = (e: any) => {
const sliderImgsLeft = document.getElementById('sliderImgsId')?.getBoundingClientRect().left || 0
const clickX = e.clientX - sliderImgsLeft
const average = shotInfo.duration / 730
handleSliderChange(clickX * average)
}
if (shotStatus === ShotStatus.FAIL || shotStatus === ShotStatus.INITFAIL || error) {
return (
<div className={styles.loadingMsg}>
<ImageLoading text="当前视频不支持动态裁剪,建议手工上传封面"></ImageLoading>
</div>
)
}
if (shotUrls?.length === 0) {
return (
<div className={styles.loadingMsg}>
<ImageLoading text="视频正在加载中,请稍后"></ImageLoading>
</div>
)
}
return (
<div className={styles.conatiner}>
<canvas className={styles.hidden} ref={canvasRef} />
<video className={styles.playContainer} ref={videoRef} src={videoSrc} />
<div className={styles.sliderBox}>
<div className={styles.smallImgs} onClick={handleClick} id='sliderImgsId'>
<div style={{ display: 'flex' }}>
{shotUrls?.map((e, i) => (
<img key={i} src={e?.img} alt="Screenshot" style={{ width: e.toobarWidth, height: e.toobarHeight }} />
))}
</div>
</div>
<SliderBar
range={false}
step={0.1}
value={sliderValue}
min={0.1}
max={shotInfo.duration}
onChange={handleSliderChange}
/>
</div>
<div className={styles.footer}>
<XtButton style={{ width: 102, height: 40 }} onClick={() => handleShotScreen(sliderValue)}>
下一步{shotIng ? <CcLoading /> : null}
</XtButton>
</div>
</div>
)
}
import 'rc-slider/assets/index.css';
import Slider, { SliderProps } from 'rc-slider';
import styles from './slider.less';
export type SliderBarProps = SliderProps;
const trackStyle = {
height: 44,
};
export const SliderBar: React.FC<SliderBarProps> = (props) => (
<div className={styles.sliderContainer}>
<Slider
className={styles.ccSlider}
defaultValue={0}
min={0}
trackStyle={trackStyle}
railStyle={trackStyle}
{...props}
/>
</div>
);