后端有一个音频文件,前端 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
中。我们可以添加一些调试信息以确保数据正确获取和处理。同时,确保 MediaSource
和 SourceBuffer
的 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
,否则设置 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">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); // 打印接收到的块 你确定这种方式可以?
对于这种情况,可能是由于浏览器的缓冲机制导致的。浏览器会尝试缓冲一定量的数据才开始播放音频。如果音频数据以小块的方式到达,浏览器可能会等待缓冲区积累到足够的数据量才开始播放。
为了确保前端能够立即处理接收到的部分数据,我们可以尝试一种不同的方法,使用 ReadableStream
的 ondata
事件来逐块处理接收到的数据。
以下是修改后的前端代码,尝试使用 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/mp4
或 audio/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 本身会处理媒体的元数据,包括持续时间。
下面是一个简化的前端示例,演示如何使用 MediaSource
和 fetch
来获取并播放 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>
代码说明
创建 MediaSource:mediaSource
对象用于媒体数据的源管理。绑定 MediaSource 到视频元素:mediaPlayer.src = URL.createObjectURL(mediaSource)
。添加 SourceBuffer:根据 MP4 的 MIME 类型添加 SourceBuffer
。这里使用 video/mp4; codecs="avc1.42E01E, mp4a.40.2"code>。流式读取数据:使用
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();
}
}
}
}
这样,前端可以立即开始播放视频,并且浏览器会自动处理并显示音频/视频的持续时间。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。