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