要实现一个Spring项目,将一个正在追加内容的录音文件以字节流的方式返回给前端,你可以使用传统的Servlet方式。以下是一个实现方案:
后端代码
- 创建一个控制器来处理文件流的请求。
- package com.example.demo.controller;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- private final File audioFile = new File("path/to/your/audio/file.wav");
-
- @GetMapping("/stream")
- public void streamAudio(HttpServletResponse response) throws IOException {
- response.setContentType("audio/wav");
- response.setHeader("Transfer-Encoding", "chunked");
-
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
- OutputStream outputStream = response.getOutputStream()) {
- byte[] buffer = new byte[1024];
- long filePointer = 0;
-
- while (true) {
- long fileLength = randomAccessFile.length();
- if (fileLength > filePointer) {
- randomAccessFile.seek(filePointer);
- int bytesRead;
- while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- filePointer += bytesRead;
- }
- outputStream.flush();
- }
-
- try {
- Thread.sleep(2000); // 每隔2秒检查文件内容的追加
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- }
- }
- }
解释
- 控制器:定义一个Spring MVC控制器来处理音频流的请求。
- 随机访问文件:使用
RandomAccessFile
来读取文件,可以随时跳到文件中的任何位置读取数据。 - 持续检查文件:在循环中检查文件的长度,如果文件长度增加,读取新的数据并写入到
OutputStream
。 - 延时:每隔2秒检查文件是否有新内容追加。
前端代码
前端可以使用 fetch
API 或 XMLHttpRequest
来请求音频流,并通过 AudioContext
处理音频数据。
以下是一个简单的前端实现:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- function streamAudio(url) {
- fetch(url)
- .then(response => {
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const sourceNode = audioContext.createBufferSource();
- const audioQueue = [];
-
- function processAudio({done, value}) {
- if (done) {
- return;
- }
-
- audioContext.decodeAudioData(value.buffer)
- .then(buffer => {
- audioQueue.push(buffer);
- if (!sourceNode.buffer) {
- playAudioBuffer(audioQueue.shift());
- }
- })
- .catch(error => {
- console.error("Error decoding audio data", error);
- });
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const newSourceNode = audioContext.createBufferSource();
- newSourceNode.buffer = buffer;
- newSourceNode.connect(audioContext.destination);
- newSourceNode.start();
- newSourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- }
- };
- }
-
- reader.read().then(processAudio);
- })
- .catch(error => {
- console.error("Error fetching audio stream", error);
- });
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
解释
- Fetch API:使用
fetch
API 从服务器获取音频流。 - AudioContext:使用
AudioContext
处理和播放音频数据。 - 流处理:使用
ReadableStreamDefaultReader
逐块读取响应数据,并将其解码和播放。 - 播放队列:维护一个播放队列,确保音频数据按顺序播放。
通过上述代码,你可以实现一个Spring项目,返回一个不断追加内容的录音文件的字节流给前端,并在前端实时播放该音频数据。
Content-Disposition为attachment 这个头是做什么的
Content-Disposition
头在HTTP响应中用于指定如何处理响应内容。它通常用于提示浏览器如何处理传回的文件。该头有两个常见的值:inline
和 attachment
。
-
inline
: 表示内容应该在网页中直接显示。如果浏览器支持该内容类型,它会在浏览器窗口中显示。例如,如果内容是一个图像,浏览器会在窗口中显示该图像。 -
attachment
: 表示内容应该被下载到本地,通常会弹出一个文件下载对话框,并提示用户保存文件。这对于需要用户下载和保存的文件很有用,如PDF文档、ZIP文件、音频文件等。
使用 Content-Disposition
头,可以指定文件名。示例如下:
httpContent-Disposition: attachment; filename="example.wav"
这会提示浏览器下载文件,并将其命名为 example.wav
。
在Spring Boot中使用 Content-Disposition
假设你有一个需要用户下载的文件。你可以在Spring Boot控制器中设置 Content-Disposition
头,如下所示:
- package com.example.demo.controller;
-
- import org.springframework.http.HttpHeaders;
- import org.springframework.http.MediaType;
- import org.springframework.http.ResponseEntity;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.InputStream;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- @GetMapping("/download")
- public ResponseEntity<byte[]> downloadAudio() throws IOException {
- File audioFile = new File("path/to/your/audio/file.wav");
- byte[] audioBytes;
-
- try (InputStream inputStream = new FileInputStream(audioFile)) {
- audioBytes = inputStream.readAllBytes();
- }
-
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
- headers.setContentDispositionFormData("attachment", "example.wav");
-
- return ResponseEntity.ok()
- .headers(headers)
- .body(audioBytes);
- }
- }
解释
- 读取文件:将音频文件读取为字节数组。
- 设置响应头:使用
HttpHeaders
设置Content-Type
和Content-Disposition
头。 - 返回响应:使用
ResponseEntity
返回带有头和文件内容的响应。
前端处理下载
当用户点击下载链接时,浏览器会根据 Content-Disposition
头自动弹出下载对话框。例如,在HTML中:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Download Audio</title>
- </head>
- <body>
- <a href="http://localhost:8080/audio/download">Download Audio File</a>
- </body>
- </html>
用户点击链接后,浏览器会根据 Content-Disposition: attachment
头提示用户下载文件并保存为 example.wav
。
总结
Content-Disposition
头用于控制内容是直接显示在浏览器中还是提示用户下载。当设置为 attachment
时,浏览器会提示用户下载文件,并可以指定下载文件的默认名称。
请求 后端 接口 报错 ERR_INVALID_CHUNKED_ENCODING 200
ERR_INVALID_CHUNKED_ENCODING
通常发生在传输数据时,分块传输编码有误。这可能是由于响应头设置错误或数据传输过程中出错引起的。在你的场景中,如果文件正在不断追加内容,并通过字节流返回给前端,分块传输编码可能是一个问题。
以下是如何正确地处理这种情况,以确保文件能够持续追加并正确传输到前端。
修正后的后端代码
确保你正确处理了传输编码,并且在传输数据时遵循HTTP协议:
- package com.example.demo.controller;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- private final File audioFile = new File("path/to/your/audio/file.wav");
-
- @GetMapping("/stream")
- public void streamAudio(HttpServletResponse response) throws IOException {
- response.setContentType("audio/wav");
- response.setHeader("Transfer-Encoding", "chunked");
-
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
- OutputStream outputStream = response.getOutputStream()) {
- byte[] buffer = new byte[1024];
- long filePointer = 0;
-
- while (true) {
- long fileLength = randomAccessFile.length();
- if (fileLength > filePointer) {
- randomAccessFile.seek(filePointer);
- int bytesRead;
- while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- filePointer += bytesRead;
- }
- outputStream.flush();
- }
-
- try {
- Thread.sleep(2000); // 每隔2秒检查文件内容的追加
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- } catch (IOException e) {
- // 处理I/O异常
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- }
前端代码
前端可以继续使用 fetch
API 来请求音频流:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- function streamAudio(url) {
- fetch(url)
- .then(response => {
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const sourceNode = audioContext.createBufferSource();
- const audioQueue = [];
-
- function processAudio({done, value}) {
- if (done) {
- return;
- }
-
- audioContext.decodeAudioData(value.buffer)
- .then(buffer => {
- audioQueue.push(buffer);
- if (!sourceNode.buffer) {
- playAudioBuffer(audioQueue.shift());
- }
- })
- .catch(error => {
- console.error("Error decoding audio data", error);
- });
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const newSourceNode = audioContext.createBufferSource();
- newSourceNode.buffer = buffer;
- newSourceNode.connect(audioContext.destination);
- newSourceNode.start();
- newSourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- }
- };
- }
-
- reader.read().then(processAudio);
- })
- .catch(error => {
- console.error("Error fetching audio stream", error);
- });
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
进一步调试和验证
- 验证后端响应头:确保响应头
Transfer-Encoding: chunked
设置正确。 - 检查网络流量:使用浏览器的开发者工具检查网络请求,确保数据块正确传输。
- 处理例外情况:在后端代码中捕获和处理任何潜在的I/O异常,并确保正确关闭资源。
通过这些步骤,你应该能够解决 ERR_INVALID_CHUNKED_ENCODING
错误,并正确流式传输正在追加内容的文件。
response.setContentType("audio/wav"); response.setHeader("Transfer-Encoding", "chunked"); 我的是mp3 文件需要添加 哪些 头信息
对于MP3文件,HTTP响应的头信息需要相应调整。特别是 Content-Type
头应该设置为 audio/mpeg
。关于 Transfer-Encoding
,你不需要显式设置它,因为Servlet容器会自动处理。
以下是更新后的代码示例,假设你使用的是一个MP3文件:
后端代码
- package com.example.demo.controller;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- private final File audioFile = new File("path/to/your/audio/file.mp3");
-
- @GetMapping("/stream")
- public void streamAudio(HttpServletResponse response) throws IOException {
- response.setContentType("audio/mpeg");
-
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
- OutputStream outputStream = response.getOutputStream()) {
- byte[] buffer = new byte[1024];
- long filePointer = 0;
-
- while (true) {
- long fileLength = randomAccessFile.length();
- if (fileLength > filePointer) {
- randomAccessFile.seek(filePointer);
- int bytesRead;
- while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- filePointer += bytesRead;
- }
- outputStream.flush();
- }
-
- try {
- Thread.sleep(2000); // 每隔2秒检查文件内容的追加
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- } catch (IOException e) {
- // 处理I/O异常
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- }
前端代码
前端部分基本保持不变,只需要确保能正确处理和播放MP3数据:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- function streamAudio(url) {
- fetch(url)
- .then(response => {
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const audioQueue = [];
- let isPlaying = false;
-
- function processAudio({ done, value }) {
- if (done) {
- return;
- }
-
- const audioData = value;
- audioContext.decodeAudioData(audioData.buffer)
- .then(buffer => {
- audioQueue.push(buffer);
- if (!isPlaying) {
- playAudioBuffer(audioQueue.shift());
- }
- })
- .catch(error => {
- console.error("Error decoding audio data", error);
- });
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const newSourceNode = audioContext.createBufferSource();
- newSourceNode.buffer = buffer;
- newSourceNode.connect(audioContext.destination);
- newSourceNode.start();
- isPlaying = true;
- newSourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- } else {
- isPlaying = false;
- }
- };
- }
-
- reader.read().then(processAudio);
- })
- .catch(error => {
- console.error("Error fetching audio stream", error);
- });
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
解释
response.setContentType("audio/mpeg");
:设置内容类型为MP3格式。OutputStream
和RandomAccessFile
:用于处理文件的读取和数据流的传输。- 前端代码:使用
fetch
和AudioContext
处理音频流,并确保正确播放音频。
这样,你可以确保MP3文件在不断追加内容的情况下,以流的方式传输给前端,并且前端可以实时播放音频。
后端 outputStream flush 之后,前端就能拿到 这部分 流的数据了么?
是的,在后端调用 outputStream.flush()
之后,前端应该能够立即接收到这部分流的数据。flush()
方法强制将任何缓冲的输出字节写入底层流中,这样可以确保数据及时传输到客户端。
下面是对前端和后端代码的详细解释,确保流数据能够及时到达前端,并且前端可以正确处理和播放这些数据。
后端代码
确保在每次读取文件并写入到输出流后,调用 flush()
方法。
- package com.example.demo.controller;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- private final File audioFile = new File("path/to/your/audio/file.mp3");
-
- @GetMapping("/stream")
- public void streamAudio(HttpServletResponse response) throws IOException {
- response.setContentType("audio/mpeg");
-
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
- OutputStream outputStream = response.getOutputStream()) {
- byte[] buffer = new byte[1024];
- long filePointer = 0;
-
- while (true) {
- long fileLength = randomAccessFile.length();
- if (fileLength > filePointer) {
- randomAccessFile.seek(filePointer);
- int bytesRead;
- while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- filePointer += bytesRead;
- }
- outputStream.flush(); // 强制将缓冲区内容写入输出流
- }
-
- try {
- Thread.sleep(2000); // 每隔2秒检查文件内容的追加
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- } catch (IOException e) {
- // 处理I/O异常
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- }
前端代码
确保前端可以正确处理从流中接收到的数据。使用 fetch
API 和 AudioContext
来解码和播放音频数据。
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- function streamAudio(url) {
- fetch(url)
- .then(response => {
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const audioQueue = [];
- let isPlaying = false;
-
- function processAudio({ done, value }) {
- if (done) {
- return;
- }
-
- audioContext.decodeAudioData(value.buffer)
- .then(buffer => {
- audioQueue.push(buffer);
- if (!isPlaying) {
- playAudioBuffer(audioQueue.shift());
- }
- })
- .catch(error => {
- console.error("Error decoding audio data", error);
- });
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const newSourceNode = audioContext.createBufferSource();
- newSourceNode.buffer = buffer;
- newSourceNode.connect(audioContext.destination);
- newSourceNode.start();
- isPlaying = true;
- newSourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- } else {
- isPlaying = false;
- }
- };
- }
-
- reader.read().then(processAudio);
- })
- .catch(error => {
- console.error("Error fetching audio stream", error);
- });
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
解释
-
后端:
response.setContentType("audio/mpeg");
: 设置内容类型为MP3。outputStream.flush()
: 每次读取和写入后调用flush()
方法,以确保数据及时传输到前端。
-
前端:
fetch
API 用于请求音频流。ReadableStreamDefaultReader
用于读取流数据。AudioContext
和decodeAudioData
用于解码和播放音频数据。audioQueue
用于管理和顺序播放解码后的音频数据。
通过这种方式,前端会在后端调用 flush()
后及时接收到流数据,并且可以正确播放音频。
Audio Context。decodeAudioData 报错 unable to decode audiodata
AudioContext.decodeAudioData
报错 "unable to decode audiodata" 可能是由于接收到的数据格式不正确或数据有问题。我们需要确保前端正确地将二进制数据传递给 decodeAudioData
方法。
修正前端代码
确保前端代码正确处理从后端接收到的流数据,并将其转换为适合 AudioContext.decodeAudioData
解码的格式。以下是修正后的前端代码示例:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- async function streamAudio(url) {
- const response = await fetch(url);
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const audioQueue = [];
- let isPlaying = false;
-
- async function processAudio({ done, value }) {
- if (done) {
- return;
- }
-
- try {
- const audioBuffer = await audioContext.decodeAudioData(value.buffer);
- audioQueue.push(audioBuffer);
- if (!isPlaying) {
- playAudioBuffer(audioQueue.shift());
- }
- } catch (error) {
- console.error("Error decoding audio data", error);
- }
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const sourceNode = audioContext.createBufferSource();
- sourceNode.buffer = buffer;
- sourceNode.connect(audioContext.destination);
- sourceNode.start();
- isPlaying = true;
- sourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- } else {
- isPlaying = false;
- }
- };
- }
-
- reader.read().then(processAudio);
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
后端代码确认
确保后端正确传输MP3文件内容,并设置正确的头信息。之前的后端代码已经很好地处理了这一点,只需确保文件路径和文件格式正确。
额外的调试步骤
- 确认文件格式:确保MP3文件确实是有效的音频文件,可以在本地播放。
- 网络调试:使用浏览器的开发者工具检查网络请求,确认从后端接收到的数据是正确的二进制数据。
- 错误处理:在前端捕获并详细日志输出所有错误信息,帮助诊断问题。
进一步调试和验证
如果依然无法解决问题,可以进行以下调试步骤:
-
前端数据检查:
- 在
reader.read().then(processAudio)
的回调中,添加日志输出value
的内容,确认接收到的数据是有效的二进制数据。
- 在
-
分块数据合并:
- 确保接收到的二进制数据块能够正确合并,并传递给
decodeAudioData
。以下是一个处理数据块的示例:
- 确保接收到的二进制数据块能够正确合并,并传递给
- function streamAudio(url) {
- fetch(url)
- .then(response => {
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- let audioDataBuffer = new Uint8Array();
-
- function processAudio({ done, value }) {
- if (done) {
- return;
- }
-
- // 合并新接收到的数据块到已有数据缓冲区
- const newBuffer = new Uint8Array(audioDataBuffer.length + value.length);
- newBuffer.set(audioDataBuffer);
- newBuffer.set(value, audioDataBuffer.length);
- audioDataBuffer = newBuffer;
-
- audioContext.decodeAudioData(audioDataBuffer.buffer)
- .then(buffer => {
- const sourceNode = audioContext.createBufferSource();
- sourceNode.buffer = buffer;
- sourceNode.connect(audioContext.destination);
- sourceNode.start();
- })
- .catch(error => {
- console.error("Error decoding audio data", error);
- });
-
- return reader.read().then(processAudio);
- }
-
- reader.read().then(processAudio);
- })
- .catch(error => {
- console.error("Error fetching audio stream", error);
- });
- }
通过这些步骤,确保前端能够正确处理和播放从后端流式传输过来的MP3数据。
Audio Context.decodeAudioData 报错 unable to decode audiodata ,是后端要修改,还是前端要处理,
AudioContext.decodeAudioData
报错 "unable to decode audiodata" 可能源于后端或前端。我们需要逐步排查问题,以确保数据正确传输和处理。
后端:确保MP3文件正确传输
首先,确保后端以正确的方式传输MP3文件数据。以下是一个示例,确保后端流式传输MP3文件:
- package com.example.demo.controller;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- private final File audioFile = new File("path/to/your/audio/file.mp3");
-
- @GetMapping("/stream")
- public void streamAudio(HttpServletResponse response) throws IOException {
- response.setContentType("audio/mpeg");
-
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
- OutputStream outputStream = response.getOutputStream()) {
- byte[] buffer = new byte[1024];
- long filePointer = 0;
-
- while (true) {
- long fileLength = randomAccessFile.length();
- if (fileLength > filePointer) {
- randomAccessFile.seek(filePointer);
- int bytesRead;
- while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- filePointer += bytesRead;
- }
- outputStream.flush();
- }
-
- try {
- Thread.sleep(2000); // 每隔2秒检查文件内容的追加
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- } catch (IOException e) {
- // 处理I/O异常
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- }
前端:处理流数据
前端需要正确地处理流数据,并确保将接收到的数据传递给 AudioContext.decodeAudioData
。
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- async function streamAudio(url) {
- const response = await fetch(url);
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const audioQueue = [];
- let isPlaying = false;
-
- async function processAudio({ done, value }) {
- if (done) {
- return;
- }
-
- const audioData = value.buffer;
- try {
- const audioBuffer = await audioContext.decodeAudioData(audioData);
- audioQueue.push(audioBuffer);
- if (!isPlaying) {
- playAudioBuffer(audioQueue.shift());
- }
- } catch (error) {
- console.error("Error decoding audio data", error);
- }
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const sourceNode = audioContext.createBufferSource();
- sourceNode.buffer = buffer;
- sourceNode.connect(audioContext.destination);
- sourceNode.start();
- isPlaying = true;
- sourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- } else {
- isPlaying = false;
- }
- };
- }
-
- reader.read().then(processAudio);
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
调试步骤
-
检查后端传输的数据:
- 确保后端确实以正确的MP3格式传输数据。
- 可以使用工具如
curl
或Postman
来请求音频流,保存为文件,确认文件可以正常播放。
-
检查前端接收到的数据:
- 在
processAudio
函数中,添加日志输出value
内容,确保接收到的是正确的二进制数据。 - 例如,可以使用
console.log(new Uint8Array(value.buffer))
输出数据。
- 在
-
数据类型转换:
- 确保前端将
value.buffer
传递给decodeAudioData
。 - 检查数据类型是否需要转换。例如,确保接收到的
value
是Uint8Array
类型。
- 确保前端将
-
前端错误处理:
- 捕获并详细日志输出所有错误信息,帮助诊断问题。
- 确保
AudioContext
初始化正确。
通过这些步骤,可以逐步排查并解决 AudioContext.decodeAudioData
报错的问题,确保前端能够正确解码和播放流式传输的MP3音频数据。
前端 js 请求后端 音频 接口,接口音频数据是在outputStream中,给我个案例
为了确保前端能够正确地从后端接收并处理音频数据,我们需要一个具体的示例来展示如何通过 JavaScript 请求后端音频接口,并处理从 OutputStream
中传输过来的音频数据。
以下是一个完整的示例,包括后端和前端代码:
后端代码
首先,后端代码使用 OutputStream
来传输音频数据。以下是一个 Spring Boot 控制器示例,演示如何流式传输一个不断追加内容的 MP3 文件:
- package com.example.demo.controller;
-
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.File;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.io.RandomAccessFile;
-
- @Controller
- @RequestMapping("/audio")
- public class AudioController {
-
- private final File audioFile = new File("path/to/your/audio/file.mp3");
-
- @GetMapping("/stream")
- public void streamAudio(HttpServletResponse response) throws IOException {
- response.setContentType("audio/mpeg");
-
- try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
- OutputStream outputStream = response.getOutputStream()) {
- byte[] buffer = new byte[1024];
- long filePointer = 0;
-
- while (true) {
- long fileLength = randomAccessFile.length();
- if (fileLength > filePointer) {
- randomAccessFile.seek(filePointer);
- int bytesRead;
- while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
- outputStream.write(buffer, 0, bytesRead);
- filePointer += bytesRead;
- }
- outputStream.flush(); // 强制将缓冲区内容写入输出流
- }
-
- try {
- Thread.sleep(2000); // 每隔2秒检查文件内容的追加
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- break;
- }
- }
- } catch (IOException e) {
- // 处理I/O异常
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- }
前端代码
前端代码使用 Fetch API 请求后端音频接口,并使用 Web Audio API 处理和播放音频数据。下面是一个示例:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>Audio Stream</title>
- </head>
- <body>
- <audio id="audio" controls autoplay></audio>
- <script>
- const audioElement = document.getElementById('audio');
-
- async function streamAudio(url) {
- const response = await fetch(url);
- const reader = response.body.getReader();
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const audioQueue = [];
- let isPlaying = false;
-
- async function processAudio({ done, value }) {
- if (done) {
- return;
- }
-
- const audioData = new Uint8Array(value);
- try {
- const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
- audioQueue.push(audioBuffer);
- if (!isPlaying) {
- playAudioBuffer(audioQueue.shift());
- }
- } catch (error) {
- console.error("Error decoding audio data", error);
- }
-
- return reader.read().then(processAudio);
- }
-
- function playAudioBuffer(buffer) {
- const sourceNode = audioContext.createBufferSource();
- sourceNode.buffer = buffer;
- sourceNode.connect(audioContext.destination);
- sourceNode.start();
- isPlaying = true;
- sourceNode.onended = () => {
- if (audioQueue.length > 0) {
- playAudioBuffer(audioQueue.shift());
- } else {
- isPlaying = false;
- }
- };
- }
-
- reader.read().then(processAudio);
- }
-
- streamAudio('http://localhost:8080/audio/stream');
- </script>
- </body>
- </html>
解释
-
后端:
response.setContentType("audio/mpeg");
: 设置内容类型为 MP3。- 使用
RandomAccessFile
读取 MP3 文件并写入OutputStream
。 - 循环检查文件是否有新的内容追加,每隔2秒检查一次,并写入新的内容到输出流。
-
前端:
- 使用
fetch
请求音频流。 - 使用
ReadableStreamDefaultReader
读取流数据。 - 将读取的二进制数据转换为
Uint8Array
。 - 使用
AudioContext
和decodeAudioData
解码并播放音频数据。 - 使用
audioQueue
管理解码后的音频数据,确保顺序播放。
- 使用
通过这种方式,前端可以从后端流式获取音频数据,并实时播放。这种方法确保了在后端追加内容时,前端能够持续接收和播放新的音频内容。