Browse Source

优化转码逻辑

main
wang 3 weeks ago
parent
commit
d33fdec960
  1. 16
      src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java
  2. 108
      src/main/java/com/threecloud/dataserviceyy/util/G729Converter.java

16
src/main/java/com/threecloud/dataserviceyy/service/VaaSyncService.java

@ -250,15 +250,17 @@ public class VaaSyncService {
cityCode, fileName, localFile.length()); cityCode, fileName, localFile.length());
} else if (g729Converter.isG729(localFile)) { } else if (g729Converter.isG729(localFile)) {
try { try {
transcodedFile = new File(localFile.getParentFile(), localFile.getName() + ".pcm.wav"); File cachedTranscodedFile = new File(
if (transcodedFile.isFile()) { localFile.getParentFile(), localFile.getName() + ".pcm.wav");
try { if (cachedTranscodedFile.isFile()) {
vaaHttpUtil.validateAudioFile(transcodedFile); if (g729Converter.isValidPcmWav(cachedTranscodedFile)) {
transcodedFile = cachedTranscodedFile;
logger.info("【录音】复用上次已完成的PCM转码文件: {} ({} 字节)", logger.info("【录音】复用上次已完成的PCM转码文件: {} ({} 字节)",
fileName, transcodedFile.length()); fileName, transcodedFile.length());
} catch (Exception invalidCache) { } else {
Files.deleteIfExists(transcodedFile.toPath()); logger.warn("【录音】删除无效的历史PCM转码文件并重新转码: {} ({} 字节)",
transcodedFile = null; cachedTranscodedFile.getAbsolutePath(), cachedTranscodedFile.length());
Files.deleteIfExists(cachedTranscodedFile.toPath());
} }
} }
if (transcodedFile == null) { if (transcodedFile == null) {

108
src/main/java/com/threecloud/dataserviceyy/util/G729Converter.java

@ -6,12 +6,13 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -75,10 +76,17 @@ public class G729Converter {
throw new IOException("待转码文件不存在: " + (src == null ? "null" : src.getPath())); throw new IOException("待转码文件不存在: " + (src == null ? "null" : src.getPath()));
} }
File dest = new File(src.getParentFile(), src.getName() + ".pcm.wav"); File dest = new File(src.getParentFile(), src.getName() + ".pcm.wav");
File tempDest = new File(dest.getAbsolutePath() + ".part");
File ffmpegLog = new File(dest.getAbsolutePath() + ".ffmpeg.log");
Files.deleteIfExists(tempDest.toPath());
Files.deleteIfExists(ffmpegLog.toPath());
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add(resolveFfmpegPath()); command.add(resolveFfmpegPath());
command.add("-y"); command.add("-y");
command.add("-nostdin");
// 部分录音盒生成的 G.729 WAV 中 fact.sample_count 填的是压缩字节数,
// 与实际采样数不一致。FFmpeg 会按数据流解码,这里不依赖该字段计算输出。
command.add("-i"); command.add("-i");
command.add(src.getAbsolutePath()); command.add(src.getAbsolutePath());
command.add("-ar"); command.add("-ar");
@ -87,40 +95,108 @@ public class G729Converter {
command.add("1"); command.add("1");
command.add("-c:a"); command.add("-c:a");
command.add("pcm_s16le"); command.add("pcm_s16le");
command.add(dest.getAbsolutePath()); command.add("-f");
command.add("wav");
command.add(tempDest.getAbsolutePath());
logger.info("执行 ffmpeg 转码: {}", String.join(" ", command)); logger.info("执行 ffmpeg 转码: {}", String.join(" ", command));
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); pb.redirectErrorStream(true);
// FFmpeg 日志可能较多,必须在进程运行期间持续排出,否则管道写满会导致转码卡死。
pb.redirectOutput(ffmpegLog);
Process process = pb.start(); Process process = pb.start();
boolean finished = process.waitFor(TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS); boolean finished = process.waitFor(TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!finished) { if (!finished) {
process.destroyForcibly(); process.destroyForcibly();
Files.deleteIfExists(tempDest.toPath());
throw new IOException("ffmpeg 转码超时(>" + TRANSCODE_TIMEOUT_SECONDS + "s): " + src.getName()); throw new IOException("ffmpeg 转码超时(>" + TRANSCODE_TIMEOUT_SECONDS + "s): " + src.getName());
} }
// 进程已结束,读取输出用于日志与错误排查
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append('\n');
}
}
int exitCode = process.exitValue(); int exitCode = process.exitValue();
if (exitCode != 0) { if (exitCode != 0) {
String err = output.length() > 500 ? output.substring(output.length() - 500) : output.toString(); String err = readLogTail(ffmpegLog, 1000);
Files.deleteIfExists(tempDest.toPath());
throw new IOException("ffmpeg 转码失败(exit=" + exitCode + "): " + src.getName() + " -> " + err); throw new IOException("ffmpeg 转码失败(exit=" + exitCode + "): " + src.getName() + " -> " + err);
} }
if (!dest.isFile() || dest.length() == 0) { if (!isValidPcmWav(tempDest)) {
throw new IOException("ffmpeg 转码未生成有效文件: " + dest.getAbsolutePath()); Files.deleteIfExists(tempDest.toPath());
throw new IOException("ffmpeg 转码结果不是有效PCM WAV: " + tempDest.getAbsolutePath()
+ ", 日志=" + readLogTail(ffmpegLog, 1000));
}
try {
Files.move(tempDest.toPath(), dest.toPath(),
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tempDest.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(ffmpegLog.toPath());
} }
return dest; return dest;
} }
/** 判断文件是否为具有非空 data 块的标准 PCM WAV,防止复用上次遗留的半成品。 */
public boolean isValidPcmWav(File file) {
if (file == null || !file.isFile() || file.length() <= 44) {
return false;
}
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
if (raf.length() < 12) {
return false;
}
byte[] riff = new byte[12];
raf.readFully(riff);
if (!(riff[0] == 'R' && riff[1] == 'I' && riff[2] == 'F' && riff[3] == 'F'
&& riff[8] == 'W' && riff[9] == 'A' && riff[10] == 'V' && riff[11] == 'E')) {
return false;
}
boolean pcm = false;
boolean hasAudioData = false;
long offset = 12;
while (offset + 8 <= raf.length()) {
raf.seek(offset);
byte[] header = new byte[8];
raf.readFully(header);
String chunkId = new String(header, 0, 4, StandardCharsets.US_ASCII);
long chunkSize = littleEndianUInt32(header, 4);
long dataOffset = offset + 8;
if (chunkSize > raf.length() - dataOffset) {
return false;
}
if ("fmt ".equals(chunkId) && chunkSize >= 2) {
raf.seek(dataOffset);
pcm = (raf.readUnsignedByte() | (raf.readUnsignedByte() << 8)) == 1;
} else if ("data".equals(chunkId)) {
hasAudioData = chunkSize > 0;
}
offset = dataOffset + chunkSize + (chunkSize & 1L);
}
return pcm && hasAudioData;
} catch (IOException e) {
return false;
}
}
private long littleEndianUInt32(byte[] bytes, int offset) {
return (bytes[offset] & 0xFFL)
| ((bytes[offset + 1] & 0xFFL) << 8)
| ((bytes[offset + 2] & 0xFFL) << 16)
| ((bytes[offset + 3] & 0xFFL) << 24);
}
private String readLogTail(File file, int maxChars) {
if (file == null || !file.isFile()) {
return "";
}
try {
byte[] bytes = Files.readAllBytes(file.toPath());
String text = new String(bytes, StandardCharsets.UTF_8);
return text.length() > maxChars ? text.substring(text.length() - maxChars) : text;
} catch (IOException e) {
return "读取FFmpeg日志失败: " + e.getMessage();
}
}
/** /**
* 根据配置解析 ffmpeg 可执行文件路径 * 根据配置解析 ffmpeg 可执行文件路径
* *

Loading…
Cancel
Save