mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 21:33:52 +02:00
Merge main into release for tag v1.15.2
This commit is contained in:
@@ -50,7 +50,7 @@ Core features include:
|
|||||||
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS
|
- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS
|
||||||
- **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API
|
- **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API
|
||||||
- **High precision**: Fine-grained, microsecond-accurate reading and writing operations
|
- **High precision**: Fine-grained, microsecond-accurate reading and writing operations
|
||||||
- **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, resampling, trimming, and more
|
- **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more
|
||||||
- **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming
|
- **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming
|
||||||
- **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped)
|
- **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped)
|
||||||
- **Zero dependencies**: Implemented in highly performant TypeScript
|
- **Zero dependencies**: Implemented in highly performant TypeScript
|
||||||
|
|||||||
+27
-27
@@ -47,23 +47,6 @@
|
|||||||
format: outputFormat,
|
format: outputFormat,
|
||||||
target
|
target
|
||||||
});
|
});
|
||||||
output.setMetadataTags({
|
|
||||||
title: 'Bigggy',
|
|
||||||
artist: 'Buck Bunny',
|
|
||||||
images: [{
|
|
||||||
data: blobData,
|
|
||||||
kind: 'coverFront',
|
|
||||||
mimeType: 'image/jpeg'
|
|
||||||
}],
|
|
||||||
trackNumber: 4,
|
|
||||||
tracksTotal: 10,
|
|
||||||
discNumber: 5,
|
|
||||||
discNumberMax: 8,
|
|
||||||
lyrics: "There's no way\nThat it's not going there",
|
|
||||||
raw: {
|
|
||||||
'ENCODER': 'Mediabunny epic own encoder'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const conversion = await Mediabunny.Conversion.init({
|
const conversion = await Mediabunny.Conversion.init({
|
||||||
input: new Mediabunny.Input({
|
input: new Mediabunny.Input({
|
||||||
@@ -109,16 +92,16 @@
|
|||||||
*/
|
*/
|
||||||
video: () => ({
|
video: () => ({
|
||||||
//discard: true,
|
//discard: true,
|
||||||
crop: {
|
//crop: {
|
||||||
left: 0,
|
// left: 0,
|
||||||
top: 0,
|
// top: 0,
|
||||||
width: 500,
|
// width: 500,
|
||||||
height: 500,
|
// height: 500,
|
||||||
},
|
//},
|
||||||
rotate: 90,
|
//rotate: 90,
|
||||||
width: 200,
|
//width: 200,
|
||||||
height: 500,
|
//height: 500,
|
||||||
fit: 'contain',
|
//fit: 'contain',
|
||||||
//forceTranscode: true,
|
//forceTranscode: true,
|
||||||
//codec: 'avc',
|
//codec: 'avc',
|
||||||
//fit: 'contain',
|
//fit: 'contain',
|
||||||
@@ -141,6 +124,23 @@
|
|||||||
//width: 200,
|
//width: 200,
|
||||||
//height: 100,
|
//height: 100,
|
||||||
}),
|
}),
|
||||||
|
tags: {
|
||||||
|
title: 'Bigggy',
|
||||||
|
artist: 'Buck Bunny',
|
||||||
|
images: [{
|
||||||
|
data: blobData,
|
||||||
|
kind: 'coverFront',
|
||||||
|
mimeType: 'image/jpeg'
|
||||||
|
}],
|
||||||
|
trackNumber: 4,
|
||||||
|
tracksTotal: 10,
|
||||||
|
discNumber: 5,
|
||||||
|
discsTotal: 8,
|
||||||
|
lyrics: "There's no way\nThat it's not going there",
|
||||||
|
raw: {
|
||||||
|
'AIGC': 'Mediabunny epic own encoder'
|
||||||
|
}
|
||||||
|
},
|
||||||
trim: {
|
trim: {
|
||||||
start: 10,
|
start: 10,
|
||||||
end: 20
|
end: 20
|
||||||
|
|||||||
+1
-6
@@ -14,12 +14,7 @@
|
|||||||
source: new Mediabunny.BlobSource(file),
|
source: new Mediabunny.BlobSource(file),
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioTrack = await input.getPrimaryAudioTrack();
|
console.log(await input.getMetadataTags())
|
||||||
const sink = new Mediabunny.EncodedPacketSink(audioTrack);
|
|
||||||
|
|
||||||
for await (const packet of sink.packets()) {
|
|
||||||
console.log(packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||||
|
|||||||
+4
-3
@@ -116,9 +116,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
let videoSource = new Mediabunny.CanvasSource(canvas, {
|
let videoSource = new Mediabunny.CanvasSource(canvas, {
|
||||||
codec: 'avc',
|
codec: 'vp9',
|
||||||
//fullCodecString: 'avc1.42001f',
|
//fullCodecString: 'avc1.42001f',
|
||||||
bitrate: 1e6
|
bitrate: 1e6,
|
||||||
|
onEncoderConfig: console.log,
|
||||||
});
|
});
|
||||||
let audioSource = new Mediabunny.AudioBufferSource({
|
let audioSource = new Mediabunny.AudioBufferSource({
|
||||||
codec: 'opus',
|
codec: 'opus',
|
||||||
@@ -207,5 +208,5 @@ Testing... <00:17.350>One... <00:18.125>Two...
|
|||||||
await output.finalize();
|
await output.finalize();
|
||||||
|
|
||||||
console.log(target);
|
console.log(target);
|
||||||
download(new Blob([target.buffer]), 'test' + format.fileExtension);
|
//download(new Blob([target.buffer]), 'test' + format.fileExtension);
|
||||||
</script>
|
</script>
|
||||||
@@ -11,6 +11,7 @@ It has the following features:
|
|||||||
- Trimming
|
- Trimming
|
||||||
- Video resizing & fitting
|
- Video resizing & fitting
|
||||||
- Video rotation
|
- Video rotation
|
||||||
|
- Video cropping
|
||||||
- Video frame rate adjustment
|
- Video frame rate adjustment
|
||||||
- Audio resampling
|
- Audio resampling
|
||||||
- Audio up/downmixing
|
- Audio up/downmixing
|
||||||
@@ -48,7 +49,7 @@ await conversion.execute();
|
|||||||
That's it! A `Conversion` simply takes an instance of `Input` and `Output`, then reads the data from the input and writes it to the output. If you're unfamiliar with [`Input`](./reading-media-files) and [`Output`](./writing-media-files), check out their respective guides.
|
That's it! A `Conversion` simply takes an instance of `Input` and `Output`, then reads the data from the input and writes it to the output. If you're unfamiliar with [`Input`](./reading-media-files) and [`Output`](./writing-media-files), check out their respective guides.
|
||||||
|
|
||||||
::: info
|
::: info
|
||||||
The `Output` passed to the `Conversion` must be *fresh*; that is, it must have no added tracks and be in the `'pending'` state (not started yet).
|
The `Output` passed to the `Conversion` must be *fresh*; that is, it must have no added tracks or metadata tags and be in the `'pending'` state (not started yet).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Unconfigured, the conversion process handles all the details automatically, such as:
|
Unconfigured, the conversion process handles all the details automatically, such as:
|
||||||
@@ -298,17 +299,17 @@ By default, any [descriptive metadata tags](../api/MetadataTags.md) of the input
|
|||||||
// Set your own metadata:
|
// Set your own metadata:
|
||||||
const conversion = await Conversion.init({
|
const conversion = await Conversion.init({
|
||||||
// ...
|
// ...
|
||||||
tags: () => ({
|
tags: {
|
||||||
title: 're:Turning',
|
title: 're:Turning',
|
||||||
artist: 'Alexander Panos',
|
artist: 'Alexander Panos',
|
||||||
}),
|
},
|
||||||
// ...
|
// ...
|
||||||
});
|
});
|
||||||
|
|
||||||
// Or, augment the input's metadata:
|
// Or, augment the input's metadata:
|
||||||
const conversion = await Conversion.init({
|
const conversion = await Conversion.init({
|
||||||
// ...
|
// ...
|
||||||
tags: inputTags => ({
|
tags: (inputTags) => ({
|
||||||
...inputTags, // Keep the existing metadata
|
...inputTags, // Keep the existing metadata
|
||||||
images: [{ // And add cover art
|
images: [{ // And add cover art
|
||||||
data: new Uint8Array(...),
|
data: new Uint8Array(...),
|
||||||
@@ -323,7 +324,7 @@ const conversion = await Conversion.init({
|
|||||||
// Or, remove all metadata
|
// Or, remove all metadata
|
||||||
const conversion = await Conversion.init({
|
const conversion = await Conversion.init({
|
||||||
// ...
|
// ...
|
||||||
tags: () => ({}),
|
tags: {},
|
||||||
// ...
|
// ...
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Here's a long list of stuff this library does:
|
|||||||
- Lazy, optimized, on-demand file reading
|
- Lazy, optimized, on-demand file reading
|
||||||
- Input and output streaming, arbitrary file size support
|
- Input and output streaming, arbitrary file size support
|
||||||
- File location independence (memory, disk, network, ...)
|
- File location independence (memory, disk, network, ...)
|
||||||
- Utilities for compression, resizing, rotation, resampling, trimming
|
- Utilities for compression, resizing, rotation, cropping, resampling, trimming
|
||||||
- Transmuxing and transcoding
|
- Transmuxing and transcoding
|
||||||
- Microsecond-accurate reading and writing precision
|
- Microsecond-accurate reading and writing precision
|
||||||
- Efficient seeking through time
|
- Efficient seeking through time
|
||||||
|
|||||||
Generated
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.15.1",
|
"version": "1.15.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.15.1",
|
"version": "1.15.2",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -7749,9 +7749,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mediabunny": {
|
"node_modules/mediabunny": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.15.1.tgz",
|
||||||
"integrity": "sha512-yeqr+4sRNzl7QqeHckr+SgPKfaQwSSmhTdVSgKgbenneC+TFlBrk+zCRhF9b7kwLtrm6qziJc/flqYQccX6W/Q==",
|
"integrity": "sha512-+eRTVzd3E4LuGYZzPSQcPzuGdAIljohSlzYTX358XsfLM2qH1lQIBYa+erx7wzVcGQLRNjdV7x7ZS0EpK04DfA==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
@@ -12242,7 +12242,7 @@
|
|||||||
},
|
},
|
||||||
"packages/mp3-encoder": {
|
"packages/mp3-encoder": {
|
||||||
"name": "@mediabunny/mp3-encoder",
|
"name": "@mediabunny/mp3-encoder",
|
||||||
"version": "1.15.1",
|
"version": "1.15.2",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.40.1"
|
"@types/emscripten": "^1.40.1"
|
||||||
|
|||||||
+9
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.15.1",
|
"version": "1.15.2",
|
||||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
|
"browser": {
|
||||||
|
"./dist/modules/src/node.js": false,
|
||||||
|
"./src/node.ts": false
|
||||||
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "./build.sh",
|
"build": "./build.sh",
|
||||||
@@ -102,11 +106,15 @@
|
|||||||
"decoder",
|
"decoder",
|
||||||
"convert",
|
"convert",
|
||||||
"mp4",
|
"mp4",
|
||||||
|
"mov",
|
||||||
"fmp4",
|
"fmp4",
|
||||||
"webm",
|
"webm",
|
||||||
|
"mkv",
|
||||||
"wav",
|
"wav",
|
||||||
"mp3",
|
"mp3",
|
||||||
"ogg",
|
"ogg",
|
||||||
|
"adts",
|
||||||
|
"flac",
|
||||||
"video",
|
"video",
|
||||||
"audio",
|
"audio",
|
||||||
"subtitles",
|
"subtitles",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/mp3-encoder",
|
"name": "@mediabunny/mp3-encoder",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.15.1",
|
"version": "1.15.2",
|
||||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const walkDir = (dir: string) => {
|
|||||||
const fixFile = (filePath: string) => {
|
const fixFile = (filePath: string) => {
|
||||||
const content = fs.readFileSync(filePath, 'utf8');
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
const fixed = content.replace(
|
const fixed = content.replace(
|
||||||
/(\s+from\s+['"])([^'"]*)(['"])/g,
|
/(\s+from\s+['"])(\.[^'"]*)(['"])/g, // This only matches relative imports
|
||||||
'$1$2.js$3',
|
'$1$2.js$3',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const createVariants = async (
|
|||||||
entryPoints: [entryPoint],
|
entryPoints: [entryPoint],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
|
logOverride: {
|
||||||
|
'import-is-undefined': 'silent', // Warning caused by the disabled "node.ts" import
|
||||||
|
},
|
||||||
banner: {
|
banner: {
|
||||||
js: `/*!
|
js: `/*!
|
||||||
* Copyright (c) 2025-present, Vanilagy and contributors
|
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||||
|
|||||||
@@ -1622,16 +1622,16 @@ const generateDocs = (entryFiles: string[], apiConfigFile: string, dry = false)
|
|||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
// Check for --dry flag
|
// Check for --dry flag
|
||||||
const dryIndex = args.indexOf('--dry');
|
const dryIndex = args.indexOf('--dry');
|
||||||
const dry = dryIndex !== -1;
|
const dry = dryIndex !== -1;
|
||||||
|
|
||||||
// Remove --dry flag from args
|
// Remove --dry flag from args
|
||||||
if (dry) {
|
if (dry) {
|
||||||
args.splice(dryIndex, 1);
|
args.splice(dryIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.length < 2) {
|
if (args.length < 2) {
|
||||||
console.error('Usage: npm run generate-docs [--dry] <entry-file1> [entry-file2 ...] <api-config-file>');
|
console.error('Usage: npm run generate-docs [--dry] <entry-file1> [entry-file2 ...] <api-config-file>');
|
||||||
console.error(' --dry: Check if docs are generatable without writing files');
|
console.error(' --dry: Check if docs are generatable without writing files');
|
||||||
|
|||||||
+22
-9
@@ -89,13 +89,13 @@ export type ConversionOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback that returns or resolves to the descriptive metadata tags that should be written to the output file.
|
* An object or a callback that returns or resolves to an object containing the descriptive metadata tags that
|
||||||
* As input, this function will be passed the tags of the input file, allowing you to modify, augment or extend
|
* should be written to the output file. If a function is passed, it will be passed the tags of the input file as
|
||||||
* them.
|
* its first argument, allowing you to modify, augment or extend them.
|
||||||
*
|
*
|
||||||
* If no function is set, the input's metadata tags will be copied to the output.
|
* If no function is set, the input's metadata tags will be copied to the output.
|
||||||
*/
|
*/
|
||||||
tags?: (inputTags: MetadataTags) => MaybePromise<MetadataTags>;
|
tags?: MetadataTags | ((inputTags: MetadataTags) => MaybePromise<MetadataTags>);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -395,8 +395,12 @@ export class Conversion {
|
|||||||
if (!(options.output instanceof Output)) {
|
if (!(options.output instanceof Output)) {
|
||||||
throw new TypeError('options.output must be an Output.');
|
throw new TypeError('options.output must be an Output.');
|
||||||
}
|
}
|
||||||
if (options.output._tracks.length > 0 || options.output.state !== 'pending') {
|
if (
|
||||||
throw new TypeError('options.output must be fresh: no tracks added and not started.');
|
options.output._tracks.length > 0
|
||||||
|
|| Object.keys(options.output._metadataTags).length > 0
|
||||||
|
|| options.output.state !== 'pending'
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.output must be fresh: no tracks or metadata tags added and not started.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options.video !== 'function') {
|
if (typeof options.video !== 'function') {
|
||||||
@@ -426,8 +430,15 @@ export class Conversion {
|
|||||||
&& options.trim.start >= options.trim.end) {
|
&& options.trim.start >= options.trim.end) {
|
||||||
throw new TypeError('options.trim.start must be less than options.trim.end.');
|
throw new TypeError('options.trim.start must be less than options.trim.end.');
|
||||||
}
|
}
|
||||||
if (options.tags !== undefined && typeof options.tags !== 'function') {
|
if (
|
||||||
throw new TypeError('options.tags, when provided, must be a function.');
|
options.tags !== undefined
|
||||||
|
&& (typeof options.tags !== 'object' || !options.tags)
|
||||||
|
&& typeof options.tags !== 'function'
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.tags, when provided, must be an object or a function.');
|
||||||
|
}
|
||||||
|
if (typeof options.tags === 'object') {
|
||||||
|
validateMetadataTags(options.tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._options = options;
|
this._options = options;
|
||||||
@@ -519,7 +530,9 @@ export class Conversion {
|
|||||||
let outputTags: MetadataTags;
|
let outputTags: MetadataTags;
|
||||||
|
|
||||||
if (this._options.tags) {
|
if (this._options.tags) {
|
||||||
const result = await this._options.tags(inputTags);
|
const result = typeof this._options.tags === 'function'
|
||||||
|
? await this._options.tags(inputTags)
|
||||||
|
: this._options.tags;
|
||||||
validateMetadataTags(result);
|
validateMetadataTags(result);
|
||||||
|
|
||||||
outputTags = result;
|
outputTags = result;
|
||||||
|
|||||||
@@ -2306,6 +2306,21 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
}; break;
|
}; break;
|
||||||
|
|
||||||
|
case 'track': {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
const parts = data.split('/');
|
||||||
|
const trackNum = Number.parseInt(parts[0]!, 10);
|
||||||
|
const tracksTotal = parts[1] && Number.parseInt(parts[1], 10);
|
||||||
|
|
||||||
|
if (Number.isInteger(trackNum) && trackNum > 0) {
|
||||||
|
this.metadataTags.trackNumber ??= trackNum;
|
||||||
|
}
|
||||||
|
if (tracksTotal && Number.isInteger(tracksTotal) && tracksTotal > 0) {
|
||||||
|
this.metadataTags.tracksTotal ??= tracksTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}; break;
|
||||||
|
|
||||||
case 'trkn': {
|
case 'trkn': {
|
||||||
if (data instanceof Uint8Array) {
|
if (data instanceof Uint8Array) {
|
||||||
const view = toDataView(data);
|
const view = toDataView(data);
|
||||||
|
|||||||
+10
-3
@@ -18,6 +18,7 @@ import {
|
|||||||
getInt24,
|
getInt24,
|
||||||
getUint24,
|
getUint24,
|
||||||
insertSorted,
|
insertSorted,
|
||||||
|
isFirefox,
|
||||||
isSafari,
|
isSafari,
|
||||||
last,
|
last,
|
||||||
mapAsyncGenerator,
|
mapAsyncGenerator,
|
||||||
@@ -1199,14 +1200,20 @@ export class CanvasSink {
|
|||||||
this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length;
|
this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const context
|
const context = canvas.getContext('2d', {
|
||||||
= canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases
|
||||||
|
}) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
||||||
assert(context);
|
assert(context);
|
||||||
|
|
||||||
context.resetTransform();
|
context.resetTransform();
|
||||||
|
|
||||||
if (!canvasIsNew) {
|
if (!canvasIsNew) {
|
||||||
context.clearRect(0, 0, this._width, this._height);
|
if (isFirefox()) {
|
||||||
|
context.fillStyle = 'black';
|
||||||
|
context.fillRect(0, 0, this._width, this._height);
|
||||||
|
} else {
|
||||||
|
context.clearRect(0, 0, this._width, this._height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sample.drawWithFit(context, {
|
sample.drawWithFit(context, {
|
||||||
|
|||||||
+19
-4
@@ -18,7 +18,16 @@ import {
|
|||||||
VideoCodec,
|
VideoCodec,
|
||||||
} from './codec';
|
} from './codec';
|
||||||
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output';
|
import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output';
|
||||||
import { assert, assertNever, CallSerializer, clamp, promiseWithResolvers, setInt24, setUint24 } from './misc';
|
import {
|
||||||
|
assert,
|
||||||
|
assertNever,
|
||||||
|
CallSerializer,
|
||||||
|
clamp,
|
||||||
|
isFirefox,
|
||||||
|
promiseWithResolvers,
|
||||||
|
setInt24,
|
||||||
|
setUint24,
|
||||||
|
} from './misc';
|
||||||
import { Muxer } from './muxer';
|
import { Muxer } from './muxer';
|
||||||
import { SubtitleParser } from './subtitles';
|
import { SubtitleParser } from './subtitles';
|
||||||
import { toAlaw, toUlaw } from './pcm';
|
import { toAlaw, toUlaw } from './pcm';
|
||||||
@@ -248,12 +257,18 @@ class VideoEncoderWrapper {
|
|||||||
canvasIsNew = true;
|
canvasIsNew = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = this.resizeCanvas.getContext('2d', { alpha: false }) as
|
const context = this.resizeCanvas.getContext('2d', {
|
||||||
CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases
|
||||||
|
}) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
||||||
assert(context);
|
assert(context);
|
||||||
|
|
||||||
if (!canvasIsNew) {
|
if (!canvasIsNew) {
|
||||||
context.clearRect(0, 0, this.codedWidth, this.codedHeight);
|
if (isFirefox()) {
|
||||||
|
context.fillStyle = 'black';
|
||||||
|
context.fillRect(0, 0, this.codedWidth, this.codedHeight);
|
||||||
|
} else {
|
||||||
|
context.clearRect(0, 0, this.codedWidth, this.codedHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videoSample.drawWithFit(context, { fit: sizeChangeBehavior });
|
videoSample.drawWithFit(context, { fit: sizeChangeBehavior });
|
||||||
|
|||||||
@@ -642,6 +642,15 @@ export const isSafari = () => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let isFirefoxCache: boolean | null = null;
|
||||||
|
export const isFirefox = () => {
|
||||||
|
if (isFirefoxCache !== null) {
|
||||||
|
return isFirefoxCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFirefoxCache = typeof navigator !== 'undefined' && navigator.userAgent?.includes('Firefox');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T or a promise that resolves to T.
|
* T or a promise that resolves to T.
|
||||||
* @group Miscellaneous
|
* @group Miscellaneous
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright (c) 2025-present, Vanilagy and contributors
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This file contains Node.js-specific code that does not run in a browser.
|
||||||
|
|
||||||
|
export * as fs from 'node:fs/promises';
|
||||||
+10
-5
@@ -15,6 +15,7 @@ import {
|
|||||||
toDataView,
|
toDataView,
|
||||||
toUint8Array,
|
toUint8Array,
|
||||||
SetRequired,
|
SetRequired,
|
||||||
|
isFirefox,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,7 +202,8 @@ export class VideoSample {
|
|||||||
return new VideoSample(
|
return new VideoSample(
|
||||||
new VideoFrame(data, {
|
new VideoFrame(data, {
|
||||||
timestamp: Math.trunc(init.timestamp! * SECOND_TO_MICROSECOND_FACTOR),
|
timestamp: Math.trunc(init.timestamp! * SECOND_TO_MICROSECOND_FACTOR),
|
||||||
duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR),
|
// Drag 0 to undefined
|
||||||
|
duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR) || undefined,
|
||||||
}),
|
}),
|
||||||
init,
|
init,
|
||||||
);
|
);
|
||||||
@@ -227,7 +229,10 @@ export class VideoSample {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canvas = new OffscreenCanvas(width, height);
|
const canvas = new OffscreenCanvas(width, height);
|
||||||
const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true });
|
const context = canvas.getContext('2d', {
|
||||||
|
alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases
|
||||||
|
willReadFrequently: true,
|
||||||
|
});
|
||||||
assert(context);
|
assert(context);
|
||||||
|
|
||||||
// Draw it to a canvas
|
// Draw it to a canvas
|
||||||
@@ -342,7 +347,7 @@ export class VideoSample {
|
|||||||
dest.set(this._data);
|
dest.set(this._data);
|
||||||
} else {
|
} else {
|
||||||
const canvas = this._data;
|
const canvas = this._data;
|
||||||
const context = canvas.getContext('2d', { alpha: false });
|
const context = canvas.getContext('2d');
|
||||||
assert(context);
|
assert(context);
|
||||||
|
|
||||||
const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);
|
const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);
|
||||||
@@ -373,13 +378,13 @@ export class VideoSample {
|
|||||||
codedWidth: this.codedWidth,
|
codedWidth: this.codedWidth,
|
||||||
codedHeight: this.codedHeight,
|
codedHeight: this.codedHeight,
|
||||||
timestamp: this.microsecondTimestamp,
|
timestamp: this.microsecondTimestamp,
|
||||||
duration: this.microsecondDuration,
|
duration: this.microsecondDuration || undefined,
|
||||||
colorSpace: this.colorSpace,
|
colorSpace: this.colorSpace,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new VideoFrame(this._data, {
|
return new VideoFrame(this._data, {
|
||||||
timestamp: this.microsecondTimestamp,
|
timestamp: this.microsecondTimestamp,
|
||||||
duration: this.microsecondDuration,
|
duration: this.microsecondDuration || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -18,6 +18,9 @@ import {
|
|||||||
toDataView,
|
toDataView,
|
||||||
toUint8Array,
|
toUint8Array,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
|
import * as nodeAlias from './node';
|
||||||
|
|
||||||
|
const node = nodeAlias; // Aliasing it prevents some bundler warnings
|
||||||
|
|
||||||
export type ReadResult = {
|
export type ReadResult = {
|
||||||
bytes: Uint8Array;
|
bytes: Uint8Array;
|
||||||
@@ -548,9 +551,7 @@ export class FilePathSource extends Source {
|
|||||||
// Let's back this source with a StreamSource, makes the implementation very simple
|
// Let's back this source with a StreamSource, makes the implementation very simple
|
||||||
this._streamSource = new StreamSource({
|
this._streamSource = new StreamSource({
|
||||||
getSize: async () => {
|
getSize: async () => {
|
||||||
const FS_MODULE_NAME = 'node:fs/promises';
|
fileHandle = await node.fs.open(filePath, 'r');
|
||||||
const fs = await import(/* @vite-ignore */ FS_MODULE_NAME) as typeof import('node:fs/promises');
|
|
||||||
fileHandle = await fs.open(filePath, 'r');
|
|
||||||
|
|
||||||
const stats = await fileHandle.stat();
|
const stats = await fileHandle.stat();
|
||||||
return stats.size;
|
return stats.size;
|
||||||
@@ -558,8 +559,9 @@ export class FilePathSource extends Source {
|
|||||||
read: async (start, end) => {
|
read: async (start, end) => {
|
||||||
assert(fileHandle);
|
assert(fileHandle);
|
||||||
|
|
||||||
const buffer = Buffer.alloc(end - start);
|
const buffer = new Uint8Array(end - start);
|
||||||
await fileHandle.read(buffer, 0, end - start, start);
|
await fileHandle.read(buffer, 0, end - start, start);
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
},
|
},
|
||||||
maxCacheSize: options.maxCacheSize,
|
maxCacheSize: options.maxCacheSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user