refactor(service): 修复音频无法播放问题
parent
e3c688b9ce
commit
22aefae6cf
|
|
@ -784,6 +784,7 @@
|
|||
}
|
||||
|
||||
.meeting-detail-page-v2 .transcript-player.transcript-player--embedded {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
|
@ -825,6 +826,14 @@
|
|||
transform: none;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .meeting-detail-hidden-audio {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.meeting-detail-page-v2 .transcript-player .player-ghost-btn {
|
||||
height: 36px !important;
|
||||
align-self: center;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Alert, Avatar, Button, Card, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Radio, Row, Select, Skeleton, Space, Switch, Tag, Typography, App, Dropdown } from 'antd';
|
||||
import {
|
||||
|
|
@ -37,6 +37,7 @@ import {
|
|||
MeetingTranscriptVO,
|
||||
MeetingVO,
|
||||
reSummary,
|
||||
resolveAudioMimeType,
|
||||
resolveMeetingPlaybackAudioUrl,
|
||||
retryMeetingChapter,
|
||||
retryMeetingSummary,
|
||||
|
|
@ -1251,6 +1252,7 @@ const MeetingDetail: React.FC = () => {
|
|||
|
||||
const emptyTranscriptNoticeShownRef = useRef<number | null>(null);
|
||||
const audioPlaybackErrorShownRef = useRef<string | null>(null);
|
||||
const loadedAudioUrlRef = useRef<string | null>(null);
|
||||
|
||||
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||
const pendingTranscriptScrollIdRef = useRef<number | null>(null);
|
||||
|
|
@ -1313,6 +1315,7 @@ const MeetingDetail: React.FC = () => {
|
|||
);
|
||||
const previewAccessPassword = useMemo(() => (meeting?.accessPassword || '').trim(), [meeting?.accessPassword]);
|
||||
const playbackAudioUrl = useMemo(() => resolveMeetingPlaybackAudioUrl(meeting), [meeting]);
|
||||
const audioMimeType = useMemo(() => resolveAudioMimeType(playbackAudioUrl), [playbackAudioUrl]);
|
||||
const audioDownloadFileName = useMemo(() => getMeetingAudioDownloadName(meeting), [meeting]);
|
||||
const audioDownloadFormatLabel = useMemo(
|
||||
() => resolveAudioExtension(playbackAudioUrl).toUpperCase(),
|
||||
|
|
@ -1760,26 +1763,28 @@ const MeetingDetail: React.FC = () => {
|
|||
setSummaryVisible(true);
|
||||
};
|
||||
|
||||
const handleAudioPlaybackError = useCallback(() => {
|
||||
const handleAudioPlaybackError = useCallback((error?: unknown) => {
|
||||
const currentAudioUrl = playbackAudioUrl || '';
|
||||
if (!currentAudioUrl || audioPlaybackErrorShownRef.current === currentAudioUrl) {
|
||||
return;
|
||||
}
|
||||
const normalizedUrl = currentAudioUrl.split('#')[0]?.split('?')[0]?.toLowerCase() || '';
|
||||
const isM4a = normalizedUrl.endsWith('.m4a');
|
||||
message.warning(
|
||||
isM4a
|
||||
? '当前 m4a 文件在本机浏览器中无法直接播放。已确认文件与服务端响应基本正常,更可能是浏览器对该录音参数或容器实现的兼容性问题。建议优先使用 mp3、wav,或下载到本地播放。'
|
||||
: '当前音频文件无法播放,请检查文件是否损坏或格式是否兼容。',
|
||||
);
|
||||
const audio = audioRef.current;
|
||||
console.warn('音频播放失败', {
|
||||
audioUrl: currentAudioUrl,
|
||||
error,
|
||||
mediaError: audio?.error ? {code: audio.error.code, message: audio.error.message} : null,
|
||||
readyState: audio?.readyState,
|
||||
networkState: audio?.networkState,
|
||||
currentSrc: audio?.currentSrc,
|
||||
});
|
||||
audioPlaybackErrorShownRef.current = currentAudioUrl;
|
||||
setAudioPlaying(false);
|
||||
}, [message, playbackAudioUrl]);
|
||||
}, [playbackAudioUrl]);
|
||||
|
||||
const seekTo = useCallback((timeMs: number) => {
|
||||
if (!audioRef.current) return;
|
||||
audioRef.current.currentTime = timeMs / 1000;
|
||||
audioRef.current.play().catch(() => handleAudioPlaybackError());
|
||||
audioRef.current.play().catch((error) => handleAudioPlaybackError(error));
|
||||
}, [handleAudioPlaybackError]);
|
||||
|
||||
const handleTranscriptRowPlay = useCallback((timeMs: number) => {
|
||||
|
|
@ -1986,50 +1991,34 @@ const MeetingDetail: React.FC = () => {
|
|||
transcriptItemRefs.current[transcriptId] = node;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return undefined;
|
||||
const syncAudioMetadata = useCallback((audio: HTMLAudioElement) => {
|
||||
setAudioDuration(Number.isFinite(audio.duration) ? audio.duration : 0);
|
||||
setAudioCurrentTime(audio.currentTime || 0);
|
||||
audio.playbackRate = audioPlaybackRate;
|
||||
}, [audioPlaybackRate]);
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
setAudioDuration(Number.isFinite(audio.duration) ? audio.duration : 0);
|
||||
setAudioCurrentTime(audio.currentTime || 0);
|
||||
audio.playbackRate = audioPlaybackRate;
|
||||
};
|
||||
const handleTimeUpdate = () => {
|
||||
setAudioCurrentTime(audio.currentTime || 0);
|
||||
};
|
||||
const handlePlay = () => setAudioPlaying(true);
|
||||
const handlePause = () => setAudioPlaying(false);
|
||||
const handleEnded = () => setAudioPlaying(false);
|
||||
const handleError = () => handleAudioPlaybackError();
|
||||
const handleAudioElement = useCallback((node: HTMLAudioElement | null) => {
|
||||
if (!node) return;
|
||||
// @ts-ignore
|
||||
audioRef.current = node;
|
||||
|
||||
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
audio.addEventListener('durationchange', handleLoadedMetadata);
|
||||
audio.addEventListener('canplay', handleLoadedMetadata);
|
||||
audio.addEventListener('timeupdate', handleTimeUpdate);
|
||||
audio.addEventListener('play', handlePlay);
|
||||
audio.addEventListener('pause', handlePause);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
audio.addEventListener('error', handleError);
|
||||
|
||||
handleLoadedMetadata();
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
audio.removeEventListener('durationchange', handleLoadedMetadata);
|
||||
audio.removeEventListener('canplay', handleLoadedMetadata);
|
||||
audio.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
audio.removeEventListener('play', handlePlay);
|
||||
audio.removeEventListener('pause', handlePause);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
audio.removeEventListener('error', handleError);
|
||||
};
|
||||
}, [audioPlaybackRate, meeting?.status, playbackAudioUrl, handleAudioPlaybackError]);
|
||||
node.playbackRate = audioPlaybackRate;
|
||||
if (playbackAudioUrl && loadedAudioUrlRef.current !== playbackAudioUrl) {
|
||||
loadedAudioUrlRef.current = playbackAudioUrl;
|
||||
node.load();
|
||||
}
|
||||
syncAudioMetadata(node);
|
||||
}, [audioPlaybackRate, playbackAudioUrl, syncAudioMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
audioPlaybackErrorShownRef.current = null;
|
||||
|
||||
if (!audio) {
|
||||
loadedAudioUrlRef.current = null;
|
||||
return;
|
||||
}
|
||||
loadedAudioUrlRef.current = playbackAudioUrl || null;
|
||||
audio.pause();
|
||||
audio.load();
|
||||
audio.playbackRate = 1;
|
||||
|
|
@ -2039,12 +2028,23 @@ const MeetingDetail: React.FC = () => {
|
|||
setAudioPlaying(false);
|
||||
}, [playbackAudioUrl]);
|
||||
|
||||
const handleAudioLoadedMetadata = (event: React.SyntheticEvent<HTMLAudioElement>) => {
|
||||
syncAudioMetadata(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleAudioTimeUpdate = (event: React.SyntheticEvent<HTMLAudioElement>) => {
|
||||
setAudioCurrentTime(event.currentTarget.currentTime || 0);
|
||||
};
|
||||
|
||||
const toggleAudioPlayback = () => {
|
||||
if (!audioRef.current) return;
|
||||
if (audioRef.current.paused) {
|
||||
audioRef.current.play().catch(() => handleAudioPlaybackError());
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
if (audio.paused) {
|
||||
audio.muted = false;
|
||||
audio.volume = 1;
|
||||
audio.play().catch((error) => handleAudioPlaybackError(error));
|
||||
} else {
|
||||
audioRef.current.pause();
|
||||
audio.pause();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -2744,10 +2744,6 @@ const MeetingDetail: React.FC = () => {
|
|||
style={{ height: '100%' }}
|
||||
styles={{ body: { padding: 0, height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 } }}
|
||||
>
|
||||
{playbackAudioUrl && (
|
||||
<audio ref={audioRef} src={playbackAudioUrl} style={{ display: 'none' }} preload="auto" />
|
||||
)}
|
||||
|
||||
<div className="transcript-panel-header">
|
||||
<div className="transcript-stage-tabs" role="tablist" aria-label="内容切换">
|
||||
{aiCatalogEnabled && (
|
||||
|
|
@ -2871,7 +2867,28 @@ const MeetingDetail: React.FC = () => {
|
|||
</div>
|
||||
{playbackAudioUrl && (
|
||||
<div className="transcript-player transcript-player--embedded">
|
||||
<button type="button" className="player-main-btn" onClick={toggleAudioPlayback} aria-label="toggle-audio">
|
||||
<audio
|
||||
ref={handleAudioElement}
|
||||
className="meeting-detail-hidden-audio"
|
||||
preload="metadata"
|
||||
playsInline
|
||||
onLoadedMetadata={handleAudioLoadedMetadata}
|
||||
onDurationChange={handleAudioLoadedMetadata}
|
||||
onCanPlay={handleAudioLoadedMetadata}
|
||||
onTimeUpdate={handleAudioTimeUpdate}
|
||||
onPlay={() => setAudioPlaying(true)}
|
||||
onPause={() => setAudioPlaying(false)}
|
||||
onEnded={() => setAudioPlaying(false)}
|
||||
onError={(event) => handleAudioPlaybackError(event.currentTarget.error)}
|
||||
>
|
||||
<source src={playbackAudioUrl} type={audioMimeType}/>
|
||||
</audio>
|
||||
<button
|
||||
type="button"
|
||||
className="player-main-btn"
|
||||
onClick={toggleAudioPlayback}
|
||||
aria-label="toggle-audio"
|
||||
>
|
||||
{audioPlaying ? <PauseOutlined /> : <CaretRightFilled />}
|
||||
</button>
|
||||
<div className="player-progress-shell">
|
||||
|
|
|
|||
Loading…
Reference in New Issue