Skip to content

Instantly share code, notes, and snippets.

@cw12574
Last active December 7, 2024 16:56
Show Gist options
  • Save cw12574/4304b5a3ef20e0f74a3d63f4385fccd3 to your computer and use it in GitHub Desktop.
Save cw12574/4304b5a3ef20e0f74a3d63f4385fccd3 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Video Transcoder</title>
</head>
<body>
<h1>Video Transcoder</h1>
<input type="file" id="fileInput" accept=".webm,.mov,.avi" />
<br><br>
<button id="transcodeButton" disabled>Transcode to MP4</button>
<br><br>
<a id="downloadLink" href="#" style="display:none;">Download Output MP4</a>
<script>
let inputFile;
let encodedChunks = [];
let trackInfo = null;
document.getElementById('fileInput').addEventListener('change', function(event) {
inputFile = event.target.files[0];
if (inputFile) {
document.getElementById('transcodeButton').disabled = false;
}
});
document.getElementById('transcodeButton').addEventListener('click', function() {
transcodeVideo();
});
async function transcodeVideo() {
document.getElementById('transcodeButton').disabled = true;
encodedChunks = [];
trackInfo = null;
const arrayBuffer = await inputFile.arrayBuffer();
const videoData = new Uint8Array(arrayBuffer);
// Demux the input video file
const frames = await demuxVideo(videoData);
// Transcode frames
await encodeFrames(frames);
// Mux encoded frames into MP4
const mp4Data = muxMP4(encodedChunks, trackInfo);
// Create a blob and download link
const blob = new Blob([mp4Data], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
const downloadLink = document.getElementById('downloadLink');
downloadLink.href = url;
downloadLink.download = 'output_h264.mp4';
downloadLink.style.display = 'inline';
downloadLink.textContent = 'Download Output MP4';
}
async function demuxVideo(data) {
return new Promise((resolve, reject) => {
const mediaSource = new MediaSource();
const video = document.createElement('video');
video.style.display = 'none';
document.body.appendChild(video);
mediaSource.addEventListener('sourceopen', function() {
const mimeType = getMimeType(inputFile.name);
const sourceBuffer = mediaSource.addSourceBuffer(mimeType);
sourceBuffer.addEventListener('updateend', function() {
mediaSource.endOfStream();
video.play();
});
sourceBuffer.appendBuffer(data);
});
video.src = URL.createObjectURL(mediaSource);
video.muted = true;
video.addEventListener('canplay', function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const frames = [];
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const duration = video.duration;
const fps = 30; // Assume 30 fps if not known
const totalFrames = Math.ceil(duration * fps);
let currentFrame = 0;
function captureFrame() {
if (currentFrame >= totalFrames) {
resolve(frames);
video.pause();
document.body.removeChild(video);
return;
}
video.currentTime = (currentFrame / fps);
video.addEventListener('seeked', function onSeeked() {
video.removeEventListener('seeked', onSeeked);
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(function(blob) {
frames.push({ data: blob, timestamp: video.currentTime * 1e6 });
currentFrame++;
captureFrame();
}, 'image/png');
});
}
captureFrame();
});
});
}
function getMimeType(filename) {
const ext = filename.split('.').pop().toLowerCase();
switch (ext) {
case 'webm':
return 'video/webm; codecs="vp8, vorbis"';
case 'mov':
return 'video/quicktime';
case 'avi':
return 'video/avi';
default:
return 'video/mp4';
}
}
async function encodeFrames(frames) {
return new Promise(async (resolve, reject) => {
const init = {
output: handleEncodedChunk,
error: (e) => { console.error(e); reject(e); },
};
const firstImageBitmap = await createImageBitmap(frames[0].data);
const encoderConfig = {
codec: 'avc1.42E01E',
width: firstImageBitmap.width,
height: firstImageBitmap.height,
bitrate: 1_000_000,
framerate: 30,
};
firstImageBitmap.close();
const support = await VideoEncoder.isConfigSupported(encoderConfig);
if (!support.supported) {
console.error('Codec not supported');
reject('Codec not supported');
return;
}
const encoder = new VideoEncoder(init);
encoder.configure(encoderConfig);
trackInfo = {
width: encoderConfig.width,
height: encoderConfig.height,
timescale: 1e6,
duration: frames[frames.length - 1].timestamp + (1e6 / encoderConfig.framerate),
};
for (const frame of frames) {
const imageBitmap = await createImageBitmap(frame.data);
const videoFrame = new VideoFrame(imageBitmap, { timestamp: frame.timestamp });
encoder.encode(videoFrame);
videoFrame.close();
imageBitmap.close();
}
encoder.flush().then(resolve);
});
}
function handleEncodedChunk(chunk) {
encodedChunks.push(chunk);
}
/* MP4 Muxer in JavaScript */
function muxMP4(chunks, track) {
// Simple MP4 Muxer implementation
// Note: This is a minimal implementation and may not support all features
function uint8ArrayToBase64(bytes) {
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function createBox(type, ...payload) {
let size = 8;
for (const item of payload) {
size += item.byteLength;
}
const result = new Uint8Array(size);
const view = new DataView(result.buffer);
view.setUint32(0, size);
result.set(type, 4);
let offset = 8;
for (const item of payload) {
result.set(item, offset);
offset += item.byteLength;
}
return result;
}
function createFtyp() {
return createBox(
new Uint8Array([0x66, 0x74, 0x79, 0x70]), // 'ftyp'
new Uint8Array([0x69, 0x73, 0x6F, 0x6D]), // major_brand: 'isom'
new Uint8Array([0x00, 0x00, 0x00, 0x01]), // minor_version
new Uint8Array([0x69, 0x73, 0x6F, 0x6D, 0x61, 0x76, 0x63, 0x31]) // compatible_brands: 'isom', 'avc1'
);
}
function createMoov(track) {
// For brevity, we'll create minimal mvhd and trak boxes
function createMvhd() {
const mvhd = new Uint8Array(108);
const view = new DataView(mvhd.buffer);
view.setUint32(0, mvhd.byteLength);
mvhd.set([0x6D, 0x76, 0x68, 0x64], 4); // 'mvhd'
// version(1 byte) + flags(3 bytes)
view.setUint32(12, track.timescale); // timescale
view.setUint32(16, track.duration); // duration
return mvhd;
}
function createTrak() {
function createTkhd() {
const tkhd = new Uint8Array(92);
const view = new DataView(tkhd.buffer);
view.setUint32(0, tkhd.byteLength);
tkhd.set([0x74, 0x6B, 0x68, 0x64], 4); // 'tkhd'
// version(1 byte) + flags(3 bytes)
view.setUint32(12, 0); // track_ID
view.setUint32(20, track.duration); // duration
view.setUint16(76, track.width); // width
view.setUint16(80, track.height); // height
return tkhd;
}
function createMdia() {
function createMdhd() {
const mdhd = new Uint8Array(32);
const view = new DataView(mdhd.buffer);
view.setUint32(0, mdhd.byteLength);
mdhd.set([0x6D, 0x64, 0x68, 0x64], 4); // 'mdhd'
// version(1 byte) + flags(3 bytes)
view.setUint32(12, track.timescale); // timescale
view.setUint32(16, track.duration); // duration
return mdhd;
}
function createHdlr() {
const hdlr = new Uint8Array(33);
const view = new DataView(hdlr.buffer);
view.setUint32(0, hdlr.byteLength);
hdlr.set([0x68, 0x64, 0x6C, 0x72], 4); // 'hdlr'
// version(1 byte) + flags(3 bytes)
hdlr.set([0x76, 0x69, 0x64, 0x65], 8); // handler_type: 'vide'
return hdlr;
}
function createMinf() {
function createVmhd() {
const vmhd = new Uint8Array(20);
const view = new DataView(vmhd.buffer);
view.setUint32(0, vmhd.byteLength);
vmhd.set([0x76, 0x6D, 0x68, 0x64], 4); // 'vmhd'
// version(1 byte) + flags(3 bytes)
return vmhd;
}
function createDinf() {
const dinf = new Uint8Array(36);
const view = new DataView(dinf.buffer);
view.setUint32(0, dinf.byteLength);
dinf.set([0x64, 0x69, 0x6E, 0x66], 4); // 'dinf'
// dref box
dinf.set([0x00, 0x00, 0x00, 0x1C], 8); // size
dinf.set([0x64, 0x72, 0x65, 0x66], 12); // 'dref'
// version(1 byte) + flags(3 bytes)
dinf.set([0x00, 0x00, 0x00, 0x01], 16); // entry_count
// url box
dinf.set([0x00, 0x00, 0x00, 0x0C], 20); // size
dinf.set([0x75, 0x72, 0x6C, 0x20], 24); // 'url '
// version(1 byte) + flags(3 bytes)
return dinf;
}
function createStbl() {
function createStsd() {
// Sample Description Box
const avcCBox = createAvcC();
const stsdSize = 16 + avcCBox.byteLength;
const stsd = new Uint8Array(stsdSize);
const view = new DataView(stsd.buffer);
view.setUint32(0, stsd.byteLength);
stsd.set([0x73, 0x74, 0x73, 0x64], 4); // 'stsd'
// version(1 byte) + flags(3 bytes)
view.setUint32(8, 1); // entry_count
stsd.set(avcCBox, 12);
return stsd;
}
function createAvcC() {
// AVC Configuration Box (avc1 + avcC)
// For simplicity, using placeholder data
const avc1Box = new Uint8Array(86);
const view = new DataView(avc1Box.buffer);
view.setUint32(0, avc1Box.byteLength);
avc1Box.set([0x61, 0x76, 0x63, 0x31], 4); // 'avc1'
// reserved 6 bytes + data_reference_index
view.setUint16(24, track.width);
view.setUint16(26, track.height);
// avcC box
avc1Box.set([0x00, 0x00, 0x00, 0x20], 78); // size
avc1Box.set([0x61, 0x76, 0x63, 0x43], 82); // 'avcC'
// For simplicity, not including actual SPS/PPS data
return avc1Box;
}
const stsd = createStsd();
const stbl = createBox(
new Uint8Array([0x73, 0x74, 0x62, 0x6C]), // 'stbl'
stsd
);
return stbl;
}
const vmhd = createVmhd();
const dinf = createDinf();
const stbl = createStbl();
const minf = createBox(
new Uint8Array([0x6D, 0x69, 0x6E, 0x66]), // 'minf'
vmhd,
dinf,
stbl
);
return minf;
}
const mdhd = createMdhd();
const hdlr = createHdlr();
const minf = createMinf();
const mdia = createBox(
new Uint8Array([0x6D, 0x64, 0x69, 0x61]), // 'mdia'
mdhd,
hdlr,
minf
);
return mdia;
}
const tkhd = createTkhd();
const mdia = createMdia();
const trak = createBox(
new Uint8Array([0x74, 0x72, 0x61, 0x6B]), // 'trak'
tkhd,
mdia
);
return trak;
}
const mvhd = createMvhd();
const trak = createTrak();
const moov = createBox(
new Uint8Array([0x6D, 0x6F, 0x6F, 0x76]), // 'moov'
mvhd,
trak
);
return moov;
}
function createMdat(chunks) {
let size = 8;
for (const chunk of chunks) {
size += chunk.byteLength;
}
const mdat = new Uint8Array(size);
const view = new DataView(mdat.buffer);
view.setUint32(0, size);
mdat.set([0x6D, 0x64, 0x61, 0x74], 4); // 'mdat'
let offset = 8;
for (const chunk of chunks) {
mdat.set(new Uint8Array(chunk.data), offset);
offset += chunk.byteLength;
}
return mdat;
}
const ftyp = createFtyp();
const moov = createMoov(track);
const mdat = createMdat(chunks);
const mp4Data = new Uint8Array(ftyp.byteLength + moov.byteLength + mdat.byteLength);
mp4Data.set(ftyp, 0);
mp4Data.set(moov, ftyp.byteLength);
mp4Data.set(mdat, ftyp.byteLength + moov.byteLength);
return mp4Data;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment