React VideoPlay 组件封装与使用指南

概述

VideoPlay 是一个基于 React 的高效视频播放器组件,提供了简洁的 API 和完整的播放控制功能,支持视频方向检测和响应式设计。

组件特性

  • 🔄 支持 forwardRef 引用传递

  • 🎮 完整的播放控制(播放/暂停)

  • 📱 自动检测视频方向(横屏/竖屏)

  • 🎯 响应式设计适配

  • 🖼️ 自定义封面图支持

  • 🔊 静音和内联播放优化

安装与引入

复制代码
import VideoPlay from '../../utils/videoPlay/VideoPlay';

基础用法

1. 基本视频播放

复制代码
import { useState, useRef } from 'react';

const App = () => {
    const [isPlaying, setIsPlaying] = useState(false);
    const videoRef = useRef(null);

    return (
        <VideoPlay
            ref={videoRef}
            isPlaying={isPlaying}
            setIsPlaying={setIsPlaying}
            videoSrc="path/to/video.mp4"
            poster="path/to/poster.jpg"
        />
    );
};

2. 完整示例(关注公众号场景)

DOM实践

复制代码
import styles from './FollowPublicCode.module.scss';
import { Button } from 'antd';
import VideoPlay from '../../utils/videoPlay/VideoPlay';
import { useState, useRef } from 'react';

const FollowPublicCode = () => {
    // 关注公众号视频播放状态
    const [isPlaying, setIsPlaying] = useState(false);
    // 关注公众号视频是否已开始播放
    const [hasStarted, setHasStarted] = useState(false);
    // 关注公众号视频组件引用
    const videoComponentRef = useRef(null);
    // 关注公众号视频元素引用
    const codeVideoRef = useRef(null);

    // 处理播放按钮点击事件
    const handlePlayClick = () => {
        console.log('点击播放按钮');
        setIsPlaying(true);
        setHasStarted(true);

        if (codeVideoRef.current) {
            codeVideoRef.current.classList.add(styles.playing);
        }
    };

    // 处理视频播放状态变化事件
    const handleVideoStateChange = (playing) => {
        setIsPlaying(playing);
        if (codeVideoRef.current) {
            if (playing) {
                codeVideoRef.current.classList.add(styles.playing);
            } else {
                codeVideoRef.current.classList.remove(styles.playing);
            }
        }
    };

    return (
        <>
            <title>
                关注公众号
            </title>
            <div className={styles.followPublicCodeContainer}>
                <div className={styles.header}>
                    <div className={styles.headerText}>
                        <h1 className={styles.h1}>关注公众号</h1>
                        <p className={styles.p}>消息通知将通过公众号推送</p>
                    </div>
                </div>
                <div className={styles.codeContainer}>
                    <div className={styles.codeVideo}>
                        {!hasStarted && (
                            <div className={styles.videoHeader}>
                                <div className={styles.headerVideoText}>
                                    <h1 className={styles.h1}>Jshinelink</h1>
                                    <p className={styles.p}>关注公众号操作指南</p>
                                </div>
                                <div
                                    className={styles.playButton}
                                    onClick={handlePlayClick}
                                >
                                    <img
                                        src="https://xingge-ai.oss-cn-shenzhen.aliyuncs.com/agg-notifs-icon/Play Circle.png"
                                        alt="播放"
                                        width={32}
                                        height={32}
                                    />
                                </div>
                                <div className={styles.headerImage}>
                                    <img src="https://xingge-ai.oss-cn-shenzhen.aliyuncs.com/agg-notifs-icon/logo.png" alt="logo" />
                                </div>
                            </div>
                        )}

                        {/* 已经开始播放(无论当前是播放还是暂停)都显示视频组件 */}
                        {hasStarted && (
                            <VideoPlay
                                ref={videoComponentRef}
                                isPlaying={isPlaying}
                                setIsPlaying={handleVideoStateChange}
                                videoSrc="https://xingge-ai.oss-cn-shenzhen.aliyuncs.com/agg-notifs-icon/test_video_1.mp4"
                            />
                        )}
                    </div>

                    <div className={styles.codeContent}>
                        <div className={styles.codeImgContent}>
                            <img className={styles.codeImg} src="https://xingge-ai.oss-cn-shenzhen.aliyuncs.com/ceshi.png" alt="关注公号" />
                        </div>
                        <div className={styles.codeImgText}>
                            <p className={styles.p}>请使用微信扫一扫或长按识别二维码</p>
                            <Button className={styles.viewButton}>长按识别二维码</Button>
                        </div>
                    </div>
                </div>
            </div>
        </>
    );
};

export default FollowPublicCode;

CSS实践

复制代码
.codeContainer {
    width: 100%;
    border-top-left-radius: 12px;
    border-top-right-radius: 12px;
    background-color: #fff;
    padding: 10px;
    flex: 1;
    margin: 0 auto;

    .codeVideo {
        width: 100%;
        height: 100%;
        max-height: 260px;
        margin-bottom: 40px;
        border-radius: 4px;
        background-image: url('https://xingge-ai.oss-cn-shenzhen.aliyuncs.com/agg-notifs-icon/Mask group.png');
        background-size: cover;
        position: relative;
        overflow: hidden;

        .videoPlayContainer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }

        &.playing {
            background-image: none;
            background-color: #000;
        }

        .videoHeader {
            display: flex;
            flex-direction: row;
            align-items: center;
            justify-content: space-between;
            width: 100%;
            height: 100%;
            min-width: 0;
            padding: 20px;
            box-sizing: border-box;
            position: relative;

            .headerVideoText {
                flex: 1;
                min-width: 0;
                padding-left: 10px;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;

                .h1 {
                    font-size: clamp(16px, 4vw, 20px);
                    font-weight: 500;
                    margin-bottom: 8px;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                }

                .p {
                    font-size: clamp(12px, 3vw, 16px);
                    font-weight: 500;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                }
            }

            .playButton {
                display: flex;
                align-items: center;
                justify-content: center;
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%, -50%);
                z-index: 2;

                img {
                    width: clamp(24px, 8vw, 32px);
                    height: clamp(24px, 8vw, 32px);
                }
            }

            .headerImage {
                margin-left: 10px;
                flex-shrink: 0;

                img {
                    width: clamp(80px, 30vw, 149px);
                    height: clamp(80px, 30vw, 149px);
                }
            }
        }
    }
}

:fullscreen {
    .codeVideo {
        height: 100vh !important;
        max-width: 100vw !important;
        border-radius: 0 !important;
        background-image: none !important;
    }
}

:-webkit-full-screen {
    .codeVideo {
        height: 100vh !important;
        max-width: 100vw !important;
        border-radius: 0 !important;
        background-image: none !important;
    }
}

// 响应式调整
@media (max-width: 480px) {
    .codeContainer {
        .codeVideo {
            .videoHeader {
                padding: 15px;

                .headerVideoText {
                    padding-left: 5px;

                    .h1 {
                        font-size: 20px;
                        margin-bottom: 5px;
                    }

                    .p {
                        font-size: 16px;
                    }
                }

                .headerImage {
                    margin-left: 5px;

                    img {
                        width: 149px;
                        height: 149px;
                    }
                }
            }
        }

        .codeContent {

            .codeImgContent,
            .codeImgText {
                padding: 0 20px;
            }

            .viewButton {
                width: 200px;
                height: 44px;
                font-size: 14px;
            }
        }
    }
}

@media (max-width: 320px) {
    .codeContainer {
        .codeVideo {
            .videoHeader {
                padding: 10px;

                .headerVideoText {
                    .h1 {
                        font-size: 12px;
                    }

                    .p {
                        font-size: 10px;
                    }
                }

                .headerImage img {
                    width: 60px;
                    height: 60px;
                }

                .playButton img {
                    width: 20px;
                    height: 20px;
                }
            }
        }
    }
}

API 文档

Props

属性 类型 必需 默认值 描述
isPlaying boolean - 控制视频播放状态
setIsPlaying function - 播放状态变更回调
videoSrc string - 视频源文件路径
poster string - 视频封面图路径

Ref 方法

通过 ref 可以访问以下方法:

复制代码
const videoRef = useRef(null);

// 播放视频
videoRef.current.play();

// 暂停视频
videoRef.current.pause();

// 获取原生 video 元素
const videoElement = videoRef.current.getVideoElement();

事件处理

内置事件

组件内部处理以下事件:

  • onLoadedMetadata: 检测视频元数据,自动判断方向

  • onPlay: 播放时更新状态

  • onPause: 暂停时更新状态

  • onEnded: 播放结束时重置状态

视频方向检测

组件自动检测视频方向并添加相应的 CSS 类:

  • 竖屏视频:添加 portrait

  • 横屏视频:添加 landscape

CSS 样式示例

复制代码
// 竖屏视频样式
    &.portrait {
        .nativeVideo {
            object-fit: contain;
            height: 100%;
            width: auto;
            max-width: 100%;
        }
    }

    // 横屏视频样式
    &.landscape {
        .nativeVideo {
            object-fit: contain;
            width: 100%;
            height: auto;
            max-height: 100%;
        }
    }

最佳实践

1. DOM管理

复制代码
import React, { forwardRef, useImperativeHandle, useRef, useEffect, useState } from "react";
import styles from './video.module.scss';

const VideoPlay = forwardRef((props, ref) => {
    const { isPlaying, setIsPlaying, poster, videoSrc } = props;
    const videoRef = useRef(null);
    const [videoOrientation, setVideoOrientation] = useState('landscape');
    useImperativeHandle(ref, () => ({
        play: () => {
            if (videoRef.current) {
                videoRef.current.play().catch(err => {
                    console.error('播放失败:', err);
                });
            }
        },
        pause: () => {
            if (videoRef.current) {
                videoRef.current.pause();
            }
        },
        getVideoElement: () => videoRef.current
    }));

    useEffect(() => {
        if (videoRef.current) {
            if (isPlaying) {
                videoRef.current.play().catch(err => {
                    console.error('播放失败:', err);
                });
            } else {
                videoRef.current.pause();
            }
        }
    }, [isPlaying]);

    // 检测视频元数据,判断方向
    const handleLoadedMetadata = () => {
        if (videoRef.current) {
            const { videoWidth, videoHeight } = videoRef.current;
            const orientation = videoHeight > videoWidth ? 'portrait' : 'landscape';
            setVideoOrientation(orientation);
            console.log(`视频方向: ${orientation}, 尺寸: ${videoWidth}x${videoHeight}`);
        }
    };

    return (
        <div className={`${styles.videoPlayContainer} ${styles[videoOrientation]}`}>
            <video
                ref={videoRef}
                className={styles.nativeVideo}
                poster={poster}
                muted
                playsInline
                controls
                onLoadedMetadata={handleLoadedMetadata}
                onPlay={() => setIsPlaying(true)}
                onPause={() => setIsPlaying(false)}
                onEnded={() => setIsPlaying(false)}
            >
                <source src={videoSrc} type="video/mp4" />
                您的浏览器不支持视频播放
            </video>
        </div>
    );
});

export default VideoPlay;

2.CSS管理

复制代码
.videoPlayContainer {
    width: 100%;
    height: 100%;
    position: relative;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;

    // 竖屏视频样式
    &.portrait {
        .nativeVideo {
            object-fit: contain;
            height: 100%;
            width: auto;
            max-width: 100%;
        }
    }

    // 横屏视频样式
    &.landscape {
        .nativeVideo {
            object-fit: contain;
            width: 100%;
            height: auto;
            max-height: 100%;
        }
    }

    .nativeVideo {
        background: #000;
        display: block;

        // 默认样式
        max-width: 100%;
        max-height: 100%;
    }
}

最终呈现

点击播放按钮播放视频

视频播放(横向)

视频播放(纵向)

播放器支持放大缩小、是否静音、是否暂停等基础组件功能!

注意事项

  1. 移动端适配 : 确保设置 playsInlinemuted 属性以支持自动播放

  2. 视频格式: 提供多种视频格式或使用兼容性好的 MP4 格式

  3. 错误边界: 添加适当的错误处理机制

  4. 加载状态: 考虑添加视频加载中的提示

故障排除

常见问题

  1. 视频无法播放

    • 检查视频路径是否正确

    • 验证视频格式兼容性

    • 确认服务器 CORS 设置

  2. 自动播放被阻止

    • 确保视频设置为静音

    • 添加用户交互触发播放

  3. 方向检测不准确

    • 确认视频元数据已加载完成

    • 检查视频尺寸信息

结语

VideoPlay 组件提供了简单易用的视频播放解决方案,通过合理的封装和完整的 API 设计,可以快速集成到各种 React 应用中。根据具体需求,您可以进一步扩展其功能或自定义样式。

注意⚠:该组件仅供参考!!!

相关推荐
Ace_317508877644 分钟前
微店平台关键字搜索接口深度解析:从 Token 动态生成到多维度数据挖掘
java·前端·javascript
Billow_lamb44 分钟前
React 创建 Context
javascript·react.js·ecmascript
苏小画1 小时前
Vue 组件库从创建到发布全流程
前端·javascript·vue.js
月小满1 小时前
DataV轮播时其他组件的内容也一起滚动 修复bug的方法
前端·vue.js·bug·大屏端
小莫分享1 小时前
Github Action 一键部署HTML 静态服务
前端·html·github
星释2 小时前
Rust 练习册 66:密码方块与文本加密
java·前端·rust
IT_陈寒2 小时前
React性能翻倍!90%开发者忽略的5个Hooks最佳实践
前端·人工智能·后端
亿元程序员2 小时前
光图片就300多M,微信小游戏给再大的分包也难啊!
前端
中工钱袋2 小时前
前端请求到底是从哪里发出去的?
前端