Spring 项目,本地有一个录音文件,我需要以字节流的方式返回给前端,不要使用webflux,就用简单的InputStream,同时这个文件还一直在追加内容,给我一个案例

要实现一个Spring项目,将一个正在追加内容的录音文件以字节流的方式返回给前端,你可以使用传统的Servlet方式。以下是一个实现方案:

后端代码

  1. 创建一个控制器来处理文件流的请求。
  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.File;
  7. import java.io.FileInputStream;
  8. import java.io.IOException;
  9. import java.io.OutputStream;
  10. import java.io.RandomAccessFile;
  11. @Controller
  12. @RequestMapping("/audio")
  13. public class AudioController {
  14. private final File audioFile = new File("path/to/your/audio/file.wav");
  15. @GetMapping("/stream")
  16. public void streamAudio(HttpServletResponse response) throws IOException {
  17. response.setContentType("audio/wav");
  18. response.setHeader("Transfer-Encoding", "chunked");
  19. try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
  20. OutputStream outputStream = response.getOutputStream()) {
  21. byte[] buffer = new byte[1024];
  22. long filePointer = 0;
  23. while (true) {
  24. long fileLength = randomAccessFile.length();
  25. if (fileLength > filePointer) {
  26. randomAccessFile.seek(filePointer);
  27. int bytesRead;
  28. while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
  29. outputStream.write(buffer, 0, bytesRead);
  30. filePointer += bytesRead;
  31. }
  32. outputStream.flush();
  33. }
  34. try {
  35. Thread.sleep(2000); // 每隔2秒检查文件内容的追加
  36. } catch (InterruptedException e) {
  37. Thread.currentThread().interrupt();
  38. break;
  39. }
  40. }
  41. }
  42. }
  43. }

解释

  1. 控制器:定义一个Spring MVC控制器来处理音频流的请求。
  2. 随机访问文件:使用 RandomAccessFile 来读取文件,可以随时跳到文件中的任何位置读取数据。
  3. 持续检查文件:在循环中检查文件的长度,如果文件长度增加,读取新的数据并写入到 OutputStream
  4. 延时:每隔2秒检查文件是否有新内容追加。

前端代码

前端可以使用 fetch API 或 XMLHttpRequest 来请求音频流,并通过 AudioContext 处理音频数据。

以下是一个简单的前端实现:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. function streamAudio(url) {
  12. fetch(url)
  13. .then(response => {
  14. const reader = response.body.getReader();
  15. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  16. const sourceNode = audioContext.createBufferSource();
  17. const audioQueue = [];
  18. function processAudio({done, value}) {
  19. if (done) {
  20. return;
  21. }
  22. audioContext.decodeAudioData(value.buffer)
  23. .then(buffer => {
  24. audioQueue.push(buffer);
  25. if (!sourceNode.buffer) {
  26. playAudioBuffer(audioQueue.shift());
  27. }
  28. })
  29. .catch(error => {
  30. console.error("Error decoding audio data", error);
  31. });
  32. return reader.read().then(processAudio);
  33. }
  34. function playAudioBuffer(buffer) {
  35. const newSourceNode = audioContext.createBufferSource();
  36. newSourceNode.buffer = buffer;
  37. newSourceNode.connect(audioContext.destination);
  38. newSourceNode.start();
  39. newSourceNode.onended = () => {
  40. if (audioQueue.length > 0) {
  41. playAudioBuffer(audioQueue.shift());
  42. }
  43. };
  44. }
  45. reader.read().then(processAudio);
  46. })
  47. .catch(error => {
  48. console.error("Error fetching audio stream", error);
  49. });
  50. }
  51. streamAudio('http://localhost:8080/audio/stream');
  52. </script>
  53. </body>
  54. </html>

解释

  1. Fetch API:使用 fetch API 从服务器获取音频流。
  2. AudioContext:使用 AudioContext 处理和播放音频数据。
  3. 流处理:使用 ReadableStreamDefaultReader 逐块读取响应数据,并将其解码和播放。
  4. 播放队列:维护一个播放队列,确保音频数据按顺序播放。

通过上述代码,你可以实现一个Spring项目,返回一个不断追加内容的录音文件的字节流给前端,并在前端实时播放该音频数据。

Content-Disposition为attachment 这个头是做什么的

Content-Disposition 头在HTTP响应中用于指定如何处理响应内容。它通常用于提示浏览器如何处理传回的文件。该头有两个常见的值:inlineattachment

  1. inline: 表示内容应该在网页中直接显示。如果浏览器支持该内容类型,它会在浏览器窗口中显示。例如,如果内容是一个图像,浏览器会在窗口中显示该图像。

  2. attachment: 表示内容应该被下载到本地,通常会弹出一个文件下载对话框,并提示用户保存文件。这对于需要用户下载和保存的文件很有用,如PDF文档、ZIP文件、音频文件等。

使用 Content-Disposition 头,可以指定文件名。示例如下:

httpContent-Disposition: attachment; filename="example.wav" 

这会提示浏览器下载文件,并将其命名为 example.wav

在Spring Boot中使用 Content-Disposition

假设你有一个需要用户下载的文件。你可以在Spring Boot控制器中设置 Content-Disposition 头,如下所示:

  1. package com.example.demo.controller;
  2. import org.springframework.http.HttpHeaders;
  3. import org.springframework.http.MediaType;
  4. import org.springframework.http.ResponseEntity;
  5. import org.springframework.stereotype.Controller;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import java.io.File;
  9. import java.io.FileInputStream;
  10. import java.io.IOException;
  11. import java.io.InputStream;
  12. @Controller
  13. @RequestMapping("/audio")
  14. public class AudioController {
  15. @GetMapping("/download")
  16. public ResponseEntity<byte[]> downloadAudio() throws IOException {
  17. File audioFile = new File("path/to/your/audio/file.wav");
  18. byte[] audioBytes;
  19. try (InputStream inputStream = new FileInputStream(audioFile)) {
  20. audioBytes = inputStream.readAllBytes();
  21. }
  22. HttpHeaders headers = new HttpHeaders();
  23. headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
  24. headers.setContentDispositionFormData("attachment", "example.wav");
  25. return ResponseEntity.ok()
  26. .headers(headers)
  27. .body(audioBytes);
  28. }
  29. }

解释

  1. 读取文件:将音频文件读取为字节数组。
  2. 设置响应头:使用 HttpHeaders 设置 Content-TypeContent-Disposition 头。
  3. 返回响应:使用 ResponseEntity 返回带有头和文件内容的响应。

前端处理下载

当用户点击下载链接时,浏览器会根据 Content-Disposition 头自动弹出下载对话框。例如,在HTML中:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Download Audio</title>
  6. </head>
  7. <body>
  8. <a href="http://localhost:8080/audio/download">Download Audio File</a>
  9. </body>
  10. </html>

用户点击链接后,浏览器会根据 Content-Disposition: attachment 头提示用户下载文件并保存为 example.wav

总结

Content-Disposition 头用于控制内容是直接显示在浏览器中还是提示用户下载。当设置为 attachment 时,浏览器会提示用户下载文件,并可以指定下载文件的默认名称。

请求 后端 接口 报错 ERR_INVALID_CHUNKED_ENCODING 200

ERR_INVALID_CHUNKED_ENCODING 通常发生在传输数据时,分块传输编码有误。这可能是由于响应头设置错误或数据传输过程中出错引起的。在你的场景中,如果文件正在不断追加内容,并通过字节流返回给前端,分块传输编码可能是一个问题。

以下是如何正确地处理这种情况,以确保文件能够持续追加并正确传输到前端。

修正后的后端代码

确保你正确处理了传输编码,并且在传输数据时遵循HTTP协议:

  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.io.OutputStream;
  9. import java.io.RandomAccessFile;
  10. @Controller
  11. @RequestMapping("/audio")
  12. public class AudioController {
  13. private final File audioFile = new File("path/to/your/audio/file.wav");
  14. @GetMapping("/stream")
  15. public void streamAudio(HttpServletResponse response) throws IOException {
  16. response.setContentType("audio/wav");
  17. response.setHeader("Transfer-Encoding", "chunked");
  18. try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
  19. OutputStream outputStream = response.getOutputStream()) {
  20. byte[] buffer = new byte[1024];
  21. long filePointer = 0;
  22. while (true) {
  23. long fileLength = randomAccessFile.length();
  24. if (fileLength > filePointer) {
  25. randomAccessFile.seek(filePointer);
  26. int bytesRead;
  27. while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
  28. outputStream.write(buffer, 0, bytesRead);
  29. filePointer += bytesRead;
  30. }
  31. outputStream.flush();
  32. }
  33. try {
  34. Thread.sleep(2000); // 每隔2秒检查文件内容的追加
  35. } catch (InterruptedException e) {
  36. Thread.currentThread().interrupt();
  37. break;
  38. }
  39. }
  40. } catch (IOException e) {
  41. // 处理I/O异常
  42. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  43. }
  44. }
  45. }

前端代码

前端可以继续使用 fetch API 来请求音频流:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. function streamAudio(url) {
  12. fetch(url)
  13. .then(response => {
  14. const reader = response.body.getReader();
  15. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  16. const sourceNode = audioContext.createBufferSource();
  17. const audioQueue = [];
  18. function processAudio({done, value}) {
  19. if (done) {
  20. return;
  21. }
  22. audioContext.decodeAudioData(value.buffer)
  23. .then(buffer => {
  24. audioQueue.push(buffer);
  25. if (!sourceNode.buffer) {
  26. playAudioBuffer(audioQueue.shift());
  27. }
  28. })
  29. .catch(error => {
  30. console.error("Error decoding audio data", error);
  31. });
  32. return reader.read().then(processAudio);
  33. }
  34. function playAudioBuffer(buffer) {
  35. const newSourceNode = audioContext.createBufferSource();
  36. newSourceNode.buffer = buffer;
  37. newSourceNode.connect(audioContext.destination);
  38. newSourceNode.start();
  39. newSourceNode.onended = () => {
  40. if (audioQueue.length > 0) {
  41. playAudioBuffer(audioQueue.shift());
  42. }
  43. };
  44. }
  45. reader.read().then(processAudio);
  46. })
  47. .catch(error => {
  48. console.error("Error fetching audio stream", error);
  49. });
  50. }
  51. streamAudio('http://localhost:8080/audio/stream');
  52. </script>
  53. </body>
  54. </html>

进一步调试和验证

  1. 验证后端响应头:确保响应头 Transfer-Encoding: chunked 设置正确。
  2. 检查网络流量:使用浏览器的开发者工具检查网络请求,确保数据块正确传输。
  3. 处理例外情况:在后端代码中捕获和处理任何潜在的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文件:

后端代码

  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.io.OutputStream;
  9. import java.io.RandomAccessFile;
  10. @Controller
  11. @RequestMapping("/audio")
  12. public class AudioController {
  13. private final File audioFile = new File("path/to/your/audio/file.mp3");
  14. @GetMapping("/stream")
  15. public void streamAudio(HttpServletResponse response) throws IOException {
  16. response.setContentType("audio/mpeg");
  17. try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
  18. OutputStream outputStream = response.getOutputStream()) {
  19. byte[] buffer = new byte[1024];
  20. long filePointer = 0;
  21. while (true) {
  22. long fileLength = randomAccessFile.length();
  23. if (fileLength > filePointer) {
  24. randomAccessFile.seek(filePointer);
  25. int bytesRead;
  26. while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
  27. outputStream.write(buffer, 0, bytesRead);
  28. filePointer += bytesRead;
  29. }
  30. outputStream.flush();
  31. }
  32. try {
  33. Thread.sleep(2000); // 每隔2秒检查文件内容的追加
  34. } catch (InterruptedException e) {
  35. Thread.currentThread().interrupt();
  36. break;
  37. }
  38. }
  39. } catch (IOException e) {
  40. // 处理I/O异常
  41. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  42. }
  43. }
  44. }

前端代码

前端部分基本保持不变,只需要确保能正确处理和播放MP3数据:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. function streamAudio(url) {
  12. fetch(url)
  13. .then(response => {
  14. const reader = response.body.getReader();
  15. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  16. const audioQueue = [];
  17. let isPlaying = false;
  18. function processAudio({ done, value }) {
  19. if (done) {
  20. return;
  21. }
  22. const audioData = value;
  23. audioContext.decodeAudioData(audioData.buffer)
  24. .then(buffer => {
  25. audioQueue.push(buffer);
  26. if (!isPlaying) {
  27. playAudioBuffer(audioQueue.shift());
  28. }
  29. })
  30. .catch(error => {
  31. console.error("Error decoding audio data", error);
  32. });
  33. return reader.read().then(processAudio);
  34. }
  35. function playAudioBuffer(buffer) {
  36. const newSourceNode = audioContext.createBufferSource();
  37. newSourceNode.buffer = buffer;
  38. newSourceNode.connect(audioContext.destination);
  39. newSourceNode.start();
  40. isPlaying = true;
  41. newSourceNode.onended = () => {
  42. if (audioQueue.length > 0) {
  43. playAudioBuffer(audioQueue.shift());
  44. } else {
  45. isPlaying = false;
  46. }
  47. };
  48. }
  49. reader.read().then(processAudio);
  50. })
  51. .catch(error => {
  52. console.error("Error fetching audio stream", error);
  53. });
  54. }
  55. streamAudio('http://localhost:8080/audio/stream');
  56. </script>
  57. </body>
  58. </html>

解释

  1. response.setContentType("audio/mpeg");:设置内容类型为MP3格式。
  2. OutputStreamRandomAccessFile:用于处理文件的读取和数据流的传输。
  3. 前端代码:使用 fetchAudioContext 处理音频流,并确保正确播放音频。

这样,你可以确保MP3文件在不断追加内容的情况下,以流的方式传输给前端,并且前端可以实时播放音频。

后端 outputStream flush 之后,前端就能拿到 这部分 流的数据了么?

是的,在后端调用 outputStream.flush() 之后,前端应该能够立即接收到这部分流的数据。flush() 方法强制将任何缓冲的输出字节写入底层流中,这样可以确保数据及时传输到客户端。

下面是对前端和后端代码的详细解释,确保流数据能够及时到达前端,并且前端可以正确处理和播放这些数据。

后端代码

确保在每次读取文件并写入到输出流后,调用 flush() 方法。

  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.io.OutputStream;
  9. import java.io.RandomAccessFile;
  10. @Controller
  11. @RequestMapping("/audio")
  12. public class AudioController {
  13. private final File audioFile = new File("path/to/your/audio/file.mp3");
  14. @GetMapping("/stream")
  15. public void streamAudio(HttpServletResponse response) throws IOException {
  16. response.setContentType("audio/mpeg");
  17. try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
  18. OutputStream outputStream = response.getOutputStream()) {
  19. byte[] buffer = new byte[1024];
  20. long filePointer = 0;
  21. while (true) {
  22. long fileLength = randomAccessFile.length();
  23. if (fileLength > filePointer) {
  24. randomAccessFile.seek(filePointer);
  25. int bytesRead;
  26. while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
  27. outputStream.write(buffer, 0, bytesRead);
  28. filePointer += bytesRead;
  29. }
  30. outputStream.flush(); // 强制将缓冲区内容写入输出流
  31. }
  32. try {
  33. Thread.sleep(2000); // 每隔2秒检查文件内容的追加
  34. } catch (InterruptedException e) {
  35. Thread.currentThread().interrupt();
  36. break;
  37. }
  38. }
  39. } catch (IOException e) {
  40. // 处理I/O异常
  41. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  42. }
  43. }
  44. }

前端代码

确保前端可以正确处理从流中接收到的数据。使用 fetch API 和 AudioContext 来解码和播放音频数据。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. function streamAudio(url) {
  12. fetch(url)
  13. .then(response => {
  14. const reader = response.body.getReader();
  15. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  16. const audioQueue = [];
  17. let isPlaying = false;
  18. function processAudio({ done, value }) {
  19. if (done) {
  20. return;
  21. }
  22. audioContext.decodeAudioData(value.buffer)
  23. .then(buffer => {
  24. audioQueue.push(buffer);
  25. if (!isPlaying) {
  26. playAudioBuffer(audioQueue.shift());
  27. }
  28. })
  29. .catch(error => {
  30. console.error("Error decoding audio data", error);
  31. });
  32. return reader.read().then(processAudio);
  33. }
  34. function playAudioBuffer(buffer) {
  35. const newSourceNode = audioContext.createBufferSource();
  36. newSourceNode.buffer = buffer;
  37. newSourceNode.connect(audioContext.destination);
  38. newSourceNode.start();
  39. isPlaying = true;
  40. newSourceNode.onended = () => {
  41. if (audioQueue.length > 0) {
  42. playAudioBuffer(audioQueue.shift());
  43. } else {
  44. isPlaying = false;
  45. }
  46. };
  47. }
  48. reader.read().then(processAudio);
  49. })
  50. .catch(error => {
  51. console.error("Error fetching audio stream", error);
  52. });
  53. }
  54. streamAudio('http://localhost:8080/audio/stream');
  55. </script>
  56. </body>
  57. </html>

解释

  1. 后端:

    • response.setContentType("audio/mpeg");: 设置内容类型为MP3。
    • outputStream.flush(): 每次读取和写入后调用 flush() 方法,以确保数据及时传输到前端。
  2. 前端:

    • fetch API 用于请求音频流。
    • ReadableStreamDefaultReader 用于读取流数据。
    • AudioContextdecodeAudioData 用于解码和播放音频数据。
    • audioQueue 用于管理和顺序播放解码后的音频数据。

通过这种方式,前端会在后端调用 flush() 后及时接收到流数据,并且可以正确播放音频。

Audio Context。decodeAudioData 报错 unable to decode audiodata

AudioContext.decodeAudioData 报错 "unable to decode audiodata" 可能是由于接收到的数据格式不正确或数据有问题。我们需要确保前端正确地将二进制数据传递给 decodeAudioData 方法。

修正前端代码

确保前端代码正确处理从后端接收到的流数据,并将其转换为适合 AudioContext.decodeAudioData 解码的格式。以下是修正后的前端代码示例:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. async function streamAudio(url) {
  12. const response = await fetch(url);
  13. const reader = response.body.getReader();
  14. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  15. const audioQueue = [];
  16. let isPlaying = false;
  17. async function processAudio({ done, value }) {
  18. if (done) {
  19. return;
  20. }
  21. try {
  22. const audioBuffer = await audioContext.decodeAudioData(value.buffer);
  23. audioQueue.push(audioBuffer);
  24. if (!isPlaying) {
  25. playAudioBuffer(audioQueue.shift());
  26. }
  27. } catch (error) {
  28. console.error("Error decoding audio data", error);
  29. }
  30. return reader.read().then(processAudio);
  31. }
  32. function playAudioBuffer(buffer) {
  33. const sourceNode = audioContext.createBufferSource();
  34. sourceNode.buffer = buffer;
  35. sourceNode.connect(audioContext.destination);
  36. sourceNode.start();
  37. isPlaying = true;
  38. sourceNode.onended = () => {
  39. if (audioQueue.length > 0) {
  40. playAudioBuffer(audioQueue.shift());
  41. } else {
  42. isPlaying = false;
  43. }
  44. };
  45. }
  46. reader.read().then(processAudio);
  47. }
  48. streamAudio('http://localhost:8080/audio/stream');
  49. </script>
  50. </body>
  51. </html>

后端代码确认

确保后端正确传输MP3文件内容,并设置正确的头信息。之前的后端代码已经很好地处理了这一点,只需确保文件路径和文件格式正确。

额外的调试步骤

  1. 确认文件格式:确保MP3文件确实是有效的音频文件,可以在本地播放。
  2. 网络调试:使用浏览器的开发者工具检查网络请求,确认从后端接收到的数据是正确的二进制数据。
  3. 错误处理:在前端捕获并详细日志输出所有错误信息,帮助诊断问题。

进一步调试和验证

如果依然无法解决问题,可以进行以下调试步骤:

  1. 前端数据检查

    • reader.read().then(processAudio) 的回调中,添加日志输出 value 的内容,确认接收到的数据是有效的二进制数据。
  2. 分块数据合并

    • 确保接收到的二进制数据块能够正确合并,并传递给 decodeAudioData。以下是一个处理数据块的示例:
  1. function streamAudio(url) {
  2. fetch(url)
  3. .then(response => {
  4. const reader = response.body.getReader();
  5. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  6. let audioDataBuffer = new Uint8Array();
  7. function processAudio({ done, value }) {
  8. if (done) {
  9. return;
  10. }
  11. // 合并新接收到的数据块到已有数据缓冲区
  12. const newBuffer = new Uint8Array(audioDataBuffer.length + value.length);
  13. newBuffer.set(audioDataBuffer);
  14. newBuffer.set(value, audioDataBuffer.length);
  15. audioDataBuffer = newBuffer;
  16. audioContext.decodeAudioData(audioDataBuffer.buffer)
  17. .then(buffer => {
  18. const sourceNode = audioContext.createBufferSource();
  19. sourceNode.buffer = buffer;
  20. sourceNode.connect(audioContext.destination);
  21. sourceNode.start();
  22. })
  23. .catch(error => {
  24. console.error("Error decoding audio data", error);
  25. });
  26. return reader.read().then(processAudio);
  27. }
  28. reader.read().then(processAudio);
  29. })
  30. .catch(error => {
  31. console.error("Error fetching audio stream", error);
  32. });
  33. }

通过这些步骤,确保前端能够正确处理和播放从后端流式传输过来的MP3数据。

Audio Context.decodeAudioData 报错 unable to decode audiodata ,是后端要修改,还是前端要处理,

AudioContext.decodeAudioData 报错 "unable to decode audiodata" 可能源于后端或前端。我们需要逐步排查问题,以确保数据正确传输和处理。

后端:确保MP3文件正确传输

首先,确保后端以正确的方式传输MP3文件数据。以下是一个示例,确保后端流式传输MP3文件:

  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.io.OutputStream;
  9. import java.io.RandomAccessFile;
  10. @Controller
  11. @RequestMapping("/audio")
  12. public class AudioController {
  13. private final File audioFile = new File("path/to/your/audio/file.mp3");
  14. @GetMapping("/stream")
  15. public void streamAudio(HttpServletResponse response) throws IOException {
  16. response.setContentType("audio/mpeg");
  17. try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
  18. OutputStream outputStream = response.getOutputStream()) {
  19. byte[] buffer = new byte[1024];
  20. long filePointer = 0;
  21. while (true) {
  22. long fileLength = randomAccessFile.length();
  23. if (fileLength > filePointer) {
  24. randomAccessFile.seek(filePointer);
  25. int bytesRead;
  26. while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
  27. outputStream.write(buffer, 0, bytesRead);
  28. filePointer += bytesRead;
  29. }
  30. outputStream.flush();
  31. }
  32. try {
  33. Thread.sleep(2000); // 每隔2秒检查文件内容的追加
  34. } catch (InterruptedException e) {
  35. Thread.currentThread().interrupt();
  36. break;
  37. }
  38. }
  39. } catch (IOException e) {
  40. // 处理I/O异常
  41. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  42. }
  43. }
  44. }

前端:处理流数据

前端需要正确地处理流数据,并确保将接收到的数据传递给 AudioContext.decodeAudioData

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. async function streamAudio(url) {
  12. const response = await fetch(url);
  13. const reader = response.body.getReader();
  14. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  15. const audioQueue = [];
  16. let isPlaying = false;
  17. async function processAudio({ done, value }) {
  18. if (done) {
  19. return;
  20. }
  21. const audioData = value.buffer;
  22. try {
  23. const audioBuffer = await audioContext.decodeAudioData(audioData);
  24. audioQueue.push(audioBuffer);
  25. if (!isPlaying) {
  26. playAudioBuffer(audioQueue.shift());
  27. }
  28. } catch (error) {
  29. console.error("Error decoding audio data", error);
  30. }
  31. return reader.read().then(processAudio);
  32. }
  33. function playAudioBuffer(buffer) {
  34. const sourceNode = audioContext.createBufferSource();
  35. sourceNode.buffer = buffer;
  36. sourceNode.connect(audioContext.destination);
  37. sourceNode.start();
  38. isPlaying = true;
  39. sourceNode.onended = () => {
  40. if (audioQueue.length > 0) {
  41. playAudioBuffer(audioQueue.shift());
  42. } else {
  43. isPlaying = false;
  44. }
  45. };
  46. }
  47. reader.read().then(processAudio);
  48. }
  49. streamAudio('http://localhost:8080/audio/stream');
  50. </script>
  51. </body>
  52. </html>

调试步骤

  1. 检查后端传输的数据

    • 确保后端确实以正确的MP3格式传输数据。
    • 可以使用工具如 curlPostman 来请求音频流,保存为文件,确认文件可以正常播放。
  2. 检查前端接收到的数据

    • processAudio 函数中,添加日志输出 value 内容,确保接收到的是正确的二进制数据。
    • 例如,可以使用 console.log(new Uint8Array(value.buffer)) 输出数据。
  3. 数据类型转换

    • 确保前端将 value.buffer 传递给 decodeAudioData
    • 检查数据类型是否需要转换。例如,确保接收到的 valueUint8Array 类型。
  4. 前端错误处理

    • 捕获并详细日志输出所有错误信息,帮助诊断问题。
    • 确保 AudioContext 初始化正确。

通过这些步骤,可以逐步排查并解决 AudioContext.decodeAudioData 报错的问题,确保前端能够正确解码和播放流式传输的MP3音频数据。

前端 js 请求后端 音频 接口,接口音频数据是在outputStream中,给我个案例

为了确保前端能够正确地从后端接收并处理音频数据,我们需要一个具体的示例来展示如何通过 JavaScript 请求后端音频接口,并处理从 OutputStream 中传输过来的音频数据。

以下是一个完整的示例,包括后端和前端代码:

后端代码

首先,后端代码使用 OutputStream 来传输音频数据。以下是一个 Spring Boot 控制器示例,演示如何流式传输一个不断追加内容的 MP3 文件:

  1. package com.example.demo.controller;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import javax.servlet.http.HttpServletResponse;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.io.OutputStream;
  9. import java.io.RandomAccessFile;
  10. @Controller
  11. @RequestMapping("/audio")
  12. public class AudioController {
  13. private final File audioFile = new File("path/to/your/audio/file.mp3");
  14. @GetMapping("/stream")
  15. public void streamAudio(HttpServletResponse response) throws IOException {
  16. response.setContentType("audio/mpeg");
  17. try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
  18. OutputStream outputStream = response.getOutputStream()) {
  19. byte[] buffer = new byte[1024];
  20. long filePointer = 0;
  21. while (true) {
  22. long fileLength = randomAccessFile.length();
  23. if (fileLength > filePointer) {
  24. randomAccessFile.seek(filePointer);
  25. int bytesRead;
  26. while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
  27. outputStream.write(buffer, 0, bytesRead);
  28. filePointer += bytesRead;
  29. }
  30. outputStream.flush(); // 强制将缓冲区内容写入输出流
  31. }
  32. try {
  33. Thread.sleep(2000); // 每隔2秒检查文件内容的追加
  34. } catch (InterruptedException e) {
  35. Thread.currentThread().interrupt();
  36. break;
  37. }
  38. }
  39. } catch (IOException e) {
  40. // 处理I/O异常
  41. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  42. }
  43. }
  44. }

前端代码

前端代码使用 Fetch API 请求后端音频接口,并使用 Web Audio API 处理和播放音频数据。下面是一个示例:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Audio Stream</title>
  6. </head>
  7. <body>
  8. <audio id="audio" controls autoplay></audio>
  9. <script>
  10. const audioElement = document.getElementById('audio');
  11. async function streamAudio(url) {
  12. const response = await fetch(url);
  13. const reader = response.body.getReader();
  14. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  15. const audioQueue = [];
  16. let isPlaying = false;
  17. async function processAudio({ done, value }) {
  18. if (done) {
  19. return;
  20. }
  21. const audioData = new Uint8Array(value);
  22. try {
  23. const audioBuffer = await audioContext.decodeAudioData(audioData.buffer);
  24. audioQueue.push(audioBuffer);
  25. if (!isPlaying) {
  26. playAudioBuffer(audioQueue.shift());
  27. }
  28. } catch (error) {
  29. console.error("Error decoding audio data", error);
  30. }
  31. return reader.read().then(processAudio);
  32. }
  33. function playAudioBuffer(buffer) {
  34. const sourceNode = audioContext.createBufferSource();
  35. sourceNode.buffer = buffer;
  36. sourceNode.connect(audioContext.destination);
  37. sourceNode.start();
  38. isPlaying = true;
  39. sourceNode.onended = () => {
  40. if (audioQueue.length > 0) {
  41. playAudioBuffer(audioQueue.shift());
  42. } else {
  43. isPlaying = false;
  44. }
  45. };
  46. }
  47. reader.read().then(processAudio);
  48. }
  49. streamAudio('http://localhost:8080/audio/stream');
  50. </script>
  51. </body>
  52. </html>

解释

  1. 后端:

    • response.setContentType("audio/mpeg");: 设置内容类型为 MP3。
    • 使用 RandomAccessFile 读取 MP3 文件并写入 OutputStream
    • 循环检查文件是否有新的内容追加,每隔2秒检查一次,并写入新的内容到输出流。
  2. 前端:

    • 使用 fetch 请求音频流。
    • 使用 ReadableStreamDefaultReader 读取流数据。
    • 将读取的二进制数据转换为 Uint8Array
    • 使用 AudioContextdecodeAudioData 解码并播放音频数据。
    • 使用 audioQueue 管理解码后的音频数据,确保顺序播放。

通过这种方式,前端可以从后端流式获取音频数据,并实时播放。这种方法确保了在后端追加内容时,前端能够持续接收和播放新的音频内容。