mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-06 15:23:47 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53833f6e94 | ||
|
|
794c97b91a | ||
|
|
5b4a5db1a5 | ||
|
|
2f0c040fcd | ||
|
|
b4a6757739 | ||
|
|
4affad934e | ||
|
|
324fae5153 | ||
|
|
08e3a85370 | ||
|
|
ad167c77e7 | ||
|
|
8b9a1acf16 | ||
|
|
0da1107858 | ||
|
|
00599e556e | ||
|
|
e1c6ab1866 | ||
|
|
bc533c71f7 | ||
|
|
879fd75547 |
+14
-6
@@ -91,13 +91,16 @@
|
||||
//const startTime = await primaryTrack.getFirstTimestamp();
|
||||
//console.log(startTime)
|
||||
|
||||
let ctx = null;
|
||||
const canvas = new OffscreenCanvas(1280, 720);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
//let ctx = null;
|
||||
let conversion = await Mediabunny.Conversion.init({
|
||||
input,
|
||||
output,
|
||||
audio: {
|
||||
codec: 'aac',
|
||||
forceTranscode: true,
|
||||
//codec: 'aac',
|
||||
//forceTranscode: true,
|
||||
//forceTranscode: true,
|
||||
//sampleFormat: 's16',
|
||||
},
|
||||
@@ -126,7 +129,10 @@
|
||||
},
|
||||
*/
|
||||
video: {
|
||||
discard: true,
|
||||
process: (sample) => {
|
||||
sample.draw(ctx, 0, 0, 1280, 720);
|
||||
return new Mediabunny.VideoSample(canvas, { timestamp: sample.timestamp, duration: sample.duration });
|
||||
},
|
||||
},
|
||||
tags: {} ?? {
|
||||
title: 'Bigggy',
|
||||
@@ -146,8 +152,10 @@
|
||||
}
|
||||
},
|
||||
trim: {
|
||||
start: 300.14984567374756 - 100,
|
||||
end: 310.1548298151939 - 100,
|
||||
start: -2,
|
||||
end: 10,
|
||||
//start: 300.14984567374756 - 100,
|
||||
//end: 310.1548298151939 - 100,
|
||||
//end: 10,
|
||||
//start: startTime,
|
||||
//end: startTime + 2,
|
||||
|
||||
@@ -225,6 +225,11 @@ Create the sink like so:
|
||||
import { VideoSampleSink } from 'mediabunny';
|
||||
|
||||
const sink = new VideoSampleSink(videoTrack);
|
||||
|
||||
// Optionally, configure the decoder:
|
||||
const sink = new VideoSampleSink(videoTrack, {
|
||||
hardwareAcceleration: 'prefer-software',
|
||||
});
|
||||
```
|
||||
|
||||
#### Single retrieval
|
||||
@@ -363,6 +368,8 @@ type CanvasSinkOptions = {
|
||||
rotation?: 0 | 90 | 180 | 270;
|
||||
crop?: { left: number; top: number; width: number; height: number };
|
||||
poolSize?: number;
|
||||
alpha?: boolean;
|
||||
decoderOptions?: VideoSinkDecoderOptions;
|
||||
};
|
||||
```
|
||||
- `width`\
|
||||
@@ -380,6 +387,10 @@ type CanvasSinkOptions = {
|
||||
Specifies the rectangular region of the input video to crop to. The crop region will automatically be clamped to the dimensions of the input video track. Cropping is performed after rotation but before resizing. The crop region is in the _display pixel space_ of the underlying video data.
|
||||
- `poolSize`\
|
||||
See [Canvas pool](#canvas-pool).
|
||||
- `alpha`\
|
||||
Whether the output canvases should have transparency instead of a black background. Defaults to `false`. Set this to `true` when using this sink to read transparent videos.
|
||||
- `decoderOptions`\
|
||||
Additional preferences for the underlying video decoder.
|
||||
|
||||
Some examples:
|
||||
```ts
|
||||
|
||||
@@ -12,7 +12,7 @@ Mediabunny exposes HLS playlists as if they were a single giant input file. Like
|
||||
|
||||
## HLS inputs
|
||||
|
||||
HLS playlists (master & media) are read through the same `Input` interface as all other media files in Mediabunny. HLS must read multiple files, meaning any [`PathedSource`](../api/PathedSource) is required:
|
||||
HLS playlists (master & media) are read through the same `Input` interface as all other media files in Mediabunny. HLS must read multiple files, meaning any [`PathedSource`](../api/PathedSource) (such as `UrlSource`) is required:
|
||||
```ts
|
||||
import { Input, UrlSource, HLS_FORMATS } from 'mediabunny';
|
||||
|
||||
@@ -233,6 +233,35 @@ This makes cross-track synchronization trivial. To know if a track's timestamps
|
||||
await track.isRelativeToUnixEpoch(); // => boolean
|
||||
```
|
||||
|
||||
### Disabling Unix offsets
|
||||
|
||||
If you don't want Mediabunny to offset packet timestamps to be in Unix time, you can set `offsetTimestampsByDateTime` to `false` in the input format options:
|
||||
```ts
|
||||
const input = new Input({
|
||||
// ...
|
||||
formatOptions: {
|
||||
hls: {
|
||||
offsetTimestampsByDateTime: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This way, track and packet timestamps behave as if no `#EXT-X-PROGRAM-DATE-TIME` tags existed. This also means that any date time gaps are completely collapsed.
|
||||
|
||||
You will still be able to query the Unix time metadata via a mapping function on the `InputTrack`:
|
||||
```ts
|
||||
const firstTimestamp = await inputTrack.getFirstTimestamp(); // => 0
|
||||
await inputTrack.getUnixTimeForTimestamp(firstTimestamp); // => 1704067200 (Unix timestamp for 2024-01-01T00:00:00Z)
|
||||
```
|
||||
|
||||
This function performs a piecewise-continuous mapping of timestamp space into Unix time space.
|
||||
|
||||
If no wall-clock time information is available, `getUnixTimeForTimestamp()` will return `null`. You can check the presence of Unix time metadata via:
|
||||
```ts
|
||||
await inputTrack.hasUnixTimeMapping(); // boolean
|
||||
```
|
||||
|
||||
## Live HLS
|
||||
|
||||
HLS playlists may be live. You can check that a track is live via:
|
||||
|
||||
Generated
+120
-62
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mediabunny",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -382,6 +382,7 @@
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1113,6 +1114,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz",
|
||||
"integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
@@ -1122,6 +1124,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz",
|
||||
"integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@fidm/asn1": "^1.0.4",
|
||||
"tweetnacl": "^1.0.1"
|
||||
@@ -1439,7 +1442,8 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@mediabunny/aac-encoder": {
|
||||
"resolved": "packages/aac-encoder",
|
||||
@@ -1569,13 +1573,15 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@minhducsun2002/leb128/-/leb128-1.0.0.tgz",
|
||||
"integrity": "sha512-eFrYUPDVHeuwWHluTG1kwNQUEUcFjVKYwPkU8z9DR1JH3AW7JtJsG9cRVGmwz809kKtGfwGJj58juCZxEvnI/g==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
@@ -1591,6 +1597,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
@@ -1603,6 +1610,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz",
|
||||
"integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
@@ -1616,6 +1624,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz",
|
||||
"integrity": "sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
@@ -1628,6 +1637,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz",
|
||||
"integrity": "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
@@ -1640,6 +1650,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz",
|
||||
"integrity": "sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.7.0",
|
||||
"@peculiar/asn1-pkcs8": "^2.7.0",
|
||||
@@ -1654,6 +1665,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz",
|
||||
"integrity": "sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
@@ -1666,6 +1678,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz",
|
||||
"integrity": "sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.7.0",
|
||||
"@peculiar/asn1-pfx": "^2.7.0",
|
||||
@@ -1682,6 +1695,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz",
|
||||
"integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
@@ -1694,6 +1708,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
|
||||
"integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.6",
|
||||
@@ -1705,6 +1720,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz",
|
||||
"integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
@@ -1717,6 +1733,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz",
|
||||
"integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.7.0",
|
||||
"@peculiar/asn1-x509": "^2.7.0",
|
||||
@@ -1729,6 +1746,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
|
||||
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
@@ -1738,6 +1756,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
|
||||
"integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.6.0",
|
||||
"@peculiar/asn1-csr": "^2.6.0",
|
||||
@@ -2210,9 +2229,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-darwin-arm64": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-darwin-arm64/-/node-av-darwin-arm64-5.2.4.tgz",
|
||||
"integrity": "sha512-uQh6jWXDeXtjuaGOXrBx2rszPDcQbRa4W1YFrmlQG+ffOo+IJyczGjhTWfgzebRksB9tMAe9ViztU39THWZAkw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-darwin-arm64/-/node-av-darwin-arm64-6.0.0.tgz",
|
||||
"integrity": "sha512-OAXfBGnOk8ddPhoZmqcq8K04m1oLgGsiKnZ9p7oPI8jVsZaI+v7NXr5lum4bVakIh2TOhnD6EMdZl8v0KHgJLA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2226,9 +2245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-darwin-x64": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-darwin-x64/-/node-av-darwin-x64-5.2.4.tgz",
|
||||
"integrity": "sha512-S9ptQF7JhvGSbvZH9jVeDAPXyRklA7rySKiFzi4UeLTMJslgFTSWFgY2nJAtHrP4wRdkAPnAVjExqd8U7jRZgg==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-darwin-x64/-/node-av-darwin-x64-6.0.0.tgz",
|
||||
"integrity": "sha512-9SMQkfzEGJgz9GedF43bPIqTp7HbeN/UWwPtAq9tR02mZ4vDGYlzvMvQXG4yx2JEiogcYyg8hDzoLud4zkqKww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2242,9 +2261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-linux-arm64": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-linux-arm64/-/node-av-linux-arm64-5.2.4.tgz",
|
||||
"integrity": "sha512-XlQAWIcqFxVItr98VShaxzx57Cfl0y+RPCPciLOHXuEmfeKjON4o/xdbChHg02hHFrVAeSCfaHWQmHNgB9m6eQ==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-linux-arm64/-/node-av-linux-arm64-6.0.0.tgz",
|
||||
"integrity": "sha512-pZsX2PPLxL/KioqZoofbm6B4aFLRGpvAB6JSNvCZIiU5eW2QSBZ68H9gC/IZ5tknH6+mX7ZT5vrJP0B0tSfvyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2258,9 +2277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-linux-x64": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-linux-x64/-/node-av-linux-x64-5.2.4.tgz",
|
||||
"integrity": "sha512-+2LdgZ7irDik++0mO6Bo1odYa2NZvze6xiy4B8jTbcf3F8k46qQEKkPjBwtV0qQu5fn3M+fUc/wk5coHdfxjxA==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-linux-x64/-/node-av-linux-x64-6.0.0.tgz",
|
||||
"integrity": "sha512-tvaH/0o+r2z4qEnHVyeUr6tIAzykjZcjssy93WEO+d2iRKO5araE702JEVBidJNTq2c/hd7p9Ml2KVLqere1ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2274,9 +2293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-win32-arm64-mingw": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-arm64-mingw/-/node-av-win32-arm64-mingw-5.2.4.tgz",
|
||||
"integrity": "sha512-uVNgRQ44uSdtMcR9O6R+nrBou5BWmroTFnGPI3PX8pHR8oe1Nim5zXtWK0qGTTqOiTPNsK5XnSshM3c5/n0/Fg==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-arm64-mingw/-/node-av-win32-arm64-mingw-6.0.0.tgz",
|
||||
"integrity": "sha512-mE2vSv984b8wrthQedup0ofeyZ8PTj/iwiXpYS1rmZmbmd2XMJ+Q8+oHKSu3+DruErOvURhWw+VflTGUlZtnBQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2290,9 +2309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-win32-arm64-msvc": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-arm64-msvc/-/node-av-win32-arm64-msvc-5.2.4.tgz",
|
||||
"integrity": "sha512-Gi+Op8j028WHbdl2jR7MnyHqR2J1TfkEogLPludb3jx54Yi+MO1zvouICFUmtSbxd9JC7CAeoU8++CZAMqcEvA==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-arm64-msvc/-/node-av-win32-arm64-msvc-6.0.0.tgz",
|
||||
"integrity": "sha512-bm9CbvhAneEeF4/k1GNXaFlM050cWDSqZ+hha88uETV4kijolQBc3UseQTFy9QvqhD8FftvMSR7C/TM/fBTimA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2306,9 +2325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-win32-x64-mingw": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-x64-mingw/-/node-av-win32-x64-mingw-5.2.4.tgz",
|
||||
"integrity": "sha512-gClAhvV/aJa681tuTlG6dTqqMpnTM5k6QVhnWolBgkbpZXMi0qABzoUBTYpuwxnOEd6j2mBBAVwqx6kut55Ukw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-x64-mingw/-/node-av-win32-x64-mingw-6.0.0.tgz",
|
||||
"integrity": "sha512-4IaoS2eA2mogjcDkOhJD3vVqGXX420/fb3ZCmgBD0G9HMW+mk7qKWq25b+0V2lg6pT9d8eq1DAEMtQ6UhKwODQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2322,9 +2341,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@seydx/node-av-win32-x64-msvc": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-x64-msvc/-/node-av-win32-x64-msvc-5.2.4.tgz",
|
||||
"integrity": "sha512-01s5ij6nudrtZ7ojiAOAgzuUrImvuQYrAE5o6uesoxYqOha6j7yRu5e4qCXRQCDKrh2FBzyO1cGDc7Ht8Ly8wQ==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@seydx/node-av-win32-x64-msvc/-/node-av-win32-x64-msvc-6.0.0.tgz",
|
||||
"integrity": "sha512-fHtjHYrAxjeCjPzHojPCnimh0p+UoiG3Tn0Av/mLSJ8RJcCqryz0iH/zdj6bSgtXHEz5LkHzLoZIVI0BMqY+fw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2429,6 +2448,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@shinyoshiaki/binary-data/-/binary-data-0.6.1.tgz",
|
||||
"integrity": "sha512-7HDb/fQAop2bCmvDIzU5+69i+UJaFgIVp99h1VzK1mpg1JwSODOkjbqD7ilTYnqlnadF8C4XjpwpepxDsGY6+w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"generate-function": "^2.3.1",
|
||||
"is-plain-object": "^2.0.3"
|
||||
@@ -2440,7 +2460,8 @@
|
||||
"node_modules/@shinyoshiaki/jspack": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@shinyoshiaki/jspack/-/jspack-0.0.6.tgz",
|
||||
"integrity": "sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg=="
|
||||
"integrity": "sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "4.2.0",
|
||||
@@ -4173,7 +4194,8 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
|
||||
"integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
@@ -4357,6 +4379,7 @@
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
|
||||
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.6",
|
||||
"pvutils": "^1.1.5",
|
||||
@@ -4501,6 +4524,7 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4563,6 +4587,7 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4587,6 +4612,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
@@ -5534,6 +5560,7 @@
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
@@ -5555,6 +5582,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -5694,6 +5722,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
|
||||
"integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@leichtgewicht/ip-codec": "^2.0.1"
|
||||
},
|
||||
@@ -6395,7 +6424,8 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
@@ -6737,6 +6767,7 @@
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
@@ -7093,6 +7124,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -7182,7 +7214,8 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-1.1.0.tgz",
|
||||
"integrity": "sha512-94smTCQOvigN4d/2R/YDjz8YVG0Sufvv2aAh8P5m42gwhCsDAJqnbNOrxJsrADuAFAA69Q/ptGzxvNcNuIJcvw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
@@ -7197,7 +7230,8 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
@@ -7284,6 +7318,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"isobject": "^3.0.1"
|
||||
},
|
||||
@@ -7295,7 +7330,8 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
@@ -7340,6 +7376,7 @@
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7943,7 +7980,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
@@ -8903,7 +8941,8 @@
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.5.4.tgz",
|
||||
"integrity": "sha512-GcCH0fySxBurJtvr0dfhz0IxHZjc1RP+F+I8xw+LIwkU1a+7HJx8NCDiww1I5u4Hz6g4eR1JlGADEGJ9r4lSfA==",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
@@ -8918,13 +8957,15 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/multicast-dns": {
|
||||
"version": "7.2.5",
|
||||
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
||||
"integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"dns-packet": "^5.2.2",
|
||||
"thunky": "^1.0.2"
|
||||
@@ -8969,24 +9010,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-av": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/node-av/-/node-av-5.2.4.tgz",
|
||||
"integrity": "sha512-L5r+6k+YGvH5MZX8o55JHQbbeRZ+iFieAb1bhKtpa8hrqBEGuSwdkXRwpE4vd414JqVJsq/EQq6s+3qKeviQTg==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-av/-/node-av-6.0.0.tgz",
|
||||
"integrity": "sha512-raMugNMsLlJpmby4vdMyjpNHEw94WYDOpHw4z3ClxowyvWKx880rGbwDdERhS6DudyoLV0DNT+A5h3HBL0xF6Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unzipper": "^0.12.3",
|
||||
"werift": "^0.23.0"
|
||||
"unzipper": "^0.12.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@seydx/node-av-darwin-arm64": "^5.2.4",
|
||||
"@seydx/node-av-darwin-x64": "^5.2.4",
|
||||
"@seydx/node-av-linux-arm64": "^5.2.4",
|
||||
"@seydx/node-av-linux-x64": "^5.2.4",
|
||||
"@seydx/node-av-win32-arm64-mingw": "^5.2.4",
|
||||
"@seydx/node-av-win32-arm64-msvc": "^5.2.4",
|
||||
"@seydx/node-av-win32-x64-mingw": "^5.2.4",
|
||||
"@seydx/node-av-win32-x64-msvc": "^5.2.4"
|
||||
"@seydx/node-av-darwin-arm64": "^6.0.0",
|
||||
"@seydx/node-av-darwin-x64": "^6.0.0",
|
||||
"@seydx/node-av-linux-arm64": "^6.0.0",
|
||||
"@seydx/node-av-linux-x64": "^6.0.0",
|
||||
"@seydx/node-av-win32-arm64-mingw": "^6.0.0",
|
||||
"@seydx/node-av-win32-arm64-msvc": "^6.0.0",
|
||||
"@seydx/node-av-win32-x64-mingw": "^6.0.0",
|
||||
"@seydx/node-av-win32-x64-msvc": "^6.0.0",
|
||||
"werift": "^0.23.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
@@ -9109,6 +9150,7 @@
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -9689,6 +9731,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
@@ -9698,6 +9741,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
|
||||
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
@@ -9801,7 +9845,8 @@
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/regex": {
|
||||
"version": "6.0.1",
|
||||
@@ -10066,7 +10111,8 @@
|
||||
"node_modules/rx.mini": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/rx.mini/-/rx.mini-1.4.0.tgz",
|
||||
"integrity": "sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA=="
|
||||
"integrity": "sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/safaridriver": {
|
||||
"version": "1.0.0",
|
||||
@@ -10736,7 +10782,8 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
@@ -10868,7 +10915,8 @@
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.19.4",
|
||||
@@ -10895,6 +10943,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
|
||||
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
@@ -10906,13 +10955,15 @@
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"license": "Unlicense"
|
||||
"license": "Unlicense",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
@@ -12409,6 +12460,7 @@
|
||||
"resolved": "https://registry.npmjs.org/werift/-/werift-0.23.0.tgz",
|
||||
"integrity": "sha512-/WcIN5DHFG9Ri4anGOmIkp8gxBGFMWSIB/m4sfZ5CWlLfD3iMhiaAUuTBuc+KV3SY9NDmvmLtiN2uaM7k3lVzw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@fidm/x509": "^1.2.1",
|
||||
"@minhducsun2002/leb128": "^1.0.0",
|
||||
@@ -12440,6 +12492,7 @@
|
||||
"resolved": "https://registry.npmjs.org/werift-common/-/werift-common-0.0.3.tgz",
|
||||
"integrity": "sha512-ma3E4BqKTyZVLhrdfTVs2T1tg9seeUtKMRn5e64LwgrogWa62+3LAUoLBUSl1yPWhgSkXId7GmcHuWDen9IJeQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
"debug": "^4.4.0"
|
||||
@@ -12453,6 +12506,7 @@
|
||||
"resolved": "https://registry.npmjs.org/werift-dtls/-/werift-dtls-0.5.7.tgz",
|
||||
"integrity": "sha512-z2fjbP7fFUFmu/Ky4bCKXzdgPTtmSY1DYi0TUf3GG2zJT4jMQ3TQmGY8y7BSSNGetvL4h3pRZ5un0EcSOWpPog==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@fidm/x509": "^1.2.1",
|
||||
"@noble/curves": "^1.3.0",
|
||||
@@ -12472,6 +12526,7 @@
|
||||
"resolved": "https://registry.npmjs.org/werift-ice/-/werift-ice-0.2.2.tgz",
|
||||
"integrity": "sha512-td52pHp+JmFnUn5jfDr/SSNO0dMCbknhuPdN1tFp9cfRj5jaktN63qnAdUuZC20QCC3ETWdsOthcm+RalHpFCQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
"buffer-crc32": "^1.0.0",
|
||||
@@ -12489,6 +12544,7 @@
|
||||
"resolved": "https://registry.npmjs.org/werift-rtp/-/werift-rtp-0.8.8.tgz",
|
||||
"integrity": "sha512-GiYMSdvCyScQaw5bnEsraSoHUVZpjfokJAiLV4R1FsiB06t6XiebPYPpkqB9nYNNKiA8Z/cYWsym7wISq1sYSQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@minhducsun2002/leb128": "^1.0.0",
|
||||
"@shinyoshiaki/jspack": "^0.0.6",
|
||||
@@ -12505,6 +12561,7 @@
|
||||
"resolved": "https://registry.npmjs.org/werift-sctp/-/werift-sctp-0.0.11.tgz",
|
||||
"integrity": "sha512-7109yuI5U7NTEHjqjn0A8VeynytkgVaxM6lRr1Ziv0D8bPcaB8A7U/P88M7WaCpWDoELHoXiRUjQycMWStIgjQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@shinyoshiaki/jspack": "^0.0.6"
|
||||
},
|
||||
@@ -12517,6 +12574,7 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -12806,7 +12864,7 @@
|
||||
},
|
||||
"packages/aac-encoder": {
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12821,7 +12879,7 @@
|
||||
},
|
||||
"packages/ac3": {
|
||||
"name": "@mediabunny/ac3",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12836,7 +12894,7 @@
|
||||
},
|
||||
"packages/flac-encoder": {
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12851,7 +12909,7 @@
|
||||
},
|
||||
"packages/mp3-encoder": {
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/emscripten": "^1.40.1"
|
||||
@@ -12866,10 +12924,10 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@mediabunny/server",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"node-av": "^5.2.4"
|
||||
"node-av": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mediabunny",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/aac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"description": "AAC encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-aac-encoder.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/ac3",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"description": "AC-3 and E-AC-3 (Dolby Digital) decoder and encoder extension for Mediabunny, based on FFmpeg.",
|
||||
"main": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
"module": "./dist/bundles/mediabunny-ac3.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/flac-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"description": "FLAC encoder extension for Mediabunny, based on libFLAC.",
|
||||
"main": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-flac-encoder.mjs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/mp3-encoder",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
|
||||
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
|
||||
|
||||
@@ -47,11 +47,11 @@ import { registerMediabunnyServer } from '@mediabunny/server';
|
||||
import * as NodeAv from 'node-av';
|
||||
|
||||
registerMediabunnyServer({
|
||||
// Use a specific hardware rendering device:
|
||||
hardwareContext: NodeAv.HardwareContext.create(
|
||||
NodeAv.AV_HWDEVICE_TYPE_VAAPI,
|
||||
'/dev/dri/renderD128',
|
||||
),
|
||||
// Use a specific hardware rendering device:
|
||||
hardwareContext: NodeAv.HardwareContext.create(
|
||||
NodeAv.AV_HWDEVICE_TYPE_VAAPI,
|
||||
'/dev/dri/renderD128',
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mediabunny/server",
|
||||
"author": "Vanilagy",
|
||||
"version": "1.46.0",
|
||||
"version": "1.48.0",
|
||||
"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",
|
||||
"module": "./dist/bundles/mediabunny-server.mjs",
|
||||
@@ -34,7 +34,7 @@
|
||||
"url": "https://github.com/sponsors/Vanilagy"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-av": "^5.2.4"
|
||||
"node-av": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mediabunny": "^1.45.0"
|
||||
|
||||
@@ -215,6 +215,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
+108
-215
@@ -14,6 +14,7 @@ import {
|
||||
VideoCodec,
|
||||
} from './codec';
|
||||
import {
|
||||
AudioEncodingConfig,
|
||||
getEncodableAudioCodecs,
|
||||
getFirstEncodableVideoCodec,
|
||||
Quality,
|
||||
@@ -50,17 +51,14 @@ import { Output, OutputTrackGroup, TrackType } from './output';
|
||||
import { Mp4OutputFormat } from './output-format';
|
||||
import {
|
||||
AudioSample,
|
||||
audioSampleToInterleavedFormat,
|
||||
clampCropRectangle,
|
||||
CropRectangle,
|
||||
toInterleavedAudioFormat,
|
||||
validateCropRectangle,
|
||||
VideoSample,
|
||||
VideoSampleResource,
|
||||
} from './sample';
|
||||
import { MetadataTags, validateMetadataTags } from './metadata';
|
||||
import { NullTarget } from './target';
|
||||
import { AudioResampler } from './resample';
|
||||
|
||||
/**
|
||||
* The options for media file conversion.
|
||||
@@ -1342,6 +1340,10 @@ export class Conversion {
|
||||
encodingConfig.transform.frameRate = trackOptions.frameRate;
|
||||
}
|
||||
|
||||
if (trackOptions.process) {
|
||||
encodingConfig.transform.process = trackOptions.process;
|
||||
}
|
||||
|
||||
if (needsRerender) {
|
||||
outputTrackRotation = 0; // Since the rotation is baked into the output
|
||||
|
||||
@@ -1353,6 +1355,12 @@ export class Conversion {
|
||||
encodingConfig.transform.alpha = alpha;
|
||||
}
|
||||
|
||||
// We need to do this because `process` can emit new timestamps
|
||||
let lastSampleTimestamp: number | null = null;
|
||||
encodingConfig.onEncodedSample = (sample) => {
|
||||
lastSampleTimestamp = sample.timestamp;
|
||||
};
|
||||
|
||||
const source = new VideoSampleSource(encodingConfig);
|
||||
videoSource = source;
|
||||
|
||||
@@ -1370,7 +1378,14 @@ export class Conversion {
|
||||
const adjustedSampleTimestamp = Math.max(sample.timestamp - this._startTimestamp, 0);
|
||||
sample.setTimestamp(adjustedSampleTimestamp);
|
||||
|
||||
await this._registerVideoSample(trackOptions, outputTrackId, source, sample);
|
||||
this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
|
||||
await source.add(sample);
|
||||
|
||||
if (lastSampleTimestamp !== null) {
|
||||
if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
|
||||
await this._synchronizer.wait(lastSampleTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
sample.close();
|
||||
}
|
||||
@@ -1403,69 +1418,6 @@ export class Conversion {
|
||||
this._outputOwnTrackGroups.push(ownGroup);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async _registerVideoSample(
|
||||
trackOptions: ConversionVideoOptions,
|
||||
outputTrackId: number,
|
||||
source: VideoSampleSource,
|
||||
sample: VideoSample,
|
||||
) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
|
||||
|
||||
let finalSamples: VideoSample[];
|
||||
if (!trackOptions.process) {
|
||||
finalSamples = [sample];
|
||||
} else {
|
||||
let processed = trackOptions.process(sample);
|
||||
if (processed instanceof Promise) processed = await processed;
|
||||
|
||||
if (!Array.isArray(processed)) {
|
||||
processed = processed === null ? [] : [processed];
|
||||
}
|
||||
|
||||
finalSamples = processed.map((x) => {
|
||||
if (x instanceof VideoSample) {
|
||||
return x;
|
||||
}
|
||||
|
||||
if (typeof VideoFrame !== 'undefined' && x instanceof VideoFrame) {
|
||||
return new VideoSample(x);
|
||||
}
|
||||
|
||||
// Calling the VideoSample constructor here will automatically handle input validation for us
|
||||
// (it throws for any non-legal argument).
|
||||
return new VideoSample(x as CanvasImageSource, {
|
||||
timestamp: sample.timestamp,
|
||||
duration: sample.duration,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
for (const finalSample of finalSamples) {
|
||||
if (this._canceled) {
|
||||
break;
|
||||
}
|
||||
|
||||
await source.add(finalSample);
|
||||
|
||||
if (this._synchronizer.shouldWait(outputTrackId, finalSample.timestamp)) {
|
||||
await this._synchronizer.wait(finalSample.timestamp);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
for (const finalSample of finalSamples) {
|
||||
if (finalSample !== sample) {
|
||||
finalSample.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
async _processAudioTrack(track: InputAudioTrack, trackOptions: ConversionAudioOptions, outputTrackId: number) {
|
||||
const sourceCodec = await track.getCodec();
|
||||
@@ -1487,16 +1439,18 @@ export class Conversion {
|
||||
|
||||
let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
|
||||
let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
|
||||
let needsResample = numberOfChannels !== originalNumberOfChannels
|
||||
|| sampleRate !== originalSampleRate
|
||||
|| firstTimestamp < this._startTimestamp
|
||||
|| (firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData);
|
||||
|
||||
const needsTrimming = firstTimestamp < this._startTimestamp;
|
||||
const needsPadding = firstTimestamp > this._startTimestamp && !this.output.format.supportsTimestampedMediaData;
|
||||
|
||||
let audioCodecs = this.output.format.getSupportedAudioCodecs();
|
||||
if (
|
||||
!trackOptions.forceTranscode
|
||||
&& !trackOptions.bitrate
|
||||
&& !needsResample
|
||||
&& numberOfChannels === originalNumberOfChannels
|
||||
&& sampleRate === originalSampleRate
|
||||
&& !needsTrimming
|
||||
&& !needsPadding
|
||||
&& audioCodecs.includes(sourceCodec)
|
||||
&& (!trackOptions.codec || trackOptions.codec === sourceCodec)
|
||||
&& !trackOptions.process
|
||||
@@ -1589,7 +1543,6 @@ export class Conversion {
|
||||
.find(codec => (NON_PCM_AUDIO_CODECS as readonly string[]).includes(codec));
|
||||
if (nonPcmCodec) {
|
||||
// We are able to encode using a non-PCM codec, but it'll require resampling
|
||||
needsResample = true;
|
||||
codecOfChoice = nonPcmCodec;
|
||||
numberOfChannels = FALLBACK_NUMBER_OF_CHANNELS;
|
||||
sampleRate = FALLBACK_SAMPLE_RATE;
|
||||
@@ -1607,44 +1560,86 @@ export class Conversion {
|
||||
return;
|
||||
}
|
||||
|
||||
if (needsResample) {
|
||||
audioSource = this._resampleAudio(
|
||||
track,
|
||||
trackOptions,
|
||||
outputTrackId,
|
||||
codecOfChoice,
|
||||
numberOfChannels,
|
||||
sampleRate,
|
||||
bitrate,
|
||||
);
|
||||
} else {
|
||||
const source = new AudioSampleSource({
|
||||
codec: codecOfChoice,
|
||||
bitrate,
|
||||
});
|
||||
audioSource = source;
|
||||
const encodingConfig: AudioEncodingConfig = {
|
||||
codec: codecOfChoice,
|
||||
bitrate,
|
||||
transform: {
|
||||
sampleFormat: trackOptions.sampleFormat,
|
||||
process: trackOptions.process,
|
||||
},
|
||||
};
|
||||
assert(encodingConfig.transform);
|
||||
|
||||
this._trackPromises.push((async () => {
|
||||
await this._started;
|
||||
if (numberOfChannels !== originalNumberOfChannels) {
|
||||
encodingConfig.transform.numberOfChannels = numberOfChannels;
|
||||
}
|
||||
if (sampleRate !== originalSampleRate) {
|
||||
encodingConfig.transform.sampleRate = sampleRate;
|
||||
}
|
||||
|
||||
const sink = new AudioSampleSink(track);
|
||||
for await (const sample of sink.samples(undefined, this._endTimestamp)) {
|
||||
if (this._canceled) {
|
||||
sample.close();
|
||||
return;
|
||||
}
|
||||
let lastSampleTimestamp: number | null = null;
|
||||
encodingConfig.onEncodedSample = (sample) => {
|
||||
lastSampleTimestamp = sample.timestamp;
|
||||
};
|
||||
|
||||
// Offset the timestamp as needed
|
||||
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
||||
const source = new AudioSampleSource(encodingConfig);
|
||||
audioSource = source;
|
||||
|
||||
await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
|
||||
this._trackPromises.push((async () => {
|
||||
await this._started;
|
||||
|
||||
if (needsPadding) {
|
||||
const paddingLength = firstTimestamp - this._startTimestamp;
|
||||
const paddingLengthSamples = Math.round(paddingLength * originalSampleRate);
|
||||
|
||||
const silentSample = new AudioSample({
|
||||
data: new Float32Array(paddingLengthSamples * originalNumberOfChannels),
|
||||
format: 'f32-planar',
|
||||
numberOfChannels: originalNumberOfChannels,
|
||||
sampleRate: originalSampleRate,
|
||||
timestamp: 0,
|
||||
});
|
||||
await this._registerAudioSample(silentSample, source, outputTrackId, () => lastSampleTimestamp);
|
||||
}
|
||||
|
||||
const sink = new AudioSampleSink(track);
|
||||
for await (let sample of sink.samples(this._startTimestamp, this._endTimestamp)) {
|
||||
if (this._canceled) {
|
||||
sample.close();
|
||||
return;
|
||||
}
|
||||
|
||||
source.close();
|
||||
this._synchronizer.closeTrack(outputTrackId);
|
||||
})());
|
||||
}
|
||||
let startFrame = 0;
|
||||
let endFrame = sample.numberOfFrames;
|
||||
|
||||
if (sample.timestamp < this._startTimestamp) {
|
||||
startFrame = Math.round((this._startTimestamp - sample.timestamp) * sample.sampleRate);
|
||||
}
|
||||
if (sample.timestamp + sample.duration > this._endTimestamp) {
|
||||
endFrame = Math.round((this._endTimestamp - sample.timestamp) * sample.sampleRate);
|
||||
}
|
||||
|
||||
if (startFrame > 0 || endFrame < sample.numberOfFrames) {
|
||||
// Trim the sample if it sticks out of the trim region on either end
|
||||
const trimmedSample = sample.trim(startFrame, endFrame);
|
||||
sample.close();
|
||||
sample = trimmedSample;
|
||||
|
||||
if (sample.numberOfFrames === 0) {
|
||||
sample.close();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Offset the timestamp as needed
|
||||
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
||||
|
||||
await this._registerAudioSample(sample, source, outputTrackId, () => lastSampleTimestamp);
|
||||
}
|
||||
|
||||
source.close();
|
||||
this._synchronizer.closeTrack(outputTrackId);
|
||||
})());
|
||||
}
|
||||
|
||||
let ownGroup: OutputTrackGroup | null = null;
|
||||
@@ -1670,126 +1665,24 @@ export class Conversion {
|
||||
|
||||
/** @internal */
|
||||
async _registerAudioSample(
|
||||
trackOptions: ConversionAudioOptions,
|
||||
outputTrackId: number,
|
||||
sample: AudioSample,
|
||||
source: AudioSampleSource,
|
||||
inputSample: AudioSample,
|
||||
outputTrackId: number,
|
||||
getLastSampleTimestamp: () => number | null,
|
||||
) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let sample = inputSample;
|
||||
|
||||
if (
|
||||
trackOptions.sampleFormat !== undefined
|
||||
&& toInterleavedAudioFormat(sample.format) !== trackOptions.sampleFormat
|
||||
) {
|
||||
// Do a sample format conversion
|
||||
sample = audioSampleToInterleavedFormat(sample, trackOptions.sampleFormat);
|
||||
}
|
||||
|
||||
this._reportProgress(outputTrackId, sample.timestamp + sample.duration);
|
||||
|
||||
let finalSamples: AudioSample[];
|
||||
if (!trackOptions.process) {
|
||||
finalSamples = [sample];
|
||||
} else {
|
||||
let processed = trackOptions.process(sample);
|
||||
if (processed instanceof Promise) processed = await processed;
|
||||
await source.add(sample);
|
||||
sample.close();
|
||||
|
||||
if (!Array.isArray(processed)) {
|
||||
processed = processed === null ? [] : [processed];
|
||||
}
|
||||
|
||||
if (!processed.every(x => x instanceof AudioSample)) {
|
||||
throw new TypeError(
|
||||
'The audio process function must return an AudioSample, null, or an array of AudioSamples.',
|
||||
);
|
||||
}
|
||||
|
||||
finalSamples = processed;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const finalSample of finalSamples) {
|
||||
if (this._canceled) {
|
||||
break;
|
||||
}
|
||||
|
||||
await source.add(finalSample);
|
||||
|
||||
if (this._synchronizer.shouldWait(outputTrackId, finalSample.timestamp)) {
|
||||
await this._synchronizer.wait(finalSample.timestamp);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (sample !== inputSample) {
|
||||
sample.close();
|
||||
}
|
||||
|
||||
for (const finalSample of finalSamples) {
|
||||
if (finalSample !== inputSample) {
|
||||
finalSample.close();
|
||||
}
|
||||
const lastSampleTimestamp = getLastSampleTimestamp();
|
||||
if (lastSampleTimestamp !== null) {
|
||||
if (this._synchronizer.shouldWait(outputTrackId, lastSampleTimestamp)) {
|
||||
await this._synchronizer.wait(lastSampleTimestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_resampleAudio(
|
||||
track: InputAudioTrack,
|
||||
trackOptions: ConversionAudioOptions,
|
||||
outputTrackId: number,
|
||||
codec: AudioCodec,
|
||||
targetNumberOfChannels: number,
|
||||
targetSampleRate: number,
|
||||
bitrate: number | Quality,
|
||||
) {
|
||||
const source = new AudioSampleSource({
|
||||
codec,
|
||||
bitrate,
|
||||
});
|
||||
|
||||
this._trackPromises.push((async () => {
|
||||
await this._started;
|
||||
|
||||
const resampler = new AudioResampler({
|
||||
targetNumberOfChannels,
|
||||
targetSampleRate,
|
||||
startTime: this._startTimestamp,
|
||||
endTime: this._endTimestamp,
|
||||
onSample: async (sample) => {
|
||||
assert(sample.timestamp >= this._startTimestamp);
|
||||
sample.setTimestamp(sample.timestamp - this._startTimestamp);
|
||||
|
||||
await this._registerAudioSample(trackOptions, outputTrackId, source, sample);
|
||||
sample.close();
|
||||
},
|
||||
});
|
||||
|
||||
const sink = new AudioSampleSink(track);
|
||||
const iterator = sink.samples(this._startTimestamp, this._endTimestamp);
|
||||
|
||||
for await (const sample of iterator) {
|
||||
if (this._canceled) {
|
||||
sample.close();
|
||||
return;
|
||||
}
|
||||
|
||||
await resampler.add(sample);
|
||||
sample.close();
|
||||
}
|
||||
|
||||
await resampler.finalize();
|
||||
|
||||
source.close();
|
||||
this._synchronizer.closeTrack(outputTrackId);
|
||||
})());
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_reportProgress(trackId: number, endTimestamp: number) {
|
||||
if (!this._computeProgress) {
|
||||
|
||||
@@ -74,6 +74,8 @@ export type VideoEncodingConfig = {
|
||||
* WebCodecs API, is created.
|
||||
*/
|
||||
onEncoderConfig?: (config: VideoEncoderConfig) => unknown;
|
||||
/** Called right before a sample is passed to the encoder. */
|
||||
onEncodedSample?: (sample: VideoSample) => unknown;
|
||||
} & VideoEncodingAdditionalOptions;
|
||||
|
||||
/**
|
||||
@@ -232,6 +234,9 @@ export const validateVideoEncodingConfig = (config: VideoEncodingConfig) => {
|
||||
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
|
||||
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
|
||||
}
|
||||
if (config.onEncodedSample !== undefined && typeof config.onEncodedSample !== 'function') {
|
||||
throw new TypeError('config.onEncodedSample, when provided, must be a function.');
|
||||
}
|
||||
|
||||
validateVideoEncodingAdditionalOptions(config.codec, config);
|
||||
};
|
||||
@@ -382,6 +387,8 @@ export type AudioEncodingConfig = {
|
||||
* WebCodecs API, is created.
|
||||
*/
|
||||
onEncoderConfig?: (config: AudioEncoderConfig) => unknown;
|
||||
/** Called right before a sample is passed to the encoder. */
|
||||
onEncodedSample?: (sample: AudioSample) => unknown;
|
||||
} & AudioEncodingAdditionalOptions;
|
||||
|
||||
/**
|
||||
@@ -462,6 +469,9 @@ export const validateAudioEncodingConfig = (config: AudioEncodingConfig) => {
|
||||
if (config.onEncoderConfig !== undefined && typeof config.onEncoderConfig !== 'function') {
|
||||
throw new TypeError('config.onEncoderConfig, when provided, must be a function.');
|
||||
}
|
||||
if (config.onEncodedSample !== undefined && typeof config.onEncodedSample !== 'function') {
|
||||
throw new TypeError('config.onEncodedSample, when provided, must be a function.');
|
||||
}
|
||||
|
||||
validateAudioEncodingAdditionalOptions(config.codec, config);
|
||||
};
|
||||
|
||||
@@ -595,6 +595,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -77,12 +77,15 @@ export class HlsDemuxer extends Demuxer {
|
||||
readMetadata() {
|
||||
return this.metadataPromise ??= (async () => {
|
||||
assert(this.input._rootSource instanceof PathedSource);
|
||||
const { rootPath } = this.input._rootSource;
|
||||
|
||||
const slice = await this.input._reader.requestEntireFile();
|
||||
assert(slice);
|
||||
const lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
|
||||
|
||||
// Important: get the root path AFTER reading data to get the final root path, possibly affected by
|
||||
// redirects. Any follow requests should be related to the redirected path, not the original one.
|
||||
const { rootPath } = this.input._rootSource;
|
||||
|
||||
const variantStreams: {
|
||||
fullPath: string;
|
||||
attributes: AttributeList;
|
||||
@@ -756,6 +759,10 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
|
||||
return this.delegate(() => this.internalTrack.backingTrack!.isRelativeToUnixEpoch());
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp(timestamp: number): MaybePromise<number | null> {
|
||||
return this.delegate(() => this.internalTrack.backingTrack!.getUnixTimeForTimestamp(timestamp));
|
||||
}
|
||||
|
||||
getBitrate(): number | null {
|
||||
return this.internalTrack.peakBitrate;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
base64ToBytes,
|
||||
} from '../misc';
|
||||
import { readAllLines, readBytes, Reader } from '../reader';
|
||||
import { CustomPathedSource, ReadableStreamSource, SourceRef, SourceRequest } from '../source';
|
||||
import { CustomPathedSource, PathedSource, ReadableStreamSource, SourceRef, SourceRequest } from '../source';
|
||||
import { HlsDemuxer } from './hls-demuxer';
|
||||
import {
|
||||
AttributeList,
|
||||
@@ -68,6 +68,7 @@ export type HlsSegmentLocation = {
|
||||
};
|
||||
|
||||
export class HlsSegmentedInput extends SegmentedInput {
|
||||
rootPath: string;
|
||||
demuxer: HlsDemuxer;
|
||||
segments: HlsSegment[] = [];
|
||||
nextLines: string[] | null = null;
|
||||
@@ -84,6 +85,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
) {
|
||||
super(demuxer.input, path, trackDeclarations);
|
||||
|
||||
this.rootPath = path;
|
||||
this.demuxer = demuxer;
|
||||
this.nextLines = lines;
|
||||
}
|
||||
@@ -126,16 +128,24 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
this.nextLines = null;
|
||||
|
||||
if (!lines) {
|
||||
using ref = await this.demuxer.input._getSourceUncached({ path: this.path, isRoot: false });
|
||||
using ref = await this.demuxer.input._getSourceUncached({ path: this.rootPath, isRoot: false });
|
||||
const reader = new Reader(ref.source);
|
||||
|
||||
const slice = await reader.requestEntireFile();
|
||||
assert(slice);
|
||||
lines = readAllLines(slice, slice.length, { ignore: canIgnoreLine });
|
||||
|
||||
if (ref.source instanceof PathedSource) {
|
||||
// Copy back the source's path to become aware of potential redirects
|
||||
this.rootPath = ref.source.rootPath;
|
||||
}
|
||||
}
|
||||
|
||||
const offsetTimestampsByDateTime = this.input._formatOptions.hls?.offsetTimestampsByDateTime !== false;
|
||||
|
||||
let headerRead = false;
|
||||
let accumulatedTime = 0;
|
||||
let accumulatedUnixTime: number | null = null;
|
||||
let nextSegmentDuration: number | null = null;
|
||||
let currentKey: HlsEncryptionInfo | null = null;
|
||||
let nextSequenceNumber = 0;
|
||||
@@ -182,6 +192,9 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
currentFirstSegment = prevLastSegment.firstSegment;
|
||||
currentInitSegment = prevLastSegment.initSegment;
|
||||
lastProgramDateTimeSeconds = prevLastSegment.lastProgramDateTimeSeconds;
|
||||
accumulatedUnixTime = prevLastSegment.unixEpochTimestamp !== null
|
||||
? prevLastSegment.unixEpochTimestamp + prevLastSegment.duration
|
||||
: null;
|
||||
prevLastSegment = null;
|
||||
}
|
||||
}
|
||||
@@ -219,7 +232,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
key = { ...key, iv };
|
||||
}
|
||||
|
||||
const fullPath = joinPaths(this.path, line);
|
||||
const fullPath = joinPaths(this.rootPath, line);
|
||||
const location: HlsSegmentLocation = {
|
||||
path: fullPath,
|
||||
offset: nextByteRange?.offset ?? 0,
|
||||
@@ -228,7 +241,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
const segment: HlsSegment = {
|
||||
timestamp: accumulatedTime,
|
||||
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
|
||||
unixEpochTimestamp: accumulatedUnixTime,
|
||||
firstSegment: currentFirstSegment,
|
||||
sequenceNumber: nextSequenceNumber,
|
||||
location,
|
||||
@@ -240,6 +253,9 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
currentFirstSegment ??= segment;
|
||||
accumulatedTime += nextSegmentDuration;
|
||||
if (accumulatedUnixTime !== null) {
|
||||
accumulatedUnixTime += nextSegmentDuration;
|
||||
}
|
||||
|
||||
this.segments.push(segment);
|
||||
} else {
|
||||
@@ -299,7 +315,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
}
|
||||
|
||||
if (!prevLastSegment) {
|
||||
const fullPath = joinPaths(this.path, uri);
|
||||
const fullPath = joinPaths(this.rootPath, uri);
|
||||
const location: HlsSegmentLocation = {
|
||||
path: fullPath,
|
||||
offset: parsedByteRange?.offset ?? 0,
|
||||
@@ -313,7 +329,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
const segment: HlsSegment = {
|
||||
timestamp: accumulatedTime,
|
||||
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
|
||||
unixEpochTimestamp: accumulatedUnixTime,
|
||||
firstSegment: null,
|
||||
sequenceNumber: null,
|
||||
location,
|
||||
@@ -375,7 +391,7 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
|
||||
currentKey = {
|
||||
method: 'AES-128',
|
||||
keyUri: joinPaths(this.path, uri),
|
||||
keyUri: joinPaths(this.rootPath, uri),
|
||||
iv,
|
||||
keyFormat,
|
||||
};
|
||||
@@ -471,15 +487,19 @@ export class HlsSegmentedInput extends SegmentedInput {
|
||||
const offset = dateTimeSeconds - lastSegmentEnd;
|
||||
|
||||
for (const segment of this.segments) {
|
||||
segment.timestamp += offset;
|
||||
segment.relativeToUnixEpoch = true;
|
||||
segment.unixEpochTimestamp = segment.timestamp + offset;
|
||||
if (offsetTimestampsByDateTime) {
|
||||
segment.timestamp = segment.unixEpochTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
accumulatedTime += offset;
|
||||
}
|
||||
|
||||
lastProgramDateTimeSeconds = dateTimeSeconds;
|
||||
accumulatedTime = dateTimeSeconds; // Snap the accumulated time to the datetime
|
||||
accumulatedUnixTime = dateTimeSeconds;
|
||||
|
||||
if (offsetTimestampsByDateTime) {
|
||||
accumulatedTime = dateTimeSeconds; // Snap the accumulated time into Unix space
|
||||
}
|
||||
} else if (line === TAG_DISCONTINUITY) {
|
||||
currentFirstSegment = null;
|
||||
// Note: the init segment is not reset; the #EXT-X-MAP statement simply lasts until the next
|
||||
|
||||
@@ -197,6 +197,7 @@ export {
|
||||
IsobmffInputFormat,
|
||||
type IsobmffInputFormatOptions,
|
||||
HlsInputFormat,
|
||||
type HlsInputFormatOptions,
|
||||
MatroskaInputFormat,
|
||||
Mp3InputFormat,
|
||||
Mp4InputFormat,
|
||||
@@ -270,6 +271,7 @@ export {
|
||||
EncodedPacketSink,
|
||||
type PacketRetrievalOptions,
|
||||
VideoSampleSink,
|
||||
type VideoSinkDecoderOptions,
|
||||
type WrappedAudioBuffer,
|
||||
type WrappedCanvas,
|
||||
} from './media-sink';
|
||||
|
||||
@@ -727,6 +727,8 @@ export const HLS_FORMATS: InputFormat[] = [HLS, MP4, QTFF, MP3, ADTS, MPEG_TS];
|
||||
export type InputFormatOptions = {
|
||||
/** ISOBMFF-specific configuration. */
|
||||
isobmff?: IsobmffInputFormatOptions;
|
||||
/** HLS-specific configuration. */
|
||||
hls?: HlsInputFormatOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -755,6 +757,26 @@ export type IsobmffInputFormatOptions = {
|
||||
_suppressPsshParsing?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Additional HLS input configuration.
|
||||
* @group Input formats
|
||||
* @public
|
||||
*/
|
||||
export type HlsInputFormatOptions = {
|
||||
/**
|
||||
* Whether, in the presence of `#EXT-X-PROGRAM-DATE-TIME` tags, to offset track and packet timestamps to be relative
|
||||
* to the Unix epoch.
|
||||
*
|
||||
* Defaults to `true`, meaning packet timestamps map directly to wall-clock time. This guarantees AV sync across
|
||||
* multiple tracks, even with gaps present.
|
||||
*
|
||||
* When you don't want this mapping, you can set this value to `false`. In addition to timestamps not being Unix
|
||||
* timestamps anymore, any gaps in the playlist are also naturally removed. When `false`, you can still access the
|
||||
* wall-clock Unix timestamps via {@link InputTrack.getUnixTimeForTimestamp}.
|
||||
*/
|
||||
offsetTimestampsByDateTime?: boolean;
|
||||
};
|
||||
|
||||
export const validateInputFormatOptions = (options: InputFormatOptions, prefix: string) => {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError(`${prefix}, when provided, must be an object.`);
|
||||
@@ -767,4 +789,15 @@ export const validateInputFormatOptions = (options: InputFormatOptions, prefix:
|
||||
throw new TypeError(`${prefix}.isobmff.resolveKeyId, when provided, must be a function.`);
|
||||
}
|
||||
}
|
||||
if (options.hls !== undefined) {
|
||||
if (!options.hls || typeof options.hls !== 'object') {
|
||||
throw new TypeError(`${prefix}.hls, when provided, must be an object.`);
|
||||
}
|
||||
if (
|
||||
options.hls.offsetTimestampsByDateTime !== undefined
|
||||
&& typeof options.hls.offsetTimestampsByDateTime !== 'boolean'
|
||||
) {
|
||||
throw new TypeError(`${prefix}.hls.offsetTimestampsByDateTime, when provided, must be a boolean.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface InputTrackBacking {
|
||||
getLanguageCode(): MaybePromise<string>;
|
||||
getTimeResolution(): MaybePromise<number>;
|
||||
isRelativeToUnixEpoch(): MaybePromise<boolean>;
|
||||
getUnixTimeForTimestamp(timestamp: number): MaybePromise<number | null>;
|
||||
getDisposition(): MaybePromise<TrackDisposition>;
|
||||
getPairingMask(): bigint;
|
||||
getBitrate(): MaybePromise<number | null>;
|
||||
@@ -203,6 +204,28 @@ export abstract class InputTrack {
|
||||
return this._backing.isRelativeToUnixEpoch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Unix time (in seconds since January 1, 1970 00:00:00 UTC) that the given track timestamp (in seconds)
|
||||
* maps to, or `null` if there is no such mapping. This provides a piecewise-continuous mapping from this track's
|
||||
* timestamp space into wall-clock time. Such mapping exists, for example, for HLS playlists with
|
||||
* `#EXT-X-PROGRAM-DATE-TIME` tags present.
|
||||
*
|
||||
* This mapping can be available even when {@link InputTrack.isRelativeToUnixEpoch} is `false`, for example for HLS
|
||||
* streams with program date time information but with {@link HlsInputFormatOptions.offsetTimestampsByDateTime}
|
||||
* set to `false`.
|
||||
*/
|
||||
async getUnixTimeForTimestamp(timestamp: number): Promise<number | null> {
|
||||
return this._backing.getUnixTimeForTimestamp(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the track's timestamps can be mapped to Unix wall clock time via
|
||||
* {@link InputTrack.getUnixTimeForTimestamp}.
|
||||
*/
|
||||
async hasUnixTimeMapping(): Promise<boolean> {
|
||||
return (await this._backing.getUnixTimeForTimestamp(await this.getFirstTimestamp())) !== null;
|
||||
}
|
||||
|
||||
/** Returns the track's disposition, i.e. information about its intended usage. */
|
||||
async getDisposition() {
|
||||
return this._backing.getDisposition();
|
||||
|
||||
+6
-2
@@ -527,8 +527,12 @@ export class Input<S extends Source = Source> extends EventEmitter<InputEvents>
|
||||
}
|
||||
this._sourceRefs.length = 0;
|
||||
|
||||
void this._demuxerPromise
|
||||
?.then(demuxer => demuxer.dispose());
|
||||
if (this._demuxerPromise) {
|
||||
// The demuxer promise may already be rejected after failed format detection.
|
||||
void this._demuxerPromise
|
||||
.then(demuxer => demuxer.dispose())
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2069,8 +2069,14 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
// referenced in the track fragment header.
|
||||
if (this.currentTrack) {
|
||||
const trackData = this.currentFragment.trackData.get(this.currentTrack.id);
|
||||
cond:
|
||||
if (trackData) {
|
||||
this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
|
||||
if (trackData.samples.length === 0) {
|
||||
// Don't associate the fragment with the track if it has no samples, this simplifies
|
||||
// other code
|
||||
this.currentFragment.trackData.delete(this.currentTrack.id);
|
||||
break cond;
|
||||
}
|
||||
|
||||
trackData.presentationTimestamps = trackData.samples
|
||||
.map((x, i) => ({ presentationTimestamp: x.presentationTimestamp, sampleIndex: i }))
|
||||
@@ -2260,12 +2266,6 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
this.currentFragment.trackData.set(track.id, trackData);
|
||||
}
|
||||
|
||||
if (sampleCount === 0) {
|
||||
// Don't associate the fragment with the track if it has no samples, this simplifies other code
|
||||
this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
let sampleDuration: number;
|
||||
if (sampleDurationPresent) {
|
||||
@@ -2317,6 +2317,8 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
trackData.currentOffset += sampleSize;
|
||||
trackData.currentTimestamp += sampleDuration;
|
||||
}
|
||||
|
||||
this.currentFragment.implicitBaseDataOffset = trackData.currentOffset;
|
||||
}; break;
|
||||
|
||||
case 'saiz': {
|
||||
@@ -2809,6 +2811,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return this.internalTrack.disposition;
|
||||
}
|
||||
|
||||
@@ -1953,6 +1953,10 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDisposition() {
|
||||
return this.internalTrack.disposition;
|
||||
}
|
||||
|
||||
+54
-3
@@ -1749,6 +1749,42 @@ const colorAlphaMergerWorkerCode = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes additional decoder preferences for video sinks.
|
||||
* @group Media sinks
|
||||
* @public
|
||||
*/
|
||||
export type VideoSinkDecoderOptions = {
|
||||
/**
|
||||
* A hint that configures the hardware acceleration method of the decoder. This is best left on `'no-preference'`,
|
||||
* the default.
|
||||
*/
|
||||
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
||||
/**
|
||||
* Hint that the selected decoder should be configured to minimize the number of packets that have to be decoded
|
||||
* before video frames are output.
|
||||
*/
|
||||
optimizeForLatency?: boolean;
|
||||
};
|
||||
|
||||
const validateVideoSinkDecoderOptions = (decoderOptions: VideoSinkDecoderOptions) => {
|
||||
if (!decoderOptions || typeof decoderOptions !== 'object') {
|
||||
throw new TypeError('decoderOptions must be an object.');
|
||||
}
|
||||
if (
|
||||
decoderOptions.hardwareAcceleration !== undefined
|
||||
&& !['no-preference', 'prefer-hardware', 'prefer-software'].includes(decoderOptions.hardwareAcceleration)
|
||||
) {
|
||||
throw new TypeError(
|
||||
'decoderOptions.hardwareAcceleration, when provided, must be \'no-preference\', \'prefer-hardware\' or'
|
||||
+ ' \'prefer-software\'.',
|
||||
);
|
||||
}
|
||||
if (decoderOptions.optimizeForLatency !== undefined && typeof decoderOptions.optimizeForLatency !== 'boolean') {
|
||||
throw new TypeError('decoderOptions.optimizeForLatency, when provided, must be a boolean.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A sink that retrieves decoded video samples (video frames) from a video track.
|
||||
* @group Media sinks
|
||||
@@ -1757,16 +1793,20 @@ const colorAlphaMergerWorkerCode = () => {
|
||||
export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
|
||||
/** @internal */
|
||||
_track: InputVideoTrack;
|
||||
/** @internal */
|
||||
_decoderOptions: VideoSinkDecoderOptions;
|
||||
|
||||
/** Creates a new {@link VideoSampleSink} for the given {@link InputVideoTrack}. */
|
||||
constructor(videoTrack: InputVideoTrack) {
|
||||
constructor(videoTrack: InputVideoTrack, decoderOptions: VideoSinkDecoderOptions = {}) {
|
||||
if (!(videoTrack instanceof InputVideoTrack)) {
|
||||
throw new TypeError('videoTrack must be an InputVideoTrack.');
|
||||
}
|
||||
validateVideoSinkDecoderOptions(decoderOptions);
|
||||
|
||||
super();
|
||||
|
||||
this._track = videoTrack;
|
||||
this._decoderOptions = decoderOptions;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -1783,10 +1823,16 @@ export class VideoSampleSink extends BaseMediaSampleSink<VideoSample> {
|
||||
|
||||
const codec = await this._track.getCodec();
|
||||
const rotation = await this._track.getRotation();
|
||||
const decoderConfig = await this._track.getDecoderConfig();
|
||||
let decoderConfig = await this._track.getDecoderConfig();
|
||||
const timeResolution = await this._track.getTimeResolution();
|
||||
assert(codec && decoderConfig);
|
||||
|
||||
decoderConfig = {
|
||||
...decoderConfig,
|
||||
hardwareAcceleration: this._decoderOptions.hardwareAcceleration,
|
||||
optimizeForLatency: this._decoderOptions.optimizeForLatency,
|
||||
};
|
||||
|
||||
return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution);
|
||||
}
|
||||
|
||||
@@ -1903,6 +1949,8 @@ export type CanvasSinkOptions = {
|
||||
* canvas is created each time.
|
||||
*/
|
||||
poolSize?: number;
|
||||
/** Additional preferences for the underlying video decoder. */
|
||||
decoderOptions?: VideoSinkDecoderOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1982,12 +2030,15 @@ export class CanvasSink {
|
||||
) {
|
||||
throw new TypeError('poolSize must be a non-negative integer.');
|
||||
}
|
||||
if (options.decoderOptions !== undefined) {
|
||||
validateVideoSinkDecoderOptions(options.decoderOptions);
|
||||
}
|
||||
|
||||
this._videoTrack = videoTrack;
|
||||
this._alpha = options.alpha ?? false;
|
||||
this._options = options;
|
||||
this._fit = options.fit ?? 'fill';
|
||||
this._videoSampleSink = new VideoSampleSink(videoTrack);
|
||||
this._videoSampleSink = new VideoSampleSink(videoTrack, options.decoderOptions);
|
||||
this._canvasPool = Array.from({ length: options.poolSize ?? 0 }, () => null);
|
||||
}
|
||||
|
||||
|
||||
+20
-2
@@ -265,6 +265,7 @@ class VideoEncoderWrapper {
|
||||
* So, we keep track of the encoder error and throw it as soon as we get the chance.
|
||||
*/
|
||||
private error: Error | null = null;
|
||||
private closed = false;
|
||||
|
||||
private lastMuxerPromise: Promise<void> = Promise.resolve();
|
||||
|
||||
@@ -429,6 +430,8 @@ class VideoEncoderWrapper {
|
||||
return new VideoSample(x);
|
||||
}
|
||||
|
||||
// Calling the VideoSample constructor here will automatically handle input validation for us
|
||||
// (it throws for any non-legal argument).
|
||||
return new VideoSample(x as CanvasImageSource, {
|
||||
timestamp: videoSample.timestamp,
|
||||
duration: videoSample.duration,
|
||||
@@ -457,6 +460,10 @@ class VideoEncoderWrapper {
|
||||
}
|
||||
assert(this.encoderInitialized);
|
||||
|
||||
if (this.closed) {
|
||||
break;
|
||||
}
|
||||
|
||||
const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 2;
|
||||
const multipleOfKeyFrameInterval = Math.floor(sampleToEncode.timestamp / keyFrameInterval);
|
||||
|
||||
@@ -474,6 +481,8 @@ class VideoEncoderWrapper {
|
||||
};
|
||||
this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval;
|
||||
|
||||
this.encodingConfig.onEncodedSample?.(sampleToEncode);
|
||||
|
||||
if (this.customEncoder) {
|
||||
this.customEncoderQueueSize++;
|
||||
|
||||
@@ -843,6 +852,8 @@ class VideoEncoderWrapper {
|
||||
await this.padFrameRate(alignedEnd);
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
|
||||
this.frameRateLastSample?.close();
|
||||
this.frameRateLastSample = null;
|
||||
|
||||
@@ -2033,6 +2044,7 @@ class AudioEncoderWrapper {
|
||||
*/
|
||||
private error: Error | null = null;
|
||||
private lastMuxerPromise: Promise<void> = Promise.resolve();
|
||||
private closed = false;
|
||||
|
||||
constructor(private source: AudioSource, private encodingConfig: AudioEncodingConfig) {}
|
||||
|
||||
@@ -2070,8 +2082,6 @@ class AudioEncoderWrapper {
|
||||
?? audioSample.numberOfChannels,
|
||||
targetSampleRate: config.transform!.sampleRate
|
||||
?? audioSample.sampleRate,
|
||||
startTime: audioSample.timestamp,
|
||||
endTime: Infinity,
|
||||
onSample: async (sample) => {
|
||||
await this.processAndEncode(sample, true);
|
||||
},
|
||||
@@ -2162,6 +2172,10 @@ class AudioEncoderWrapper {
|
||||
}
|
||||
assert(this.encoderInitialized);
|
||||
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle padding of gaps with silence to avoid audio drift over time, like in
|
||||
// https://github.com/Vanilagy/mediabunny/issues/176
|
||||
// TODO An open question is how encoders deal with the first AudioData having a non-zero timestamp, and with
|
||||
@@ -2197,6 +2211,8 @@ class AudioEncoderWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
this.encodingConfig.onEncodedSample?.(audioSample);
|
||||
|
||||
if (this.customEncoder) {
|
||||
this.customEncoderQueueSize++;
|
||||
|
||||
@@ -2536,6 +2552,8 @@ class AudioEncoderWrapper {
|
||||
}
|
||||
this.resampler = null;
|
||||
|
||||
this.closed = true;
|
||||
|
||||
if (this.customEncoder) {
|
||||
if (!forceClose) {
|
||||
void this.customEncoderCallSerializer.call(() => this.customEncoder!.flush());
|
||||
|
||||
@@ -264,6 +264,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -1103,6 +1103,10 @@ abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -454,6 +454,10 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
+10
-15
@@ -20,41 +20,32 @@ export class AudioResampler {
|
||||
targetSampleRate: number;
|
||||
sourceNumberOfChannels: number | null = null;
|
||||
targetNumberOfChannels: number;
|
||||
endTime: number;
|
||||
startTime: number | null = null;
|
||||
onSample: (sample: AudioSample) => Promise<void>;
|
||||
|
||||
bufferSizeInFrames: number;
|
||||
bufferSizeInSamples: number;
|
||||
outputBuffer: Float32Array;
|
||||
/** Start frame of current buffer */
|
||||
bufferStartFrame: number;
|
||||
bufferStartFrame = 0;
|
||||
/** The highest index written to in the current buffer */
|
||||
maxWrittenFrame: number | null = null;
|
||||
channelMixer!: (sourceData: Float32Array, sourceFrameIndex: number, targetChannelIndex: number) => number;
|
||||
tempSourceBuffer!: Float32Array;
|
||||
timestampOffset: number;
|
||||
|
||||
constructor(options: {
|
||||
targetSampleRate: number;
|
||||
targetNumberOfChannels: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
onSample: (sample: AudioSample) => Promise<void>;
|
||||
}) {
|
||||
this.targetSampleRate = options.targetSampleRate;
|
||||
this.targetNumberOfChannels = options.targetNumberOfChannels;
|
||||
this.endTime = options.endTime;
|
||||
this.onSample = options.onSample;
|
||||
|
||||
this.bufferSizeInFrames = Math.floor(this.targetSampleRate * 5.0); // 5 seconds
|
||||
this.bufferSizeInSamples = this.bufferSizeInFrames * this.targetNumberOfChannels;
|
||||
|
||||
this.outputBuffer = new Float32Array(this.bufferSizeInSamples);
|
||||
|
||||
this.bufferStartFrame = Math.floor(options.startTime * this.targetSampleRate);
|
||||
// Set to ensure that if the buffer start frame lands on a fractional sample, that the first timestamp still
|
||||
// comes out as exactly startTime
|
||||
this.timestampOffset = options.startTime - this.bufferStartFrame / this.targetSampleRate;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,6 +177,7 @@ export class AudioResampler {
|
||||
// they see fit.
|
||||
this.sourceSampleRate = audioSample.sampleRate;
|
||||
this.sourceNumberOfChannels = audioSample.numberOfChannels;
|
||||
this.startTime = audioSample.timestamp;
|
||||
|
||||
// Pre-allocate temporary buffer for source data
|
||||
this.tempSourceBuffer = new Float32Array(this.sourceSampleRate * this.sourceNumberOfChannels);
|
||||
@@ -193,6 +185,8 @@ export class AudioResampler {
|
||||
this.doChannelMixerSetup();
|
||||
}
|
||||
|
||||
assert(this.startTime !== null);
|
||||
|
||||
const requiredSamples = audioSample.numberOfFrames * audioSample.numberOfChannels;
|
||||
this.ensureTempBufferSize(requiredSamples);
|
||||
|
||||
@@ -201,8 +195,8 @@ export class AudioResampler {
|
||||
const sourceView = new Float32Array(this.tempSourceBuffer.buffer, 0, sourceDataSize / 4);
|
||||
audioSample.copyTo(sourceView, { planeIndex: 0, format: 'f32' });
|
||||
|
||||
const inputStartTime = audioSample.timestamp;
|
||||
const inputEndTime = Math.min(audioSample.timestamp + audioSample.duration, this.endTime);
|
||||
const inputStartTime = audioSample.timestamp - this.startTime;
|
||||
const inputEndTime = inputStartTime + audioSample.duration;
|
||||
|
||||
// Compute which output frames are affected by this sample
|
||||
const outputStartFrame = Math.floor(inputStartTime * this.targetSampleRate);
|
||||
@@ -266,17 +260,18 @@ export class AudioResampler {
|
||||
return; // Nothing to finalize
|
||||
}
|
||||
|
||||
assert(this.startTime !== null);
|
||||
|
||||
const samplesWritten = (this.maxWrittenFrame + 1) * this.targetNumberOfChannels;
|
||||
|
||||
const outputData = new Float32Array(samplesWritten);
|
||||
outputData.set(this.outputBuffer.subarray(0, samplesWritten));
|
||||
|
||||
const timestampSeconds = this.bufferStartFrame / this.targetSampleRate;
|
||||
const audioSample = new AudioSample({
|
||||
format: 'f32',
|
||||
sampleRate: this.targetSampleRate,
|
||||
numberOfChannels: this.targetNumberOfChannels,
|
||||
timestamp: timestampSeconds + this.timestampOffset,
|
||||
timestamp: this.startTime + this.bufferStartFrame / this.targetSampleRate,
|
||||
data: outputData,
|
||||
});
|
||||
|
||||
|
||||
@@ -2712,6 +2712,74 @@ export class AudioSample implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link AudioSample} containing only the frames in the range [startSample, endSample). Both bounds
|
||||
* must lie within this sample's range of frames. The returned sample's timestamp is shifted to match the start of
|
||||
* the trimmed section.
|
||||
*/
|
||||
trim(startSample: number, endSample = this.numberOfFrames) {
|
||||
if (!Number.isInteger(startSample) || startSample < 0) {
|
||||
throw new TypeError('startSample must be a non-negative integer.');
|
||||
}
|
||||
if (!Number.isInteger(endSample) || endSample < 0) {
|
||||
throw new TypeError('endSample must be a non-negative integer.');
|
||||
}
|
||||
if (startSample > this.numberOfFrames) {
|
||||
throw new RangeError('startSample out of range.');
|
||||
}
|
||||
if (endSample > this.numberOfFrames) {
|
||||
throw new RangeError('endSample out of range.');
|
||||
}
|
||||
if (endSample < startSample) {
|
||||
throw new RangeError('endSample must not be less than startSample.');
|
||||
}
|
||||
|
||||
if (this._closed) {
|
||||
throw new Error('AudioSample is closed.');
|
||||
}
|
||||
|
||||
const frameCount = endSample - startSample;
|
||||
const bytesPerSample = getBytesPerSample(this.format);
|
||||
|
||||
let data: Uint8Array;
|
||||
if (formatIsPlanar(this.format)) {
|
||||
const planeSize = frameCount * bytesPerSample;
|
||||
data = new Uint8Array(planeSize * this.numberOfChannels);
|
||||
|
||||
if (frameCount > 0) {
|
||||
// Copy plane-by-plane
|
||||
for (let i = 0; i < this.numberOfChannels; i++) {
|
||||
this.copyTo(data.subarray(i * planeSize, (i + 1) * planeSize), {
|
||||
planeIndex: i,
|
||||
format: this.format,
|
||||
frameOffset: startSample,
|
||||
frameCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Trivial
|
||||
data = new Uint8Array(frameCount * this.numberOfChannels * bytesPerSample);
|
||||
|
||||
if (frameCount > 0) {
|
||||
this.copyTo(data, {
|
||||
planeIndex: 0,
|
||||
format: this.format,
|
||||
frameOffset: startSample,
|
||||
frameCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new AudioSample({
|
||||
data,
|
||||
format: this.format,
|
||||
sampleRate: this.sampleRate,
|
||||
numberOfChannels: this.numberOfChannels,
|
||||
timestamp: this.timestamp + startSample / this.sampleRate,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this audio sample, releasing held resources. Audio samples should be closed as soon as they are not
|
||||
* needed anymore.
|
||||
|
||||
+23
-2
@@ -41,7 +41,12 @@ export type AssociatedGroup = {
|
||||
export type Segment = {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
relativeToUnixEpoch: boolean;
|
||||
/**
|
||||
* The Unix time (in seconds) corresponding to this segment's start timestamp, or null if unknown. This is computed
|
||||
* whenever the source provides wall-clock information (e.g. HLS program date time), even if the segment timestamps
|
||||
* themselves are not shifted into Unix time space.
|
||||
*/
|
||||
unixEpochTimestamp: number | null;
|
||||
firstSegment: Segment | null;
|
||||
};
|
||||
|
||||
@@ -97,6 +102,18 @@ export abstract class SegmentedInput {
|
||||
return lastSegment.timestamp + lastSegment.duration;
|
||||
}
|
||||
|
||||
async getUnixTimeForTimestamp(timestamp: number): Promise<number | null> {
|
||||
let segment = await this.getSegmentAt(timestamp, {});
|
||||
segment ??= await this.getFirstSegment({});
|
||||
|
||||
if (!segment || segment.unixEpochTimestamp === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elapsed = timestamp - segment.timestamp;
|
||||
return segment.unixEpochTimestamp + elapsed;
|
||||
}
|
||||
|
||||
async getTrackBackings(): Promise<InputTrackBacking[]> {
|
||||
return this.trackBackingsPromise ??= (async () => {
|
||||
const backings: InputTrackBacking[] = [];
|
||||
@@ -310,7 +327,11 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
|
||||
await this.hydrate();
|
||||
|
||||
assert(this.segmentedInput.firstSegment);
|
||||
return this.segmentedInput.firstSegment.relativeToUnixEpoch;
|
||||
return this.segmentedInput.firstSegment.unixEpochTimestamp === this.segmentedInput.firstSegment.timestamp;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp(timestamp: number) {
|
||||
return this.segmentedInput.getUnixTimeForTimestamp(timestamp);
|
||||
}
|
||||
|
||||
getBitrate() {
|
||||
|
||||
+26
-5
@@ -289,10 +289,15 @@ export class SourceRef<S extends Source = Source> implements Disposable {
|
||||
*/
|
||||
export abstract class PathedSource extends Source {
|
||||
constructor(
|
||||
/** The path that points to the root file; the entry file of the media. */
|
||||
/**
|
||||
* The path that points to the root file; the entry file of the media.
|
||||
*
|
||||
* This path may be modified by the source to indicate a redirect: an updated path to perform new requests
|
||||
* relative to.
|
||||
*/
|
||||
public rootPath: FilePath,
|
||||
/** The callback that is called for each requested file; must return a {@link Source} or {@link SourceRef}. */
|
||||
public requestHandler: (request: SourceRequest) => MaybePromise<Source | SourceRef>,
|
||||
public readonly requestHandler: (request: SourceRequest) => MaybePromise<Source | SourceRef>,
|
||||
) {
|
||||
if (typeof rootPath !== 'string') {
|
||||
throw new TypeError('rootPath must be a string.');
|
||||
@@ -778,7 +783,10 @@ export class UrlSource extends PathedSource {
|
||||
? url.href
|
||||
: url;
|
||||
|
||||
super(urlString, request => new UrlSource(request.path, this._options));
|
||||
super(
|
||||
urlString,
|
||||
request => new UrlSource(request.path, this._options),
|
||||
);
|
||||
|
||||
this._url = url;
|
||||
this._options = options;
|
||||
@@ -905,6 +913,11 @@ export class UrlSource extends PathedSource {
|
||||
throw new Error(`Error fetching ${String(this._url)}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
if (response.redirected) {
|
||||
// Modify our own root path so that future subrequests get made relative to the redirected URL
|
||||
this.rootPath = response.url;
|
||||
}
|
||||
|
||||
outer:
|
||||
if (this._orchestrator.fileSize === null) {
|
||||
// See if we can deduce the file size from the response
|
||||
@@ -1989,6 +2002,14 @@ class ReadOrchestrator {
|
||||
} satisfies ReadResult));
|
||||
} else {
|
||||
// The requested region was satisfied by the cache, but the entire prefetch region was not
|
||||
promise.catch((error) => {
|
||||
if (this.disposed) {
|
||||
return; // Swallow the error
|
||||
}
|
||||
|
||||
// Nobody's awaiting this result but an errored read is still notable
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -2103,12 +2124,12 @@ class ReadOrchestrator {
|
||||
if (worker.pendingSlices.length > 0) {
|
||||
worker.pendingSlices.forEach(x => x.reject(error)); // Make sure to propagate any errors
|
||||
worker.pendingSlices.length = 0;
|
||||
} else {
|
||||
} else if (!worker.aborted && !this.disposed) {
|
||||
throw error; // So it doesn't get swallowed
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (worker.running) {
|
||||
if (worker.running || this.workers.length >= this.options.maxWorkerCount) {
|
||||
// Rare, but can happen with multiple concurrent reads. In this case, don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -402,6 +402,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return false;
|
||||
}
|
||||
|
||||
getUnixTimeForTimestamp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPairingMask() {
|
||||
return 1n;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { AudioSample } from '../../src/sample.js';
|
||||
|
||||
const SAMPLE_RATE = 48000;
|
||||
const NUM_CHANNELS = 2;
|
||||
const NUM_FRAMES = 10;
|
||||
const TIMESTAMP = 1;
|
||||
|
||||
const cellValue = (frame: number, channel: number) => frame * 10 + channel;
|
||||
|
||||
const makeInterleavedSample = () => {
|
||||
const data = new Int16Array(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);
|
||||
}
|
||||
}
|
||||
|
||||
return new AudioSample({
|
||||
data,
|
||||
format: 's16',
|
||||
sampleRate: SAMPLE_RATE,
|
||||
numberOfChannels: NUM_CHANNELS,
|
||||
timestamp: TIMESTAMP,
|
||||
});
|
||||
};
|
||||
|
||||
const makePlanarSample = () => {
|
||||
const data = new Float32Array(NUM_FRAMES * NUM_CHANNELS);
|
||||
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||
data[c * NUM_FRAMES + f] = cellValue(f, c);
|
||||
}
|
||||
}
|
||||
|
||||
return new AudioSample({
|
||||
data,
|
||||
format: 'f32-planar',
|
||||
sampleRate: SAMPLE_RATE,
|
||||
numberOfChannels: NUM_CHANNELS,
|
||||
timestamp: TIMESTAMP,
|
||||
});
|
||||
};
|
||||
|
||||
const readCell = (sample: AudioSample, frame: number, channel: number) => {
|
||||
if (sample.format === 's16') {
|
||||
const out = new Int16Array(sample.numberOfFrames * sample.numberOfChannels);
|
||||
sample.copyTo(out, { planeIndex: 0, format: 's16' });
|
||||
return out[frame * sample.numberOfChannels + channel];
|
||||
} else {
|
||||
const out = new Float32Array(sample.numberOfFrames);
|
||||
sample.copyTo(out, { planeIndex: channel, format: 'f32-planar' });
|
||||
return out[frame];
|
||||
}
|
||||
};
|
||||
|
||||
for (const [label, makeSample] of [
|
||||
['interleaved', makeInterleavedSample],
|
||||
['planar', makePlanarSample],
|
||||
] as const) {
|
||||
test(`trim, normal case (${label})`, () => {
|
||||
using sample = makeSample();
|
||||
using trimmed = sample.trim(3, 8);
|
||||
|
||||
expect(trimmed).not.toBe(sample);
|
||||
expect(trimmed.format).toBe(sample.format);
|
||||
expect(trimmed.sampleRate).toBe(SAMPLE_RATE);
|
||||
expect(trimmed.numberOfChannels).toBe(NUM_CHANNELS);
|
||||
expect(trimmed.numberOfFrames).toBe(5);
|
||||
expect(trimmed.duration).toBeCloseTo(5 / SAMPLE_RATE);
|
||||
expect(trimmed.timestamp).toBeCloseTo(TIMESTAMP + 3 / SAMPLE_RATE);
|
||||
|
||||
// The trimmed frame i corresponds to the original frame i + 3
|
||||
for (let f = 0; f < trimmed.numberOfFrames; f++) {
|
||||
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||
expect(readCell(trimmed, f, c)).toBe(cellValue(f + 3, c));
|
||||
}
|
||||
}
|
||||
|
||||
// The original sample must be untouched
|
||||
expect(sample.numberOfFrames).toBe(NUM_FRAMES);
|
||||
expect(readCell(sample, 3, 0)).toBe(cellValue(3, 0));
|
||||
});
|
||||
|
||||
test(`trim, full range is an independent copy (${label})`, () => {
|
||||
using sample = makeSample();
|
||||
using trimmed = sample.trim(0, NUM_FRAMES);
|
||||
|
||||
expect(trimmed.numberOfFrames).toBe(NUM_FRAMES);
|
||||
expect(trimmed.timestamp).toBeCloseTo(TIMESTAMP);
|
||||
for (let f = 0; f < NUM_FRAMES; f++) {
|
||||
for (let c = 0; c < NUM_CHANNELS; c++) {
|
||||
expect(readCell(trimmed, f, c)).toBe(cellValue(f, c));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test(`trim, pathological zero-sample case (${label})`, () => {
|
||||
using sample = makeSample();
|
||||
|
||||
// Empty range in the middle
|
||||
using empty = sample.trim(4, 4);
|
||||
expect(empty.numberOfFrames).toBe(0);
|
||||
expect(empty.duration).toBe(0);
|
||||
expect(empty.format).toBe(sample.format);
|
||||
expect(empty.numberOfChannels).toBe(NUM_CHANNELS);
|
||||
expect(empty.timestamp).toBeCloseTo(TIMESTAMP + 4 / SAMPLE_RATE);
|
||||
|
||||
// Empty range right at the end (startSample === numberOfFrames) is still valid
|
||||
using emptyAtEnd = sample.trim(NUM_FRAMES, NUM_FRAMES);
|
||||
expect(emptyAtEnd.numberOfFrames).toBe(0);
|
||||
expect(emptyAtEnd.timestamp).toBeCloseTo(TIMESTAMP + NUM_FRAMES / SAMPLE_RATE);
|
||||
});
|
||||
|
||||
test(`trim, illegal params (${label})`, () => {
|
||||
using sample = makeSample();
|
||||
|
||||
// Non-integer
|
||||
expect(() => sample.trim(1.5, 5)).toThrow(TypeError);
|
||||
expect(() => sample.trim(1, 5.5)).toThrow(TypeError);
|
||||
|
||||
// Negative
|
||||
expect(() => sample.trim(-1, 5)).toThrow(TypeError);
|
||||
expect(() => sample.trim(0, -1)).toThrow(TypeError);
|
||||
|
||||
// Out of range
|
||||
expect(() => sample.trim(0, NUM_FRAMES + 1)).toThrow(RangeError);
|
||||
expect(() => sample.trim(NUM_FRAMES + 1, NUM_FRAMES + 1)).toThrow(RangeError);
|
||||
|
||||
// End before start
|
||||
expect(() => sample.trim(6, 3)).toThrow(RangeError);
|
||||
});
|
||||
}
|
||||
|
||||
test('trim, throws on a closed sample', () => {
|
||||
const sample = makeInterleavedSample();
|
||||
sample.close();
|
||||
expect(() => sample.trim(0, 5)).toThrow('AudioSample is closed.');
|
||||
});
|
||||
@@ -526,11 +526,14 @@ test.concurrent('Single-value PDT', { timeout: 15_000 }, async () => {
|
||||
|
||||
const tracks = await input.getTracks();
|
||||
expect((await Promise.all(tracks.map(x => x.isRelativeToUnixEpoch()))).every(x => x)).toBe(true);
|
||||
expect((await Promise.all(tracks.map(x => x.hasUnixTimeMapping()))).every(x => x)).toBe(true);
|
||||
|
||||
const track = tracks[0]!;
|
||||
const firstTimestamp = await track.getFirstTimestamp();
|
||||
expect(firstTimestamp).toBe(Date.parse('2013-05-08T17:40:50Z') / 1000);
|
||||
|
||||
expect(await track.getUnixTimeForTimestamp(firstTimestamp)).toBe(firstTimestamp);
|
||||
|
||||
const endTimestamp = await track.computeDuration();
|
||||
expect(endTimestamp).toBe(firstTimestamp + 50);
|
||||
|
||||
@@ -600,6 +603,49 @@ test.concurrent('PDT with bad values', { timeout: 15_000 }, async () => {
|
||||
expect(await audioTrack.isRelativeToUnixEpoch()).toBe(false);
|
||||
});
|
||||
|
||||
test.concurrent('Single-value PDT with unix offsets disabled', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('https://playertest.longtailvideo.com/adaptive/aviion/manifest.m3u8'),
|
||||
formats: ALL_FORMATS,
|
||||
formatOptions: {
|
||||
hls: {
|
||||
offsetTimestampsByDateTime: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tracks = await input.getTracks();
|
||||
expect((await Promise.all(tracks.map(x => x.isRelativeToUnixEpoch()))).every(x => x)).toBe(false);
|
||||
expect((await Promise.all(tracks.map(x => x.hasUnixTimeMapping()))).every(x => x)).toBe(true);
|
||||
|
||||
const track = tracks[0]!;
|
||||
const firstTimestamp = await track.getFirstTimestamp();
|
||||
expect(firstTimestamp).toBe(0);
|
||||
|
||||
const endTimestamp = await track.computeDuration();
|
||||
expect(endTimestamp).toBe(firstTimestamp + 50);
|
||||
|
||||
const firstPacket = await new EncodedPacketSink(track).getFirstPacket();
|
||||
assert(firstPacket);
|
||||
expect(firstPacket.timestamp).toBe(firstTimestamp); // Kinda obvious check tbh
|
||||
|
||||
const unixStartTime = await track.getUnixTimeForTimestamp(firstTimestamp);
|
||||
expect(unixStartTime).toBe(Date.parse('2013-05-08T17:40:50Z') / 1000);
|
||||
|
||||
const unixEndTime = await track.getUnixTimeForTimestamp(endTimestamp);
|
||||
expect(unixEndTime).toBe(Date.parse('2013-05-08T17:41:40Z') / 1000);
|
||||
|
||||
const unixTimeBeforeStart = await track.getUnixTimeForTimestamp(firstTimestamp - 10);
|
||||
expect(unixTimeBeforeStart).toBe(Date.parse('2013-05-08T17:40:40Z') / 1000);
|
||||
|
||||
const unixTimeAfterEnd = await track.getUnixTimeForTimestamp(endTimestamp + 10);
|
||||
expect(unixTimeAfterEnd).toBe(Date.parse('2013-05-08T17:41:50Z') / 1000);
|
||||
|
||||
const timestampDt = 0.001;
|
||||
const unixTimeDt = (await track.getUnixTimeForTimestamp(firstTimestamp + 0.001))! - unixStartTime!;
|
||||
expect(unixTimeDt).toBeCloseTo(timestampDt);
|
||||
});
|
||||
|
||||
test.concurrent('Alternative audio only', { timeout: 15_000 }, async () => {
|
||||
using input = new Input({
|
||||
source: new UrlSource('https://playertest.longtailvideo.com/adaptive/alt-audio-no-video/sintel/playlist.m3u8'),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { Input, UnsupportedInputFormatError } from '../../src/input.js';
|
||||
import { ALL_FORMATS } from '../../src/input-format.js';
|
||||
import { BufferSource } from '../../src/source.js';
|
||||
|
||||
test('Disposing after failed format detection does not emit an unhandled rejection', async () => {
|
||||
const input = new Input({
|
||||
source: new BufferSource(new Uint8Array([1, 2, 3, 4])),
|
||||
formats: ALL_FORMATS,
|
||||
});
|
||||
|
||||
await expect(input.getFormat()).rejects.toThrow(UnsupportedInputFormatError);
|
||||
|
||||
const unhandledRejections: unknown[] = [];
|
||||
const onUnhandledRejection = (reason: unknown) => {
|
||||
unhandledRejections.push(reason);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', onUnhandledRejection);
|
||||
try {
|
||||
input.dispose();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
} finally {
|
||||
process.off('unhandledRejection', onUnhandledRejection);
|
||||
}
|
||||
|
||||
expect(unhandledRejections).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user