从expressjs服务器获取React.js应用程序中的音频流

问题描述 投票:0回答:1

问候 SO 社区,

我在expressjs中创建了后端,它有一个端点,在其字节范围内提供请求的音频/mp3文件流。

对渐进式下载说不

据我研究并了解到,渐进式下载有一个主要缺点,文件必须从头开始下载到用户想要收听的位置,甚至从末尾开始。因此,在此之后,我从某个地方,特别是从 SO 得到了一个解决方案,然后我决定实现一个后端,从参数中获取有声读物 id 及其部分索引,从标头中获取范围,并将流通过管道传输到客户端。

发现与 Spotify 类似的实现

在解决了前端问题之后,我检查了 Spotify 网络应用程序;它还以字节范围获取歌曲的内容。我看到标题包括范围、内容长度等,就像我一样。但问题是我如何在客户端实现。需要指导以获得有效的方法。奇怪的是,我在 HTML 内容中没有找到任何视频或音频元素标签。但我使用音频标签和音频元素 Web API 事件来实现一个糟糕的实现。

我为什么在这里?

1)第二块替换第一个块

我尝试在客户端获取流(在我的 React 应用程序中),并将第一个文件块作为流获取,然后使用

URL.createObjectURL(blob)
将流转换为 blob url,并将其提供给音频元素(或任何 npm 包,如
react-player
)。然后我请求第二个块(将是第一个块之后的块)。然后,我再次将其转换为 blob url 并将其提供给音频元素。在这个时间范围内,我的第二个块替换为第一个块。这就是问题所在。

2)解决第一个问题会产生另一个问题

我通过将从后端获得的音频块推送到一个数组中,然后从该音频块数组创建一个 Blob 对象,然后使用

URL.createObjectURL(audioBlobCreatedFromArray);
创建一个 url 并使用
useState()
将其设置为状态,解决了第一个问题。每次获取下一个字节时都会重复此过程。最后,我修复了第一个问题,但每次从头开始播放音频时都会更新状态,因为每次都会获取到 Blob 的新 url。 另一个问题是,当用户尝试与进度条交互并将播放头移动到中间或末尾时。我怎样才能管理播放到那时。这是我实施的一种完全混乱且丑陋的方法。

我需要什么?

我需要类似于 Spotify 的前端实现(不精确,但至少基本相似的工作示例)。就像 Spotify 如何将正确的字节范围设置到进度条上的正确位置一样,并且可以通过在该时间戳处移动播放头来从任意点获取字节范围。奇怪的是,DOM 中没有

audio
iframe
元素。

想看看我糟糕的前端实现

import React, { useState, useEffect } from "react";
import ReactPlayer from "react-player";

const AudioPlayer = ({ audioBookId, sectionIndex }) => {
  const [audioChunks, setAudioChunks] = useState([]);
  const [audioBlob, setAudioBlob] = useState(null);
  const [audioURL, setAudioURL] = useState(null);
  const [played, setPlayed] = useState(0);

  useEffect(() => {
    // Fetch the initial audio data (500KB)
    fetchAudioData(0, 50000);
  }, []);

  const fetchAudioData = async (start, end) => {
    try {
      const response = await fetch(
        `http://localhost:5000/api/audioBook/stream/${audioBookId}/${sectionIndex}`,
        { headers: { Range: `bytes=${start}-${end}` } }
      );

      if (response.ok) {
        setAudioURL()
        const blob = await response.blob();
        handleAudioChunk(blob);
      }
    } catch (error) {
      console.error("Error fetching audio data:", error);
    }
  };

  const handleAudioChunk = async (chunkData) => {
    // Append the received audio chunk to the array
    setAudioChunks((prevChunks) => [...prevChunks, chunkData]);
  };

  useEffect(() => {
    if (audioChunks.length > 0) {
      // Create a Blob from all audio chunks
      const audioBlob = new Blob(audioChunks, { type: "audio/mpeg" });

      // Create an object URL from the Blob
      const url = URL.createObjectURL(audioBlob);

      // Set the audio Blob and URL
      setAudioBlob(audioBlob);
      setAudioURL(url);
    }
  }, [audioChunks]);

  const handleProgress = (state) => {
    // Update the played percentage
    setPlayed(state.played);
    console.log(state);
    // Check if we need to fetch more data when close to the end
    if (state.played > 0.9) {
      const nextStart = Math.floor(audioChunks.length * 50000 + 1);
      const nextEnd = nextStart + 50000 - 1;
      fetchAudioData(nextStart, nextEnd);
    }
  };

  return (
    <div>
      {audioURL ? (
        <ReactPlayer
          height="80px"
          url={audioURL}
          controls
          onProgress={handleProgress}
          config={{ file: { forceAudio: true } }}
        />
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
};

export default AudioPlayer;

想看看我的后端实现吗?

const getAudioFileStream = async (req, res) => {
  const { range } = req.headers;
  const { audioBookId, sectionIndex } = req.params;

  // Check the format of range is correct or not
  if (!/^bytes=\d+-\d+$/.test(range)) {
    return res.status(400).send({
      message: "FAILED",
      description: "Range: incorrect format or missing",
    });
  }

  try {
    // Get the start and end with parsed as integer
    let [start, end] = range
      .replace(/bytes=/, "")
      .split("-")
      .map((part) => {
        return parseInt(part, 10);
      });

    // A projection string that will make sure to get the specific listen url based on section index.
    const PROJECTION = {
      sections: { $arrayElemAt: ["$sections", Number(sectionIndex)] },
    };

    const audioBookSection = await AudioBookModel.getAudioBookById(
      audioBookId,
      PROJECTION
    );

    if (audioBookSection.status === "SUCCESS") {
      if (!audioBookSection.data.sections.length) {
        return res
          .status(404)
          .send({ message: "FAILED", description: "Section not found" });
      }
      const audioFileUrl = audioBookSection.data.sections[0].listen_url;

      const audioFileHead = await axios.head(audioFileUrl);
      const fileSize = parseInt(audioFileHead.headers["content-length"], 10);

      end = end < fileSize ? end : fileSize - 1;

      const chunkSize = end - start + 1;

      res.status(206).header({
        "Content-Range": `bytes ${start}-${end}/${fileSize}`,
        "Accept-Ranges": "bytes",
        "Content-Length": chunkSize,
        "Content-Type": "audio/mpeg",
      });

      const audioFileStream = await axios.get(audioFileUrl, {
        responseType: "stream",
        headers: { Range: `bytes=${start}-${end}` },
      });

      audioFileStream.data.pipe(res);
    } else if (audioBookSection.status === "FAILED") {
      return res.status(404).send({
        message: audioBookSection.status,
        description: "Audio book not found",
      });
    } else {
      return res.status(500).send({
        message: audioBookSection.status,
        error: audioBookSection.error,
      });
    }
  } catch (error) {
    return res.status(500).send({
      status: "INTERNAL SERVER ERROR",
      error: {
        message: error.message,
        identifier: "0x000B02", // for only development purpose while debugging
        stack: error.stack,
      },
    });
  }
};
node.js reactjs streaming spotify audio-streaming
1个回答
0
投票

据我研究并了解到,渐进式下载有一个主要缺点,文件必须从头开始下载到用户想要收听的位置,甚至从末尾开始。

你的研究是错误的。

浏览器足够智能,可以只下载所需的内容。对于支持范围请求的服务器,浏览器将自动发出这些范围请求。它还会根据可用的客户端带宽、存储甚至电池(如果适用)智能地进行预缓存。

需要注意的是,普通的旧 MP3 本质上是不可搜索的...所以您应该使用容器格式,除了裸 MP3 之外,它几乎可以是任何格式。

对于有声读物,无论如何您都应该使用 Opus 编解码器,这对于给定的音频质量来说效率更高。然后,您可以使用 WebM 作为容器。

在解决了前端问题之后,我检查了 Spotify 网络应用程序;它还以字节范围获取歌曲的内容。我看到标题包括范围、内容长度等,就像我一样。但问题是我如何在客户端实现

是的,浏览器会为您完成此操作。您不需要实施任何事情。只需将 URL 作为音频插入

src
即可开始。您可能仍然需要整个章节的单独音频文件,只是为了可以缓存整个章节以供离线收听,而无需缓存整本书。

奇怪的是,DOM 中没有音频或 iframe 元素。

你不需要一个。一个简单的

new Audio('https://example.com/woot.webm')
就可以了。

© www.soinside.com 2019 - 2024. All rights reserved.