feat(VoiceReceiver): Proof of concept - Video recording
This commit is contained in:
49
examples/VoiceChannel/RecordingVideo.js
Normal file
49
examples/VoiceChannel/RecordingVideo.js
Normal 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');
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
3
typings/index.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user