Spring服务音频流问题:Chrome需完整下载后才播放
音频流式播放问题解决方案
核心问题原因
- 响应头设置时机错误:代码先执行gRPC调用再设置响应头,导致浏览器无法提前获取音频类型、传输编码等关键元信息,必须等首字节到达才会处理,阻塞了流式播放触发。
- gRPC阻塞Stub导致数据缓冲:使用阻塞Stub会让当前线程等待数据,可能导致Spring的输出流缓冲数据,无法实时发送给客户端。
- WAV格式的局限性:标准WAV文件头包含总长度信息,浏览器需要完整头才能确定音频时长并开始播放,流式传输时无法提前拿到完整头。
具体修复步骤
1. 提前设置并刷新响应头
修改StreamRestController的stream方法,先设置所有响应头并强制刷新,确保浏览器优先拿到元信息:
@GetMapping public void stream(final HttpServletResponse response) throws Exception { // 先设置响应头,这是流式播放的前提 response.setStatus(200); response.setContentType("audio/wav"); response.addHeader("Cache-Control", "no-cache, no-store"); response.addHeader("Connection", "keep-alive"); response.addHeader("Content-Transfer-Encoding", "chunked"); response.addHeader("Accept-Ranges", "bytes"); // 强制刷新响应头,避免Spring缓冲 response.flushBuffer(); StreamObserverPublisher streamObserverPublisher = new StreamObserverPublisher(); final OutputStream os = response.getOutputStream(); streamObserverPublisher.subscribe(new Flow.Subscriber<>() { @Override public void onSubscribe(Flow.Subscription s) {} @Override public void onNext(byte[] bytes) { try { os.write(bytes); response.flushBuffer(); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void onError(Throwable t) { try { os.close(); } catch (IOException e) { throw new RuntimeException(e); } throw new RuntimeException(t); } @Override public void onComplete() { try { os.close(); } catch (IOException e) { throw new RuntimeException(e); } } }); // 最后执行gRPC调用 grpcService.synthesize(createRequest(), streamObserverPublisher); }
2. 改用gRPC异步Stub
替换阻塞Stub为异步Stub,确保数据一从gRPC服务端接收就立即发送给客户端,避免线程阻塞导致缓冲:
public class GrpcServive extends SmartSpeechGrpc.SmartSpeechImplBase { private final SslContext sslCtx; private final CallCredentials callCredentials; private final ManagedChannel channel; // 复用通道,不要每次创建 public GrpcServive() { try { sslCtx = GrpcSslContexts.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .build(); callCredentials = new BearerToken("token..."); // 初始化可复用的gRPC通道 channel = NettyChannelBuilder.forTarget("host...") .sslContext(sslCtx) .enableRetry() .build(); } catch (Exception e) { throw new RuntimeException(e); } } @Override public void synthesize(Synthesis.SynthesisRequest request, StreamObserver<Synthesis.SynthesisResponse> responseObserver ) { // 使用异步Stub SmartSpeechGrpc.SmartSpeechStub asyncStub = SmartSpeechGrpc.newStub(channel) .withCallCredentials(callCredentials) .withWaitForReady(); asyncStub.synthesize(request, new StreamObserver<>() { @Override public void onNext(Synthesis.SynthesisResponse response) { responseObserver.onNext(response); } @Override public void onError(Throwable throwable) { responseObserver.onError(throwable); } @Override public void onCompleted() { responseObserver.onCompleted(); } }); } // 销毁时关闭通道 @PreDestroy public void shutdownChannel() { channel.shutdown(); } }
3. 切换为流式友好的音频格式(关键优化)
WAV格式天生不适合流式播放,建议切换为MP3或Opus格式,浏览器对这类帧结构的音频支持边下载边播放:
- 修改请求的音频编码:
private Synthesis.SynthesisRequest createRequest() { return Synthesis.SynthesisRequest.newBuilder() .setText("One, two, three, four, five, six, seven, eight, nine, ten.") .setContentType(TEXT) .setVoice("Kin_8000") .setAudioEncoding(MP3) // 改为MP3 .build(); }
- 更新响应的Content-Type:
response.setContentType("audio/mpeg");
4. 调整客户端audio标签配置
将preload设为metadata,让浏览器仅加载元数据,而非预加载整个文件:
<audio controls preload="metadata"> <source src="/api/stream" type="audio/mpeg"> </audio>
额外优化
关闭Spring和Tomcat的输出缓冲,确保数据实时发送:
在application.properties中添加:
server.servlet.output-buffer-size=0 server.tomcat.output-buffer-size=0
内容的提问来源于stack exchange,提问作者Anton Danilov
相关产品推荐
相关产品推荐

