背景
产品经理前段时间给官网首页轮播图加上了视频播放的feature
,不过因为还要支持自动轮播,所以每次视频都放不完就切走了。因为之前急着上线,当时的解决方案是首页关掉了自动轮播,只能用户手动切换。最近新的迭代需求,产品又把这个提了出来,需要优化:
- 视频文件播放完,切换至下一个
Slider
展示;- 静态图片固定展示
3s
后切至下一个Slider
;- 依然支持鼠标移入后的暂停轮播功能。
而且还急着上线,排在了T0
优先级。结果当初做支持视频播放功能的前端同学这两周请假了,这活落在了我的头上😅。想想当初我还嘲笑那位同学被安排了这么个活,现在回旋镖打到自己身上了🫤。
调研
首先我想着能不能找到现成的完美支持我们需求的轮播图组件,不过我简单搜索了一下,并没有找到合适的开源组件;考虑了一下,要么装个Swiper.js
手搓,要么在现有正在使用的Ant Design
组件库的Carousel
组件上手搓了。既然都手搓,还是用现成的吧🤣。
分析
对需求进行了一下分析 :
-
视频文件播放完,切换至下一个 Slider 展示。这个实现起来不难,主要有以下几种思路:
-
后端返回视频资源路径的同时,返回该视频资源的时长,前端手动给对应的
Slider
实现相应的播放时长,需要后端接口改动; -
前端拿到视频资源,手动加载获取视频的时长,接着和之前一样实现对应的播放时长,需要加载整个视频才能拿到;
-
给视频加上
onEnded
事件监听,准确获取视频播放结束时刻,进行后续操作。
隐性功能:切换到下一页之前,这一页的视频要停止播放;下次展示该视频时,需要重新播放。
-
-
静态图片固定展示 3s 后切至下一个 Slider。这个比较简单;
-
依然支持鼠标移入后的暂停轮播功能 。看起来比较简单,
Carousel
组件本身就支持pauseOnfocused
。隐性功能:视频页在
pauseOnfocused
时,需要轮播,而不是播放完一次就暂停不播了。
开发
-
视频文件播放完,切换至下一个 Slider 展示 。因为优先考虑前端单独实现且重视性能,所以直接采用
video
标签的onEnded
事件属性方案。-
首先确定手动播放视频的方法:
ts// 视频播放方法 const playVideo = (ele: HTMLVideoElement) => { if (ele?.paused) try { ele?.play() } catch (_err) { ele?.play() } }
-
在视频播放时,将
Carousel
组件的autoplay
属性改为false
,播放结束后再改为true
,且暂停视频播放:ts// 视频播放 const playVideo = (ele: HTMLVideoElement) => { if (ele.paused){ setAutoplay(true) try { ele.play() } catch (_err) { ele.play() } } } // 视频结束 const handleEnded = (e: any) => { e.target.pause(); setAutoplay(true); }
-
发现修改
Carousel
组件的autoplay
属性,会导致整个组件重新渲染,内部的视频标签也会重新渲染导致闪烁。所以给内部的媒体组件加上React.memo()
。tsxconst MedaNode = memo(({ video, imageUrl, index, handleEnded }: MedaProps) => { // 视频 if (video) { return <video src={imageUrl} autoPlay muted preload="auto" disablePictureInPicture id={`video-${index}`} onEnded={handleEnded} /> } // 静图 return <img src={imgUrl} /> }) export default Banner ({ bannerList }: BannerProps) { // ... const handleEnded = useCallback((e: any) => { e.target.pause(); setAutoplay(true) }, []) // ... return ( <Carousel> {list?.length && list.map((item, index) => { const { id, imageUrl, linkUrl, video } = item; return ( <div key={id} onClick={() => linkUrl && Router.push(linkUrl)} > <MedaNode video={video} imageUrl={imageUrl} index={index} handleEnded={handleEnded} /> </div>) })} </Carousel>) }
-
-
静态图片固定展示 3s 后切至下一个 Slider 。这个功能,只要开启了
Carousel
组件的autoplay
就可以直接实现; -
依然支持鼠标移入后的暂停轮播功能 。分析一下并不难,因为
Slider
是图片时,该功能是自动支持,且Slider
是视频时,自动轮播是关闭的,播放完需要前端手动切换到下一页。-
只要对视频播放结束方法进行改造,添加手动切换:
tsconst handleEnded = useCallback((e: any) => { e.target.pause(); carouselRef.current?.next(); setAutoplay(true) }), [])
-
此外要注意功能分析时发现的
隐性功能
:视频页在pauseOnfocused
时,需要轮播,而不是播放完一次就暂停不播了。一开始认为只要handleEnded
方法在mouseEnter
为true
时,不做任何操作:tsconst handleEnded = useCallback((e: any) => { if(mouseEnter) return; e.target.pause(); carouselRef.current?.next(); setAutoplay(true); }), [mouseEnter])
就可以实现,因为
video
标签加上了autoPlay、 loop
属性,不去pause
,自然就会一直重复播放下去。结果发现,加上了loop
属性的video
标签无法触发onEnded
事件,加上了autoPlay
属性后去pause
时,视频总是无法回到播放时间为0
的时刻,导致重新play
时,会闪烁。经过重复尝试最终修改:ts// 视频播放 const playVideo = (ele: HTMLVideoElement) => { if (ele?.paused){ ele.currentTime = 0; try { ele?.play() } catch (_err) { ele?.play() } } } // 视频结束 const handleEnded = useCallback((e: any) => { if (!mouseEnter) playVideo(e.target) else { e.target.pause(); carouselRef.current?.next(); setAutoplay(true) } }, [mouseEnter])
最终代码
tsximport { Carousel } from "antd"; import { CarouselRef } from "antd/es/carousel"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { BannerProps, MedaProps } from "src/interfaces/type"; import { getFileExt } from "src/utils"; // 视频播放方法 const playVideo = (ele: HTMLVideoElement) => { if (ele?.paused) try { ele?.play() } catch (_err) { ele?.play() } } const MedaNode = memo(({ video, imageUrl, index, handleEnded }: MedaProps) => { // 视频 if (video) { return <video src={imageUrl} autoPlay muted preload="auto" disablePictureInPicture id={`video-${index}`} onEnded={handleEnded} /> } // 静图 return <img src={imgUrl} /> }) export default Banner ({ bannerList }: BannerProps) { const [autoplay, setAutoplay] = useState<boolean>(false); const [current, setCurrent] = useState<number>(0); const carouselRef = useRef<any>(null); const list = useMemo(() => { return bannerList.map((item: any) => { const ext = getFileExt(item.imageUrl); // 截取文件后缀 return { ...item, video: ext === 'mp4' }; // 返回新对象 }); }, [bannerList]); useEffect(() => { const currentItem = list[current]; if (!currentItem) return; const videoElem = document.getElementById(`video-${current}`) as HTMLVideoElement; if (currentItem.video) { setAutoplay(false) playVideo(videoElem) } }, [current, list]); const handleEnded = useCallback((e: any) => { if (!mouseEnter) playVideo(e.target) else { e.target.pause(); carouselRef.current?.next(); setAutoplay(true) } }, [mouseEnter]) return ( <Carousel afterChange={(cur) => setCurrent(cur)} autoplay={autoplay} ref={carouselRef} speed={800} >{list?.length && list.map((item, index) => { const { id, imageUrl, linkUrl, video } = item; return ( <div key={id} onClick={() => linkUrl && Router.push(linkUrl)} > <MedaNode video={video} imageUrl={imageUrl} index={index} handleEnded={handleEnded} /> </div>) }) } </Carousel>) }
-
结果
功能完美实现,通过产品审核,"暂时"还没出现问题🫡。