From d47a66febdce294a5c1d9dae8f48211a51b9edba Mon Sep 17 00:00:00 2001 From: chenhao Date: Tue, 9 Jun 2026 09:22:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20ffmpeg=20=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=92=8C=E9=9F=B3=E9=A2=91=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 `application-dev.yml` 中的 `ffmpeg-path` 配置 - 在 `SpeakerServiceImpl` 中添加 `StandardCharsets.UTF_8` 编码 - 优化 `LegacyMeetingAdapterServiceImpl` 和 `MeetingDomainSupport` 中的音频处理逻辑 --- .../impl/LegacyMeetingAdapterServiceImpl.java | 26 +++---- .../biz/impl/MeetingDomainSupport.java | 77 +++++++++++++++++-- .../service/biz/impl/SpeakerServiceImpl.java | 7 +- .../src/main/resources/application-dev.yml | 2 +- 4 files changed, 91 insertions(+), 21 deletions(-) diff --git a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java index df0d6fc..b45ec47 100644 --- a/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/android/legacy/impl/LegacyMeetingAdapterServiceImpl.java @@ -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); diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java index 385c1f3..2a8edc3 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingDomainSupport.java @@ -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 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) { diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java index 8526b60..d915e09 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/SpeakerServiceImpl.java @@ -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 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 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()); diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index 3053b45..921b8ca 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -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