Compare commits

...
7 Commits
19 changed files with 545 additions and 322 deletions
+14 -6
View File
@@ -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,
+1 -1
View File
@@ -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';
+120 -62
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.46.0",
"version": "1.47.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.46.0",
"version": "1.47.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.47.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.47.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.47.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.47.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.47.0",
"license": "MPL-2.0",
"dependencies": {
"node-av": "^5.2.4"
"node-av": "^6.0.0"
},
"funding": {
"type": "individual",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.46.0",
"version": "1.47.0",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/aac-encoder",
"author": "Vanilagy",
"version": "1.46.0",
"version": "1.47.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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/ac3",
"author": "Vanilagy",
"version": "1.46.0",
"version": "1.47.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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/flac-encoder",
"author": "Vanilagy",
"version": "1.46.0",
"version": "1.47.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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/mp3-encoder",
"author": "Vanilagy",
"version": "1.46.0",
"version": "1.47.0",
"description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+5 -5
View File
@@ -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',
),
});
```
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@mediabunny/server",
"author": "Vanilagy",
"version": "1.46.0",
"version": "1.47.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"
+108 -215
View File
@@ -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) {
+10
View File
@@ -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);
};
+6 -2
View File
@@ -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(() => {});
}
}
/**
+9 -7
View File
@@ -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': {
+20 -2
View File
@@ -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());
+10 -15
View File
@@ -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,
});
+68
View File
@@ -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.
+139
View File
@@ -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.');
});
+28
View File
@@ -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([]);
});