React如何實(shí)現(xiàn)視頻旋轉(zhuǎn)縮放
一、背景
原本我們預(yù)覽視頻僅僅是簡(jiǎn)單的video標(biāo)簽實(shí)現(xiàn)就可以滿足業(yè)務(wù)的需求了,但是某一天,產(chǎn)品說(shuō):”業(yè)務(wù)的視頻是用手機(jī)拍的,方向不一定是正的,還有視頻的寬高太小了看不清,所以希望我們能讓視頻做到旋轉(zhuǎn)跳躍(不是)旋轉(zhuǎn)+按比例縮小放大“。不過(guò)吐槽歸吐槽,既然需求合理該做還是得做啊。
二、實(shí)現(xiàn)
這個(gè)功能初時(shí)看起來(lái)很頭疼,在細(xì)細(xì)思考下來(lái)后發(fā)現(xiàn),實(shí)現(xiàn)思路其實(shí)并不復(fù)雜,可以通過(guò)transform去輾轉(zhuǎn)騰挪,最終完成我們想要的效果。
1. 原始效果
我們最初始的效果就是如同圖片預(yù)覽一般,點(diǎn)擊彈窗播放視頻,所以這里僅僅使用了video標(biāo)簽。
2. 還原原生video控制欄功能
如果我們直接去旋轉(zhuǎn)video標(biāo)簽,那么他的控制欄也會(huì)跟著旋轉(zhuǎn),這并不符合我們的期望,所以我們需要在video標(biāo)簽外加一層容器,然后自定義控制欄和viode同級(jí),做到只旋轉(zhuǎn)video標(biāo)簽。
但是這里我嫌自定義控制欄太麻煩,于是這里選擇了一個(gè)自帶UI的三方視頻組件進(jìn)行改造。這里采用的是vime
,有興趣了解的可以去vimejs.com/。最終是展示這個(gè)樣子
3. 旋轉(zhuǎn)
以上前置工作做完之后,接下來(lái)就進(jìn)入正題,添加我們的旋轉(zhuǎn)功能。利用transform
對(duì)video進(jìn)行旋轉(zhuǎn),同時(shí)寬高數(shù)值對(duì)換。由于vime中video標(biāo)簽設(shè)置了position: absolute
,所以這里我們還要調(diào)整初始的top
跟left
import { Player, Video, DefaultUi, Settings, MenuItem } from '@vime/react'; // 初始寬度 const INIT_WIDTH = 370; // 初始高度 const INIT_HEIGHT = 658; const Demo = ({ src }) => { const [visible, setVisible] = useState(false); // vime組件寬高比例 const [aspectRatio, setAspectRatio] = useState('9:16'); // 容器寬度,vime組件會(huì)根據(jù)容器寬高自適應(yīng) const [width, setWidth] = useState<string | number>(INIT_WIDTH); // 旋轉(zhuǎn)角度 const degRef = useRef(0); useEffect(() => { if (src) { setVisible(true); } else { setVisible(false); } }, [src]); const closeVideoPreview = () => { setVisible(false); }; const onRotate = () => { const videoEl: HTMLVideoElement | null = document.querySelector('.sc-vm-file'); if (!videoEl) return; const vWidth = INIT_WIDTH; const vHeight = INIT_HEIGHT; const deg = degRef.current < 270 ? degRef.current + 90 : 0; // 旋轉(zhuǎn)后樣式 let newRatio = '16:9'; let newWidth = vHeight; let resWidth = `${vWidth}px`; let resHeight = `${vHeight}px`; let top = (vWidth - vHeight) / 2; let left = (vHeight - vWidth) / 2; // 水平角度復(fù)原處理 if (deg === 0 || deg === 180) { newWidth = INIT_WIDTH; newRatio = '9:16'; resWidth = '100%'; resHeight = '100%'; top = 0; left = 0; } videoEl.style.width = resWidth; videoEl.style.height = resHeight; videoEl.style.transform = `rotate(${deg}deg)`; videoEl.style.top = `${top}px`; videoEl.style.left = `${left}px`; setWidth(newWidth); setAspectRatio(newRatio); }; return ( <> {visible ? ( <div className='video-preview'> <div className='video-preview-mask' onClick={() => closeVideoPreview()} /> <div className="video-preview-content" onClick={() => closeVideoPreview()}> <div className="video-preview-box" style={{ width }} onClick={(event) => { event.stopPropagation(); }} > <Player icons="custom" aspectRatio={aspectRatio} > <Video> <source data-src={src} /> </Video> <DefaultUi noControls> // 。。。 <Settings active={openMenu}> <MenuItem label="旋轉(zhuǎn)" onClick={onRotate} /> </Settings> </DefaultUi> </Player> </div> </div> </div> ) : null} </> ); };
此時(shí)就能實(shí)現(xiàn)以下效果:
4. 全屏
上面雖然已經(jīng)實(shí)現(xiàn)了旋轉(zhuǎn)效果,但是并沒(méi)有結(jié)束,當(dāng)我們點(diǎn)擊全屏功能之后,此時(shí)旋轉(zhuǎn)的樣式,就變得奇怪了
這是由于我們先前寫(xiě)死了寬高數(shù)值,導(dǎo)致旋轉(zhuǎn)后video寬高沒(méi)有適應(yīng)全屏,將全屏的寬高設(shè)置為100vh跟100vw即可兼容全屏狀態(tài)下的旋轉(zhuǎn)。
const player = useRef<HTMLVmPlayerElement>(null); const changeStyle = (deg: number) => { const videoEl: HTMLVideoElement | null = document.querySelector('.sc-vm-file'); if (!videoEl) return; // 獲取窗口的寬度 const screenWidth = window.innerWidth; // 獲取整個(gè)屏幕的高度 const screenFullHeight = screen.height; const vWidth = INIT_WIDTH; const vHeight = INIT_HEIGHT; // 旋轉(zhuǎn)后樣式 // ... // 當(dāng)全屏旋轉(zhuǎn)90/270度時(shí),寬高處理 if (player.current?.isFullscreenActive) { newWidth = 'auto'; resWidth = '100vh'; resHeight = '100vw'; top = (screenFullHeight - screenWidth) / 2; left = (screenWidth - screenFullHeight) / 2; } // ... }; // 旋轉(zhuǎn) const onRotate = () => { setOpenMenu(false); const newDeg = degRef.current < 270 ? degRef.current + 90 : 0; degRef.current = newDeg; changeStyle(newDeg); }; // 全屏 const onVmFullscreenChange = () => { if (degRef.current === 90 || degRef.current === 270) { changeStyle(degRef.current); } }; return ( ... <Player ref={player} aspectRatio={aspectRatio} onVmFullscreenChange={onVmFullscreenChange} > ... </Player> )
全屏+旋轉(zhuǎn) 效果展示
5. 比例縮放
走到這里,旋轉(zhuǎn)功能原本已經(jīng)完成,但是不要忘記還有一個(gè)縮小放大功能,從全屏旋轉(zhuǎn)的例子來(lái)看,縮放功能也會(huì)影響到旋轉(zhuǎn)效果,還要對(duì)旋轉(zhuǎn)再做兼容處理。
我們先實(shí)現(xiàn)縮放功能:
// 比例 const [scale, setScale] = useState('1'); // 比例縮放 const changeScale = (event: Event) => { const radio = event.target as HTMLVmMenuRadioElement; const scaleVal = Number(radio.value); setScale(radio.value); setOpenMenu(false); const videoEl: HTMLVideoElement | null = document.querySelector('.sc-vm-file'); if (!videoEl) return; // 寬高 * 選中比例 const vWidth = INIT_WIDTH * scaleVal; const vHeight = INIT_HEIGHT * scaleVal; // 視頻設(shè)置新寬高 videoEl.style.width = `${vWidth}px`; videoEl.style.height = `${vHeight}px`; setWidth(vWidth); }; return ( ... <Submenu label="縮放比例" hint={scale}> <MenuRadioGroup value={scale} onVmCheck={changeScale}> <MenuRadio label="1" value="1" /> <MenuRadio label="1.2" value="1.2" /> <MenuRadio label="1.4" value="1.4" /> <MenuRadio label="1.6" value="1.6" /> <MenuRadio label="1.8" value="1.8" /> <MenuRadio label="2" value="2" /> </MenuRadioGroup> </Submenu> ... )
我們?cè)诳s放時(shí)改變了寬高,但是旋轉(zhuǎn)使用的依然是初始寬高,那么在縮放之后旋轉(zhuǎn),會(huì)變回初始大小,而旋轉(zhuǎn)后再縮放,則會(huì)導(dǎo)致樣式錯(cuò)誤。所以,我們?cè)诳s放和旋轉(zhuǎn)方法中需要拿到上一次變換過(guò)的樣式,再進(jìn)行新的變換。
const scaleRef = useRef(1); const changeStyle = (deg: number) => { const videoEl: HTMLVideoElement | null = document.querySelector('.sc-vm-file'); if (!videoEl) return; // 獲取窗口的寬度 const screenWidth = window.innerWidth; // 獲取整個(gè)屏幕的高度 const screenFullHeight = screen.height; const vWidth = INIT_WIDTH * scaleRef.current; const vHeight = INIT_HEIGHT * scaleRef.current; // 旋轉(zhuǎn)后樣式 // ... // 水平角度處理 if (deg === 0 || deg === 180) { newWidth = INIT_WIDTH * scaleRef.current; // ... } // ... }; // 比例縮放 const changeScale = (event: Event) => { const radio = event.target as HTMLVmMenuRadioElement; scaleRef.current = Number(radio.value); setScale(radio.value); changeStyle(degRef.current); };
最終效果展示
三、總結(jié)
實(shí)現(xiàn)視頻的旋轉(zhuǎn)縮放功能實(shí)際上并不復(fù)雜,主要還是通過(guò)調(diào)整css樣式做到我們想要的效果。我們?cè)跇I(yè)務(wù)中遇到這種“沒(méi)做過(guò),看起來(lái)很麻煩”的問(wèn)題時(shí),始終應(yīng)該還是抱著“只有你想不到,沒(méi)有我做不到”的態(tài)度,只要深入思考一下或者動(dòng)手嘗試一下,大概就會(huì)發(fā)出“哦,原來(lái)這么簡(jiǎn)單”的感嘆。有道是:山重水復(fù)疑無(wú)路,柳暗花明又一村。
到此這篇關(guān)于React如何實(shí)現(xiàn)視頻旋轉(zhuǎn)縮放的文章就介紹到這了,更多相關(guān)React視頻旋轉(zhuǎn)縮放內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react 實(shí)現(xiàn)圖片正在加載中 加載完成 加載失敗三個(gè)階段的原理解析
這篇文章主要介紹了react 實(shí)現(xiàn)圖片正在加載中 加載完成 加載失敗三個(gè)階段的,通過(guò)使用loading的圖片來(lái)占位,具體原理解析及實(shí)現(xiàn)代碼跟隨小編一起通過(guò)本文學(xué)習(xí)吧2021-05-05React實(shí)現(xiàn)圖片懶加載的常見(jiàn)方式
圖片懶加載是一種優(yōu)化網(wǎng)頁(yè)性能的技術(shù),它允許在用戶滾動(dòng)到圖片位置之前延遲加載圖片,通過(guò)懶加載,可以在用戶需要查看圖片時(shí)才加載圖片,避免了不必要的圖片加載,本文給大家介紹了React實(shí)現(xiàn)圖片懶加載的常見(jiàn)方式,需要的朋友可以參考下2024-01-01在?React?中使用?Context?API?實(shí)現(xiàn)跨組件通信的方法
在React中,ContextAPI是一個(gè)很有用的特性,可用于組件間的狀態(tài)共享,它允許跨組件傳遞數(shù)據(jù)而無(wú)需通過(guò)每個(gè)組件手動(dòng)傳遞props,本文給大家介紹在?React?中如何使用?Context?API?來(lái)實(shí)現(xiàn)跨組件的通信,感興趣的朋友一起看看吧2024-09-09如何用react優(yōu)雅的書(shū)寫(xiě)CSS
這篇文章主要介紹了如何用react優(yōu)雅的書(shū)寫(xiě)CSS,幫助大家更好的理解和學(xué)習(xí)使用react,感興趣的朋友可以了解下2021-04-04React如何使用refresh_token實(shí)現(xiàn)無(wú)感刷新頁(yè)面
本文主要介紹了React如何使用refresh_token實(shí)現(xiàn)無(wú)感刷新頁(yè)面,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案
這篇文章主要為大家介紹了React?中使用?RxJS?優(yōu)化數(shù)據(jù)流的處理方案示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Reactjs?+?Nodejs?+?Mongodb?實(shí)現(xiàn)文件上傳功能實(shí)例詳解
今天是使用?Reactjs?+?Nodejs?+?Mongodb?實(shí)現(xiàn)文件上傳功能,前端我們使用?Reactjs?+?Axios?來(lái)搭建前端上傳文件應(yīng)用,后端我們使用?Node.js?+?Express?+?Multer?+?Mongodb?來(lái)搭建后端上傳文件處理應(yīng)用,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-06-06