fix: use html audio to load stream

This commit is contained in:
jialin
2026-03-17 16:35:55 +08:00
committed by jialin
parent c29ad1270b
commit 9aae2c7f50
17 changed files with 1000 additions and 385 deletions
+1 -1
View File
@@ -189,4 +189,4 @@ const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
);
};
export default React.memo(AudioAnimation);
export default AudioAnimation;
@@ -18,8 +18,9 @@ const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
// ========================================================
const initAudioContext = useCallback(() => {
audioContext.current = new (window.AudioContext ||
window.webkitAudioContext)();
audioContext.current = new (
window.AudioContext || (window as any).webkitAudioContext
)();
analyser.current = audioContext.current.createAnalyser();
analyser.current.fftSize = 512;
@@ -52,7 +53,9 @@ const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
});
audioRef.current.addEventListener('timeupdate', () => {
const current = audioRef.current?.currentTime || 0;
props.onTimeUpdate?.();
props.onAudioProcess?.(current);
});
audioRef.current.addEventListener('ended', () => {
@@ -85,11 +88,6 @@ const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
props.onVolumeChange?.();
});
audioRef.current.addEventListener('audioprocess', () => {
const current = audioRef.current?.currentTime || 0;
props.onAudioProcess?.(current);
});
audioRef.current.addEventListener('playing', () => {
props.onPlaying?.();
});
@@ -111,16 +109,44 @@ const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
play: () => {
audioRef.current?.play();
if (audioRef.current) {
// If playback has ended, reset to beginning
if (audioRef.current.currentTime >= audioRef.current.duration) {
audioRef.current.currentTime = 0;
}
audioRef.current.play();
}
},
pause: () => {
audioRef.current?.pause();
},
seekTo: (ratio: number) => {
if (audioRef.current && audioRef.current.duration) {
audioRef.current.currentTime = ratio * audioRef.current.duration;
}
},
// Compatibility layer for wavesurfer interface
wavesurfer: {
current: {
play: () => {
if (audioRef.current) {
// If playback has ended, reset to beginning
if (audioRef.current.currentTime >= audioRef.current.duration) {
audioRef.current.currentTime = 0;
}
return audioRef.current.play();
}
return Promise.resolve();
},
isPlaying: () => {
return audioRef.current ? !audioRef.current?.paused : false;
}
}
}
}));
useEffect(() => {
if (audioRef.current) {
console.log('audioRef.current', audioRef.current, props.url);
initEnvents();
}
return () => {
@@ -138,7 +164,6 @@ const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
audioRef.current?.removeEventListener('seeked', () => {});
audioRef.current?.removeEventListener('seeking', () => {});
audioRef.current?.removeEventListener('volumechange', () => {});
audioRef.current?.removeEventListener('audioprocess', () => {});
audioRef.current?.removeEventListener('playing', () => {});
audioRef.current?.removeEventListener('loadedmetadata', () => {});
audioRef.current?.removeEventListener('ended', () => {});
@@ -146,6 +171,13 @@ const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
};
}, [audioRef.current]);
// Reload audio when URL changes
useEffect(() => {
if (audioRef.current && props.url) {
audioRef.current.load();
}
}, [props.url]);
return (
<audio
controls
@@ -36,8 +36,9 @@ const AudioPlayer: React.FC<
const mediaElement = useRef<any>(null);
const initAudioContext = useCallback(() => {
audioContext.current = new (window.AudioContext ||
window.webkitAudioContext)();
audioContext.current = new (
window.AudioContext || window.webkitAudioContext
)();
analyser.current = audioContext.current.createAnalyser();
analyser.current.fftSize = 512;
@@ -218,4 +219,4 @@ const AudioPlayer: React.FC<
);
});
export default React.memo(AudioPlayer);
export default AudioPlayer;
+22 -4
View File
@@ -4,16 +4,34 @@ import SpeechItem from './speech-item';
interface SpeechContentProps {
dataList: any[];
loading?: boolean;
onPlay?: () => void;
onPause?: () => void;
playerRef?: React.RefObject<any>;
isPlaying?: boolean;
isStream?: boolean;
analyserData?: {
data: Uint8Array;
analyser: any;
};
}
const SpeechContent: React.FC<SpeechContentProps> = (props) => {
return (
<>
<div>
{props.dataList.map((item) => (
<SpeechItem key={item.uid} {...item} />
<SpeechItem
key={item.uid}
{...item}
isStream={props.isStream}
isPlaying={props.isPlaying}
onPlay={props.onPlay}
onPause={props.onPause}
playerRef={props.playerRef}
analyserData={props.analyserData}
/>
))}
</>
</div>
);
};
export default React.memo(SpeechContent);
export default SpeechContent;
+173 -110
View File
@@ -8,8 +8,14 @@ import { useIntl } from '@umijs/max';
import { Button, Slider, Tooltip } from 'antd';
import dayjs from 'dayjs';
import _, { throttle } from 'lodash';
import React, { useCallback, useRef, useState } from 'react';
import AudioPlayer from './audio-player';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import RawAudioPlayer from '../audio-player/raw-audio-player';
import './styles/index.less';
import './styles/slider-progress.less';
@@ -34,10 +40,25 @@ interface SpeechContentProps {
format: string;
speed: number;
audioUrl: string;
onPlay?: () => void;
onPause?: () => void;
isStream?: boolean;
isPlaying?: boolean;
playerRef?: React.RefObject<any>;
analyserData?: {
data: Uint8Array;
analyser: any;
};
}
const SpeechItem: React.FC<SpeechContentProps> = (props) => {
const { isStream } = props;
console.log(
'Rendering SpeechItem with props:',
props.isStream,
props.analyserData
);
const intl = useIntl();
const [isPlay, setIsPlay] = useState(props.autoplay);
const [isPlay, setIsPlay] = useState(props.autoplay || props.isPlaying);
const [duration, setDuration] = useState<number>(0);
const [animationSize, setAnimationSize] = useState({ width: 900, height: 0 });
const [currentTime, setCurrentTime] = useState(0);
@@ -48,23 +69,47 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
const wrapper = useRef<any>(null);
const ref = useRef<any>(null);
const handlePlay = useCallback(async () => {
useEffect(() => {
setIsPlay(props.autoplay || props.isPlaying);
}, [props.autoplay, props.isPlaying]);
const isPCMStream = useMemo(() => {
return props.audioUrl?.startsWith('pcm-stream://');
}, [props.audioUrl]);
// Sync internal ref with external playerRef if provided
React.useEffect(() => {
if (props.playerRef) {
(props.playerRef as any).current = ref.current;
}
}, [props.playerRef, ref.current]);
const onPause = () => {
ref.current?.pause();
props.onPause?.();
};
const onPlay = async () => {
await ref.current?.wavesurfer.current?.play();
props.onPlay?.();
};
const handlePlay = async () => {
try {
if (ref.current?.wavesurfer.current?.isPlaying()) {
ref.current?.pause();
onPause();
setIsPlay(false);
return;
} else {
await ref.current?.wavesurfer.current?.play();
await onPlay();
setIsPlay(true);
}
} catch (error) {
console.log('error:', error);
}
}, [ref.current]);
};
const handleOnAnalyse = useCallback((data: any, analyser: any) => {
console.log('data+++++++++++++++++=:', data);
setAudioChunks((pre: any) => {
return {
data: data,
@@ -76,9 +121,11 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
const handleOnFinish = useCallback(() => {
setIsPlay(false);
}, []);
const handleOnPlay = useCallback(() => {
setIsPlay(true);
}, []);
const handleOnPause = useCallback(() => {
setIsPlay(false);
}, []);
@@ -87,22 +134,9 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
setCurrentTime(current);
}, 100);
const handleOnAudioprocess = useCallback(
(current: number) => {
console.log('current:', current);
throttleUpdateCurrentTime(current);
},
[throttleUpdateCurrentTime]
);
const handleReay = useCallback((duration: number) => {
setIsPlay(props.autoplay);
setDuration(duration);
}, []);
const handleOnClick = useCallback((value: number) => {
console.log('current:', value);
}, []);
const handleOnAudioprocess = (current: number) => {
throttleUpdateCurrentTime(current);
};
const handleAnimationResize = useCallback((size: any) => {
setAnimationSize({
@@ -121,7 +155,6 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
};
const handleReady = useCallback((duration: number) => {
console.log('ready+++++++++++++++', duration);
setDuration(duration);
}, []);
@@ -130,9 +163,16 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
setCurrentTime(value);
}, []);
const convertFormat = () => {
if (props.format === 'pcm') {
return 'wav';
}
return props.format;
};
const onDownload = useCallback(() => {
const url = props.audioUrl || '';
const filename = `audio-${dayjs().format('YYYYMMDDHHmmss')}.${props.format}`;
const filename = `audio-${dayjs().format('YYYYMMDDHHmmss')}.${convertFormat()}`;
const link = document.createElement('a');
link.href = url;
@@ -142,6 +182,71 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
link.remove();
}, [props.audioUrl, props.format]);
console.log('isPCMStream:', isPCMStream, isPlay);
console.log('props.analyserData:', props.analyserData);
const renderPlayerActions = () => {
if (isPCMStream) {
return null;
}
return (
<div>
<Slider
className="slider-progress"
value={currentTime}
max={duration}
step={0.01}
onChange={handleSliderChange}
></Slider>
<div className="speech-actions">
<span className="tags">
<span className="item">{props.format}</span>
</span>
<span className="duration">
{_.round(currentTime, 2) || _.round(duration, 2)}
</span>
<div className="actions">
<Tooltip
title={
isPlay
? intl.formatMessage({ id: 'playground.audio.button.stop' })
: intl.formatMessage({ id: 'playground.audio.button.play' })
}
>
<Button
disabled={!props.audioUrl || duration === 0}
onClick={handlePlay}
icon={
isPlay ? (
<PauseCircleOutlined className="font-size-16" />
) : (
<PlayCircleOutlined className="font-size-16" />
)
}
type="text"
size="small"
></Button>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id: 'playground.audio.button.download'
})}
>
<Button
disabled={!props.audioUrl || duration === 0}
onClick={onDownload}
icon={<DownloadOutlined className="font-size-16" />}
type="text"
size="small"
></Button>
</Tooltip>
</div>
</div>
</div>
);
};
return (
<div>
<div className="speech-item">
@@ -150,93 +255,51 @@ const SpeechItem: React.FC<SpeechContentProps> = (props) => {
style={{ height: 120, width: '100%' }}
ref={wrapper}
>
<AudioPlayer
{...props}
audioUrl={props.audioUrl}
onReady={handleReady}
onFinish={handleOnFinish}
onPlay={handleOnPlay}
onPause={handleOnPause}
onAnalyse={handleOnAnalyse}
onAudioprocess={handleOnAudioprocess}
ref={ref}
></AudioPlayer>
{/* <RawAudioPlayer
{...props}
url={props.audioUrl}
onReady={handleReady}
onEnded={handleOnFinish}
onPlay={handleOnPlay}
onPause={handleOnPause}
onAnalyse={handleOnAnalyse}
onAudioProcess={handleOnAudioprocess}
ref={ref}
></RawAudioPlayer> */}
{isPlay && (
<AudioAnimation
maxBarCount={100}
amplitude={60}
fixedHeight={true}
height={120}
width={800}
analyserData={audioChunks}
></AudioAnimation>
)}
</div>
</div>
<Slider
className="slider-progress"
value={currentTime}
max={duration}
step={0.01}
onChange={handleSliderChange}
></Slider>
<div className="speech-actions">
<span className="tags">
<span className="item">{props.format}</span>
</span>
<span className="duration">
{_.round(currentTime, 2) || _.round(duration, 2)}
</span>
<div className="actions">
<Tooltip
title={
isPlay
? intl.formatMessage({ id: 'playground.audio.button.stop' })
: intl.formatMessage({ id: 'playground.audio.button.play' })
}
>
<Button
disabled={!props.audioUrl || duration === 0}
onClick={handlePlay}
icon={
isPlay ? (
<PauseCircleOutlined className="font-size-16" />
) : (
<PlayCircleOutlined className="font-size-16" />
)
}
type="text"
size="small"
></Button>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id: 'playground.audio.button.download'
})}
>
<Button
disabled={!props.audioUrl || duration === 0}
onClick={onDownload}
icon={<DownloadOutlined className="font-size-16" />}
type="text"
size="small"
></Button>
</Tooltip>
<>
{/* {!isPCMStream && (
<AudioPlayer
{...props}
audioUrl={props.audioUrl}
onReady={handleReady}
onFinish={handleOnFinish}
onPlay={handleOnPlay}
onPause={handleOnPause}
onAnalyse={handleOnAnalyse}
onAudioprocess={handleOnAudioprocess}
ref={ref}
></AudioPlayer>
)} */}
{!isPCMStream && (
<RawAudioPlayer
{...props}
url={props.audioUrl}
onReady={handleReady}
onEnded={handleOnFinish}
onPlay={handleOnPlay}
onPause={handleOnPause}
onAnalyse={handleOnAnalyse}
onAudioProcess={handleOnAudioprocess}
ref={ref}
></RawAudioPlayer>
)}
{isPlay &&
(props.analyserData?.analyser?.current ||
audioChunks.analyser?.current) && (
<AudioAnimation
maxBarCount={100}
amplitude={60}
fixedHeight={true}
height={120}
width={800}
analyserData={props.analyserData || audioChunks}
></AudioAnimation>
)}
</>
</div>
</div>
{renderPlayerActions()}
</div>
);
};
export default React.memo(SpeechItem);
export default SpeechItem;