mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-10 01:03:45 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6319bf2eee | ||
|
|
5ffb5a0979 | ||
|
|
cf2edfada2 | ||
|
|
a97e9800d2 | ||
|
|
23f5e96afd | ||
|
|
9e203c0c8a | ||
|
|
dec669163f | ||
|
|
e16b01a4c6 |
@@ -127,6 +127,7 @@ const sponsors = {
|
|||||||
{ image: 'https://avatars.githubusercontent.com/u/2565549', name: 'MatthewNorton', url: 'https://github.com/MatthewNorton' },
|
{ image: 'https://avatars.githubusercontent.com/u/2565549', name: 'MatthewNorton', url: 'https://github.com/MatthewNorton' },
|
||||||
{ image: 'https://avatars.githubusercontent.com/u/272247752', name: 'Motionik', url: 'https://github.com/Motionik' },
|
{ image: 'https://avatars.githubusercontent.com/u/272247752', name: 'Motionik', url: 'https://github.com/Motionik' },
|
||||||
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
{ image: 'https://avatars.githubusercontent.com/u/2698271', name: 'Matthew Gardner', url: 'https://github.com/spheric' },
|
||||||
|
{ image: 'https://avatars.githubusercontent.com/u/41021374', name: 'arth', url: 'https://github.com/arthtyagi' },
|
||||||
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
{ image: 'https://avatars.githubusercontent.com/u/5475819', name: 'AJ Funk', url: 'https://github.com/AJFunk' },
|
||||||
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
{ image: 'https://avatars.githubusercontent.com/u/30229596', name: 'Pablo Bonilla', url: 'https://github.com/devPablo' },
|
||||||
{ image: 'https://avatars.githubusercontent.com/u/536008', name: 'Satish Goda', url: 'https://github.com/satishgoda' },
|
{ image: 'https://avatars.githubusercontent.com/u/536008', name: 'Satish Goda', url: 'https://github.com/satishgoda' },
|
||||||
|
|||||||
Generated
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
".",
|
".",
|
||||||
@@ -12958,7 +12958,7 @@
|
|||||||
},
|
},
|
||||||
"packages/aac-encoder": {
|
"packages/aac-encoder": {
|
||||||
"name": "@mediabunny/aac-encoder",
|
"name": "@mediabunny/aac-encoder",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.40.1"
|
"@types/emscripten": "^1.40.1"
|
||||||
@@ -12973,7 +12973,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ac3": {
|
"packages/ac3": {
|
||||||
"name": "@mediabunny/ac3",
|
"name": "@mediabunny/ac3",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.40.1"
|
"@types/emscripten": "^1.40.1"
|
||||||
@@ -12988,7 +12988,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dts": {
|
"packages/dts": {
|
||||||
"name": "@mediabunny/dts",
|
"name": "@mediabunny/dts",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.40.1"
|
"@types/emscripten": "^1.40.1"
|
||||||
@@ -13003,7 +13003,7 @@
|
|||||||
},
|
},
|
||||||
"packages/flac-encoder": {
|
"packages/flac-encoder": {
|
||||||
"name": "@mediabunny/flac-encoder",
|
"name": "@mediabunny/flac-encoder",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.40.1"
|
"@types/emscripten": "^1.40.1"
|
||||||
@@ -13018,7 +13018,7 @@
|
|||||||
},
|
},
|
||||||
"packages/mp3-encoder": {
|
"packages/mp3-encoder": {
|
||||||
"name": "@mediabunny/mp3-encoder",
|
"name": "@mediabunny/mp3-encoder",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/emscripten": "^1.40.1"
|
"@types/emscripten": "^1.40.1"
|
||||||
@@ -13033,7 +13033,7 @@
|
|||||||
},
|
},
|
||||||
"packages/prores": {
|
"packages/prores": {
|
||||||
"name": "@mediabunny/prores",
|
"name": "@mediabunny/prores",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"turbores": "^1.2.2"
|
"turbores": "^1.2.2"
|
||||||
@@ -13048,10 +13048,10 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@mediabunny/server",
|
"name": "@mediabunny/server",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mediabunny/prores": "^1.55.4",
|
"@mediabunny/prores": "^1.55.6",
|
||||||
"node-av": "^6.0.0"
|
"node-av": "^6.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mediabunny",
|
"name": "mediabunny",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"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": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/aac-encoder",
|
"name": "@mediabunny/aac-encoder",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"description": "AAC encoder extension for Mediabunny, based on libavcodec.",
|
"description": "AAC encoder extension for Mediabunny, based on libavcodec.",
|
||||||
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||||
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/ac3",
|
"name": "@mediabunny/ac3",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on libavcodec.",
|
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on libavcodec.",
|
||||||
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
||||||
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/dts",
|
"name": "@mediabunny/dts",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.",
|
"description": "DTS decoder and encoder extension for Mediabunny, based on libavcodec.",
|
||||||
"main": "./dist/bundles/mediabunny-dts.mjs",
|
"main": "./dist/bundles/mediabunny-dts.mjs",
|
||||||
"module": "./dist/bundles/mediabunny-dts.mjs",
|
"module": "./dist/bundles/mediabunny-dts.mjs",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/flac-encoder",
|
"name": "@mediabunny/flac-encoder",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
|
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
|
||||||
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||||
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/mp3-encoder",
|
"name": "@mediabunny/mp3-encoder",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/prores",
|
"name": "@mediabunny/prores",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
|
"description": "Apple ProRes decoder extension for Mediabunny, based on TurboRes.",
|
||||||
"main": "./dist/bundles/mediabunny-prores.mjs",
|
"main": "./dist/bundles/mediabunny-prores.mjs",
|
||||||
"module": "./dist/bundles/mediabunny-prores.mjs",
|
"module": "./dist/bundles/mediabunny-prores.mjs",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@mediabunny/server",
|
"name": "@mediabunny/server",
|
||||||
"author": "Vanilagy",
|
"author": "Vanilagy",
|
||||||
"version": "1.55.4",
|
"version": "1.55.6",
|
||||||
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
|
"description": "Adds full video and audio decoder and encoder support to Mediabunny for use in server-side environments (Node, Bun, Deno). Based on NodeAV.",
|
||||||
"main": "./dist/bundles/mediabunny-server.cjs",
|
"main": "./dist/bundles/mediabunny-server.cjs",
|
||||||
"module": "./dist/bundles/mediabunny-server.mjs",
|
"module": "./dist/bundles/mediabunny-server.mjs",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-av": "^6.0.0",
|
"node-av": "^6.0.0",
|
||||||
"@mediabunny/prores": "^1.55.4"
|
"@mediabunny/prores": "^1.55.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"mediabunny": "^1.45.0"
|
"mediabunny": "^1.45.0"
|
||||||
|
|||||||
+11
-2
@@ -12,7 +12,16 @@ import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
|||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { Logging } from './logging';
|
import { Logging } from './logging';
|
||||||
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
||||||
import { assert, isThenable, MaybePromise, Rational, Rotation, roundToDivisor, simplifyRational } from './misc';
|
import {
|
||||||
|
assert,
|
||||||
|
isNumber,
|
||||||
|
isThenable,
|
||||||
|
MaybePromise,
|
||||||
|
Rational,
|
||||||
|
Rotation,
|
||||||
|
roundToDivisor,
|
||||||
|
simplifyRational,
|
||||||
|
} from './misc';
|
||||||
import { TrackType } from './output';
|
import { TrackType } from './output';
|
||||||
import { EncodedPacket, PacketType } from './packet';
|
import { EncodedPacket, PacketType } from './packet';
|
||||||
import { TrackDisposition } from './metadata';
|
import { TrackDisposition } from './metadata';
|
||||||
@@ -869,7 +878,7 @@ export class InputVideoTrack extends InputTrack {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
options.targetPacketCount !== undefined
|
options.targetPacketCount !== undefined
|
||||||
&& (!Number.isFinite(options.targetPacketCount) || options.targetPacketCount < 0)
|
&& (!isNumber(options.targetPacketCount) || options.targetPacketCount < 0)
|
||||||
) {
|
) {
|
||||||
throw new TypeError('options.targetPacketCount must be a non-negative number.');
|
throw new TypeError('options.targetPacketCount must be a non-negative number.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1558,10 +1558,9 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
|
|
||||||
case 'esds': {
|
case 'esds': {
|
||||||
const track = this.currentTrack;
|
const track = this.currentTrack;
|
||||||
if (!track) {
|
if (!track || track.info?.type !== 'audio') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
assert(track.info?.type === 'audio');
|
|
||||||
|
|
||||||
slice.skip(4); // Version + flags
|
slice.skip(4); // Version + flags
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { MpegTsMuxer } from './mpeg-ts/mpeg-ts-muxer';
|
|||||||
import { WaveMuxer } from './wave/wave-muxer';
|
import { WaveMuxer } from './wave/wave-muxer';
|
||||||
import { HlsMuxer } from './hls/hls-muxer';
|
import { HlsMuxer } from './hls/hls-muxer';
|
||||||
import { HLS_MIME_TYPE } from './hls/hls-misc';
|
import { HLS_MIME_TYPE } from './hls/hls-misc';
|
||||||
import { MaybePromise, FilePath, toArray } from './misc';
|
import { MaybePromise, FilePath, isNumber, toArray } from './misc';
|
||||||
import { Target } from './target';
|
import { Target } from './target';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,7 +229,7 @@ export abstract class IsobmffOutputFormat extends OutputFormat {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
options.minimumFragmentDuration !== undefined
|
options.minimumFragmentDuration !== undefined
|
||||||
&& (!Number.isFinite(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0)
|
&& (!isNumber(options.minimumFragmentDuration) || options.minimumFragmentDuration < 0)
|
||||||
) {
|
) {
|
||||||
throw new TypeError('options.minimumFragmentDuration, when provided, must be a non-negative number.');
|
throw new TypeError('options.minimumFragmentDuration, when provided, must be a non-negative number.');
|
||||||
}
|
}
|
||||||
@@ -514,7 +514,7 @@ export class MkvOutputFormat extends OutputFormat {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
options.minimumClusterDuration !== undefined
|
options.minimumClusterDuration !== undefined
|
||||||
&& (!Number.isFinite(options.minimumClusterDuration) || options.minimumClusterDuration < 0)
|
&& (!isNumber(options.minimumClusterDuration) || options.minimumClusterDuration < 0)
|
||||||
) {
|
) {
|
||||||
throw new TypeError('options.minimumClusterDuration, when provided, must be a non-negative number.');
|
throw new TypeError('options.minimumClusterDuration, when provided, must be a non-negative number.');
|
||||||
}
|
}
|
||||||
@@ -861,7 +861,7 @@ export class OggOutputFormat extends OutputFormat {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
options.maximumPageDuration !== undefined
|
options.maximumPageDuration !== undefined
|
||||||
&& (!Number.isFinite(options.maximumPageDuration) || options.maximumPageDuration <= 0)
|
&& (!isNumber(options.maximumPageDuration) || options.maximumPageDuration <= 0)
|
||||||
) {
|
) {
|
||||||
throw new TypeError('options.maximumPageDuration, when provided, must be a positive number.');
|
throw new TypeError('options.maximumPageDuration, when provided, must be a positive number.');
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-67
@@ -2658,7 +2658,7 @@ export class AudioSample implements Disposable {
|
|||||||
const { format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
|
const { format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
|
||||||
let { planeIndex } = options;
|
let { planeIndex } = options;
|
||||||
|
|
||||||
const srcFormat = this.format;
|
let srcFormat = this.format;
|
||||||
const destFormat = format ?? this.format;
|
const destFormat = format ?? this.format;
|
||||||
if (!destFormat) throw new Error('Destination format not determined');
|
if (!destFormat) throw new Error('Destination format not determined');
|
||||||
|
|
||||||
@@ -2705,88 +2705,133 @@ export class AudioSample implements Disposable {
|
|||||||
frameOffset,
|
frameOffset,
|
||||||
copyFrameCount,
|
copyFrameCount,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Per spec, only f32-planar conversion must be supported, but in practice, all browsers support all
|
try {
|
||||||
// destination formats, so let's just delegate here:
|
// The spec only requires f32-planar to work here, but most of the time, all formats work. Sometimes
|
||||||
this._data.copyTo(destination, {
|
// they don't, and that's why we try-catch it.
|
||||||
planeIndex,
|
this._data.copyTo(destination, {
|
||||||
frameOffset,
|
planeIndex,
|
||||||
frameCount: copyFrameCount,
|
frameOffset,
|
||||||
format: destFormat,
|
frameCount: copyFrameCount,
|
||||||
});
|
format: destFormat,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (destFormat === 'f32-planar') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format conversion likely wasn't supported, so let's fall back to the manual conversion path
|
||||||
|
srcFormat = 'f32-planar';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
const readFn = getReadFunction(srcFormat);
|
|
||||||
const srcBytesPerSample = getBytesPerSample(srcFormat);
|
|
||||||
const srcIsPlanar = formatIsPlanar(srcFormat);
|
|
||||||
|
|
||||||
let uint8Data: Uint8Array;
|
const readFn = getReadFunction(srcFormat);
|
||||||
if (this._data instanceof AudioSampleResource) {
|
const srcBytesPerSample = getBytesPerSample(srcFormat);
|
||||||
const getDataPlaneValidated = (index: number) => {
|
const srcIsPlanar = formatIsPlanar(srcFormat);
|
||||||
const result = (this._data as AudioSampleResource).getDataPlane(index);
|
|
||||||
if (!(result instanceof Uint8Array)) {
|
|
||||||
throw new TypeError('getDataPlane() must return a Uint8Array.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedSize = numFrames * srcBytesPerSample * (srcIsPlanar ? 1 : numChannels);
|
let uint8Data: Uint8Array;
|
||||||
if (result.byteLength !== expectedSize) {
|
if (this._data instanceof AudioSampleResource) {
|
||||||
throw new TypeError(
|
const getDataPlaneValidated = (index: number) => {
|
||||||
`Data plane ${index} has invalid size. Expected exactly ${expectedSize} bytes, got`
|
const result = (this._data as AudioSampleResource).getDataPlane(index);
|
||||||
+ ` ${result.byteLength} bytes.`,
|
if (!(result instanceof Uint8Array)) {
|
||||||
);
|
throw new TypeError('getDataPlane() must return a Uint8Array.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
const expectedSize = numFrames * srcBytesPerSample * (srcIsPlanar ? 1 : numChannels);
|
||||||
};
|
if (result.byteLength !== expectedSize) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Data plane ${index} has invalid size. Expected exactly ${expectedSize} bytes, got`
|
||||||
|
+ ` ${result.byteLength} bytes.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (srcIsPlanar) {
|
return result;
|
||||||
if (destIsPlanar) {
|
};
|
||||||
// Only one source plane will be extracted, so let's fetch only that one
|
|
||||||
uint8Data = getDataPlaneValidated(planeIndex);
|
if (srcIsPlanar) {
|
||||||
planeIndex = 0; // To fix the subsequent access
|
if (destIsPlanar) {
|
||||||
} else {
|
// Only one source plane will be extracted, so let's fetch only that one
|
||||||
// Pack all planes tightly together
|
uint8Data = getDataPlaneValidated(planeIndex);
|
||||||
uint8Data = new Uint8Array(numFrames * srcBytesPerSample * numChannels);
|
planeIndex = 0; // To fix the subsequent access
|
||||||
for (let ch = 0; ch < numChannels; ch++) {
|
|
||||||
const planeData = getDataPlaneValidated(ch);
|
|
||||||
uint8Data.set(planeData, ch * numFrames * srcBytesPerSample);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
uint8Data = getDataPlaneValidated(0); // That's the only plane there is
|
// Pack all planes tightly together
|
||||||
|
uint8Data = new Uint8Array(numFrames * srcBytesPerSample * numChannels);
|
||||||
|
for (let ch = 0; ch < numChannels; ch++) {
|
||||||
|
const planeData = getDataPlaneValidated(ch);
|
||||||
|
uint8Data.set(planeData, ch * numFrames * srcBytesPerSample);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
uint8Data = this._data;
|
uint8Data = getDataPlaneValidated(0); // That's the only plane there is
|
||||||
}
|
}
|
||||||
|
} else if (this._data instanceof Uint8Array) {
|
||||||
|
uint8Data = this._data;
|
||||||
|
} else {
|
||||||
|
assert(srcFormat === 'f32-planar');
|
||||||
|
|
||||||
const srcView = toDataView(uint8Data);
|
if (destIsPlanar) {
|
||||||
|
// Only one source plane will be read, so let's copy only that one
|
||||||
|
uint8Data = new Uint8Array(this._data.allocationSize({
|
||||||
|
format: 'f32-planar',
|
||||||
|
planeIndex,
|
||||||
|
}));
|
||||||
|
this._data.copyTo(uint8Data, {
|
||||||
|
format: 'f32-planar',
|
||||||
|
planeIndex,
|
||||||
|
});
|
||||||
|
planeIndex = 0; // To fix the subsequent access
|
||||||
|
} else {
|
||||||
|
// All planes will be read, so fetch 'em all
|
||||||
|
uint8Data = new Uint8Array(this._data.allocationSize({
|
||||||
|
format: 'f32-planar',
|
||||||
|
planeIndex: 0,
|
||||||
|
}) * numChannels);
|
||||||
|
|
||||||
for (let i = 0; i < copyFrameCount; i++) {
|
for (let ch = 0; ch < numChannels; ch++) {
|
||||||
if (destIsPlanar) {
|
this._data.copyTo(
|
||||||
const destOffset = i * destBytesPerSample;
|
uint8Data.subarray(
|
||||||
|
ch * numFrames * srcBytesPerSample,
|
||||||
|
(ch + 1) * numFrames * srcBytesPerSample,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
format: 'f32-planar',
|
||||||
|
planeIndex: ch,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcView = toDataView(uint8Data);
|
||||||
|
|
||||||
|
for (let i = 0; i < copyFrameCount; i++) {
|
||||||
|
if (destIsPlanar) {
|
||||||
|
const destOffset = i * destBytesPerSample;
|
||||||
|
let srcOffset: number;
|
||||||
|
if (srcIsPlanar) {
|
||||||
|
srcOffset = (planeIndex * numFrames + (i + frameOffset)) * srcBytesPerSample;
|
||||||
|
} else {
|
||||||
|
srcOffset = (((i + frameOffset) * numChannels) + planeIndex) * srcBytesPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = readFn(srcView, srcOffset);
|
||||||
|
writeFn(destView, destOffset, normalized);
|
||||||
|
} else {
|
||||||
|
for (let ch = 0; ch < numChannels; ch++) {
|
||||||
|
const destIndex = i * numChannels + ch;
|
||||||
|
const destOffset = destIndex * destBytesPerSample;
|
||||||
let srcOffset: number;
|
let srcOffset: number;
|
||||||
if (srcIsPlanar) {
|
if (srcIsPlanar) {
|
||||||
srcOffset = (planeIndex * numFrames + (i + frameOffset)) * srcBytesPerSample;
|
srcOffset = (ch * numFrames + (i + frameOffset)) * srcBytesPerSample;
|
||||||
} else {
|
} else {
|
||||||
srcOffset = (((i + frameOffset) * numChannels) + planeIndex) * srcBytesPerSample;
|
srcOffset = (((i + frameOffset) * numChannels) + ch) * srcBytesPerSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = readFn(srcView, srcOffset);
|
const normalized = readFn(srcView, srcOffset);
|
||||||
writeFn(destView, destOffset, normalized);
|
writeFn(destView, destOffset, normalized);
|
||||||
} else {
|
|
||||||
for (let ch = 0; ch < numChannels; ch++) {
|
|
||||||
const destIndex = i * numChannels + ch;
|
|
||||||
const destOffset = destIndex * destBytesPerSample;
|
|
||||||
let srcOffset: number;
|
|
||||||
if (srcIsPlanar) {
|
|
||||||
srcOffset = (ch * numFrames + (i + frameOffset)) * srcBytesPerSample;
|
|
||||||
} else {
|
|
||||||
srcOffset = (((i + frameOffset) * numChannels) + ch) * srcBytesPerSample;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = readFn(srcView, srcOffset);
|
|
||||||
writeFn(destView, destOffset, normalized);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3201,9 +3246,9 @@ export const toInterleavedAudioFormat = (format: AudioSampleFormat): 'u8' | 's16
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format
|
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format crashes the tab when
|
||||||
* crashes the tab when there are more than 2 channels. This function works around that by always
|
* there are more than 2 channels. This function works around that by always copying with the source format and then
|
||||||
* copying with the source format and then manually converting to the destination format.
|
* manually converting to the destination format.
|
||||||
*
|
*
|
||||||
* See https://bugs.webkit.org/show_bug.cgi?id=302521.
|
* See https://bugs.webkit.org/show_bug.cgi?id=302521.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+36
-4
@@ -496,6 +496,14 @@ export type BlobSourceOptions = {
|
|||||||
useStreamReader?: boolean;
|
useStreamReader?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const blobReaderRegistry = typeof FinalizationRegistry !== 'undefined'
|
||||||
|
? new FinalizationRegistry<ReadableStreamDefaultReader<Uint8Array>>((reader) => {
|
||||||
|
// Browsers don't GC readers that aren't "done", which creates indefinite memory leaks,
|
||||||
|
// see https://github.com/Vanilagy/mediabunny/issues/144#issuecomment-5465467062. So, we need to do it instead.
|
||||||
|
void reader.cancel().catch(() => {});
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A source backed by a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Since a
|
* A source backed by a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Since a
|
||||||
* [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) is also a `Blob`, this is the source to use when
|
* [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) is also a `Blob`, this is the source to use when
|
||||||
@@ -510,6 +518,8 @@ export class BlobSource extends Source {
|
|||||||
_options: BlobSourceOptions;
|
_options: BlobSourceOptions;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_orchestrator: ReadOrchestrator;
|
_orchestrator: ReadOrchestrator;
|
||||||
|
/** @internal */
|
||||||
|
_readers = new WeakMap<ReadWorker, ReadableStreamDefaultReader<Uint8Array> | null>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link BlobSource} backed by the specified
|
* Creates a new {@link BlobSource} backed by the specified
|
||||||
@@ -541,6 +551,17 @@ export class BlobSource extends Source {
|
|||||||
maxCacheSize: options.maxCacheSize ?? (8 * 2 ** 20 /* 8 MiB */),
|
maxCacheSize: options.maxCacheSize ?? (8 * 2 ** 20 /* 8 MiB */),
|
||||||
maxWorkerCount: 4,
|
maxWorkerCount: 4,
|
||||||
runWorker: this._runWorker.bind(this),
|
runWorker: this._runWorker.bind(this),
|
||||||
|
onIdleWorkerRemoved: (worker) => {
|
||||||
|
const reader = this._readers.get(worker);
|
||||||
|
|
||||||
|
if (reader) {
|
||||||
|
this._readers.delete(worker);
|
||||||
|
blobReaderRegistry?.unregister(worker);
|
||||||
|
|
||||||
|
// If we don't do this, memory leaks indefinitely
|
||||||
|
void reader.cancel().catch(() => {});
|
||||||
|
}
|
||||||
|
},
|
||||||
prefetchProfile: PREFETCH_PROFILES.fileSystem,
|
prefetchProfile: PREFETCH_PROFILES.fileSystem,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -562,9 +583,6 @@ export class BlobSource extends Source {
|
|||||||
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
|
return this._orchestrator.read(start, end, minReadPosition, maxReadPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
_readers = new WeakMap<ReadWorker, ReadableStreamDefaultReader<Uint8Array> | null>();
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
private async _runWorker(worker: ReadWorker) {
|
private async _runWorker(worker: ReadWorker) {
|
||||||
assert(worker.strictTarget);
|
assert(worker.strictTarget);
|
||||||
@@ -578,10 +596,15 @@ export class BlobSource extends Source {
|
|||||||
// - ReadableStream stalls under backpressure (especially video)
|
// - ReadableStream stalls under backpressure (especially video)
|
||||||
// Affects Safari and all iOS browsers (Chrome, Firefox, etc.).
|
// Affects Safari and all iOS browsers (Chrome, Firefox, etc.).
|
||||||
// Use arrayBuffer() fallback for WebKit browsers.
|
// Use arrayBuffer() fallback for WebKit browsers.
|
||||||
if ('stream' in this._blob && !isWebKit() && this._options.useStreamReader !== false) {
|
if (
|
||||||
|
'stream' in this._blob && !isWebKit()
|
||||||
|
&& this._options.useStreamReader !== false
|
||||||
|
) {
|
||||||
// Get a reader of the blob starting at the required offset, and then keep it around
|
// Get a reader of the blob starting at the required offset, and then keep it around
|
||||||
const slice = this._blob.slice(worker.currentPos);
|
const slice = this._blob.slice(worker.currentPos);
|
||||||
reader = slice.stream().getReader();
|
reader = slice.stream().getReader();
|
||||||
|
|
||||||
|
blobReaderRegistry?.register(worker, reader, worker);
|
||||||
} else {
|
} else {
|
||||||
// We'll need to use more primitive ways
|
// We'll need to use more primitive ways
|
||||||
reader = null;
|
reader = null;
|
||||||
@@ -1970,6 +1993,7 @@ class ReadOrchestrator {
|
|||||||
runWorker: (worker: ReadWorker) => Promise<void>;
|
runWorker: (worker: ReadWorker) => Promise<void>;
|
||||||
prefetchProfile: PrefetchProfile;
|
prefetchProfile: PrefetchProfile;
|
||||||
maxWorkerCount: number;
|
maxWorkerCount: number;
|
||||||
|
onIdleWorkerRemoved?: (worker: ReadWorker) => void;
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
read(
|
read(
|
||||||
@@ -2263,6 +2287,7 @@ class ReadOrchestrator {
|
|||||||
assert(oldestIndex !== null);
|
assert(oldestIndex !== null);
|
||||||
assert(oldestWorker.pendingSlices.length === 0);
|
assert(oldestWorker.pendingSlices.length === 0);
|
||||||
this.workers.splice(oldestIndex, 1);
|
this.workers.splice(oldestIndex, 1);
|
||||||
|
this.options.onIdleWorkerRemoved?.(oldestWorker);
|
||||||
} else {
|
} else {
|
||||||
return null; // All workers are still running, we can't create a new one
|
return null; // All workers are still running, we can't create a new one
|
||||||
}
|
}
|
||||||
@@ -2410,6 +2435,7 @@ class ReadOrchestrator {
|
|||||||
otherWorker.currentPos, otherWorker.targetPos, // These should typically be equal when the worker's idle
|
otherWorker.currentPos, otherWorker.targetPos, // These should typically be equal when the worker's idle
|
||||||
)) {
|
)) {
|
||||||
this.workers.splice(i, 1);
|
this.workers.splice(i, 1);
|
||||||
|
this.options.onIdleWorkerRemoved?.(otherWorker);
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2486,6 +2512,7 @@ class ReadOrchestrator {
|
|||||||
|
|
||||||
worker.running = false;
|
worker.running = false;
|
||||||
this.workers.splice(index, 1);
|
this.workers.splice(index, 1);
|
||||||
|
this.options.onIdleWorkerRemoved?.(worker);
|
||||||
|
|
||||||
if (this.fileSize === null) {
|
if (this.fileSize === null) {
|
||||||
// We can now deduce the file size!
|
// We can now deduce the file size!
|
||||||
@@ -2597,6 +2624,11 @@ class ReadOrchestrator {
|
|||||||
|
|
||||||
worker.pendingSlices.length = 0;
|
worker.pendingSlices.length = 0;
|
||||||
worker.aborted = true;
|
worker.aborted = true;
|
||||||
|
|
||||||
|
if (!worker.running) {
|
||||||
|
// Running workers clean up after themselves when they notice the abort
|
||||||
|
this.options.onIdleWorkerRemoved?.(worker);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const queuedRead of this.queuedReads) {
|
for (const queuedRead of this.queuedReads) {
|
||||||
|
|||||||
@@ -137,3 +137,108 @@ test('trim, throws on a closed sample', () => {
|
|||||||
sample.close();
|
sample.close();
|
||||||
expect(() => sample.trim(0, 5)).toThrow('AudioSample is closed.');
|
expect(() => sample.trim(0, 5)).toThrow('AudioSample is closed.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('copyTo, AudioData-backed, planar destination falls back to manual conversion', () => {
|
||||||
|
withMockedNativeCopyTo((calls) => {
|
||||||
|
using sample = makeAudioDataBackedSample();
|
||||||
|
|
||||||
|
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||||
|
const out = new Int16Array(NUM_FRAMES);
|
||||||
|
sample.copyTo(out, { planeIndex: c, format: 's16-planar' });
|
||||||
|
|
||||||
|
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||||
|
expect(out[f]).toBe(cellValue(f, c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With a frame offset
|
||||||
|
const out = new Int16Array(NUM_FRAMES - 3);
|
||||||
|
sample.copyTo(out, { planeIndex: 1, format: 's16-planar', frameOffset: 3 });
|
||||||
|
for (let f = 0; f < NUM_FRAMES - 3; f++) {
|
||||||
|
expect(out[f]).toBe(cellValue(f + 3, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The native conversion must have been attempted (and thrown)
|
||||||
|
expect(calls.some(options => options.format === 's16-planar')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copyTo, AudioData-backed, interleaved destination falls back to manual conversion', () => {
|
||||||
|
withMockedNativeCopyTo((calls) => {
|
||||||
|
using sample = makeAudioDataBackedSample();
|
||||||
|
|
||||||
|
const out = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
|
||||||
|
sample.copyTo(out, { planeIndex: 0, format: 'f32' });
|
||||||
|
|
||||||
|
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||||
|
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||||
|
expect(out[f * NUM_CHANNELS + c]).toBe(Math.fround(cellValue(f, c) / 32767));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The native conversion must have been attempted (and thrown)
|
||||||
|
expect(calls.some(options => options.format === 'f32')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('copyTo, AudioData-backed, control case with native format conversion', () => {
|
||||||
|
using sample = makeAudioDataBackedSample();
|
||||||
|
|
||||||
|
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||||
|
const out = new Int16Array(NUM_FRAMES);
|
||||||
|
sample.copyTo(out, { planeIndex: c, format: 's16-planar' });
|
||||||
|
|
||||||
|
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||||
|
// Allow off-by-one
|
||||||
|
expect(Math.abs(out[f]! - cellValue(f, c))).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
|
||||||
|
sample.copyTo(out, { planeIndex: 0, format: 'f32' });
|
||||||
|
|
||||||
|
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||||
|
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||||
|
expect(out[f * NUM_CHANNELS + c]).toBe(Math.fround(cellValue(f, c) / 32767));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const withMockedNativeCopyTo = (fn: (calls: AudioDataCopyToOptions[]) => void) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
const original = AudioData.prototype.copyTo;
|
||||||
|
const calls: AudioDataCopyToOptions[] = [];
|
||||||
|
|
||||||
|
AudioData.prototype.copyTo = function (this: AudioData, destination, options) {
|
||||||
|
calls.push(options);
|
||||||
|
if (options.format !== undefined && options.format !== 'f32-planar') {
|
||||||
|
throw new DOMException('Format conversion not supported.', 'NotSupportedError');
|
||||||
|
}
|
||||||
|
|
||||||
|
return original.call(this, destination, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fn(calls);
|
||||||
|
} finally {
|
||||||
|
AudioData.prototype.copyTo = original;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeAudioDataBackedSample = () => {
|
||||||
|
const data = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
|
||||||
|
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||||
|
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||||
|
data[f * NUM_CHANNELS + c] = cellValue(f, c) / 32767;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AudioSample(new AudioData({
|
||||||
|
format: 'f32',
|
||||||
|
sampleRate: SAMPLE_RATE,
|
||||||
|
numberOfFrames: NUM_FRAMES,
|
||||||
|
numberOfChannels: NUM_CHANNELS,
|
||||||
|
timestamp: TIMESTAMP * 1e6,
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user