You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring服务音频流问题:Chrome需完整下载后才播放

音频流式播放问题解决方案

核心问题原因

  1. 响应头设置时机错误:代码先执行gRPC调用再设置响应头,导致浏览器无法提前获取音频类型、传输编码等关键元信息,必须等首字节到达才会处理,阻塞了流式播放触发。
  2. gRPC阻塞Stub导致数据缓冲:使用阻塞Stub会让当前线程等待数据,可能导致Spring的输出流缓冲数据,无法实时发送给客户端。
  3. 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格式,浏览器对这类帧结构的音频支持边下载边播放:

  1. 修改请求的音频编码:
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();
}
  1. 更新响应的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:04:50