• 短视频无尽流前端开发指南


    62fc1ca39faaa893e03faea2774087be.gif

    本文基于对家装家居内容短视频无尽流的开发实践,总结出了一套适应于该场景及衍生场景的前端开发指南,通过阅读本文可以快速了解短视频无尽流的前端开发。

    前言

    短视频无尽流是当下比较热门的一种业务场景,在日常生活中随处可见。本文基于对家装家居内容短视频无尽流的开发实践,总结出了一套适应于该场景及衍生场景的前端开发指南,通过阅读本文可以快速了解短视频无尽流的前端开发。

    3b6b8510b20479f467f326326878d74c.gif

    短视频无尽流介绍

    短视频有着“短、平、快”的特点,用户可以通过短视频快速获得一些输入。在家装家居领域,可以通过几十秒到几分钟的短视频向用户输出装修干货经验、居家好物推荐等等。在短视频无尽流场景中,会基于引流内容以及相关算法推荐出更多内容,用户随着手势上滑可以持续浏览获得内容输入。

    短视频无尽流结构拆解

    短视频无尽流从结构上可以拆解为两层:滑动轮播容器、单张内容卡片。单张内容卡片又可以拆解为自定义控制栏的视频播放器(下文简称视频播放器)和内容相关信息两部分。

    内容相关信息为业务呈现模块,不同的业务有各自的表达方式,本文不对该部分展开介绍。下面将基于react介绍滑动轮播容器和视频播放器的开发指南。

    66676df2d7ac391f930366f1949e38cb.png

      视频播放器

    家装家居内容短视频无尽流使用的是淘宝App内置的同层渲染播放器(VideoX桥接),本文为了增强普适性,直接采用HTML5 video标签作为播放器来介绍。

    播放器自身的控制栏样式比较单一,往往不能满足业务诉求,需要实现自定义的控制栏。本小节将介绍如何实现播放器状态按钮和播放器进度条,以及播放器的激活和销毁,为应用在滑动轮播容器做前置准备。

    • 视频播放器状态按钮

    常规来讲播放器需要展示出两种状态:暂停中、缓冲中,播放中有进度条在推进一般不需要做额外展示。状态按钮组件实现如下:

    tips:将按钮状态内置在组件中,暴露修改状态的方法给父元素,可以避免在改变按钮状态时触发父元素的re-render。

    1. // ...
    2. const StatusButton = forwardRef((_, ref) => {
    3. const [status, setStatus] = useState(EStatus.PLAY);
    4. useImperativeHandle(ref, () => ({
    5. setStatus,
    6. }));
    7. return (
    8. <div className={styles.statusButton}>
    9. {(() => {
    10. switch (status) {
    11. case EStatus.PAUSE:
    12. return <div>{/* 暂停Icon */}div>;
    13. case EStatus.WAITING:
    14. return <div>{/* 缓冲Icon */}div>;
    15. default:
    16. return null;
    17. }
    18. })()}
    19. div>
    20. );
    21. });
    22. export default memo(StatusButton);
    1. // ...
    2. const VideoPlayer: FC = (props) => {
    3. const { source } = props;
    4. const videoPlayerRef = useRef<HTMLVideoElement | null>(null);
    5. const statusButtonRef = useRefnull>(null);
    6. const onPlay = () => {
    7. statusButtonRef.current?.setStatus(EPlayerStatus.PLAY);
    8. };
    9. useEffect(() => {
    10. videoPlayerRef.current?.addEventListener('play', onPlay);
    11. // 暂停(pause)和缓冲(waiting)监听方法类似
    12. // ...
    13. return () => {
    14. videoPlayerRef.current?.removeEventListener('play', onPlay);
    15. };
    16. }, []);
    17. return (
    18. <div className={styles.videoPlayerContainer}>
    19. {/* 播放器 */}
    20. <video
    21. ref={videoPlayerRef}
    22. className={styles.item}
    23. src={source}
    24. playsInline
    25. autoPlay
    26. />
    27. {/* 播放器状态按钮 */}
    28. <StatusButton ref={statusButtonRef} />
    29. div>
    30. );
    31. };
    32. export default memo(VideoPlayer);
    • 视频播放器进度条

    388d80299a2c49588fc4c8a6ce9e5f20.gif

    有两种情况会引起进度条“走动”:

    1. 视频正常播放,进度更新。

    2. 用户手动拖拽进度条。

    对于1,进度条组件对父元素暴露更新进度的方法,父元素监听到播放器 timeupdate 时去调用该方法即可。

    对于2,可以使用 @use-gesture/vanilla 实现跟手的拖拽效果,用户停止拖拽时去做播放器的跳帧操作:

    1. // ...
    2. useEffect(() => {
    3. const gesture = new DragGesture(
    4. // 拖拽“点”
    5. thumbRef.current,
    6. (state) => {
    7. if (state.first) {
    8. setIsDragging(true);
    9. }
    10. const x = state.xy[0];
    11. let walked: number;
    12. // 判断是否超出边界
    13. if (x < 0) {
    14. walked = 0;
    15. } else if (x > OVERALL_WIDTH) {
    16. walked = OVERALL_WIDTH;
    17. } else {
    18. walked = x;
    19. }
    20. setCurrentWalked(walked);
    21. if (state.last) {
    22. // 用户停止拖拽后,跳帧至当前时间
    23. const duration = Math.ceil((walked / OVERALL_WIDTH) * maxDuration);
    24. onChangeCurrentTime(duration);
    25. setIsDragging(false);
    26. }
    27. },
    28. {
    29. axis: 'x',
    30. pointer: { touch: true },
    31. },
    32. );
    33. return () => {
    34. gesture.destroy();
    35. };
    36. }, []);
    37. return (
    38. <div ref={thumbRef} />
    39. );
    • 视频播放器激活及销毁

    虽然该场景下存在n个内容卡片,但是我们只需要屏幕当中的那一个内容卡片渲染视频播放器,其余内容卡片仅保留封面图占位即可,减少内存占用。

    e4506cc0b78b31d2f0d7f5cdbdc89691.png

    1. // ...
    2. const VideoPlayer = forwardRef((props, ref) => {
    3. // 播放器状态 默认为非激活状态
    4. const [activeStatus, setActiveStatus] = useState<boolean>(false);
    5. // ...
    6. /**
    7. * 隐藏封面占位
    8. */
    9. const hidePoster = () => {
    10. posterRef.current?.hide();
    11. };
    12. /**
    13. * 激活播放器
    14. */
    15. const activate = () => {
    16. setActiveStatus(true);
    17. };
    18. /**
    19. * 销毁播放器
    20. */
    21. const inActivate = () => {
    22. setActiveStatus(false);
    23. // 销毁播放器时展示封面占位
    24. posterRef.current?.show();
    25. };
    26. useImperativeHandle(ref, () => ({
    27. activate,
    28. inActivate,
    29. }));
    30. useEffect(() => {
    31. // 监听视频首帧加载完成时再去隐藏封面占位,防止屏幕闪动
    32. videoPlayerRef.current?.addEventListener('loadeddata', hidePoster);
    33. // ...
    34. return () => {
    35. videoPlayerRef.current?.removeEventListener('loadeddata', hidePoster);
    36. };
    37. }, []);
    38. // ...
    39. });
    40. export default memo(VideoPlayer);

      滑动轮播容器

    对于滑动轮播容器,采用swiper实现。swiper是强大的轮播组件,有丰富的内置能力,封装了react组件可以方便地使用。

    • 虚拟轮播

    由于短视频无尽流有”无尽“的特性,用户单次可能会浏览几十篇内容,因此可以使用swiper的virtual能力减少内存占用,会随着手动轮播切换增删节点,仅保留视角内上下有限个swiper slide节点。如下图所示,当前需要用到500个slide,但是通过动态增删节点保证实际渲染出的节点数最多只有5个(个数可配置)。

    4205e62211650fd98d2602796609e314.png

    swiper入参配置可参考:

    1. // swiper 6.x版本 和 8.x版本 使用上会有一定区别,注释中会将不同点标注出来
    2. import * as React from 'react';
    3. // [swiper 8.x] 引入 swiper
    4. import { Virtual } from 'swiper';
    5. // [swiper 6.x] 引入 swiper
    6. // import SwiperCore, { Virtual } from 'swiper';
    7. import { Swiper, SwiperSlide } from 'swiper/react';
    8. import type { FC } from 'react';
    9. import type { IVideoCardItem } from '../../types';
    10. import styles from './index.module.less';
    11. // [swiper 6.x] 加载 Virtual 模块
    12. // SwiperCore.use([Virtual]);
    13. const VideoSwiper: FC = () => {
    14. return (
    15. <Swiper
    16. className={styles.videoSwiperContainer}
    17. // 切换方向
    18. direction="vertical"
    19. // 初始索引
    20. initialSlide={0}
    21. // 切换角度,防止误切
    22. touchAngle={30}
    23. // [swiper 8.x] 加载 Virtual 模块
    24. modules={[Virtual]}
    25. // virtual配置,如下配置会保证至多有5 slide
    26. virtual={{
    27. // 在activeslide前多渲染1slide
    28. addSlidesBefore: 1,
    29. // 在activeslide后多预渲染1slide
    30. addSlidesAfter: 1,
    31. }}
    32. >
    33. {/* ... */}
    34. Swiper>
    35. );
    36. };

    tips:移动端swiper切换时可能存在闪屏/抖动的异常情况,可以使用如下代码开启硬件加速,可以解决大部分异常情况。

    1. .videoSwiperContainer :global {
    2. .swiper-wrapper {
    3. transform: translate3d(0, 0, 0);
    4. .swiper-slide {
    5. transform: translate3d(0, 0, 0);
    6. }
    7. }
    8. }
    • 视频播放器实例管理

    上述中提到只需要屏幕当中的那一个内容卡片渲染播放器,其余展示封面图占位即可。在轮播容器完成一次切换即 onTransitionEnd  时销毁上一个内容卡片的播放器,同时激活当前内容卡片的播放器,保证始终只有一个播放器处于激活状态。

    tips:当前内容卡片的播放器激活后,由于还需要加载视频资源,因此切换后会有短暂的等待时间。为了提升用户体验,可以配合视频资源的预加载,优先使用端侧提供的预加载能力,若没有该支持,可以尝试使用blob等预加载方案。

    d57034ede975828c166630065345cc69.gif

    底部悬浮loading条

    61d364720e707045002c618963273299.gif

    无尽流场景不可避免的是加载loading,对于全屏幕的轮播容器,loading的出现/消失尽量避免产生布局偏移,如果loading过程中用户想去做一些点击操作,但刚好操作的瞬间loading结束了,若此时发生了布局偏移,会造成用户点到非预期的行动点,有损用户体验。可以采用类似此轻量级的悬浮式loading:

    1. .loadingBar {
    2. &Container {
    3. position: fixed;
    4. left: 0;
    5. bottom: 0;
    6. z-index: 99;
    7. display: flex;
    8. align-items: center;
    9. justify-content: flex-end;
    10. width: 100vw;
    11. height: 5rpx;
    12. }
    13. &Item {
    14. height: 5rpx;
    15. background-color: #fd0;
    16. animation: loading 0.6s linear infinite;
    17. }
    18. }
    19. @keyframes loading {
    20. 0% {
    21. width: 0;
    22. opacity: 0;
    23. }
    24. 30% {
    25. width: 30vw;
    26. opacity: 1;
    27. }
    28. 70% {
    29. width: 70vw;
    30. opacity: 1;
    31. }
    32. 100% {
    33. width: 100vw;
    34. opacity: 0;
    35. }
    36. }

    总结

    本文通过对短视频无尽流结构的拆解,从各个功能点的角度介绍了如何实现并落地该场景。除了短视频无尽流外,还适用于其衍生场景,如图文卡片无尽流、直播无尽流、3D场景无尽流等等。针对于不同场景,不变的是轮播容器的构建,在此基础上根据不同场景构建单个场景卡片的逻辑即可。

    团队介绍

    我们是大淘宝-家装家居技术-前端团队,团队支撑大淘宝家居家装业务。旗下包括:每平每屋App、淘宝【极有家】频道。我们连通电商平台和商家店铺,覆盖居家生活、装修设计、线下市场,3D场景化展现居家生活,我们力求让每件单品都不再孤立呈现,置身其中,感受家的优选方案。期待与您一起共筑美好的理想家。

    ✿  拓展阅读

    f757eddfa351b57ded5d38662b90970c.jpeg

    f65306af47448e59da6949270bf57719.jpeg

    作者|胡少鹏(棣棠)

    编辑|橙子君

    b0e67bd186e391547bd128709f874448.png

  • 相关阅读:
    5G 3GPP全球频谱介绍
    Win11怎么安装语音包?Win11语音包安装教程
    【雷神笔记本快捷键】雷神笔记本FN功能快捷键大全以及电脑CPU处于低功耗但电脑风扇高速转动噪音较大解决方案
    select多选回显问题 (取巧~)
    CORS(跨域资源共享)
    Calcite parser config介绍
    【Java从入门到精通 07】:面向对象编程(基础部分)
    音视频编码
    怎么成为稚晖君?
    基于Istio的高级流量管理三
  • 原文地址:https://blog.csdn.net/Taobaojishu/article/details/126188038