专栏名称: 前端早读课
我们关注前端,产品体验设计,更关注前端同行的成长。 每天清晨五点早读,四万+同行相伴成长。
目录
相关文章推荐
前端早读课  ·  【早阅】构建更高效、更灵活的前端项目:Edg ... ·  22 小时前  
香港新港人  ·  2025 除夕煙花倒數 維多利亞港 ... ·  昨天  
香港新港人  ·  2025 除夕煙花倒數 維多利亞港 ... ·  昨天  
风动幡动还是心动  ·  辞旧迎新 ·  2 天前  
风动幡动还是心动  ·  辞旧迎新 ·  2 天前  
前端早读课  ·  【第3440期】探索 React ... ·  2 天前  
前端早读课  ·  【图书】AI群星闪耀时 ·  3 天前  
51好读  ›  专栏  ›  前端早读课

【第3438期】如何实现视频旋转缩放

前端早读课  · 公众号  · 前端  · 2024-12-29 08:00

主要观点总结

文章介绍了如何实现视频在浏览器中的旋转、缩放功能,包括处理视频控制栏、实现旋转、适配全屏以及比例缩放等技术细节。

关键观点总结

关键观点1: 背景介绍

文章介绍了原本视频预览的简易实现方式,以及产品提出的需求,即实现视频的旋转和缩放功能。

关键观点2: 功能实现

文章详细描述了如何通过CSS样式调整来实现视频的旋转和缩放功能,包括旋转、全屏适配、比例缩放等步骤,并提供了相应的代码示例。

关键观点3: 总结

文章总结了实现视频旋转缩放功能的实际并不复杂,主要通过调整CSS样式达到效果,并鼓励面对类似问题时深入思考和尝试。


正文

前言

介绍了如何实现视频在浏览器中的旋转和缩放功能,包括处理视频控制栏、实现旋转、适配全屏以及比例缩放等技术细节。今日前端早读课文章由 @王耀分享,公号:Goodme 前端团队授权。

正文从这开始~~

【早阅】如何在不吹嘘的情况下展示价值?

一、背景

原本我们预览视频仅仅是简单的 video 标签实现就可以满足业务的需求了,但是某一天,产品说:” 业务的视频是用手机拍的,方向不一定是正的,还有视频的宽高太小了看不清,所以希望我们能让视频做到旋转跳跃(不是)旋转 + 按比例缩小放大 “。此时我的内心 OS:你 * 是《舞娘》听多了吧。不过吐槽归吐槽,既然需求合理该做还是得做啊。

二、实现

这个功能初时看起来很头疼,在细细思考下来后发现,实现思路其实并不复杂,可以通过 transform 去辗转腾挪,最终完成我们想要的效果。

1. 原始效果

我们最初始的效果就是如同图片预览一般,点击弹窗播放视频,所以这里仅仅使用了 video 标签。

2. 还原原生 video 控制栏功能

如果我们直接去旋转 video 标签,那么他的控制栏也会跟着旋转,这并不符合我们的期望,所以我们需要在 video 标签外加一层容器,然后自定义控制栏和 viode 同级,做到只旋转 video 标签。

但是这里我嫌自定义控制栏太麻烦,于是这里选择了一个自带 UI 的三方视频组件进行改造。这里采用的是 vime,有兴趣了解的可以去 https://vimejs.com/。最终是展示这个样子。

目前建议采用这视频组件:https://vidstack.io/

vidstack Github:https://github.com/vidstack/player

3. 旋转

以上前置工作做完之后,接下来就进入正题,添加我们的旋转功能。利用 transform 对 video 进行旋转,同时宽高数值对换。由于 vime 中 video 标签设置了 position: absolute,所以这里我们还要调整初始的 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组件会根据容器宽高自适应
const [width, setWidth] = useState<string | number>(INIT_WIDTH);
// 旋转角度
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;
// 旋转后样式
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;
// 水平角度复原处理
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="旋转" onClick={onRotate} />
Settings>
DefaultUi>
Player>
div>
div>
div>
) : null}
>
);
};

此时就能实现以下效果:

4. 全屏

上面虽然已经实现了旋转效果,但是并没有结束,当我们点击全屏功能之后,此时旋转的样式,就变得奇怪了

这是由于我们先前写死了宽高数值,导致旋转后 video 宽高没有适应全屏,将全屏的宽高设置为 100vh 跟 100vw 即可兼容全屏状态下的旋转。

 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;
// 获取整个屏幕的高度
const screenFullHeight = screen.height;
const vWidth = INIT_WIDTH;
const vHeight = INIT_HEIGHT;
// 旋转后样式
// ...
// 当全屏旋转90/270度时,宽高处理
if (player.current?.isFullscreenActive) {
newWidth = 'auto';
resWidth = '100vh';
resHeight = '100vw';
top = (screenFullHeight - screenWidth) / 2;
left = (screenWidth - screenFullHeight) / 2;
}
// ...
};

// 旋转
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>
)

全屏 + 旋转 效果展示

5. 比例缩放

走到这里,旋转功能原本已经完成,但是不要忘记还有一个缩小放大功能,从全屏旋转的例子来看,缩放功能也会影响到旋转效果,还要对旋转再做兼容处理。

我们先实现缩放功能:

 // 比例
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;
// 视频设置新宽高
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>
...
)

我们在缩放时改变了宽高,但是旋转使用的依然是初始宽高,那么在缩放之后旋转,会变回初始大小,而旋转后再缩放,则会导致样式错误。所以,我们在缩放和旋转方法中需要拿到上一次变换过的样式,再进行新的变换。

 const scaleRef = useRef(1);

const changeStyle = (deg: number) => {
const videoEl: HTMLVideoElement | null = document.querySelector('.sc-vm-file');
if (!videoEl) return;
// 获取窗口的宽度
const screenWidth = window.innerWidth;
// 获取整个屏幕的高度
const screenFullHeight = screen.height;
const vWidth = INIT_WIDTH * scaleRef.current;
const vHeight = INIT_HEIGHT * scaleRef.current;
// 旋转后样式
// ...
// 水平角度处理
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);
};

最终效果展示

三、总结

实现视频的旋转缩放功能实际上并不复杂,主要还是通过调整 css 样式做到我们想要的效果。我们在业务中遇到这种 “没做过,看起来很麻烦” 的问题时,始终应该还是抱着 “只有你想不到,没有我做不到” 的态度,只要深入思考一下或者动手尝试一下,大概就会发出 “哦,原来这么简单” 的感叹。有道是:山重水复疑无路,柳暗花明又一村。

关于本文
作者:@王耀
原文:https://mp.weixin.qq.com/s/ADkckWr9BVFIBopWIAm-Dg

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。