Skip to content

Instantly share code, notes, and snippets.

@seatedro
Created October 3, 2024 22:10
Show Gist options
  • Save seatedro/02b29b0e4a7fdeeff3275a987a31fbe8 to your computer and use it in GitHub Desktop.
Save seatedro/02b29b0e4a7fdeeff3275a987a31fbe8 to your computer and use it in GitHub Desktop.
furthest i got to an avi demuxer
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AVI Video Player</title>
</head>
<body>
<h1>AVI Video Player</h1>
<input type="file" id="fileInput" accept=".avi" />
<canvas id="videoCanvas"></canvas>
<div id="output"></div>
<script>
// Utility functions
function log(message) {
console.log(message);
document.getElementById("output").innerHTML += message + "<br>";
}
function readUint32(dataView, offset) {
return dataView.getUint32(offset, true);
}
function readFourCC(dataView, offset) {
return String.fromCharCode(
dataView.getUint8(offset),
dataView.getUint8(offset + 1),
dataView.getUint8(offset + 2),
dataView.getUint8(offset + 3)
);
}
// AVI Parser class
class AviChunk {
constructor(name, size, offset) {
this.name = name;
this.size = size;
this.offset = offset;
this.data = {};
}
}
class AviParser {
constructor(arrayBuffer) {
this.dataView = new DataView(arrayBuffer);
this.offset = 0;
this.chunks = [];
this.moviOffset = 0;
this.moviSize = 0;
this.avihChunk = null; // Store avih chunk data here
}
parse() {
log("Starting AVI parsing...");
this.parseRiff();
this.parseAviContents();
log("AVI parsing completed.");
return this.chunks;
}
parseRiff() {
const riffChunk = this.readChunk();
if (riffChunk.name !== "RIFF") {
throw new Error("Not a valid AVI file: RIFF header not found");
}
log(`Found RIFF chunk: size ${riffChunk.size}`);
const fileType = this.readFourCC();
if (fileType !== "AVI ") {
throw new Error("Not a valid AVI file: AVI header not found");
}
log("Confirmed AVI file type");
}
parseAviContents() {
while (this.offset < this.dataView.byteLength) {
const chunk = this.readChunk();
log(`Parsing chunk: ${chunk.name}, size: ${chunk.size}`);
if (chunk.name === "LIST") {
const listType = this.readFourCC();
log(`Found LIST chunk: ${listType}, size ${chunk.size}`);
if (listType === "movi") {
this.moviOffset = this.offset;
this.moviSize = chunk.size - 4;
this.parseMoviContents(chunk.size - 4);
} else {
this.parseListContents(chunk.size - 4, listType);
}
} else {
this.parseChunkContents(chunk);
}
this.chunks.push(chunk);
}
}
parseListContents(size, listType) {
const endOffset = this.offset + size;
while (this.offset < endOffset) {
const chunk = this.readChunk();
log(` Subchunk: ${chunk.name}, size ${chunk.size}`);
if (chunk.name === "LIST") {
const subListType = this.readFourCC();
log(` Found nested LIST chunk: ${subListType}, size ${chunk.size}`);
this.parseListContents(chunk.size - 4, subListType);
} else {
this.parseChunkContents(chunk);
}
this.chunks.push(chunk);
}
}
parseMoviContents(size) {
const endOffset = this.offset + size;
while (this.offset < endOffset) {
const chunk = this.readChunk();
log(` Subchunk: ${chunk.name}, size ${chunk.size}`);
if (chunk.name.startsWith("00db")) { // Video frame chunk
const frameData = new Uint8Array(this.dataView.buffer, this.offset, chunk.size);
this.chunks.push({name: chunk.name, size: chunk.size, offset: this.offset, data: frameData, order: this.chunks.length});
}
this.offset += chunk.size;
}
}
parseChunkContents(chunk) {
switch (chunk.name) {
case "avih":
chunk.data = this.parseAvihChunk();
this.avihChunk = chunk.data; // Store avih data for later use
break;
case "strh":
chunk.data = this.parseStrhChunk();
break;
case "strf":
chunk.data = this.parseStrfChunk();
break;
default:
// Skip unknown chunks
this.offset += chunk.size;
}
}
parseAvihChunk() {
return {
microSecPerFrame: this.readUint(4),
maxBytesPerSec: this.readUint(4),
paddingGranularity: this.readUint(4),
flags: this.readUint(4),
totalFrames: this.readUint(4),
initialFrames: this.readUint(4),
streams: this.readUint(4),
suggestedBufferSize: this.readUint(4),
width: this.readUint(4),
height: this.readUint(4),
reserved: [
this.readUint(4),
this.readUint(4),
this.readUint(4),
this.readUint(4),
],
};
}
parseStrhChunk() {
return {
fccType: this.readFourCC(),
fccHandler: this.readFourCC(),
flags: this.readUint(4),
priority: this.readUint(2),
language: this.readUint(2),
initialFrames: this.readUint(4),
scale: this.readUint(4),
rate: this.readUint(4),
start: this.readUint(4),
length: this.readUint(4),
suggestedBufferSize: this.readUint(4),
quality: this.readUint(4),
sampleSize: this.readUint(4),
frame: {
left: this.readUint(2),
top: this.readUint(2),
right: this.readUint(2),
bottom: this.readUint(2),
},
};
}
parseStrfChunk() {
return {
size: this.readUint(4),
width: this.readUint(4),
height: this.readUint(4),
planes: this.readUint(2),
bitCount: this.readUint(2),
compression: this.readFourCC(),
imageSize: this.readUint(4),
xPelsPerMeter: this.readUint(4),
yPelsPerMeter: this.readUint(4),
clrUsed: this.readUint(4),
clrImportant: this.readUint(4),
};
}
readChunk() {
const name = this.readFourCC();
const size = this.readUint(4);
return new AviChunk(name, size, this.offset);
}
readFourCC() {
const fourCC = String.fromCharCode(
this.dataView.getUint8(this.offset),
this.dataView.getUint8(this.offset + 1),
this.dataView.getUint8(this.offset + 2),
this.dataView.getUint8(this.offset + 3)
);
this.offset += 4;
return fourCC;
}
readUint(size) {
let value;
switch (size) {
case 1:
value = this.dataView.getUint8(this.offset);
break;
case 2:
value = this.dataView.getUint16(this.offset, true);
break;
case 4:
value = this.dataView.getUint32(this.offset, true);
break;
default:
throw new Error(`Unsupported uint size: ${size}`);
}
this.offset += size;
return value;
}
}
async function playAvi(file) {
const arrayBuffer = await file.arrayBuffer();
const parser = new AviParser(arrayBuffer);
const chunks = parser.parse();
// Set up canvas for rendering frames
const canvas = document.getElementById("videoCanvas");
const ctx = canvas.getContext("2d");
const strfChunk = chunks.find(chunk => chunk.name === "strf");
if (!strfChunk) {
throw new Error("Video stream information not found");
}
const width = strfChunk.data.width;
const height = strfChunk.data.height;
canvas.width = width;
canvas.height = height;
// Extract frames from movi chunk and sort by order
const frameChunks = chunks.filter(chunk => chunk.name.startsWith("00db")).sort((a, b) => a.order - b.order);
if (frameChunks.length === 0) {
throw new Error("No video frames found");
}
let currentFrame = 0;
const frameRate = 1000000 / parser.avihChunk.microSecPerFrame; // Use stored avih data for frame rate calculation
function drawFrame() {
if (currentFrame >= frameChunks.length) {
return;
}
const frameData = frameChunks[currentFrame].data;
// Assuming frameData is a Uint8Array containing pixel data in RGB format
const imageData = ctx.createImageData(width, height);
for (let i = 0, j = 0; i < frameData.length && j < imageData.data.length; i += 3, j += 4) {
imageData.data[j] = frameData[i]; // R
imageData.data[j + 1] = frameData[i + 1]; // G
imageData.data[j + 2] = frameData[i + 2]; // B
imageData.data[j + 3] = 255; // A (fully opaque)
}
ctx.putImageData(imageData, 0, 0);
currentFrame++;
setTimeout(drawFrame, 1000 / frameRate); // Synchronize frame rendering with frame rate
}
drawFrame();
}
document.getElementById("fileInput").addEventListener("change", function (event) {
const file = event.target.files[0];
if (!file) return;
try {
playAvi(file);
} catch (error) {
console.error("Error playing AVI file:", error);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment