以下是一个简单的案例,展示如何在后端提供音频文件数据,并在前端通过 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>
运行步骤
- 确保你的 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">
<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>
运行步骤
- 确保你的 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">
<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>
详细说明
- 后端:- 使用
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
中。我们可以添加一些调试信息以确保数据正确获取和处理。同时,确保
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>
详细说明
- 错误处理:增加了
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">
<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>
改进要点
- 双重检查:在
appendNext
中,首先检查isAppending
标志以防止重入,然后检查队列是否为空。 - 明确的标志管理:在
updateend
事件处理器中,如果队列中仍有数据块,则调用appendNext
,否则设置isAppending
为false
。 - 调用顺序:确保
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
来处理流式响应,但如果后端没有立即发送数据,前端将会等待完整的响应。
为了使前端能够立即处理接收到的部分数据,我们需要确保以下几点:
- 后端部分刷新数据:确保后端能够部分发送数据并保持连接。
- 前端处理流式响应:使用
ReadableStream
和TextDecoder
在前端逐块处理接收到的数据。
后端(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>
核心逻辑
- 后端:在每次
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">
<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>
核心逻辑
- 后端:确保在每次
flush
之后暂停一段时间,模拟逐块传输数据。 - 前端:使用
ReadableStream
逐块读取响应数据,在每次接收到数据块时立即处理。
验证步骤
- 后端:启动 Spring Boot 应用程序,并确保音频文件路径正确。
- 前端:在浏览器中加载 HTML 页面,点击 "Play Audio" 按钮。
- 调试信息:在浏览器控制台查看调试信息,确保数据块被正确读取和处理。
通过这种方法,你可以实现前端逐块处理后端发送的音频数据,并确保数据流式传输的实时性。
你给出的这个前端代码,也是只能等我后台 接口 运行 结束后,前端 才能执行 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>
在这个代码示例中:
mediaSource.addSourceBuffer('audio/mp4; codecs="mp4a.40.2"')
:这里的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">
<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>
代码说明
- 创建 MediaSource:
mediaSource
对象用于媒体数据的源管理。 - 绑定 MediaSource 到视频元素:
mediaPlayer.src = URL.createObjectURL(mediaSource)
。 - 添加 SourceBuffer:根据 MP4 的 MIME 类型添加
SourceBuffer
。这里使用video/mp4; codecs="avc1.42E01E, mp4a.40.2"
。 - 流式读取数据:使用
fetch
和ReadableStream
来逐块读取数据,并将其传输到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();
}
}
}
}
这样,前端可以立即开始播放视频,并且浏览器会自动处理并显示音频/视频的持续时间。
版权归原作者 qq_33192454 所有, 如有侵权,请联系我们删除。