From 6eb5c025913e1f157a7ca033130c97cfd23de36b Mon Sep 17 00:00:00 2001 From: David Payr <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:46:39 +0100 Subject: [PATCH] Add VideoSample and AudioSample abstractions --- dev/player.html | 38 -- package-lock.json | 744 +++++++++++++---------- package.json | 14 +- src/conversion.ts | 56 +- src/custom-coder.ts | 9 +- src/index.ts | 16 +- src/isobmff/isobmff-demuxer.ts | 8 +- src/media-sink.ts | 405 +++++-------- src/media-source.ts | 117 ++-- src/misc.ts | 27 +- src/packet.ts | 4 +- src/sample.ts | 1006 +++++++++++++++++++++++++++----- 12 files changed, 1571 insertions(+), 873 deletions(-) diff --git a/dev/player.html b/dev/player.html index f10bd9d..1752009 100644 --- a/dev/player.html +++ b/dev/player.html @@ -53,44 +53,6 @@ if (!(await audioTrack?.canDecode())) { audioTrack = null; } - -//const startTime2 = audioContext.currentTime + 0.2; -//const sink = new Metamuxer.AudioDataSink(audioTrack); -//for await (const { data } of sink.data(0)) { -// const sample = new Metamuxer.AudioSample(data) -// -// console.log(sample.toAudioData()) -// -// const audioBuffer = sample.toAudioBuffer() -// -// //console.log(sample, sample.allocationSize({ planeIndex: 7, format: 'u8-planar' })) -// -// //break; -// -// /* -// const audioBuffer = new AudioBuffer({ -// numberOfChannels: sample.numberOfChannels, -// length: sample.numberOfFrames, -// sampleRate: sample.sampleRate, -// }); -// -// // All user agents are required to support conversion to f32-planar -// const dataBytes = new Uint8Array(sample.allocationSize({ planeIndex: 0, format: 'u8-planar' })); -// -// for (let i = 0; i < sample.numberOfChannels; i++) { -// sample.copyTo(dataBytes, { planeIndex: i, format: 'u8-planar' }); -// audioBuffer.copyToChannel(new Float32Array(dataBytes).map(x => (x - 128)/128), i); -// } -// */ -// -// const node = audioContext.createBufferSource(); -// node.buffer = audioBuffer; -// node.connect(audioContext.destination); -// node.start(startTime2 + sample.timestamp); -//} -// -//await new Promise(() => {}) - const canvas = document.querySelector('canvas'); const context = canvas.getContext('2d'); let videoRotation = 0; diff --git a/package-lock.json b/package-lock.json index 3116aa6..35a1f14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,23 +13,24 @@ "@types/dom-webcodecs": "^0.1.13" }, "devDependencies": { - "@eslint/js": "^9.16.0", - "@microsoft/api-extractor": "^7.48.0", - "@stylistic/eslint-plugin": "^2.11.0", - "esbuild": "^0.23.1", - "eslint": "^9.16.0", - "typescript": "^5.5.4", - "typescript-eslint": "^8.16.0" + "@eslint/js": "^9.22.0", + "@microsoft/api-extractor": "^7.52.1", + "@stylistic/eslint-plugin": "^4.2.0", + "esbuild": "^0.25.1", + "eslint": "^9.22.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -39,13 +40,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -55,13 +57,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -71,13 +74,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -87,13 +91,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -103,13 +108,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -119,13 +125,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -135,13 +142,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -151,13 +159,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -167,13 +176,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -183,13 +193,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -199,13 +210,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -215,13 +227,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -231,13 +244,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -247,13 +261,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -263,13 +278,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -279,13 +295,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -294,14 +311,32 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -311,13 +346,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -327,13 +363,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -343,13 +380,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -359,13 +397,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -375,13 +414,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -391,13 +431,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -446,12 +487,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.0.tgz", - "integrity": "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -464,6 +506,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -471,20 +514,35 @@ "node": "*" } }, - "node_modules/@eslint/core": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", - "integrity": "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==", + "node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -508,6 +566,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -523,13 +582,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -538,29 +599,33 @@ } }, "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz", + "integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", - "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -616,10 +681,11 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18" }, @@ -629,64 +695,55 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.48.0", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.48.0.tgz", - "integrity": "sha512-FMFgPjoilMUWeZXqYRlJ3gCVRhB7WU/HN88n8OLqEsmsG4zBdX/KQdtJfhq95LQTQ++zfu0Em1LLb73NqRCLYQ==", + "version": "7.52.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.52.1.tgz", + "integrity": "sha512-m3I5uAwE05orsu3D1AGyisX5KxsgVXB+U4bWOOaX/Z7Ftp/2Cy41qsNhO6LPvSxHBaapyser5dVorF1t5M6tig==", "dev": true, + "license": "MIT", "dependencies": { - "@microsoft/api-extractor-model": "7.30.0", + "@microsoft/api-extractor-model": "7.30.4", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.10.0", + "@rushstack/node-core-library": "5.12.0", "@rushstack/rig-package": "0.5.3", - "@rushstack/terminal": "0.14.3", - "@rushstack/ts-command-line": "4.23.1", + "@rushstack/terminal": "0.15.1", + "@rushstack/ts-command-line": "4.23.6", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", - "typescript": "5.4.2" + "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.0.tgz", - "integrity": "sha512-26/LJZBrsWDKAkOWRiQbdVgcfd1F3nyJnAiJzsAgpouPk7LtOIj7PK9aJtBaw/pUXrkotEg27RrT+Jm/q0bbug==", + "version": "7.30.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.30.4.tgz", + "integrity": "sha512-RobC0gyVYsd2Fao9MTKOfTdBm41P/bCMUmzS5mQ7/MoAKEqy0FOBph3JOYdq4X4BsEnMEiSHc+0NUNmdzxCpjA==", "dev": true, + "license": "MIT", "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", - "@rushstack/node-core-library": "5.10.0" - } - }, - "node_modules/@microsoft/api-extractor/node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" + "@rushstack/node-core-library": "5.12.0" } }, "node_modules/@microsoft/tsdoc": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@microsoft/tsdoc-config": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.1.tgz", "integrity": "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==", "dev": true, + "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.15.1", "ajv": "~8.12.0", @@ -699,6 +756,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -712,6 +770,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -721,6 +780,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -730,15 +790,16 @@ } }, "node_modules/@rushstack/node-core-library": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.10.0.tgz", - "integrity": "sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.12.0.tgz", + "integrity": "sha512-QSwwzgzWoil1SCQse+yCHwlhRxNv2dX9siPnAb9zR/UmMhac4mjMrlMZpk64BlCeOFi1kJKgXRkihSwRMbboAQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", - "fs-extra": "~7.0.1", + "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", @@ -758,6 +819,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -780,12 +842,13 @@ } }, "node_modules/@rushstack/terminal": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.3.tgz", - "integrity": "sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.1.tgz", + "integrity": "sha512-3vgJYwumcjoDOXU3IxZfd616lqOdmr8Ezj4OWgJZfhmiBK4Nh7eWcv8sU8N/HdzXcuHDXCRGn/6O2Q75QvaZMA==", "dev": true, + "license": "MIT", "dependencies": { - "@rushstack/node-core-library": "5.10.0", + "@rushstack/node-core-library": "5.12.0", "supports-color": "~8.1.1" }, "peerDependencies": { @@ -798,24 +861,26 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.1.tgz", - "integrity": "sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==", + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.6.tgz", + "integrity": "sha512-7WepygaF3YPEoToh4MAL/mmHkiIImQq3/uAkQX46kVoKTNOOlCtFGyNnze6OYuWw2o9rxsyrHVfIBKxq/am2RA==", "dev": true, + "license": "MIT", "dependencies": { - "@rushstack/terminal": "0.14.3", + "@rushstack/terminal": "0.15.1", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "node_modules/@stylistic/eslint-plugin": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz", - "integrity": "sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz", + "integrity": "sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.13.0", + "@typescript-eslint/utils": "^8.23.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "estraverse": "^5.3.0", @@ -825,7 +890,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": ">=8.40.0" + "eslint": ">=9.0.0" } }, "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { @@ -844,7 +909,8 @@ "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/dom-mediacapture-transform": { "version": "0.1.10", @@ -872,20 +938,21 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", - "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/type-utils": "8.16.0", - "@typescript-eslint/utils": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -896,24 +963,21 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", - "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4" }, "engines": { @@ -924,22 +988,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", - "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0" + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -950,15 +1011,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", - "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.16.0", - "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -968,19 +1030,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", - "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -990,19 +1049,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", - "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/visitor-keys": "8.16.0", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1011,10 +1071,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1022,6 +1080,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1031,6 +1090,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1042,10 +1102,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1054,15 +1115,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", - "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.16.0", - "@typescript-eslint/types": "8.16.0", - "@typescript-eslint/typescript-estree": "8.16.0" + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1072,21 +1134,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", - "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/types": "8.26.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1123,6 +1182,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -1139,6 +1199,7 @@ "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", "dev": true, + "license": "MIT", "peerDependencies": { "ajv": "^8.5.0" }, @@ -1153,6 +1214,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -1185,6 +1247,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -1210,6 +1273,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1222,6 +1286,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1316,11 +1381,12 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1328,30 +1394,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" } }, "node_modules/escape-string-regexp": { @@ -1367,29 +1434,31 @@ } }, "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz", + "integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.9.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", - "@eslint/plugin-kit": "^0.2.3", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.1.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.22.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", + "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", @@ -1426,10 +1495,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -1521,6 +1591,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1553,16 +1624,17 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -1573,6 +1645,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1593,10 +1666,11 @@ "dev": true }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -1618,6 +1692,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1661,17 +1736,18 @@ "dev": true }, "node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=14.14" } }, "node_modules/function-bind": { @@ -1700,6 +1776,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1711,13 +1788,15 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", @@ -1750,10 +1829,11 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1770,6 +1850,7 @@ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1824,6 +1905,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -1838,13 +1920,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1856,7 +1940,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/json-buffer": { "version": "3.0.1", @@ -1868,7 +1953,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -1877,10 +1963,14 @@ "dev": true }, "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -1939,6 +2029,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -1951,6 +2042,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -1960,6 +2052,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -2044,6 +2137,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -2080,6 +2174,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2123,13 +2218,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2156,15 +2253,17 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -2189,6 +2288,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -2198,6 +2298,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2242,13 +2343,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } @@ -2270,6 +2373,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2297,6 +2401,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2305,15 +2410,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/type-check": { @@ -2329,10 +2435,11 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2342,14 +2449,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.16.0.tgz", - "integrity": "sha512-wDkVmlY6O2do4V+lZd0GtRfbtXbeD0q9WygwXXSJnC1xorE8eqyC2L1tJimqpSeFrOzRlYtWnUp/uzgHQOgfBQ==", + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.26.1.tgz", + "integrity": "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.16.0", - "@typescript-eslint/parser": "8.16.0", - "@typescript-eslint/utils": "8.16.0" + "@typescript-eslint/eslint-plugin": "8.26.1", + "@typescript-eslint/parser": "8.26.1", + "@typescript-eslint/utils": "8.26.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2359,21 +2467,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">= 10.0.0" } }, "node_modules/uri-js": { @@ -2413,7 +2518,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 0ead9c3..a1dfe19 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ "@types/dom-webcodecs": "^0.1.13" }, "devDependencies": { - "@eslint/js": "^9.16.0", - "@microsoft/api-extractor": "^7.48.0", - "@stylistic/eslint-plugin": "^2.11.0", - "esbuild": "^0.23.1", - "eslint": "^9.16.0", - "typescript": "^5.5.4", - "typescript-eslint": "^8.16.0" + "@eslint/js": "^9.22.0", + "@microsoft/api-extractor": "^7.52.1", + "@stylistic/eslint-plugin": "^4.2.0", + "esbuild": "^0.25.1", + "eslint": "^9.22.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1" } } diff --git a/src/conversion.ts b/src/conversion.ts index df84e7d..75b4410 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -13,24 +13,25 @@ import { Input } from './input'; import { InputAudioTrack, InputTrack, InputVideoTrack } from './input-track'; import { AudioBufferSink, - AudioDataSink, + AudioSampleSink, CanvasSink, EncodedPacketSink, - VideoFrameSink, + VideoSampleSink, } from './media-sink'; import { AudioBufferSource, - AudioDataSource, AudioEncodingConfig, AudioSource, EncodedVideoPacketSource, EncodedAudioPacketSource, VideoEncodingConfig, - VideoFrameSource, VideoSource, + VideoSampleSource, + AudioSampleSource, } from './media-source'; -import { assert, clamp, normalizeRotation, promiseWithResolvers, Rotation, setVideoFrameTiming } from './misc'; +import { assert, clamp, normalizeRotation, promiseWithResolvers, Rotation } from './misc'; import { Output, TrackType } from './output'; +import { VideoSample } from './sample'; /** @public */ export type ConversionOptions = { @@ -467,7 +468,7 @@ export class Conversion { }; if (needsRerender) { - const source = new VideoFrameSource(encodingConfig); + const source = new VideoSampleSource(encodingConfig); videoSource = source; this._trackPromises.push((async () => { @@ -491,36 +492,37 @@ export class Conversion { return; } - await source.add(new VideoFrame(canvas, { - timestamp: 1e6 * Math.max(timestamp - this._startTimestamp, 0), - duration: 1e6 * duration, - })); + const sample = new VideoSample(canvas, { + timestamp: Math.max(timestamp - this._startTimestamp, 0), + duration, + }); + + await source.add(sample); + sample.close(); } })()); } else { - const source = new VideoFrameSource(encodingConfig); + const source = new VideoSampleSource(encodingConfig); videoSource = source; this._trackPromises.push((async () => { await this._started; - const sink = new VideoFrameSink(track); + const sink = new VideoSampleSink(track); - for await (const { frame, timestamp } of sink.frames(this._startTimestamp, this._endTimestamp)) { - if (this._synchronizer.shouldWait(track.id, timestamp)) { - await this._synchronizer.wait(timestamp); + for await (const sample of sink.samples(this._startTimestamp, this._endTimestamp)) { + if (this._synchronizer.shouldWait(track.id, sample.timestamp)) { + await this._synchronizer.wait(sample.timestamp); } - const clone = setVideoFrameTiming(frame, { - timestamp: Math.max(timestamp - this._startTimestamp, 0), - }); + sample.setTimestamp(Math.max(sample.timestamp - this._startTimestamp, 0)); if (this._canceled) { return; } - await source.add(clone); - clone.close(); + await source.add(sample); + sample.close(); } await source.close(); @@ -662,7 +664,7 @@ export class Conversion { if (needsResample) { audioSource = this._resampleAudio(track, codecOfChoice, numberOfChannels, sampleRate); } else { - const source = new AudioDataSource({ + const source = new AudioSampleSource({ codec: codecOfChoice, bitrate: this._options.audio?.bitrate ?? QUALITY_HIGH, onEncodedPacket: packet => this._reportProgress(track.id, packet.timestamp + packet.duration), @@ -672,18 +674,18 @@ export class Conversion { this._trackPromises.push((async () => { await this._started; - const sink = new AudioDataSink(track); - for await (const { data, timestamp } of sink.data(undefined, this._endTimestamp)) { - if (this._synchronizer.shouldWait(track.id, timestamp)) { - await this._synchronizer.wait(timestamp); + const sink = new AudioSampleSink(track); + for await (const sample of sink.samples(undefined, this._endTimestamp)) { + if (this._synchronizer.shouldWait(track.id, sample.timestamp)) { + await this._synchronizer.wait(sample.timestamp); } if (this._canceled) { return; } - await source.add(data); - data.close(); + await source.add(sample); + sample.close(); } await source.close(); diff --git a/src/custom-coder.ts b/src/custom-coder.ts index 9a6c3c8..715825a 100644 --- a/src/custom-coder.ts +++ b/src/custom-coder.ts @@ -1,11 +1,12 @@ import { AudioCodec, VideoCodec } from './codec'; import { EncodedPacket } from './packet'; +import { AudioSample, VideoSample } from './sample'; /** @public */ export abstract class CustomVideoDecoder { codec!: VideoCodec; config!: VideoDecoderConfig; - onFrame!: (frame: VideoFrame) => unknown; + onSample!: (sample: VideoSample) => unknown; // eslint-disable-next-line @typescript-eslint/no-unused-vars static supports(codec: VideoCodec, config: VideoDecoderConfig): boolean { @@ -22,7 +23,7 @@ export abstract class CustomVideoDecoder { export abstract class CustomAudioDecoder { codec!: AudioCodec; config!: AudioDecoderConfig; - onData!: (data: AudioData) => unknown; + onSample!: (sample: AudioSample) => unknown; // eslint-disable-next-line @typescript-eslint/no-unused-vars static supports(codec: AudioCodec, config: AudioDecoderConfig): boolean { @@ -47,7 +48,7 @@ export abstract class CustomVideoEncoder { } abstract init(): void; - abstract encode(videoFrame: VideoFrame, options: VideoEncoderEncodeOptions): Promise | void; + abstract encode(videoSample: VideoSample, options: VideoEncoderEncodeOptions): Promise | void; abstract flush(): Promise | void; abstract close(): Promise | void; } @@ -64,7 +65,7 @@ export abstract class CustomAudioEncoder { } abstract init(): void; - abstract encode(audioData: AudioData): Promise | void; + abstract encode(audioSample: AudioSample): Promise | void; abstract flush(): Promise | void; abstract close(): Promise | void; } diff --git a/src/index.ts b/src/index.ts index 24ca56a..71b15ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,12 +30,12 @@ export { MediaSource, VideoSource, EncodedVideoPacketSource, - VideoFrameSource, + VideoSampleSource, CanvasSource, MediaStreamVideoTrackSource, AudioSource, EncodedAudioPacketSource, - AudioDataSource, + AudioSampleSource, AudioBufferSource, MediaStreamAudioTrackSource, SubtitleSource, @@ -67,7 +67,7 @@ export { getEncodableSubtitleCodecs, } from './codec'; export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target'; -export { Rotation, AnyIterable, setVideoFrameTiming } from './misc'; +export { Rotation, AnyIterable, SetRequired } from './misc'; export { Source, BufferSource, StreamSource, StreamSourceOptions, BlobSource, UrlSource } from './source'; export { InputFormat, @@ -92,20 +92,20 @@ export { Input, InputOptions } from './input'; export { InputTrack, InputVideoTrack, InputAudioTrack, PacketStats } from './input-track'; export { EncodedPacket, PacketType } from './packet'; export { + VideoSample, + VideoSampleInit, AudioSample, AudioSampleInit, } from './sample'; export { PacketRetrievalOptions, EncodedPacketSink, - BaseMediaFrameSink, - VideoFrameSink, - WrappedVideoFrame, + BaseMediaSampleSink, + VideoSampleSink, CanvasSinkOptions, CanvasSink, WrappedCanvas, - AudioDataSink, - WrappedAudioData, + AudioSampleSink, AudioBufferSink, WrappedAudioBuffer, } from './media-sink'; diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 26a6d16..ccd0c0b 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -2200,10 +2200,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { // but can jump right into the correct fragment (or at least nearby). const lookupEntryIndex = this.internalTrack.fragmentLookupTable ? binarySearchLessOrEqual( - this.internalTrack.fragmentLookupTable, - searchTimestamp, - x => x.timestamp, - ) + this.internalTrack.fragmentLookupTable, + searchTimestamp, + x => x.timestamp, + ) : -1; const lookupEntry = lookupEntryIndex !== -1 ? this.internalTrack.fragmentLookupTable![lookupEntryIndex]! diff --git a/src/media-sink.ts b/src/media-sink.ts index e880d0a..b4b3bf5 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -17,6 +17,7 @@ import { } from './misc'; import { EncodedPacket } from './packet'; import { fromAlaw, fromUlaw } from './pcm'; +import { AudioSample, VideoSample } from './sample'; /** @public */ export type PacketRetrievalOptions = { @@ -180,18 +181,11 @@ export class EncodedPacketSink { } } -export type WrappedMediaFrame = { - frame: T; - timestamp: number; - duration: number; -}; - abstract class DecoderWrapper< - MediaFrame extends VideoFrame | AudioData, - WrappedFrame extends WrappedMediaFrame = WrappedMediaFrame, + MediaSample extends VideoSample | AudioSample, > { constructor( - public onFrame: (frame: WrappedFrame) => unknown, + public onSample: (sample: MediaSample) => unknown, public onError: (error: DOMException) => unknown, ) {} @@ -202,36 +196,29 @@ abstract class DecoderWrapper< } /** @public */ -export abstract class BaseMediaFrameSink< - MediaFrame extends VideoFrame | AudioData, - /** @internal */ - WrappedFrame extends WrappedMediaFrame = WrappedMediaFrame, +export abstract class BaseMediaSampleSink< + MediaSample extends VideoSample | AudioSample, > { /** @internal */ abstract _createDecoder( - onFrame: (frame: WrappedFrame) => unknown, + onSample: (sample: MediaSample) => unknown, onError: (error: DOMException) => unknown - ): Promise>; + ): Promise>; /** @internal */ abstract _createPacketSink(): EncodedPacketSink; /** @internal */ - private _duplicateFrame(frame: WrappedFrame) { - return structuredClone(frame); - } - - /** @internal */ - protected mediaFramesInRange( + protected mediaSamplesInRange( startTimestamp = 0, endTimestamp = Infinity, - ): AsyncGenerator { + ): AsyncGenerator { validateTimestamp(startTimestamp); validateTimestamp(endTimestamp); const MAX_QUEUE_SIZE = 8; - const frameQueue: WrappedFrame[] = []; - let firstFrameQueued = false; - let lastFrame: WrappedFrame | null = null; + const sampleQueue: MediaSample[] = []; + let firstSampleQueued = false; + let lastSample: MediaSample | null = null; let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers(); let { promise: queueDequeue, resolve: onQueueDequeue } = promiseWithResolvers(); let decoderIsFlushed = false; @@ -246,38 +233,38 @@ export abstract class BaseMediaFrameSink< // The following is the "pump" process that keeps pumping packets into the decoder (async () => { const decoderError = new Error(); - const decoder = await this._createDecoder((wrappedFrame) => { + const decoder = await this._createDecoder((sample) => { onQueueDequeue(); - if (wrappedFrame.timestamp >= endTimestamp) { + if (sample.timestamp >= endTimestamp) { ended = true; } if (ended) { - wrappedFrame.frame.close(); + sample.close(); return; } - if (lastFrame) { - if (wrappedFrame.timestamp > startTimestamp) { + if (lastSample) { + if (sample.timestamp > startTimestamp) { // We don't know ahead of time what the first first is. This is because the first first is the // last first whose timestamp is less than or equal to the start timestamp. Therefore we need to // wait for the first first after the start timestamp, and then we'll know that the previous // first was the first first. - frameQueue.push(lastFrame); - firstFrameQueued = true; + sampleQueue.push(lastSample); + firstSampleQueued = true; } else { - lastFrame.frame.close(); + lastSample.close(); } } - if (wrappedFrame.timestamp >= startTimestamp) { - frameQueue.push(wrappedFrame); - firstFrameQueued = true; + if (sample.timestamp >= startTimestamp) { + sampleQueue.push(sample); + firstSampleQueued = true; } - lastFrame = firstFrameQueued ? null : wrappedFrame; + lastSample = firstSampleQueued ? null : sample; - if (frameQueue.length > 0) { + if (sampleQueue.length > 0) { onQueueNotEmpty(); ({ promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers()); } @@ -319,7 +306,7 @@ export abstract class BaseMediaFrameSink< await packets.next(); // Skip the start packet as we already have it while (currentPacket && !ended) { - if (frameQueue.length + decoder.getDecodeQueueSize() > MAX_QUEUE_SIZE) { + if (sampleQueue.length + decoder.getDecodeQueueSize() > MAX_QUEUE_SIZE) { ({ promise: queueDequeue, resolve: onQueueDequeue } = promiseWithResolvers()); await queueDequeue; continue; @@ -340,8 +327,8 @@ export abstract class BaseMediaFrameSink< if (!terminated) await decoder.flush(); decoder.close(); - if (!firstFrameQueued && lastFrame) { - frameQueue.push(lastFrame); + if (!firstSampleQueued && lastSample) { + sampleQueue.push(lastSample); } decoderIsFlushed = true; @@ -360,8 +347,8 @@ export abstract class BaseMediaFrameSink< return { value: undefined, done: true }; } else if (outOfBandError) { throw outOfBandError; - } else if (frameQueue.length > 0) { - const value = frameQueue.shift()!; + } else if (sampleQueue.length > 0) { + const value = sampleQueue.shift()!; onQueueDequeue(); return { value, done: false }; } else if (!decoderIsFlushed) { @@ -377,10 +364,10 @@ export abstract class BaseMediaFrameSink< onQueueDequeue(); onQueueNotEmpty(); - lastFrame?.frame.close(); + lastSample?.close(); - for (const frame of frameQueue) { - frame.frame.close(); + for (const sample of sampleQueue) { + sample.close(); } return { value: undefined, done: true }; @@ -395,15 +382,15 @@ export abstract class BaseMediaFrameSink< } /** @internal */ - protected mediaFramesAtTimestamps( + protected mediaSamplesAtTimestamps( timestamps: AnyIterable, - ): AsyncGenerator { + ): AsyncGenerator { validateAnyIterable(timestamps); const timestampIterator = toAsyncIterator(timestamps); const timestampsOfInterest: number[] = []; const MAX_QUEUE_SIZE = 8; - const frameQueue: (WrappedFrame | null)[] = []; + const sampleQueue: (MediaSample | null)[] = []; let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers(); let { promise: queueDequeue, resolve: onQueueDequeue } = promiseWithResolvers(); let decoderIsFlushed = false; @@ -414,9 +401,9 @@ export abstract class BaseMediaFrameSink< // the consumer. let outOfBandError = null as Error | null; - let lastUsedFrame = null as WrappedFrame | null; - const pushToQueue = (frame: WrappedFrame | null) => { - frameQueue.push(frame); + let lastUsedSample = null as MediaSample | null; + const pushToQueue = (sample: MediaSample | null) => { + sampleQueue.push(sample); onQueueNotEmpty(); ({ promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers()); }; @@ -424,29 +411,29 @@ export abstract class BaseMediaFrameSink< // The following is the "pump" process that keeps pumping packets into the decoder (async () => { const decoderError = new Error(); - const decoder = await this._createDecoder((wrappedFrame) => { + const decoder = await this._createDecoder((sample) => { onQueueDequeue(); if (terminated) { - wrappedFrame.frame.close(); + sample.close(); return; } - let frameUsed = false; + let sampleUsed = false; while ( timestampsOfInterest.length > 0 - && wrappedFrame.timestamp - timestampsOfInterest[0]! > -1e-10 // Give it a little epsilon + && sample.timestamp - timestampsOfInterest[0]! > -1e-10 // Give it a little epsilon ) { - pushToQueue(this._duplicateFrame(wrappedFrame)); + pushToQueue(sample.clone() as MediaSample); timestampsOfInterest.shift(); - frameUsed = true; + sampleUsed = true; } - if (frameUsed) { - lastUsedFrame?.frame.close(); - lastUsedFrame = wrappedFrame; + if (sampleUsed) { + lastUsedSample?.close(); + lastUsedSample = sample; } else { - wrappedFrame.frame.close(); + sample.close(); } }, (error) => { if (!outOfBandError) { @@ -463,7 +450,7 @@ export abstract class BaseMediaFrameSink< for await (const timestamp of timestampIterator) { validateTimestamp(timestamp); - while (frameQueue.length + decoder.getDecodeQueueSize() > MAX_QUEUE_SIZE && !terminated) { + while (sampleQueue.length + decoder.getDecodeQueueSize() > MAX_QUEUE_SIZE && !terminated) { ({ promise: queueDequeue, resolve: onQueueDequeue } = promiseWithResolvers()); await queueDequeue; } @@ -501,10 +488,10 @@ export abstract class BaseMediaFrameSink< targetPacket.sequenceNumber === lastPacket.sequenceNumber && timestampsOfInterest.length === 0 ) { - // Special case: We have a repeat packet, but the frame for that packet has already been - // decoded. Therefore, we need to push the frame here instead of in the decoder callback. - if (lastUsedFrame) { - pushToQueue(this._duplicateFrame(lastUsedFrame)); + // Special case: We have a repeat packet, but the sample for that packet has already been + // decoded. Therefore, we need to push the sample here instead of in the decoder callback. + if (lastUsedSample) { + pushToQueue(lastUsedSample.clone() as MediaSample); } } else { timestampsOfInterest.push(targetPacket.timestamp); @@ -528,7 +515,7 @@ export abstract class BaseMediaFrameSink< if (!terminated) { await decoder.flush(); - lastUsedFrame?.frame.close(); + lastUsedSample?.close(); } decoder.close(); @@ -548,8 +535,8 @@ export abstract class BaseMediaFrameSink< return { value: undefined, done: true }; } else if (outOfBandError) { throw outOfBandError; - } else if (frameQueue.length > 0) { - const value = frameQueue.shift(); + } else if (sampleQueue.length > 0) { + const value = sampleQueue.shift(); assert(value !== undefined); onQueueDequeue(); return { value, done: false }; @@ -565,10 +552,10 @@ export abstract class BaseMediaFrameSink< onQueueDequeue(); onQueueNotEmpty(); - for (const frame of frameQueue) { - frame?.frame.close(); + for (const sample of sampleQueue) { + sample?.close(); } - lastUsedFrame?.frame.close(); + lastUsedSample?.close(); return { value: undefined, done: true }; }, @@ -582,44 +569,45 @@ export abstract class BaseMediaFrameSink< } } -class VideoDecoderWrapper extends DecoderWrapper { +class VideoDecoderWrapper extends DecoderWrapper { decoder: VideoDecoder | null = null; customDecoder: CustomVideoDecoder | null = null; lastCustomDecoderPromise = Promise.resolve(); customDecoderQueueSize = 0; - frameQueue: VideoFrame[] = []; + sampleQueue: VideoSample[] = []; constructor( - onFrame: (frame: WrappedMediaFrame) => unknown, + onSample: (sample: VideoSample) => unknown, onError: (error: DOMException) => unknown, codec: VideoCodec, decoderConfig: VideoDecoderConfig, + public rotation: Rotation, public timeResolution: number, ) { - super(onFrame, onError); + super(onSample, onError); - const frameHandler = (frame: VideoFrame) => { + const sampleHandler = (sample: VideoSample) => { // For correct B-frame handling, we don't just hand over the frames directly but instead add them to a // queue, because we want to ensure frames are emitted in presentation order. We flush the queue each time // we receive a frame with a timestamp larger than the highest we've seen so far, as we can sure that is // not a B-frame. Typically, WebCodecs automatically guarantees that frames are emitted in presentation // order, but some browsers (Safari) don't always follow this rule. - if (this.frameQueue.length > 0 && (frame.timestamp >= last(this.frameQueue)!.timestamp)) { - for (const frame of this.frameQueue) { - this.wrapAndEmitFrame(frame); + if (this.sampleQueue.length > 0 && (sample.timestamp >= last(this.sampleQueue)!.timestamp)) { + for (const sample of this.sampleQueue) { + this.finalizeAndEmitSample(sample); } - this.frameQueue.length = 0; + this.sampleQueue.length = 0; } const insertionIndex = binarySearchLessOrEqual( - this.frameQueue, - frame.timestamp, + this.sampleQueue, + sample.timestamp, x => x.timestamp, ); - this.frameQueue.splice(insertionIndex + 1, 0, frame); + this.sampleQueue.splice(insertionIndex + 1, 0, sample); }; const MatchingCustomDecoder = customVideoDecoders.find(x => x.supports(codec, decoderConfig)); @@ -628,28 +616,24 @@ class VideoDecoderWrapper extends DecoderWrapper { this.customDecoder = new MatchingCustomDecoder() as CustomVideoDecoder; this.customDecoder.codec = codec; this.customDecoder.config = decoderConfig; - this.customDecoder.onFrame = frameHandler; + this.customDecoder.onSample = sampleHandler; this.customDecoder.init(); } else { this.decoder = new VideoDecoder({ - output: frameHandler, + output: frame => sampleHandler(new VideoSample(frame)), error: onError, }); this.decoder.configure(decoderConfig); } } - wrapAndEmitFrame(frame: VideoFrame) { - // Round the microsecond timestamps to the time resolution - const timestamp = Math.round(frame.timestamp / 1e6 * this.timeResolution) / this.timeResolution; - const duration = Math.round((frame.duration ?? 0) / 1e6 * this.timeResolution) / this.timeResolution; + finalizeAndEmitSample(sample: VideoSample) { + // Round the timestamps to the time resolution + sample.setTimestamp(Math.round(sample.timestamp * this.timeResolution) / this.timeResolution); + sample.setDuration(Math.round(sample.duration * this.timeResolution) / this.timeResolution); - this.onFrame({ - frame, - timestamp, - duration, - }); + this.onSample(sample); } getDecodeQueueSize() { @@ -683,10 +667,10 @@ class VideoDecoderWrapper extends DecoderWrapper { await this.decoder.flush(); } - for (const frame of this.frameQueue) { - this.wrapAndEmitFrame(frame); + for (const sample of this.sampleQueue) { + this.finalizeAndEmitSample(sample); } - this.frameQueue.length = 0; + this.sampleQueue.length = 0; } close() { @@ -697,22 +681,15 @@ class VideoDecoderWrapper extends DecoderWrapper { this.decoder.close(); } - for (const frame of this.frameQueue) { - frame.close(); + for (const sample of this.sampleQueue) { + sample.close(); } - this.frameQueue.length = 0; + this.sampleQueue.length = 0; } } /** @public */ -export type WrappedVideoFrame = { - frame: VideoFrame; - timestamp: number; - duration: number; -}; - -/** @public */ -export class VideoFrameSink extends BaseMediaFrameSink { +export class VideoSampleSink extends BaseMediaSampleSink { /** @internal */ _videoTrack: InputVideoTrack; @@ -728,7 +705,7 @@ export class VideoFrameSink extends BaseMediaFrameSink { /** @internal */ async _createDecoder( - onFrame: (frame: WrappedMediaFrame) => unknown, + onSample: (sample: VideoSample) => unknown, onError: (error: DOMException) => unknown, ) { if (!(await this._videoTrack.canDecode())) { @@ -739,11 +716,12 @@ export class VideoFrameSink extends BaseMediaFrameSink { } const codec = this._videoTrack.codec; + const rotation = this._videoTrack.rotation; const decoderConfig = await this._videoTrack.getDecoderConfig(); const timeResolution = this._videoTrack.timeResolution; assert(codec && decoderConfig); - return new VideoDecoderWrapper(onFrame, onError, codec, decoderConfig, timeResolution); + return new VideoDecoderWrapper(onSample, onError, codec, decoderConfig, rotation, timeResolution); } /** @internal */ @@ -751,36 +729,21 @@ export class VideoFrameSink extends BaseMediaFrameSink { return new EncodedPacketSink(this._videoTrack); } - /** @internal */ - _wrappedFrameToWrappedVideoFrame(frame: WrappedMediaFrame): WrappedVideoFrame { - return { - frame: frame.frame, - timestamp: frame.timestamp, - duration: frame.duration, - }; - } - - async getFrame(timestamp: number) { + async getSample(timestamp: number) { validateTimestamp(timestamp); - for await (const frame of this.mediaFramesAtTimestamps([timestamp])) { - return frame && this._wrappedFrameToWrappedVideoFrame(frame); + for await (const sample of this.mediaSamplesAtTimestamps([timestamp])) { + return sample; } throw new Error('Internal error: Iterator returned nothing.'); } - frames(startTimestamp = 0, endTimestamp = Infinity) { - return mapAsyncGenerator( - this.mediaFramesInRange(startTimestamp, endTimestamp), - frame => this._wrappedFrameToWrappedVideoFrame(frame), - ); + samples(startTimestamp = 0, endTimestamp = Infinity) { + return this.mediaSamplesInRange(startTimestamp, endTimestamp); } - framesAtTimestamps(timestamps: AnyIterable) { - return mapAsyncGenerator( - this.mediaFramesAtTimestamps(timestamps), - frame => frame && this._wrappedFrameToWrappedVideoFrame(frame), - ); + samplesAtTimestamps(timestamps: AnyIterable) { + return this.mediaSamplesAtTimestamps(timestamps); } } @@ -813,7 +776,7 @@ export class CanvasSink { /** @internal */ _rotation: Rotation; /** @internal */ - _videoFrameSink: VideoFrameSink; + _videoSampleSink: VideoSampleSink; /** @internal */ _canvasPool: (HTMLCanvasElement | OffscreenCanvas | null)[]; /** @internal */ @@ -877,12 +840,12 @@ export class CanvasSink { this._height = height; this._rotation = rotation; this._fit = options.fit ?? 'fill'; - this._videoFrameSink = new VideoFrameSink(videoTrack); + this._videoSampleSink = new VideoSampleSink(videoTrack); this._canvasPool = Array.from({ length: options.poolSize ?? 0 }, () => null); } /** @internal */ - _videoFrameToWrappedCanvas(frame: WrappedVideoFrame): WrappedCanvas { + _videoSampleToWrappedCanvas(sample: VideoSample): WrappedCanvas { let canvas = this._canvasPool[this._nextCanvasIndex]; if (!canvas) { if (typeof OffscreenCanvas !== 'undefined') { @@ -903,12 +866,13 @@ export class CanvasSink { this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length; } - const context = canvas.getContext('2d', { alpha: false }); + const context + = canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; assert(context); context.resetTransform(); - // These variables specify where the final frame will be drawn on the canvas + // These variables specify where the final sample will be drawn on the canvas let dx: number; let dy: number; let newWidth: number; @@ -920,15 +884,15 @@ export class CanvasSink { newWidth = this._width; newHeight = this._height; } else { - const [frameWidth, frameHeight] = this._rotation % 180 === 0 - ? [frame.frame.codedWidth, frame.frame.codedHeight] - : [frame.frame.codedHeight, frame.frame.codedWidth]; + const [sampleWidth, sampleHeight] = this._rotation % 180 === 0 + ? [sample.codedWidth, sample.codedHeight] + : [sample.codedHeight, sample.codedWidth]; const scale = this._fit === 'contain' - ? Math.min(this._width / frameWidth, this._height / frameHeight) - : Math.max(this._width / frameWidth, this._height / frameHeight); - newWidth = frameWidth * scale; - newHeight = frameHeight * scale; + ? Math.min(this._width / sampleWidth, this._height / sampleHeight) + : Math.max(this._width / sampleWidth, this._height / sampleHeight); + newWidth = sampleWidth * scale; + newHeight = sampleHeight * scale; dx = (this._width - newWidth) / 2; dy = (this._height - newHeight) / 2; } @@ -936,46 +900,46 @@ export class CanvasSink { const aspectRatioChange = this._rotation % 180 === 0 ? 1 : newWidth / newHeight; context.translate(this._width / 2, this._height / 2); context.rotate(this._rotation * Math.PI / 180); - // This aspect ratio compensation is done so that we can draw the frame with the intended dimensions and + // This aspect ratio compensation is done so that we can draw the sample with the intended dimensions and // don't need to think about how those dimensions change after the rotation context.scale(1 / aspectRatioChange, aspectRatioChange); context.translate(-this._width / 2, -this._height / 2); - context.drawImage(frame.frame, dx, dy, newWidth, newHeight); + context.drawImage(sample.toCanvasImageSource(), dx, dy, newWidth, newHeight); const result = { canvas, - timestamp: frame.timestamp, - duration: frame.duration, + timestamp: sample.timestamp, + duration: sample.duration, }; - frame.frame.close(); + sample.close(); return result; } async getCanvas(timestamp: number) { validateTimestamp(timestamp); - const frame = await this._videoFrameSink.getFrame(timestamp); - return frame && this._videoFrameToWrappedCanvas(frame); + const sample = await this._videoSampleSink.getSample(timestamp); + return sample && this._videoSampleToWrappedCanvas(sample); } canvases(startTimestamp = 0, endTimestamp = Infinity) { return mapAsyncGenerator( - this._videoFrameSink.frames(startTimestamp, endTimestamp), - frame => this._videoFrameToWrappedCanvas(frame), + this._videoSampleSink.samples(startTimestamp, endTimestamp), + sample => this._videoSampleToWrappedCanvas(sample), ); } canvasesAtTimestamps(timestamps: AnyIterable) { return mapAsyncGenerator( - this._videoFrameSink.framesAtTimestamps(timestamps), - frame => frame && this._videoFrameToWrappedCanvas(frame), + this._videoSampleSink.samplesAtTimestamps(timestamps), + sample => sample && this._videoSampleToWrappedCanvas(sample), ); } } -class AudioDecoderWrapper extends DecoderWrapper { +class AudioDecoderWrapper extends DecoderWrapper { decoder: AudioDecoder | null = null; customDecoder: CustomAudioDecoder | null = null; @@ -983,25 +947,20 @@ class AudioDecoderWrapper extends DecoderWrapper { customDecoderQueueSize = 0; constructor( - onData: (data: WrappedMediaFrame) => unknown, + onSample: (sample: AudioSample) => unknown, onError: (error: DOMException) => unknown, codec: AudioCodec, decoderConfig: AudioDecoderConfig, ) { - super(onData, onError); + super(onSample, onError); - const dataHandler = (data: AudioData) => { + const sampleHandler = (sample: AudioSample) => { const sampleRate = decoderConfig.sampleRate; - // Round the microsecond timestamps to the sample rate - const timestamp = Math.round(data.timestamp / 1e6 * sampleRate) / sampleRate; - const duration = Math.round(data.duration / 1e6 * sampleRate) / sampleRate; + // Round the timestamp to the sample rate + sample.setTimestamp(Math.round(sample.timestamp * sampleRate) / sampleRate); - onData({ - frame: data, - timestamp, - duration, - }); + onSample(sample); }; const MatchingCustomDecoder = customAudioDecoders.find(x => x.supports(codec, decoderConfig)); @@ -1010,12 +969,12 @@ class AudioDecoderWrapper extends DecoderWrapper { this.customDecoder = new MatchingCustomDecoder() as CustomAudioDecoder; this.customDecoder.codec = codec; this.customDecoder.config = decoderConfig; - this.customDecoder.onData = dataHandler; + this.customDecoder.onSample = sampleHandler; this.customDecoder.init(); } else { this.decoder = new AudioDecoder({ - output: dataHandler, + output: data => sampleHandler(new AudioSample(data)), error: onError, }); this.decoder.configure(decoderConfig); @@ -1066,7 +1025,7 @@ class AudioDecoderWrapper extends DecoderWrapper { // There are a lot of PCM variants not natively supported by the browser and by AudioData. Therefore we need a simple // decoder that maps any input PCM format into a PCM format supported by the browser. -class PcmAudioDecoderWrapper extends DecoderWrapper { +class PcmAudioDecoderWrapper extends DecoderWrapper { codec: PcmAudioCodec; inputSampleSize: 1 | 2 | 3 | 4; @@ -1081,11 +1040,11 @@ class PcmAudioDecoderWrapper extends DecoderWrapper { currentTimestamp: number | null = null; constructor( - onData: (data: WrappedMediaFrame) => unknown, + onSample: (sample: AudioSample) => unknown, onError: (error: DOMException) => unknown, public decoderConfig: AudioDecoderConfig, ) { - super(onData, onError); + super(onSample, onError); assert((PCM_AUDIO_CODECS as readonly string[]).includes(decoderConfig.codec)); this.codec = decoderConfig.codec as PcmAudioCodec; @@ -1207,21 +1166,16 @@ class PcmAudioDecoderWrapper extends DecoderWrapper { const preciseTimestamp = this.currentTimestamp; this.currentTimestamp += preciseDuration; - const audioData = new AudioData({ + const audioSample = new AudioSample({ format: this.outputFormat, data: outputBuffer, numberOfChannels: this.decoderConfig.numberOfChannels, sampleRate: this.decoderConfig.sampleRate, numberOfFrames, - timestamp: 1e6 * preciseTimestamp, + timestamp: preciseTimestamp, }); - // Since all other decoders are async, we'll make this one behave async as well - queueMicrotask(() => this.onFrame({ - frame: audioData, - timestamp: preciseTimestamp, - duration: preciseDuration, - })); + this.onSample(audioSample); } async flush() { @@ -1234,14 +1188,7 @@ class PcmAudioDecoderWrapper extends DecoderWrapper { } /** @public */ -export type WrappedAudioData = { - data: AudioData; - timestamp: number; - duration: number; -}; - -/** @public */ -export class AudioDataSink extends BaseMediaFrameSink { +export class AudioSampleSink extends BaseMediaSampleSink { /** @internal */ _audioTrack: InputAudioTrack; @@ -1257,7 +1204,7 @@ export class AudioDataSink extends BaseMediaFrameSink { /** @internal */ async _createDecoder( - onData: (data: WrappedMediaFrame) => unknown, + onSample: (sample: AudioSample) => unknown, onError: (error: DOMException) => unknown, ) { if (!(await this._audioTrack.canDecode())) { @@ -1272,47 +1219,32 @@ export class AudioDataSink extends BaseMediaFrameSink { assert(codec && decoderConfig); if ((PCM_AUDIO_CODECS as readonly string[]).includes(decoderConfig.codec)) { - return new PcmAudioDecoderWrapper(onData, onError, decoderConfig); + return new PcmAudioDecoderWrapper(onSample, onError, decoderConfig); } else { - return new AudioDecoderWrapper(onData, onError, codec, decoderConfig); + return new AudioDecoderWrapper(onSample, onError, codec, decoderConfig); } } - /** @internal */ - _wrappedFrameToWrappedAudioData(frame: WrappedMediaFrame): WrappedAudioData { - return { - data: frame.frame, - timestamp: frame.timestamp, - duration: frame.duration, - }; - } - /** @internal */ _createPacketSink() { return new EncodedPacketSink(this._audioTrack); } - async getData(timestamp: number) { + async getSample(timestamp: number) { validateTimestamp(timestamp); - for await (const data of this.mediaFramesAtTimestamps([timestamp])) { - return data && this._wrappedFrameToWrappedAudioData(data); + for await (const sample of this.mediaSamplesAtTimestamps([timestamp])) { + return sample; } throw new Error('Internal error: Iterator returned nothing.'); } - data(startTimestamp = 0, endTimestamp = Infinity) { - return mapAsyncGenerator( - this.mediaFramesInRange(startTimestamp, endTimestamp), - data => this._wrappedFrameToWrappedAudioData(data), - ); + samples(startTimestamp = 0, endTimestamp = Infinity) { + return this.mediaSamplesInRange(startTimestamp, endTimestamp); } - dataAtTimestamps(timestamps: AnyIterable) { - return mapAsyncGenerator( - this.mediaFramesAtTimestamps(timestamps), - data => data && this._wrappedFrameToWrappedAudioData(data), - ); + samplesAtTimestamps(timestamps: AnyIterable) { + return this.mediaSamplesAtTimestamps(timestamps); } } @@ -1326,60 +1258,43 @@ export type WrappedAudioBuffer = { /** @public */ export class AudioBufferSink { /** @internal */ - _audioDataSink: AudioDataSink; + _audioSampleSink: AudioSampleSink; constructor(audioTrack: InputAudioTrack) { if (!(audioTrack instanceof InputAudioTrack)) { throw new TypeError('audioTrack must be an InputAudioTrack.'); } - this._audioDataSink = new AudioDataSink(audioTrack); + this._audioSampleSink = new AudioSampleSink(audioTrack); } /** @internal */ - _audioDataToWrappedArrayBuffer(data: WrappedAudioData): WrappedAudioBuffer { - const audioBuffer = new AudioBuffer({ - numberOfChannels: data.data.numberOfChannels, - length: data.data.numberOfFrames, - sampleRate: data.data.sampleRate, - }); - - // All user agents are required to support conversion to f32-planar - const dataBytes = new Float32Array(data.data.allocationSize({ planeIndex: 0, format: 'f32-planar' }) / 4); - - for (let i = 0; i < data.data.numberOfChannels; i++) { - data.data.copyTo(dataBytes, { planeIndex: i, format: 'f32-planar' }); - audioBuffer.copyToChannel(dataBytes, i); - } - - const result = { - buffer: audioBuffer, - timestamp: data.timestamp, - duration: data.duration, + _audioSampleToWrappedArrayBuffer(sample: AudioSample): WrappedAudioBuffer { + return { + buffer: sample.toAudioBuffer(), + timestamp: sample.timestamp, + duration: sample.duration, }; - - data.data.close(); - return result; } async getBuffer(timestamp: number) { validateTimestamp(timestamp); - const data = await this._audioDataSink.getData(timestamp); - return data && this._audioDataToWrappedArrayBuffer(data); + const data = await this._audioSampleSink.getSample(timestamp); + return data && this._audioSampleToWrappedArrayBuffer(data); } buffers(startTimestamp = 0, endTimestamp = Infinity) { return mapAsyncGenerator( - this._audioDataSink.data(startTimestamp, endTimestamp), - data => this._audioDataToWrappedArrayBuffer(data), + this._audioSampleSink.samples(startTimestamp, endTimestamp), + data => this._audioSampleToWrappedArrayBuffer(data), ); } buffersAtTimestamps(timestamps: AnyIterable) { return mapAsyncGenerator( - this._audioDataSink.dataAtTimestamps(timestamps), - data => data && this._audioDataToWrappedArrayBuffer(data), + this._audioSampleSink.samplesAtTimestamps(timestamps), + data => data && this._audioSampleToWrappedArrayBuffer(data), ); } } diff --git a/src/media-source.ts b/src/media-source.ts index e47e055..ecd7470 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -26,6 +26,7 @@ import { customAudioEncoders, } from './custom-coder'; import { EncodedPacket } from './packet'; +import { AudioSample, VideoSample } from './sample'; /** @public */ export abstract class MediaSource { @@ -194,33 +195,33 @@ class VideoEncoderWrapper { constructor(private source: VideoSource, private encodingConfig: VideoEncodingConfig) {} - async add(videoFrame: VideoFrame, shouldClose: boolean, encodeOptions?: VideoEncoderEncodeOptions) { + async add(videoSample: VideoSample, shouldClose: boolean, encodeOptions?: VideoEncoderEncodeOptions) { this.source._ensureValidAdd(); - // Ensure video frame size remains constant + // Ensure video sample size remains constant if (this.lastWidth !== null && this.lastHeight !== null) { - if (videoFrame.codedWidth !== this.lastWidth || videoFrame.codedHeight !== this.lastHeight) { + if (videoSample.codedWidth !== this.lastWidth || videoSample.codedHeight !== this.lastHeight) { throw new Error( - `Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight},` - + ` got ${videoFrame.codedWidth}x${videoFrame.codedHeight}.`, + `Video sample size must remain constant. Expected ${this.lastWidth}x${this.lastHeight},` + + ` got ${videoSample.codedWidth}x${videoSample.codedHeight}.`, ); } } else { - this.lastWidth = videoFrame.codedWidth; - this.lastHeight = videoFrame.codedHeight; + this.lastWidth = videoSample.codedWidth; + this.lastHeight = videoSample.codedHeight; } if (!this.encoderInitialized) { if (this.ensureEncoderPromise) { await this.ensureEncoderPromise; } else { - await this.ensureEncoder(videoFrame); + await this.ensureEncoder(videoSample); } } assert(this.encoderInitialized); const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 5; - const multipleOfKeyFrameInterval = Math.floor((videoFrame.timestamp / 1e6) / keyFrameInterval); + const multipleOfKeyFrameInterval = Math.floor(videoSample.timestamp / keyFrameInterval); // Ensure a key frame every KEY_FRAME_INTERVAL seconds. It is important that all video tracks follow the same // "key frame" rhythm, because aligned key frames are required to start new fragments in ISOBMFF or clusters @@ -236,14 +237,14 @@ class VideoEncoderWrapper { if (this.customEncoder) { this.customEncoderQueueSize++; this.lastCustomEncoderPromise = this.lastCustomEncoderPromise.then(() => { - return this.customEncoder!.encode(videoFrame, finalEncodeOptions); + return this.customEncoder!.encode(videoSample, finalEncodeOptions); }); void this.lastCustomEncoderPromise.then(() => { this.customEncoderQueueSize--; if (shouldClose) { - videoFrame.close(); + videoSample.close(); } }); @@ -252,10 +253,12 @@ class VideoEncoderWrapper { } } else { assert(this.encoder); + const videoFrame = videoSample.toVideoFrame(); this.encoder.encode(videoFrame, finalEncodeOptions); + videoFrame.close(); if (shouldClose) { - videoFrame.close(); + videoSample.close(); } // We need to do this after sending the frame to the encoder as the frame otherwise might be closed @@ -267,7 +270,7 @@ class VideoEncoderWrapper { await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure } - private async ensureEncoder(videoFrame: VideoFrame) { + private async ensureEncoder(videoSample: VideoSample) { if (this.encoder) { return; } @@ -275,8 +278,8 @@ class VideoEncoderWrapper { const { promise, resolve } = promiseWithResolvers(); this.ensureEncoderPromise = promise; - const width = videoFrame.codedWidth; - const height = videoFrame.codedHeight; + const width = videoSample.codedWidth; + const height = videoSample.codedHeight; const bitrate = this.encodingConfig.bitrate instanceof Quality ? this.encodingConfig.bitrate._toVideoBitrate(this.encodingConfig.codec, width, height) : this.encodingConfig.bitrate; @@ -365,7 +368,7 @@ class VideoEncoderWrapper { } /** @public */ -export class VideoFrameSource extends VideoSource { +export class VideoSampleSource extends VideoSource { /** @internal */ private _encoder: VideoEncoderWrapper; @@ -376,12 +379,12 @@ export class VideoFrameSource extends VideoSource { this._encoder = new VideoEncoderWrapper(this, encodingConfig); } - add(videoFrame: VideoFrame, encodeOptions?: VideoEncoderEncodeOptions) { - if (!(videoFrame instanceof VideoFrame)) { - throw new TypeError('videoFrame must be a VideoFrame.'); + add(videoSample: VideoSample, encodeOptions?: VideoEncoderEncodeOptions) { + if (!(videoSample instanceof VideoSample)) { + throw new TypeError('videoSample must be a VideoSample.'); } - return this._encoder.add(videoFrame, false, encodeOptions); + return this._encoder.add(videoSample, false, encodeOptions); } /** @internal */ @@ -419,13 +422,8 @@ export class CanvasSource extends VideoSource { throw new TypeError('duration must be a non-negative number.'); } - const frame = new VideoFrame(this._canvas, { - timestamp: Math.round(1e6 * timestamp), - duration: Math.round(1e6 * duration) || undefined, // Drag 0 duration to undefined, glitches some codecs - alpha: 'discard', - }); - - return this._encoder.add(frame, true, encodeOptions); + const sample = new VideoSample(this._canvas, { timestamp, duration }); + return this._encoder.add(sample, true, encodeOptions); } /** @internal */ @@ -475,7 +473,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { return; } - void this._encoder.add(videoFrame, true); + void this._encoder.add(new VideoSample(videoFrame), true); }, }); @@ -587,31 +585,31 @@ class AudioEncoderWrapper { constructor(private source: AudioSource, private encodingConfig: AudioEncodingConfig) {} - async add(audioData: AudioData, shouldClose: boolean) { + async add(audioSample: AudioSample, shouldClose: boolean) { this.source._ensureValidAdd(); // Ensure audio parameters remain constant if (this.lastNumberOfChannels !== null && this.lastSampleRate !== null) { if ( - audioData.numberOfChannels !== this.lastNumberOfChannels - || audioData.sampleRate !== this.lastSampleRate + audioSample.numberOfChannels !== this.lastNumberOfChannels + || audioSample.sampleRate !== this.lastSampleRate ) { throw new Error( `Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at` - + ` ${this.lastSampleRate} Hz, got ${audioData.numberOfChannels} channels at` - + ` ${audioData.sampleRate} Hz.`, + + ` ${this.lastSampleRate} Hz, got ${audioSample.numberOfChannels} channels at` + + ` ${audioSample.sampleRate} Hz.`, ); } } else { - this.lastNumberOfChannels = audioData.numberOfChannels; - this.lastSampleRate = audioData.sampleRate; + this.lastNumberOfChannels = audioSample.numberOfChannels; + this.lastSampleRate = audioSample.sampleRate; } if (!this.encoderInitialized) { if (this.ensureEncoderPromise) { await this.ensureEncoderPromise; } else { - await this.ensureEncoder(audioData); + await this.ensureEncoder(audioSample); } } assert(this.encoderInitialized); @@ -619,14 +617,14 @@ class AudioEncoderWrapper { if (this.customEncoder) { this.customEncoderQueueSize++; this.lastCustomEncoderPromise = this.lastCustomEncoderPromise.then(() => { - return this.customEncoder!.encode(audioData); + return this.customEncoder!.encode(audioSample); }); void this.lastCustomEncoderPromise.then(() => { this.customEncoderQueueSize--; if (shouldClose) { - audioData.close(); + audioSample.close(); } }); @@ -636,13 +634,15 @@ class AudioEncoderWrapper { await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure } else if (this.isPcmEncoder) { - await this.doPcmEncoding(audioData, shouldClose); + await this.doPcmEncoding(audioSample, shouldClose); } else { assert(this.encoder); + const audioData = audioSample.toAudioData(); this.encoder.encode(audioData); + audioData.close(); if (shouldClose) { - audioData.close(); + audioSample.close(); } if (this.encoder.encodeQueueSize >= 4) { @@ -653,12 +653,12 @@ class AudioEncoderWrapper { } } - private async doPcmEncoding(audioData: AudioData, shouldClose: boolean) { + private async doPcmEncoding(audioSample: AudioSample, shouldClose: boolean) { assert(this.outputSampleSize); assert(this.writeOutputValue); // Need to extract data from the audio data before we close it - const { numberOfChannels, numberOfFrames, sampleRate, timestamp } = audioData; + const { numberOfChannels, numberOfFrames, sampleRate, timestamp } = audioSample; const CHUNK_SIZE = 2048; const outputs: { @@ -668,7 +668,7 @@ class AudioEncoderWrapper { // Prepare all of the output buffers, each being bounded by CHUNK_SIZE so we don't generate huge packets for (let frame = 0; frame < numberOfFrames; frame += CHUNK_SIZE) { - const frameCount = Math.min(CHUNK_SIZE, audioData.numberOfFrames - frame); + const frameCount = Math.min(CHUNK_SIZE, audioSample.numberOfFrames - frame); const outputSize = frameCount * numberOfChannels * this.outputSampleSize; const outputBuffer = new ArrayBuffer(outputSize); const outputView = new DataView(outputBuffer); @@ -676,12 +676,11 @@ class AudioEncoderWrapper { outputs.push({ frameCount, view: outputView }); } - // All user agents are required to support conversion to f32-planar - const allocationSize = audioData.allocationSize(({ planeIndex: 0, format: 'f32-planar' })); + const allocationSize = audioSample.allocationSize(({ planeIndex: 0, format: 'f32-planar' })); const floats = new Float32Array(allocationSize / Float32Array.BYTES_PER_ELEMENT); for (let i = 0; i < numberOfChannels; i++) { - audioData.copyTo(floats, { planeIndex: i, format: 'f32-planar' }); + audioSample.copyTo(floats, { planeIndex: i, format: 'f32-planar' }); for (let j = 0; j < outputs.length; j++) { const { frameCount, view } = outputs[j]!; @@ -697,7 +696,7 @@ class AudioEncoderWrapper { } if (shouldClose) { - audioData.close(); + audioSample.close(); } const meta: EncodedAudioChunkMetadata = { @@ -716,7 +715,7 @@ class AudioEncoderWrapper { const packet = new EncodedPacket( new Uint8Array(outputBuffer), 'key', - timestamp / 1e6 + startFrame / sampleRate, + timestamp + startFrame / sampleRate, frameCount / sampleRate, ); @@ -725,7 +724,7 @@ class AudioEncoderWrapper { } } - private async ensureEncoder(audioData: AudioData) { + private async ensureEncoder(audioSample: AudioSample) { if (this.encoderInitialized) { return; } @@ -733,7 +732,7 @@ class AudioEncoderWrapper { const { promise, resolve } = promiseWithResolvers(); this.ensureEncoderPromise = promise; - const { numberOfChannels, sampleRate } = audioData; + const { numberOfChannels, sampleRate } = audioSample; const bitrate = this.encodingConfig.bitrate instanceof Quality ? this.encodingConfig.bitrate._toAudioBitrate(this.encodingConfig.codec) : this.encodingConfig.bitrate; @@ -903,7 +902,7 @@ class AudioEncoderWrapper { } /** @public */ -export class AudioDataSource extends AudioSource { +export class AudioSampleSource extends AudioSource { /** @internal */ private _encoder: AudioEncoderWrapper; @@ -914,12 +913,12 @@ export class AudioDataSource extends AudioSource { this._encoder = new AudioEncoderWrapper(this, encodingConfig); } - add(audioData: AudioData) { - if (!(audioData instanceof AudioData)) { - throw new TypeError('audioData must be an AudioData.'); + add(audioSample: AudioSample) { + if (!(audioSample instanceof AudioSample)) { + throw new TypeError('audioSample must be an AudioSample.'); } - return this._encoder.add(audioData, false); + return this._encoder.add(audioSample, false); } /** @internal */ @@ -972,16 +971,16 @@ export class AudioBufferSource extends AudioSource { ); } - const audioData = new AudioData({ + const audioSample = new AudioSample({ format: 'f32-planar', sampleRate, numberOfFrames: framesToCopy, numberOfChannels, - timestamp: (1e6 * (this._accumulatedFrameCount + currentRelativeFrame)) / sampleRate, + timestamp: (this._accumulatedFrameCount + currentRelativeFrame) / sampleRate, data: chunkData, }); - promises.push(this._encoder.add(audioData, true)); + promises.push(this._encoder.add(audioSample, true)); currentRelativeFrame += framesToCopy; remainingFrames -= framesToCopy; @@ -1033,7 +1032,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { return; } - void this._encoder.add(audioData, true); + void this._encoder.add(new AudioSample(audioData), true); }, }); diff --git a/src/misc.ts b/src/misc.ts index 4eb5baf..e4d8b19 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -63,8 +63,12 @@ export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => { } }; -export const toDataView = (bytes: Uint8Array) => { - return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); +export const toDataView = (source: AllowSharedBufferSource) => { + if (source instanceof ArrayBuffer) { + return new DataView(source); + } else { + return new DataView(source.buffer, source.byteOffset, source.byteLength); + } }; export const textEncoder = new TextEncoder(); @@ -356,20 +360,6 @@ export const clamp = (value: number, min: number, max: number) => { export const UNDETERMINED_LANGUAGE = 'und'; -/** @public */ -export const setVideoFrameTiming = (frame: VideoFrame, timing: { - timestamp?: number; - duration?: number; -}) => { - const clone = new VideoFrame(frame, { - timestamp: timing.timestamp !== undefined ? 1e6 * timing.timestamp : undefined, - duration: timing.duration !== undefined ? 1e6 * timing.duration : undefined, - }); - frame.close(); - - return clone; -}; - export const roundToPrecision = (value: number, digits: number) => { const factor = 10 ** digits; return Math.round(value * factor) / factor; @@ -393,5 +383,8 @@ export const isIso639Dash2LanguageCode = (x: string) => { return ISO_639_2_REGEX.test(x); }; -// Since the result will be floored, add a bit of eps to compensate for floating point errors +// Since the result will be truncated, add a bit of eps to compensate for floating point errors export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON); + +/** @public */ +export type SetRequired = T & Required>; diff --git a/src/packet.ts b/src/packet.ts index 5212176..6c968ef 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -40,11 +40,11 @@ export class EncodedPacket { } get microsecondTimestamp() { - return Math.floor(SECOND_TO_MICROSECOND_FACTOR * this.timestamp); + return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp); } get microsecondDuration() { - return Math.floor(SECOND_TO_MICROSECOND_FACTOR * this.duration); + return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration); } toEncodedVideoChunk() { diff --git a/src/sample.ts b/src/sample.ts index 3563120..fa3d577 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -1,196 +1,916 @@ -export const PLACEHOLDER_DATA = new Uint8Array(0); +import { + assert, + clamp, + isAllowSharedBufferSource, + Rotation, + SECOND_TO_MICROSECOND_FACTOR, + toDataView, + toUint8Array, + SetRequired, +} from './misc'; /** @public */ -export type SampleType = 'key' | 'delta'; +export type VideoSampleInit = { + format?: VideoPixelFormat; + codedWidth?: number; + codedHeight?: number; + rotation?: Rotation; + timestamp?: number; + duration?: number; + colorSpace?: VideoColorSpaceInit; +}; /** @public */ -export class EncodedVideoSample { - constructor( - public readonly data: Uint8Array, - public readonly type: SampleType, - public readonly timestamp: number, - public readonly duration: number, - public readonly sequenceNumber = -1, - public readonly byteLength = data.byteLength, - ) { - if (!(data instanceof Uint8Array)) { - throw new TypeError('data must be a Uint8Array.'); - } - if (type !== 'key' && type !== 'delta') { - throw new TypeError('type must be either "key" or "delta".'); - } - if (!Number.isFinite(timestamp)) { - throw new TypeError('timestamp must be a number.'); - } - if (!Number.isFinite(duration) || duration < 0) { - throw new TypeError('duration must be a non-negative number.'); - } - if (!Number.isInteger(byteLength) || byteLength < 0) { - throw new TypeError('byteLength must be a non-negative integer.'); - } +export class VideoSample { + /** @internal */ + _data!: VideoFrame | OffscreenCanvas | Uint8Array | null; + /** @internal */ + _closed: boolean = false; + + readonly format!: VideoPixelFormat | null; + readonly codedWidth!: number; + readonly codedHeight!: number; + readonly rotation!: Rotation; + readonly timestamp!: number; + readonly duration!: number; + readonly colorSpace!: VideoColorSpace; + + get displayWidth() { + return this.rotation % 180 === 0 ? this.codedWidth : this.codedHeight; } - get isMetadataOnly() { - return this.data === PLACEHOLDER_DATA; + get displayHeight() { + return this.rotation % 180 === 0 ? this.codedHeight : this.codedWidth; } get microsecondTimestamp() { - return Math.floor(1e6 * this.timestamp); + return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp); } get microsecondDuration() { - return Math.floor(1e6 * this.duration); + return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration); } - toEncodedVideoChunk() { - if (this.isMetadataOnly) { - throw new TypeError('Metadata-only samples cannot be converted to a chunk.'); - } + constructor(data: VideoFrame, init?: VideoSampleInit); + constructor(data: CanvasImageSource, init: SetRequired); + constructor( + data: BufferSource, + init: SetRequired + ); + constructor( + data: VideoFrame | CanvasImageSource | BufferSource, + init?: VideoSampleInit, + ) { + if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { + if (!init || typeof init !== 'object') { + throw new TypeError('init must be an object.'); + } + if (!('format' in init) || typeof init.format !== 'string') { + throw new TypeError('init.format must be a string.'); + } + if (!Number.isInteger(init.codedWidth) || init.codedWidth! <= 0) { + throw new TypeError('init.codedWidth must be a positive integer.'); + } + if (!Number.isInteger(init.codedHeight) || init.codedHeight! <= 0) { + throw new TypeError('init.codedHeight must be a positive integer.'); + } + if (init.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) { + throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.'); + } + if (!Number.isFinite(init.timestamp)) { + throw new TypeError('init.timestamp must be a number.'); + } + if (init.duration !== undefined && (!Number.isFinite(init.duration) || init.duration < 0)) { + throw new TypeError('init.duration, when provided, must be a non-negative number.'); + } - if (typeof EncodedVideoChunk === 'undefined') { - throw new Error('Your browser does not support EncodedVideoChunk.'); - } + this._data = toUint8Array(data).slice(); // Copy it - return new EncodedVideoChunk({ - data: this.data, - type: this.type, - timestamp: this.microsecondTimestamp, - duration: this.microsecondDuration, - }); + this.format = init.format; + this.codedWidth = init.codedWidth!; + this.codedHeight = init.codedHeight!; + this.rotation = init.rotation ?? 0; + this.timestamp = init.timestamp!; + this.duration = init.duration ?? 0; + this.colorSpace = new VideoColorSpace(init.colorSpace); + } else if (typeof VideoFrame !== 'undefined' && data instanceof VideoFrame) { + if (init?.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) { + throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.'); + } + if (init?.timestamp !== undefined && !Number.isFinite(init?.timestamp)) { + throw new TypeError('init.timestamp, when provided, must be a number.'); + } + if (init?.duration !== undefined && (!Number.isFinite(init.duration) || init.duration < 0)) { + throw new TypeError('init.duration, when provided, must be a non-negative number.'); + } + + this._data = data; + + this.format = data.format; + this.codedWidth = data.codedWidth; + this.codedHeight = data.codedHeight; + this.rotation = init?.rotation ?? 0; + this.timestamp = init?.timestamp ?? data.timestamp / 1e6; + this.duration = init?.duration ?? (data.duration ?? 0) / 1e6; + this.colorSpace = data.colorSpace; + } else if ( + (typeof HTMLImageElement !== 'undefined' && data instanceof HTMLImageElement) + || (typeof SVGImageElement !== 'undefined' && data instanceof SVGImageElement) + || (typeof ImageBitmap !== 'undefined' && data instanceof ImageBitmap) + || (typeof HTMLVideoElement !== 'undefined' && data instanceof HTMLVideoElement) + || (typeof HTMLCanvasElement !== 'undefined' && data instanceof HTMLCanvasElement) + || (typeof OffscreenCanvas !== 'undefined' && data instanceof OffscreenCanvas) + ) { + if (!init || typeof init !== 'object') { + throw new TypeError('init must be an object.'); + } + if (init.rotation !== undefined && ![0, 90, 180, 270].includes(init.rotation)) { + throw new TypeError('init.rotation, when provided, must be 0, 90, 180, or 270.'); + } + if (!Number.isFinite(init.timestamp)) { + throw new TypeError('init.timestamp must be a number.'); + } + if (init.duration !== undefined && (!Number.isFinite(init.duration) || init.duration < 0)) { + throw new TypeError('init.duration, when provided, must be a non-negative number.'); + } + + if (typeof VideoFrame !== 'undefined') { + return new VideoSample( + new VideoFrame(data, { + timestamp: Math.trunc(init.timestamp! * SECOND_TO_MICROSECOND_FACTOR), + duration: Math.trunc((init.duration ?? 0) * SECOND_TO_MICROSECOND_FACTOR), + }), + init, + ); + } + + let width = 0; + let height = 0; + + // Determine the dimensions of the thing + if ('naturalWidth' in data) { + width = data.naturalWidth; + height = data.naturalHeight; + } else if ('videoWidth' in data) { + width = data.videoWidth; + height = data.videoHeight; + } else if ('width' in data) { + width = Number(data.width); + height = Number(data.height); + } + + if (!width || !height) { + throw new TypeError('Could not determine dimensions.'); + } + + const canvas = new OffscreenCanvas(width, height); + const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true }); + assert(context); + + // Draw it to a canvas + context.drawImage(data, 0, 0); + this._data = canvas; + + this.format = 'RGBX'; + this.codedWidth = width; + this.codedHeight = height; + this.rotation = init.rotation ?? 0; + this.timestamp = init.timestamp!; + this.duration = init.duration ?? 0; + this.colorSpace = new VideoColorSpace({ + matrix: 'rgb', + primaries: 'bt709', + transfer: 'iec61966-2-1', + fullRange: true, + }); + } else { + throw new TypeError('Invalid data type: Must be a BufferSource or CanvasImageSource.'); + } } - clone(options?: { - timestamp?: number; - duration?: number; - }) { - if (options !== undefined && (!options || typeof options !== 'object')) { - throw new TypeError('options, when provided, must be an object.'); - } - if (options?.timestamp !== undefined && !Number.isFinite(options.timestamp)) { - throw new TypeError('options.timestamp, when provided, must be a number.'); - } - if (options?.duration !== undefined && !Number.isFinite(options.duration)) { - throw new TypeError('options.duration, when provided, must be a number.'); + clone() { + if (this._closed) { + throw new Error('VideoSample is closed.'); } - return new EncodedVideoSample( - this.data, - this.type, - options?.timestamp ?? this.timestamp, - options?.duration ?? this.duration, - this.byteLength, + assert(this._data !== null); + + if (isVideoFrame(this._data)) { + return new VideoSample(this._data.clone()); + } else if (this._data instanceof Uint8Array) { + return new VideoSample(this._data.slice(), { + format: this.format!, + codedWidth: this.codedWidth, + codedHeight: this.codedHeight, + timestamp: this.timestamp, + duration: this.duration, + colorSpace: this.colorSpace, + }); + } else { + return new VideoSample(this._data, { + format: this.format!, + codedWidth: this.codedWidth, + codedHeight: this.codedHeight, + timestamp: this.timestamp, + duration: this.duration, + colorSpace: this.colorSpace, + }); + } + } + + close() { + if (this._closed) { + return; + } + + if (isVideoFrame(this._data)) { + this._data.close(); + } else { + this._data = null; // GC that shit + } + + this._closed = true; + } + + allocationSize() { + if (this._closed) { + throw new Error('VideoSample is closed.'); + } + + assert(this._data !== null); + + if (isVideoFrame(this._data)) { + return this._data.allocationSize(); + } else if (this._data instanceof Uint8Array) { + return this._data.byteLength; + } else { + return this.codedWidth * this.codedHeight * 4; // RGBX + } + } + + async copyTo(destination: AllowSharedBufferSource) { + if (!isAllowSharedBufferSource(destination)) { + throw new TypeError('destination must be an ArrayBuffer or an ArrayBuffer view.'); + } + + if (this._closed) { + throw new Error('VideoSample is closed.'); + } + + assert(this._data !== null); + + if (isVideoFrame(this._data)) { + await this._data.copyTo(destination); + } else if (this._data instanceof Uint8Array) { + const dest = toUint8Array(destination); + dest.set(this._data); + } else { + const canvas = this._data; + const context = canvas.getContext('2d', { alpha: false }); + assert(context); + + const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight); + const dest = toUint8Array(destination); + dest.set(imageData.data); + } + } + + toVideoFrame() { + if (this._closed) { + throw new Error('VideoSample is closed.'); + } + + assert(this._data !== null); + + if (isVideoFrame(this._data)) { + return new VideoFrame(this._data, { + timestamp: this.microsecondTimestamp, + duration: this.microsecondDuration || undefined, // Drag 0 duration to undefined, glitches some codecs + }); + } else if (this._data instanceof Uint8Array) { + return new VideoFrame(this._data, { + format: this.format!, + codedWidth: this.codedWidth, + codedHeight: this.codedHeight, + timestamp: this.microsecondTimestamp, + duration: this.microsecondDuration, + colorSpace: this.colorSpace, + }); + } else { + return new VideoFrame(this._data, { + timestamp: this.microsecondTimestamp, + duration: this.microsecondDuration, + }); + } + } + + draw( + context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, + dx: number, + dy: number, + dWidth: number = this.displayWidth, + dHeight: number = this.displayHeight, + ) { + if (!( + (typeof CanvasRenderingContext2D !== 'undefined' && context instanceof CanvasRenderingContext2D) + || ( + typeof OffscreenCanvasRenderingContext2D !== 'undefined' + && context instanceof OffscreenCanvasRenderingContext2D + ) + )) { + throw new TypeError('context must be a CanvasRenderingContext2D or OffscreenCanvasRenderingContext2D.'); + } + if (!Number.isFinite(dx)) { + throw new TypeError('dx must be a number.'); + } + if (!Number.isFinite(dy)) { + throw new TypeError('dy must be a number.'); + } + if (!Number.isFinite(dWidth) || dWidth < 0) { + throw new TypeError('dWidth must be a non-negative number.'); + } + if (!Number.isFinite(dHeight) || dHeight < 0) { + throw new TypeError('dHeight must be a non-negative number.'); + } + + if (this._closed) { + throw new Error('VideoSample is closed.'); + } + + const source = this.toCanvasImageSource(); + + context.save(); + + const centerX = dx + dWidth / 2; + const centerY = dy + dHeight / 2; + + context.translate(centerX, centerY); + context.rotate(this.rotation * Math.PI / 180); + + const aspectRatioChange = this.rotation % 180 === 0 ? 1 : dWidth / dHeight; + + // Scale to compensate for aspect ratio changes when rotated + context.scale(1 / aspectRatioChange, aspectRatioChange); + + context.drawImage( + source, + -dWidth / 2, + -dHeight / 2, + dWidth, + dHeight, ); + + // Restore the previous transformation state + context.restore(); } - static fromEncodedVideoChunk(chunk: EncodedVideoChunk) { - if (!(chunk instanceof EncodedVideoChunk)) { - throw new TypeError('chunk must be an EncodedVideoChunk.'); + toCanvasImageSource() { + if (this._closed) { + throw new Error('VideoSample is closed.'); } - const data = new Uint8Array(chunk.byteLength); - chunk.copyTo(data); + assert(this._data !== null); - return new EncodedVideoSample( - data, - chunk.type, - chunk.timestamp / 1e6, - (chunk.duration ?? 0) / 1e6, - ); + if (this._data instanceof Uint8Array) { + // Requires VideoFrame to be defined + return this.toVideoFrame(); + } else { + return this._data; + } + } + + setRotation(newRotation: Rotation) { + if (![0, 90, 180, 270].includes(newRotation)) { + throw new TypeError('newRotation must be 0, 90, 180, or 270.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (this.rotation as Rotation) = newRotation; + } + + setTimestamp(newTimestamp: number) { + if (!Number.isFinite(newTimestamp)) { + throw new TypeError('newTimestamp must be a number.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (this.timestamp as number) = newTimestamp; + } + + setDuration(newDuration: number) { + if (!Number.isFinite(newDuration) || newDuration < 0) { + throw new TypeError('newDuration must be a non-negative number.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (this.duration as number) = newDuration; } } +const isVideoFrame = (x: unknown): x is VideoFrame => { + return typeof VideoFrame !== 'undefined' && x instanceof VideoFrame; +}; + +const AUDIO_SAMPLE_FORMATS = new Set( + ['f32', 'f32-planar', 's16', 's16-planar', 's32', 's32-planar', 'u8', 'u8-planar'], +); + /** @public */ -export class EncodedAudioSample { - constructor( - public readonly data: Uint8Array, - public readonly type: SampleType, - public readonly timestamp: number, - public readonly duration: number, - public readonly sequenceNumber = -1, - public readonly byteLength = data.byteLength, - ) { - if (!(data instanceof Uint8Array)) { - throw new TypeError('data must be a Uint8Array.'); - } - if (type !== 'key' && type !== 'delta') { - throw new TypeError('type must be either "key" or "delta".'); - } - if (!Number.isFinite(timestamp)) { - throw new TypeError('timestamp must be a number.'); - } - if (!Number.isFinite(duration) || duration < 0) { - throw new TypeError('duration must be a non-negative number.'); - } - if (!Number.isInteger(byteLength) || byteLength < 0) { - throw new TypeError('byteLength must be a non-negative integer.'); - } - } +export type AudioSampleInit = { + data: AllowSharedBufferSource; + format: AudioSampleFormat; + numberOfChannels: number; + sampleRate: number; + timestamp: number; +}; - get isMetadataOnly() { - return this.data === PLACEHOLDER_DATA; - } +/** @public */ +export class AudioSample { + /** @internal */ + _data: AudioData | Uint8Array; + /** @internal */ + _closed: boolean = false; + + readonly format: AudioSampleFormat; + readonly sampleRate: number; + readonly numberOfFrames: number; + readonly numberOfChannels: number; + readonly duration: number; + readonly timestamp: number; get microsecondTimestamp() { - return Math.floor(1e6 * this.timestamp); + return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.timestamp); } get microsecondDuration() { - return Math.floor(1e6 * this.duration); + return Math.trunc(SECOND_TO_MICROSECOND_FACTOR * this.duration); } - toEncodedAudioChunk() { - if (this.isMetadataOnly) { - throw new TypeError('Metadata-only samples cannot be converted to a chunk.'); - } + constructor(init: AudioData | AudioSampleInit) { + if (isAudioData(init)) { + if (init.format === null) { + throw new TypeError('AudioData with null format is not supported.'); + } - if (typeof EncodedAudioChunk === 'undefined') { - throw new Error('Your browser does not support EncodedAudioChunk.'); - } + this._data = init; - return new EncodedAudioChunk({ - data: this.data, - type: this.type, - timestamp: this.microsecondTimestamp, - duration: this.microsecondDuration, - }); + this.format = init.format; + this.sampleRate = init.sampleRate; + this.numberOfFrames = init.numberOfFrames; + this.numberOfChannels = init.numberOfChannels; + this.timestamp = init.timestamp / 1e6; + this.duration = init.numberOfFrames / init.sampleRate; + } else { + if (!init || typeof init !== 'object') { + throw new TypeError('Invalid AudioDataInit: must be an object.'); + } + + if (!AUDIO_SAMPLE_FORMATS.has(init.format)) { + throw new TypeError('Invalid AudioDataInit: invalid format.'); + } + if (!Number.isFinite(init.sampleRate) || init.sampleRate <= 0) { + throw new TypeError('Invalid AudioDataInit: sampleRate must be > 0.'); + } + if (!Number.isInteger(init.numberOfChannels) || init.numberOfChannels === 0) { + throw new TypeError('Invalid AudioDataInit: numberOfChannels must be an integer > 0.'); + } + if (!Number.isFinite(init?.timestamp)) { + throw new TypeError('init.timestamp must be a number.'); + } + + const numberOfFrames + = init.data.byteLength / (getBytesPerSample(init.format) * init.numberOfChannels); + if (!Number.isInteger(numberOfFrames)) { + throw new TypeError('Invalid AudioDataInit: data size is not a multiple of frame size.'); + } + + this.format = init.format; + this.sampleRate = init.sampleRate; + this.numberOfFrames = numberOfFrames; + this.numberOfChannels = init.numberOfChannels; + this.timestamp = init.timestamp; + this.duration = numberOfFrames / init.sampleRate; + + let dataBuffer: Uint8Array; + if (init.data instanceof ArrayBuffer) { + dataBuffer = new Uint8Array(init.data); + } else if (ArrayBuffer.isView(init.data)) { + dataBuffer = new Uint8Array(init.data.buffer, init.data.byteOffset, init.data.byteLength); + } else { + throw new TypeError('Invalid AudioDataInit: data is not a BufferSource.'); + } + + const expectedSize + = this.numberOfFrames * this.numberOfChannels * getBytesPerSample(this.format); + if (dataBuffer.byteLength < expectedSize) { + throw new TypeError('Invalid AudioDataInit: insufficient data size.'); + } + + this._data = dataBuffer; + } } - clone(options?: { - timestamp?: number; - duration?: number; + allocationSize(options: { + planeIndex: number; + format?: AudioSampleFormat; + frameOffset?: number; + frameCount?: number; }) { - if (options !== undefined && (!options || typeof options !== 'object')) { - throw new TypeError('options, when provided, must be an object.'); + if (!options || typeof options !== 'object') { + throw new TypeError('options must be an object.'); } - if (options?.timestamp !== undefined && !Number.isFinite(options.timestamp)) { - throw new TypeError('options.timestamp, when provided, must be a number.'); - } - if (options?.duration !== undefined && !Number.isFinite(options.duration)) { - throw new TypeError('options.duration, when provided, must be a number.'); + if (!Number.isInteger(options.planeIndex) || options.planeIndex < 0) { + throw new TypeError('planeIndex must be a non-negative integer.'); } - return new EncodedAudioSample( - this.data, - this.type, - options?.timestamp ?? this.timestamp, - options?.duration ?? this.duration, - this.byteLength, - ); + if (options.format !== undefined && !AUDIO_SAMPLE_FORMATS.has(options.format)) { + throw new TypeError('Invalid format.'); + } + if (options.frameOffset !== undefined && (!Number.isInteger(options.frameOffset) || options.frameOffset < 0)) { + throw new TypeError('frameOffset must be a non-negative integer.'); + } + if (options.frameCount !== undefined && (!Number.isInteger(options.frameCount) || options.frameCount < 0)) { + throw new TypeError('frameCount must be a non-negative integer.'); + } + + if (this._closed) { + throw new Error('AudioSample is closed.'); + } + + const destFormat = options.format ?? this.format; + + const frameOffset = options.frameOffset ?? 0; + if (frameOffset >= this.numberOfFrames) { + throw new RangeError('frameOffset out of range'); + } + + const copyFrameCount + = options.frameCount !== undefined ? options.frameCount : (this.numberOfFrames - frameOffset); + if (copyFrameCount > (this.numberOfFrames - frameOffset)) { + throw new RangeError('frameCount out of range'); + } + + const bytesPerSample = getBytesPerSample(destFormat); + const isPlanar = formatIsPlanar(destFormat); + if (isPlanar && options.planeIndex >= this.numberOfChannels) { + throw new RangeError('planeIndex out of range'); + } + if (!isPlanar && options.planeIndex !== 0) { + throw new RangeError('planeIndex out of range'); + } + + const elementCount = isPlanar ? copyFrameCount : copyFrameCount * this.numberOfChannels; + return elementCount * bytesPerSample; } - static fromEncodedAudioChunk(chunk: EncodedAudioChunk) { - if (!(chunk instanceof EncodedAudioChunk)) { - throw new TypeError('chunk must be an EncodedAudioChunk.'); + copyTo( + destination: AllowSharedBufferSource, + options: { + planeIndex: number; + format?: AudioSampleFormat; + frameOffset?: number; + frameCount?: number; + }, + ) { + if (!isAllowSharedBufferSource(destination)) { + throw new TypeError('destination must be an ArrayBuffer or an ArrayBuffer view.'); + } + if (!options || typeof options !== 'object') { + throw new TypeError('options must be an object.'); + } + if (!Number.isInteger(options.planeIndex) || options.planeIndex < 0) { + throw new TypeError('planeIndex must be a non-negative integer.'); } - const data = new Uint8Array(chunk.byteLength); - chunk.copyTo(data); + if (options.format !== undefined && !AUDIO_SAMPLE_FORMATS.has(options.format)) { + throw new TypeError('Invalid format.'); + } + if (options.frameOffset !== undefined && (!Number.isInteger(options.frameOffset) || options.frameOffset < 0)) { + throw new TypeError('frameOffset must be a non-negative integer.'); + } + if (options.frameCount !== undefined && (!Number.isInteger(options.frameCount) || options.frameCount < 0)) { + throw new TypeError('frameCount must be a non-negative integer.'); + } - return new EncodedAudioSample( - data, - chunk.type as SampleType, // Typing is weird - chunk.timestamp / 1e6, - (chunk.duration ?? 0) / 1e6, - ); + if (this._closed) { + throw new Error('AudioSample is closed.'); + } + + const { planeIndex, format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options; + + const destFormat = format ?? this.format; + if (!destFormat) throw new Error('Destination format not determined'); + + const numFrames = this.numberOfFrames; + const numChannels = this.numberOfChannels; + const frameOffset = optFrameOffset ?? 0; + if (frameOffset >= numFrames) { + throw new RangeError('frameOffset out of range'); + } + + const copyFrameCount = optFrameCount !== undefined ? optFrameCount : (numFrames - frameOffset); + if (copyFrameCount > (numFrames - frameOffset)) { + throw new RangeError('frameCount out of range'); + } + + const destBytesPerSample = getBytesPerSample(destFormat); + const destIsPlanar = formatIsPlanar(destFormat); + if (destIsPlanar && planeIndex >= numChannels) { + throw new RangeError('planeIndex out of range'); + } + if (!destIsPlanar && planeIndex !== 0) { + throw new RangeError('planeIndex out of range'); + } + + const destElementCount = destIsPlanar ? copyFrameCount : copyFrameCount * numChannels; + const requiredSize = destElementCount * destBytesPerSample; + if (destination.byteLength < requiredSize) { + throw new RangeError('Destination buffer is too small'); + } + + const destView = toDataView(destination); + const writeFn = getWriteFunction(destFormat); + + if (isAudioData(this._data)) { + if (destIsPlanar) { + if (destFormat === 'f32-planar') { + // Simple, since the browser must support f32-planar, we can just delegate here + this._data.copyTo(destination, { + planeIndex, + frameOffset, + frameCount: copyFrameCount, + format: 'f32-planar', + }); + } else { + // Allocate temporary buffer for f32-planar data + const tempBuffer = new ArrayBuffer(copyFrameCount * 4); + const tempArray = new Float32Array(tempBuffer); + this._data.copyTo(tempArray, { + planeIndex, + frameOffset, + frameCount: copyFrameCount, + format: 'f32-planar', + }); + + // Convert each f32 sample to destination format + const tempView = new DataView(tempBuffer); + for (let i = 0; i < copyFrameCount; i++) { + const destOffset = i * destBytesPerSample; + const sample = tempView.getFloat32(i * 4, true); + writeFn(destView, destOffset, sample); + } + } + } else { + // Destination is interleaved. + // Allocate a temporary Float32Array to hold one channel's worth of data. + const numCh = numChannels; + const temp = new Float32Array(copyFrameCount); + for (let ch = 0; ch < numCh; ch++) { + this._data.copyTo(temp, { + planeIndex: ch, + frameOffset, + frameCount: copyFrameCount, + format: 'f32-planar', + }); + for (let i = 0; i < copyFrameCount; i++) { + const destIndex = i * numCh + ch; + const destOffset = destIndex * destBytesPerSample; + writeFn(destView, destOffset, temp[i]!); + } + } + } + } else { + // Branch for Uint8Array data (non-AudioData) + const uint8Data = this._data; + const srcView = new DataView(uint8Data.buffer, uint8Data.byteOffset, uint8Data.byteLength); + + const srcFormat = this.format; + const readFn = getReadFunction(srcFormat); + const srcBytesPerSample = getBytesPerSample(srcFormat); + const srcIsPlanar = formatIsPlanar(srcFormat); + + for (let i = 0; i < copyFrameCount; i++) { + if (destIsPlanar) { + const destOffset = i * destBytesPerSample; + let srcOffset: number; + if (srcIsPlanar) { + srcOffset = (planeIndex * numFrames + (i + frameOffset)) * srcBytesPerSample; + } else { + srcOffset = (((i + frameOffset) * numChannels) + planeIndex) * srcBytesPerSample; + } + + const normalized = readFn(srcView, srcOffset); + writeFn(destView, destOffset, normalized); + } else { + for (let ch = 0; ch < numChannels; ch++) { + const destIndex = i * numChannels + ch; + const destOffset = destIndex * destBytesPerSample; + let srcOffset: number; + if (srcIsPlanar) { + srcOffset = (ch * numFrames + (i + frameOffset)) * srcBytesPerSample; + } else { + srcOffset = (((i + frameOffset) * numChannels) + ch) * srcBytesPerSample; + } + + const normalized = readFn(srcView, srcOffset); + writeFn(destView, destOffset, normalized); + } + } + } + } + } + + clone(): AudioSample { + if (this._closed) { + throw new Error('AudioSample is closed.'); + } + + if (isAudioData(this._data)) { + return new AudioSample(this._data.clone()); + } else { + return new AudioSample({ + + format: this.format, + sampleRate: this.sampleRate, + numberOfFrames: this.numberOfFrames, + numberOfChannels: this.numberOfChannels, + timestamp: this.timestamp, + data: this._data, + }); + } + } + + close(): void { + if (this._closed) { + return; + } + + if (isAudioData(this._data)) { + this._data.close(); + } else { + this._data = new Uint8Array(0); + } + + this._closed = true; + } + + toAudioData() { + if (this._closed) { + throw new Error('AudioSample is closed.'); + } + + if (isAudioData(this._data)) { + if (this._data.timestamp === this.microsecondTimestamp) { + // Timestamp matches, let's just return the data (but cloned) + return this._data.clone(); + } else { + // It's impossible to simply change an AudioData's timestamp, so we'll need to create a new one + if (formatIsPlanar(this.format)) { + const size = this.allocationSize({ planeIndex: 0, format: this.format }); + const data = new ArrayBuffer(size * this.numberOfChannels); + + // We gotta read out each plane individually + for (let i = 0; i < this.numberOfChannels; i++) { + this.copyTo(new Uint8Array(data, i * size, size), { planeIndex: i, format: this.format }); + } + + return new AudioData({ + + format: this.format, + sampleRate: this.sampleRate, + numberOfFrames: this.numberOfFrames, + numberOfChannels: this.numberOfChannels, + timestamp: this.microsecondTimestamp, + data, + }); + } else { + const data = new ArrayBuffer(this.allocationSize({ planeIndex: 0, format: this.format })); + + this.copyTo(new DataView(data), { planeIndex: 0, format: this.format }); + + return new AudioData({ + + format: this.format, + sampleRate: this.sampleRate, + numberOfFrames: this.numberOfFrames, + numberOfChannels: this.numberOfChannels, + timestamp: this.microsecondTimestamp, + data, + }); + } + } + } else { + return new AudioData({ + + format: this.format, + sampleRate: this.sampleRate, + numberOfFrames: this.numberOfFrames, + numberOfChannels: this.numberOfChannels, + timestamp: this.microsecondTimestamp, + data: this._data, + }); + } + } + + toAudioBuffer() { + if (this._closed) { + throw new Error('AudioSample is closed.'); + } + + const audioBuffer = new AudioBuffer({ + numberOfChannels: this.numberOfChannels, + length: this.numberOfFrames, + sampleRate: this.sampleRate, + }); + + const dataBytes = new Float32Array(this.allocationSize({ planeIndex: 0, format: 'f32-planar' }) / 4); + + for (let i = 0; i < this.numberOfChannels; i++) { + this.copyTo(dataBytes, { planeIndex: i, format: 'f32-planar' }); + audioBuffer.copyToChannel(dataBytes, i); + } + + return audioBuffer; + } + + setTimestamp(newTimestamp: number) { + if (!Number.isFinite(newTimestamp)) { + throw new TypeError('newTimestamp must be a number.'); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (this.timestamp as number) = newTimestamp; } } + +const getBytesPerSample = (format: AudioSampleFormat): number => { + switch (format) { + case 'u8': + case 'u8-planar': + return 1; + case 's16': + case 's16-planar': + return 2; + case 's32': + case 's32-planar': + return 4; + case 'f32': + case 'f32-planar': + return 4; + default: + throw new Error('Unknown AudioSampleFormat'); + } +}; + +const formatIsPlanar = (format: AudioSampleFormat): boolean => { + switch (format) { + case 'u8-planar': + case 's16-planar': + case 's32-planar': + case 'f32-planar': + return true; + default: + return false; + } +}; + +const getReadFunction = (format: AudioSampleFormat): (view: DataView, offset: number) => number => { + switch (format) { + case 'u8': + case 'u8-planar': + return (view, offset) => (view.getUint8(offset) - 128) / 128; + case 's16': + case 's16-planar': + return (view, offset) => view.getInt16(offset, true) / 32768; + case 's32': + case 's32-planar': + return (view, offset) => view.getInt32(offset, true) / 2147483648; + case 'f32': + case 'f32-planar': + return (view, offset) => view.getFloat32(offset, true); + } +}; + +const getWriteFunction = (format: AudioSampleFormat): (view: DataView, offset: number, value: number) => void => { + switch (format) { + case 'u8': + case 'u8-planar': + return (view, offset, value) => + view.setUint8(offset, clamp((value + 1) * 127.5, 0, 255)); + case 's16': + case 's16-planar': + return (view, offset, value) => + view.setInt16(offset, clamp(Math.round(value * 32767), -32768, 32767), true); + case 's32': + case 's32-planar': + return (view, offset, value) => + view.setInt32(offset, clamp(Math.round(value * 2147483647), -2147483648, 2147483647), true); + case 'f32': + case 'f32-planar': + return (view, offset, value) => view.setFloat32(offset, value, true); + } +}; + +const isAudioData = (x: unknown): x is AudioData => { + return typeof AudioData !== 'undefined' && x instanceof AudioData; +};