0


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

以下是一个简单的案例,展示如何在后端提供音频文件数据,并在前端通过 JavaScript 获取并播放该音频。

后端 (以 Java 为例)

在后端,我们创建一个 API 来提供音频文件数据。假设你使用的是 Spring Boot 框架,代码如下:

import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public ResponseEntity<byte[]> getAudio() throws IOException {
        ClassPathResource audioFile = new ClassPathResource("audio/sample.mp3");
        byte[] audioBytes = audioFile.getInputStream().readAllBytes();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentLength(audioBytes.length);

        return ResponseEntity.ok().headers(headers).body(audioBytes);
    }
}

在这个例子中,

audio/sample.mp3

是你的音频文件路径。

前端 (JavaScript)

在前端,我们使用 JavaScript 通过

fetch

API 请求后端提供的音频数据,并将其播放出来。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', async () => {
            try {
                const response = await fetch('/audio');
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }

                const arrayBuffer = await response.arrayBuffer();
                const blob = new Blob([arrayBuffer], { type: 'audio/mp3' });
                const url = URL.createObjectURL(blob);

                const audioPlayer = document.getElementById('audioPlayer');
                audioPlayer.src = url;
                audioPlayer.play();
            } catch (error) {
                console.error('Error fetching the audio file:', error);
            }
        });
    </script>
</body>
</html>

运行步骤

  1. 确保你的 Spring Boot 项目运行,并且在 src/main/resources/audio 目录下有 sample.mp3 文件。
  2. 将前端 HTML 文件放置在你的前端项目中,并确保可以通过浏览器访问。
  3. 打开前端页面,点击 "Play Audio" 按钮,音频文件应该会开始播放。

这个例子展示了如何从后端提供音频文件,并在前端通过 JavaScript 获取和播放音频文件。如果有任何问题或需要更详细的说明,请告诉我!

这种情况 后端的音频文件是否不能再追加数据了。若我的音频文件实时再追加数据,1分钟后文件不再改变了,如何处理

对于实时追加数据的音频文件处理,我们可以使用流式传输的方式。下面是一个更新的示例,展示了如何在后端实时追加数据,并在前端通过 JavaScript 获取和播放该音频。

后端 (以 Java 为例)

假设我们使用 Spring Boot,后端需要使用流式传输的方式来发送音频数据。这里使用

OutputStream

来持续传输音频数据。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public void streamAudio(HttpServletResponse response) throws IOException {
        response.setContentType("audio/mp3");

        try (InputStream inputStream = new FileInputStream("path/to/your/audio/file.mp3");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        }
    }
}

前端 (JavaScript)

在前端,我们使用

MediaSource

API 来处理实时音频数据流。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', async () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
                
                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    const pump = async () => {
                        const { done, value } = await reader.read();
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        sourceBuffer.appendBuffer(value);
                        pump();
                    };

                    pump();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

运行步骤

  1. 确保你的 Spring Boot 项目运行,并且音频文件路径正确。
  2. 将前端 HTML 文件放置在你的前端项目中,并确保可以通过浏览器访问。
  3. 打开前端页面,点击 "Play Audio" 按钮,音频文件应该会开始实时播放。

说明

  1. 后端:通过 OutputStream 传输音频数据,可以在后端代码中实现实时数据追加。
  2. 前端:使用 MediaSource API 来处理实时音频数据流,确保流式传输能够在音频数据更新时继续播放。

这种方法可以处理实时追加数据的音频文件,并在前端进行流式播放。如果有任何进一步的问题或需要更详细的说明,请告诉我!

若我的 音频 文件 比较大的时候,是否写入一部分 前端就能播放一部分呢?

是的,当音频文件较大时,可以使用流式传输的方式,使得前端能够在接收到一部分数据后立即播放。通过使用

MediaSource

API,我们可以在后端逐步发送音频数据,前端逐步接收并播放。

后端(Java)

后端使用

OutputStream

逐步发送音频数据。以下是一个示例,展示如何实现:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public void streamAudio(HttpServletResponse response) throws IOException {
        response.setContentType("audio/mpeg");

        try (InputStream inputStream = new FileInputStream("path/to/your/large/audio/file.mp3");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        }
    }
}

前端(JavaScript)

前端使用

MediaSource

API 来处理流式音频数据。以下是一个示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    const pump = async () => {
                        const { done, value } = await reader.read();
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        sourceBuffer.appendBuffer(value);
                        pump();
                    };

                    pump();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

详细说明

  1. 后端:- 使用 OutputStream 将音频文件按块传输到前端。- 每次读取固定大小的字节块(例如 1024 字节),并写入输出流。- response.setContentType("audio/mpeg") 确保响应头的媒体类型正确。
  2. 前端:- 创建一个 MediaSource 实例,并将其 URL 设置为音频播放器的源。- 监听 sourceopen 事件,并通过 fetch API 请求后端音频流。- 使用 ReadableStreamDefaultReader 逐块读取响应体,并将数据块附加到 SourceBuffer。- 当数据传输结束时,调用 mediaSource.endOfStream() 结束流式传输。

运行步骤

  1. 确保你的 Spring Boot 项目运行,并且音频文件路径正确。
  2. 将前端 HTML 文件放置在你的前端项目中,并确保可以通过浏览器访问。
  3. 打开前端页面,点击 "Play Audio" 按钮,音频文件应该会开始播放,并在传输过程中继续播放。

这种方法能够处理大文件的实时传输,使得前端可以在接收到部分数据后立即播放。

sourceBuffer.appendBuffer(value); 报错,Field to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer is still processing an 'appendBuffer' or remove operation,如何处理

这个错误通常是由于在

appendBuffer

方法正在处理的时候再次调用

appendBuffer

导致的。为了解决这个问题,我们可以使用队列或者其他方式来管理数据的传输,以确保在

appendBuffer

完成之前不会触发下一次的

appendBuffer

调用。

一种解决方案是使用一个队列来管理数据块,确保每次只有一个数据块被添加到

SourceBuffer

中。下面是一个修改后的前端代码:

document.getElementById('playAudio').addEventListener('click', () => {
    const audioPlayer = document.getElementById('audioPlayer');
    const mediaSource = new MediaSource();
    audioPlayer.src = URL.createObjectURL(mediaSource);

    let queue = [];
    let isAppending = false;

    mediaSource.addEventListener('sourceopen', async () => {
        const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

        try {
            const response = await fetch('/audio');
            const reader = response.body.getReader();

            const pump = async () => {
                const { done, value } = await reader.read();
                if (done) {
                    mediaSource.endOfStream();
                    return;
                }

                queue.push(value);
                if (!isAppending) {
                    appendNext();
                }
            };

            const appendNext = async () => {
                if (queue.length === 0) {
                    return;
                }
                
                isAppending = true;
                const data = queue.shift();
                sourceBuffer.appendBuffer(data);
                await new Promise(resolve => {
                    sourceBuffer.addEventListener('updateend', resolve, { once: true });
                });
                isAppending = false;
                appendNext();
            };

            pump();
        } catch (error) {
            console.error('Error fetching the audio file:', error);
            mediaSource.endOfStream();
        }
    });
});

在这个修改后的代码中,我们引入了一个

queue

数组来存储待处理的数据块。当有新的数据块到达时,它们会被添加到队列中。

appendNext

函数用来处理队列中的下一个数据块,确保只有一个数据块被添加到

SourceBuffer

中。在

appendBuffer

完成之前,不会触发下一次的

appendBuffer

调用,从而避免了错误的发生。

这种方法能够解决并发调用

appendBuffer

方法导致的错误,确保数据的顺序传输并在合适的时候进行处理。

但我点击 播放的时候,没有任何数据,

看起来可能是数据还没有正确添加到

SourceBuffer

中。我们可以添加一些调试信息以确保数据正确获取和处理。同时,确保

MediaSource

SourceBuffer

的 MIME 类型匹配,并且后端传输数据正确。

以下是经过调试改进的前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            let queue = [];
            let isAppending = false;

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                sourceBuffer.addEventListener('updateend', () => {
                    isAppending = false;
                    if (queue.length > 0) {
                        appendNext();
                    }
                });

                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    const pump = async () => {
                        const { done, value } = await reader.read();
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        queue.push(value);
                        if (!isAppending) {
                            appendNext();
                        }

                        // Continue reading the stream
                        pump();
                    };

                    const appendNext = () => {
                        if (queue.length === 0 || isAppending) {
                            return;
                        }

                        isAppending = true;
                        const data = queue.shift();
                        sourceBuffer.appendBuffer(data);
                    };

                    pump();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

详细说明

  1. 错误处理:增加了 error 事件监听器来捕获 SourceBuffer 的错误。
  2. 调试信息:增加了一些 console.error 来捕获和显示错误信息,帮助调试。
  3. 队列处理:在 updateend 事件中调用 appendNext 来确保下一个数据块在当前数据块处理完后被添加到 SourceBuffer

后端确认

确保你的后端代码正确,能够流式传输音频数据:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public void streamAudio(HttpServletResponse response) throws IOException {
        response.setContentType("audio/mpeg");

        try (InputStream inputStream = new FileInputStream("path/to/your/large/audio/file.mp3");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        }
    }
}

验证步骤

  1. 确认后端音频文件路径正确,并且可以被读取。
  2. 确认前端文件在浏览器中能够正确加载。
  3. 在浏览器控制台中查看调试信息,确保数据块被正确读取并添加到 SourceBuffer

通过上述改进和调试信息,你应该能够更容易地识别和解决问题,并确保音频数据正确地流式传输和播放。

sourceBuffer.addEventListener('updateend', () => { isAppending = false; if (queue.length > 0) { appendNext(); } }); 这里面 大于0 的判断 就够了么,合适么

你是对的,简单的

queue.length > 0

判断可能会导致某些情况下无法正确处理队列中的数据,尤其是当异步操作频繁发生时。为了更稳妥的处理队列中的数据并确保

appendBuffer

处理完成后再处理下一个数据块,可以使用一个更加健壮的队列处理机制。

下面是一个改进的代码,确保在任何情况下都能正确处理队列中的数据:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            let queue = [];
            let isAppending = false;

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                sourceBuffer.addEventListener('updateend', () => {
                    if (queue.length > 0) {
                        appendNext();
                    } else {
                        isAppending = false;
                    }
                });

                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    const pump = async () => {
                        const { done, value } = await reader.read();
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        queue.push(value);
                        if (!isAppending) {
                            appendNext();
                        }

                        // Continue reading the stream
                        pump();
                    };

                    const appendNext = () => {
                        if (isAppending) return;
                        if (queue.length === 0) return;

                        isAppending = true;
                        const data = queue.shift();
                        sourceBuffer.appendBuffer(data);
                    };

                    pump();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

改进要点

  1. 双重检查:在 appendNext 中,首先检查 isAppending 标志以防止重入,然后检查队列是否为空。
  2. 明确的标志管理:在 updateend 事件处理器中,如果队列中仍有数据块,则调用 appendNext,否则设置 isAppendingfalse
  3. 调用顺序:确保 appendNext 调用顺序正确,避免在 appendBuffer 尚未完成时再次调用 appendBuffer

后端确认

确保后端代码流式传输音频数据,并设置正确的 MIME 类型:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public void streamAudio(HttpServletResponse response) throws IOException {
        response.setContentType("audio/mpeg");

        try (InputStream inputStream = new FileInputStream("path/to/your/large/audio/file.mp3");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        }
    }
}

验证步骤

  1. 后端:确保音频文件路径正确,并能够读取。
  2. 前端:确保在浏览器中正确加载 HTML 文件,点击 "Play Audio" 按钮。
  3. 控制台调试:在浏览器控制台查看调试信息,确保数据块被正确读取和处理。

通过以上改进,你应该能够正确处理大文件的实时流式传输,并确保数据块在

appendBuffer

完成后按顺序正确添加到

SourceBuffer

中。

我后端接口 每次 flush 数据后,停留了一秒,前端再请求的时候,也还是只有当后端全部flush之后,才开始执行fetch中的逻辑啊

你提到的问题表明你的前端 fetch 请求只能在后端完全 flush 数据之后处理,这是因为

fetch

API 默认使用

ReadableStream

来处理流式响应,但如果后端没有立即发送数据,前端将会等待完整的响应。

为了使前端能够立即处理接收到的部分数据,我们需要确保以下几点:

  1. 后端部分刷新数据:确保后端能够部分发送数据并保持连接。
  2. 前端处理流式响应:使用 ReadableStreamTextDecoder 在前端逐块处理接收到的数据。

后端(Java)

后端需要在每次

flush

之后发送部分数据,并保持连接。以下是修改后的示例:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public void streamAudio(HttpServletResponse response) throws IOException, InterruptedException {
        response.setContentType("audio/mpeg");

        try (InputStream inputStream = new FileInputStream("path/to/your/large/audio/file.mp3");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
                // Simulate delay for each chunk
                TimeUnit.SECONDS.sleep(1); // Simulate 1 second delay
            }
        }
    }
}

前端(JavaScript)

前端需要处理流式响应,并在数据到达时立即处理。以下是修改后的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            let queue = [];
            let isAppending = false;

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                sourceBuffer.addEventListener('updateend', () => {
                    if (queue.length > 0) {
                        appendNext();
                    } else {
                        isAppending = false;
                    }
                });

                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    const pump = async () => {
                        const { done, value } = await reader.read();
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        queue.push(value);
                        if (!isAppending) {
                            appendNext();
                        }

                        // Continue reading the stream
                        pump();
                    };

                    const appendNext = () => {
                        if (isAppending) return;
                        if (queue.length === 0) return;

                        isAppending = true;
                        const data = queue.shift();
                        sourceBuffer.appendBuffer(data);
                    };

                    pump();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

核心逻辑

  1. 后端:在每次 flush 后暂停一段时间(例如 1 秒),以模拟实时数据发送。确保部分数据发送后连接保持打开状态。
  2. 前端:使用 ReadableStream 逐块读取响应数据,确保每个数据块立即被添加到 SourceBuffer 中进行播放。

验证步骤

  1. 后端:启动 Spring Boot 应用程序,并确保音频文件路径正确。
  2. 前端:在浏览器中加载 HTML 页面,点击 "Play Audio" 按钮。
  3. 控制台调试:在浏览器控制台查看调试信息,确保数据块被正确读取和处理。

通过这种方法,你可以实现前端逐块处理后端发送的音频数据,并确保流式传输的实时性。

const pump = async () => { const { done, value } = await reader.read(); if (done) { mediaSource.endOfStream(); return; } queue.push(value); if (!isAppending) { appendNext(); } // Continue reading the stream pump(); }; 这个里面 我想打印 value,只有当后台接口 运行结束后,value才打印,这样没法后端刷新flush一次之后,前端立刻获得啊

为了实现后端在每次 flush 数据后,前端立即获得数据并处理,我们需要确保后端和前端都支持逐块传输和处理数据。在这种情况下,前端可以在每次接收到数据块时立即处理,而不是等待整个传输结束。

后端(Java)

后端需要在每次 flush 之后暂停一段时间,以模拟逐块传输数据:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;

@RestController
public class AudioController {

    @GetMapping("/audio")
    public void streamAudio(HttpServletResponse response) throws IOException, InterruptedException {
        response.setContentType("audio/mpeg");

        try (InputStream inputStream = new FileInputStream("path/to/your/large/audio/file.mp3");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
                // Simulate delay for each chunk
                TimeUnit.SECONDS.sleep(1); // Simulate 1 second delay
            }
        }
    }
}

前端(JavaScript)

前端需要使用

ReadableStream

逐块读取响应数据,并确保每个数据块立即被添加到

SourceBuffer

中进行播放。下面是修改后的前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            let queue = [];
            let isAppending = false;

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                sourceBuffer.addEventListener('updateend', () => {
                    if (queue.length > 0) {
                        appendNext();
                    } else {
                        isAppending = false;
                    }
                });

                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    const pump = async () => {
                        const { done, value } = await reader.read();
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        console.log('Received chunk:', value); // 打印接收到的块

                        queue.push(value);
                        if (!isAppending) {
                            appendNext();
                        }

                        // Continue reading the stream
                        pump();
                    };

                    const appendNext = () => {
                        if (isAppending) return;
                        if (queue.length === 0) return;

                        isAppending = true;
                        const data = queue.shift();
                        sourceBuffer.appendBuffer(data);
                    };

                    pump();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

核心逻辑

  1. 后端:确保在每次 flush 之后暂停一段时间,模拟逐块传输数据。
  2. 前端:使用 ReadableStream 逐块读取响应数据,在每次接收到数据块时立即处理。

验证步骤

  1. 后端:启动 Spring Boot 应用程序,并确保音频文件路径正确。
  2. 前端:在浏览器中加载 HTML 页面,点击 "Play Audio" 按钮。
  3. 调试信息:在浏览器控制台查看调试信息,确保数据块被正确读取和处理。

通过这种方法,你可以实现前端逐块处理后端发送的音频数据,并确保数据流式传输的实时性。

你给出的这个前端代码,也是只能等我后台 接口 运行 结束后,前端 才能执行 console.log('Received chunk:', value); // 打印接收到的块 你确定这种方式可以?

对于这种情况,可能是由于浏览器的缓冲机制导致的。浏览器会尝试缓冲一定量的数据才开始播放音频。如果音频数据以小块的方式到达,浏览器可能会等待缓冲区积累到足够的数据量才开始播放。

为了确保前端能够立即处理接收到的部分数据,我们可以尝试一种不同的方法,使用

ReadableStream

ondata

事件来逐块处理接收到的数据。

以下是修改后的前端代码,尝试使用

ondata

事件来处理数据:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            let isAppending = false;
            let sourceBuffer;

            mediaSource.addEventListener('sourceopen', async () => {
                sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                sourceBuffer.addEventListener('updateend', () => {
                    isAppending = false;
                });

                try {
                    const response = await fetch('/audio');
                    const reader = response.body.getReader();

                    reader.read().then(function process({ done, value }) {
                        if (done) {
                            mediaSource.endOfStream();
                            return;
                        }

                        console.log('Received chunk:', value); // 打印接收到的块

                        if (!isAppending) {
                            isAppending = true;
                            sourceBuffer.appendBuffer(value);
                        } else {
                            // 如果正在追加数据,则等待更新结束后再继续处理下一块数据
                            sourceBuffer.addEventListener('updateend', () => {
                                process(reader.read());
                            }, { once: true });
                        }
                    });
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

在这个修改后的代码中,我们使用

reader.read()

方法来逐块读取数据,并在每次接收到数据块时立即处理。即使后端数据还未全部传输完毕,前端也可以逐块处理接收到的数据。

请尝试使用这个修改后的前端代码,并在浏览器控制台中查看是否能够立即处理接收到的音频数据。

failed to execute 'endOfStream' on MediaSource : the updating attribute is on on one more 但我不想使用 队列

理解了,如果你不想使用队列,那么我们可以简化逻辑,仅在每次接收到新数据时立即追加到

SourceBuffer

中。我们可以通过监听

updateend

事件来确保在前一次追加数据完成后再追加下一块数据。

以下是修改后的前端代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                sourceBuffer.addEventListener('updateend', () => {
                    fetchNextChunk();
                });

                let reader;
                let readerClosed = false;

                const fetchNextChunk = async () => {
                    if (readerClosed) return;

                    const { done, value } = await reader.read();
                    if (done) {
                        mediaSource.endOfStream();
                        readerClosed = true;
                        return;
                    }

                    sourceBuffer.appendBuffer(value);
                };

                try {
                    const response = await fetch('/audio');
                    reader = response.body.getReader();
                    fetchNextChunk();
                } catch (error) {
                    console.error('Error fetching the audio file:', error);
                    mediaSource.endOfStream();
                }
            });
        });
    </script>
</body>
</html>

这段代码简化了逻辑,没有使用队列,而是在每次接收到数据时立即追加到

SourceBuffer

中。同时,通过监听

updateend

事件,确保在前一次追加数据完成后再追加下一块数据。这样就可以实现流式传输的实时性,不需要等待整个音频文件传输完毕。

若我的 mediaSource 是从后端 流的方式一次性 接收 完毕的,前端给我个简单案例

如果你的

MediaSource

是从后端一次性接收完毕的,那么你可以简化前端代码,不再需要使用流式传输的方式。下面是一个简单的前端案例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', async () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                try {
                    const response = await fetch('/audio'); // Assuming this returns the entire audio file
                    const buffer = await response.arrayBuffer();
                    sourceBuffer.appendBuffer(buffer);
                } catch (error) {
                    console.error('Error fetching or decoding the audio file:', error);
                }
            });
        });
    </script>
</body>
</html>

在这个案例中,前端代码直接使用

fetch

请求获取整个音频文件,并将其追加到

SourceBuffer

中。这种情况下,不需要使用流式传输的方式,因为整个音频文件一次性发送到前端。

但是 点击 播放 那个 小三角的时候,进度条没有总时间,sourcebuffer 打印出来后,长度为243884

如果

sourceBuffer

的长度为 243884,这可能是音频文件的总字节数,但是这并不会自动为浏览器提供音频文件的持续时间信息。为了让浏览器能够正确显示音频文件的持续时间,我们需要手动设置音频元素的持续时间。

以下是一个简单的示例,演示了如何通过获取音频文件的持续时间,并将其设置到音频元素中:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', async () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                try {
                    const response = await fetch('/audio'); // Assuming this returns the entire audio file
                    const buffer = await response.arrayBuffer();
                    sourceBuffer.appendBuffer(buffer);

                    // Get the duration of the audio file
                    const audioContext = new AudioContext();
                    const audioBuffer = await audioContext.decodeAudioData(buffer);
                    const duration = audioBuffer.duration;

                    // Set the duration to the audio element
                    audioPlayer.duration = duration;
                } catch (error) {
                    console.error('Error fetching or decoding the audio file:', error);
                }
            });
        });
    </script>
</body>
</html>

在这个示例中,我们使用

AudioContext

对象来解码音频文件并获取其持续时间,然后将持续时间设置到音频元素的

duration

属性中。这样,浏览器就能够正确显示音频文件的持续时间,并在进度条中提供总时间。

const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg'); 若为 mp4 还是这个代码么

如果你的媒体文件是 MP4 格式,那么

addSourceBuffer

中的 MIME 类型需要相应地更改为 MP4 的 MIME 类型。对于 MP4 文件,常用的 MIME 类型是

video/mp4

audio/mp4

,具体取决于文件中是否包含视频轨道或仅包含音频轨道。

以下是修改后的代码示例,假设你的 MP4 文件仅包含音频轨道:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Audio Stream</title>
</head>
<body>
    <button id="playAudio">Play Audio</button>
    <audio id="audioPlayer" controls></audio>

    <script>
        document.getElementById('playAudio').addEventListener('click', async () => {
            const audioPlayer = document.getElementById('audioPlayer');
            const mediaSource = new MediaSource();
            audioPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                try {
                    const response = await fetch('/audio'); // Assuming this returns the entire MP4 file
                    const buffer = await response.arrayBuffer();
                    sourceBuffer.appendBuffer(buffer);

                    // Get the duration of the audio file (for MP4, duration can be fetched differently)
                    const audioContext = new AudioContext();
                    const audioBuffer = await audioContext.decodeAudioData(buffer);
                    const duration = audioBuffer.duration;

                    // Set the duration to the audio element
                    audioPlayer.duration = duration;
                } catch (error) {
                    console.error('Error fetching or decoding the audio file:', error);
                }
            });
        });
    </script>
</body>
</html>

在这个代码示例中:

  1. mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"'):这里的 audio/mp4 MIME 类型用于表示 MP4 音频文件,mp4a.40.2 是一个常见的 AAC 编码的 MP4 音频格式。如果你的 MP4 文件中包含视频轨道,应该使用 video/mp4 MIME 类型。
  2. 其余代码和之前的逻辑基本相同,还是使用 AudioContext 对象来解码音频文件并获取其持续时间。

注意事项

  • 如果你的 MP4 文件包含视频轨道,应使用 video/mp4 MIME 类型,并在 <audio> 标签改为 <video> 标签。
  • AudioContext.decodeAudioData 方法对于包含视频轨道的 MP4 文件可能无法正常工作。在这种情况下,可以考虑使用 MediaSource API 的事件回调来获取媒体文件的时长。

如果你的 MP4 文件包含视频轨道,以下是适用于

<video>

标签的代码示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Video Stream</title>
</head>
<body>
    <button id="playVideo">Play Video</button>
    <video id="videoPlayer" controls></video>

    <script>
        document.getElementById('playVideo').addEventListener('click', async () => {
            const videoPlayer = document.getElementById('videoPlayer');
            const mediaSource = new MediaSource();
            videoPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                try {
                    const response = await fetch('/video'); // Assuming this returns the entire MP4 file
                    const buffer = await response.arrayBuffer();
                    sourceBuffer.appendBuffer(buffer);
                } catch (error) {
                    console.error('Error fetching or decoding the video file:', error);
                }
            });
        });
    </script>
</body>
</html>

在这里,我们假设视频文件包含视频轨道(

avc1.42E01E

)和音频轨道(

mp4a.40.2

)。这个示例适用于

<video>

标签,并且不需要额外设置时长,浏览器会自动处理。

解析出来的时间 为undefined

ChatGPT

当解析 MP4 文件的音频时,如果

AudioContext.decodeAudioData

无法正常工作,我们可以依赖浏览器的内置媒体处理功能来获取文件的持续时间。对于 MP4 文件,MediaSource API 本身会处理媒体的元数据,包括持续时间。

下面是一个简化的前端示例,演示如何使用

MediaSource

fetch

来获取并播放 MP4 文件,同时让浏览器自动解析并显示音频/视频的持续时间。

HTML 和 JavaScript 示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Static Video Stream</title>
</head>
<body>
    <button id="playMedia">Play Media</button>
    <video id="mediaPlayer" controls></video>

    <script>
        document.getElementById('playMedia').addEventListener('click', async () => {
            const mediaPlayer = document.getElementById('mediaPlayer');
            const mediaSource = new MediaSource();
            mediaPlayer.src = URL.createObjectURL(mediaSource);

            mediaSource.addEventListener('sourceopen', async () => {
                const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
                const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);

                sourceBuffer.addEventListener('error', (e) => {
                    console.error('SourceBuffer error:', e);
                });

                try {
                    const response = await fetch('/media'); // Assuming this returns the entire MP4 file
                    const reader = response.body.getReader();
                    const stream = new ReadableStream({
                        start(controller) {
                            function push() {
                                reader.read().then(({ done, value }) => {
                                    if (done) {
                                        mediaSource.endOfStream();
                                        controller.close();
                                        return;
                                    }
                                    controller.enqueue(value);
                                    push();
                                });
                            }
                            push();
                        }
                    });

                    const mediaResponse = new Response(stream);
                    const mediaBuffer = await mediaResponse.arrayBuffer();
                    sourceBuffer.appendBuffer(mediaBuffer);
                } catch (error) {
                    console.error('Error fetching or decoding the media file:', error);
                    mediaSource.endOfStream();
                }
            });

            mediaSource.addEventListener('sourceended', () => {
                console.log('Media source ended.');
                // Duration should be available by now
                console.log('Media duration:', mediaPlayer.duration);
            });
        });
    </script>
</body>
</html>

代码说明

  1. 创建 MediaSourcemediaSource 对象用于媒体数据的源管理。
  2. 绑定 MediaSource 到视频元素mediaPlayer.src = URL.createObjectURL(mediaSource)
  3. 添加 SourceBuffer:根据 MP4 的 MIME 类型添加 SourceBuffer。这里使用 video/mp4; codecs="avc1.42E01E, mp4a.40.2"
  4. 流式读取数据:使用 fetchReadableStream 来逐块读取数据,并将其传输到 SourceBuffer 中。
  5. 处理错误:添加错误处理程序,以便在 SourceBuffer 出现错误时进行日志记录。
  6. 自动处理媒体元数据MediaSource 和浏览器会自动处理媒体的元数据,包括持续时间。添加 sourceended 事件监听器来输出媒体的总持续时间。

后端示例(Java)

确保后端正确地流式传输 MP4 文件:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

@RestController
public class MediaController {

    @GetMapping("/media")
    public void streamMedia(HttpServletResponse response) throws IOException {
        response.setContentType("video/mp4");

        try (InputStream inputStream = new FileInputStream("path/to/your/large/media/file.mp4");
             OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[8192];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        }
    }
}

这样,前端可以立即开始播放视频,并且浏览器会自动处理并显示音频/视频的持续时间。


本文转载自: https://blog.csdn.net/qq_33192454/article/details/140446886
版权归原作者 qq_33192454 所有, 如有侵权,请联系我们删除。

“后端有一个音频文件,前端 js 请求后端 音频 接口,接口中音频数据是在outputStream中,给我个案例”的评论:

还没有评论