665 lines
21 KiB
TypeScript
665 lines
21 KiB
TypeScript
import React, { Dispatch, ReactElement, SetStateAction, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||
import {
|
||
noop,
|
||
get,
|
||
addEventListenerWrapper,
|
||
dataURLToBlob,
|
||
nextTick,
|
||
toRealNumber,
|
||
getTransforms,
|
||
formatDurationTime
|
||
} from '@zhst/func';
|
||
import Align from 'rc-align';
|
||
import { Rect, IScreenshotButtonProp, AlignType } from '@zhst/types'
|
||
import { useLatest, useUpdateEffect, useFullscreen, useUnmount } from '@zhst/hooks';
|
||
import classNames from 'classnames';
|
||
import download from 'downloadjs';
|
||
import { Button, message } from '..';
|
||
import Icon from '../iconfont';
|
||
import {
|
||
Cropper,
|
||
EVENT_CROP_START,
|
||
EVENT_CROP_END,
|
||
} from '../ImageEditor';
|
||
import FlvPlayer, { FLV_EVENT } from './components/FlvPlayer';
|
||
import Range from './components/Progress';
|
||
import Loading from './components/Loading';
|
||
import { CROP_TYPE } from '../utils/constants';
|
||
import { getShowStatus } from './videoPlayerHelper'
|
||
import './index.less'
|
||
|
||
const componentName = `zhst-image__video-view`;
|
||
|
||
export interface VideoViewProps {
|
||
/* 播放地址 */
|
||
url: string;
|
||
/* 播放总时间 */
|
||
maxDuration?: number;
|
||
/* 截图渲染 */
|
||
screenshotButtonAlign?: AlignType;
|
||
screenshotButtonRender?: (screenshotButtonProp: IScreenshotButtonProp) => ReactElement;
|
||
/* 默认截图框 */
|
||
defautlNormalizationRect?: Rect;
|
||
/* 截图回调 */
|
||
onCropChange?: (showCrop: boolean, normalizationRect: null | Rect) => void;
|
||
}
|
||
|
||
export interface VideoViewRef {
|
||
/* 当前图片模式 */
|
||
cropAble: boolean;
|
||
setShowCrop: Dispatch<SetStateAction<boolean>>;
|
||
downloadVideoframe: () => void;
|
||
}
|
||
|
||
const VideoPlayer = forwardRef<VideoViewRef, VideoViewProps>((props, ref) => {
|
||
const {
|
||
url,
|
||
maxDuration = 20,
|
||
screenshotButtonAlign = {
|
||
points: ['bl', 'br'],
|
||
offset: [6, 0],
|
||
overflow: {
|
||
adjustX: true,
|
||
adjustY: true,
|
||
},
|
||
},
|
||
screenshotButtonRender = () => <div style={{ color: '#fff', width: '80px', top: 0 }}>回调DOM</div>,
|
||
onCropChange,
|
||
defautlNormalizationRect: defaultNormalizationRect,
|
||
} = props;
|
||
// ========================== 播放 =========================
|
||
//实例参数
|
||
const containerRef: any = useRef(null); //容器ref
|
||
const videoRef: any = useRef(null); //video 标签dom
|
||
const videoInsRef: any = useRef(null); //flv 实例
|
||
const [playSeq, setPlaySeq] = useState(0); // 通过重置playid使FLV组件重新渲染
|
||
const videoRemoveListener = useRef(noop); //移除dom监听的中间函数
|
||
const loadingTimeRef = useRef<number | null>(0); //最后一次加载时间
|
||
const delayLoadingTimer: any = useRef(null); //算loading的定时器
|
||
//状态参数
|
||
const [isReady, setIsReady] = useState(false); //
|
||
const [isPlay, setIsPlay] = useState(false); //当前是否播放
|
||
|
||
const [isEnd, setIsEnd] = useState(false); //是否播放结束
|
||
const [isError, setIsError] = useState(false); //播放出错
|
||
const [isVideoLoadFinished, setIsVideoLoadFinish] = useState(false); //是否缓存加载完成
|
||
const [playTime, setPlayTime] = useState(0); //当前播放时间
|
||
const [isLoadingVideo, setIsLoadingVideo] = useState(true); //是否加载中
|
||
const [isDelayLoading, setIsDelayLoading] = useState(false); //显示的转圈loading 延迟0.2s显示
|
||
//设置延迟转圈圈
|
||
const latestIsLoadingVideo = useLatest(isLoadingVideo);
|
||
const setIsLoadingVideoWrapper = (isLoading: boolean) => {
|
||
setIsLoadingVideo((preLoading) => {
|
||
if (!preLoading && isLoading) {
|
||
loadingTimeRef.current = new Date().getTime();
|
||
}
|
||
if (!isLoading) {
|
||
loadingTimeRef.current = null;
|
||
}
|
||
//延迟0。2s相关
|
||
if (!isLoading) {
|
||
setIsDelayLoading(false);
|
||
}
|
||
if (!delayLoadingTimer.current && preLoading) {
|
||
delayLoadingTimer.current = setTimeout(() => {
|
||
if (latestIsLoadingVideo.current) {
|
||
//0.2s后才显示
|
||
setIsDelayLoading(true);
|
||
}
|
||
delayLoadingTimer.current = null;
|
||
}, 200);
|
||
}
|
||
return isLoading;
|
||
});
|
||
};
|
||
// 初始化loading 30s 直接显示错误
|
||
// TODO :逻辑忘记了 不应该是每次init player吗?
|
||
useEffect(() => {
|
||
let timer = setInterval(() => {
|
||
if (loadingTimeRef.current) {
|
||
if (new Date().getTime() - loadingTimeRef.current > 1000 * 30) {
|
||
checkIsErr()
|
||
}
|
||
}
|
||
}, 1000);
|
||
return () => {
|
||
clearInterval(timer);
|
||
};
|
||
}, []);
|
||
|
||
//结束的时候暂停 保证不播了
|
||
useUpdateEffect(() => {
|
||
if (isEnd) {
|
||
videoInsRef?.current?.pause?.();
|
||
}
|
||
}, [isEnd]);
|
||
|
||
// 捕捉视频播放报错
|
||
const checkIsErr = () => {
|
||
setIsError(true)
|
||
try {
|
||
videoInsRef?.current?.destroy?.();
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
const latestMaxDuration = useLatest(maxDuration);
|
||
const initPlayer = useCallback((ins: any, dom: any) => {
|
||
videoRef.current = dom;
|
||
videoInsRef.current = ins;
|
||
const maxDuration = latestMaxDuration.current || 0;
|
||
//监听播放事件
|
||
let video = dom;
|
||
let errorLister = (e: any) => {
|
||
checkIsErr();
|
||
console.error('视频出错了', e, video.currentTime);
|
||
};
|
||
let waitingListener = () => {
|
||
setIsLoadingVideoWrapper(true);
|
||
// console.debug('视频加载等待', e, video.currentTime);
|
||
};
|
||
let playingListener = () => {
|
||
setIsLoadingVideoWrapper(false);
|
||
setIsError(false)
|
||
// console.debug('视频从等待中播放', e, video.currentTime);
|
||
};
|
||
let playLister = () => {
|
||
setIsPlay(true);
|
||
setIsError(false)
|
||
// console.debug('提示该视频正在播放中', e, video.currentTime);
|
||
};
|
||
let pauseListener = () => {
|
||
setIsPlay(false);
|
||
// console.debug('暂停播放', e, video.currentTime);
|
||
};
|
||
let endedListner = () => {
|
||
setIsEnd(true);
|
||
setIsVideoLoadFinish(true);
|
||
// console.debug('视频播放完了', e, video.currentTime);
|
||
};
|
||
let timeupdateListner = () => {
|
||
let nowTime = video.currentTime;
|
||
if (nowTime >= maxDuration) {
|
||
setIsEnd(true);
|
||
setIsVideoLoadFinish(true);
|
||
}
|
||
setPlayTime(nowTime);
|
||
};
|
||
// see https://github.com/bilibili/flv.js/issues/337
|
||
let windowErrorHandle = (errorEvent: any) => {
|
||
try {
|
||
if (
|
||
errorEvent['message'] ==
|
||
"Uncaught TypeError: Cannot read property 'flushStashedSamples' of null"
|
||
) {
|
||
checkIsErr();
|
||
console.error('视频出错了 window监听', errorEvent);
|
||
}
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
};
|
||
video.addEventListener('error', errorLister);
|
||
video.addEventListener('waiting', waitingListener);
|
||
video.addEventListener('playing', playingListener);
|
||
video.addEventListener('play', playLister);
|
||
video.addEventListener('pause', pauseListener);
|
||
video.addEventListener('ended', endedListner);
|
||
video.addEventListener('timeupdate', timeupdateListner);
|
||
window.addEventListener('error', windowErrorHandle);
|
||
|
||
videoRemoveListener.current = () => {
|
||
video.removeEventListener('error', errorLister);
|
||
video.removeEventListener('waiting', waitingListener);
|
||
video.removeEventListener('playing', playingListener);
|
||
video.removeEventListener('play', playLister);
|
||
video.removeEventListener('pause', pauseListener);
|
||
video.removeEventListener('ended', endedListner);
|
||
video.removeEventListener('timeupdate', timeupdateListner);
|
||
window.removeEventListener('error', windowErrorHandle);
|
||
};
|
||
|
||
videoInsRef?.current.on(FLV_EVENT.ERROR, (type: any, errDetail: any, info: any) => {
|
||
checkIsErr();
|
||
console.error('videoInsRef 错误', type, errDetail, info, video.currentTime);
|
||
});
|
||
let playPromise = videoInsRef?.current.play();
|
||
//先ready 遮挡会导致播放失败
|
||
setIsReady(true);
|
||
playPromise
|
||
.then(() => {
|
||
setIsReady(true);
|
||
})
|
||
.catch((...arg: any) => {
|
||
try {
|
||
} catch (error) {}
|
||
// setIsError(true);
|
||
console.error('playPromise视频出错了', arg);
|
||
});
|
||
}, []);
|
||
|
||
useUnmount(() => {
|
||
try {
|
||
videoRemoveListener.current();
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
});
|
||
|
||
const reload = async () => {
|
||
if (videoInsRef.current) {
|
||
let oldTime = videoInsRef.current.currentTime;
|
||
videoInsRef.current.currentTime = 0;
|
||
|
||
//如果修改时间不成功,则走重新加载的逻辑
|
||
if (oldTime === videoInsRef.current.currentTime) {
|
||
//重置状态
|
||
setIsReady(false);
|
||
setIsPlay(false);
|
||
setIsLoadingVideoWrapper(false);
|
||
setIsReady(false);
|
||
setIsEnd(false);
|
||
setIsVideoLoadFinish(false);
|
||
setPlayTime(0);
|
||
|
||
//清楚dom事件监听
|
||
try {
|
||
videoRemoveListener.current();
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
setPlaySeq((pre) => pre + 1);
|
||
return;
|
||
}
|
||
videoInsRef.current.play();
|
||
}
|
||
|
||
setPlayTime(0);
|
||
setIsEnd(false);
|
||
};
|
||
const seek = (v: number) => {
|
||
if (videoInsRef.current && isVideoLoadFinished) {
|
||
setPlayTime(parseFloat(v as any));
|
||
videoInsRef.current.currentTime = parseFloat(v as any);
|
||
} else {
|
||
message.warning('待视频加载完,才可操作进度条')
|
||
}
|
||
};
|
||
// ========================== 视频opt bar =========================
|
||
const [isFullscreen, { toggleFullscreen }] = useFullscreen(containerRef, {
|
||
pageFullscreen: true,
|
||
});
|
||
const showMaxDuration = !!maxDuration
|
||
? maxDuration
|
||
: toRealNumber(get(videoRef, 'current.duration', 0));
|
||
const showSlider = videoInsRef.current && isVideoLoadFinished;
|
||
const showStatus: any = getShowStatus(isDelayLoading, isEnd, isError);
|
||
|
||
// ========================== 截图 =========================
|
||
const corpContainerRef: any = useRef();
|
||
const cropInsRef: any = useRef(null);
|
||
const [showCrop, setShowCrop] = useState<boolean>(false);
|
||
|
||
//回显默认框选
|
||
const isFirstFlagRef = useRef<boolean>(true);
|
||
useEffect(() => {
|
||
const isFirst = isFirstFlagRef.current;
|
||
if (!isLoadingVideo && isReady && isFirst && defaultNormalizationRect && !showStatus) {
|
||
nextTick(() => {
|
||
setShowCrop(true);
|
||
});
|
||
}
|
||
}, [isLoadingVideo, showStatus]);
|
||
|
||
//定位按钮相关参数
|
||
const alginContainerRef: any = useRef(null);
|
||
const alignRef: any = useRef(null);
|
||
const [cropRect, setCropRect] = useState<Rect| null>(null);
|
||
|
||
useEffect(() => {
|
||
showCrop ? videoInsRef?.current?.pause() : videoInsRef?.current?.play();
|
||
}, [showCrop]);
|
||
|
||
useEffect(() => {
|
||
let handlerCropStart: { remove: () => void; };
|
||
let handlerCropEnd: { remove: () => void; };
|
||
setCropRect(null);
|
||
if (!isReady) return;
|
||
if (showCrop) {
|
||
handlerCropStart = addEventListenerWrapper(corpContainerRef.current, EVENT_CROP_START, () => {
|
||
setCropRect(null);
|
||
});
|
||
handlerCropEnd = addEventListenerWrapper(corpContainerRef.current, EVENT_CROP_END, (event: { detail: any; }) => {
|
||
const data = event.detail;
|
||
setCropRect({
|
||
x: data.left,
|
||
y: data.top,
|
||
w: data.width,
|
||
h: data.height,
|
||
});
|
||
alignRef?.current?.forceAlign?.();
|
||
});
|
||
let video: any = videoRef.current;
|
||
//计算 limitcroppbox
|
||
let scale = Math.min(
|
||
video.offsetWidth / video.videoWidth,
|
||
video.offsetHeight / video.videoHeight
|
||
);
|
||
let finalVideoWidth = video.videoWidth * scale;
|
||
let finalVideoHeight = video.videoHeight * scale;
|
||
let cropBoxLimited = {
|
||
width: finalVideoWidth,
|
||
height: finalVideoHeight,
|
||
top: (video.offsetHeight - finalVideoHeight) / 2,
|
||
left: (video.offsetWidth - finalVideoWidth) / 2,
|
||
};
|
||
//获取视频图片
|
||
let canvas = document.createElement('canvas');
|
||
canvas.width = video.offsetWidth;
|
||
canvas.height = video.offsetHeight;
|
||
canvas.style.display = 'none';
|
||
document.body.appendChild(canvas);
|
||
let ctx = canvas.getContext('2d');
|
||
ctx?.drawImage(
|
||
video,
|
||
(video.offsetWidth - finalVideoWidth) / 2,
|
||
(video.offsetHeight - finalVideoHeight) / 2,
|
||
finalVideoWidth,
|
||
finalVideoHeight
|
||
);
|
||
let imageData = canvas.toDataURL('image/png');
|
||
canvas.parentNode?.removeChild(canvas);
|
||
//回显编辑框
|
||
const isFirst = isFirstFlagRef.current;
|
||
let initialCropBoxData = null;
|
||
if (isFirst && defaultNormalizationRect) {
|
||
initialCropBoxData = {
|
||
left: defaultNormalizationRect.x * finalVideoWidth + cropBoxLimited.left,
|
||
top: defaultNormalizationRect.y * finalVideoHeight + cropBoxLimited.top,
|
||
width: defaultNormalizationRect.w * finalVideoWidth,
|
||
height: defaultNormalizationRect.h * finalVideoHeight,
|
||
};
|
||
}
|
||
isFirstFlagRef.current = false;
|
||
|
||
cropInsRef.current = new Cropper(corpContainerRef.current, {
|
||
showMask: true,
|
||
cropBoxLimited,
|
||
img: imageData,
|
||
initialCropBoxData,
|
||
});
|
||
}
|
||
return () => {
|
||
handlerCropStart?.remove();
|
||
handlerCropEnd?.remove();
|
||
cropInsRef?.current?.destroy?.();
|
||
cropInsRef.current = null;
|
||
};
|
||
}, [showCrop, isReady]);
|
||
|
||
const latestCropRect = useLatest(cropRect);
|
||
|
||
const getCropInfo = async () => {
|
||
const cropRect = latestCropRect.current as any;
|
||
let video: any = videoRef.current;
|
||
if (!video) return
|
||
let rectList = [];
|
||
let extendRectList = [];
|
||
let selectIndex = 0;
|
||
//获取视频图片的url
|
||
let scale = Math.min(
|
||
video.offsetWidth / video.videoWidth,
|
||
video.offsetHeight / video.videoHeight
|
||
);
|
||
let finalVideoWidth = video.videoWidth * scale;
|
||
let finalVideoHeight = video.videoHeight * scale;
|
||
let canvas = document.createElement('canvas');
|
||
canvas.width = finalVideoWidth;
|
||
canvas.height = finalVideoHeight;
|
||
canvas.style.display = 'none';
|
||
document.body.appendChild(canvas);
|
||
let ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||
ctx.drawImage(
|
||
video,
|
||
0,
|
||
0,
|
||
finalVideoWidth,
|
||
finalVideoHeight
|
||
);
|
||
let base64 = canvas.toDataURL('image/jpeg');
|
||
const blobData = dataURLToBlob(base64);
|
||
canvas.parentNode?.removeChild(canvas);
|
||
const file = new window.File([blobData], `${new Date().getTime()}`);
|
||
let newRect = {
|
||
w: cropRect.w / finalVideoWidth,
|
||
h: cropRect.h / finalVideoHeight,
|
||
x: (cropRect.x - (video.offsetWidth - finalVideoWidth) / 2) / finalVideoWidth,
|
||
y: (cropRect.y - (video.offsetHeight - finalVideoHeight) / 2) / finalVideoHeight
|
||
};
|
||
rectList.push(newRect);
|
||
extendRectList.push(newRect);
|
||
//扩展框获取imgkey
|
||
extendRectList.forEach(async (rect, index) => {
|
||
extendRectList[index] = {
|
||
...rect,
|
||
};
|
||
})
|
||
return {
|
||
rectList,
|
||
extendRectList,
|
||
selectIndex,
|
||
file
|
||
};
|
||
};
|
||
|
||
//回调
|
||
useEffect(() => {
|
||
//计算归一化crop rect
|
||
let normalizationRect = null;
|
||
if (showCrop && cropRect) {
|
||
let video: any = videoRef.current;
|
||
let scale = Math.min(
|
||
video.offsetWidth / video.videoWidth,
|
||
video.offsetHeight / video.videoHeight
|
||
);
|
||
let finalVideoWidth = video.videoWidth * scale;
|
||
let finalVideoHeight = video.videoHeight * scale;
|
||
let cropBoxLimited = {
|
||
width: finalVideoWidth,
|
||
height: finalVideoHeight,
|
||
top: (video.offsetHeight - finalVideoHeight) / 2,
|
||
left: (video.offsetWidth - finalVideoWidth) / 2,
|
||
};
|
||
normalizationRect = {
|
||
x: (cropRect.x - cropBoxLimited.left) / cropBoxLimited.width,
|
||
y: (cropRect.y - cropBoxLimited.top) / cropBoxLimited.height,
|
||
w: cropRect.w / cropBoxLimited.width,
|
||
h: cropRect.h / cropBoxLimited.height,
|
||
};
|
||
}
|
||
|
||
onCropChange?.(showCrop, normalizationRect);
|
||
}, [showCrop, cropRect]);
|
||
|
||
// ========================== 截帧 =========================
|
||
const downloadVideoframe = useCallback(async () => {
|
||
try {
|
||
videoInsRef?.current?.pause?.();
|
||
let video: any = videoRef.current;
|
||
var canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d')
|
||
let base64;
|
||
//当视频处于还未加载出来时,截屏为黑色图片
|
||
if (video.readyState === 0) {
|
||
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
||
canvas.width = video.offsetWidth;
|
||
canvas.height = video.offsetHeight;
|
||
// @ts-ignore
|
||
ctx.fillStyle = 'black';
|
||
ctx?.fillRect(0, 0, canvas.width, canvas.height);
|
||
base64 = canvas.toDataURL();
|
||
} else {
|
||
canvas.width = video.videoWidth;
|
||
canvas.height = video.videoHeight;
|
||
ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||
base64 = canvas.toDataURL('image/png');
|
||
}
|
||
download(base64);
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}, []);
|
||
|
||
// ============================== 暴露出去的方法 ===============================
|
||
const latestIsReady = useLatest(isReady);
|
||
const cropAble = !showStatus && isReady;
|
||
useImperativeHandle(ref, () => ({
|
||
cropAble,
|
||
setShowCrop: (dispatch) => {
|
||
const isReady = latestIsReady.current;
|
||
if (!isReady) return;
|
||
setShowCrop(dispatch);
|
||
},
|
||
downloadVideoframe,
|
||
}));
|
||
|
||
return (
|
||
<div className={classNames(`${componentName}`)} ref={containerRef}>
|
||
{url && (
|
||
<>
|
||
<FlvPlayer
|
||
playId={playSeq}
|
||
autoPlay={true}
|
||
className={classNames(`${componentName}-flv`)}
|
||
type={url.startsWith('http') ? 'mp4' : 'flv'}
|
||
url={url}
|
||
config={{
|
||
enableStashBuffer: true,
|
||
stashInitialSize: 1024 * 700,
|
||
isLive: true,
|
||
hasAudio: false,
|
||
hasVideo: true,
|
||
}}
|
||
onCreat={initPlayer}
|
||
/>
|
||
{/* //截图 */}
|
||
<div
|
||
className={classNames(`${componentName}-crop-container`)}
|
||
ref={corpContainerRef}
|
||
style={{
|
||
display: isFullscreen ? 'none' : 'block',
|
||
}}
|
||
>
|
||
{/* <div ref={corpRef}></div> */}
|
||
</div>
|
||
{showCrop && cropRect && (
|
||
<>
|
||
<div
|
||
ref={alginContainerRef}
|
||
className={classNames(`${componentName}-align`)}
|
||
style={Object.assign(
|
||
{
|
||
width: cropRect.w,
|
||
height: cropRect.h,
|
||
},
|
||
getTransforms({
|
||
translateX: cropRect.x,
|
||
translateY: cropRect.y,
|
||
})
|
||
)}
|
||
></div>
|
||
<Align
|
||
ref={alignRef}
|
||
monitorWindowResize
|
||
align={screenshotButtonAlign}
|
||
target={function () {
|
||
return alginContainerRef.current;
|
||
}}
|
||
>
|
||
{screenshotButtonRender({
|
||
model: 'IMAGE',
|
||
getCropInfo,
|
||
setShowCrop,
|
||
cropType: CROP_TYPE['CUSTOM'],
|
||
})}
|
||
</Align>
|
||
</>
|
||
)}
|
||
{/* 视频进度条 */}
|
||
{!showCrop && (
|
||
<div className={`${componentName}-opt`}>
|
||
<div>
|
||
<Button
|
||
type="text"
|
||
onClick={() => {
|
||
if (!isPlay) {
|
||
//播放中暂停
|
||
videoInsRef?.current?.play();
|
||
setShowCrop(false);
|
||
} else {
|
||
videoInsRef?.current?.pause();
|
||
}
|
||
}}
|
||
>
|
||
<Icon
|
||
styles={{
|
||
color: '#fff',
|
||
display: 'flex',
|
||
}}
|
||
color="#1890ff"
|
||
icon={!isPlay ? 'icon-shipinbofang' : 'icon-shipinzanting'}
|
||
/>
|
||
</Button>
|
||
</div>
|
||
<div
|
||
className={`${componentName}-opt-range`}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
}}
|
||
>
|
||
<Range
|
||
value={playTime}
|
||
min={0}
|
||
max={showMaxDuration}
|
||
showSlider={showSlider}
|
||
onChange={seek}
|
||
/>
|
||
<div>
|
||
{/* TODO: 删除扩展方法format */}
|
||
{formatDurationTime(playTime)}/{formatDurationTime(showMaxDuration)}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Button
|
||
type="text"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
toggleFullscreen();
|
||
}}
|
||
>
|
||
<Icon
|
||
styles={{
|
||
color: '#fff',
|
||
display: 'flex',
|
||
}}
|
||
size={18}
|
||
icon={isFullscreen ? 'icon-cancle_fullscreen' : 'icon-fullscreen'}
|
||
/>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{/* mask */}
|
||
{!!showStatus && (
|
||
<Loading status={showStatus} reload={() => reload()} />
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
});
|
||
|
||
export default VideoPlayer;
|