Last active
December 7, 2024 16:56
-
-
Save cw12574/4304b5a3ef20e0f74a3d63f4385fccd3 to your computer and use it in GitHub Desktop.
This file contains hidden or 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> | |
<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