feat: Voice Gateway v8
This commit is contained in:
@@ -12,6 +12,7 @@ const proxy = new ProxyAgent({
|
|||||||
const client = new Discord.Client({
|
const client = new Discord.Client({
|
||||||
ws: {
|
ws: {
|
||||||
agent: proxy, // WebSocket Proxy
|
agent: proxy, // WebSocket Proxy
|
||||||
|
// Do not use the `proxy` option if you don't need to use the WebSocket Proxy
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
// API Proxy
|
// API Proxy
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ client.on('ready', async client => {
|
|||||||
const audio = connection.receiver.createStream('user_id', {
|
const audio = connection.receiver.createStream('user_id', {
|
||||||
mode: 'pcm',
|
mode: 'pcm',
|
||||||
end: 'manual',
|
end: 'manual',
|
||||||
|
paddingSilence: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.pipe(fs.createWriteStream('test.pcm'));
|
audio.pipe(fs.createWriteStream('test.pcm'));
|
||||||
|
|||||||
@@ -27,19 +27,13 @@ client.on('ready', async client => {
|
|||||||
|
|
||||||
const connectionStream = await connection.joinStreamConnection('user_id');
|
const connectionStream = await connection.joinStreamConnection('user_id');
|
||||||
|
|
||||||
const video = connectionStream.receiver.createVideoStream('user_id', {
|
const video = connectionStream.receiver.createVideoStream('user_id', fs.createWriteStream('video.mkv')); // Output file using matroska container
|
||||||
portUdp: 50004, // Temporary port
|
|
||||||
output: fs.createWriteStream('video.mkv'), // Output file using matroska container
|
|
||||||
// If you want video with audio, set isEnableAudio to true
|
|
||||||
isEnableAudio: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
video.stream.stderr.on('data', data => {
|
|
||||||
console.log(`FFmpeg: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
video.on('ready', () => {
|
video.on('ready', () => {
|
||||||
console.log('FFmpeg process ready!');
|
console.log('FFmpeg process ready!');
|
||||||
|
video.stream.stderr.on('data', data => {
|
||||||
|
console.log(`FFmpeg: ${data}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// After 15s
|
// After 15s
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"tough-cookie": "^4.1.4",
|
"tough-cookie": "^4.1.4",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"undici": "^6.21.0",
|
"undici": "^6.21.0",
|
||||||
|
"werift-rtp": "^0.8.4",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
118
pnpm-lock.yaml
generated
118
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
|||||||
undici:
|
undici:
|
||||||
specifier: ^6.21.0
|
specifier: ^6.21.0
|
||||||
version: 6.21.1
|
version: 6.21.1
|
||||||
|
werift-rtp:
|
||||||
|
specifier: ^0.8.4
|
||||||
|
version: 0.8.4
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.16.0
|
specifier: ^8.16.0
|
||||||
version: 8.18.0
|
version: 8.18.0
|
||||||
@@ -353,12 +356,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
'@definitelytyped/header-parser@0.2.16':
|
'@definitelytyped/header-parser@0.2.17':
|
||||||
resolution: {integrity: sha512-UFsgPft5bhZn07UNGz/9ck4AhdKgLFEOmi2DNr7gXcGL89zbe3u5oVafKUT8j1HOtSBjT8ZEQsXHKlbq+wwF/Q==}
|
resolution: {integrity: sha512-U0juKFkTOcbkSfO83WSzMEJHYDwoBFiq0tf/JszulL3+7UoSiqunpGmxXS54bm3eGqy7GWjV8AqPQHdeoEaWBQ==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
'@definitelytyped/typescript-versions@0.1.6':
|
'@definitelytyped/typescript-versions@0.1.7':
|
||||||
resolution: {integrity: sha512-gQpXFteIKrOw4ldmBZQfBrD3WobaIG1SwOr/3alXWkcYbkOWa2NRxQbiaYQ2IvYTGaZK26miJw0UOAFiuIs4gA==}
|
resolution: {integrity: sha512-sBzBi1SBn79OkSr8V0H+FzR7QumHk23syPyRxod/VRBrSkgN9rCliIe+nqLoWRAKN8EeKbp00ketnJNLZhucdA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
|
|
||||||
'@definitelytyped/utils@0.1.8':
|
'@definitelytyped/utils@0.1.8':
|
||||||
@@ -529,6 +532,9 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@minhducsun2002/leb128@1.0.0':
|
||||||
|
resolution: {integrity: sha512-eFrYUPDVHeuwWHluTG1kwNQUEUcFjVKYwPkU8z9DR1JH3AW7JtJsG9cRVGmwz809kKtGfwGJj58juCZxEvnI/g==}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -568,6 +574,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-0gRyXPH0hbmfZXwgvCH1z9H/kQwOlLeui86zKafZENpdjmuZznSkDFHRSUksr8Y5W/BxkqnR2WbNEfululh20Q==}
|
resolution: {integrity: sha512-0gRyXPH0hbmfZXwgvCH1z9H/kQwOlLeui86zKafZENpdjmuZznSkDFHRSUksr8Y5W/BxkqnR2WbNEfululh20Q==}
|
||||||
engines: {node: '>=v14.0.0'}
|
engines: {node: '>=v14.0.0'}
|
||||||
|
|
||||||
|
'@shinyoshiaki/binary-data@0.6.1':
|
||||||
|
resolution: {integrity: sha512-7HDb/fQAop2bCmvDIzU5+69i+UJaFgIVp99h1VzK1mpg1JwSODOkjbqD7ilTYnqlnadF8C4XjpwpepxDsGY6+w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
'@shinyoshiaki/jspack@0.0.6':
|
||||||
|
resolution: {integrity: sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg==}
|
||||||
|
|
||||||
'@sinclair/typebox@0.24.51':
|
'@sinclair/typebox@0.24.51':
|
||||||
resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==}
|
resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==}
|
||||||
|
|
||||||
@@ -703,6 +716,9 @@ packages:
|
|||||||
add-stream@1.0.0:
|
add-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
|
resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==}
|
||||||
|
|
||||||
|
aes-js@3.1.2:
|
||||||
|
resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==}
|
||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
@@ -898,6 +914,9 @@ packages:
|
|||||||
bare-events@2.5.4:
|
bare-events@2.5.4:
|
||||||
resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==}
|
resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==}
|
||||||
|
|
||||||
|
base64-js@1.5.1:
|
||||||
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
bcrypt-pbkdf@1.0.2:
|
bcrypt-pbkdf@1.0.2:
|
||||||
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
|
||||||
|
|
||||||
@@ -925,6 +944,9 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
builtin-modules@1.1.1:
|
builtin-modules@1.1.1:
|
||||||
resolution: {integrity: sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==}
|
resolution: {integrity: sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1736,6 +1758,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==}
|
resolution: {integrity: sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==}
|
||||||
deprecated: This package is no longer supported.
|
deprecated: This package is no longer supported.
|
||||||
|
|
||||||
|
generate-function@2.3.1:
|
||||||
|
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||||
|
|
||||||
gensync@1.0.0-beta.2:
|
gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1938,6 +1963,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
ignore@4.0.6:
|
ignore@4.0.6:
|
||||||
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
|
resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -2078,6 +2106,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-plain-object@2.0.4:
|
||||||
|
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-property@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
|
||||||
|
|
||||||
is-regex@1.1.4:
|
is-regex@1.1.4:
|
||||||
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2129,6 +2164,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
isobject@3.0.1:
|
||||||
|
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
isstream@0.1.2:
|
isstream@0.1.2:
|
||||||
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
|
resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==}
|
||||||
|
|
||||||
@@ -2655,6 +2694,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
mp4box@0.5.3:
|
||||||
|
resolution: {integrity: sha512-RIvyFZdPDIg3+mL6vUdPBSyQRrEfKO3ryAeJ4xJJV7HBHQUH3KfLlZRzfSpBHCd/HqR63HfbrWQI/CwXDvYENQ==}
|
||||||
|
|
||||||
ms@2.1.2:
|
ms@2.1.2:
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||||
|
|
||||||
@@ -3149,6 +3191,9 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
rx.mini@1.4.0:
|
||||||
|
resolution: {integrity: sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA==}
|
||||||
|
|
||||||
rxjs@6.6.7:
|
rxjs@6.6.7:
|
||||||
resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
|
resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
|
||||||
engines: {npm: '>=2.0.0'}
|
engines: {npm: '>=2.0.0'}
|
||||||
@@ -3784,6 +3829,10 @@ packages:
|
|||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
|
||||||
|
werift-rtp@0.8.4:
|
||||||
|
resolution: {integrity: sha512-n2FqQoSZnrS6ztMFkMMUee0ORh4JqdkEaaXwJ3NlemCoshcX3bfdKo4HukLwH2oBomfHFRIAvrqGRpo5JdYTzw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
@@ -4248,13 +4297,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
'@definitelytyped/header-parser@0.2.16':
|
'@definitelytyped/header-parser@0.2.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@definitelytyped/typescript-versions': 0.1.6
|
'@definitelytyped/typescript-versions': 0.1.7
|
||||||
'@definitelytyped/utils': 0.1.8
|
'@definitelytyped/utils': 0.1.8
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
|
|
||||||
'@definitelytyped/typescript-versions@0.1.6': {}
|
'@definitelytyped/typescript-versions@0.1.7': {}
|
||||||
|
|
||||||
'@definitelytyped/utils@0.1.8':
|
'@definitelytyped/utils@0.1.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4553,6 +4602,8 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
'@minhducsun2002/leb128@1.0.0': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -4603,6 +4654,13 @@ snapshots:
|
|||||||
|
|
||||||
'@sapphire/utilities@3.16.2': {}
|
'@sapphire/utilities@3.16.2': {}
|
||||||
|
|
||||||
|
'@shinyoshiaki/binary-data@0.6.1':
|
||||||
|
dependencies:
|
||||||
|
generate-function: 2.3.1
|
||||||
|
is-plain-object: 2.0.4
|
||||||
|
|
||||||
|
'@shinyoshiaki/jspack@0.0.6': {}
|
||||||
|
|
||||||
'@sinclair/typebox@0.24.51': {}
|
'@sinclair/typebox@0.24.51': {}
|
||||||
|
|
||||||
'@sinclair/typebox@0.27.8': {}
|
'@sinclair/typebox@0.27.8': {}
|
||||||
@@ -4735,6 +4793,8 @@ snapshots:
|
|||||||
|
|
||||||
add-stream@1.0.0: {}
|
add-stream@1.0.0: {}
|
||||||
|
|
||||||
|
aes-js@3.1.2: {}
|
||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.5(supports-color@9.4.0)
|
debug: 4.3.5(supports-color@9.4.0)
|
||||||
@@ -4960,6 +5020,8 @@ snapshots:
|
|||||||
bare-events@2.5.4:
|
bare-events@2.5.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
bcrypt-pbkdf@1.0.2:
|
bcrypt-pbkdf@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl: 0.14.5
|
tweetnacl: 0.14.5
|
||||||
@@ -4992,6 +5054,11 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer@6.0.3:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
builtin-modules@1.1.1: {}
|
builtin-modules@1.1.1: {}
|
||||||
|
|
||||||
builtins@1.0.3: {}
|
builtins@1.0.3: {}
|
||||||
@@ -5477,7 +5544,7 @@ snapshots:
|
|||||||
|
|
||||||
dts-critic@3.3.11(typescript@5.5.4):
|
dts-critic@3.3.11(typescript@5.5.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@definitelytyped/header-parser': 0.2.16
|
'@definitelytyped/header-parser': 0.2.17
|
||||||
command-exists: 1.2.9
|
command-exists: 1.2.9
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -5487,8 +5554,8 @@ snapshots:
|
|||||||
|
|
||||||
dtslint@4.2.1(typescript@5.5.4):
|
dtslint@4.2.1(typescript@5.5.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@definitelytyped/header-parser': 0.2.16
|
'@definitelytyped/header-parser': 0.2.17
|
||||||
'@definitelytyped/typescript-versions': 0.1.6
|
'@definitelytyped/typescript-versions': 0.1.7
|
||||||
'@definitelytyped/utils': 0.1.8
|
'@definitelytyped/utils': 0.1.8
|
||||||
dts-critic: 3.3.11(typescript@5.5.4)
|
dts-critic: 3.3.11(typescript@5.5.4)
|
||||||
fs-extra: 6.0.1
|
fs-extra: 6.0.1
|
||||||
@@ -6012,6 +6079,10 @@ snapshots:
|
|||||||
wide-align: 1.1.5
|
wide-align: 1.1.5
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
generate-function@2.3.1:
|
||||||
|
dependencies:
|
||||||
|
is-property: 1.0.2
|
||||||
|
|
||||||
gensync@1.0.0-beta.2: {}
|
gensync@1.0.0-beta.2: {}
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
@@ -6228,6 +6299,8 @@ snapshots:
|
|||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
ignore@4.0.6: {}
|
ignore@4.0.6: {}
|
||||||
|
|
||||||
ignore@5.3.1: {}
|
ignore@5.3.1: {}
|
||||||
@@ -6353,6 +6426,12 @@ snapshots:
|
|||||||
|
|
||||||
is-plain-obj@1.1.0: {}
|
is-plain-obj@1.1.0: {}
|
||||||
|
|
||||||
|
is-plain-object@2.0.4:
|
||||||
|
dependencies:
|
||||||
|
isobject: 3.0.1
|
||||||
|
|
||||||
|
is-property@1.0.2: {}
|
||||||
|
|
||||||
is-regex@1.1.4:
|
is-regex@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
@@ -6396,6 +6475,8 @@ snapshots:
|
|||||||
|
|
||||||
isexe@3.1.1: {}
|
isexe@3.1.1: {}
|
||||||
|
|
||||||
|
isobject@3.0.1: {}
|
||||||
|
|
||||||
isstream@0.1.2: {}
|
isstream@0.1.2: {}
|
||||||
|
|
||||||
istanbul-lib-coverage@3.2.2: {}
|
istanbul-lib-coverage@3.2.2: {}
|
||||||
@@ -7165,6 +7246,8 @@ snapshots:
|
|||||||
|
|
||||||
modify-values@1.0.1: {}
|
modify-values@1.0.1: {}
|
||||||
|
|
||||||
|
mp4box@0.5.3: {}
|
||||||
|
|
||||||
ms@2.1.2: {}
|
ms@2.1.2: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
@@ -7630,6 +7713,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
rx.mini@1.4.0: {}
|
||||||
|
|
||||||
rxjs@6.6.7:
|
rxjs@6.6.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 1.14.1
|
tslib: 1.14.1
|
||||||
@@ -8322,6 +8407,19 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
|
|
||||||
|
werift-rtp@0.8.4:
|
||||||
|
dependencies:
|
||||||
|
'@minhducsun2002/leb128': 1.0.0
|
||||||
|
'@shinyoshiaki/binary-data': 0.6.1
|
||||||
|
'@shinyoshiaki/jspack': 0.0.6
|
||||||
|
aes-js: 3.1.2
|
||||||
|
buffer: 6.0.3
|
||||||
|
debug: 4.3.5(supports-color@9.4.0)
|
||||||
|
mp4box: 0.5.3
|
||||||
|
rx.mini: 1.4.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ class VoiceStateUpdate extends Action {
|
|||||||
member = guild.members._add(data.member);
|
member = guild.members._add(data.member);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit event
|
|
||||||
if (member?.user.id === client.user.id) {
|
|
||||||
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
|
|
||||||
client.voice.onVoiceStateUpdate(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
|
* Emitted whenever a member changes voice state - e.g. joins/leaves a channel, mutes/unmutes.
|
||||||
* @event Client#voiceStateUpdate
|
* @event Client#voiceStateUpdate
|
||||||
@@ -43,14 +37,13 @@ class VoiceStateUpdate extends Action {
|
|||||||
|
|
||||||
const newState = client.voiceStates._add(data);
|
const newState = client.voiceStates._add(data);
|
||||||
|
|
||||||
|
client.emit(Events.VOICE_STATE_UPDATE, oldState, newState);
|
||||||
|
}
|
||||||
// Emit event
|
// Emit event
|
||||||
if (data.user_id === client.user.id) {
|
if (data.user_id === client.user?.id) {
|
||||||
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
|
client.emit('debug', `[VOICE] received voice state update: ${JSON.stringify(data)}`);
|
||||||
client.voice.onVoiceStateUpdate(data);
|
client.voice.onVoiceStateUpdate(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.emit(Events.VOICE_STATE_UPDATE, oldState, newState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,9 @@ class VoiceConnection extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Our current video state
|
* Our current video state
|
||||||
* @type {boolean}
|
* @type {boolean | null}
|
||||||
*/
|
*/
|
||||||
this.videoStatus = false;
|
this.videoStatus = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The authentication data needed to connect to the voice server
|
* The authentication data needed to connect to the voice server
|
||||||
@@ -98,7 +98,7 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* @event VoiceConnection#debug
|
* @event VoiceConnection#debug
|
||||||
* @param {string} message The debug message
|
* @param {string} message The debug message
|
||||||
*/
|
*/
|
||||||
this.emit('debug', `audio player - ${m}`);
|
this.emit('debug', `media player - ${m}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.player.on('error', e => {
|
this.player.on('error', e => {
|
||||||
@@ -233,24 +233,39 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* @param {boolean} value Video on or off
|
* @param {boolean} value Video on or off
|
||||||
*/
|
*/
|
||||||
setVideoStatus(value) {
|
setVideoStatus(value) {
|
||||||
if (this.status !== VoiceStatus.CONNECTED) return;
|
|
||||||
if (value === this.videoStatus) return;
|
if (value === this.videoStatus) return;
|
||||||
|
if (this.status !== VoiceStatus.CONNECTED) return;
|
||||||
this.videoStatus = value;
|
this.videoStatus = value;
|
||||||
|
if (!value) {
|
||||||
this.sockets.ws
|
this.sockets.ws
|
||||||
.sendPacket({
|
.sendPacket({
|
||||||
op: VoiceOpcodes.SOURCES,
|
op: VoiceOpcodes.SOURCES,
|
||||||
d: {
|
d: {
|
||||||
audio_ssrc: this.authentication.ssrc,
|
audio_ssrc: this.authentication.ssrc,
|
||||||
video_ssrc: value ? this.authentication.ssrc + 1 : 0,
|
video_ssrc: 0,
|
||||||
rtx_ssrc: value ? this.authentication.ssrc + 2 : 0,
|
rtx_ssrc: 0,
|
||||||
|
streams: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.emit('debug', e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.sockets.ws
|
||||||
|
.sendPacket({
|
||||||
|
op: VoiceOpcodes.SOURCES,
|
||||||
|
d: {
|
||||||
|
audio_ssrc: this.authentication.ssrc,
|
||||||
|
video_ssrc: this.authentication.ssrc + 1,
|
||||||
|
rtx_ssrc: this.authentication.ssrc + 2,
|
||||||
streams: [
|
streams: [
|
||||||
{
|
{
|
||||||
type: 'video',
|
type: 'video',
|
||||||
rid: '100',
|
rid: '100',
|
||||||
ssrc: value ? this.authentication.ssrc + 1 : 0,
|
ssrc: this.authentication.ssrc + 1,
|
||||||
active: true,
|
active: true,
|
||||||
quality: 100,
|
quality: 100,
|
||||||
rtx_ssrc: value ? this.authentication.ssrc + 2 : 0,
|
rtx_ssrc: this.authentication.ssrc + 2,
|
||||||
max_bitrate: 8000000,
|
max_bitrate: 8000000,
|
||||||
max_framerate: 60,
|
max_framerate: 60,
|
||||||
max_resolution: {
|
max_resolution: {
|
||||||
@@ -266,6 +281,7 @@ class VoiceConnection extends EventEmitter {
|
|||||||
this.emit('debug', e);
|
this.emit('debug', e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The voice state of this connection
|
* The voice state of this connection
|
||||||
@@ -926,9 +942,9 @@ class StreamConnection extends VoiceConnection {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream state
|
* Stream state
|
||||||
* @type {boolean}
|
* @type {boolean | null}
|
||||||
*/
|
*/
|
||||||
this.isPaused = false;
|
this.isPaused = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Viewer IDs
|
* Viewer IDs
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class AnnexBDispatcher extends VideoDispatcher {
|
|||||||
this._nalFunctions = nalFunctions;
|
this._nalFunctions = nalFunctions;
|
||||||
}
|
}
|
||||||
|
|
||||||
codecCallback(frame) {
|
_codecCallback(frame) {
|
||||||
let accessUnit = frame;
|
let accessUnit = frame;
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class AnnexBDispatcher extends VideoDispatcher {
|
|||||||
this._playChunk(Buffer.concat([this.createPayloadExtension(), nalu]), isLastNal);
|
this._playChunk(Buffer.concat([this.createPayloadExtension(), nalu]), isLastNal);
|
||||||
} else {
|
} else {
|
||||||
const [naluHeader, naluData] = this._nalFunctions.splitHeader(nalu);
|
const [naluHeader, naluData] = this._nalFunctions.splitHeader(nalu);
|
||||||
const dataFragments = this.partitionVideoData(naluData);
|
const dataFragments = this.partitionMtu(naluData);
|
||||||
// Send as Fragmentation Unit A (FU-A):
|
// Send as Fragmentation Unit A (FU-A):
|
||||||
for (let fragmentIndex = 0; fragmentIndex < dataFragments.length; fragmentIndex++) {
|
for (let fragmentIndex = 0; fragmentIndex < dataFragments.length; fragmentIndex++) {
|
||||||
const data = dataFragments[fragmentIndex];
|
const data = dataFragments[fragmentIndex];
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const Util = require('../../../util/Util');
|
|||||||
const Silence = require('../util/Silence');
|
const Silence = require('../util/Silence');
|
||||||
const VolumeInterface = require('../util/VolumeInterface');
|
const VolumeInterface = require('../util/VolumeInterface');
|
||||||
|
|
||||||
|
const CHANNELS = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @external WritableStream
|
* @external WritableStream
|
||||||
* @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable}
|
* @see {@link https://nodejs.org/api/stream.html#stream_class_stream_writable}
|
||||||
@@ -37,6 +39,22 @@ class AudioDispatcher extends BaseDispatcher {
|
|||||||
if (typeof plp !== 'undefined') this.setPLP(plp);
|
if (typeof plp !== 'undefined') this.setPLP(plp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get TIMESTAMP_INC() {
|
||||||
|
return 480 * CHANNELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
get FRAME_LENGTH() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of the dispatcher
|
||||||
|
* @returns {'audio'}
|
||||||
|
*/
|
||||||
|
getTypeDispatcher() {
|
||||||
|
return 'audio';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the bitrate of the current Opus encoder if using a compatible Opus stream.
|
* Set the bitrate of the current Opus encoder if using a compatible Opus stream.
|
||||||
* @param {number} value New bitrate, in kbps
|
* @param {number} value New bitrate, in kbps
|
||||||
@@ -103,6 +121,17 @@ class AudioDispatcher extends BaseDispatcher {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync with another video dispatcher to ensure that the audio and video are played at the same time.
|
||||||
|
* @param {VideoDispatcher} otherDispatcher The video dispatcher to sync with
|
||||||
|
*/
|
||||||
|
setSyncVideoDispatcher(otherDispatcher) {
|
||||||
|
if (otherDispatcher.getTypeDispatcher() !== 'video') {
|
||||||
|
throw new Error('Dispatcher must be a video dispatcher');
|
||||||
|
}
|
||||||
|
this._syncDispatcher = otherDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
// Volume stubs for docs
|
// Volume stubs for docs
|
||||||
/* eslint-disable no-empty-function*/
|
/* eslint-disable no-empty-function*/
|
||||||
get volumeDecibels() {}
|
get volumeDecibels() {}
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ const { Writable } = require('node:stream');
|
|||||||
const { setTimeout } = require('node:timers');
|
const { setTimeout } = require('node:timers');
|
||||||
const secretbox = require('../util/Secretbox');
|
const secretbox = require('../util/Secretbox');
|
||||||
|
|
||||||
const CHANNELS = 2;
|
|
||||||
|
|
||||||
const MAX_UINT_16 = 2 ** 16 - 1;
|
const MAX_UINT_16 = 2 ** 16 - 1;
|
||||||
const MAX_UINT_32 = 2 ** 32 - 1;
|
const MAX_UINT_32 = 2 ** 32 - 1;
|
||||||
|
|
||||||
const extensions = [{ id: 5, len: 2, val: 0 }];
|
const extensions = [{ id: 5, length: 2, value: 0 }];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @external WritableStream
|
* @external WritableStream
|
||||||
@@ -28,7 +26,7 @@ class BaseDispatcher extends Writable {
|
|||||||
});
|
});
|
||||||
this.streams = streams;
|
this.streams = streams;
|
||||||
/**
|
/**
|
||||||
* The Audio Player that controls this dispatcher
|
* The Player that controls this dispatcher
|
||||||
* @type {MediaPlayer}
|
* @type {MediaPlayer}
|
||||||
*/
|
*/
|
||||||
this.player = player;
|
this.player = player;
|
||||||
@@ -52,14 +50,6 @@ class BaseDispatcher extends Writable {
|
|||||||
this.sequence = 0;
|
this.sequence = 0;
|
||||||
this.timestamp = 0;
|
this.timestamp = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* Video FPS
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
this.fps = 0;
|
|
||||||
|
|
||||||
this.mtu = 1200;
|
|
||||||
|
|
||||||
const streamError = (type, err) => {
|
const streamError = (type, err) => {
|
||||||
/**
|
/**
|
||||||
* Emitted when the dispatcher encounters an error.
|
* Emitted when the dispatcher encounters an error.
|
||||||
@@ -86,6 +76,10 @@ class BaseDispatcher extends Writable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTypeDispatcher() {
|
||||||
|
return 'base';
|
||||||
|
}
|
||||||
|
|
||||||
resetNonceBuffer() {
|
resetNonceBuffer() {
|
||||||
this._nonceBuffer =
|
this._nonceBuffer =
|
||||||
this.player.voiceConnection.authentication.mode === 'aead_aes256_gcm_rtpsize'
|
this.player.voiceConnection.authentication.mode === 'aead_aes256_gcm_rtpsize'
|
||||||
@@ -93,25 +87,6 @@ class BaseDispatcher extends Writable {
|
|||||||
: Buffer.alloc(24);
|
: Buffer.alloc(24);
|
||||||
}
|
}
|
||||||
|
|
||||||
get TIMESTAMP_INC() {
|
|
||||||
return this.extensionEnabled ? 90000 / this.fps : 480 * CHANNELS;
|
|
||||||
}
|
|
||||||
|
|
||||||
get FRAME_LENGTH() {
|
|
||||||
return this.extensionEnabled ? 1000 / this.fps : 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
partitionVideoData(data) {
|
|
||||||
const out = [];
|
|
||||||
const dataLength = data.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < dataLength; i += this.mtu) {
|
|
||||||
out.push(data.slice(i, i + this.mtu));
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNewSequence() {
|
getNewSequence() {
|
||||||
const currentSeq = this.sequence;
|
const currentSeq = this.sequence;
|
||||||
this.sequence++;
|
this.sequence++;
|
||||||
@@ -128,8 +103,20 @@ class BaseDispatcher extends Writable {
|
|||||||
this.emit('start');
|
this.emit('start');
|
||||||
this.startTime = performance.now();
|
this.startTime = performance.now();
|
||||||
}
|
}
|
||||||
if (this.extensionEnabled) {
|
if (this._syncDispatcher && !this._syncDispatcher.startTime) {
|
||||||
this.codecCallback(chunk);
|
this.pause();
|
||||||
|
const cb = () => {
|
||||||
|
this.resume();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
this._syncDispatcher.once('start', cb);
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
this.removeListener('start', cb);
|
||||||
|
this.resume();
|
||||||
|
}, 10_000).unref();
|
||||||
|
}
|
||||||
|
if (this.getTypeDispatcher() === 'video') {
|
||||||
|
this._codecCallback(chunk);
|
||||||
} else {
|
} else {
|
||||||
this._playChunk(chunk);
|
this._playChunk(chunk);
|
||||||
}
|
}
|
||||||
@@ -166,8 +153,7 @@ class BaseDispatcher extends Writable {
|
|||||||
this.streams.ffmpeg.pause();
|
this.streams.ffmpeg.pause();
|
||||||
this.streams.video.unpipe(this);
|
this.streams.video.unpipe(this);
|
||||||
}
|
}
|
||||||
if (!this.extensionEnabled) {
|
if (this.getTypeDispatcher() === 'audio') {
|
||||||
// Audio
|
|
||||||
if (silence) {
|
if (silence) {
|
||||||
this.streams.silence.pipe(this);
|
this.streams.silence.pipe(this);
|
||||||
this._silence = true;
|
this._silence = true;
|
||||||
@@ -201,7 +187,7 @@ class BaseDispatcher extends Writable {
|
|||||||
*/
|
*/
|
||||||
resume() {
|
resume() {
|
||||||
if (!this.pausedSince) return;
|
if (!this.pausedSince) return;
|
||||||
if (!this.extensionEnabled) this.streams.silence.unpipe(this);
|
if (this.getTypeDispatcher() === 'audio') this.streams.silence.unpipe(this);
|
||||||
if (this.streams.opus) this.streams.opus.pipe(this);
|
if (this.streams.opus) this.streams.opus.pipe(this);
|
||||||
if (this.streams.video) {
|
if (this.streams.video) {
|
||||||
this.streams.ffmpeg.resume();
|
this.streams.ffmpeg.resume();
|
||||||
@@ -246,14 +232,15 @@ class BaseDispatcher extends Writable {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
_playChunk(chunk, isLastPacket) {
|
_playChunk(chunk, isLastPacket = false) {
|
||||||
if (
|
if (
|
||||||
(this.player.dispatcher !== this && this.player.videoDispatcher !== this) ||
|
(this.player.dispatcher !== this && this.player.videoDispatcher !== this) ||
|
||||||
!this.player.voiceConnection.authentication.secret_key
|
!this.player.voiceConnection.authentication.secret_key
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this[this.extensionEnabled ? '_sendVideoPacket' : '_sendPacket'](this._createPacket(chunk, isLastPacket));
|
const packet = this._createPacket(chunk, isLastPacket);
|
||||||
|
if (packet) this._sendPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -277,8 +264,9 @@ class BaseDispatcher extends Writable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a single extension of type playout-delay
|
* Creates a one-byte extension header & a single extension of type playout-delay
|
||||||
* Discord seems to send this extension on every video packet
|
* @see https://docs.discord.sex/topics/voice-connections#sending-and-receiving-voice
|
||||||
|
* Discord expects a playout delay RTP extension header on every video packet.
|
||||||
* @see https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/playout-delay
|
* @see https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/playout-delay
|
||||||
* @returns {Buffer} playout-delay extension <Buffer 51 00 00 00>
|
* @returns {Buffer} playout-delay extension <Buffer 51 00 00 00>
|
||||||
*/
|
*/
|
||||||
@@ -289,6 +277,9 @@ class BaseDispatcher extends Writable {
|
|||||||
* EXTENSION DATA - each extension payload is 32 bits
|
* EXTENSION DATA - each extension payload is 32 bits
|
||||||
*/
|
*/
|
||||||
const data = Buffer.alloc(4);
|
const data = Buffer.alloc(4);
|
||||||
|
|
||||||
|
// https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/playout-delay
|
||||||
|
if (ext.id === 5) {
|
||||||
/**
|
/**
|
||||||
* 0 1 2 3 4 5 6 7
|
* 0 1 2 3 4 5 6 7
|
||||||
+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+
|
||||||
@@ -296,16 +287,18 @@ class BaseDispatcher extends Writable {
|
|||||||
+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+
|
||||||
|
|
||||||
where len = actual length - 1
|
where len = actual length - 1
|
||||||
*/
|
/
|
||||||
data[0] = (ext.id & 0b00001111) << 4;
|
data[0] = (ext.id & 0b00001111) << 4;
|
||||||
data[0] |= (ext.len - 1) & 0b00001111;
|
data[0] |= (ext.len - 1) & 0b00001111;
|
||||||
|
|
||||||
/** Specific to type playout-delay
|
/** Specific to type playout-delay
|
||||||
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
|
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
| MIN delay | MAX delay |
|
| MIN delay | MAX delay |
|
||||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
*/
|
*/
|
||||||
data.writeUIntBE(ext.val, 1, 2); // Not quite but its 0 anyway
|
data.writeUIntBE(ext.value, 1, 2); // Not quite but its 0 anyway
|
||||||
|
}
|
||||||
extensionsData.push(data);
|
extensionsData.push(data);
|
||||||
}
|
}
|
||||||
return Buffer.concat(extensionsData);
|
return Buffer.concat(extensionsData);
|
||||||
@@ -352,24 +345,50 @@ class BaseDispatcher extends Writable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_createPacket(buffer, isLastPacket = false) {
|
_createPacket(buffer, isLastPacket) {
|
||||||
// Header
|
/*
|
||||||
const rtpHeader = this.extensionEnabled
|
// Packet is raw rtp from ffmpeg
|
||||||
? Buffer.concat([Buffer.alloc(12), this.createHeaderExtension()])
|
const rtp = webrtc.RtpPacket.deSerialize(buffer);
|
||||||
: Buffer.alloc(12); // RTP_HEADER_SIZE
|
if (!rtp.payload) {
|
||||||
rtpHeader[0] = this.extensionEnabled ? 0x90 : 0x80; // Version + Flags (1 byte)
|
console.log('no payload', rtp);
|
||||||
rtpHeader[1] = this.payloadType; // Payload Type (1 byte)
|
return null;
|
||||||
|
|
||||||
if (this.extensionEnabled) {
|
|
||||||
if (isLastPacket) {
|
|
||||||
rtpHeader[1] |= 0x80;
|
|
||||||
}
|
}
|
||||||
|
// Header
|
||||||
|
// https://docs.discord.sex/topics/voice-connections#rtp-packet-structure
|
||||||
|
let rtpHeader = buffer.slice(0, 12); // RTP_HEADER_SIZE
|
||||||
|
rtpHeader[0] = 0x80; // Version + Flags (1 byte)
|
||||||
|
rtpHeader[1] = this.payloadType; // Payload Type (1 byte)
|
||||||
|
if (this.extensionEnabled) {
|
||||||
|
rtpHeader = Buffer.concat([rtpHeader, this.createHeaderExtension()]);
|
||||||
|
rtpHeader[0] |= 1 << 4; // 0x90
|
||||||
|
}
|
||||||
|
rtpHeader.writeUIntBE(this.getNewSequence(), 2, 2);
|
||||||
|
rtpHeader.writeUIntBE(this.timestamp, 4, 4);
|
||||||
|
rtpHeader.writeUIntBE(
|
||||||
|
this.player.voiceConnection.authentication.ssrc + Number(this.getTypeDispatcher() === 'video'),
|
||||||
|
8,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
// Header
|
||||||
|
let rtpHeader = Buffer.alloc(12); // RTP_HEADER_SIZE
|
||||||
|
rtpHeader[0] = 0x80; // Version + Flags (1 byte)
|
||||||
|
rtpHeader[1] = this.payloadType; // Payload Type (1 byte)
|
||||||
|
if (this.extensionEnabled) {
|
||||||
|
rtpHeader = Buffer.concat([rtpHeader, this.createHeaderExtension()]);
|
||||||
|
rtpHeader[0] |= 1 << 4; // 0x90
|
||||||
|
}
|
||||||
|
if (this.getTypeDispatcher() === 'video' && isLastPacket) {
|
||||||
|
rtpHeader[1] |= 1 << 7; // Marker bit
|
||||||
}
|
}
|
||||||
|
|
||||||
rtpHeader.writeUIntBE(this.getNewSequence(), 2, 2);
|
rtpHeader.writeUIntBE(this.getNewSequence(), 2, 2);
|
||||||
rtpHeader.writeUIntBE(this.timestamp, 4, 4);
|
rtpHeader.writeUIntBE(this.timestamp, 4, 4);
|
||||||
rtpHeader.writeUIntBE(this.player.voiceConnection.authentication.ssrc + this.extensionEnabled, 8, 4);
|
rtpHeader.writeUIntBE(
|
||||||
|
this.player.voiceConnection.authentication.ssrc + Number(this.getTypeDispatcher() === 'video'),
|
||||||
|
8,
|
||||||
|
4,
|
||||||
|
);
|
||||||
return Buffer.concat([rtpHeader, ...this._encrypt(buffer, rtpHeader)]);
|
return Buffer.concat([rtpHeader, ...this._encrypt(buffer, rtpHeader)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,28 +398,24 @@ class BaseDispatcher extends Writable {
|
|||||||
* @event BaseDispatcher#debug
|
* @event BaseDispatcher#debug
|
||||||
* @param {string} info The debug info
|
* @param {string} info The debug info
|
||||||
*/
|
*/
|
||||||
|
if (this.getTypeDispatcher() === 'audio') {
|
||||||
this._setSpeaking(this.player.isScreenSharing ? 1 << 1 : 1 << 0); // 1 << 0 = SPEAKING, 1 << 1 = SOUND SHARE
|
this._setSpeaking(this.player.isScreenSharing ? 1 << 1 : 1 << 0); // 1 << 0 = SPEAKING, 1 << 1 = SOUND SHARE
|
||||||
|
} else if (this.getTypeDispatcher() === 'video') {
|
||||||
|
this._setVideoStatus(true);
|
||||||
|
this._setStreamStatus(false);
|
||||||
|
}
|
||||||
if (!this.player.voiceConnection.sockets.udp) {
|
if (!this.player.voiceConnection.sockets.udp) {
|
||||||
this.emit('debug', 'Failed to send a packet - no UDP socket');
|
this.emit('debug', 'Failed to send a packet - no UDP socket');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.player.voiceConnection.sockets.udp.send(packet).catch(e => {
|
this.player.voiceConnection.sockets.udp.send(packet).catch(e => {
|
||||||
this._setSpeaking(0);
|
if (this.getTypeDispatcher() === 'audio') {
|
||||||
this.emit('debug', `Failed to send a packet - ${e}`);
|
this._setSpeaking(this._setSpeaking(0));
|
||||||
});
|
} else if (this.getTypeDispatcher() === 'video') {
|
||||||
}
|
|
||||||
|
|
||||||
_sendVideoPacket(packet) {
|
|
||||||
this._setVideoStatus(true);
|
|
||||||
this._setStreamStatus(false);
|
|
||||||
if (!this.player.voiceConnection.sockets.udp) {
|
|
||||||
this.emit('debug', 'Failed to send a video packet - no UDP socket');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.player.voiceConnection.sockets.udp.send(packet).catch(e => {
|
|
||||||
this._setVideoStatus(false);
|
this._setVideoStatus(false);
|
||||||
this._setStreamStatus(true);
|
this._setStreamStatus(true);
|
||||||
this.emit('debug', `Failed to send a video packet - ${e}`);
|
}
|
||||||
|
this.emit('debug', `Failed to send a packet - ${e}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,25 +26,25 @@ class VP8Dispatcher extends VideoDispatcher {
|
|||||||
super(player, highWaterMark, streams, fps, Util.getPayloadType('VP8'));
|
super(player, highWaterMark, streams, fps, Util.getPayloadType('VP8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
makeChunk(buffer, isFirstFrame) {
|
makeChunk(buffer, isFirstPacket) {
|
||||||
// Vp8 payload descriptor
|
// Vp8 payload descriptor
|
||||||
const payloadDescriptorBuf = Buffer.alloc(2);
|
const payloadDescriptorBuf = Buffer.alloc(2);
|
||||||
payloadDescriptorBuf[0] = isFirstFrame ? 0x90 : 0x80; // Mark S bit, indicates start of frame: payloadDescriptorBuf[0] |= 0b00010000;
|
payloadDescriptorBuf[0] = 0x80;
|
||||||
payloadDescriptorBuf[1] = 0x80;
|
payloadDescriptorBuf[1] = 0x80;
|
||||||
|
if (isFirstPacket) {
|
||||||
|
payloadDescriptorBuf[0] |= 1 << 4; // Mark S bit, indicates start of frame
|
||||||
|
}
|
||||||
// Vp8 pictureid payload extension
|
// Vp8 pictureid payload extension
|
||||||
const pictureIdBuf = Buffer.alloc(2);
|
const pictureIdBuf = Buffer.alloc(2);
|
||||||
pictureIdBuf.writeUintBE(this.count, 0, 2);
|
pictureIdBuf.writeUintBE(this.count, 0, 2);
|
||||||
pictureIdBuf[0] |= 0x80;
|
pictureIdBuf[0] |= 0x80;
|
||||||
return Buffer.concat([payloadDescriptorBuf, pictureIdBuf, buffer]);
|
return Buffer.concat([this.createPayloadExtension(), payloadDescriptorBuf, pictureIdBuf, buffer]);
|
||||||
}
|
}
|
||||||
|
|
||||||
codecCallback(chunk) {
|
_codecCallback(chunk) {
|
||||||
const chunkSplit = this.partitionVideoData(chunk);
|
const chunkSplit = this.partitionMtu(chunk).map((c, i) => this.makeChunk(c, i === 0));
|
||||||
for (let i = 0; i < chunkSplit.length; i++) {
|
for (let i = 0; i < chunkSplit.length; i++) {
|
||||||
this._playChunk(
|
this._playChunk(chunkSplit[i], i + 1 === chunkSplit.length);
|
||||||
Buffer.concat([this.createPayloadExtension(), this.makeChunk(chunkSplit[i], i == 0)]),
|
|
||||||
i + 1 === chunkSplit.length,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,40 @@ const BaseDispatcher = require('./BaseDispatcher');
|
|||||||
class VideoDispatcher extends BaseDispatcher {
|
class VideoDispatcher extends BaseDispatcher {
|
||||||
constructor(player, highWaterMark = 12, streams, fps, payloadType) {
|
constructor(player, highWaterMark = 12, streams, fps, payloadType) {
|
||||||
super(player, highWaterMark, payloadType, true, streams);
|
super(player, highWaterMark, payloadType, true, streams);
|
||||||
|
/**
|
||||||
|
* Video FPS
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
this.fps = fps;
|
this.fps = fps;
|
||||||
|
|
||||||
|
this.mtu = 1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
get TIMESTAMP_INC() {
|
||||||
|
return 90000 / this.fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
get FRAME_LENGTH() {
|
||||||
|
return 1000 / this.fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of the dispatcher
|
||||||
|
* @returns {'video'}
|
||||||
|
*/
|
||||||
|
getTypeDispatcher() {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
|
||||||
|
partitionMtu(data) {
|
||||||
|
const out = [];
|
||||||
|
const dataLength = data.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < dataLength; i += this.mtu) {
|
||||||
|
out.push(data.slice(i, i + this.mtu));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +59,10 @@ class VideoDispatcher extends BaseDispatcher {
|
|||||||
setFPSSource(value) {
|
setFPSSource(value) {
|
||||||
this.fps = value;
|
this.fps = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_codecCallback() {
|
||||||
|
throw new Error('The _codecCallback method must be implemented');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = VideoDispatcher;
|
module.exports = VideoDispatcher;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class VoiceWebSocket extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this.attempts = 0;
|
this.attempts = 0;
|
||||||
|
|
||||||
|
this._sequenceNumber = -1;
|
||||||
|
|
||||||
this.dead = false;
|
this.dead = false;
|
||||||
this.connection.on('closing', this.shutdown.bind(this));
|
this.connection.on('closing', this.shutdown.bind(this));
|
||||||
}
|
}
|
||||||
@@ -75,7 +77,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||||||
* The actual WebSocket used to connect to the Voice WebSocket Server.
|
* The actual WebSocket used to connect to the Voice WebSocket Server.
|
||||||
* @type {WebSocket}
|
* @type {WebSocket}
|
||||||
*/
|
*/
|
||||||
this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 7 });
|
this.ws = WebSocket.create(`wss://${this.connection.authentication.endpoint}/`, { v: 8 });
|
||||||
this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`);
|
this.emit('debug', `[WS] connecting, ${this.attempts} attempts, ${this.ws.url}`);
|
||||||
this.ws.onopen = this.onOpen.bind(this);
|
this.ws.onopen = this.onOpen.bind(this);
|
||||||
this.ws.onmessage = this.onMessage.bind(this);
|
this.ws.onmessage = this.onMessage.bind(this);
|
||||||
@@ -144,9 +146,10 @@ class VoiceWebSocket extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Called whenever the connection to the WebSocket server is lost.
|
* Called whenever the connection to the WebSocket server is lost.
|
||||||
|
* @param {CloseEvent} event The WebSocket close event
|
||||||
*/
|
*/
|
||||||
onClose() {
|
onClose(event) {
|
||||||
this.emit('debug', `[WS] closed`);
|
this.emit('debug', `[WS] closed with code ${event.code} and reason: ${event.reason}`);
|
||||||
if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
|
if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +168,7 @@ class VoiceWebSocket extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
onPacket(packet) {
|
onPacket(packet) {
|
||||||
this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
|
this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
|
||||||
|
if (packet.seq) this._sequenceNumber = packet.seq;
|
||||||
switch (packet.op) {
|
switch (packet.op) {
|
||||||
case VoiceOpcodes.HELLO:
|
case VoiceOpcodes.HELLO:
|
||||||
this.setHeartbeat(packet.d.heartbeat_interval);
|
this.setHeartbeat(packet.d.heartbeat_interval);
|
||||||
@@ -266,7 +270,13 @@ class VoiceWebSocket extends EventEmitter {
|
|||||||
* Sends a heartbeat packet.
|
* Sends a heartbeat packet.
|
||||||
*/
|
*/
|
||||||
sendHeartbeat() {
|
sendHeartbeat() {
|
||||||
this.sendPacket({ op: VoiceOpcodes.HEARTBEAT, d: Math.floor(Math.random() * 10e10) }).catch(() => {
|
this.sendPacket({
|
||||||
|
op: VoiceOpcodes.HEARTBEAT,
|
||||||
|
d: {
|
||||||
|
t: Date.now(),
|
||||||
|
seq_ack: this._sequenceNumber,
|
||||||
|
},
|
||||||
|
}).catch(() => {
|
||||||
this.emit('warn', 'Tried to send heartbeat, but connection is not open');
|
this.emit('warn', 'Tried to send heartbeat, but connection is not open');
|
||||||
this.clearHeartbeat();
|
this.clearHeartbeat();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Please use the @dank074/discord-video-stream library for the best support.
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const { Readable: ReadableStream } = require('stream');
|
const { Readable: ReadableStream } = require('stream');
|
||||||
const prism = require('prism-media');
|
const prism = require('prism-media');
|
||||||
const { H264NalSplitter } = require('./processing/AnnexBNalSplitter');
|
const { H264NalSplitter, H265NalSplitter } = require('./processing/AnnexBNalSplitter');
|
||||||
const { IvfTransformer } = require('./processing/IvfSplitter');
|
const { IvfTransformer } = require('./processing/IvfSplitter');
|
||||||
const { H264Dispatcher } = require('../dispatcher/AnnexBDispatcher');
|
const { H264Dispatcher } = require('../dispatcher/AnnexBDispatcher');
|
||||||
const AudioDispatcher = require('../dispatcher/AudioDispatcher');
|
const AudioDispatcher = require('../dispatcher/AudioDispatcher');
|
||||||
@@ -62,6 +62,19 @@ const FFMPEG_H264_ARGUMENTS = options => [
|
|||||||
'h264_metadata=aud=insert',
|
'h264_metadata=aud=insert',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const FFMPEG_H265_ARGUMENTS = options => [
|
||||||
|
'-c:v',
|
||||||
|
'libx265',
|
||||||
|
'-f',
|
||||||
|
'hevc',
|
||||||
|
'-preset',
|
||||||
|
options?.presetH265 || 'faster',
|
||||||
|
'-profile:v',
|
||||||
|
'main',
|
||||||
|
'-bf',
|
||||||
|
'0',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player for a Voice Connection.
|
* Player for a Voice Connection.
|
||||||
* @private
|
* @private
|
||||||
@@ -188,15 +201,19 @@ class MediaPlayer extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get stream type
|
// Get stream type
|
||||||
if (this.voiceConnection.videoCodec == 'VP8') {
|
if (this.voiceConnection.videoCodec === 'VP8') {
|
||||||
args.push(...FFMPEG_VP8_ARGUMENTS);
|
args.push(...FFMPEG_VP8_ARGUMENTS);
|
||||||
// Remove '-speed', '5' bc bad quality
|
// Remove '-speed', '5' bc bad quality
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.voiceConnection.videoCodec == 'H264') {
|
if (this.voiceConnection.videoCodec === 'H264') {
|
||||||
args.push(...FFMPEG_H264_ARGUMENTS(options));
|
args.push(...FFMPEG_H264_ARGUMENTS(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.voiceConnection.videoCodec === 'H265') {
|
||||||
|
args.push(...FFMPEG_H265_ARGUMENTS(options));
|
||||||
|
}
|
||||||
|
|
||||||
args.push('-force_key_frames', '00:02');
|
args.push('-force_key_frames', '00:02');
|
||||||
|
|
||||||
if (options?.inputFFmpegArgs) {
|
if (options?.inputFFmpegArgs) {
|
||||||
@@ -229,7 +246,7 @@ class MediaPlayer extends EventEmitter {
|
|||||||
return this.playAnnexBVideo(ffmpeg, options, streams, 'H264');
|
return this.playAnnexBVideo(ffmpeg, options, streams, 'H264');
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Invalid codec (Supported: VP8, H264)');
|
throw new Error('Invalid codec (Supported: VP8, H264, H265)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,7 +264,12 @@ class MediaPlayer extends EventEmitter {
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
playAnnexBVideo(stream, options, streams, type) {
|
playAnnexBVideo(stream, options, streams, type) {
|
||||||
this.destroyVideoDispatcher();
|
this.destroyVideoDispatcher();
|
||||||
const videoStream = new H264NalSplitter();
|
let videoStream;
|
||||||
|
if (type === 'H264') {
|
||||||
|
videoStream = new H264NalSplitter();
|
||||||
|
} else if (type === 'H265') {
|
||||||
|
videoStream = new H265NalSplitter();
|
||||||
|
}
|
||||||
stream.pipe(videoStream);
|
stream.pipe(videoStream);
|
||||||
streams.video = videoStream;
|
streams.video = videoStream;
|
||||||
const dispatcher = this.createVideoDispatcher(options, streams);
|
const dispatcher = this.createVideoDispatcher(options, streams);
|
||||||
|
|||||||
37
src/client/voice/player/processing/PCMInsertSilence.js
Normal file
37
src/client/voice/player/processing/PCMInsertSilence.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Buffer } = require('node:buffer');
|
||||||
|
const { Transform } = require('node:stream');
|
||||||
|
|
||||||
|
class PCMInsertSilence extends Transform {
|
||||||
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
// 48Khz, 2 channels, 16-bit (2 bytes per channel)
|
||||||
|
this.sampleRate = 48000;
|
||||||
|
this.channels = 2;
|
||||||
|
// 4 bytes per frame (2 channels * 2 bytes)
|
||||||
|
this.bytesPerFrame = this.channels * 2;
|
||||||
|
this.lastChunkTime = Date.now();
|
||||||
|
this.silenceThresholdMs = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
_transform(chunk, encoding, callback) {
|
||||||
|
const now = Date.now();
|
||||||
|
const gap = now - this.lastChunkTime;
|
||||||
|
|
||||||
|
if (gap >= this.silenceThresholdMs) {
|
||||||
|
const missingFrames = Math.floor((gap / 1000) * this.sampleRate);
|
||||||
|
const silenceBuffer = Buffer.alloc(missingFrames * this.bytesPerFrame, 0);
|
||||||
|
this.push(silenceBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastChunkTime = now;
|
||||||
|
|
||||||
|
this.push(chunk);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PCMInsertSilence,
|
||||||
|
};
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const { Buffer } = require('node:buffer');
|
const { Buffer } = require('node:buffer');
|
||||||
const crypto = require('node:crypto');
|
const crypto = require('node:crypto');
|
||||||
const { nextTick } = require('node:process');
|
|
||||||
const { setTimeout } = require('node:timers');
|
const { setTimeout } = require('node:timers');
|
||||||
const FFmpegHandler = require('./FFmpegHandler');
|
const { RtpPacket } = require('werift-rtp');
|
||||||
|
const Recorder = require('./Recorder');
|
||||||
const Speaking = require('../../../util/Speaking');
|
const Speaking = require('../../../util/Speaking');
|
||||||
|
const Util = require('../../../util/Util');
|
||||||
const secretbox = require('../util/Secretbox');
|
const secretbox = require('../util/Secretbox');
|
||||||
const { SILENCE_FRAME } = require('../util/Silence');
|
const { SILENCE_FRAME } = require('../util/Silence');
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ const { SILENCE_FRAME } = require('../util/Silence');
|
|||||||
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
|
// https://github.com/discordjs/discord.js/issues/3524#issuecomment-540373200
|
||||||
const DISCORD_SPEAKING_DELAY = 250;
|
const DISCORD_SPEAKING_DELAY = 250;
|
||||||
|
|
||||||
const HEADER_EXTENSION_BYTE = Buffer.from([0xbe, 0xde]);
|
// Unused: const HEADER_EXTENSION_BYTE = Buffer.from([0xbe, 0xde]);
|
||||||
const UNPADDED_NONCE_LENGTH = 4;
|
const UNPADDED_NONCE_LENGTH = 4;
|
||||||
const AUTH_TAG_LENGTH = 16;
|
const AUTH_TAG_LENGTH = 16;
|
||||||
|
|
||||||
@@ -57,16 +58,21 @@ class PacketHandler extends EventEmitter {
|
|||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
makeVideoStream(user, portUdp, codec, output, isEnableAudio = false) {
|
makeVideoStream(user, output) {
|
||||||
if (this.videoStreams.has(user)) return this.videoStreams.get(user);
|
if (this.videoStreams.has(user)) return this.videoStreams.get(user);
|
||||||
const stream = new FFmpegHandler(this, user, codec, portUdp, output, isEnableAudio);
|
const stream = new Recorder(this, {
|
||||||
|
userId: user,
|
||||||
|
output,
|
||||||
|
portUdpH264: 65506,
|
||||||
|
portUdpOpus: 65510,
|
||||||
|
});
|
||||||
stream.on('ready', () => {
|
stream.on('ready', () => {
|
||||||
this.videoStreams.set(user, stream);
|
this.videoStreams.set(user, stream);
|
||||||
});
|
});
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseBuffer(buffer, shouldReturnTuple = false) {
|
parseBuffer(buffer) {
|
||||||
const { secret_key, mode } = this.receiver.connection.authentication;
|
const { secret_key, mode } = this.receiver.connection.authentication;
|
||||||
// Open packet
|
// Open packet
|
||||||
if (!secret_key) return new Error('secret_key cannot be null or undefined');
|
if (!secret_key) return new Error('secret_key cannot be null or undefined');
|
||||||
@@ -115,30 +121,21 @@ class PacketHandler extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldReturnTuple) {
|
/*
|
||||||
return [header, packet];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip decrypted RTP Header Extension if present
|
// Strip decrypted RTP Header Extension if present
|
||||||
if (buffer.slice(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) {
|
if (buffer.slice(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) {
|
||||||
const headerExtensionLength = buffer.slice(14).readUInt16BE();
|
const headerExtensionLength = buffer.slice(14).readUInt16BE();
|
||||||
packet = packet.subarray(4 * headerExtensionLength);
|
packet = packet.subarray(4 * headerExtensionLength);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return packet;
|
return RtpPacket.deSerialize(Buffer.concat([header, packet]));
|
||||||
}
|
}
|
||||||
|
|
||||||
audioReceiver(buffer) {
|
audioReceiver(ssrc, userStat, opusPacket) {
|
||||||
const ssrc = buffer.readUInt32BE(8);
|
|
||||||
const userStat = this.connection.ssrcMap.get(ssrc);
|
|
||||||
|
|
||||||
if (!userStat) return;
|
|
||||||
|
|
||||||
let opusPacket;
|
|
||||||
const streamInfo = this.streams.get(userStat.userId);
|
const streamInfo = this.streams.get(userStat.userId);
|
||||||
// If the user is in video, we need to check if the packet is just silence
|
// If the user is in video, we need to check if the packet is just silence
|
||||||
if (userStat.hasVideo) {
|
if (userStat.hasVideo) {
|
||||||
opusPacket = this.parseBuffer(buffer);
|
|
||||||
if (opusPacket instanceof Error) {
|
if (opusPacket instanceof Error) {
|
||||||
// Only emit an error if we were actively receiving packets from this user
|
// Only emit an error if we were actively receiving packets from this user
|
||||||
if (streamInfo) {
|
if (streamInfo) {
|
||||||
@@ -146,7 +143,14 @@ class PacketHandler extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (SILENCE_FRAME.equals(opusPacket)) {
|
// Check payload type
|
||||||
|
if (opusPacket.header.payloadType !== Util.getPayloadType('opus')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!opusPacket.payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (SILENCE_FRAME.equals(opusPacket.payload)) {
|
||||||
// If this is a silence frame, pretend we never received it
|
// If this is a silence frame, pretend we never received it
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -176,65 +180,68 @@ class PacketHandler extends EventEmitter {
|
|||||||
|
|
||||||
if (streamInfo) {
|
if (streamInfo) {
|
||||||
const { stream } = streamInfo;
|
const { stream } = streamInfo;
|
||||||
if (!opusPacket) {
|
|
||||||
opusPacket = this.parseBuffer(buffer);
|
|
||||||
if (opusPacket instanceof Error) {
|
if (opusPacket instanceof Error) {
|
||||||
this.emit('error', opusPacket);
|
this.emit('error', opusPacket);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (opusPacket.header.payloadType !== Util.getPayloadType('opus')) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (opusPacket === null) {
|
stream.push(opusPacket.payload);
|
||||||
// ! null marks EOF for stream
|
|
||||||
nextTick(() => this.destroy());
|
|
||||||
}
|
|
||||||
stream.push(opusPacket);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audioReceiverForStream(buffer) {
|
audioReceiverForStream(ssrc, userStat, packet) {
|
||||||
const ssrc = buffer.readUInt32BE(8);
|
|
||||||
const userStat = this.connection.ssrcMap.get(ssrc); // Audio_ssrc
|
|
||||||
if (!userStat) return;
|
|
||||||
const streamInfo = this.videoStreams.get(userStat.userId);
|
const streamInfo = this.videoStreams.get(userStat.userId);
|
||||||
if (!streamInfo) return;
|
if (!streamInfo) return;
|
||||||
const packet = this.parseBuffer(buffer, true);
|
|
||||||
if (packet instanceof Error) {
|
if (packet instanceof Error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (streamInfo.isEnableAudio) {
|
if (packet.header.payloadType !== Util.getPayloadType('opus')) {
|
||||||
streamInfo.sendPayloadToFFmpeg(Buffer.concat(packet), true);
|
return;
|
||||||
}
|
}
|
||||||
|
streamInfo.feed(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
videoReceiver(buffer) {
|
/**
|
||||||
const ssrc = buffer.readUInt32BE(8);
|
* Test
|
||||||
const userStat = this.connection.ssrcMap.get(ssrc - 1); // Video_ssrc
|
* @param {number} ssrc ssrc
|
||||||
|
* @param {Object} userStat { userId, hasVideo }
|
||||||
if (!userStat) return;
|
* @param {RtpPacket} packet RtpPacket
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
videoReceiver(ssrc, userStat, packet) {
|
||||||
const streamInfo = this.videoStreams.get(userStat.userId);
|
const streamInfo = this.videoStreams.get(userStat.userId);
|
||||||
// If the user is in video, we need to check if the packet is just silence
|
// If the user is in video, we need to check if the packet is just silence
|
||||||
if (userStat.hasVideo) {
|
if (userStat.hasVideo) {
|
||||||
const packet = this.parseBuffer(buffer, true);
|
|
||||||
if (packet instanceof Error) {
|
if (packet instanceof Error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let [header, videoPacket] = packet;
|
|
||||||
if (SILENCE_FRAME.equals(videoPacket)) {
|
if (packet.header.payloadType === Util.getPayloadType('opus')) {
|
||||||
// If this is a silence frame, pretend we never received it
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.receiver.emit('videoData', ssrc - 1, userStat, header, videoPacket);
|
|
||||||
|
|
||||||
if (streamInfo) {
|
if (streamInfo) {
|
||||||
streamInfo.sendPayloadToFFmpeg(Buffer.concat(packet));
|
streamInfo.feed(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
push(buffer) {
|
push(buffer) {
|
||||||
this.audioReceiver(buffer);
|
const ssrc = buffer.readUInt32BE(8);
|
||||||
this.videoReceiver(buffer);
|
let userStat, packet;
|
||||||
this.audioReceiverForStream(buffer);
|
if (this.connection.ssrcMap.has(ssrc)) {
|
||||||
|
userStat = this.connection.ssrcMap.get(ssrc); // Audio_ssrc
|
||||||
|
packet = this.parseBuffer(buffer);
|
||||||
|
this.audioReceiver(ssrc, userStat, packet);
|
||||||
|
this.audioReceiverForStream(ssrc, userStat, packet);
|
||||||
|
} else if (this.connection.ssrcMap.has(ssrc - 1)) {
|
||||||
|
userStat = this.connection.ssrcMap.get(ssrc - 1); // Video_ssrc
|
||||||
|
packet = this.parseBuffer(buffer);
|
||||||
|
this.videoReceiver(ssrc, userStat, packet);
|
||||||
|
}
|
||||||
|
if (userStat && !(packet instanceof Error)) this.receiver.emit('receiverData', userStat, packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// When udp connection is closed (STREAM_DELETE), destroy all streams (Memory leak)
|
// When udp connection is closed (STREAM_DELETE), destroy all streams (Memory leak)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const EventEmitter = require('events');
|
|||||||
const prism = require('prism-media');
|
const prism = require('prism-media');
|
||||||
const PacketHandler = require('./PacketHandler');
|
const PacketHandler = require('./PacketHandler');
|
||||||
const { Error } = require('../../../errors');
|
const { Error } = require('../../../errors');
|
||||||
|
const { PCMInsertSilence } = require('../player/processing/PCMInsertSilence');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives audio packets from a voice connection.
|
* Receives audio packets from a voice connection.
|
||||||
@@ -33,6 +34,8 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
* audio
|
* audio
|
||||||
* @property {string} [end='silence'] When the stream should be destroyed. If `silence`, this will be when the user
|
* @property {string} [end='silence'] When the stream should be destroyed. If `silence`, this will be when the user
|
||||||
* stops talking. Otherwise, if `manual`, this should be handled by you.
|
* stops talking. Otherwise, if `manual`, this should be handled by you.
|
||||||
|
* @property {boolean} [paddingSilence=false] Whether to add silence padding
|
||||||
|
* If 'end' is set to 'silence', this property automatically defaults to `false`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,10 +45,22 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
* @param {ReceiveStreamOptions} options Options.
|
* @param {ReceiveStreamOptions} options Options.
|
||||||
* @returns {ReadableStream}
|
* @returns {ReadableStream}
|
||||||
*/
|
*/
|
||||||
createStream(user, { mode = 'opus', end = 'silence' } = {}) {
|
createStream(user, { mode = 'opus', end = 'silence', paddingSilence = false } = {}) {
|
||||||
user = this.connection.client.users.resolve(user);
|
user = this.connection.client.users.resolve(user);
|
||||||
|
if (end === 'silence') paddingSilence = false;
|
||||||
if (!user) throw new Error('VOICE_USER_MISSING');
|
if (!user) throw new Error('VOICE_USER_MISSING');
|
||||||
const stream = this.packets.makeStream(user.id, end);
|
const stream = this.packets.makeStream(user.id, end); // Opus stream
|
||||||
|
if (paddingSilence) {
|
||||||
|
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 });
|
||||||
|
const pcmTransformer = new PCMInsertSilence();
|
||||||
|
stream.pipe(decoder).pipe(pcmTransformer);
|
||||||
|
if (mode === 'opus') {
|
||||||
|
const encoder = new prism.opus.Encoder({ channels: 2, rate: 48000, frameSize: 960 });
|
||||||
|
pcmTransformer.pipe(encoder);
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
return pcmTransformer;
|
||||||
|
} else {
|
||||||
if (mode === 'pcm') {
|
if (mode === 'pcm') {
|
||||||
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 });
|
const decoder = new prism.opus.Decoder({ channels: 2, rate: 48000, frameSize: 960 });
|
||||||
stream.pipe(decoder);
|
stream.pipe(decoder);
|
||||||
@@ -53,38 +68,28 @@ class VoiceReceiver extends EventEmitter {
|
|||||||
}
|
}
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Options passed to `VoiceReceiver#createVideoStream`.
|
|
||||||
* @typedef {Object} ReceiveVideoStreamOptions
|
|
||||||
* @property {number} portUdp The UDP port to use for the video stream (local stream).
|
|
||||||
* @property {WritableStream|string} output Output stream or file path to write the video stream to.
|
|
||||||
* @property {boolean} [isEnableAudio=false] Enable audio for the video stream.
|
|
||||||
* <info>If you intend to record the stream with audio, make sure that `portUdp` and `portUdp + 2` are not in use.</info>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new video receiving stream. If a stream already exists for a user, then that stream will be returned
|
* Creates a new video receiving stream. If a stream already exists for a user, then that stream will be returned
|
||||||
* rather than generating a new one.
|
* rather than generating a new one.
|
||||||
* <info>Proof of concept - Requires a very good internet connection</info>
|
* <info>Proof of concept - Requires a very good internet connection</info>
|
||||||
* @param {UserResolvable} user The user to start listening to.
|
* @param {UserResolvable} user The user to start listening to.
|
||||||
* @param {ReceiveVideoStreamOptions} options Options.
|
* @param {WritableStream|string} output Output stream or file path to write the video stream to.
|
||||||
* @returns {FFmpegHandler} The video stream for the specified user.
|
* @returns {Recorder} The video stream for the specified user.
|
||||||
*/
|
*/
|
||||||
createVideoStream(user, { portUdp, output, isEnableAudio = false } = {}) {
|
createVideoStream(user, output) {
|
||||||
user = this.connection.client.users.resolve(user);
|
user = this.connection.client.users.resolve(user);
|
||||||
if (!user) throw new Error('VOICE_USER_MISSING');
|
if (!user) throw new Error('VOICE_USER_MISSING');
|
||||||
const stream = this.packets.makeVideoStream(user.id, portUdp, 'H264', output, isEnableAudio);
|
const stream = this.packets.makeVideoStream(user.id, output);
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted whenever there is a video data (Raw)
|
* Emitted whenever there is a data packet received.
|
||||||
* @event VoiceReceiver#videoData
|
* @event VoiceReceiver#receiverData
|
||||||
* @param {number} ssrc SSRC
|
|
||||||
* @param {{ userId: Snowflake, hasVideo: boolean }} ssrcData SSRC Data
|
* @param {{ userId: Snowflake, hasVideo: boolean }} ssrcData SSRC Data
|
||||||
* @param {Buffer} header The unencrypted RTP header contains 12 bytes, Buffer<0xbe, 0xde> and the extension size
|
* @param {RtpPacket} header RTP Packet
|
||||||
* @param {Buffer} packetDecrypt Decrypted contains the extension, if any, the video packet
|
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ const { Buffer } = require('node:buffer');
|
|||||||
const { Writable } = require('stream');
|
const { Writable } = require('stream');
|
||||||
const find = require('find-process');
|
const find = require('find-process');
|
||||||
const kill = require('tree-kill');
|
const kill = require('tree-kill');
|
||||||
|
const { RtpPacket } = require('werift-rtp');
|
||||||
const Util = require('../../../util/Util');
|
const Util = require('../../../util/Util');
|
||||||
|
const { randomPorts } = require('../util/Function');
|
||||||
const { StreamOutput } = require('../util/Socket');
|
const { StreamOutput } = require('../util/Socket');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a FFmpeg handler
|
* Represents a FFmpeg handler
|
||||||
* @extends {EventEmitter}
|
* @extends {EventEmitter}
|
||||||
*/
|
*/
|
||||||
class FFmpegHandler extends EventEmitter {
|
class Recorder extends EventEmitter {
|
||||||
constructor(receiver, userId, codec, portUdp, output, isEnableAudio) {
|
constructor(receiver, { userId, portUdpH264, portUdpOpus, output } = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
Object.defineProperty(this, 'receiver', { value: receiver });
|
Object.defineProperty(this, 'receiver', { value: receiver });
|
||||||
@@ -26,27 +28,18 @@ class FFmpegHandler extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
|
||||||
/**
|
this.portUdpH264 = portUdpH264;
|
||||||
* If the audio is enabled
|
this.portUdpH265 = null;
|
||||||
* @type {boolean}
|
this.portUdpOpus = portUdpOpus;
|
||||||
*/
|
|
||||||
this.isEnableAudio = isEnableAudio;
|
|
||||||
|
|
||||||
/**
|
this.promise = null;
|
||||||
* The codec of the stream
|
|
||||||
* @type {VideoCodec}
|
|
||||||
*/
|
|
||||||
this.codec = codec;
|
|
||||||
|
|
||||||
/**
|
if (!portUdpH264 || !portUdpOpus) {
|
||||||
* The UDP port to listen to
|
this.promise = randomPorts(6, 'udp4').then(ports => {
|
||||||
* @type {number}
|
ports = ports.filter(port => port % 2 === 0);
|
||||||
*/
|
this.portUdpH264 ??= ports[0];
|
||||||
this.portUdp = portUdp;
|
this.portUdpOpus ??= ports[1];
|
||||||
|
});
|
||||||
const isStream = output instanceof Writable;
|
|
||||||
if (isStream) {
|
|
||||||
this.outputStream = StreamOutput(output);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,35 +48,54 @@ class FFmpegHandler extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this.output = output;
|
this.output = output;
|
||||||
|
|
||||||
const sdpData = Util.getSDPCodecName(portUdp, this.isEnableAudio);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The FFmpeg process is ready or not
|
* The FFmpeg process is ready or not
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
|
|
||||||
|
this.socket = createSocket('udp4');
|
||||||
|
|
||||||
|
this.init(output);
|
||||||
|
}
|
||||||
|
async init(output) {
|
||||||
|
await this.promise;
|
||||||
|
const sdpData = Util.getSDPCodecName(this.portUdpH264, this.portUdpH265, this.portUdpOpus);
|
||||||
|
const isStream = output instanceof Writable;
|
||||||
|
if (isStream) {
|
||||||
|
this.outputStream = StreamOutput(output);
|
||||||
|
}
|
||||||
const stream = spawn('ffmpeg', [
|
const stream = spawn('ffmpeg', [
|
||||||
'-reorder_queue_size',
|
'-reorder_queue_size',
|
||||||
'50',
|
'500',
|
||||||
|
'-thread_queue_size',
|
||||||
|
'500',
|
||||||
'-err_detect',
|
'-err_detect',
|
||||||
'ignore_err',
|
'ignore_err',
|
||||||
'-flags2',
|
'-flags2',
|
||||||
'+export_mvs',
|
'+export_mvs',
|
||||||
'-fflags',
|
'-fflags',
|
||||||
'+genpts',
|
'+genpts+discardcorrupt',
|
||||||
'-fflags',
|
|
||||||
'+discardcorrupt',
|
|
||||||
'-use_wallclock_as_timestamps',
|
'-use_wallclock_as_timestamps',
|
||||||
'1',
|
'1',
|
||||||
|
'-f',
|
||||||
|
'sdp',
|
||||||
|
'-analyzeduration',
|
||||||
|
'1M',
|
||||||
|
'-probesize',
|
||||||
|
'1M',
|
||||||
'-protocol_whitelist',
|
'-protocol_whitelist',
|
||||||
'file,udp,rtp,pipe,fd',
|
'file,udp,rtp,pipe,fd',
|
||||||
'-i',
|
'-i',
|
||||||
'-', // Read from stdin
|
'-', // Read from stdin
|
||||||
'-buffer_size',
|
'-buffer_size',
|
||||||
'1000000',
|
'4M',
|
||||||
'-max_delay',
|
'-max_delay',
|
||||||
'500000',
|
'500000', // 500ms
|
||||||
|
'-rtbufsize',
|
||||||
|
'4M',
|
||||||
|
'-c',
|
||||||
|
'copy',
|
||||||
'-y',
|
'-y',
|
||||||
'-f',
|
'-f',
|
||||||
'matroska',
|
'matroska',
|
||||||
@@ -102,33 +114,36 @@ class FFmpegHandler extends EventEmitter {
|
|||||||
this.ready = true;
|
this.ready = true;
|
||||||
this.emit('ready');
|
this.emit('ready');
|
||||||
});
|
});
|
||||||
this.socket = createSocket('udp4');
|
|
||||||
this.socketAudio = createSocket('udp4');
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Send a payload to FFmpeg via UDP
|
* Send a payload to FFmpeg via UDP
|
||||||
* @param {Buffer} payload The payload
|
* @param {RtpPacket|string|Buffer} payload The payload
|
||||||
* @param {boolean} isAudio If the payload is audio
|
|
||||||
* @param {*} callback Callback
|
* @param {*} callback Callback
|
||||||
*/
|
*/
|
||||||
sendPayloadToFFmpeg(
|
feed(
|
||||||
payload,
|
payload,
|
||||||
isAudio = false,
|
|
||||||
callback = e => {
|
callback = e => {
|
||||||
if (e) {
|
if (e) {
|
||||||
console.error('Error sending packet:', e);
|
console.error('Error sending packet:', e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const message = Buffer.from(payload);
|
if (!(payload instanceof RtpPacket)) {
|
||||||
if (isAudio && !this.isEnableAudio) {
|
payload = RtpPacket.deSerialize(Buffer.isBuffer(payload) ? payload : Buffer.from(payload));
|
||||||
|
}
|
||||||
|
const message = payload.serialize();
|
||||||
|
// Get port from payloadType
|
||||||
|
let port;
|
||||||
|
if (payload.header.payloadType === Util.getPayloadType('opus')) {
|
||||||
|
port = this.portUdpOpus;
|
||||||
|
} else if (payload.header.payloadType === Util.getPayloadType('H264')) {
|
||||||
|
port = this.portUdpH264;
|
||||||
|
} else if (payload.header.payloadType === Util.getPayloadType('H265')) {
|
||||||
|
port = this.portUdpH265;
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAudio) {
|
this.socket.send(message, 0, message.length, port, '127.0.0.1', callback);
|
||||||
this.socketAudio.send(message, 0, message.length, this.portUdp + 2, '127.0.0.1', callback);
|
|
||||||
} else {
|
|
||||||
this.socket.send(message, 0, message.length, this.portUdp, '127.0.0.1', callback);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -138,21 +153,21 @@ class FFmpegHandler extends EventEmitter {
|
|||||||
let process = list.find(o => o.pid === ffmpegPid || o.ppid === ffmpegPid || o.cmd.includes(args));
|
let process = list.find(o => o.pid === ffmpegPid || o.ppid === ffmpegPid || o.cmd.includes(args));
|
||||||
if (process) {
|
if (process) {
|
||||||
kill(process.pid);
|
kill(process.pid);
|
||||||
this.receiver.videoStreams.delete(this.userId);
|
this.receiver?.videoStreams?.delete(this.userId);
|
||||||
this.emit('closed');
|
this.emit('closed');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the FFmpegHandler becomes ready to start working.
|
* Emitted when the Recorder becomes ready to start working.
|
||||||
* @event FFmpegHandler#ready
|
* @event Recorder#ready
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the FFmpegHandler is closed.
|
* Emitted when the Recorder is closed.
|
||||||
* @event FFmpegHandler#closed
|
* @event Recorder#closed
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FFmpegHandler;
|
module.exports = Recorder;
|
||||||
@@ -1,5 +1,103 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const dgram = require('dgram');
|
||||||
|
const { setImmediate } = require('node:timers');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} InterfaceAddresses
|
||||||
|
* @property {string} [udp4] - IPv4 address
|
||||||
|
* @property {string} [udp6] - IPv6 address
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the interface address for a given socket type.
|
||||||
|
* @param {"udp4"|"udp6"} type - The socket type.
|
||||||
|
* @param {InterfaceAddresses} [interfaceAddresses] - The interface addresses mapping.
|
||||||
|
* @returns {string|undefined} The interface address if available.
|
||||||
|
*/
|
||||||
|
function interfaceAddress(type, interfaceAddresses) {
|
||||||
|
return interfaceAddresses ? interfaceAddresses[type] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random available port.
|
||||||
|
* @param {"udp4"|"udp6"} [protocol="udp4"] - The socket type.
|
||||||
|
* @param {InterfaceAddresses} [interfaceAddresses] - The interface addresses mapping.
|
||||||
|
* @returns {Promise<number>} The assigned random port.
|
||||||
|
*/
|
||||||
|
async function randomPort(protocol = 'udp4', interfaceAddresses) {
|
||||||
|
const socket = dgram.createSocket(protocol);
|
||||||
|
|
||||||
|
setImmediate(() =>
|
||||||
|
socket.bind({
|
||||||
|
port: 0,
|
||||||
|
address: interfaceAddress(protocol, interfaceAddresses),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
socket.once('error', reject);
|
||||||
|
socket.once('listening', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = socket.address()?.port;
|
||||||
|
await new Promise(resolve => socket.close(resolve));
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get multiple random available ports.
|
||||||
|
* @param {number} num - Number of ports to find.
|
||||||
|
* @param {"udp4"|"udp6"} [protocol="udp4"] - The socket type.
|
||||||
|
* @param {InterfaceAddresses} [interfaceAddresses] - The interface addresses mapping.
|
||||||
|
* @returns {Promise<number[]>} An array of assigned random ports.
|
||||||
|
*/
|
||||||
|
async function randomPorts(num, protocol = 'udp4', interfaceAddresses) {
|
||||||
|
return Promise.all(Array.from({ length: num }).map(() => randomPort(protocol, interfaceAddresses)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an available port within a given range.
|
||||||
|
* @param {number} min - The minimum port number.
|
||||||
|
* @param {number} max - The maximum port number.
|
||||||
|
* @param {"udp4"|"udp6"} [protocol="udp4"] - The socket type.
|
||||||
|
* @param {InterfaceAddresses} [interfaceAddresses] - The interface addresses mapping.
|
||||||
|
* @returns {Promise<number>} The available port within range.
|
||||||
|
* @throws {Error} If no port is found within the range.
|
||||||
|
*/
|
||||||
|
async function findPort(min, max, protocol = 'udp4', interfaceAddresses) {
|
||||||
|
let port;
|
||||||
|
|
||||||
|
for (let i = min; i <= max; i++) {
|
||||||
|
const socket = dgram.createSocket(protocol);
|
||||||
|
|
||||||
|
setImmediate(() =>
|
||||||
|
socket.bind({
|
||||||
|
port: i,
|
||||||
|
address: interfaceAddress(protocol, interfaceAddresses),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const error = await new Promise(resolve => {
|
||||||
|
socket.once('error', resolve);
|
||||||
|
socket.once('listening', () => resolve(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise(resolve => socket.close(resolve));
|
||||||
|
|
||||||
|
if (error) continue;
|
||||||
|
|
||||||
|
const addressInfo = socket.address();
|
||||||
|
if (addressInfo && addressInfo.port >= min && addressInfo.port <= max) {
|
||||||
|
port = addressInfo.port;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!port) throw new Error('port not found');
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
function parseStreamKey(key) {
|
function parseStreamKey(key) {
|
||||||
const Arr = key.split(':');
|
const Arr = key.split(':');
|
||||||
const type = Arr[0];
|
const type = Arr[0];
|
||||||
@@ -10,5 +108,9 @@ function parseStreamKey(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
randomPort,
|
||||||
|
randomPorts,
|
||||||
|
findPort,
|
||||||
|
interfaceAddress,
|
||||||
parseStreamKey,
|
parseStreamKey,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -168,3 +168,4 @@ exports.DiscordAuthWebsocket = require('./util/RemoteAuth');
|
|||||||
exports.PurchasedFlags = require('./util/PurchasedFlags');
|
exports.PurchasedFlags = require('./util/PurchasedFlags');
|
||||||
exports.Poll = require('./structures/Poll').Poll;
|
exports.Poll = require('./structures/Poll').Poll;
|
||||||
exports.PollAnswer = require('./structures/PollAnswer').PollAnswer;
|
exports.PollAnswer = require('./structures/PollAnswer').PollAnswer;
|
||||||
|
exports.Recorder = require('./client/voice/receiver/Recorder');
|
||||||
|
|||||||
@@ -38,6 +38,11 @@
|
|||||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ReactionType}
|
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ReactionType}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @external RtpPacket
|
||||||
|
* @see {@link https://github.com/shinyoshiaki/werift-webrtc/blob/develop/doc/classes/RtpPacket.md}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @external TeamMemberRole
|
* @external TeamMemberRole
|
||||||
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/TeamMemberRole}
|
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/TeamMemberRole}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const payloadTypes = [
|
|||||||
payload_type: 103,
|
payload_type: 103,
|
||||||
rtx_payload_type: 104,
|
rtx_payload_type: 104,
|
||||||
encode: false,
|
encode: false,
|
||||||
decode: false, // Working but very glitchy
|
decode: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'H264',
|
name: 'H264',
|
||||||
@@ -988,27 +988,43 @@ class Util extends null {
|
|||||||
return payloadTypes;
|
return payloadTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the payload type of the codec
|
||||||
|
* @param {'opus' | 'H264' | 'H265' | 'VP8' | 'VP9' | 'AV1'} codecName - Codec name
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
static getPayloadType(codecName) {
|
static getPayloadType(codecName) {
|
||||||
return payloadTypes.find(p => p.name === codecName).payload_type;
|
return payloadTypes.find(p => p.name === codecName).payload_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSDPCodecName(portUdp, isEnableAudio) {
|
static getSDPCodecName(portUdpH264, portUdpH265, portUdpOpus) {
|
||||||
|
const payloadTypeH264 = Util.getPayloadType('H264');
|
||||||
|
const payloadTypeH265 = Util.getPayloadType('H265');
|
||||||
|
const payloadTypeOpus = Util.getPayloadType('opus');
|
||||||
let sdpData = `v=0
|
let sdpData = `v=0
|
||||||
o=- 0 0 IN IP4 0.0.0.0
|
o=- 0 0 IN IP4 0.0.0.0
|
||||||
s=-
|
s=-
|
||||||
c=IN IP4 0.0.0.0
|
c=IN IP4 0.0.0.0
|
||||||
t=0 0
|
t=0 0
|
||||||
a=tool:libavformat 61.1.100
|
a=tool:libavformat 61.1.100
|
||||||
m=video ${portUdp} RTP/AVP 105
|
m=video ${portUdpH264} RTP/AVP ${payloadTypeH264}
|
||||||
a=rtpmap:105 H264/90000
|
c=IN IP4 127.0.0.1
|
||||||
a=fmtp:105 profile-level-id=42e01f;sprop-parameter-sets=Z0IAH6tAoAt2AtwEBAaQeJEV,aM4JyA==;packetization-mode=1
|
b=AS:1000
|
||||||
|
a=rtpmap:${payloadTypeH264} H264/90000
|
||||||
|
a=fmtp:${payloadTypeH264} profile-level-id=42e01f;sprop-parameter-sets=Z0IAH6tAoAt2AtwEBAaQeJEV,aM4JyA==;packetization-mode=1
|
||||||
${
|
${
|
||||||
isEnableAudio
|
portUdpH265
|
||||||
? `m=audio ${portUdp + 2} RTP/AVP 120
|
? `m=video ${portUdpH265} RTP/AVP ${payloadTypeH265}
|
||||||
a=rtpmap:120 opus/48000/2
|
c=IN IP4 127.0.0.1
|
||||||
a=fmtp:120 minptime=10;useinbandfec=1`
|
b=AS:1000
|
||||||
|
a=rtpmap:${payloadTypeH265} H265/90000`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
m=audio ${portUdpOpus} RTP/AVP ${payloadTypeOpus}
|
||||||
|
c=IN IP4 127.0.0.1
|
||||||
|
b=AS:96
|
||||||
|
a=rtpmap:${payloadTypeOpus} opus/48000/2
|
||||||
|
a=fmtp:${payloadTypeOpus} minptime=10;useinbandfec=1
|
||||||
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
|
||||||
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
|
||||||
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
|
||||||
|
|||||||
57
typings/index.d.ts
vendored
57
typings/index.d.ts
vendored
@@ -20,6 +20,7 @@ import {
|
|||||||
underscore,
|
underscore,
|
||||||
userMention,
|
userMention,
|
||||||
} from '@discordjs/builders';
|
} from '@discordjs/builders';
|
||||||
|
import { RtpPacket } from 'werift-rtp';
|
||||||
import { Collection } from '@discordjs/collection';
|
import { Collection } from '@discordjs/collection';
|
||||||
import {
|
import {
|
||||||
APIActionRowComponent,
|
APIActionRowComponent,
|
||||||
@@ -983,8 +984,6 @@ export class BaseDispatcher extends Writable {
|
|||||||
public count: number;
|
public count: number;
|
||||||
public sequence: number;
|
public sequence: number;
|
||||||
public timestamp: number;
|
public timestamp: number;
|
||||||
public mtu: number;
|
|
||||||
public fps: number;
|
|
||||||
public payloadType: number;
|
public payloadType: number;
|
||||||
public extensionEnabled: boolean;
|
public extensionEnabled: boolean;
|
||||||
|
|
||||||
@@ -1010,10 +1009,14 @@ export class AudioDispatcher extends VolumeMixin(BaseDispatcher) {
|
|||||||
constructor(player: object, options?: StreamOptions, streams?: object);
|
constructor(player: object, options?: StreamOptions, streams?: object);
|
||||||
public readonly bitrateEditable: boolean;
|
public readonly bitrateEditable: boolean;
|
||||||
|
|
||||||
|
public getTypeDispatcher(): 'audio';
|
||||||
|
|
||||||
public setBitrate(value: number | 'auto'): boolean;
|
public setBitrate(value: number | 'auto'): boolean;
|
||||||
public setFEC(enabled: boolean): boolean;
|
public setFEC(enabled: boolean): boolean;
|
||||||
public setPLP(value: number): boolean;
|
public setPLP(value: number): boolean;
|
||||||
|
|
||||||
|
public setSyncVideoDispatcher(otherDispatcher: VideoDispatcher): void;
|
||||||
|
|
||||||
public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this;
|
public on(event: 'volumeChange', listener: (oldVolume: number, newVolume: number) => void): this;
|
||||||
public on(event: string, listener: (...args: any[]) => void): this;
|
public on(event: string, listener: (...args: any[]) => void): this;
|
||||||
|
|
||||||
@@ -1024,6 +1027,11 @@ export class AudioDispatcher extends VolumeMixin(BaseDispatcher) {
|
|||||||
export class VideoDispatcher extends BaseDispatcher {
|
export class VideoDispatcher extends BaseDispatcher {
|
||||||
constructor(player: object, options?: StreamOptions, streams?: object, fps?: number);
|
constructor(player: object, options?: StreamOptions, streams?: object, fps?: number);
|
||||||
|
|
||||||
|
public mtu: number;
|
||||||
|
public fps: number;
|
||||||
|
|
||||||
|
public getTypeDispatcher(): 'video';
|
||||||
|
|
||||||
public setFPSSource(value: number): void;
|
public setFPSSource(value: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1133,60 +1141,53 @@ export class StreamConnectionReadonly extends VoiceConnection {
|
|||||||
public override playVideo(): VideoDispatcher;
|
public override playVideo(): VideoDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FFmpegHandler extends EventEmitter {
|
export class Recorder<Ready extends boolean = boolean, T = any> extends EventEmitter {
|
||||||
public codec: 'H264';
|
constructor(receiver: T, options: { ffmpegArgs: string[]; channels: number; frameDuration: number });
|
||||||
public portUdp: number;
|
private promise: Promise<void>;
|
||||||
public ready: boolean;
|
public readonly receiver: T;
|
||||||
public stream: ChildProcessWithoutNullStreams;
|
public portUdpH264: number;
|
||||||
|
public portUdpOpus: number;
|
||||||
|
public ready: Ready;
|
||||||
|
public stream: If<Ready, ChildProcessWithoutNullStreams>;
|
||||||
public socket: Socket;
|
public socket: Socket;
|
||||||
public socketAudio: Socket;
|
|
||||||
public output: Writable | string;
|
public output: Writable | string;
|
||||||
public isEnableAudio: boolean;
|
|
||||||
public userId: Snowflake;
|
public userId: Snowflake;
|
||||||
public sendPayloadToFFmpeg(payload: Buffer, isAudio?: boolean): void;
|
public feed(payload: RtpPacket | BufferResolvable): void;
|
||||||
public on(event: 'ready' | 'closed', listener: () => void): this;
|
public on(event: 'ready' | 'closed', listener: (recorder: Recorder<true, T>) => void): this;
|
||||||
public once(event: 'ready' | 'closed', listener: () => void): this;
|
public once(event: 'ready' | 'closed', listener: (recorder: Recorder<true, T>) => void): this;
|
||||||
public destroy(): void;
|
public destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VoiceReceiver extends EventEmitter {
|
export class VoiceReceiver extends EventEmitter {
|
||||||
constructor(connection: VoiceConnection);
|
constructor(connection: VoiceConnection);
|
||||||
public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual' }): Readable;
|
public createStream(
|
||||||
public createVideoStream(
|
|
||||||
user: UserResolvable,
|
user: UserResolvable,
|
||||||
options?: {
|
options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual'; paddingSilence?: boolean },
|
||||||
portUdp: number;
|
): Readable;
|
||||||
output: Writable | string;
|
public createVideoStream(user: UserResolvable, output: Writable | string): Recorder<false, any>;
|
||||||
isEnableAudio: boolean;
|
|
||||||
},
|
|
||||||
): FFmpegHandler;
|
|
||||||
|
|
||||||
public on(event: 'debug', listener: (error: Error | string) => void): this;
|
public on(event: 'debug', listener: (error: Error | string) => void): this;
|
||||||
public on(
|
public on(
|
||||||
event: 'videoData',
|
event: 'receiverData',
|
||||||
listener: (
|
listener: (
|
||||||
ssrc: number,
|
|
||||||
ssrcData: {
|
ssrcData: {
|
||||||
userId: Snowflake;
|
userId: Snowflake;
|
||||||
hasVideo: boolean;
|
hasVideo: boolean;
|
||||||
},
|
},
|
||||||
headerRaw: Buffer,
|
packet: RtpPacket,
|
||||||
packetDecrypt: Buffer,
|
|
||||||
) => void,
|
) => void,
|
||||||
): this;
|
): this;
|
||||||
public on(event: string, listener: (...args: any[]) => void): this;
|
public on(event: string, listener: (...args: any[]) => void): this;
|
||||||
|
|
||||||
public once(event: 'debug', listener: (error: Error | string) => void): this;
|
public once(event: 'debug', listener: (error: Error | string) => void): this;
|
||||||
public once(
|
public once(
|
||||||
event: 'videoData',
|
event: 'receiverData',
|
||||||
listener: (
|
listener: (
|
||||||
ssrc: number,
|
|
||||||
ssrcData: {
|
ssrcData: {
|
||||||
userId: Snowflake;
|
userId: Snowflake;
|
||||||
hasVideo: boolean;
|
hasVideo: boolean;
|
||||||
},
|
},
|
||||||
headerRaw: Buffer,
|
packet: RtpPacket,
|
||||||
packetDecrypt: Buffer,
|
|
||||||
) => void,
|
) => void,
|
||||||
): this;
|
): this;
|
||||||
public once(event: string, listener: (...args: any[]) => void): this;
|
public once(event: string, listener: (...args: any[]) => void): this;
|
||||||
|
|||||||
Reference in New Issue
Block a user