Created
October 3, 2024 22:10
-
-
Save seatedro/02b29b0e4a7fdeeff3275a987a31fbe8 to your computer and use it in GitHub Desktop.
furthest i got to an avi demuxer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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