Skip to content

Instantly share code, notes, and snippets.

@XueshiQiao
Last active August 31, 2024 03:12
Show Gist options
  • Select an option

  • Save XueshiQiao/a0e7188643e65747907a7cd24bfe7ffb to your computer and use it in GitHub Desktop.

Select an option

Save XueshiQiao/a0e7188643e65747907a7cd24bfe7ffb to your computer and use it in GitHub Desktop.
Play streamed pcm on browser
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PCM to MP3 Player (with Queue and Delay)</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lamejs/1.2.0/lame.min.js"></script>
</head>
<body>
<input type="file" id="fileInput" accept=".pcm" />
<button id="playButton" disabled>Play</button>
<audio id="audioPlayer" controls></audio>
<div id="log"></div>
<script>
const fileInput = document.getElementById("fileInput");
const playButton = document.getElementById("playButton");
const audioPlayer = document.getElementById("audioPlayer");
const logElement = document.getElementById("log");
let mediaSource;
let sourceBuffer;
let pcmData;
let appendQueue = [];
let isAppending = false;
let appendCount = 0;
function log(message) {
console.log(message);
logElement.innerHTML += message + "<br>";
logElement.scrollTop = logElement.scrollHeight;
}
fileInput.addEventListener("change", handleFileSelect);
playButton.addEventListener("click", startPlayback);
function handleFileSelect(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function (e) {
pcmData = new Int16Array(e.target.result);
playButton.disabled = false;
log(
`File loaded: ${file.name}, size: ${pcmData.length} samples`,
);
};
reader.onerror = function (e) {
log(`Error reading file: ${e.target.error}`);
};
reader.readAsArrayBuffer(file);
}
function startPlayback() {
log("Starting playback...");
mediaSource = new MediaSource();
audioPlayer.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener("sourceopen", sourceOpen);
mediaSource.addEventListener("error", (e) =>
log(`MediaSource error: ${e}`),
);
audioPlayer.addEventListener("error", (e) =>
log(`Audio player error: ${e.target.error.message}`),
);
}
function sourceOpen() {
log("MediaSource opened");
try {
sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg");
sourceBuffer.addEventListener("error", (e) =>
log(`SourceBuffer error: ${e}`),
);
sourceBuffer.addEventListener(
"updateend",
sourceBufferUpdateEnd,
);
processAudio();
} catch (e) {
log(`Error in sourceOpen: ${e.message}`);
}
}
function processAudio() {
log("Processing audio...");
const chunkSize = 1152; // MP3 frame size
const sampleRate = 24000;
const channels = 1;
const bitRate = 128;
const mp3encoder = new lamejs.Mp3Encoder(
channels,
sampleRate,
bitRate,
);
for (let i = 0; i < pcmData.length; i += chunkSize) {
const chunk = pcmData.slice(i, i + chunkSize);
const mp3Data = mp3encoder.encodeBuffer(chunk);
if (mp3Data.length > 0) {
appendToSourceBuffer(mp3Data);
}
}
const mp3End = mp3encoder.flush();
if (mp3End.length > 0) {
appendToSourceBuffer(mp3End);
}
appendQueue.push(null); // Signal end of stream
log("Audio processing completed");
}
function appendToSourceBuffer(data) {
appendQueue.push(data);
if (!isAppending) {
appendNextChunk();
}
}
function appendNextChunk() {
if (appendQueue.length === 0 || isAppending) return;
isAppending = true;
const chunk = appendQueue.shift();
if (chunk === null) {
// End of stream
mediaSource.endOfStream();
log("MediaSource stream ended");
return;
}
try {
sourceBuffer.appendBuffer(new Uint8Array(chunk));
appendCount++;
log(
`Appending chunk ${appendCount}, size: ${chunk.length} bytes`,
);
} catch (e) {
log(`Error appending to SourceBuffer: ${e.message}`);
isAppending = false;
appendNextChunk();
}
}
function sourceBufferUpdateEnd() {
log(`Chunk ${appendCount} appended successfully`);
// Introduce a small delay before processing the next chunk
setTimeout(() => {
isAppending = false;
appendNextChunk();
}, 15); // 10ms delay
}
</script>
</body>
</html>
@XueshiQiao

XueshiQiao commented Aug 31, 2024

Copy link
Copy Markdown
Author
  1. To overcome WebAPI player limitations with streamed PCM data from external sources like WebSocket or gRPC, here we use a local pcm file instead for simplicity (sint16, 24000 samplerate, mono channel), we implemented a MediaSource workaround..
  2. Encode each PCM slice into an MP3 slice using the MP3 encoder.
  3. Append mp3 slices to MediaSource one by one

@XueshiQiao

Copy link
Copy Markdown
Author

Must be run on a local server, such as with the Python command python3 -m http.server 8989.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment