feat(VoiceReceiver): Proof of concept - Video recording

This commit is contained in:
Elysia
2024-10-27 12:13:57 +07:00
parent a3dfa8ab90
commit f4beb1e4ff
6 changed files with 76 additions and 10 deletions

View File

@@ -0,0 +1,49 @@
// https://v12.discordjs.guide/voice/receiving-audio.html#basic-usage
/*
Install:
- An Opus library: @discordjs/opus or opusscript
- An encryption packages:
+ sodium (best performance)
+ libsodium-wrappers
+ @stablelib/xchacha20poly1305
- ffmpeg (install and add to your system environment)
*/
const { Client } = require('../../src/index');
const client = new Client();
const fs = require('fs');
client.on('ready', async client => {
console.log(`${client.user.username} is ready!`);
const channel = client.channels.cache.get('voice_id');
const connection = await client.voice.joinChannel(channel, {
selfMute: true,
selfDeaf: true,
selfVideo: false,
});
const connectionStream = await connection.joinStreamConnection('user_id');
const video = connectionStream.receiver.createVideoStream('user_id', {
portUdp: 5004,
output: fs.createWriteStream('video.ts'), // Output file using MPEG-TS container
});
video.stream.stderr.on('data', data => {
console.log(`FFmpeg: ${data}`);
});
video.on('ready', () => {
console.log('FFmpeg process ready!');
});
// After 15s
setTimeout(() => {
video.destroy();
}, 15_000);
});
client.login('token');

View File

@@ -5,6 +5,8 @@ const { createSocket } = require('dgram');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const { Buffer } = require('node:buffer'); const { Buffer } = require('node:buffer');
const { Writable } = require('stream'); const { Writable } = require('stream');
const find = require('find-process');
const kill = require('tree-kill');
const Util = require('../../../util/Util'); const Util = require('../../../util/Util');
const { StreamOutput } = require('../util/Socket'); const { StreamOutput } = require('../util/Socket');
@@ -46,11 +48,7 @@ class FFmpegHandler extends EventEmitter {
*/ */
this.ready = false; this.ready = false;
/** const stream = spawn('ffmpeg', [
* The FFmpeg process
* @type {ChildProcessWithoutNullStreams}
*/
this.stream = spawn('ffmpeg', [
'-reorder_queue_size', '-reorder_queue_size',
'50', '50',
'-err_detect', '-err_detect',
@@ -73,9 +71,15 @@ class FFmpegHandler extends EventEmitter {
'500000', '500000',
'-y', '-y',
'-f', // Specify the format '-f', // Specify the format
'matroska', // MKV format 'mpegts', // MKV format
isStream ? this.outputStream.url : output, isStream ? this.outputStream.url : output,
]); ]);
/**
* The FFmpeg process
* @type {ChildProcessWithoutNullStreams}
*/
this.stream = stream;
this.stream.stdin.write(sdpData); this.stream.stdin.write(sdpData);
this.stream.stdin.end(); this.stream.stdin.end();
this.stream.stderr.once('data', data => { this.stream.stderr.once('data', data => {
@@ -101,6 +105,17 @@ class FFmpegHandler extends EventEmitter {
const message = Buffer.from(payload); const message = Buffer.from(payload);
this.socket.send(message, 0, message.length, this.portUdp, '127.0.0.1', callback); this.socket.send(message, 0, message.length, this.portUdp, '127.0.0.1', callback);
} }
destroy() {
const ffmpegPid = this.stream.pid; // But it is ppid ;-;
const args = this.stream.spawnargs.slice(1).join(' '); // Skip ffmpeg
find('name', 'ffmpeg', true).then(list => {
let process = list.find(o => o.pid === ffmpegPid || o.ppid === ffmpegPid || o.cmd.includes(args));
if (process) {
kill(process.pid);
}
});
}
} }
module.exports = FFmpegHandler; module.exports = FFmpegHandler;

View File

@@ -59,6 +59,7 @@ class VoiceReceiver extends EventEmitter {
* @typedef {Object} ReceiveVideoStreamOptions * @typedef {Object} ReceiveVideoStreamOptions
* @property {number} [portUdp] The UDP port to use for the video stream (local stream). * @property {number} [portUdp] The UDP port to use for the video stream (local stream).
* @property {string} [codec='H264'] The codec to use for encoding the video. Default is 'H264'. * @property {string} [codec='H264'] The codec to use for encoding the video. Default is 'H264'.
* <info>H265 supported, but not implemented</info>
* @property {any} [output] Additional output options, as required. * @property {any} [output] Additional output options, as required.
*/ */
@@ -70,9 +71,10 @@ class VoiceReceiver extends EventEmitter {
* @param {ReceiveVideoStreamOptions} options Options. * @param {ReceiveVideoStreamOptions} options Options.
* @returns {FFmpegHandler} The video stream for the specified user. * @returns {FFmpegHandler} The video stream for the specified user.
*/ */
createVideoStream(user, { portUdp, codec = 'H264', output } = {}) { createVideoStream(user, { portUdp, codec, 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');
codec = 'H264';
const stream = this.packets.makeVideoStream(user.id, portUdp, codec, output); const stream = this.packets.makeVideoStream(user.id, portUdp, codec, output);
return stream; return stream;
} }

View File

@@ -68,7 +68,7 @@ const payloadTypes = [
priority: 5000, priority: 5000,
payload_type: 109, payload_type: 109,
rtx_payload_type: 110, rtx_payload_type: 110,
encode: true, encode: false,
decode: false, decode: false,
}, },
]; ];
@@ -951,7 +951,6 @@ class Util extends null {
payloadType = payload.payload_type; payloadType = payload.payload_type;
} else { } else {
const payloadType = packet[1] > 120 ? packet[1] & 0x80 : packet[1]; const payloadType = packet[1] > 120 ? packet[1] & 0x80 : packet[1];
console.log('payloadType', payloadType, packet, portUdp);
payload = payloadTypes.find(p => p.payload_type === payloadType); payload = payloadTypes.find(p => p.payload_type === payloadType);
} }
let sdpData = `o=- 0 0 IN IP4 127.0.0.1 let sdpData = `o=- 0 0 IN IP4 127.0.0.1

3
typings/index.d.ts vendored
View File

@@ -1109,6 +1109,7 @@ export class FFmpegHandler extends EventEmitter {
public sendPayloadToFFmpeg(payload: Buffer): void; public sendPayloadToFFmpeg(payload: Buffer): void;
public on(event: 'ready', listener: () => void): this; public on(event: 'ready', listener: () => void): this;
public once(event: 'ready', listener: () => void): this; public once(event: 'ready', listener: () => void): this;
public destroy(): void;
} }
export class VoiceReceiver extends EventEmitter { export class VoiceReceiver extends EventEmitter {
@@ -1116,7 +1117,7 @@ export class VoiceReceiver extends EventEmitter {
public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual' }): Readable; public createStream(user: UserResolvable, options?: { mode?: 'opus' | 'pcm'; end?: 'silence' | 'manual' }): Readable;
public createVideoStream( public createVideoStream(
user: UserResolvable, user: UserResolvable,
options?: { portUdp: number; codec: VideoCodec | 'H265' | 'VP9' | 'AV1'; output: Writable | string }, options?: { portUdp: number; codec: 'H264'; output: Writable | string },
): FFmpegHandler; ): FFmpegHandler;
public on(event: 'debug', listener: (error: Error | string) => void): this; public on(event: 'debug', listener: (error: Error | string) => void): this;