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

qq_33192454 2024-08-01 11:03:01 阅读 95

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

后端 (以 Java 为例)

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

<code>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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

运行步骤

确保你的 Spring Boot 项目运行,并且在 src/main/resources/audio 目录下有 sample.mp3 文件。将前端 HTML 文件放置在你的前端项目中,并确保可以通过浏览器访问。打开前端页面,点击 "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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

运行步骤

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

说明

后端:通过 OutputStream 传输音频数据,可以在后端代码中实现实时数据追加。前端:使用 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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

详细说明

后端

使用 OutputStream 将音频文件按块传输到前端。每次读取固定大小的字节块(例如 1024 字节),并写入输出流。response.setContentType("audio/mpeg") 确保响应头的媒体类型正确。

前端

创建一个 MediaSource 实例,并将其 URL 设置为音频播放器的源。监听 sourceopen 事件,并通过 fetch API 请求后端音频流。使用 ReadableStreamDefaultReader 逐块读取响应体,并将数据块附加到 SourceBuffer。当数据传输结束时,调用 mediaSource.endOfStream() 结束流式传输。

运行步骤

确保你的 Spring Boot 项目运行,并且音频文件路径正确。将前端 HTML 文件放置在你的前端项目中,并确保可以通过浏览器访问。打开前端页面,点击 "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 中。我们可以添加一些调试信息以确保数据正确获取和处理。同时,确保 MediaSourceSourceBuffer 的 MIME 类型匹配,并且后端传输数据正确。

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

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

详细说明

错误处理:增加了 error 事件监听器来捕获 SourceBuffer 的错误。调试信息:增加了一些 console.error 来捕获和显示错误信息,帮助调试。队列处理:在 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();

}

}

}

}

验证步骤

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

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

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

你是对的,简单的 queue.length > 0 判断可能会导致某些情况下无法正确处理队列中的数据,尤其是当异步操作频繁发生时。为了更稳妥的处理队列中的数据并确保 appendBuffer 处理完成后再处理下一个数据块,可以使用一个更加健壮的队列处理机制。

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

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

改进要点

双重检查:在 appendNext 中,首先检查 isAppending 标志以防止重入,然后检查队列是否为空。明确的标志管理:在 updateend 事件处理器中,如果队列中仍有数据块,则调用 appendNext,否则设置 isAppendingfalse调用顺序:确保 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();

}

}

}

}

验证步骤

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

通过以上改进,你应该能够正确处理大文件的实时流式传输,并确保数据块在 appendBuffer 完成后按顺序正确添加到 SourceBuffer 中。

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

你提到的问题表明你的前端 fetch 请求只能在后端完全 flush 数据之后处理,这是因为 fetch API 默认使用 ReadableStream 来处理流式响应,但如果后端没有立即发送数据,前端将会等待完整的响应。

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

后端部分刷新数据:确保后端能够部分发送数据并保持连接。前端处理流式响应:使用 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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

核心逻辑

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

验证步骤

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

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

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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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>

核心逻辑

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

验证步骤

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

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

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

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

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

以下是修改后的前端代码,尝试使用 ondata 事件来处理数据:

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Real-Time Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Static Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Static Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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/mp4audio/mp4,具体取决于文件中是否包含视频轨道或仅包含音频轨道。

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

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Static Audio Stream</title>

</head>

<body>

<button id="playAudio">Play Audio</button>code>

<audio id="audioPlayer" controls></audio>code>

<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"');code>

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>

在这个代码示例中:

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

注意事项

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

如果你的 MP4 文件包含视频轨道,以下是适用于 <video> 标签的代码示例:

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Static Video Stream</title>

</head>

<body>

<button id="playVideo">Play Video</button>code>

<video id="videoPlayer" controls></video>code>

<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"');code>

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 本身会处理媒体的元数据,包括持续时间。

下面是一个简化的前端示例,演示如何使用 MediaSourcefetch 来获取并播放 MP4 文件,同时让浏览器自动解析并显示音频/视频的持续时间。

HTML 和 JavaScript 示例

<!DOCTYPE html>

<html lang="en">code>

<head>

<meta charset="UTF-8">code>

<meta name="viewport" content="width=device-width, initial-scale=1.0">code>

<title>Static Video Stream</title>

</head>

<body>

<button id="playMedia">Play Media</button>code>

<video id="mediaPlayer" controls></video>code>

<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"';code>

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>

代码说明

创建 MediaSourcemediaSource 对象用于媒体数据的源管理。绑定 MediaSource 到视频元素mediaPlayer.src = URL.createObjectURL(mediaSource)添加 SourceBuffer:根据 MP4 的 MIME 类型添加 SourceBuffer。这里使用 video/mp4; codecs="avc1.42E01E, mp4a.40.2"code>。流式读取数据:使用 fetchReadableStream 来逐块读取数据,并将其传输到 SourceBuffer 中。处理错误:添加错误处理程序,以便在 SourceBuffer 出现错误时进行日志记录。自动处理媒体元数据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();

}

}

}

}

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。