网页全局音量控制-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> ) }
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>
问题:
1 静音时,需存一下音量,取消静音时,再恢复
2 新加音视频组件,都要注册一下
浙公网安备 33010602011771号