网页全局音量控制-react + antd

核心代码:
import React, {
    useState,
    useRef,
    createContext,
    useContext,
    useCallback,
  } from 'react'

export const GlobalVolumeContext = createContext({
    globalVolume: 0,
    updateGlobalVolume: (val: any) => {},
    registerAudio: (ref: any) => {},
    unregisterAudio: (ref: any) => {},
})

// 使用全局音量的Hook
export const useGlobalVolume = () => {
    const context = useContext(GlobalVolumeContext)
    if (!context) {
        console.error('useGlobalVolume must be used within a GlobalVolumeProvider')
    }
    return context
}

// 音量提供者组件
export  const GlobalVolumeProvider = ({children}) => {
    const [globalVolume, setGlobalVolume] = useState(0.7)

    const audioInstances = useRef(new Set())

    const registerAudio = useCallback(
        audioRef => {
        audioInstances.current.add(audioRef)
        },
        [globalVolume]
    )

    const unregisterAudio = useCallback(audioRef => {
        audioInstances.current.delete(audioRef)
    }, [])

    const updateGlobalVolume = useCallback(newVolume => {
        setGlobalVolume(newVolume)

        audioInstances.current.forEach(audioRef => {
        if (audioRef?.current) {
            audioRef.current.volume =
            newVolume * (audioRef?.current?._localVolume ?? 1)
        }
        })
    }, [])

    const value = {
        globalVolume,
        updateGlobalVolume,
        registerAudio,
        unregisterAudio,
    }

    return (
    <GlobalVolumeContext.Provider value={value}>
        {children}
    </GlobalVolumeContext.Provider>
    )
}
View Code

 

 
 
 
HTML可直接运行版:
 
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>全局音量控制系统 - 已修复</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/antd@4.24.10/dist/antd.min.css">
    <script src="https://cdn.jsdelivr.net/npm/react@17/umd/react.development.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.development.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/antd@4.24.10/dist/antd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.22.0/babel.min.js"></script>
    <style>
        body {
            padding: 20px;
            background-color: #f5f5f5;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            padding: 24px;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .volume-info {
            background: #f0f7ff;
            border: 1px solid #91d5ff;
            border-radius: 6px;
            padding: 12px;
            margin: 16px 0;
        }
        .audio-player {
            margin-bottom: 16px;
            border: 1px solid #d9d9d9;
            border-radius: 6px;
            padding: 16px;
        }
        .icon-fix-note {
            color: #faad14;
            font-size: 12px;
            margin-top: 8px;
        }
    </style>
</head>
<body>
    <div id="root"></div>

    <script type="text/babel">
        const { 
            useState, 
            useContext, 
            useRef, 
            useEffect, 
            useCallback,
            createContext 
        } = React;
        
        const { 
            Slider, 
            Card, 
            Row, 
            Col, 
            Button, 
            Space, 
            Typography, 
            Divider,
            Alert,
            Progress
        } = antd;
        
        const { Title, Text, Paragraph } = Typography;
        console.log('window.antd:',window.antd)

        // 正确引入图标 - 修复的核心部分
        // 从Ant Design图标库单独引入每个需要的图标[citation:1][citation:9]
        const icons = window.antd?.icons || {}
        const SoundOutlined = icons?.SoundOutlined || (() => React.createElement('span', {}, '🔊'));
        const PlayCircleOutlined = icons.PlayCircleOutlined || (() => React.createElement('span', {}, '▶️'));
        const PauseCircleOutlined = icons.PauseCircleOutlined || (() => React.createElement('span', {}, '⏸️'));
        const InfoCircleOutlined = icons.InfoCircleOutlined || (() => React.createElement('span', {}, 'ℹ️'));

        // 创建全局音量上下文
        const GlobalVolumeContext = createContext();

        // 全局音量提供者组件
        const GlobalVolumeProvider = ({ children }) => {
            const [globalVolume, setGlobalVolume] = useState(0.7);
            
            const audioInstances = useRef(new Set());
            
            const registerAudio = useCallback((audioRef) => {
                audioInstances.current.add(audioRef);
                if (audioRef.current) {
                    audioRef.current.volume = globalVolume * (audioRef.current._localVolume || 1);
                }
            }, [globalVolume]);
            
            const unregisterAudio = useCallback((audioRef) => {
                audioInstances.current.delete(audioRef);
            }, []);
            
            const updateGlobalVolume = useCallback((newVolume) => {
                setGlobalVolume(newVolume);
                
                audioInstances.current.forEach(audioRef => {
                    if (audioRef.current) {
                        audioRef.current.volume = newVolume * (audioRef.current._localVolume || 1);
                    }
                });
            }, []);
            
            const value = {
                globalVolume,
                updateGlobalVolume,
                registerAudio,
                unregisterAudio
            };
            
            return (
                <GlobalVolumeContext.Provider value={value}>
                    {children}
                </GlobalVolumeContext.Provider>
            );
        };

        // 使用全局音量的Hook
        const useGlobalVolume = () => {
            const context = useContext(GlobalVolumeContext);
            if (!context) {
                throw new Error('useGlobalVolume must be used within a GlobalVolumeProvider');
            }
            return context;
        };

        // 音频播放器组件
        const EnhancedAudioPlayer = ({ 
            src, 
            title, 
            defaultLocalVolume = 1,
            autoPlay = false
        }) => {
            const audioRef = useRef(null);
            const { globalVolume, registerAudio, unregisterAudio } = useGlobalVolume();
            const [localVolume, setLocalVolume] = useState(defaultLocalVolume);
            const [isPlaying, setIsPlaying] = useState(false);
            const [duration, setDuration] = useState(0);
            const [currentTime, setCurrentTime] = useState(0);
            
            useEffect(() => {
                audioRef.current._localVolume = localVolume;
                registerAudio(audioRef);
                
                return () => {
                    unregisterAudio(audioRef);
                };
            }, [registerAudio, unregisterAudio, localVolume]);
            
            useEffect(() => {
                if (audioRef.current) {
                    audioRef.current.volume = globalVolume * localVolume;
                }
            }, [globalVolume, localVolume]);
            
            useEffect(() => {
                const audio = audioRef.current;
                if (!audio) return;
                
                const handleLoadedMetadata = () => {
                    setDuration(audio.duration);
                };
                
                const handleTimeUpdate = () => {
                    setCurrentTime(audio.currentTime);
                };
                
                const handlePlay = () => setIsPlaying(true);
                const handlePause = () => setIsPlaying(false);
                const handleEnded = () => setIsPlaying(false);
                
                audio.addEventListener('loadedmetadata', handleLoadedMetadata);
                audio.addEventListener('timeupdate', handleTimeUpdate);
                audio.addEventListener('play', handlePlay);
                audio.addEventListener('pause', handlePause);
                audio.addEventListener('ended', handleEnded);
                
                if (autoPlay) {
                    audio.play().catch(e => console.log('Auto-play prevented:', e));
                }
                
                return () => {
                    audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
                    audio.removeEventListener('timeupdate', handleTimeUpdate);
                    audio.removeEventListener('play', handlePlay);
                    audio.removeEventListener('pause', handlePause);
                    audio.removeEventListener('ended', handleEnded);
                };
            }, [src, autoPlay]);
            
            const togglePlay = () => {
                if (!audioRef.current) return;
                
                if (isPlaying) {
                    audioRef.current.pause();
                } else {
                    audioRef.current.play().catch(e => console.log('Play failed:', e));
                }
            };
            
            const handleLocalVolumeChange = (value) => {
                setLocalVolume(value / 100);
                audioRef.current._localVolume = value / 100;
            };
            
            const handleSeek = (value) => {
                if (audioRef.current) {
                    audioRef.current.currentTime = value;
                }
            };
            
            const formatTime = (time) => {
                if (isNaN(time)) return '0:00';
                const minutes = Math.floor(time / 60);
                const seconds = Math.floor(time % 60);
                return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
            };
            
            const actualVolumePercent = Math.round(globalVolume * localVolume * 100);
            
            return (
                <div className="audio-player">
                    <Row justify="space-between" align="middle" style={{ marginBottom: 12 }}>
                        <Col>
                            <Text strong>{title}</Text>
                        </Col>
                        <Col>
                            <Text type="secondary">
                               --- 实际音量: {actualVolumePercent}%
                            </Text>
                        </Col>
                    </Row>
                    
                    <audio ref={audioRef} src={src} preload="metadata" />
                    
                    <Row gutter={[16, 16]} align="middle" style={{ marginBottom: 12 }}>
                        <Col flex="none">
                            <Button 
                                type="text" 
                                icon={isPlaying ? 
                                    React.createElement(PauseCircleOutlined) : 
                                    React.createElement(PlayCircleOutlined)}
                                onClick={togglePlay}
                                size="large"
                            />
                        </Col>
                        
                        <Col flex="auto">
                            <div style={{ marginBottom: 8 }}>
                                <Text type="secondary" style={{ fontSize: 12 }}>
                                    {formatTime(currentTime)} / {formatTime(duration)}
                                </Text>
                            </div>
                            <Slider
                                min={0}
                                max={duration || 100}
                                value={currentTime}
                                onChange={handleSeek}
                                disabled={!duration}
                            />
                        </Col>
                    </Row>
                    
                    <Divider style={{ margin: '12px 0' }} />
                    
                    <Row gutter={16} align="middle">
                        <Col span={6}>
                            <Text type="secondary">本地音量:</Text>
                        </Col>
                        <Col span={18}>
                            <Slider
                                min={0}
                                max={100}
                                value={localVolume * 100}
                                onChange={handleLocalVolumeChange}
                            />
                        </Col>
                    </Row>
                    
                    <Progress 
                        percent={actualVolumePercent} 
                        size="small" 
                        showInfo={false}
                        strokeColor={{
                            '0%': '#ff4d4f',
                            '50%': '#faad14',
                            '100%': '#52c41a',
                        }}
                    />
                </div>
            );
        };

        // 全局音量控制组件
        const GlobalVolumeControl = () => {
            const { globalVolume, updateGlobalVolume } = useGlobalVolume();
            
            return (
                <Card 
                    title="网页全局音量控制" 
                    style={{ marginBottom: 24 }}
                    extra={React.createElement(SoundOutlined)}
                >
                    <div className="volume-info">
                        <Paragraph>
                            {React.createElement(InfoCircleOutlined, { style: { marginRight: 8 } })}
                            网页音量会与系统音量叠加:最终音量 = 系统音量 × 网页音量
                        </Paragraph>
                    </div>
                    
                    <Row gutter={16} align="middle">
                        <Col span={4}>
                            <Text strong>{Math.round(globalVolume * 100)}%</Text>
                        </Col>
                        <Col span={20}>
                            <Slider
                                min={0}
                                max={100}
                                value={globalVolume * 100}
                                onChange={(value) => updateGlobalVolume(value / 100)}
                            />
                        </Col>
                    </Row>
                    
                    <div className="icon-fix-note">
                        图标问题已修复 - 使用正确的Ant Design图标引入方式
                    </div>
                </Card>
            );
        };

        // 倒计时组件
        const CountdownTimer = () => {
            const [timeLeft, setTimeLeft] = useState(2);
            const [isActive, setIsActive] = useState(false);
            
            useEffect(() => {
                let interval = null;
                
                if (isActive && timeLeft > 0) {
                    interval = setInterval(() => {
                        setTimeLeft(timeLeft - 1);
                    }, 1000);
                } else if (timeLeft === 0) {
                    setIsActive(false);
                }
                
                return () => clearInterval(interval);
            }, [isActive, timeLeft]);
            
            const startTimer = () => {
                setIsActive(true);
                setTimeLeft(2);
            };
            
            return (
                <Card title="倒计时提示音" style={{ marginBottom: 16 }}>
                    <Space direction="vertical" style={{ width: '100%' }}>
                        <Text>剩余时间: {timeLeft}秒</Text>
                        <Button 
                            type="primary" 
                            onClick={startTimer} 
                            disabled={isActive}
                        >
                            开始倒计时
                        </Button>
                        {timeLeft === 0 && (
                            <EnhancedAudioPlayer
                                // src="https://assets.mixkit.co/active_storage/sfx/250/250-preview.mp3"
                                src="https://developer.mozilla.org/shared-assets/audio/t-rex-roar.mp3"
                                title="倒计时结束提示"
                                autoPlay={true}
                                defaultLocalVolume={0.8}
                            />
                        )}
                    </Space>
                </Card>
            );
        };

        // 主应用组件
        const App = () => {
            const recordings = [
                { 
                    id: 1, 
                    title: '录音片段 1', 
                    src: 'https://assets.mixkit.co/active_storage/sfx/250/250-preview.mp3' 
                },
                { 
                    id: 2, 
                    title: '录音片段 2', 
                    src: 'https://assets.mixkit.co/active_storage/sfx/251/251-preview.mp3' 
                },
                { 
                    id: 3, 
                    title: '录音片段 3', 
                    src: 'https://assets.mixkit.co/active_storage/sfx/252/252-preview.mp3' 
                },
            ];
            
            return (
                <GlobalVolumeProvider>
                    <div className="container">
                        <Title level={2}>网页音量控制系统 - 图标问题已修复</Title>
                        
                        <Alert 
                            message="修复说明"
                            description="已解决antd.icons未定义错误:正确引入图标组件并使用兼容性处理"
                            type="success"
                            showIcon
                            style={{ marginBottom: 24 }}
                        />
                        
                        <GlobalVolumeControl />
                        
                        <Row gutter={[16, 16]}>
                            <Col span={24}>
                                <CountdownTimer />
                            </Col>
                            
                            <Col span={24}>
                                <Card title="通话与提示音" style={{ marginBottom: 16 }}>
                                    <Space direction="vertical" style={{ width: '100%' }}>
                                        <EnhancedAudioPlayer
                                            src="https://assets.mixkit.co/active_storage/sfx/253/253-preview.mp3"
                                            title="热线通话提示音"
                                            defaultLocalVolume={0.9}
                                        />
                                        <EnhancedAudioPlayer
                                            src="https://assets.mixkit.co/active_storage/sfx/254/254-preview.mp3"
                                            title="文字聊天提示音"
                                            defaultLocalVolume={0.6}
                                        />
                                    </Space>
                                </Card>
                            </Col>
                            
                            <Col span={24}>
                                <Card title="录音播放">
                                    <Space direction="vertical" style={{ width: '100%' }}>
                                        {recordings.map(recording => (
                                            <EnhancedAudioPlayer
                                                key={recording.id}
                                                src={recording.src}
                                                title={recording.title}
                                                defaultLocalVolume={1}
                                            />
                                        ))}
                                    </Space>
                                </Card>
                            </Col>
                        </Row>
                    </div>
                </GlobalVolumeProvider>
            );
        };

        // 渲染应用
        ReactDOM.render(<App />, document.getElementById('root'));
    </script>
</body>
</html>
View Code

 

问题:

1 静音时,需存一下音量,取消静音时,再恢复

2 新加音视频组件,都要注册一下

 

posted on 2025-11-16 12:27  joyful2  阅读(0)  评论(0)    收藏  举报

导航