mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 10:53:50 +02:00
Add metadata extraction example
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Metadata extraction example | Mediakit</title>
|
||||
<script type="module" src="./metadata-extraction.ts"></script>
|
||||
<link rel="stylesheet" href="../index.css">
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-gray-50 text-gray-800">
|
||||
<h1 class="text-3xl">Metadata extraction example</h1>
|
||||
<p>Select or drop a media file, and Mediakit will start extracting various metadata about that file.</p>
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button class="rounded-lg bg-gray-200 hover:bg-gray-300 px-5 py-1">
|
||||
Select media file
|
||||
</button>
|
||||
|
||||
<a href="../../assets/big-buck-bunny-trimmed.mp4" download class="rounded-lg bg-gray-200 hover:bg-gray-300 px-5 group grid place-items-center" title="Download sample file">
|
||||
<img src="../../assets/download-icon.svg" class="opacity-30 group-hover:opacity-80">
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs opacity-60 mt-0.5" id="file-name"></p>
|
||||
|
||||
<hr class="w-96 my-4 border-gray-300" style="display: none;">
|
||||
|
||||
<p id="bytes-read" class="text-center text-xs font-medium mb-4"></p>
|
||||
<div id="metadata-container" class="text-sm"></div>
|
||||
|
||||
<div class="fixed top-0 left-0 flex gap-2 p-2 items-center">
|
||||
<img src="../../assets/mediakit-logo.svg" class="size-6">
|
||||
<p class="text-sm font-semibold">Mediakit</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://github.com/Vanilagy/metamuxer/tree/main/examples/metadata-extraction"
|
||||
target="_blank"
|
||||
class="flex items-center gap-2 fixed top-0 right-0 py-2 px-5 bg-gray-200 hover:bg-gray-300 rounded-bl-xl"
|
||||
>
|
||||
<img src="../../assets/github-mark.svg" class="size-6">
|
||||
<p>View source code</p>
|
||||
</a>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
@reference "../index.css";
|
||||
|
||||
ul {
|
||||
@apply py-1 px-4 bg-gray-500/10 rounded-lg;
|
||||
}
|
||||
|
||||
b {
|
||||
@apply font-semibold;
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Input, ALL_FORMATS, BlobSource } from 'mediakit';
|
||||
|
||||
const selectMediaButton = document.querySelector('button')!;
|
||||
const fileNameElement = document.querySelector('#file-name')!;
|
||||
const horizontalRule = document.querySelector('hr')!;
|
||||
const bytesReadElement = document.querySelector('#bytes-read')!;
|
||||
const metadataContainer = document.querySelector('#metadata-container')!;
|
||||
|
||||
const extractMetadata = (file: File) => {
|
||||
// Create a new input from the file
|
||||
const input = new Input({
|
||||
source: new BlobSource(file),
|
||||
formats: ALL_FORMATS, // Accept all formats
|
||||
});
|
||||
|
||||
let bytesRead = 0;
|
||||
let fileSize: number | null = null;
|
||||
|
||||
const updateBytesRead = () => {
|
||||
bytesReadElement.textContent = `Bytes read: ${bytesRead} / ${fileSize === null ? '?' : fileSize}`;
|
||||
|
||||
if (fileSize !== null) {
|
||||
bytesReadElement.textContent += ` (${(100 * bytesRead / fileSize).toPrecision(3)}% of entire file)`;
|
||||
}
|
||||
};
|
||||
|
||||
input.source.onread = (start, end) => {
|
||||
bytesRead += end - start;
|
||||
updateBytesRead();
|
||||
};
|
||||
|
||||
// Get the input's size
|
||||
void input.source.getSize().then(size => fileSize = size);
|
||||
|
||||
// This object contains all the data that gets displayed:
|
||||
const object = {
|
||||
'Format': input.getFormat().then(format => format.getName()),
|
||||
'Full MIME type': input.getMimeType(),
|
||||
'Duration': input.computeDuration().then(duration => `${duration} seconds`),
|
||||
'Tracks': input.getTracks().then(tracks => tracks.map(track => ({
|
||||
'Type': track.type,
|
||||
'Codec': track.codec,
|
||||
'Full codec string': track.getCodecParameterString(),
|
||||
'Duration': track.computeDuration().then(duration => `${duration} seconds`),
|
||||
'Language code': track.languageCode,
|
||||
...(track.isVideoTrack()
|
||||
? {
|
||||
'Coded width': `${track.codedWidth} pixels`,
|
||||
'Coded height': `${track.codedHeight} pixels`,
|
||||
'Rotation': `${track.rotation}° clockwise`,
|
||||
}
|
||||
: track.isAudioTrack()
|
||||
? {
|
||||
'Number of channels': track.numberOfChannels,
|
||||
'Sample rate': `${track.sampleRate} Hz`,
|
||||
}
|
||||
: {}),
|
||||
'Packet statistics': shortDelay().then(() => track.computePacketStats()).then(stats => ({
|
||||
'Packet count': stats.packetCount,
|
||||
'Average packet rate': `${stats.averagePacketRate} Hz${track.isVideoTrack() ? ' (FPS)' : ''}`,
|
||||
'Average bitrate': `${stats.averageBitrate} bps`,
|
||||
})),
|
||||
...(track.isVideoTrack()
|
||||
? {
|
||||
'Color space': track.getColorSpace().then(colorSpace => ({
|
||||
'Color primaries': colorSpace.primaries ?? 'Unknown',
|
||||
'Transfer characteristics': colorSpace.transfer ?? 'Unknown',
|
||||
'Matrix coefficients': colorSpace.matrix ?? 'Unknown',
|
||||
'Full range': colorSpace.fullRange ?? 'Unknown',
|
||||
'HDR': track.hasHighDynamicRange(),
|
||||
})),
|
||||
}
|
||||
: {}
|
||||
),
|
||||
}))),
|
||||
};
|
||||
|
||||
fileNameElement.textContent = file.name;
|
||||
horizontalRule.style.display = '';
|
||||
bytesReadElement.innerHTML = '';
|
||||
metadataContainer.innerHTML = '';
|
||||
|
||||
const htmlElement = renderValue(object);
|
||||
metadataContainer.append(bytesReadElement, htmlElement);
|
||||
};
|
||||
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
extractMetadata(file);
|
||||
});
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
document.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = 'copy';
|
||||
});
|
||||
|
||||
document.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
const files = event.dataTransfer?.files;
|
||||
const file = files && files.length > 0 ? files[0] : undefined;
|
||||
if (file) {
|
||||
extractMetadata(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Creates an HTML element to display any given value
|
||||
const renderValue = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
const arrayAsObject: Record<string, unknown> = Object.fromEntries(
|
||||
value.map((item, index) => [(index + 1).toString(), item]),
|
||||
);
|
||||
return renderObject(arrayAsObject);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
return renderObject(value as Record<string, unknown>);
|
||||
} else {
|
||||
const spanElement = document.createElement('span');
|
||||
spanElement.textContent = String(value);
|
||||
return spanElement;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a <ul> element that renders an object. Fields that are unresolves promises will be displayed with a
|
||||
// loading indicator.
|
||||
const renderObject = (object: Record<string, unknown>) => {
|
||||
const listElement = document.createElement('ul');
|
||||
const keys = Object.keys(object);
|
||||
|
||||
for (const key of keys) {
|
||||
const value = object[key];
|
||||
const listItem = document.createElement('li');
|
||||
const keySpan = document.createElement('b');
|
||||
keySpan.textContent = `${key}: `;
|
||||
|
||||
listItem.appendChild(keySpan);
|
||||
|
||||
if (value instanceof Promise) {
|
||||
// Show loading text until the promise resolves
|
||||
const loadingSpan = document.createElement('i');
|
||||
loadingSpan.textContent = 'Loading...';
|
||||
loadingSpan.className = 'opacity-50';
|
||||
listItem.appendChild(loadingSpan);
|
||||
|
||||
value.then((resolvedValue) => {
|
||||
// Replace the loading text with the resolved value
|
||||
listItem.removeChild(loadingSpan);
|
||||
listItem.appendChild(renderValue(resolvedValue));
|
||||
}).catch((error) => {
|
||||
// Show the promise error
|
||||
listItem.removeChild(loadingSpan);
|
||||
const errorSpan = document.createElement('span');
|
||||
errorSpan.textContent = String(error);
|
||||
errorSpan.className = 'text-red-500';
|
||||
listItem.appendChild(errorSpan);
|
||||
});
|
||||
} else {
|
||||
listItem.appendChild(renderValue(value));
|
||||
}
|
||||
|
||||
listElement.appendChild(listItem);
|
||||
}
|
||||
|
||||
return listElement;
|
||||
};
|
||||
|
||||
const shortDelay = () => {
|
||||
return new Promise(resolve => setTimeout(resolve, 1000 / 60));
|
||||
};
|
||||
Reference in New Issue
Block a user