我在expressjs中创建了后端,它有一个端点,在其字节范围内提供请求的音频/mp3文件流。
对渐进式下载说不
据我研究并了解到,渐进式下载有一个主要缺点,文件必须从头开始下载到用户想要收听的位置,甚至从末尾开始。因此,在此之后,我从某个地方,特别是从 SO 得到了一个解决方案,然后我决定实现一个后端,从参数中获取有声读物 id 及其部分索引,从标头中获取范围,并将流通过管道传输到客户端。
在解决了前端问题之后,我检查了 Spotify 网络应用程序;它还以字节范围获取歌曲的内容。我看到标题包括范围、内容长度等,就像我一样。但问题是我如何在客户端实现。需要指导以获得有效的方法。奇怪的是,我在 HTML 内容中没有找到任何视频或音频元素标签。但我使用音频标签和音频元素 Web API 事件来实现一个糟糕的实现。
1)第二块替换第一个块
我尝试在客户端获取流(在我的 React 应用程序中),并将第一个文件块作为流获取,然后使用
将流转换为 blob url,并将其提供给音频元素(或任何 npm 包,如URL.createObjectURL(blob)
)。然后我请求第二个块(将是第一个块之后的块)。然后,我再次将其转换为 blob url 并将其提供给音频元素。在这个时间范围内,我的第二个块替换为第一个块。这就是问题所在。react-player
2)解决第一个问题会产生另一个问题
我通过将从后端获得的音频块推送到一个数组中,然后从该音频块数组创建一个 Blob 对象,然后使用
创建一个 url 并使用URL.createObjectURL(audioBlobCreatedFromArray);
将其设置为状态,解决了第一个问题。每次获取下一个字节时都会重复此过程。最后,我修复了第一个问题,但每次从头开始播放音频时都会更新状态,因为每次都会获取到 Blob 的新 url。 另一个问题是,当用户尝试与进度条交互并将播放头移动到中间或末尾时。我怎样才能管理播放到那时。这是我实施的一种完全混乱且丑陋的方法。useState()
我需要什么?
我需要类似于 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,
},
});
}
};
据我研究并了解到,渐进式下载有一个主要缺点,文件必须从头开始下载到用户想要收听的位置,甚至从末尾开始。
你的研究是错误的。
浏览器足够智能,可以只下载所需的内容。对于支持范围请求的服务器,浏览器将自动发出这些范围请求。它还会根据可用的客户端带宽、存储甚至电池(如果适用)智能地进行预缓存。
需要注意的是,普通的旧 MP3 本质上是不可搜索的...所以您应该使用容器格式,除了裸 MP3 之外,它几乎可以是任何格式。
对于有声读物,无论如何您都应该使用 Opus 编解码器,这对于给定的音频质量来说效率更高。然后,您可以使用 WebM 作为容器。
在解决了前端问题之后,我检查了 Spotify 网络应用程序;它还以字节范围获取歌曲的内容。我看到标题包括范围、内容长度等,就像我一样。但问题是我如何在客户端实现
是的,浏览器会为您完成此操作。您不需要实施任何事情。只需将 URL 作为音频插入
src
即可开始。您可能仍然需要整个章节的单独音频文件,只是为了可以缓存整个章节以供离线收听,而无需缓存整本书。
奇怪的是,DOM 中没有音频或 iframe 元素。
你不需要一个。一个简单的
new Audio('https://example.com/woot.webm')
就可以了。