mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-04 06:13:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b12468008 | ||
|
|
2277810853 | ||
|
|
19b1c015b6 | ||
|
|
b1b493ab10 | ||
|
|
cf0bbe896a | ||
|
|
c5d299c4ec | ||
|
|
b06f510382 | ||
|
|
14ad80df5d | ||
|
|
514a533445 | ||
|
|
9072a7a9f7 | ||
|
|
fba463f46b | ||
|
|
3621c59034 | ||
|
|
a16911874c | ||
|
|
f59a79dcf7 | ||
|
|
19b7036d82 | ||
|
|
47bae059db | ||
|
|
84c9d37cb7 | ||
|
|
3e96184a6f | ||
|
|
ee59837857 | ||
|
|
08cf34ee0d |
+1
-1
@@ -10,6 +10,6 @@ indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yaml]
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -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,7 +5,7 @@
|
||||
[](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, 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.
|
||||
@@ -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
-4
@@ -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.Mp4OutputFormat();
|
||||
const outputFormat = new Mediabunny.WavOutputFormat({ large: true });
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Cancel';
|
||||
@@ -38,8 +38,8 @@
|
||||
target
|
||||
}),
|
||||
audio: {
|
||||
codec: 'opus',
|
||||
bitrate: 128000,
|
||||
//codec: 'opus',
|
||||
//bitrate: 128000,
|
||||
//numberOfChannels: 1,
|
||||
//sampleRate: 4000
|
||||
//discard: true
|
||||
|
||||
+13
-6
@@ -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');
|
||||
@@ -15,14 +15,21 @@
|
||||
formats: Mediabunny.ALL_FORMATS,
|
||||
source
|
||||
});
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1920;
|
||||
canvas.height = 1080;
|
||||
canvas.style.background = 'ghostwhite';
|
||||
const ctx = canvas.getContext('2d');
|
||||
document.body.append(canvas);
|
||||
|
||||
const videoTrack = await input.getPrimaryVideoTrack();
|
||||
const sink = new Mediabunny.VideoSampleSink(videoTrack);
|
||||
|
||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||
for await (const packet of sink.packets()) {
|
||||
if (packet.timestamp >= 2) break;
|
||||
console.log(packet);
|
||||
}
|
||||
const sample = await sink.getSample(3);
|
||||
console.log(sample);
|
||||
|
||||
sample.draw(ctx, 1500, 500, 50, 50, 0, 0);
|
||||
|
||||
/*
|
||||
let timestamps = [];
|
||||
|
||||
+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"]');
|
||||
|
||||
@@ -4,11 +4,13 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
import { HeadConfig } from 'vitepress';
|
||||
|
||||
const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,'
|
||||
+ ' and faster than anybunny else.';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default withMermaid({
|
||||
title: 'Mediabunny',
|
||||
description: 'A JavaScript library for reading, writing, and converting media files. Directly in the browser, and'
|
||||
+ ' faster than anybunny else.',
|
||||
description: DESCRIPTION,
|
||||
cleanUrls: true,
|
||||
head: [
|
||||
['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }],
|
||||
@@ -16,9 +18,13 @@ export default withMermaid({
|
||||
['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:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
|
||||
['meta', { property: 'og:locale', content: 'en-US' }],
|
||||
['meta', { property: 'og:description', content: DESCRIPTION }],
|
||||
['meta', { name: 'twitter:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:site', content: '@vanilagy' }],
|
||||
['meta', { name: 'twitter:description', content: DESCRIPTION }],
|
||||
],
|
||||
themeConfig: {
|
||||
logo: '/mediabunny-logo.svg',
|
||||
@@ -75,6 +81,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: {
|
||||
@@ -113,6 +120,7 @@ export default withMermaid({
|
||||
|
||||
((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push(
|
||||
['meta', { property: 'og:title', content: title }],
|
||||
['meta', { property: 'twitter:title', content: title }],
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
@@ -227,8 +227,11 @@ const output = new Output({
|
||||
The following options are available:
|
||||
```ts
|
||||
type WavOutputFormatOptions = {
|
||||
large?: boolean;
|
||||
onHeader?: (data: Uint8Array, position: number) => unknown;
|
||||
};
|
||||
```
|
||||
- `large`\
|
||||
When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible for regular WAVE files.
|
||||
- `onHeader`\
|
||||
Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the start of the data chunk (with a placeholder size of 0).
|
||||
@@ -309,17 +309,29 @@ The `VideoFrame` returned by this method **must** be closed separately from the
|
||||
|
||||
---
|
||||
|
||||
It's also common to draw video samples to a `<canvas>` element or an `OffscreenCanvas`. For this, you can use the following method:
|
||||
It's also common to draw video samples to a `<canvas>` element or an `OffscreenCanvas`. For this, you can use the following methods:
|
||||
```ts
|
||||
draw(
|
||||
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dWidth?: number,
|
||||
dHeight?: number,
|
||||
dWidth?: number, // defaults to displayWidth
|
||||
dHeight?: number, // defaults to displayHeight
|
||||
): void;
|
||||
|
||||
draw(
|
||||
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sWidth: number,
|
||||
sHeight: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dWidth?: number, // defaults to sWidth
|
||||
dHeight?: number, // defaults to sHeight
|
||||
): void;
|
||||
```
|
||||
This method is similar to [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) and paints the video frame at the given position with the given dimensions. If the dimensions aren't specified, they will default to the display dimensions. This method will automatically draw the frame with the correct rotation based on its `rotation` property.
|
||||
These methods behave like [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) and paint the video frame at the given position with the given dimensions. This method will automatically draw the frame with the correct rotation based on its `rotation` property.
|
||||
|
||||
If you want to draw the raw underlying image to a canvas directly (without respecting the rotation metadata), then you can use the following method:
|
||||
```ts
|
||||
|
||||
+2
-1
@@ -95,6 +95,7 @@ const sponsors = {
|
||||
{ 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' },
|
||||
{ image: 'https://avatars.githubusercontent.com/u/91711202', name: 'ihasq', url: 'https://github.com/ihasq' },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
@@ -237,7 +238,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">
|
||||
|
||||
@@ -85,7 +85,7 @@ const startRecording = async () => {
|
||||
|
||||
// Add the video track, with the canvas as the source
|
||||
videoSource = new CanvasSource(canvas, {
|
||||
codec: 'vp9',
|
||||
codec: 'avc',
|
||||
bitrate: QUALITY_MEDIUM,
|
||||
keyFrameInterval: 0.5,
|
||||
latencyMode: 'realtime', // Allow the encoder to skip frames to keep up with real-time constraints
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+24
-38
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.0",
|
||||
"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.2",
|
||||
"version": "1.2.0",
|
||||
"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,
|
||||
|
||||
+6
-4
@@ -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;
|
||||
}
|
||||
|
||||
@@ -289,7 +291,7 @@ export class WaveInputFormat extends InputFormat {
|
||||
|
||||
const riffReader = new RiffReader(input._mainReader);
|
||||
const riffType = riffReader.readAscii(4);
|
||||
if (riffType !== 'RIFF' && riffType !== 'RIFX') {
|
||||
if (riffType !== 'RIFF' && riffType !== 'RIFX' && riffType !== 'RF64') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -220,7 +220,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
|
||||
const fileSize = await this.input.source.getSize();
|
||||
|
||||
while (this.metadataReader.pos < fileSize - MIN_HEADER_SIZE) {
|
||||
while (this.metadataReader.pos <= fileSize - MIN_HEADER_SIZE) {
|
||||
await this.metadataReader.reader.loadRange(
|
||||
this.metadataReader.pos,
|
||||
this.metadataReader.pos + MAX_HEADER_SIZE,
|
||||
@@ -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,
|
||||
@@ -562,7 +557,7 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
readContiguousElements(reader: EBMLReader, totalSize: number) {
|
||||
const startIndex = reader.pos;
|
||||
|
||||
while (reader.pos - startIndex < totalSize) {
|
||||
while (reader.pos - startIndex <= totalSize - MIN_HEADER_SIZE) {
|
||||
this.traverseElement(reader);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1473,7 +1468,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
}
|
||||
|
||||
const endPos = dataStartPos + size;
|
||||
if (endPos >= segment.elementEndPos - MIN_HEADER_SIZE) {
|
||||
if (endPos > segment.elementEndPos - MIN_HEADER_SIZE) {
|
||||
// No more elements fit in this segment
|
||||
break;
|
||||
} else {
|
||||
|
||||
@@ -737,6 +737,7 @@ class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||
// Round the timestamps to the time resolution
|
||||
sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution);
|
||||
sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution);
|
||||
sample.setRotation(this.rotation);
|
||||
|
||||
this.onSample(sample);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -546,6 +546,12 @@ export class Mp3OutputFormat extends OutputFormat {
|
||||
* @public
|
||||
*/
|
||||
export type WavOutputFormatOptions = {
|
||||
/**
|
||||
* When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible
|
||||
* for regular WAVE files.
|
||||
*/
|
||||
large?: boolean;
|
||||
|
||||
/**
|
||||
* Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the
|
||||
* start of the data chunk (with a placeholder size of 0).
|
||||
@@ -565,6 +571,9 @@ export class WavOutputFormat extends OutputFormat {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError('options must be an object.');
|
||||
}
|
||||
if (options.large !== undefined && typeof options.large !== 'boolean') {
|
||||
throw new TypeError('options.large, when provided, must be a boolean.');
|
||||
}
|
||||
if (options.onHeader !== undefined && typeof options.onHeader !== 'function') {
|
||||
throw new TypeError('options.onHeader, when provided, must be a function.');
|
||||
}
|
||||
|
||||
+111
-2
@@ -366,9 +366,79 @@ export class VideoSample {
|
||||
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dWidth: number = this.displayWidth,
|
||||
dHeight: number = this.displayHeight,
|
||||
dWidth?: number,
|
||||
dHeight?: number,
|
||||
): void;
|
||||
/**
|
||||
* Draws the video sample to a 2D canvas context. Rotation metadata will be taken into account.
|
||||
*
|
||||
* @param sx - The x-coordinate of the top left corner of the sub-rectangle of the source image to draw into the
|
||||
* destination context.
|
||||
* @param sy - The y-coordinate of the top left corner of the sub-rectangle of the source image to draw into the
|
||||
* destination context.
|
||||
* @param sWidth - The width of the sub-rectangle of the source image to draw into the destination context.
|
||||
* @param sHeight - The height of the sub-rectangle of the source image to draw into the destination context.
|
||||
* @param dx - The x-coordinate in the destination canvas at which to place the top-left corner of the source image.
|
||||
* @param dy - The y-coordinate in the destination canvas at which to place the top-left corner of the source image.
|
||||
* @param dWidth - The width in pixels with which to draw the image in the destination canvas.
|
||||
* @param dHeight - The height in pixels with which to draw the image in the destination canvas.
|
||||
*/
|
||||
draw(
|
||||
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sWidth: number,
|
||||
sHeight: number,
|
||||
dx: number,
|
||||
dy: number,
|
||||
dWidth?: number,
|
||||
dHeight?: number,
|
||||
): void;
|
||||
draw(
|
||||
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||
arg1: number,
|
||||
arg2: number,
|
||||
arg3?: number,
|
||||
arg4?: number,
|
||||
arg5?: number,
|
||||
arg6?: number,
|
||||
arg7?: number,
|
||||
arg8?: number,
|
||||
) {
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
let sWidth = this.displayWidth;
|
||||
let sHeight = this.displayHeight;
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
let dWidth = this.displayWidth;
|
||||
let dHeight = this.displayHeight;
|
||||
|
||||
if (arg5 !== undefined) {
|
||||
sx = arg1!;
|
||||
sy = arg2!;
|
||||
sWidth = arg3!;
|
||||
sHeight = arg4!;
|
||||
dx = arg5;
|
||||
dy = arg6!;
|
||||
|
||||
if (arg7 !== undefined) {
|
||||
dWidth = arg7;
|
||||
dHeight = arg8!;
|
||||
} else {
|
||||
dWidth = sWidth;
|
||||
dHeight = sHeight;
|
||||
}
|
||||
} else {
|
||||
dx = arg1;
|
||||
dy = arg2;
|
||||
|
||||
if (arg3 !== undefined) {
|
||||
dWidth = arg3;
|
||||
dHeight = arg4!;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(
|
||||
(typeof CanvasRenderingContext2D !== 'undefined' && context instanceof CanvasRenderingContext2D)
|
||||
|| (
|
||||
@@ -378,6 +448,18 @@ export class VideoSample {
|
||||
)) {
|
||||
throw new TypeError('context must be a CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D.');
|
||||
}
|
||||
if (!Number.isFinite(sx)) {
|
||||
throw new TypeError('sx must be a number.');
|
||||
}
|
||||
if (!Number.isFinite(sy)) {
|
||||
throw new TypeError('sy must be a number.');
|
||||
}
|
||||
if (!Number.isFinite(sWidth) || sWidth < 0) {
|
||||
throw new TypeError('sWidth must be a non-negative number.');
|
||||
}
|
||||
if (!Number.isFinite(sHeight) || sHeight < 0) {
|
||||
throw new TypeError('sHeight must be a non-negative number.');
|
||||
}
|
||||
if (!Number.isFinite(dx)) {
|
||||
throw new TypeError('dx must be a number.');
|
||||
}
|
||||
@@ -395,6 +477,29 @@ export class VideoSample {
|
||||
throw new Error('VideoSample is closed.');
|
||||
}
|
||||
|
||||
// The provided sx,sy,sWidth,sHeight refer to the final rotated image, but that's not actually how the image is
|
||||
// stored. Therefore, we must map these back onto the original, pre-rotation image.
|
||||
if (this.rotation === 90) {
|
||||
[sx, sy, sWidth, sHeight] = [
|
||||
sy,
|
||||
this.codedHeight - sx - sWidth,
|
||||
sHeight,
|
||||
sWidth,
|
||||
];
|
||||
} else if (this.rotation === 180) {
|
||||
[sx, sy] = [
|
||||
this.codedWidth - sx - sWidth,
|
||||
this.codedHeight - sy - sHeight,
|
||||
];
|
||||
} else if (this.rotation === 270) {
|
||||
[sx, sy, sWidth, sHeight] = [
|
||||
this.codedWidth - sy - sHeight,
|
||||
sx,
|
||||
sHeight,
|
||||
sWidth,
|
||||
];
|
||||
}
|
||||
|
||||
const source = this.toCanvasImageSource();
|
||||
|
||||
context.save();
|
||||
@@ -412,6 +517,10 @@ export class VideoSample {
|
||||
|
||||
context.drawImage(
|
||||
source,
|
||||
sx,
|
||||
sy,
|
||||
sWidth,
|
||||
sHeight,
|
||||
-dWidth / 2,
|
||||
-dHeight / 2,
|
||||
dWidth,
|
||||
|
||||
+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
|
||||
|
||||
@@ -35,6 +35,21 @@ export class RiffReader {
|
||||
return view.getUint32(offset, this.littleEndian);
|
||||
}
|
||||
|
||||
readU64() {
|
||||
let low: number;
|
||||
let high: number;
|
||||
|
||||
if (this.littleEndian) {
|
||||
low = this.readU32();
|
||||
high = this.readU32();
|
||||
} else {
|
||||
high = this.readU32();
|
||||
low = this.readU32();
|
||||
}
|
||||
|
||||
return high * 0x100000000 + low;
|
||||
}
|
||||
|
||||
readAscii(length: number) {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
@@ -14,14 +14,20 @@ export class RiffWriter {
|
||||
|
||||
constructor(private writer: Writer) {}
|
||||
|
||||
writeU16(value: number) {
|
||||
this.helperView.setUint16(0, value, true);
|
||||
this.writer.write(this.helper.subarray(0, 2));
|
||||
}
|
||||
|
||||
writeU32(value: number) {
|
||||
this.helperView.setUint32(0, value, true);
|
||||
this.writer.write(this.helper.subarray(0, 4));
|
||||
}
|
||||
|
||||
writeU16(value: number) {
|
||||
this.helperView.setUint16(0, value, true);
|
||||
this.writer.write(this.helper.subarray(0, 2));
|
||||
writeU64(value: number) {
|
||||
this.helperView.setUint32(0, value, true);
|
||||
this.helperView.setUint32(4, Math.floor(value / 2 ** 32), true);
|
||||
this.writer.write(this.helper);
|
||||
}
|
||||
|
||||
writeAscii(text: string) {
|
||||
|
||||
@@ -53,9 +53,13 @@ export class WaveDemuxer extends Demuxer {
|
||||
const actualFileSize = await this.metadataReader.reader.source.getSize();
|
||||
|
||||
const riffType = this.metadataReader.readAscii(4);
|
||||
this.metadataReader.littleEndian = riffType === 'RIFF';
|
||||
this.metadataReader.littleEndian = riffType !== 'RIFX';
|
||||
|
||||
const totalFileSize = Math.min(this.metadataReader.readU32() + 8, actualFileSize);
|
||||
const isRf64 = riffType === 'RF64';
|
||||
|
||||
const outerChunkSize = this.metadataReader.readU32();
|
||||
|
||||
let totalFileSize = isRf64 ? actualFileSize : Math.min(outerChunkSize + 8, actualFileSize);
|
||||
const format = this.metadataReader.readAscii(4);
|
||||
|
||||
if (format !== 'WAVE') {
|
||||
@@ -63,6 +67,9 @@ export class WaveDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
this.metadataReader.pos = 12;
|
||||
let chunksRead = 0;
|
||||
let dataChunkSize: number | null = null;
|
||||
|
||||
while (this.metadataReader.pos < totalFileSize) {
|
||||
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 8);
|
||||
|
||||
@@ -70,14 +77,28 @@ export class WaveDemuxer extends Demuxer {
|
||||
const chunkSize = this.metadataReader.readU32();
|
||||
const startPos = this.metadataReader.pos;
|
||||
|
||||
if (isRf64 && chunksRead === 0 && chunkId !== 'ds64') {
|
||||
throw new Error('Invalid RF64 file: First chunk must be "ds64".');
|
||||
}
|
||||
|
||||
if (chunkId === 'fmt ') {
|
||||
await this.parseFmtChunk(chunkSize);
|
||||
} else if (chunkId === 'data') {
|
||||
dataChunkSize ??= chunkSize;
|
||||
|
||||
this.dataStart = this.metadataReader.pos;
|
||||
this.dataSize = Math.min(chunkSize, totalFileSize - this.dataStart);
|
||||
this.dataSize = Math.min(dataChunkSize, totalFileSize - this.dataStart);
|
||||
} else if (chunkId === 'ds64') {
|
||||
// File and data chunk sizes are defined in here instead
|
||||
|
||||
const riffChunkSize = this.metadataReader.readU64();
|
||||
dataChunkSize = this.metadataReader.readU64();
|
||||
|
||||
totalFileSize = Math.min(riffChunkSize + 8, actualFileSize);
|
||||
}
|
||||
|
||||
this.metadataReader.pos = startPos + chunkSize + (chunkSize & 1); // Handle padding
|
||||
chunksRead++;
|
||||
}
|
||||
|
||||
if (!this.audioInfo) {
|
||||
|
||||
+57
-9
@@ -18,10 +18,13 @@ import { assert } from '../misc';
|
||||
|
||||
export class WaveMuxer extends Muxer {
|
||||
private format: WavOutputFormat;
|
||||
private isRf64: boolean;
|
||||
private writer: Writer;
|
||||
private riffWriter: RiffWriter;
|
||||
private headerWritten = false;
|
||||
private dataSize = 0;
|
||||
private sampleRate: number | null = null;
|
||||
private sampleCount = 0;
|
||||
|
||||
constructor(output: Output, format: WavOutputFormat) {
|
||||
super(output);
|
||||
@@ -29,6 +32,7 @@ export class WaveMuxer extends Muxer {
|
||||
this.format = format;
|
||||
this.writer = output._writer;
|
||||
this.riffWriter = new RiffWriter(output._writer);
|
||||
this.isRf64 = !!format._options.large;
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -58,13 +62,22 @@ export class WaveMuxer extends Muxer {
|
||||
assert(meta.decoderConfig);
|
||||
|
||||
this.writeHeader(track, meta.decoderConfig);
|
||||
this.sampleRate = meta.decoderConfig.sampleRate;
|
||||
this.headerWritten = true;
|
||||
}
|
||||
|
||||
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||
|
||||
if (!this.isRf64 && this.writer.getPos() + packet.data.byteLength >= 2 ** 32) {
|
||||
throw new Error(
|
||||
'Adding more audio data would exceed the maximum RIFF size of 4 GiB. To write larger files, use'
|
||||
+ ' RF64 by setting `large: true` in the WavOutputFormatOptions.',
|
||||
);
|
||||
}
|
||||
|
||||
this.writer.write(packet.data);
|
||||
this.dataSize += packet.data.byteLength;
|
||||
this.sampleCount += Math.round(packet.duration * this.sampleRate!);
|
||||
|
||||
await this.writer.flush();
|
||||
} finally {
|
||||
@@ -101,10 +114,26 @@ export class WaveMuxer extends Muxer {
|
||||
const blockSize = pcmInfo.sampleSize * channels;
|
||||
|
||||
// RIFF header
|
||||
this.riffWriter.writeAscii('RIFF');
|
||||
this.riffWriter.writeU32(0); // File size placeholder
|
||||
this.riffWriter.writeAscii(this.isRf64 ? 'RF64' : 'RIFF');
|
||||
|
||||
if (this.isRf64) {
|
||||
this.riffWriter.writeU32(0xffffffff); // Not used in RF64
|
||||
} else {
|
||||
this.riffWriter.writeU32(0); // File size placeholder
|
||||
}
|
||||
|
||||
this.riffWriter.writeAscii('WAVE');
|
||||
|
||||
if (this.isRf64) {
|
||||
this.riffWriter.writeAscii('ds64');
|
||||
this.riffWriter.writeU32(28); // Chunk size
|
||||
this.riffWriter.writeU64(0); // RIFF size placeholder
|
||||
this.riffWriter.writeU64(0); // Data size placeholder
|
||||
this.riffWriter.writeU64(0); // Sample count placeholder
|
||||
this.riffWriter.writeU32(0); // Table length
|
||||
// Empty table
|
||||
}
|
||||
|
||||
// fmt chunk
|
||||
this.riffWriter.writeAscii('fmt ');
|
||||
this.riffWriter.writeU32(16); // Chunk size
|
||||
@@ -117,7 +146,12 @@ export class WaveMuxer extends Muxer {
|
||||
|
||||
// data chunk
|
||||
this.riffWriter.writeAscii('data');
|
||||
this.riffWriter.writeU32(0); // Data size placeholder
|
||||
|
||||
if (this.isRf64) {
|
||||
this.riffWriter.writeU32(0xffffffff); // Not used in RF64
|
||||
} else {
|
||||
this.riffWriter.writeU32(0); // Data size placeholder
|
||||
}
|
||||
|
||||
if (this.format._options.onHeader) {
|
||||
const { data, start } = this.writer.stopTrackingWrites();
|
||||
@@ -130,13 +164,27 @@ export class WaveMuxer extends Muxer {
|
||||
|
||||
const endPos = this.writer.getPos();
|
||||
|
||||
// Write file size
|
||||
this.writer.seek(4);
|
||||
this.riffWriter.writeU32(this.dataSize + 36); // File size - 8
|
||||
if (this.isRf64) {
|
||||
// Write riff size
|
||||
this.writer.seek(20);
|
||||
this.riffWriter.writeU64(endPos - 8);
|
||||
|
||||
// Write data chunk size
|
||||
this.writer.seek(40);
|
||||
this.riffWriter.writeU32(this.dataSize);
|
||||
// Write data size
|
||||
this.writer.seek(28);
|
||||
this.riffWriter.writeU64(this.dataSize);
|
||||
|
||||
// Write sample count
|
||||
this.writer.seek(36);
|
||||
this.riffWriter.writeU64(this.sampleCount);
|
||||
} else {
|
||||
// Write file size
|
||||
this.writer.seek(4);
|
||||
this.riffWriter.writeU32(endPos - 8);
|
||||
|
||||
// Write data chunk size
|
||||
this.writer.seek(40);
|
||||
this.riffWriter.writeU32(this.dataSize);
|
||||
}
|
||||
|
||||
this.writer.seek(endPos);
|
||||
|
||||
|
||||
+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