Spring 项目,本地有一个录音文件,我需要以字节流的方式返回给前端,给我一个案例

在 Spring Boot 项目中,你可以使用 @RestController 注解创建一个控制器,并通过 HTTP 请求以字节流的方式将本地录音文件返回给前端。以下是一个完整的示例:

  1. 创建 Spring Boot 项目:确保你已经创建了一个 Spring Boot 项目,并添加了 spring-boot-starter-web 依赖。

  2. 创建控制器:在控制器中编写代码读取本地文件并以字节流的方式返回。

package com.example.demo.controller;

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

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping("/stream")
    public ResponseEntity<InputStreamResource> streamAudio() {
        try {
            // 本地录音文件路径
            File audioFile = new File("path/to/your/audio/file.wav");
            
            // 确保文件存在
            if (!audioFile.exists()) {
                return new ResponseEntity<>(HttpStatus.NOT_FOUND);
            }
            
            FileInputStream fileInputStream = new FileInputStream(audioFile);
            InputStreamResource inputStreamResource = new InputStreamResource(fileInputStream);

            // 设置响应头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentLength(audioFile.length());
            headers.setContentDispositionFormData("attachment", audioFile.getName());

            return new ResponseEntity<>(inputStreamResource, headers, HttpStatus.OK);
        } catch (IOException e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
  1. 运行应用:启动你的 Spring Boot 应用,然后访问 http://localhost:8080/audio/stream(假设你的应用运行在默认的 8080 端口),你应该能够以字节流的方式下载或播放录音文件。

注意事项

  • 路径配置:确保 path/to/your/audio/file.wav 是你本地录音文件的正确路径。
  • 异常处理:上面代码简单处理了文件不存在和 IO 异常,你可以根据实际需要进行更复杂的异常处理。
  • MediaType:根据你的音频文件类型,设置合适的 MediaType。例如,MediaType.APPLICATION_OCTET_STREAM 是通用的二进制流类型,你也可以使用 MediaType.AUDIO_WAVMediaType.AUDIO_MP3 等。

通过这种方式,前端可以通过 HTTP 请求获取到录音文件的字节流,并在浏览器中进行播放或下载处理。

若这个 文件,每隔2秒钟还会继续追加内容,针对追加的内容我还需要通过流给到前端,如何来做?

要实现一个可以持续传输动态更新内容(例如,每隔2秒追加内容)的音频流,你可以使用 Spring WebFlux 提供的响应式编程模型。 Spring WebFlux 支持服务器推送事件(SSE)和其他流媒体传输协议,可以处理这种持续数据流的场景。

下面是一个示例,演示如何通过 Spring WebFlux 实现动态更新内容的流媒体传输:

  1. 添加依赖:确保你的 pom.xml 中包含 spring-boot-starter-webflux 依赖。
xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 其他依赖 --> </dependencies> 
  1. 创建控制器:在控制器中使用 Flux 来流式传输音频文件的内容。
package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        // 本地录音文件路径
        File audioFile = new File("path/to/your/audio/file.wav");

        // 创建一个 Flux 来每隔2秒发送音频数据
        return Flux.interval(Duration.ofSeconds(2))
                .flatMap(tick -> {
                    try {
                        // 读取文件追加内容的逻辑
                        FileInputStream fileInputStream = new FileInputStream(audioFile);
                        byte[] buffer = new byte[1024];
                        int bytesRead = fileInputStream.read(buffer);
                        fileInputStream.close();

                        if (bytesRead > 0) {
                            DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(buffer, 0, bytesRead);
                            return Flux.just(ServerSentEvent.builder(dataBuffer).build());
                        } else {
                            return Flux.empty();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        return Flux.error(e);
                    }
                });
    }
}

解释:

  • 依赖:引入了 spring-boot-starter-webflux 以支持 WebFlux 功能。
  • 流处理Flux.interval(Duration.ofSeconds(2)) 每隔2秒生成一个事件。
  • 读取文件:每次事件触发时读取文件内容并发送给前端。
  • 响应式传输:通过 ServerSentEvent 将数据作为 SSE 事件发送到前端。

注意事项:

  1. 文件读取位置:示例代码中每次都从头读取文件并发送全部内容,实际应用中可以实现更智能的文件读取方式,例如记录上次读取的位置并仅发送新追加的部分。
  2. 前端处理:前端需要处理 SSE 事件流,可以使用 JavaScript 的 EventSource 或其他库来接收和处理音频数据流。

前端示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        sourceNode.connect(audioContext.destination);

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                sourceNode.buffer = buffer;
                sourceNode.start(0);
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

这个示例中,前端使用 EventSource 来接收 SSE 事件,并使用 AudioContext 来处理和播放音频数据。注意实际应用中需要根据具体需求调整文件读取和音频播放逻辑。

若我不用 这种 flux 的方式来给前端,还按照正常字节流的方式 能否实现呢,

可以实现这种方式,只是逻辑会稍微复杂一些。你需要在后台实现一个长连接(例如使用 ServletOutputStream),并不断地向这个连接写入新的音频数据。这种方式要求你管理好文件的读取位置以及流的刷新。以下是一个示例:

  1. 创建控制器:使用 HttpServletResponse 直接输出字节流。
package com.example.demo.controller;

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

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping("/stream")
    public void streamAudio(HttpServletResponse response) {
        File audioFile = new File("path/to/your/audio/file.wav");
        response.setContentType("audio/wav");

        try (RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r")) {
            ServletOutputStream outputStream = response.getOutputStream();
            byte[] buffer = new byte[1024];
            long filePointer = 0;

            while (true) {
                // 设置文件指针到上次读取的位置
                randomAccessFile.seek(filePointer);
                int bytesRead = randomAccessFile.read(buffer);

                if (bytesRead > 0) {
                    outputStream.write(buffer, 0, bytesRead);
                    outputStream.flush();
                    filePointer += bytesRead;
                } else {
                    // 如果没有新的数据,等待2秒后再试
                    Thread.sleep(2000);
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

解释

  • 文件读取:使用 RandomAccessFile 读取文件,这样可以控制文件指针的位置。
  • 长连接:通过 ServletOutputStream 进行字节流输出,并在每次读取后刷新流。
  • 文件指针filePointer 用于记录上次读取的位置,这样每次读取新的数据时不会重复读取。
  • 等待机制:如果没有新数据,则线程等待2秒后再继续读取。

前端示例

前端可以使用标准的 HTML5 音频标签来播放流式音频数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay>
        <source src="http://localhost:8080/audio/stream" type="audio/wav">
        Your browser does not support the audio element.
    </audio>
</body>
</html>

这个示例中,前端只是简单地使用 audio 标签来播放音频流。

注意事项

  1. 长连接:这种方式会占用一个长连接,如果同时有多个用户访问,可能会导致资源耗尽。可以考虑使用 ExecutorService 或其他方式来管理并发连接。
  2. 异常处理:需要更完善的异常处理机制,保证在发生错误时能够正确关闭流和文件。
  3. 性能:由于需要不断读取文件并保持连接,可能会对服务器性能造成影响,需要进行性能测试和优化。

这种方式可以满足你的需求,通过长连接不断地将新的音频数据推送到前端,前端则可以持续播放接收到的音频数据。

若这个 文件,每隔2秒钟还会继续追加内容,针对追加的内容我还需要通过流给到前端,使用webflux如何来做?同时支持记录上次读取的位置并仅发送新追加的部分。

为了实现每隔2秒读取文件的新内容并通过WebFlux流式传输到前端,可以使用 Flux 来不断地读取文件的新内容并发送到客户端。我们将使用 RandomAccessFile 来记录上次读取的位置,并仅发送新追加的部分。

下面是具体的实现步骤:

  1. 引入依赖:确保你的 pom.xml 中包含 spring-boot-starter-webflux 依赖。
xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 其他依赖 --> </dependencies> 
  1. 创建控制器:使用 Flux 处理文件流,记录上次读取的位置并发送新追加的部分。
package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        return Flux.interval(Duration.ofSeconds(2))
                .flatMap(tick -> {
                    try {
                        // 本地录音文件路径
                        File audioFile = new File("path/to/your/audio/file.wav");

                        // 使用 RandomAccessFile 记录上次读取的位置
                        RandomAccessFile randomAccessFile = new RandomAccessFile(audioFile, "r");
                        long filePointer = randomAccessFile.length();

                        // 创建一个新的 Flux 来每隔2秒发送音频数据
                        return Flux.generate(sink -> {
                            try {
                                randomAccessFile.seek(filePointer);
                                byte[] buffer = new byte[1024];
                                int bytesRead = randomAccessFile.read(buffer);

                                if (bytesRead > 0) {
                                    filePointer += bytesRead;
                                    DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(buffer, 0, bytesRead);
                                    sink.next(ServerSentEvent.builder(dataBuffer).build());
                                } else {
                                    sink.complete();
                                }
                            } catch (IOException e) {
                                sink.error(e);
                            }
                        });
                    } catch (IOException e) {
                        return Flux.error(e);
                    }
                });
    }
}

解释

  • 依赖:引入了 spring-boot-starter-webflux 以支持 WebFlux 功能。
  • Flux.interval:每隔2秒生成一个事件。
  • RandomAccessFile:使用 RandomAccessFile 读取文件内容,并记录上次读取的位置。
  • Flux.generate:在每次间隔事件中,读取新的文件内容并通过 ServerSentEvent 发送到客户端。

前端示例

前端可以使用标准的 HTML5 音频标签来播放流式音频数据,并通过 EventSource 接收 SSE 数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        sourceNode.connect(audioContext.destination);

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                sourceNode.buffer = buffer;
                sourceNode.start(0);
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

这个示例中,前端使用 EventSource 来接收 SSE 事件,并使用 AudioContext 来处理和播放音频数据。实际应用中需要根据具体需求调整文件读取和音频播放逻辑。

注意事项

  1. 文件读取位置:示例代码中使用 RandomAccessFilefilePointer 记录上次读取的位置,并在每次间隔事件中读取新的文件内容。
  2. 长连接:SSE 使用长连接,如果同时有多个用户访问,可能会导致资源耗尽。可以考虑使用 ExecutorService 或其他方式来管理并发连接。
  3. 性能:需要进行性能测试和优化,以保证在高并发情况下的性能和稳定性。

通过这种方式,前端可以持续接收服务器推送的新音频数据,并进行播放。

若这个 文件,首次给到前端的时候,是文件中的所有内容,之后每隔2秒钟还会继续追加内容,针对追加的内容我还需要通过流给到前端,使用webflux如何来做?同时支持记录上次读取的位置并仅发送新追加的部分。

为了实现首次发送文件的所有内容,然后每隔2秒发送追加的内容,并记录上次读取的位置,可以利用 Spring WebFluxRandomAccessFile 来实现。以下是一个完整的解决方案:

  1. 引入依赖:确保你的 pom.xml 中包含 spring-boot-starter-webflux 依赖。
xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 其他依赖 --> </dependencies> 
  1. 创建控制器:使用 Flux 处理文件流,首先发送文件的全部内容,然后每隔2秒发送追加的内容。
package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFile(audioFile, 0))
                .flatMapMany(initialData -> {
                    long initialFilePointer = initialData.length;
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(initialData).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        byte[] newData = readFile(audioFile, initialFilePointer);
                                        initialFilePointer += newData.length;
                                        return ServerSentEvent.builder(new DefaultDataBufferFactory().wrap(newData)).build();
                                    }))
                    );
                });
    }

    private byte[] readFile(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                return actualData;
            } else {
                return new byte[0];
            }
        }
    }
}

解释

  • 读取文件readFile 方法用于读取文件内容,并从指定位置开始读取。
  • 初始数据发送:在 streamAudio 方法中,首先读取文件的全部内容并发送给前端。
  • 定期发送新数据:使用 Flux.interval(Duration.ofSeconds(2)) 每隔2秒检查文件的更新部分,并发送新追加的内容。
  • 组合 Flux:通过 Flux.concat 组合初始数据和定期发送的新数据。

前端示例

前端使用标准的 HTML5 音频标签来播放流式音频数据,并通过 EventSource 接收 SSE 数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        sourceNode.connect(audioContext.destination);

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                sourceNode.buffer = buffer;
                sourceNode.start(0);
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

注意事项

  1. 文件读取位置:示例代码中使用 RandomAccessFileposition 记录上次读取的位置,并在每次间隔事件中读取新的文件内容。
  2. 长连接:SSE 使用长连接,如果同时有多个用户访问,可能会导致资源耗尽。可以考虑使用 ExecutorService 或其他方式来管理并发连接。
  3. 性能:需要进行性能测试和优化,以保证在高并发情况下的性能和稳定性。

通过这种方式,前端可以持续接收服务器推送的新音频数据,并进行播放。首次请求时发送文件的所有内容,然后每隔2秒发送新追加的内容。

上面 你给我的方式,确定,一开始的时候,给到 前端 的是文件中的全部内容,而不是1024个byte?

对不起,我之前的代码示例中确实有一个问题,它只发送了1024字节的数据。我们需要确保在初始发送时,发送文件中的所有内容,而后续每隔2秒发送追加的内容。

为了实现这个目标,我们需要在读取初始内容时读取整个文件,而不仅仅是固定大小的字节块。以下是修正后的代码:

  1. 引入依赖:确保你的 pom.xml 中包含 spring-boot-starter-webflux 依赖。
xml<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 其他依赖 --> </dependencies> 
  1. 创建控制器:使用 Flux 处理文件流,首先发送文件的全部内容,然后每隔2秒发送追加的内容。
package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFileFromStart(audioFile))
                .flatMapMany(initialData -> {
                    long initialFilePointer = initialData.length();
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(initialData).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer);
                                        initialFilePointer += newDataBuffer.readableByteCount();
                                        return ServerSentEvent.builder(newDataBuffer).build();
                                    }))
                    );
                });
    }

    private DataBuffer readFileFromStart(File file) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(buffer);
            return new DefaultDataBufferFactory().wrap(buffer);
        }
    }

    private DataBuffer readNewData(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                return new DefaultDataBufferFactory().wrap(buffer, 0, bytesRead);
            } else {
                return new DefaultDataBufferFactory().wrap(new byte[0]);
            }
        }
    }
}

解释

  • 读取整个文件readFileFromStart 方法用于读取文件的全部内容。
  • 发送初始数据:在 streamAudio 方法中,首先读取文件的全部内容并发送给前端。
  • 定期发送新数据:使用 Flux.interval(Duration.ofSeconds(2)) 每隔2秒检查文件的更新部分,并发送新追加的内容。
  • 组合 Flux:通过 Flux.concat 组合初始数据和定期发送的新数据。

前端示例

前端可以使用标准的 HTML5 音频标签来播放流式音频数据,并通过 EventSource 接收 SSE 数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        sourceNode.connect(audioContext.destination);

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                sourceNode.buffer = buffer;
                sourceNode.start(0);
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

注意事项

  1. 文件读取位置:示例代码中使用 RandomAccessFileposition 记录上次读取的位置,并在每次间隔事件中读取新的文件内容。
  2. 长连接:SSE 使用长连接,如果同时有多个用户访问,可能会导致资源耗尽。可以考虑使用 ExecutorService 或其他方式来管理并发连接。
  3. 性能:需要进行性能测试和优化,以保证在高并发情况下的性能和稳定性。

通过这种方式,前端可以持续接收服务器推送的新音频数据,并进行播放。首次请求时发送文件的所有内容,然后每隔2秒发送新追加的内容。

这种方式 是否会存在,后端追加的内容,前端无法播放的情况

在流式传输音频数据的过程中,确保前端能够持续无缝播放新追加的内容,确实有一些挑战。为了确保前端能够正确播放后端追加的音频内容,必须保证:

  1. 正确的音频数据流格式:音频数据流必须是前端播放器能够理解和连续播放的格式。
  2. 顺序性和连贯性:传输的数据必须是顺序且连贯的,不能有数据丢失或重复。

修改后的方案

以下是经过调整的后端代码,确保文件内容首次传输时包括所有数据,然后每隔2秒传输追加内容。并且,确保音频数据格式正确,以便前端能正确播放:

  1. 后端代码
package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFileFromStart(audioFile))
                .flatMapMany(initialData -> {
                    long initialFilePointer = initialData.readableByteCount();
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(initialData).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer);
                                        initialFilePointer += newDataBuffer.readableByteCount();
                                        return ServerSentEvent.builder(newDataBuffer).build();
                                    }))
                    );
                });
    }

    private DataBuffer readFileFromStart(File file) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(buffer);
            return new DefaultDataBufferFactory().wrap(buffer);
        }
    }

    private DataBuffer readNewData(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                return new DefaultDataBufferFactory().wrap(buffer, 0, bytesRead);
            } else {
                return new DefaultDataBufferFactory().wrap(new byte[0]);
            }
        }
    }
}
  1. 前端代码

前端代码需要处理接收到的音频数据并确保顺序播放。一个简单的实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        let audioQueue = [];
        let isPlaying = false;

        function playAudioBuffer(buffer) {
            let newSourceNode = audioContext.createBufferSource();
            newSourceNode.buffer = buffer;
            newSourceNode.connect(audioContext.destination);
            newSourceNode.start();
            newSourceNode.onended = () => {
                if (audioQueue.length > 0) {
                    playAudioBuffer(audioQueue.shift());
                } else {
                    isPlaying = false;
                }
            };
        }

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                if (isPlaying) {
                    audioQueue.push(buffer);
                } else {
                    isPlaying = true;
                    playAudioBuffer(buffer);
                }
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

解释

  • 音频队列:在前端维护一个音频缓冲队列 audioQueue,确保音频数据按顺序播放。
  • 音频播放逻辑:使用 AudioContext 处理和播放音频数据,确保播放结束后自动播放队列中的下一个缓冲区。
  • 初始数据传输:后端在初始连接时发送整个文件的内容,然后每隔2秒发送追加的内容。

注意事项

  1. 确保文件完整性:需要确保传输的数据完整且顺序,特别是在文件内容被追加的情况下,避免数据丢失或重复。
  2. 音频格式:确保音频数据格式正确,并且前端能够识别和播放。
  3. 流处理:在实际应用中,需要考虑音频流的网络延迟和丢包问题,以及处理错误和异常的机制。

通过上述方式,可以实现前端在初始请求时接收到完整的音频文件内容,并且每隔2秒接收和播放文件追加的新内容。

new DefaultDataBufferFactory().wrap 这个 是只有一个参数的啊

对,你是对的,DefaultDataBufferFactory().wrap 确实只有一个参数。为了简化代码并确保正确处理所有音频数据,我们需要正确管理和传输这些数据。以下是修正后的完整解决方案:

后端代码

package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFileFromStart(audioFile))
                .flatMapMany(initialData -> {
                    long initialFilePointer = initialData.readableByteCount();
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(initialData).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer);
                                        initialFilePointer += newDataBuffer.readableByteCount();
                                        return ServerSentEvent.builder(newDataBuffer).build();
                                    }))
                    );
                });
    }

    private DataBuffer readFileFromStart(File file) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(buffer);
            return new DefaultDataBufferFactory().wrap(buffer);
        }
    }

    private DataBuffer readNewData(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                return new DefaultDataBufferFactory().wrap(actualData);
            } else {
                return new DefaultDataBufferFactory().wrap(new byte[0]);
            }
        }
    }
}

前端代码

前端代码需要处理接收到的音频数据并确保顺序播放。一个简单的实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        let audioQueue = [];
        let isPlaying = false;

        function playAudioBuffer(buffer) {
            let newSourceNode = audioContext.createBufferSource();
            newSourceNode.buffer = buffer;
            newSourceNode.connect(audioContext.destination);
            newSourceNode.start();
            newSourceNode.onended = () => {
                if (audioQueue.length > 0) {
                    playAudioBuffer(audioQueue.shift());
                } else {
                    isPlaying = false;
                }
            };
        }

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                if (isPlaying) {
                    audioQueue.push(buffer);
                } else {
                    isPlaying = true;
                    playAudioBuffer(buffer);
                }
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

解释

  • 读取整个文件readFileFromStart 方法读取文件的全部内容,并在初始连接时发送给前端。
  • 读取新数据readNewData 方法从上次读取的位置开始读取文件的新内容,并每隔2秒发送给前端。
  • 前端播放逻辑:前端使用 AudioContext 处理和播放音频数据,并维护一个音频缓冲队列 audioQueue,确保音频数据按顺序播放。

注意事项

  1. 文件读取位置:示例代码中使用 RandomAccessFileposition 记录上次读取的位置,并在每次间隔事件中读取新的文件内容。
  2. 长连接:SSE 使用长连接,如果同时有多个用户访问,可能会导致资源耗尽。可以考虑使用 ExecutorService 或其他方式来管理并发连接。
  3. 性能:需要进行性能测试和优化,以保证在高并发情况下的性能和稳定性。

通过上述方式,可以实现前端在初始请求时接收到完整的音频文件内容,并且每隔2秒接收和播放文件追加的新内容。

DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer);这一段代码中报错:Transform 'initialFilePointer' into final one element array

这是因为 initialFilePointer 是在 lambda 表达式内部修改的,而 Java 需要在 lambda 中使用的变量是 finaleffectively final。为了绕过这个限制,可以使用一个长度为1的数组来代替 long 变量,使得它可以在 lambda 表达式内部修改。以下是修改后的代码:

package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFileFromStart(audioFile))
                .flatMapMany(initialData -> {
                    long[] initialFilePointer = {initialData.readableByteCount()};
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(initialData).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer[0]);
                                        initialFilePointer[0] += newDataBuffer.readableByteCount();
                                        return ServerSentEvent.builder(newDataBuffer).build();
                                    }))
                    );
                });
    }

    private DataBuffer readFileFromStart(File file) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(buffer);
            return new DefaultDataBufferFactory().wrap(buffer);
        }
    }

    private DataBuffer readNewData(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                return new DefaultDataBufferFactory().wrap(actualData);
            } else {
                return new DefaultDataBufferFactory().wrap(new byte[0]);
            }
        }
    }
}

解释

  • initialFilePointer 数组:使用一个长度为1的数组来代替 long 变量,使得它可以在 lambda 表达式内部修改。
  • 读取整个文件readFileFromStart 方法读取文件的全部内容,并在初始连接时发送给前端。
  • 读取新数据readNewData 方法从上次读取的位置开始读取文件的新内容,并每隔2秒发送给前端。

前端代码

前端代码需要处理接收到的音频数据并确保顺序播放。一个简单的实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let sourceNode = audioContext.createBufferSource();
        let audioQueue = [];
        let isPlaying = false;

        function playAudioBuffer(buffer) {
            let newSourceNode = audioContext.createBufferSource();
            newSourceNode.buffer = buffer;
            newSourceNode.connect(audioContext.destination);
            newSourceNode.start();
            newSourceNode.onended = () => {
                if (audioQueue.length > 0) {
                    playAudioBuffer(audioQueue.shift());
                } else {
                    isPlaying = false;
                }
            };
        }

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data);
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                if (isPlaying) {
                    audioQueue.push(buffer);
                } else {
                    isPlaying = true;
                    playAudioBuffer(buffer);
                }
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

通过上述修改,后端能够正确读取和传输文件内容,并且前端能够持续接收和播放追加的音频数据。

请求 后端 接口 报了 CORS error

CORS(跨域资源共享)错误通常发生在浏览器阻止了从一个域(例如,http://localhost:3000)向另一个域(例如,http://localhost:8080)发起的请求。为了允许跨域请求,后端需要设置CORS配置。

在Spring Boot项目中,可以通过以下方式配置CORS:

方法1:全局配置CORS

在Spring Boot应用程序中添加一个全局的CORS配置类:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*"); // 允许所有来源
        config.addAllowedHeader("*"); // 允许所有头
        config.addAllowedMethod("*"); // 允许所有方法(POST, GET, etc)
        config.setAllowCredentials(true); // 允许携带凭证

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return new CorsFilter(source);
    }
}

方法2:在控制器级别配置CORS

如果你只需要对某些特定的控制器或方法启用CORS,可以使用@CrossOrigin注解:

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.time.Duration;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @CrossOrigin(origins = "*") // 允许所有来源跨域请求
    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<DataBuffer>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFileFromStart(audioFile))
                .flatMapMany(initialData -> {
                    long[] initialFilePointer = {initialData.readableByteCount()};
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(initialData).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer[0]);
                                        initialFilePointer[0] += newDataBuffer.readableByteCount();
                                        return ServerSentEvent.builder(newDataBuffer).build();
                                    }))
                    );
                });
    }

    private DataBuffer readFileFromStart(File file) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(buffer);
            return new DefaultDataBufferFactory().wrap(buffer);
        }
    }

    private DataBuffer readNewData(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                return new DefaultDataBufferFactory().wrap(actualData);
            } else {
                return new DefaultDataBufferFactory().wrap(new byte[0]);
            }
        }
    }
}

确认CORS配置生效

确保在应用程序启动时CORS配置被正确加载。你可以通过检查控制台日志或使用浏览器开发者工具来确认CORS配置是否生效。

再次确认前端请求

在前端,确认你正在使用正确的URL来请求后端接口。例如:

const source = new EventSource('http://localhost:8080/audio/stream');

确保端口和路径都正确无误。

通过以上配置,应该能够解决CORS错误,使前端能够成功请求后端接口并接收音频流。

前端页面 报错:Unit8array is not defined

前端页面报错 Uint8Array is not defined 可能是因为拼写错误或者其他问题。确保拼写正确是 Uint8Array 而不是 Unit8Array

以下是修正后的前端代码,确保正确处理接收到的音频数据并确保顺序播放:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let audioQueue = [];
        let isPlaying = false;

        function playAudioBuffer(buffer) {
            let newSourceNode = audioContext.createBufferSource();
            newSourceNode.buffer = buffer;
            newSourceNode.connect(audioContext.destination);
            newSourceNode.start();
            newSourceNode.onended = () => {
                if (audioQueue.length > 0) {
                    playAudioBuffer(audioQueue.shift());
                } else {
                    isPlaying = false;
                }
            };
        }

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data); // 修正拼写错误
            audioContext.decodeAudioData(audioData.buffer, function(buffer) {
                if (isPlaying) {
                    audioQueue.push(buffer);
                } else {
                    isPlaying = true;
                    playAudioBuffer(buffer);
                }
            });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

解释

  • Uint8Array:确保拼写正确,Uint8Array 是一种 JavaScript 内置类型,用于处理字节数据。
  • 前端播放逻辑:维护一个音频缓冲队列 audioQueue,确保音频数据按顺序播放。

确认后端发送的数据格式

确保后端发送的音频数据格式正确,以便前端可以正确处理和播放音频数据。可以使用以下方式检查和确认数据格式:

  • 检查后端响应:使用浏览器开发者工具检查 EventSource 接收到的数据,确保数据格式正确。
  • 测试代码:可以先测试固定的数据块,确保前端可以正确处理和播放音频数据。

通过以上修改,前端应该能够正确处理接收到的音频数据,并确保顺序播放。如果还有其他问题,请提供更多详细信息,以便进一步排查和解决。

decodeAudioData on baseAudioContext unable todecode audio data

decodeAudioData 方法返回一个 Promise,如果解码失败,需要捕获异常并处理错误。为了确保你的音频数据正确解码并播放,修改后的前端代码如下:

前端代码修正

确保在解码音频数据时正确处理错误:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let audioQueue = [];
        let isPlaying = false;

        function playAudioBuffer(buffer) {
            let newSourceNode = audioContext.createBufferSource();
            newSourceNode.buffer = buffer;
            newSourceNode.connect(audioContext.destination);
            newSourceNode.start();
            newSourceNode.onended = () => {
                if (audioQueue.length > 0) {
                    playAudioBuffer(audioQueue.shift());
                } else {
                    isPlaying = false;
                }
            };
        }

        source.onmessage = function(event) {
            const audioData = new Uint8Array(event.data); // 确保拼写正确
            audioContext.decodeAudioData(audioData.buffer)
                .then(buffer => {
                    if (isPlaying) {
                        audioQueue.push(buffer);
                    } else {
                        isPlaying = true;
                        playAudioBuffer(buffer);
                    }
                })
                .catch(error => {
                    console.error("Unable to decode audio data", error);
                });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

检查后端发送的数据格式

确保后端发送的数据格式正确。可能需要对数据进行 Base64 编码并在前端解码。以下是相应的后端和前端代码修改。

后端代码修正

对发送的数据进行 Base64 编码:

package com.example.demo.controller;

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;

@RestController
@RequestMapping("/audio")
public class AudioController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> streamAudio() {
        File audioFile = new File("path/to/your/audio/file.wav");

        return Mono.fromCallable(() -> readFileFromStart(audioFile))
                .flatMapMany(initialData -> {
                    long[] initialFilePointer = {initialData.readableByteCount()};
                    return Flux.concat(
                            Flux.just(ServerSentEvent.builder(encodeToBase64(initialData)).build()),
                            Flux.interval(Duration.ofSeconds(2))
                                    .flatMap(tick -> Mono.fromCallable(() -> {
                                        DataBuffer newDataBuffer = readNewData(audioFile, initialFilePointer[0]);
                                        initialFilePointer[0] += newDataBuffer.readableByteCount();
                                        return ServerSentEvent.builder(encodeToBase64(newDataBuffer)).build();
                                    }))
                    );
                });
    }

    private DataBuffer readFileFromStart(File file) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            byte[] buffer = new byte[(int) randomAccessFile.length()];
            randomAccessFile.readFully(buffer);
            return new DefaultDataBufferFactory().wrap(buffer);
        }
    }

    private DataBuffer readNewData(File file, long position) throws IOException {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
            randomAccessFile.seek(position);
            byte[] buffer = new byte[1024];
            int bytesRead = randomAccessFile.read(buffer);
            if (bytesRead > 0) {
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                return new DefaultDataBufferFactory().wrap(actualData);
            } else {
                return new DefaultDataBufferFactory().wrap(new byte[0]);
            }
        }
    }

    private String encodeToBase64(DataBuffer dataBuffer) {
        byte[] bytes = new byte[dataBuffer.readableByteCount()];
        dataBuffer.read(bytes);
        return Base64.getEncoder().encodeToString(bytes);
    }
}

前端代码修正

在前端解码 Base64 数据:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Audio Stream</title>
</head>
<body>
    <audio id="audio" controls autoplay></audio>
    <script>
        const audioElement = document.getElementById('audio');
        const source = new EventSource('http://localhost:8080/audio/stream');

        let audioContext = new (window.AudioContext || window.webkitAudioContext)();
        let audioQueue = [];
        let isPlaying = false;

        function playAudioBuffer(buffer) {
            let newSourceNode = audioContext.createBufferSource();
            newSourceNode.buffer = buffer;
            newSourceNode.connect(audioContext.destination);
            newSourceNode.start();
            newSourceNode.onended = () => {
                if (audioQueue.length > 0) {
                    playAudioBuffer(audioQueue.shift());
                } else {
                    isPlaying = false;
                }
            };
        }

        source.onmessage = function(event) {
            const audioDataBase64 = event.data;
            const audioData = Uint8Array.from(atob(audioDataBase64), c => c.charCodeAt(0));
            audioContext.decodeAudioData(audioData.buffer)
                .then(buffer => {
                    if (isPlaying) {
                        audioQueue.push(buffer);
                    } else {
                        isPlaying = true;
                        playAudioBuffer(buffer);
                    }
                })
                .catch(error => {
                    console.error("Unable to decode audio data", error);
                });
        };

        source.onerror = function(event) {
            console.error("EventSource failed:", event);
            source.close();
        };
    </script>
</body>
</html>

解释

  1. 后端编码:使用 Base64 编码音频数据,以确保数据在传输过程中不会被损坏。
  2. 前端解码:使用 atob 方法将 Base64 编码的字符串转换为 Uint8Array,然后使用 decodeAudioData 解码音频数据。

通过这些修改,应该能够正确处理音频数据,并解决解码失败的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值