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

@ -179,7 +179,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
String stagingUrl = storeStagingAudio(audioFile); String stagingUrl = storeStagingAudio(audioFile);
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl); String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> { taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meeting.setAudioUrl(relocatedUrl); meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl);
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null); meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED);
@ -248,7 +248,7 @@ public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterServ
String stagingUrl = storeStagingAudio(audioFile); String stagingUrl = storeStagingAudio(audioFile);
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl); String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> { taskSecurityContextRunner.runAsTenantUser(meeting.getTenantId(), meeting.getCreatorId(), () -> {
meeting.setAudioUrl(relocatedUrl); meetingDomainSupport.applyMeetingAudioMetadata(meeting, relocatedUrl);
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS); meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
meeting.setAudioSaveMessage(null); meeting.setAudioSaveMessage(null);
meeting.setOfflineRecordingStatus(MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED); 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.AudioInputStream;
import javax.sound.sampled.AudioSystem; import javax.sound.sampled.AudioSystem;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@ -35,6 +37,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -55,6 +58,9 @@ public class MeetingDomainSupport {
@Value("${unisbase.app.upload-path}") @Value("${unisbase.app.upload-path}")
private String uploadPath; private String uploadPath;
@Value("${imeeting.audio.ffmpeg-path:ffmpeg}")
private String ffmpegPath;
public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags, public Meeting initMeeting(String title, LocalDateTime meetingTime, String participants, String tags,
String audioUrl, String meetingType, String meetingSource, String audioUrl, String meetingType, String meetingSource,
Long tenantId, Long creatorId, String creatorName, Long tenantId, Long creatorId, String creatorName,
@ -526,11 +532,11 @@ public class MeetingDomainSupport {
} }
private Integer resolveAudioDurationSecondsByUrl(String audioUrl) { private Integer resolveAudioDurationSecondsByUrl(String audioUrl) {
try {
Path audioPath = resolvePublicAudioPath(audioUrl); Path audioPath = resolvePublicAudioPath(audioUrl);
if (audioPath == null) { if (audioPath == null) {
return null; return null;
} }
try {
File file = audioPath.toFile(); File file = audioPath.toFile();
if (!file.exists()) { if (!file.exists()) {
return null; return null;
@ -544,9 +550,70 @@ public class MeetingDomainSupport {
return (int) Math.ceil(frameLength / frameRate); return (int) Math.ceil(frameLength / frameRate);
} }
} catch (Exception ex) { } 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; 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) { private Path resolvePublicAudioPath(String audioUrl) {

View File

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

View File

@ -37,4 +37,4 @@ imeeting:
h5: h5:
base-url: ${IMEETING_H5_BASE_URL:http://127.0.0.1:3000} base-url: ${IMEETING_H5_BASE_URL:http://127.0.0.1:3000}
audio: 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