diff --git a/src/appComponent.tsx b/src/appComponent.tsx index cab5007..8d37f9c 100644 --- a/src/appComponent.tsx +++ b/src/appComponent.tsx @@ -79,6 +79,7 @@ const AppComponent = memo(function AppComponent(props) { width: 750, height: 420, theme: '#00D3FF', + isShowMultiple: false, poster: 'https://cdn.gudsen.com/2021/06/28/f81356b08b4842d7a3719499f557c8e4.JPG', }} onProgressMouseDown={onProgressMouseDown} diff --git a/src/components/screenshot/index.module.scss b/src/components/screenshot/index.module.scss new file mode 100644 index 0000000..3e64bdb --- /dev/null +++ b/src/components/screenshot/index.module.scss @@ -0,0 +1,36 @@ +@import 'src/assets/css/mixin.scss'; +.screenshot { + width: 280px; + @include position(absolute, auto, 0, 55px, auto); + background: #fff; + padding: 3px; + overflow: hidden; + .img { + height: 155px; + overflow: hidden; + position: relative; + canvas { + @include position(absolute, 50%, auto, auto, 50%); + transform: translate(-50%, -50%); + } + } + .close { + @include position(absolute, -24px, -24px, auto, auto); + @include wh(0, 0); + z-index: 1; + border: 26px solid transparent; + border-left-color: #fff; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + border-radius: 100%; + .icon { + @include position(absolute, -8px, 5px, auto, auto); + transform: rotate(45deg); + cursor: pointer; + } + } + .save { + padding: 10px; + text-align: center; + } +} diff --git a/src/components/screenshot/index.tsx b/src/components/screenshot/index.tsx new file mode 100644 index 0000000..a60b1b6 --- /dev/null +++ b/src/components/screenshot/index.tsx @@ -0,0 +1,23 @@ +import React, { memo, FC } from 'react'; +import style from './index.module.scss'; +import Broadcast from '@/components/svgIcon'; +const Index: FC<{ + setIsscreenshot: Function; + screenshotLoading: boolean; +}> = memo(function Index({ setIsscreenshot, screenshotLoading }) { + return ( +
+
setIsscreenshot(false)}> + +
+
+ +
+

+ {screenshotLoading ? '截图加载失败,在点击下' : '鼠标右键图片另存为'} +

+
+ ); +}); + +export default Index; diff --git a/src/core/controller/index.tsx b/src/core/controller/index.tsx index 3dd64eb..1562b62 100644 --- a/src/core/controller/index.tsx +++ b/src/core/controller/index.tsx @@ -1,17 +1,14 @@ -import React, { memo, useContext, useRef, useState, useMemo } from 'react'; +import React, { memo, useContext, useRef, useMemo, useEffect, useState } from 'react'; import Broadcast from '@/components/svgIcon'; import Progress from '../progress'; import Controls from '../controls'; import { FlowContext } from '@/core/context'; import { useVideo } from '@/core/useVideo'; -import useWindowClient from '@/utils/useWindowClient'; -import usePrevious from '@/utils/usePrevious'; import EndComponent from '@/components/end'; +import Screenshot from '@/components/screenshot'; import './index.scss'; -const Index = memo(function Index(props) { - const { clientX } = useWindowClient(); - +const Index = memo(function Index() { const reviceProps = useContext(FlowContext); const { dispatch, propsAttributes } = reviceProps; @@ -25,57 +22,72 @@ const Index = memo(function Index(props) { const timer = useRef(null!); - const viewClientX = useRef(null!); - - const prevCalculation = useRef(null!); - const controllerRef = useRef(null!); - const [pre, setPre] = useState(0); - - viewClientX.current = clientX; - /** - * @description 返回上一次的记录值 + * @description 检查器,检查鼠标是否移动 */ - prevCalculation.current = usePrevious(pre) as number; + const userActivity = useRef(false); + const inactivityTimeout = useRef(null!); /** - * @description 显示操作控件 + * @description 是否在控制器上 */ - const showControl = (e: any, status: string) => { - dispatch!({ type: 'isControl', data: status === 'enter' && !isEndEd ? true : false }); - }; - /** - * @description 隐藏鼠标 - */ - const hiddleCursor = () => { - if (timer.current) { - clearInterval(timer.current); - } + const isControlsContainerMove = useRef(false); + + const [isScreenshot, setIsscreenshot] = useState(false); + + const [screenshotLoading, setScreenshotLoading] = useState(false); + + useEffect(() => { timer.current = setInterval(() => { - setPre(viewClientX.current); - /** - * @description 如果在1200ms没有任何操作的话,就隐藏控件和鼠标 - */ - if (viewClientX.current! !== prevCalculation.current) { + if (userActivity.current) { + /** + * @description 重置 + */ + userActivity.current = false; dispatch!({ type: 'isControl', data: true }); controllerRef.current.style.cursor = 'pointer'; - } else { - dispatch!({ type: 'isControl', data: false }); - controllerRef.current.style.cursor = 'none'; + inactivityTimeout.current && clearTimeout(inactivityTimeout.current); + inactivityTimeout.current = setTimeout( + () => { + /** + * @note 当鼠标移动在控制器Controls上时,这个时候,鼠标不能隐藏 + */ + if (!userActivity.current && !isControlsContainerMove.current) { + dispatch!({ type: 'isControl', data: false }); + controllerRef.current.style.cursor = 'none'; + } + }, + propsAttributes!.hideMouseTime ? propsAttributes!.hideMouseTime : 2000, + ); } - }, 1200); - }; - const clearTimer = () => { - controllerRef.current.style.cursor = 'pointer'; - if (timer.current) { - clearInterval(timer.current); - } + }, 200); + return () => { + timer.current && clearInterval(timer.current); + }; + }, []); + + /** + * @description 显示操作控件 + */ + const showControl = (status: string) => { + dispatch!({ type: 'isControl', data: status === 'enter' && !isEndEd ? true : false }); }; const handlePlay = () => { handleChangePlayState && handleChangePlayState(); }; + const mouseMove: React.MouseEventHandler = (e) => { + userActivity.current = true; + /** + * @note 这个时候要将控制器区设置为false,此时鼠标在播放器,如果不设置为false,会造成移出不能隐藏控制器的bug + */ + isControlsContainerMove.current = false; + }; + const leaveMove: React.MouseEventHandler = (e) => { + userActivity.current = false; + controllerRef.current.style.cursor = 'pointer'; + }; /** * @description 暂停键的位置 */ @@ -93,18 +105,25 @@ const Index = memo(function Index(props) { }; } }, [propsAttributes!.pausePlacement]); + /** + * @param status 鼠标状态 + * @description 控制器容器鼠标操作 + */ + const controlsContainerMove = (status: string) => { + isControlsContainerMove.current = status === 'move' ? true : false; + }; return (
showControl(e, 'enter')} - onMouseLeave={(e) => showControl(e, 'leave')} + onMouseEnter={(e) => [showControl('enter')]} + onMouseLeave={(e) => [showControl('leave'), e.stopPropagation()]} ref={controllerRef} >
{!isPlay && !isEndEd && ( @@ -116,15 +135,22 @@ const Index = memo(function Index(props) { style={pausePosition} /> )} -
+
[controlsContainerMove('move')]} + onMouseLeave={(e) => [controlsContainerMove('leave')]} + > + {isScreenshot ? ( + + ) : null} - +
{isEndEd ? ( propsAttributes!.setEndPlayContent ? ( propsAttributes!.setEndPlayContent ) : ( - [handleChangePlayState(), hiddleCursor()]} /> + [handleChangePlayState(), showControl('enter')]} /> ) ) : null}
diff --git a/src/core/controls/index.tsx b/src/core/controls/index.tsx index e10b605..b0ab998 100644 --- a/src/core/controls/index.tsx +++ b/src/core/controls/index.tsx @@ -1,9 +1,9 @@ -import React, { memo, useContext, useRef, useEffect, useMemo, useCallback } from 'react'; +import React, { memo, useContext, useRef, useEffect, useMemo, FC } from 'react'; import Broadcast from '@/components/svgIcon'; import Tooltip from '@/components/tooltip'; -import { FlowContext } from '@/core/context'; +import { contextType, FlowContext } from '@/core/context'; import { useVideo } from '@/core/useVideo'; -import { secondsToMinutesAndSecondes, createALabel } from '@/utils'; +import { secondsToMinutesAndSecondes, capture } from '@/utils'; import { useControls } from './variable'; import useWindowClient from '@/utils/useWindowClient'; import screenfull, { Screenfull } from 'screenfull'; @@ -14,286 +14,295 @@ import VolumeComponent from './volume'; import MonitorComponent from './monitor'; import './index.scss'; -const Index = memo(function Index(props) { - /** - * @description 音量滑动元素 - */ - const volumeSliderMirror = useRef(null!); +const Index: FC<{ setIsscreenshot: Function; setScreenshotLoading: Function }> = memo( + function Index({ setIsscreenshot, setScreenshotLoading }) { + /** + * @description 音量滑动元素 + */ + const volumeSliderMirror = useRef(null!); - const clientYdistance = useRef(0); + const clientYdistance = useRef(0); - const volumeInterval = useRef(null); + const volumeInterval = useRef(null); - const reviceProps = useContext(FlowContext); + const reviceProps = useContext(FlowContext); - const revicePropsData = useRef(); + const { propsAttributes } = reviceProps!; - const { isPlay, handleChangePlayState, currentTime, duration, isPictureinpicture, volume } = - useVideo( - { - videoElement: reviceProps.videoRef, - }, - [reviceProps.videoRef], - ); + const revicePropsData = useRef(); - const { controlsState, dispatch } = useControls(); + const { isPlay, handleChangePlayState, currentTime, duration, isPictureinpicture, volume } = + useVideo( + { + videoElement: reviceProps.videoRef, + }, + [reviceProps.videoRef], + ); - const { clientY } = useWindowClient(); + const { controlsState, dispatch } = useControls(); - clientYdistance.current = clientY; + const { clientY } = useWindowClient(); - revicePropsData.current = reviceProps; + clientYdistance.current = clientY; - useEffect(() => { - /** - * @description 如果调用了setVolume函数,这边的数据就要保持和video的数据一致 - */ - dispatch({ type: 'volume', data: Math.floor(volume * 100) }); - }, [volume]); + revicePropsData.current = reviceProps; - useEffect(() => { - // 为了防止音量滑动元素计时器不暂停 - window.addEventListener('mouseup', whenMouseUpDo); - return () => { - window.removeEventListener('mouseup', whenMouseUpDo); - }; - }, []); + useEffect(() => { + /** + * @description 如果调用了setVolume函数,这边的数据就要保持和video的数据一致 + */ + dispatch({ type: 'volume', data: Math.floor(volume * 100) }); + }, [volume]); - /** - * @description 更新当前音量控制条 - */ - const updateCurrentVolume = (volumePercent: number) => { - const videoRef = reviceProps.videoRef!; - if (volumePercent >= 0 && volumePercent <= 1) { - videoRef.volume = volumePercent; - videoRef.muted = false; - dispatch({ type: 'volume', data: volumePercent * 100 }); - dispatch({ type: 'isMuted', data: Math.floor(volumePercent * 100) === 0 ? true : false }); - } - if (volumePercent < 0) { - videoRef.volume = 0; - videoRef.muted = true; - dispatch({ type: 'volume', data: 0 }); - dispatch({ type: 'isMuted', data: true }); - } - if (volumePercent > 1) { - videoRef.volume = 1; - dispatch({ type: 'volume', data: 100 }); - } - }; - /** - * - * @param e event对象 - * @description 音量滑动元素点击 - */ - const changeCurrentVolume: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const volumeSliderMirrorElement = ( - volumeSliderMirror.current as HTMLDivElement & { element: HTMLDivElement } - ).element; - // 获取音量区域高度 - const volumeAreaHeight = volumeSliderMirrorElement.offsetHeight; - // 获取当前位置在整个音量区域高度的占比 - const volumePercent = - 1 - (e.clientY - volumeSliderMirrorElement.getBoundingClientRect().top) / volumeAreaHeight; - // 修改当前音量大小 - updateCurrentVolume(volumePercent); - }; + useEffect(() => { + // 为了防止音量滑动元素计时器不暂停 + window.addEventListener('mouseup', whenMouseUpDo); + return () => { + window.removeEventListener('mouseup', whenMouseUpDo); + }; + }, []); - const slideCurrentVolume: React.MouseEventHandler = (e) => { - e.stopPropagation(); - const volumeSliderMirrorElement = ( - volumeSliderMirror.current as HTMLDivElement & { element: HTMLDivElement } - ).element; - // 获取音量区域高度 - const volumeAreaHeight = volumeSliderMirrorElement.offsetHeight; - // 防止点击的时候,再次出发计时器,重而造成点击卡顿 - volumeInterval.current && clearInterval(volumeInterval.current); - volumeInterval.current = setInterval(() => { + /** + * @description 更新当前音量控制条 + */ + const updateCurrentVolume = (volumePercent: number) => { + const videoRef = reviceProps.videoRef!; + if (volumePercent >= 0 && volumePercent <= 1) { + videoRef.volume = volumePercent; + videoRef.muted = false; + dispatch({ type: 'volume', data: volumePercent * 100 }); + dispatch({ type: 'isMuted', data: Math.floor(volumePercent * 100) === 0 ? true : false }); + } + if (volumePercent < 0) { + videoRef.volume = 0; + videoRef.muted = true; + dispatch({ type: 'volume', data: 0 }); + dispatch({ type: 'isMuted', data: true }); + } + if (volumePercent > 1) { + videoRef.volume = 1; + dispatch({ type: 'volume', data: 100 }); + } + }; + /** + * + * @param e event对象 + * @description 音量滑动元素点击 + */ + const changeCurrentVolume: React.MouseEventHandler = (e) => { + e.stopPropagation(); + const volumeSliderMirrorElement = ( + volumeSliderMirror.current as HTMLDivElement & { element: HTMLDivElement } + ).element; + // 获取音量区域高度 + const volumeAreaHeight = volumeSliderMirrorElement.offsetHeight; // 获取当前位置在整个音量区域高度的占比 const volumePercent = - 1 - - (clientYdistance.current - volumeSliderMirrorElement.getBoundingClientRect().top) / - volumeAreaHeight; + 1 - (e.clientY - volumeSliderMirrorElement.getBoundingClientRect().top) / volumeAreaHeight; // 修改当前音量大小 updateCurrentVolume(volumePercent); - dispatch({ type: 'isSlideVolume', data: true }); - }, 1); - }; - // 当鼠标抬起时 - const whenMouseUpDo = () => { - volumeInterval.current && clearInterval(volumeInterval.current); - dispatch({ type: 'isSlideVolume', data: false }); - }; - const clearVolumeInterval: React.MouseEventHandler = (e) => { - e.stopPropagation(); - whenMouseUpDo(); - }; - /** - * @description 开启全屏 - */ - const requestFullScreen = () => { - if (screenfull.isEnabled) { - screenfull.toggle(reviceProps.videoContainerRef!); - screenfull.on('change', () => - dispatch({ type: 'isScreentFull', data: (screenfull as Screenfull).isFullscreen }), - ); - } - }; - /** - * @description 开启画中画 - */ - const pictureInPicture = () => { - if (isPictureinpicture) { - (document as any).exitPictureInPicture(); - } else { - (reviceProps.videoRef! as any).requestPictureInPicture(); - } - }; - /** - * @description 设置视频倍数 - */ - const selectPlayRate = (playbackRate: number) => { - reviceProps.videoRef!.playbackRate = playbackRate; - dispatch({ type: 'multiple', data: playbackRate }); - }; - const multipleText = useMemo(() => { - if (controlsState.multiple === 1.0) { - return '倍数'; - } else { - return multipleList.filter((item) => item.id === controlsState.multiple)[0].name; - } - }, [controlsState.multiple]); - /** - * @description 在线截图 - */ - const screenshot = () => { - const canvas = document.createElement('canvas') as HTMLCanvasElement; - canvas.width = reviceProps.videoRef!.offsetWidth; - canvas.height = reviceProps.videoRef!.offsetHeight; - const context = canvas.getContext('2d')!; - context.drawImage(reviceProps.videoRef!, 0, 0, canvas.width, canvas.height); - try { - createALabel(canvas.toDataURL('image/png')); - } catch (error) {} - }; - /** - * @description 设置 - */ - const switchChange = (e: string, flag: string) => { - const { videoRef, lightOffMaskRef } = revicePropsData.current; - if (flag === 'lights') { - if (lightOffMaskRef) { - lightOffMaskRef.style.display = e === 'yes' ? 'block' : 'none'; + }; + + const slideCurrentVolume: React.MouseEventHandler = (e) => { + e.stopPropagation(); + const volumeSliderMirrorElement = ( + volumeSliderMirror.current as HTMLDivElement & { element: HTMLDivElement } + ).element; + // 获取音量区域高度 + const volumeAreaHeight = volumeSliderMirrorElement.offsetHeight; + // 防止点击的时候,再次出发计时器,重而造成点击卡顿 + volumeInterval.current && clearInterval(volumeInterval.current); + volumeInterval.current = setInterval(() => { + // 获取当前位置在整个音量区域高度的占比 + const volumePercent = + 1 - + (clientYdistance.current - volumeSliderMirrorElement.getBoundingClientRect().top) / + volumeAreaHeight; + // 修改当前音量大小 + updateCurrentVolume(volumePercent); + dispatch({ type: 'isSlideVolume', data: true }); + }, 1); + }; + // 当鼠标抬起时 + const whenMouseUpDo = () => { + volumeInterval.current && clearInterval(volumeInterval.current); + dispatch({ type: 'isSlideVolume', data: false }); + }; + const clearVolumeInterval: React.MouseEventHandler = (e) => { + e.stopPropagation(); + whenMouseUpDo(); + }; + /** + * @description 开启全屏 + */ + const requestFullScreen = () => { + if (screenfull.isEnabled) { + screenfull.toggle(reviceProps.videoContainerRef!); + screenfull.on('change', () => + dispatch({ type: 'isScreentFull', data: (screenfull as Screenfull).isFullscreen }), + ); } - } else { - const loop = videoRef.loop; - videoRef.loop = loop ? false : true; - } - }; - /** - * @description 网页全屏 - */ - const clientFullScreen = () => { - const videoContainerRef = reviceProps.videoContainerRef!; - if (videoContainerRef.classList.contains('clientFullScreen')) { - videoContainerRef.classList.remove('clientFullScreen'); - dispatch({ type: 'isWebPageFullScreen', data: false }); - } else { - videoContainerRef.classList.add('clientFullScreen'); - dispatch({ type: 'isWebPageFullScreen', data: true }); - } - }; - /** - * @description 静音键和非静音键的切换 - */ - const toggleVolume = () => { - reviceProps.videoRef!.volume = controlsState.isMuted ? defaultVolume / 100 : 0; - dispatch({ type: 'isMuted', data: controlsState.isMuted ? false : true }); - reviceProps.videoRef!.muted = controlsState.isMuted ? false : true; - }; + }; + /** + * @description 开启画中画 + */ + const pictureInPicture = () => { + if (isPictureinpicture) { + (document as any).exitPictureInPicture(); + } else { + (reviceProps.videoRef! as any).requestPictureInPicture(); + } + }; + /** + * @description 设置视频倍数 + */ + const selectPlayRate = (playbackRate: number) => { + reviceProps.videoRef!.playbackRate = playbackRate; + dispatch({ type: 'multiple', data: playbackRate }); + }; + const multipleText = useMemo(() => { + if (controlsState.multiple === 1.0) { + return '倍数'; + } else { + return multipleList.filter((item) => item.id === controlsState.multiple)[0].name; + } + }, [controlsState.multiple]); + /** + * @description 在线截图 + */ + const screenshot = async () => { + const output = document.querySelector('#JoL-screenshotCanvas')!; + const canvas = capture(reviceProps.videoRef!, 0.45); + setIsscreenshot(true); + if (output) { + setScreenshotLoading(false); + output.innerHTML = ''; + output.appendChild(canvas); + } else { + setScreenshotLoading(true); + } + }; + /** + * @description 设置 + */ + const switchChange = (e: string, flag: string) => { + const { videoRef, lightOffMaskRef } = revicePropsData.current!; + if (flag === 'lights') { + if (lightOffMaskRef) { + lightOffMaskRef.style.display = e === 'yes' ? 'block' : 'none'; + } + } else { + const loop = videoRef!.loop; + videoRef!.loop = loop ? false : true; + } + }; + /** + * @description 网页全屏 + */ + const clientFullScreen = () => { + const videoContainerRef = reviceProps.videoContainerRef!; + if (videoContainerRef.classList.contains('clientFullScreen')) { + videoContainerRef.classList.remove('clientFullScreen'); + dispatch({ type: 'isWebPageFullScreen', data: false }); + } else { + videoContainerRef.classList.add('clientFullScreen'); + dispatch({ type: 'isWebPageFullScreen', data: true }); + } + }; + /** + * @description 静音键和非静音键的切换 + */ + const toggleVolume = () => { + reviceProps.videoRef!.volume = controlsState.isMuted ? defaultVolume / 100 : 0; + dispatch({ type: 'isMuted', data: controlsState.isMuted ? false : true }); + reviceProps.videoRef!.muted = controlsState.isMuted ? false : true; + }; - return ( -
- -
- - - - - } + return ( +
+ - + {propsAttributes!.isShowMultiple && ( + - } - /> - - } - /> - - } - /> + )} + + + + + } + /> + + } + /> + + } + /> + + } + /> +
-
- ); -}); + ); + }, +); export default Index; diff --git a/src/core/index.tsx b/src/core/index.tsx index f107d21..d0ec005 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -175,5 +175,12 @@ const JoLPlayer = function JoLPlayer(props: videoparameter, ref: React.Ref(JoLPlayer); - +JoLPlayerComponent.defaultProps = { + option: { + width: 750, + height: 420, + videoSrc: 'https://cdn.gudsen.com/2021/06/28/f81356b08b4842d7a3719499f557c8e4.JPG', + isShowMultiple: true, + }, +}; export default JoLPlayerComponent; diff --git a/src/core/progress/index.tsx b/src/core/progress/index.tsx index ccc7830..45eba67 100644 --- a/src/core/progress/index.tsx +++ b/src/core/progress/index.tsx @@ -172,7 +172,7 @@ const Index = memo(function Index(props) { return (
diff --git a/src/icons/svg/close.svg b/src/icons/svg/close.svg new file mode 100644 index 0000000..314e1c4 --- /dev/null +++ b/src/icons/svg/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/interface/index.ts b/src/interface/index.ts index fd22738..ede0ef8 100644 --- a/src/interface/index.ts +++ b/src/interface/index.ts @@ -40,6 +40,30 @@ export interface videoOption { * @description 暂停键的位置 */ pausePlacement?: pausePlacement; + /** + * @description 多少毫秒,无任何操作,隐藏鼠标和控制器/ms + */ + hideMouseTime?: U; + /** + * @description 是否显示播放倍数功能 + */ + isShowMultiple?: K; + /** + * @description 是否显示设置功能 + */ + isShowSet?: K; + /** + * @description 是否显示截图功能 + */ + isShowScreenshot?: K; + /** + * @description 是否显示画中画 + */ + isShowPicture?: K; + /** + * @description 是否显示网页全屏 + */ + isShowWebFullScreen?: K; } export interface videoAttributes { /** diff --git a/src/utils/index.ts b/src/utils/index.ts index 885aa95..121d29f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -37,3 +37,14 @@ export const createALabel = (path: string, fileName: string = 'JoL-player.png') link.remove(); window.URL.revokeObjectURL(path); }; + +export const capture = (video: HTMLVideoElement, scaleFactor: number = 0.25) => { + var w = video.videoWidth * scaleFactor; + var h = video.videoHeight * scaleFactor; + var canvas = document.createElement('canvas') as HTMLCanvasElement; + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + ctx!.drawImage(video, 0, 0, w, h); + return canvas; +};