fix: 修正 ffmpeg 路径和音频处理逻辑

- 更新 `application-dev.yml` 中的 `ffmpeg-path` 配置
- 在 `SpeakerServiceImpl` 中添加 `StandardCharsets.UTF_8` 编码
- 优化 `LegacyMeetingAdapterServiceImpl` 和 `MeetingDomainSupport` 中的音频处理逻辑
dev_na
chenhao 2026-06-09 09:22:09 +08:00
parent e1e321a86d
commit d47a66febd
4 changed files with 91 additions and 21 deletions

View File

@ -178,19 +178,19 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
String stagingUrl = storeStagingAudio(audioFile);
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meeting.setAudioUrl(relocatedUrl);
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED);
meeting.setStatus(1);
meetingService.updateById(meeting);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl);
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED);
meeting.setStatus(1);
meetingService.updateById(meeting);
resetOrCreateAsrTask(meetingId, profile);
resetOrCreateChapterTask(meetingId, profile);
resetOrCreateSummaryTask(meetingId, profile);
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
});
resetOrCreateAsrTask(meetingId, profile);
resetOrCreateChapterTask(meetingId, profile);
resetOrCreateSummaryTask(meetingId, profile);
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
});
return new LegacyUploadAudioResponse(meetingId, relocatedUrl);
}
@ -248,7 +248,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
String stagingUrl = storeStagingAudio(audioFile);
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meeting.setAudioUrl(relocatedUrl);
meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl);
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED);

View File

@ -24,6 +24,8 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -35,6 +37,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
import java.util.stream.Collectors;
@ -55,6 +58,9 @@ public class MeetingDomainSupport {
@Value("${unisbase.app.upload-path}")
private String uploadPath;
@Value("${imeeting.audio.ffmpeg-path:ffmpeg}")
private String ffmpegPath;
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
String audioUrl, String meetingType, String meetingSource,
Long tenantId, Long creatorId, String creatorName,
@ -526,11 +532,11 @@ public class MeetingDomainSupport {
}
private Integer resolveAudioDurationSecondsByUrl(String audioUrl) {
Path audioPath = resolvePublicAudioPath(audioUrl);
if (audioPath == null) {
return null;
}
try {
Path audioPath = resolvePublicAudioPath(audioUrl);
if (audioPath == null) {
return null;
}
File file = audioPath.toFile();
if (!file.exists()) {
return null;
@ -544,9 +550,70 @@ public class MeetingDomainSupport {
return (int) Math.ceil(frameLength / frameRate);
}
} catch (Exception ex) {
log.warn("Failed to resolve audio duration from audioUrl={}, skip effective duration update", audioUrl, ex);
log.warn("AudioSystem failed to resolve audio duration from audioUrl={}, fallback to ffprobe", audioUrl, ex);
}
return resolveAudioDurationSecondsByFfprobe(audioPath);
}
private Integer resolveAudioDurationSecondsByFfprobe(Path audioPath) {
if (audioPath == null || !Files.exists(audioPath)) {
return null;
}
List<String> command = List.of(
resolveFfprobePath(),
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
audioPath.toString()
);
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
byte[] output;
try (InputStream processStream = process.getInputStream()) {
output = processStream.readAllBytes();
}
if (!process.waitFor(30, TimeUnit.SECONDS)) {
process.destroyForcibly();
return null;
}
if (process.exitValue() != 0) {
return null;
}
String raw = new String(output, StandardCharsets.UTF_8).trim();
if (raw.isBlank()) {
return null;
}
double duration = Double.parseDouble(raw);
return duration > 0 ? (int) Math.ceil(duration) : null;
} catch (Exception ex) {
log.warn("ffprobe failed to resolve audio duration from path={}", audioPath, ex);
return null;
}
}
private String resolveFfprobePath() {
if (ffmpegPath == null || ffmpegPath.isBlank()) {
return "ffprobe";
}
String trimmed = ffmpegPath.trim();
try {
Path ffmpeg = Paths.get(trimmed);
Path fileName = ffmpeg.getFileName();
if (fileName != null) {
String normalizedName = fileName.toString().toLowerCase();
if ("ffmpeg".equals(normalizedName) || "ffmpeg.exe".equals(normalizedName)) {
Path sibling = ffmpeg.resolveSibling(normalizedName.endsWith(".exe") ? "ffprobe.exe" : "ffprobe");
if (Files.exists(sibling)) {
return sibling.toString();
}
}
}
} catch (Exception ex) {
log.debug("Failed to derive ffprobe path from ffmpegPath={}", ffmpegPath, ex);
}
return "ffprobe";
}
private Path resolvePublicAudioPath(String audioUrl) {

View File

@ -24,6 +24,7 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -51,6 +52,7 @@ public class SpeakerServiceImpl extends ServiceImpl<SpeakerMapper, Speaker> impl
private final AiModelService aiModelService;
private final ObjectMapper objectMapper;
private final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(10))
.build();
@ -245,12 +247,13 @@ public class SpeakerServiceImpl extends ServiceImpl<SpeakerMapper, Speaker> impl
if (speaker.getUserId() != null) {
body.put("user_id", String.valueOf(speaker.getUserId()));
}
body.put("file_url", buildFileUrl(speaker.getVoicePath()));
body.put("audio_address", buildFileUrl(speaker.getVoicePath()));
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)));
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body),
StandardCharsets.UTF_8));
if (asrModel.getApiKey() != null && !asrModel.getApiKey().isEmpty()) {
requestBuilder.header("Authorization", "Bearer " + asrModel.getApiKey());

View File

@ -37,4 +37,4 @@ imeeting:
h5:
base-url: ${IMEETING_H5_BASE_URL:http://127.0.0.1:3000}
audio:
ffmpeg-path: D:\tools\exe\ffmpeg-master-latest-win64-gpl-shared\bin\ffmpeg
ffmpeg-path: D:\tools\exe\ffmpeg-master-latest-win64-gpl-shared\bin\ffmpeg.exe