mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-05 14:53:49 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06f510382 | ||
|
|
14ad80df5d | ||
|
|
514a533445 | ||
|
|
9072a7a9f7 | ||
|
|
fba463f46b | ||
|
|
3621c59034 | ||
|
|
a16911874c | ||
|
|
f59a79dcf7 | ||
|
|
19b7036d82 | ||
|
|
47bae059db | ||
|
|
84c9d37cb7 | ||
|
|
3e96184a6f | ||
|
|
ee59837857 | ||
|
|
08cf34ee0d | ||
|
|
1b382d3e57 | ||
|
|
f78fc697cd | ||
|
|
39df195ada | ||
|
|
326e03cfc9 | ||
|
|
23aef27154 | ||
|
|
9daab77bb5 | ||
|
|
5a6e3eebda | ||
|
|
c4fb325329 | ||
|
|
1071bf9d49 | ||
|
|
a8fa2e4cec | ||
|
|
12f14b1f66 | ||
|
|
d4d93a7007 | ||
|
|
f44cb26d76 |
+1
-1
@@ -10,6 +10,6 @@ indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yaml]
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -1 +0,0 @@
|
||||
build/* linguist-generated
|
||||
@@ -2,6 +2,7 @@ name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -11,9 +11,7 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -41,6 +39,11 @@ jobs:
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload ${{ github.event.release.tag_name }} dist/bundles/mediabunny.cjs dist/bundles/mediabunny.min.cjs dist/bundles/mediabunny.mjs dist/bundles/mediabunny.min.mjs dist/mediabunny.d.ts
|
||||
|
||||
- name: Create Publish to npm
|
||||
run: npm publish --provenance
|
||||
env:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/node_modules
|
||||
/build
|
||||
/dist
|
||||
/dist-docs
|
||||
.DS_Store
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
[](https://www.npmjs.com/package/mediabunny)
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/public/mediabunny-logo.svg" height="180">
|
||||
<img src="./docs/public/mediabunny-logo.svg" width="180" height="180">
|
||||
</div>
|
||||
|
||||
Mediabunny is a JavaScript library for reading, writing, and converting media files (like MP4 or WebM), directly in the browser. It aims to be a complete toolkit for high-performance media operations on the web. It's written from scratch in pure TypeScript, has zero dependencies, is very performant, and is extremely tree-shakable, meaning you only include what you use. You can think of it a bit like [FFmpeg](https://ffmpeg.org/), but built from the ground up for the web.
|
||||
Mediabunny is a JavaScript library for reading, writing, and converting media files (like MP4, WebM, MP3), directly in the browser. It aims to be a complete toolkit for high-performance media operations on the web. It's written from scratch in pure TypeScript, has zero dependencies, is very performant, and is extremely tree-shakable, meaning you only include what you use. You can think of it a bit like [FFmpeg](https://ffmpeg.org/), but built from the ground up for the web.
|
||||
|
||||
[Documentation](https://mediabunny.dev) | [Examples](https://mediabunny.dev/examples) | [Sponsoring](#sponsoring) | [License](#license) | [Discord](https://discord.gg/hmpkyYuS4U)
|
||||
|
||||
@@ -16,11 +16,11 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.gling.ai/" target="_blank">
|
||||
<img src="./docs/public/sponsors/gling.svg" height="60" alt="Gling AI">
|
||||
<img src="./docs/public/sponsors/gling.svg" width="60" height="60" alt="Gling AI">
|
||||
</a>
|
||||
|
||||
<a href="https://diffusion.studio/" target="_blank">
|
||||
<img src="./docs/public/sponsors/diffusionstudio.png" height="60" alt="Diffusion Studio">
|
||||
<img src="./docs/public/sponsors/diffusionstudio.png" width="60" height="60" alt="Diffusion Studio">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +152,7 @@ For development, clone this repository and install it using a modern version of
|
||||
|
||||
```bash
|
||||
npm install # Install dependencies
|
||||
npm run watch # Development build with watch mode
|
||||
npm run watch # Build bundles on watch mode
|
||||
|
||||
npm run build # Production build with type definitions
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
|
||||
"mainEntryPointFilePath": "build/index.d.ts",
|
||||
"mainEntryPointFilePath": "dist/modules/index.d.ts",
|
||||
"bundledPackages": [],
|
||||
"compiler": {},
|
||||
"apiReport": {
|
||||
|
||||
@@ -37,20 +37,20 @@ const esmConfig = {
|
||||
|
||||
const ctxUmd = await esbuild.context({
|
||||
...umdConfig,
|
||||
outfile: 'dist/mediabunny.js',
|
||||
outfile: 'dist/bundles/mediabunny.cjs',
|
||||
});
|
||||
const ctxEsm = await esbuild.context({
|
||||
...esmConfig,
|
||||
outfile: 'dist/mediabunny.mjs',
|
||||
outfile: 'dist/bundles/mediabunny.mjs',
|
||||
});
|
||||
const ctxUmdMinified = await esbuild.context({
|
||||
...umdConfig,
|
||||
outfile: 'dist/mediabunny.min.js',
|
||||
outfile: 'dist/bundles/mediabunny.min.cjs',
|
||||
minify: true,
|
||||
});
|
||||
const ctxEsmMinified = await esbuild.context({
|
||||
...esmConfig,
|
||||
outfile: 'dist/mediabunny.min.mjs',
|
||||
outfile: 'dist/bundles/mediabunny.min.mjs',
|
||||
minify: true,
|
||||
});
|
||||
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<script src="../dist/mediabunny.js"></script>
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
|
||||
<script type="module">
|
||||
const fileInput = document.createElement('input');
|
||||
@@ -21,7 +21,7 @@
|
||||
chunked: true,
|
||||
chunkSize: 2**20
|
||||
});
|
||||
const outputFormat = new Mediabunny.MovOutputFormat();
|
||||
const outputFormat = new Mediabunny.Mp4OutputFormat();
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Cancel';
|
||||
@@ -38,7 +38,8 @@
|
||||
target
|
||||
}),
|
||||
audio: {
|
||||
codec: 'pcm-f64'
|
||||
codec: 'opus',
|
||||
bitrate: 128000,
|
||||
//numberOfChannels: 1,
|
||||
//sampleRate: 4000
|
||||
//discard: true
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<script src="../dist/mediabunny.js"></script>
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
|
||||
<script type="module">
|
||||
const fileInput = document.createElement('input');
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<button>Go</button>
|
||||
|
||||
<script src="../dist/mediabunny.js"></script>
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
|
||||
<script type="module">
|
||||
function download(blob, filename) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<script src="../dist/mediabunny.js"></script>
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
|
||||
<script type="module">
|
||||
function download(blob, filename) {
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../dist/mediabunny.js"></script>
|
||||
<script src="../dist/bundles/mediabunny.cjs"></script>
|
||||
|
||||
<script type="module">
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
|
||||
@@ -11,11 +11,14 @@ export default withMermaid({
|
||||
+ ' faster than anybunny else.',
|
||||
cleanUrls: true,
|
||||
head: [
|
||||
['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }],
|
||||
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
|
||||
['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }],
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:site_name', content: 'Mediabunny' }],
|
||||
['meta', { property: 'og:url', content: 'https://mediabunny.dev/' }],
|
||||
['meta', { property: 'og:image', content: '/mediabunny-og-image.png' }],
|
||||
['meta', { property: 'og:locale', content: 'en-US' }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
],
|
||||
themeConfig: {
|
||||
logo: '/mediabunny-logo.svg',
|
||||
@@ -72,6 +75,7 @@ export default withMermaid({
|
||||
{ icon: 'github', link: 'https://github.com/Vanilagy/mediabunny' },
|
||||
{ icon: 'discord', link: 'https://discord.gg/hmpkyYuS4U' },
|
||||
{ icon: 'x', link: 'https://x.com/vanilagy' },
|
||||
{ icon: 'bluesky', link: 'https://bsky.app/profile/vanilagy.bsky.social' },
|
||||
],
|
||||
|
||||
search: {
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ features:
|
||||
target: _self
|
||||
icon:
|
||||
src: /mingcute--magic-3-line.svg
|
||||
- title: Live recording
|
||||
- title: Live recording & streaming
|
||||
details: Record a video from live sources and stream it to a video element.
|
||||
link: /examples/live-recording
|
||||
target: _self
|
||||
|
||||
@@ -27,7 +27,9 @@ ESM is preferred because it gives you tree shaking.
|
||||
|
||||
You can also just include the library using a script tag in your HTML:
|
||||
```html
|
||||
<script src="path/to/mediabunny.js"></script>
|
||||
<script src="mediabunny.cjs"></script>
|
||||
```
|
||||
|
||||
You can download the built distribution file from the [releases page](https://github.com/Vanilagy/mediabunny/releases).
|
||||
This will add a `Mediabunny` object to the global scope.
|
||||
|
||||
You can download a built distribution file from the [releases page](https://github.com/Vanilagy/mediabunny/releases). Use the `*.cjs` builds for normal script tag inclusion, or the `*.mjs` builds for script tags with `type="module"` or direct imports via ESM. Including the `mediabunny.d.ts` declaration file in your TypeScript project will declare a global `Mediabunny` namespace.
|
||||
@@ -66,6 +66,13 @@ This library is the result of unifying these libraries into one, solving all the
|
||||
|
||||
Due to tree shaking, if you only need an MP4 or WebM muxer, this library's bundle size will still be very small.
|
||||
|
||||
### Migration
|
||||
|
||||
If you're coming from mp4-muxer or webm-muxer, you should migrate to Mediabunny. For that, refer to these guides:
|
||||
|
||||
- [Guide: Migrating from mp4-muxer to Mediabunny](https://github.com/Vanilagy/mp4-muxer/blob/main/MIGRATION-GUIDE.md)
|
||||
- [Guide: Migrating from webm-muxer to Mediabunny](https://github.com/Vanilagy/webm-muxer/blob/main/MIGRATION-GUIDE.md)
|
||||
|
||||
## Technical overview
|
||||
|
||||
At its core, Mediabunny is a collection of multiplexers and demultiplexers, one of each for every container format. Demultiplexers stream data from *sources*, while multiplexers stream data to *targets*. Every demultiplexer is capable of extracting file metadata as well as compressed media data, while multiplexers write metadata and encoded media data into a new file.
|
||||
|
||||
+6
-5
@@ -6,7 +6,7 @@ title: Mediabunny
|
||||
hero:
|
||||
name: Mediabunny
|
||||
text: Complete media toolkit
|
||||
tagline: A JavaScript library for reading, writing, and converting media files. Directly in the browser, and faster than anybunny else.
|
||||
tagline: A JavaScript library for reading, writing, and converting video and audio files. Directly in the browser, and faster than anybunny else.
|
||||
image:
|
||||
src: /mediabunny-logo.svg
|
||||
alt: Mediabunny logo
|
||||
@@ -92,6 +92,7 @@ const sponsors = {
|
||||
{ image: '/sponsors/diffusionstudio.png', name: 'Diffusion Studio', url: 'https://diffusion.studio/' },
|
||||
],
|
||||
individual: [
|
||||
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/58149663', name: 'H7GhosT', url: 'https://github.com/H7GhosT' },
|
||||
],
|
||||
@@ -123,7 +124,7 @@ npm install mediabunny
|
||||
<div class="flex flex-col lg:flex-row lg:gap-20 lg:items-center">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="inline-block" style="background: -webkit-linear-gradient(-30deg, #ff45ac, #ff78c2); -webkit-background-clip: text; color: transparent;">Read any media file, efficiently</h1>
|
||||
<p class="text-lg">Mediabunny allows you efficiently read data from any media file, no matter the size: duration, resolution, rotation, tracks, codecs and other metadata, as well as raw or decoded media data from anywhere in the file. Load only what you need.</p>
|
||||
<p class="text-lg">Mediabunny allows you efficiently read data from any video or audio file, no matter the size: duration, resolution, rotation, tracks, codecs and other metadata, as well as raw or decoded media data from anywhere in the file. Load only what you need.</p>
|
||||
<a class="!no-underline inline-flex items-center gap-1.5" :no-icon="true" href="/guide/reading-media-files">
|
||||
Docs
|
||||
<span class="vpi-arrow-right" />
|
||||
@@ -236,7 +237,7 @@ await conversion.execute();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col-reverse lg:flex-row gap-4 lg:gap-20 lg:items-center">
|
||||
<div class="flex flex-col-reverse lg:flex-row gap-4 lg:gap-20 items-center">
|
||||
<div class="relative flex-1 min-w-0">
|
||||
<div class="absolute size-70 rounded-full bg-[#ff45ac]/0 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 blur-[200px]" />
|
||||
<img class="relative" src="./assets/inspiring-io.svg">
|
||||
@@ -323,7 +324,7 @@ await conversion.execute();
|
||||
<template v-if="sponsors.gold.length > 0">
|
||||
<h3 class="!text-2xl">Gold sponsors</h3>
|
||||
<div class="flex flex-wrap mt-1 justify-center gap-1">
|
||||
<a v-for="sponsor in sponsors.gold" :href="sponsor.url" target="_blank" class="flex items-center p-2 rounded-full hover:bg-(--vp-c-gray-3) !text-[initial] !no-underline">
|
||||
<a v-for="sponsor in sponsors.gold" :href="sponsor.url" target="_blank" class="flex items-center p-2 rounded-full hover:bg-(--vp-c-gray-3) !text-(--vp-c-text-1) !no-underline">
|
||||
<img :src="sponsor.image" class="size-16 rounded-full">
|
||||
<p class="!my-0 !font-medium px-3">{{ sponsor.name }}</p>
|
||||
</a>
|
||||
@@ -332,7 +333,7 @@ await conversion.execute();
|
||||
<template v-if="sponsors.individual.length > 0">
|
||||
<h4>Individual sponsors</h4>
|
||||
<div class="flex flex-wrap mt-1 justify-center">
|
||||
<a v-for="sponsor in sponsors.individual" :href="sponsor.url" target="_blank" class="flex gap-1 w-24 flex-col items-center p-2 rounded-xl hover:bg-(--vp-c-gray-3) !text-[initial] !no-underline">
|
||||
<a v-for="sponsor in sponsors.individual" :href="sponsor.url" target="_blank" class="flex gap-1 w-24 flex-col items-center p-2 rounded-xl hover:bg-(--vp-c-gray-3) !text-(--vp-c-text-1) !no-underline">
|
||||
<img :src="sponsor.image" class="size-8 rounded-full">
|
||||
<p class="!my-0 !font-medium text-xs !leading-4">{{ sponsor.name }}</p>
|
||||
</a>
|
||||
|
||||
@@ -113,6 +113,7 @@ const compressFile = async (file: File) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<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>Live recording example | Mediabunny</title>
|
||||
<title>Live recording & streaming example | Mediabunny</title>
|
||||
<script type="module" src="../base.ts"></script>
|
||||
<script type="module" src="./live-recording.ts"></script>
|
||||
<link rel="stylesheet" href="../base.css">
|
||||
@@ -12,7 +12,7 @@
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col items-center py-10 bg-zinc-50 text-zinc-800 dark:bg-zinc-900 dark:text-zinc-200 px-2">
|
||||
<h1 class="text-3xl font-bold text-orange-500 text-center">Live recording example</h1>
|
||||
<h1 class="text-3xl font-bold text-orange-500 text-center">Live recording & streaming example</h1>
|
||||
<p class="max-w-lg text-center">The live canvas state and your microphone input will be written into a fragmented MP4 file and live-streamed to a <code><video></code> element.</p>
|
||||
|
||||
<button id="toggle-button" class="rounded-lg bg-zinc-200 dark:bg-zinc-750 hover:bg-zinc-300 dark:hover:bg-zinc-700 px-5 py-2 mt-4">
|
||||
|
||||
@@ -253,7 +253,7 @@ const runAudioIterator = async () => {
|
||||
}
|
||||
|
||||
// To play back audio, we loop over all audio chunks (typically very short) of the file and play them at the correct
|
||||
// timestamp. The result is a continuous, uninteruppted audio signal.
|
||||
// timestamp. The result is a continuous, uninterrupted audio signal.
|
||||
for await (const { buffer, timestamp } of audioBufferIterator!) {
|
||||
const node = audioContext!.createBufferSource();
|
||||
node.buffer = buffer;
|
||||
@@ -544,6 +544,7 @@ const formatSeconds = (seconds: number) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -155,6 +155,7 @@ const shortDelay = () => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<p id="error-element" class="text-red-500"></p>
|
||||
|
||||
<div class="w-full max-w-80 h-2 rounded-full bg-zinc-200 dark:bg-zinc-750 overflow-hidden" id="progress-bar-container" style="display: none;">
|
||||
<div class="h-full bg-teal-500 w-0" id="progress-bar"></div>
|
||||
<div class="h-full bg-pink-500 w-0" id="progress-bar"></div>
|
||||
</div>
|
||||
<p class="text-xs font-medium mt-1.5 tabular-nums" id="progress-text" style="display: none;"></p>
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ const generateThumbnails = async (file: File) => {
|
||||
selectMediaButton.addEventListener('click', () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/*,video/x-matroska,audio/*';
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) {
|
||||
|
||||
Generated
+24
-38
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.6",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@types/dom-mediacapture-transform": "^0.1.11",
|
||||
@@ -319,9 +319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -329,9 +329,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -339,13 +339,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
||||
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.0"
|
||||
"@babel/types": "^7.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -355,14 +355,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
|
||||
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1238,18 +1238,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"version": "0.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
|
||||
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@@ -1262,16 +1258,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
@@ -1280,9 +1266,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.29",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
|
||||
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+14
-10
@@ -1,25 +1,27 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.6",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"main": "./dist/mediabunny.js",
|
||||
"module": "./dist/mediabunny.mjs",
|
||||
"types": "./dist/mediabunny.d.ts",
|
||||
"main": "./dist/bundles/mediabunny.cjs",
|
||||
"module": "./dist/modules/index.js",
|
||||
"types": "./dist/modules/index.d.ts",
|
||||
"exports": {
|
||||
"types": "./dist/mediabunny.d.ts",
|
||||
"import": "./dist/mediabunny.mjs",
|
||||
"require": "./dist/mediabunny.js"
|
||||
"types": "./dist/modules/index.d.ts",
|
||||
"import": "./dist/modules/index.js",
|
||||
"require": "./dist/bundles/mediabunny.cjs"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"package.json",
|
||||
"LICENSE",
|
||||
"dist"
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "tsx scripts/ensure-license-headers.ts && node build.mjs && tsc -p src && api-extractor run && npm run check-docblocks && tsx scripts/append-namespace.ts",
|
||||
"build": "rm -rf dist && tsx scripts/ensure-license-headers.ts && tsc -p src && npm run fix-build-import-paths && node build.mjs && api-extractor run && npm run check-docblocks && npm run append-namespace",
|
||||
"watch": "node build.mjs --watch",
|
||||
"lint": "eslint .",
|
||||
"check": "tsc -p src --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit && rm tsconfig.vite.tsbuildinfo",
|
||||
@@ -28,7 +30,9 @@
|
||||
"docs:build": "vitepress build docs && npm run examples:build",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"dev": "vite",
|
||||
"examples:build": "vite build"
|
||||
"examples:build": "vite build",
|
||||
"fix-build-import-paths": "find dist -name \"*.js\" -type f -exec sed -i -r \"s/ from '([^']+)';/ from '\\1.js';/g\" {} \\;",
|
||||
"append-namespace": "echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { appendFileSync } from 'fs';
|
||||
|
||||
appendFileSync('dist/mediabunny.d.ts', '\nexport as namespace Mediabunny;');
|
||||
@@ -6,6 +6,9 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/// <reference types="dom-mediacapture-transform" preserve="true" />
|
||||
/// <reference types="dom-webcodecs" preserve="true" />
|
||||
|
||||
export {
|
||||
Output,
|
||||
OutputOptions,
|
||||
|
||||
+5
-3
@@ -13,6 +13,7 @@ import { IsobmffReader } from './isobmff/isobmff-reader';
|
||||
import { EBMLId, EBMLReader } from './matroska/ebml';
|
||||
import { MatroskaDemuxer } from './matroska/matroska-demuxer';
|
||||
import { Mp3Demuxer } from './mp3/mp3-demuxer';
|
||||
import { FRAME_HEADER_SIZE } from './mp3/mp3-misc';
|
||||
import { Mp3Reader } from './mp3/mp3-reader';
|
||||
import { OggDemuxer } from './ogg/ogg-demuxer';
|
||||
import { OggReader } from './ogg/ogg-reader';
|
||||
@@ -245,9 +246,10 @@ export class Mp3InputFormat extends InputFormat {
|
||||
}
|
||||
|
||||
// Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find
|
||||
// another header nearby:
|
||||
// another header right after it:
|
||||
mp3Reader.pos = firstHeader.startPos + firstHeader.totalSize;
|
||||
const secondHeader = mp3Reader.readNextFrameHeader(Math.min(framesStartPos + 4096, sourceSize));
|
||||
await mp3Reader.reader.loadRange(mp3Reader.pos, mp3Reader.pos + FRAME_HEADER_SIZE);
|
||||
const secondHeader = mp3Reader.readNextFrameHeader(mp3Reader.pos + FRAME_HEADER_SIZE);
|
||||
if (!secondHeader) {
|
||||
return false;
|
||||
}
|
||||
@@ -257,7 +259,7 @@ export class Mp3InputFormat extends InputFormat {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have found two matching MP3 frames, a strong indicator that this is an MP3 file
|
||||
// We have found two matching consecutive MP3 frames, a strong indicator that this is an MP3 file
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
isIso639Dash2LanguageCode,
|
||||
roundToMultiple,
|
||||
normalizeRotation,
|
||||
Bitstream,
|
||||
} from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { Reader } from '../reader';
|
||||
@@ -74,6 +75,7 @@ type InternalTrack = {
|
||||
fragmentLookupTable: FragmentLookupTableEntry[] | null;
|
||||
currentFragmentState: FragmentTrackState | null;
|
||||
fragments: Fragment[];
|
||||
fragmentsWithKeyFrame: Fragment[];
|
||||
/** The segment durations of all edit list entries leading up to the main one (from which the offset is taken.) */
|
||||
editListPreviousSegmentDurations: number;
|
||||
/** The media time offset of the main edit list entry (with media time !== -1) */
|
||||
@@ -167,6 +169,7 @@ type FragmentTrackState = {
|
||||
type FragmentTrackData = {
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
firstKeyFrameTimestamp: number | null;
|
||||
samples: FragmentTrackSample[];
|
||||
presentationTimestamps: {
|
||||
presentationTimestamp: number;
|
||||
@@ -607,6 +610,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
fragmentLookupTable: null,
|
||||
currentFragmentState: null,
|
||||
fragments: [],
|
||||
fragmentsWithKeyFrame: [],
|
||||
editListPreviousSegmentDurations: 0,
|
||||
editListOffset: 0,
|
||||
} satisfies InternalTrack as InternalTrack;
|
||||
@@ -701,7 +705,10 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
if (relevantEntryFound) {
|
||||
throw new Error('Unsupported edit list: multiple edits are not supported.');
|
||||
console.warn(
|
||||
'Unsupported edit list: multiple edits are not currently supported. Only using first edit.',
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (mediaTime === -1) {
|
||||
@@ -710,7 +717,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
if (mediaRate !== 1) {
|
||||
throw new Error('Unsupported edit list: media rate must be 1.');
|
||||
console.warn('Unsupported edit list entry: media rate must be 1.');
|
||||
break;
|
||||
}
|
||||
|
||||
track.editListPreviousSegmentDurations = previousSegmentDurations;
|
||||
@@ -942,7 +950,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (sampleSize === 16) {
|
||||
track.info.codec = 'pcm-s16be';
|
||||
} else {
|
||||
throw new Error(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
||||
console.warn(`Unsupported sample size ${sampleSize} for codec 'twos'.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else if (lowercaseBoxName === 'sowt') {
|
||||
if (sampleSize === 8) {
|
||||
@@ -950,7 +959,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (sampleSize === 16) {
|
||||
track.info.codec = 'pcm-s16';
|
||||
} else {
|
||||
throw new Error(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
||||
console.warn(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else if (lowercaseBoxName === 'raw ') {
|
||||
track.info.codec = 'pcm-u8';
|
||||
@@ -1193,7 +1203,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 32) {
|
||||
track.info.codec = 'pcm-s32';
|
||||
} else {
|
||||
throw new Error(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else {
|
||||
if (pcmSampleSize === 16) {
|
||||
@@ -1203,7 +1214,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 32) {
|
||||
track.info.codec = 'pcm-s32be';
|
||||
} else {
|
||||
throw new Error(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
console.warn(`Invalid ipcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
}
|
||||
} else if (track.info.codec === 'pcm-f32be') {
|
||||
@@ -1215,7 +1227,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 64) {
|
||||
track.info.codec = 'pcm-f64';
|
||||
} else {
|
||||
throw new Error(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
} else {
|
||||
if (pcmSampleSize === 32) {
|
||||
@@ -1223,7 +1236,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
} else if (pcmSampleSize === 64) {
|
||||
track.info.codec = 'pcm-f64be';
|
||||
} else {
|
||||
throw new Error(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
console.warn(`Invalid fpcm sample size ${pcmSampleSize}.`);
|
||||
track.info.codec = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1405,8 +1419,27 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
}; break;
|
||||
|
||||
case 'stz2': {
|
||||
throw new Error('Unsupported.');
|
||||
};
|
||||
const track = this.currentTrack;
|
||||
assert(track);
|
||||
|
||||
if (!track.sampleTable) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.metadataReader.pos += 4; // Version + flags
|
||||
this.metadataReader.pos += 3; // Reserved
|
||||
|
||||
const fieldSize = this.metadataReader.readU8(); // in bits
|
||||
const sampleCount = this.metadataReader.readU32();
|
||||
|
||||
const bytes = this.metadataReader.readBytes(Math.ceil(sampleCount * fieldSize / 8));
|
||||
const bitstream = new Bitstream(bytes);
|
||||
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
const sampleSize = bitstream.readBits(fieldSize);
|
||||
track.sampleTable.sampleSizes.push(sampleSize);
|
||||
}
|
||||
}; break;
|
||||
|
||||
case 'stss': {
|
||||
const track = this.currentTrack;
|
||||
@@ -1635,6 +1668,16 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
);
|
||||
this.currentTrack.fragments.splice(insertionIndex + 1, 0, this.currentFragment);
|
||||
|
||||
const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
|
||||
if (hasKeyFrame) {
|
||||
const insertionIndex = binarySearchLessOrEqual(
|
||||
this.currentTrack.fragmentsWithKeyFrame,
|
||||
this.currentFragment.moofOffset,
|
||||
x => x.moofOffset,
|
||||
);
|
||||
this.currentTrack.fragmentsWithKeyFrame.splice(insertionIndex + 1, 0, this.currentFragment);
|
||||
}
|
||||
|
||||
const { currentFragmentState } = this.currentTrack;
|
||||
assert(currentFragmentState);
|
||||
|
||||
@@ -1733,7 +1776,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
assert(track.currentFragmentState);
|
||||
|
||||
if (this.currentFragment.trackData.has(track.id)) {
|
||||
throw new Error('Can\'t have two trun boxes for the same track in one fragment.');
|
||||
console.warn('Can\'t have two trun boxes for the same track in one fragment. Ignoring...');
|
||||
break;
|
||||
}
|
||||
|
||||
const version = this.metadataReader.readU8();
|
||||
@@ -1770,6 +1814,7 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
const trackData: FragmentTrackData = {
|
||||
startTimestamp: 0,
|
||||
endTimestamp: 0,
|
||||
firstKeyFrameTimestamp: null,
|
||||
samples: [],
|
||||
presentationTimestamps: [],
|
||||
startTimestampIsFinal: false,
|
||||
@@ -1831,13 +1876,19 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
|
||||
.sort((a, b) => a.presentationTimestamp - b.presentationTimestamp);
|
||||
|
||||
// Update sample durations based on presentation order
|
||||
for (let i = 0; i < trackData.presentationTimestamps.length - 1; i++) {
|
||||
const current = trackData.presentationTimestamps[i]!;
|
||||
const next = trackData.presentationTimestamps[i + 1]!;
|
||||
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
||||
const currentEntry = trackData.presentationTimestamps[i]!;
|
||||
const currentSample = trackData.samples[currentEntry.sampleIndex]!;
|
||||
|
||||
const duration = next.presentationTimestamp - current.presentationTimestamp;
|
||||
trackData.samples[current.sampleIndex]!.duration = duration;
|
||||
if (trackData.firstKeyFrameTimestamp === null && currentSample.isKeyFrame) {
|
||||
trackData.firstKeyFrameTimestamp = currentSample.presentationTimestamp;
|
||||
}
|
||||
|
||||
if (i < trackData.presentationTimestamps.length - 1) {
|
||||
// Update sample durations based on presentation order
|
||||
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
||||
currentSample.duration = nextEntry.presentationTimestamp - currentEntry.presentationTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
const firstSample = trackData.samples[trackData.presentationTimestamps[0]!.sampleIndex]!;
|
||||
@@ -2074,13 +2125,13 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
correctSampleFound: true,
|
||||
};
|
||||
} else {
|
||||
// Walk the list of fragments until we find the next fragment for this track
|
||||
// Walk the list of fragments until we find the next fragment for this track with a key frame
|
||||
let currentFragment = locationInFragment.fragment;
|
||||
while (currentFragment.nextFragment) {
|
||||
currentFragment = currentFragment.nextFragment;
|
||||
|
||||
const trackData = currentFragment.trackData.get(this.internalTrack.id);
|
||||
if (trackData) {
|
||||
if (trackData && trackData.firstKeyFrameTimestamp !== null) {
|
||||
const fragmentIndex = binarySearchExact(
|
||||
this.internalTrack.fragments,
|
||||
currentFragment.moofOffset,
|
||||
@@ -2089,9 +2140,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
assert(fragmentIndex !== -1);
|
||||
|
||||
const keyFrameIndex = trackData.samples.findIndex(x => x.isKeyFrame);
|
||||
if (keyFrameIndex === -1) {
|
||||
throw new Error('Not supported: Fragment does not contain key sample.');
|
||||
}
|
||||
assert(keyFrameIndex !== -1); // There must be one
|
||||
|
||||
return {
|
||||
fragmentIndex,
|
||||
@@ -2231,26 +2280,34 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
}
|
||||
|
||||
private findKeySampleInFragmentsForTimestamp(timestampInTimescale: number) {
|
||||
const fragmentIndex = binarySearchLessOrEqual(
|
||||
const indexInKeyFrameFragments = binarySearchLessOrEqual(
|
||||
// This array is technically not sorted by start timestamp, but for any reasonable file, it basically is.
|
||||
this.internalTrack.fragments,
|
||||
this.internalTrack.fragmentsWithKeyFrame,
|
||||
timestampInTimescale,
|
||||
x => x.trackData.get(this.internalTrack.id)!.startTimestamp,
|
||||
);
|
||||
|
||||
let fragmentIndex = -1;
|
||||
let sampleIndex = -1;
|
||||
let correctSampleFound = false;
|
||||
|
||||
if (fragmentIndex !== -1) {
|
||||
const fragment = this.internalTrack.fragments[fragmentIndex]!;
|
||||
if (indexInKeyFrameFragments !== -1) {
|
||||
const fragment = this.internalTrack.fragmentsWithKeyFrame[indexInKeyFrameFragments]!;
|
||||
|
||||
// Now, let's find the actual index of the fragment in the list of ALL fragments, not just key frame ones
|
||||
fragmentIndex = binarySearchExact(
|
||||
this.internalTrack.fragments,
|
||||
fragment.moofOffset,
|
||||
x => x.moofOffset,
|
||||
);
|
||||
assert(fragmentIndex !== -1);
|
||||
|
||||
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
||||
const index = findLastIndex(trackData.presentationTimestamps, (x) => {
|
||||
const sample = trackData.samples[x.sampleIndex]!;
|
||||
return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('Not supported: Fragment does not begin with a key sample.');
|
||||
}
|
||||
assert(index !== -1); // It's a key frame fragment, so there must be a key frame
|
||||
|
||||
const entry = trackData.presentationTimestamps[index]!;
|
||||
sampleIndex = entry.sampleIndex;
|
||||
|
||||
@@ -484,24 +484,18 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
.map((block, i) => ({ timestamp: block.timestamp, blockIndex: i }))
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
let hasKeyFrame = false;
|
||||
for (let i = 0; i < trackData.presentationTimestamps.length; i++) {
|
||||
const entry = trackData.presentationTimestamps[i]!;
|
||||
const block = trackData.blocks[entry.blockIndex]!;
|
||||
const currentEntry = trackData.presentationTimestamps[i]!;
|
||||
const currentBlock = trackData.blocks[currentEntry.blockIndex]!;
|
||||
|
||||
if (block.isKeyFrame) {
|
||||
hasKeyFrame = true;
|
||||
|
||||
if (trackData.firstKeyFrameTimestamp === null && block.isKeyFrame) {
|
||||
trackData.firstKeyFrameTimestamp = block.timestamp;
|
||||
}
|
||||
if (trackData.firstKeyFrameTimestamp === null && currentBlock.isKeyFrame) {
|
||||
trackData.firstKeyFrameTimestamp = currentBlock.timestamp;
|
||||
}
|
||||
|
||||
if (i < trackData.presentationTimestamps.length - 1) {
|
||||
// Update block durations based on presentation order
|
||||
const nextEntry = trackData.presentationTimestamps[i + 1]!;
|
||||
const nextBlock = trackData.blocks[nextEntry.blockIndex]!;
|
||||
block.duration = nextBlock.timestamp - block.timestamp;
|
||||
currentBlock.duration = nextEntry.timestamp - currentBlock.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,6 +514,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
);
|
||||
track.clusters.splice(insertionIndex + 1, 0, cluster);
|
||||
|
||||
const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null;
|
||||
if (hasKeyFrame) {
|
||||
const insertionIndex = binarySearchLessOrEqual(
|
||||
track.clustersWithKeyFrame,
|
||||
@@ -1210,7 +1205,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
correctBlockFound: true,
|
||||
};
|
||||
} else {
|
||||
// Walk the list of clusters until we find the next cluster for this track
|
||||
// Walk the list of clusters until we find the next cluster for this track with a key frame
|
||||
let currentCluster = locationInCluster.cluster;
|
||||
while (currentCluster.nextCluster) {
|
||||
currentCluster = currentCluster.nextCluster;
|
||||
|
||||
+6
-4
@@ -416,8 +416,9 @@ class VideoEncoderWrapper {
|
||||
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
||||
if (!support.supported) {
|
||||
throw new Error(
|
||||
'This specific encoder configuration is not supported by this browser. Consider using another'
|
||||
+ ' codec or changing your video parameters.',
|
||||
`This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,`
|
||||
+ ` ${encoderConfig.width}x${encoderConfig.height}) is not supported by this browser. Consider`
|
||||
+ ` using another codec or changing your video parameters.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -988,8 +989,9 @@ class AudioEncoderWrapper {
|
||||
const support = await AudioEncoder.isConfigSupported(encoderConfig);
|
||||
if (!support.supported) {
|
||||
throw new Error(
|
||||
'This specific encoder configuration not supported by this browser. Consider using another'
|
||||
+ ' codec or changing your audio parameters.',
|
||||
`This specific encoder configuration (${encoderConfig.codec}, ${encoderConfig.bitrate} bps,`
|
||||
+ ` ${encoderConfig.numberOfChannels} channels, ${encoderConfig.sampleRate} Hz) is not`
|
||||
+ ` supported by this browser. Consider using another codec or changing your audio parameters.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -134,7 +134,9 @@ export const writeBits = (bytes: Uint8Array, start: number, end: number, value:
|
||||
};
|
||||
|
||||
export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => {
|
||||
if (source instanceof ArrayBuffer) {
|
||||
if (source instanceof Uint8Array) {
|
||||
return source;
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
return new Uint8Array(source);
|
||||
} else {
|
||||
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
||||
@@ -142,7 +144,9 @@ export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => {
|
||||
};
|
||||
|
||||
export const toDataView = (source: AllowSharedBufferSource) => {
|
||||
if (source instanceof ArrayBuffer) {
|
||||
if (source instanceof DataView) {
|
||||
return source;
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
return new DataView(source);
|
||||
} else {
|
||||
return new DataView(source.buffer, source.byteOffset, source.byteLength);
|
||||
@@ -202,11 +206,10 @@ export const colorSpaceIsComplete = (
|
||||
};
|
||||
|
||||
export const isAllowSharedBufferSource = (x: unknown) => {
|
||||
// Quite a mouthful:
|
||||
return (
|
||||
x instanceof ArrayBuffer
|
||||
|| (typeof SharedArrayBuffer !== 'undefined' && x instanceof SharedArrayBuffer)
|
||||
|| (ArrayBuffer.isView(x) && !(x instanceof DataView))
|
||||
|| ArrayBuffer.isView(x)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: n
|
||||
|
||||
reader.pos += 1;
|
||||
|
||||
if (firstByte !== 0xff) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((secondByte & 0xe0) !== 0xe0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Mp3Reader {
|
||||
assert(this.fileSize);
|
||||
until ??= this.fileSize;
|
||||
|
||||
while (this.pos < until - FRAME_HEADER_SIZE) {
|
||||
while (this.pos <= until - FRAME_HEADER_SIZE) {
|
||||
const word = this.readU32();
|
||||
this.pos -= 4;
|
||||
|
||||
|
||||
+1
-5
@@ -890,7 +890,6 @@ export class AudioSample {
|
||||
}
|
||||
|
||||
return new AudioData({
|
||||
|
||||
format: this.format,
|
||||
sampleRate: this.sampleRate,
|
||||
numberOfFrames: this.numberOfFrames,
|
||||
@@ -900,11 +899,9 @@ export class AudioSample {
|
||||
});
|
||||
} else {
|
||||
const data = new ArrayBuffer(this.allocationSize({ planeIndex: 0, format: this.format }));
|
||||
|
||||
this.copyTo(new DataView(data), { planeIndex: 0, format: this.format });
|
||||
this.copyTo(data, { planeIndex: 0, format: this.format });
|
||||
|
||||
return new AudioData({
|
||||
|
||||
format: this.format,
|
||||
sampleRate: this.sampleRate,
|
||||
numberOfFrames: this.numberOfFrames,
|
||||
@@ -916,7 +913,6 @@ export class AudioSample {
|
||||
}
|
||||
} else {
|
||||
return new AudioData({
|
||||
|
||||
format: this.format,
|
||||
sampleRate: this.sampleRate,
|
||||
numberOfFrames: this.numberOfFrames,
|
||||
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../build",
|
||||
"outDir": "../dist/modules",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"declarationMap": true,
|
||||
"stripInternal": true,
|
||||
"noEmit": false
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ const rollupInput = Object.fromEntries(
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
mediabunny: path.resolve(__dirname, './dist/mediabunny.mjs'),
|
||||
mediabunny: path.resolve(__dirname, './dist/bundles/mediabunny.mjs'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
||||
Reference in New Issue
Block a user