Transfer-Encoding: chunked 实时音频播放
什么是 Transfer-Encoding: chunked
浏览器在大多数情况下是支持以 Transfer-Encoding 为 "chunked" 的方式传输的音频实时播放的。这种传输方式允许服务器将音频数据分块传输,而不是一次性发送整个音频文件。这样,浏览器可以在接收到部分音频数据后立即开始播放,而无需等待整个文件下载完成。
以下是一些支持实时播放的条件和注意事项:
HTTP/1.1 支持:Transfer-Encoding: chunked 是 HTTP/1.1 协议的一部分,绝大多数现代浏览器都支持。
媒体格式:浏览器需要支持所传输的音频格式,例如 MP3、AAC、OGG 等。
响应头:确保响应头中正确设置了 Transfer-Encoding: chunked。
流式处理:服务器需要以流的方式发送音频数据块,而不是一次性发送所有数据。
通过这种方式,浏览器会在接收到第一块音频数据后立即开始播放,从而实现实时播放的效果。
在 HTTP 协议中,"Transfer-Encoding: chunked" 是一种用于分块传输数据的编码方式。它允许服务器将响应体分成一系列较小的数据块(chunk),每个块都有其自己的大小标头。这种方法使得服务器可以在生成响应的同时发送数据,而无需在发送之前知道整个响应的大小。
Chunked 编码
Chunked 编码工作原理
在使用 chunked 编码传输数据时,响应体会被分成一系列数据块。每个数据块由以下部分组成:
- 块大小(十六进制表示)后跟 CRLF(回车换行)。
- 数据块本身。
- 另一个 CRLF。
最后,一个大小为 0 的块标志着数据传输的结束。结构如下:
<size in hex>\r\n
<data>\r\n
<size in hex>\r\n
<data>\r\n
...
0\r\n
\r\n
Chunked 编码示例
假设我们要传输以下字符串:"Hello, world! This is chunked encoding."
将字符串分块,例如我们分成两块:
- "Hello, world! "
- "This is chunked encoding."
计算每个块的大小并转换为十六进制:
- "Hello, world! " 的大小是 14(十进制),即 0E(十六进制)
- "This is chunked encoding." 的大小是 20(十进制),即 14(十六进制)
使用 chunked 编码格式传输:
0E\r\n
Hello, world! \r\n
14\r\n
This is chunked encoding.\r\n
0\r\n
\r\n
tio-boot 实时播放音频示例
- 服务端 返回
Transfer-Encoding: chunked
编码的 PCM 16kHz 音频文件 这里是 tio-boot - 要使用
curl
接收Transfer-Encoding: chunked
编码的 PCM 16kHz 音频文件, - 播放 将
curl
的输出重定向到一个播放程序。这可以通过在 Unix 系统上使用arecord
和aplay
来实现。arecord
用于录制音频,aplay
用于播放音频。 windows 使用ffplay
步骤
- 服务器端准备 确保服务器端能够以
chunked
编码方式返回 PCM 16kHz 音频数据。例如,使用 tio-boot 实现:
package com.litongjava.tio.web.hello.handler;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.core.ChannelContext;
import com.litongjava.tio.core.Tio;
import com.litongjava.tio.http.common.HeaderName;
import com.litongjava.tio.http.common.HeaderValue;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.encoder.ChunkEncoder;
import com.litongjava.tio.http.common.sse.ChunkedPacket;
import com.litongjava.tio.http.server.util.SseEmitter;
import com.litongjava.tio.utils.http.ContentTypeUtils;
import com.litongjava.tio.utils.hutool.ResourceUtil;
import com.litongjava.tio.utils.resp.RespBodyVo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AudioChunkHandler {
public HttpResponse tts(HttpRequest httpRequest) {
// 获取channelContext
ChannelContext channelContext = httpRequest.getChannelContext();
// 获取response
HttpResponse response = TioRequestContext.getResponse();
// 判断文件是否存在
URL resource = ResourceUtil.getResource("samples/Blowin_in_the_Wind-16k.pcm");
if (resource == null) {
response.fail(RespBodyVo.fail("Resource not found"));
return response;
}
// 文件扩展名,根据实际情况设置
String fileExt = "pcm";
String contentType = ContentTypeUtils.getContentType(fileExt);
// 设置为流式输出,这样不会计算content-length,because Content-Length can't be present with
// Transfer-Encoding
response.setStream(true);
// 设置响应头
response.addHeader(HeaderName.Transfer_Encoding, HeaderValue.from("chunked"));
response.addHeader(HeaderName.Content_Type, HeaderValue.from(contentType));
// 发送初始响应头,客户端会自动保持连接
Tio.send(channelContext, response);
// 打开文件
try (InputStream inputStream = resource.openStream()) {
// 读取文件并响应到客户端
byte[] buffer = new byte[1024 * 10];
int bytesRead;
int i = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
i++;
ChunkedPacket ssePacket = new ChunkedPacket(ChunkEncoder.encodeChunk(buffer, bytesRead));
Tio.send(channelContext, ssePacket);
log.info("sned:{}:{}", i, bytesRead);
}
// 发送结束标志,客户会手动关闭连接
ChunkedPacket endPacket = new ChunkedPacket(ChunkEncoder.encodeChunk(new byte[0]));
Tio.send(channelContext, endPacket);
SseEmitter.closeChunkConnection(channelContext);
} catch (IOException e) {
response.fail(RespBodyVo.fail("Failed to open resource:" + e.getMessage()));
return response;
}
//发送null
return null;
}
}
- 完整的源码地址 https://github.com/litongjava/java-ee-tio-boot-study/tree/main/tio-boot-latest-study/tio-boot-audio-chunked
- 客户端使用
curl
接收并播放音频
可以使用 curl
从服务器获取音频数据,并通过管道将其传递给 aplay
进行播放。以下是一个示例命令:
curl http://localhost/tts | aplay -f S16_LE -r 16000 -c 1
解释:
curl http://localhost/tts
从服务器获取音频数据。|
管道符号,将curl
的输出传递给aplay
。aplay -f S16_LE -r 16000 -c 1
使用aplay
播放音频,其中:-f S16_LE
表示音频格式为 16 位小端 (signed 16-bit little-endian) PCM。-r 16000
表示采样率为 16000 Hz。-c 1
表示单声道 (mono)。
Windows 平台
在 Windows 上,可以使用一些其他工具,如 ffplay
来播放音频。以下是一个示例:
安装 ffmpeg 需要安装 ffmpeg,
ffplay
是 ffmpeg 的一部分。使用
curl
和ffplay
播放音频
先测试保存到文件播放
curl -o output.pcm http://localhost/tts
ffplay -f s16le -ar 16000 -ac 1 output.pcm
测试成功
curl http://localhost/tts | ffplay -f s16le -ar 16000 -ac 1 -
测试失败
解释:
curl http://localhost/tts
从服务器获取音频数据。|
管道符号,将curl
的输出传递给ffplay
。ffplay -f s16le -ar 16000 -ac 1 -
使用ffplay
播放音频,其中:-f s16le
表示音频格式为 16 位小端 PCM。-ar 16000
表示采样率为 16000 Hz。-ac 1
表示单声道。
通过这种方式,可以使用 curl
接收以 chunked
编码传输的 PCM 16kHz 音频数据并进行实时播放。
手动编写代码,实现播放客户端
使用 Go 编写一个程序,通过 HTTP 请求实时接收 PCM 音频流并播放它, go-audio-stream-player 开源地址https://github.com/litongjava/go-audio-stream-player
go-audio-stream-player -f s16le -ar 16000 -ac 1 http://localhost/tts