要实现前端解析后端返回的第一段流的可播放时长,你可以使用MediaSource Extensions
(MSE) API 和 AudioContext
API 结合来处理这个问题。MSE 允许你动态地创建媒体流并将其附加到HTMLMediaElement
,而AudioContext
可以用于解析音频数据并获取其时长。
下面是一个详细的解决方案:
后端代码
假设后端每隔两秒向流中写入音频数据,但第一次放置500KB的音频数据:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
public class AudioStreamController {
private static final Path AUDIO_FILE_PATH = Paths.get("/path/to/your/audio/file.mp3");
private static final int BUFFER_SIZE = 4096;
@GetMapping("/audio-stream")
public void streamAudio(HttpServletResponse response) throws IOException, InterruptedException {
response.setContentType("audio/mpeg");
response.setHeader("Connection", "keep-alive");
byte[] initialBuffer = new byte[500 * 1024]; // 500KB buffer for the first write
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
try (OutputStream outputStream = response.getOutputStream()) {
// Read the initial 500KB data
try (var inputStream = Files.newInputStream(AUDIO_FILE_PATH)) {
bytesRead = inputStream.read(initialBuffer);
outputStream.write(initialBuffer, 0, bytesRead);
outputStream.flush();
}
// Keep writing remaining data every 2 seconds
while (true) {
try (var inputStream = Files.newInputStream(AUDIO_FILE_PATH)) {
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
Thread.sleep(2000); // Wait for 2 seconds
}
}
}
}
}
}
前端代码
前端代码使用MediaSource
和AudioContext
来解析音频流的可播放时长:
<!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="playButton">Play Audio</button>
<audio id="audioPlayer" controls></audio>
<div id="duration"></div>
<script>
document.getElementById('playButton').addEventListener('click', () => {
const audioElement = document.getElementById('audioPlayer');
const mediaSource = new MediaSource();
audioElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', () => {
const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
fetch('/audio-stream')
.then(response => {
const reader = response.body.getReader();
let initialBuffer = [];
function readChunk() {
reader.read().then(({ done, value }) => {
if (done) {
mediaSource.endOfStream();
return;
}
if (initialBuffer.length === 0) {
initialBuffer = value;
getDuration(initialBuffer);
}
sourceBuffer.appendBuffer(value);
readChunk();
});
}
readChunk();
});
});
function getDuration(buffer) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioContext.decodeAudioData(buffer.buffer.slice(0, buffer.length), decodedData => {
const duration = decodedData.duration;
document.getElementById('duration').innerText = `Duration: ${duration.toFixed(2)} seconds`;
});
}
});
</script>
</body>
</html>
关键点
-
后端:
- 在第一次写入时发送500KB的音频数据。
- 每隔两秒写入后续的音频数据。
-
前端:
- 使用
MediaSource
API处理流式音频数据。 - 使用
AudioContext
API解析初始500KB数据的时长。
- 使用
注意事项
- 初始500KB数据量: 确保500KB的音频数据足够大,以包含足够的音频帧来计算时长。
- 跨域问题: 确保服务器启用了CORS,以允许前端访问音频流。
- 浏览器支持: 确保浏览器支持
MediaSource
和AudioContext
API。
通过这种方式,前端可以在接收到后端的初始音频流数据后,解析并显示该音频数据的可播放时长。
Failed to execute appendBuffer on sourceBuffer this sourceBuffer is still processing an appendBuffer or remove operation 报了 上面的错
这个错误通常是由于在前一个 appendBuffer
操作完成之前,另一个 appendBuffer
操作被调用了。为了避免这个问题,需要确保每次调用 appendBuffer
之前,SourceBuffer
处于空闲状态。你可以通过监听 updateend
事件来确保 SourceBuffer
空闲时才进行下一次 appendBuffer
操作。
以下是修正后的前端代码,使用队列来管理 appendBuffer
操作,确保每次只有一个 appendBuffer
操作在进行:
<!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="playButton">Play Audio</button>
<audio id="audioPlayer" controls></audio>
<div id="duration"></div>
<script>
document.getElementById('playButton').addEventListener('click', () => {
const audioElement = document.getElementById('audioPlayer');
const mediaSource = new MediaSource();
audioElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', () => {
const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
const fetchQueue = [];
let initialBufferReceived = false;
fetch('/audio-stream')
.then(response => {
const reader = response.body.getReader();
function processBuffer(buffer) {
if (!initialBufferReceived) {
initialBufferReceived = true;
getDuration(buffer);
}
if (sourceBuffer.updating || fetchQueue.length > 0) {
fetchQueue.push(buffer);
} else {
sourceBuffer.appendBuffer(buffer);
}
}
sourceBuffer.addEventListener('updateend', () => {
if (fetchQueue.length > 0) {
sourceBuffer.appendBuffer(fetchQueue.shift());
}
});
function readChunk() {
reader.read().then(({ done, value }) => {
if (done) {
mediaSource.endOfStream();
return;
}
processBuffer(value);
readChunk();
});
}
readChunk();
});
});
function getDuration(buffer) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioContext.decodeAudioData(buffer.buffer.slice(0, buffer.length))
.then(decodedData => {
const duration = decodedData.duration;
document.getElementById('duration').innerText = `Duration: ${duration.toFixed(2)} seconds`;
})
.catch(error => {
console.error('Error decoding audio data:', error);
});
}
});
</script>
</body>
</html>
关键点
-
队列管理:
- 使用一个数组
fetchQueue
来保存待处理的缓冲区。 - 每次
appendBuffer
操作前,检查sourceBuffer
是否在更新状态(sourceBuffer.updating
)。
- 使用一个数组
-
事件监听:
- 监听
sourceBuffer
的updateend
事件。当sourceBuffer
完成更新后,从队列中取出下一个缓冲区进行处理。
- 监听
-
首次缓冲处理:
- 在接收到第一个缓冲区时,通过
getDuration
方法使用AudioContext
解析音频数据,并计算音频时长。
- 在接收到第一个缓冲区时,通过
通过这种方式,确保每次只有一个 appendBuffer
操作在进行,避免了并发处理带来的错误。
后端解析音频文件的时长,从指定时刻开始读取流‘
为了在后端解析音频文件的时长并从指定时刻开始读取流,可以使用Java中的javax.sound.sampled
包来处理音频文件。你可以使用AudioInputStream
来读取音频数据并计算时长。此外,可以使用RandomAccessFile
来从指定位置读取音频数据流。
下面是一个示例,展示如何在Spring Boot后端解析音频文件的时长,并从指定时刻开始读取流:
Maven依赖
首先,在你的pom.xml
文件中添加必要的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>mp3spi</artifactId>
<version>1.9.5-1</version>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>tritonus-share</artifactId>
<version>0.3.6-2</version>
</dependency>
</dependencies>
控制器类
创建一个Spring Boot控制器类来处理音频文件:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.sound.sampled.*;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
@RestController
public class AudioStreamController {
private static final String AUDIO_FILE_PATH = "/path/to/your/audio/file.mp3";
@GetMapping("/audio-duration")
public String getAudioDuration() throws UnsupportedAudioFileException, IOException {
File audioFile = new File(AUDIO_FILE_PATH);
AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(audioFile);
if (fileFormat instanceof AudioFileFormat) {
long microseconds = (Long) fileFormat.properties().get("duration");
int seconds = (int) (microseconds / 1000000);
return "Audio duration: " + seconds + " seconds";
}
return "Could not determine audio duration";
}
@GetMapping("/audio-stream")
public void streamAudio(@RequestParam long startMillis, HttpServletResponse response) throws IOException, UnsupportedAudioFileException {
File audioFile = new File(AUDIO_FILE_PATH);
AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(audioFile);
AudioFormat format = fileFormat.getFormat();
long frameSize = format.getFrameSize();
float frameRate = format.getFrameRate();
// Calculate the starting frame based on the start time in milliseconds
long startFrame = (long) (startMillis / 1000.0 * frameRate);
response.setContentType("audio/mpeg");
response.setHeader("Connection", "keep-alive");
try (RandomAccessFile raf = new RandomAccessFile(audioFile, "r");
OutputStream outputStream = response.getOutputStream()) {
raf.seek(startFrame * frameSize); // Move to the starting frame
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = raf.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
}
}
关键点
-
解析音频时长:
- 使用
AudioSystem.getAudioFileFormat
获取音频文件格式信息。 - 从文件格式信息中提取
duration
属性来获取音频时长。
- 使用
-
从指定时刻读取流:
- 计算从指定时间开始的帧数。
- 使用
RandomAccessFile
定位到指定帧的位置并读取音频数据。
前端代码
前端可以发送请求以获取音频文件的时长和从指定时刻开始读取流。
<!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="playButton">Play Audio from 10 seconds</button>
<audio id="audioPlayer" controls></audio>
<div id="duration"></div>
<script>
document.getElementById('playButton').addEventListener('click', () => {
const audioElement = document.getElementById('audioPlayer');
audioElement.src = '/audio-stream?startMillis=10000'; // Start from 10 seconds
audioElement.play();
});
fetch('/audio-duration')
.then(response => response.text())
.then(duration => {
document.getElementById('duration').innerText = duration;
})
.catch(error => console.error('Error fetching audio duration:', error));
</script>
</body>
</html>
关键点
-
获取音频时长:
- 前端通过
fetch
请求/audio-duration
端点获取音频时长并显示。
- 前端通过
-
从指定时刻开始播放:
- 在点击播放按钮时,将音频源设置为从指定时间开始的流,并调用
play
方法播放音频。
- 在点击播放按钮时,将音频源设置为从指定时间开始的流,并调用
通过这种方式,你可以在后端解析音频文件的时长并从指定时刻开始读取流,前端可以根据需要播放指定时刻的音频数据。
后端解析音频文件的时长,从音频结尾处的前5秒钟开始读取流‘
为了在后端解析音频文件的时长并从音频结尾处的前5秒钟开始读取流,你可以结合使用 javax.sound.sampled
包来解析音频文件的时长,并使用 RandomAccessFile
从指定位置读取音频数据流。
后端代码示例
假设你使用 Spring Boot 来实现这一功能:
Maven 依赖
确保在你的 pom.xml
文件中添加必要的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>mp3spi</artifactId>
<version>1.9.5-1</version>
</dependency>
<dependency>
<groupId>com.googlecode.soundlibs</groupId>
<artifactId>tritonus-share</artifactId>
<version>0.3.6-2</version>
</dependency>
</dependencies>
控制器类
创建一个 Spring Boot 控制器类来处理音频文件:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.sound.sampled.*;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
@RestController
public class AudioStreamController {
private static final String AUDIO_FILE_PATH = "/path/to/your/audio/file.mp3";
@GetMapping("/audio-duration")
public String getAudioDuration() throws UnsupportedAudioFileException, IOException {
File audioFile = new File(AUDIO_FILE_PATH);
AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(audioFile);
if (fileFormat instanceof AudioFileFormat) {
long microseconds = (Long) fileFormat.properties().get("duration");
int seconds = (int) (microseconds / 1000000);
return "Audio duration: " + seconds + " seconds";
}
return "Could not determine audio duration";
}
@GetMapping("/audio-stream")
public void streamAudio(@RequestParam(required = false, defaultValue = "5") int lastSeconds, HttpServletResponse response) throws IOException, UnsupportedAudioFileException {
File audioFile = new File(AUDIO_FILE_PATH);
AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(audioFile);
AudioFormat format = fileFormat.getFormat();
long frameSize = format.getFrameSize();
float frameRate = format.getFrameRate();
// 计算音频时长
long microseconds = (Long) fileFormat.properties().get("duration");
int durationSeconds = (int) (microseconds / 1000000);
// 计算从音频结尾前指定秒数开始的帧
long startMillis = Math.max(0, (durationSeconds - lastSeconds) * 1000);
long startFrame = (long) (startMillis / 1000.0 * frameRate);
response.setContentType("audio/mpeg");
response.setHeader("Connection", "keep-alive");
try (RandomAccessFile raf = new RandomAccessFile(audioFile, "r");
OutputStream outputStream = response.getOutputStream()) {
raf.seek(startFrame * frameSize); // 移动到起始帧
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = raf.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
}
}
前端代码
前端可以发送请求以获取音频文件的时长和从音频结尾处的前5秒钟开始读取流。
<!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="playButton">Play Last 5 Seconds</button>
<audio id="audioPlayer" controls></audio>
<div id="duration"></div>
<script>
document.getElementById('playButton').addEventListener('click', () => {
const audioElement = document.getElementById('audioPlayer');
audioElement.src = '/audio-stream?lastSeconds=5'; // 从结尾前5秒开始
audioElement.play();
});
fetch('/audio-duration')
.then(response => response.text())
.then(duration => {
document.getElementById('duration').innerText = duration;
})
.catch(error => console.error('Error fetching audio duration:', error));
</script>
</body>
</html>
关键点
-
解析音频时长:
- 使用
AudioSystem.getAudioFileFormat
获取音频文件格式信息。 - 从文件格式信息中提取
duration
属性来获取音频时长。
- 使用
-
从指定时刻读取流:
- 计算从音频结尾前指定秒数开始的帧数。
- 使用
RandomAccessFile
定位到指定帧的位置并读取音频数据。
-
前端:
- 通过
fetch
请求/audio-duration
端点获取音频时长并显示。 - 在点击播放按钮时,将音频源设置为从结尾前指定时间开始的流,并调用
play
方法播放音频。
- 通过
通过这种方式,你可以在后端解析音频文件的时长,并从音频结尾处的前5秒钟开始读取流,前端可以根据需要播放指定时刻的音频数据。