mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-30 12:23:52 +02:00
Add thumbnail generation example, fix MP3 detection code error, prefer canvas element over OffscreenCanvas
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
<link rel="stylesheet" href="../index.css">
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-gray-50 text-gray-800">
|
||||
<body class="flex flex-col items-center py-10 bg-gray-50 text-gray-800 px-2">
|
||||
<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>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<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">
|
||||
<div class="fixed top-0 left-0 flex gap-2 py-2 px-5 items-center">
|
||||
<img src="../../assets/mediakit-logo.svg" class="size-6">
|
||||
<p class="text-sm font-semibold">Mediakit</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!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>Thumbnail generation example | Mediakit</title>
|
||||
<script type="module" src="./thumbnail-generation.ts"></script>
|
||||
<link rel="stylesheet" href="../index.css">
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-gray-50 text-gray-800 px-2">
|
||||
<h1 class="text-3xl">Thumbnail generation example</h1>
|
||||
<p>Select or drop a media file, and Mediakit will extract video thumbnails for it.</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="error-element" class="text-red-500"></p>
|
||||
<div id="thumbnail-container" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"></div>
|
||||
|
||||
<div class="fixed top-0 left-0 flex gap-2 py-2 px-5 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/thumbnail-generation"
|
||||
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>
|
||||
</html>
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Input, ALL_FORMATS, BlobSource, CanvasSink } from 'mediakit';
|
||||
|
||||
const selectMediaButton = document.querySelector('button')!;
|
||||
const fileNameElement = document.querySelector('#file-name')!;
|
||||
const horizontalRule = document.querySelector('hr')!;
|
||||
const thumbnailContainer = document.querySelector('#thumbnail-container')!;
|
||||
const errorElement = document.querySelector('#error-element')!;
|
||||
|
||||
const THUMBNAIL_COUNT = 16;
|
||||
const THUMBNAIL_SIZE = 200;
|
||||
|
||||
const generateThumbnails = async (file: File) => {
|
||||
fileNameElement.textContent = file.name;
|
||||
horizontalRule.style.display = '';
|
||||
errorElement.innerHTML = '';
|
||||
thumbnailContainer.innerHTML = '';
|
||||
|
||||
try {
|
||||
// Create a new input from the file
|
||||
const input = new Input({
|
||||
source: new BlobSource(file),
|
||||
formats: ALL_FORMATS, // Accept all formats
|
||||
});
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
if (!videoTrack) {
|
||||
throw new Error('File has no video track.');
|
||||
}
|
||||
|
||||
// Compute width and height of the thumbnails such that the larger dimension is equal to THUMBNAIL_SIZE
|
||||
const width = videoTrack.displayWidth > videoTrack.displayHeight
|
||||
? THUMBNAIL_SIZE
|
||||
: Math.floor(THUMBNAIL_SIZE * videoTrack.displayWidth / videoTrack.displayHeight);
|
||||
const height = videoTrack.displayHeight > videoTrack.displayWidth
|
||||
? THUMBNAIL_SIZE
|
||||
: Math.floor(THUMBNAIL_SIZE * videoTrack.displayHeight / videoTrack.displayWidth);
|
||||
|
||||
// Create thumbnail elements
|
||||
const thumbnailElements = [];
|
||||
for (let i = 0; i < THUMBNAIL_COUNT; i++) {
|
||||
const thumbnailElement = document.createElement('div');
|
||||
thumbnailElement.className = 'rounded-lg overflow-hidden bg-gray-100 relative';
|
||||
thumbnailElement.style.width = `${width}px`;
|
||||
thumbnailElement.style.height = `${height}px`;
|
||||
thumbnailElements.push(thumbnailElement);
|
||||
thumbnailContainer.append(thumbnailElement);
|
||||
}
|
||||
|
||||
// Prepare the timestamps for the thumbnails, equally spaced between the first and last timestamp of the video
|
||||
const firstTimestamp = await videoTrack.getFirstTimestamp();
|
||||
const lastTimestamp = await videoTrack.computeDuration();
|
||||
const timestamps = Array.from(
|
||||
{ length: THUMBNAIL_COUNT },
|
||||
(_, i) => firstTimestamp + i * (lastTimestamp - firstTimestamp) / THUMBNAIL_COUNT,
|
||||
);
|
||||
|
||||
// Create a CanvasSink for extracting resized frames from the video track
|
||||
const sink = new CanvasSink(videoTrack, {
|
||||
width: Math.floor(width * window.devicePixelRatio),
|
||||
height: Math.floor(height * window.devicePixelRatio),
|
||||
fit: 'fill',
|
||||
});
|
||||
|
||||
// Iterate over all thumbnail canvases
|
||||
let i = 0;
|
||||
for await (const wrappedCanvas of sink.canvasesAtTimestamps(timestamps)) {
|
||||
const container = thumbnailElements[i]!;
|
||||
|
||||
if (wrappedCanvas) {
|
||||
const canvasElement = wrappedCanvas.canvas as HTMLCanvasElement;
|
||||
canvasElement.className = 'size-full';
|
||||
container.append(canvasElement);
|
||||
|
||||
canvasElement.animate(
|
||||
[{ transform: 'scale(1.2)' }, { transform: 'scale(1)' }],
|
||||
{ duration: 333, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' },
|
||||
);
|
||||
|
||||
const timestampElement = document.createElement('p');
|
||||
timestampElement.textContent = wrappedCanvas.timestamp.toFixed(2) + ' s';
|
||||
timestampElement.className
|
||||
= 'absolute bottom-0 right-0 bg-black/30 text-white px-1 py-0.5 text-[11px] rounded-tl-lg';
|
||||
container.append(timestampElement);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
} catch (e) {
|
||||
errorElement.textContent = String(e);
|
||||
thumbnailContainer.innerHTML = '';
|
||||
}
|
||||
};
|
||||
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
void generateThumbnails(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) {
|
||||
void generateThumbnails(file);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user