- Add automatic recording stop when user turns off camera/screenshare - Listen for startStreaming events to detect video_ssrc=0 (camera/screen off) - Add disconnect and connection close event handling - Implement proper cleanup of event listeners to prevent memory leaks - Add targetUserId tracking for accurate event filtering - Update both videoRecording.js and webcamRecording.js services - Update discord.js-selfbot-v13 submodule with latest webcam recording fixes
375 lines
11 KiB
JavaScript
375 lines
11 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { createWriteStream } from "fs";
|
|
import { VIDEO_CONFIG } from "../config/videoConfig.js";
|
|
|
|
// Store active recordings
|
|
const activeRecordings = new Map();
|
|
|
|
class VideoRecordingService {
|
|
constructor() {
|
|
this.outputDir = VIDEO_CONFIG.OUTPUT_DIR;
|
|
this.autoStopTimeout = VIDEO_CONFIG.AUTO_STOP_TIMEOUT;
|
|
this.processingDelay = VIDEO_CONFIG.PROCESSING_DELAY;
|
|
}
|
|
|
|
/**
|
|
* Start recording a user's video in a voice channel
|
|
* @param {Object} options - Recording options
|
|
* @param {Object} options.client - Discord client
|
|
* @param {Object} options.voiceChannel - Voice channel to record in
|
|
* @param {Object} options.user - User to record
|
|
* @param {Object} options.textChannel - Text channel for notifications
|
|
* @returns {Promise<Object>} Recording result
|
|
*/
|
|
async startRecording({ client, voiceChannel, user, textChannel }) {
|
|
try {
|
|
// Check if already recording
|
|
if (activeRecordings.has(voiceChannel.id)) {
|
|
return {
|
|
success: false,
|
|
message: VIDEO_CONFIG.ERRORS.ALREADY_RECORDING,
|
|
};
|
|
}
|
|
|
|
// Ensure output directory exists
|
|
this._ensureOutputDirectory();
|
|
|
|
// Create unique filename with timestamp
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
const videoFilename = `recording-${timestamp}${VIDEO_CONFIG.VIDEO_EXTENSION}`;
|
|
const videoPath = path.join(this.outputDir, videoFilename);
|
|
|
|
console.log(
|
|
`[Docker] Attempting to join voice channel: ${voiceChannel.name}`
|
|
);
|
|
|
|
// Join the voice channel
|
|
const connection = await client.voice.joinChannel(
|
|
voiceChannel.id,
|
|
VIDEO_CONFIG.VOICE_SETTINGS
|
|
);
|
|
|
|
console.log(`[Docker] Successfully joined voice channel`);
|
|
|
|
// Create stream connection for video recording
|
|
const connectionStream = await connection.joinStreamConnection(user.id);
|
|
|
|
// Create video stream with MKV container
|
|
const videoStream = connectionStream.receiver.createVideoStream(
|
|
user.id,
|
|
createWriteStream(videoPath)
|
|
);
|
|
|
|
// Set up video stream event handlers
|
|
this._setupVideoStreamHandlers(videoStream, textChannel, voiceChannel.id);
|
|
|
|
// Set up voice connection event handlers for auto-stop
|
|
this._setupVoiceConnectionHandlers(
|
|
connection,
|
|
user.id,
|
|
voiceChannel.id,
|
|
textChannel
|
|
);
|
|
|
|
// Store recording info
|
|
const recordingInfo = {
|
|
connection,
|
|
connectionStream,
|
|
videoStream,
|
|
videoFilename,
|
|
videoPath,
|
|
startTime: Date.now(),
|
|
userId: user.id,
|
|
textChannelId: textChannel.id,
|
|
voiceChannelId: voiceChannel.id,
|
|
voiceChannelName: voiceChannel.name,
|
|
targetUserId: user.id,
|
|
};
|
|
|
|
activeRecordings.set(voiceChannel.id, recordingInfo);
|
|
|
|
console.log(`[Docker] Started recording in ${voiceChannel.name}`);
|
|
console.log(`[Docker] Output will be saved to: ${this.outputDir}`);
|
|
|
|
// Set up auto-stop timeout
|
|
this._setupAutoStop(voiceChannel.id, textChannel);
|
|
|
|
return {
|
|
success: true,
|
|
message: "Recording started successfully",
|
|
recordingInfo,
|
|
};
|
|
} catch (error) {
|
|
console.error("[Docker] Error starting recording:", error);
|
|
return {
|
|
success: false,
|
|
message: "Failed to start recording",
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop recording in a voice channel
|
|
* @param {string} voiceChannelId - Voice channel ID
|
|
* @param {Object} textChannel - Text channel for notifications
|
|
* @param {string} stopMessage - Message to display when stopping
|
|
* @returns {Object} Stop result
|
|
*/
|
|
async stopRecording(
|
|
voiceChannelId,
|
|
textChannel,
|
|
stopMessage = "Recording stopped."
|
|
) {
|
|
const recording = activeRecordings.get(voiceChannelId);
|
|
|
|
if (!recording) {
|
|
return {
|
|
success: false,
|
|
message: "No active recording found",
|
|
};
|
|
}
|
|
|
|
try {
|
|
console.log(
|
|
`[Docker] Stopping video recording for user: ${recording.userId}`
|
|
);
|
|
|
|
// Clean up streams and connections
|
|
this._cleanupRecording(recording);
|
|
|
|
// Remove from active recordings
|
|
activeRecordings.delete(voiceChannelId);
|
|
|
|
const duration = Math.round((Date.now() - recording.startTime) / 1000);
|
|
|
|
textChannel.send(
|
|
`${stopMessage} ${VIDEO_CONFIG.MESSAGES.PROCESSING.replace(
|
|
"{duration}",
|
|
duration
|
|
)}`
|
|
);
|
|
|
|
// Process the video file after a delay
|
|
setTimeout(() => {
|
|
this._processVideoFile(recording, textChannel, duration);
|
|
}, this.processingDelay);
|
|
|
|
return {
|
|
success: true,
|
|
message: "Recording stopped successfully",
|
|
duration,
|
|
filename: recording.videoFilename,
|
|
};
|
|
} catch (error) {
|
|
console.error("[Docker] Error stopping recording:", error);
|
|
return {
|
|
success: false,
|
|
message: "Error stopping recording, but file may have been saved",
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get status of all active recordings and saved files
|
|
* @returns {Object} Status information
|
|
*/
|
|
getStatus() {
|
|
const activeCount = activeRecordings.size;
|
|
let fileCount = 0;
|
|
let files = [];
|
|
|
|
try {
|
|
if (fs.existsSync(this.outputDir)) {
|
|
const dirContents = fs.readdirSync(this.outputDir);
|
|
files = dirContents.filter((file) =>
|
|
file.endsWith(VIDEO_CONFIG.VIDEO_EXTENSION)
|
|
);
|
|
fileCount = files.length;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error reading output directory:", error);
|
|
}
|
|
|
|
return {
|
|
activeRecordings: activeCount,
|
|
outputDir: this.outputDir,
|
|
videoFiles: fileCount,
|
|
recentFiles: files.slice(-3),
|
|
activeRecordingsList: Array.from(activeRecordings.values()).map((r) => ({
|
|
voiceChannelName: r.voiceChannelName,
|
|
userId: r.userId,
|
|
startTime: r.startTime,
|
|
duration: Math.round((Date.now() - r.startTime) / 1000),
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if a voice channel has an active recording
|
|
* @param {string} voiceChannelId - Voice channel ID
|
|
* @returns {boolean} Whether recording is active
|
|
*/
|
|
isRecording(voiceChannelId) {
|
|
return activeRecordings.has(voiceChannelId);
|
|
}
|
|
|
|
/**
|
|
* Get recording info for a specific voice channel
|
|
* @param {string} voiceChannelId - Voice channel ID
|
|
* @returns {Object|null} Recording info or null if not found
|
|
*/
|
|
getRecordingInfo(voiceChannelId) {
|
|
return activeRecordings.get(voiceChannelId) || null;
|
|
}
|
|
|
|
// Private methods
|
|
|
|
_ensureOutputDirectory() {
|
|
if (!fs.existsSync(this.outputDir)) {
|
|
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
_setupVideoStreamHandlers(videoStream, textChannel, voiceChannelId) {
|
|
videoStream.on("ready", () => {
|
|
console.log("[Docker] FFmpeg process ready for video recording!");
|
|
videoStream.stream.stderr.on("data", (data) => {
|
|
console.log(`[Docker] FFmpeg: ${data}`);
|
|
});
|
|
});
|
|
|
|
videoStream.on("error", (error) => {
|
|
console.error("[Docker] Video recording error:", error);
|
|
textChannel.send("Video recording encountered an error!");
|
|
this._handleRecordingError(voiceChannelId);
|
|
});
|
|
}
|
|
|
|
_setupVoiceConnectionHandlers(
|
|
connection,
|
|
targetUserId,
|
|
voiceChannelId,
|
|
textChannel
|
|
) {
|
|
// Listen for streaming events to detect when screenshare is turned off
|
|
connection.on("startStreaming", ({ video_ssrc, user_id, audio_ssrc }) => {
|
|
console.log(
|
|
`[Docker] Video streaming event - User: ${user_id}, Video SSRC: ${video_ssrc}, Target: ${targetUserId}`
|
|
);
|
|
|
|
// Check if this is our target user and they turned off their screenshare
|
|
if (user_id === targetUserId && (video_ssrc === 0 || !video_ssrc)) {
|
|
console.log(
|
|
`[Docker] Target user ${targetUserId} turned off screenshare, stopping video recording`
|
|
);
|
|
this.stopRecording(
|
|
voiceChannelId,
|
|
textChannel,
|
|
"📹 Video recording stopped - user turned off screenshare."
|
|
);
|
|
}
|
|
});
|
|
|
|
// Listen for user disconnect events
|
|
connection.on("disconnect", (userId) => {
|
|
if (userId === targetUserId) {
|
|
console.log(
|
|
`[Docker] Target user ${targetUserId} disconnected, stopping video recording`
|
|
);
|
|
this.stopRecording(
|
|
voiceChannelId,
|
|
textChannel,
|
|
"📹 Video recording stopped - user disconnected."
|
|
);
|
|
}
|
|
});
|
|
|
|
// Listen for connection close
|
|
connection.on("close", () => {
|
|
console.log(`[Docker] Voice connection closed, stopping video recording`);
|
|
if (this.isRecording(voiceChannelId)) {
|
|
this.stopRecording(
|
|
voiceChannelId,
|
|
textChannel,
|
|
"📹 Video recording stopped - voice connection closed."
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
_setupAutoStop(voiceChannelId, textChannel) {
|
|
setTimeout(() => {
|
|
if (activeRecordings.has(voiceChannelId)) {
|
|
this.stopRecording(
|
|
voiceChannelId,
|
|
textChannel,
|
|
"Recording stopped automatically after 30 seconds (demo limit)."
|
|
);
|
|
}
|
|
}, this.autoStopTimeout);
|
|
}
|
|
|
|
_cleanupRecording(recording) {
|
|
// Remove all event listeners first
|
|
if (recording.connection) {
|
|
recording.connection.removeAllListeners("startStreaming");
|
|
recording.connection.removeAllListeners("disconnect");
|
|
recording.connection.removeAllListeners("close");
|
|
}
|
|
|
|
if (recording.videoStream) {
|
|
recording.videoStream.destroy();
|
|
}
|
|
if (recording.connectionStream) {
|
|
recording.connectionStream.destroy();
|
|
}
|
|
if (recording.connection) {
|
|
recording.connection.disconnect();
|
|
}
|
|
}
|
|
|
|
_handleRecordingError(voiceChannelId) {
|
|
const recording = activeRecordings.get(voiceChannelId);
|
|
if (recording) {
|
|
this._cleanupRecording(recording);
|
|
activeRecordings.delete(voiceChannelId);
|
|
}
|
|
}
|
|
|
|
_processVideoFile(recording, textChannel, duration) {
|
|
try {
|
|
if (fs.existsSync(recording.videoPath)) {
|
|
const stats = fs.statSync(recording.videoPath);
|
|
if (stats.size > 0) {
|
|
textChannel.send(
|
|
VIDEO_CONFIG.MESSAGES.SAVED.replace(
|
|
"{filename}",
|
|
recording.videoFilename
|
|
)
|
|
);
|
|
textChannel.send(VIDEO_CONFIG.MESSAGES.LOCATION);
|
|
console.log(
|
|
`[Docker] Video recording completed: ${recording.videoFilename}, Duration: ${duration}s`
|
|
);
|
|
} else {
|
|
textChannel.send(`❌ ${VIDEO_CONFIG.ERRORS.NO_VIDEO_DATA}`);
|
|
fs.unlinkSync(recording.videoPath); // Delete empty file
|
|
}
|
|
} else {
|
|
textChannel.send(`❌ ${VIDEO_CONFIG.ERRORS.FILE_NOT_CREATED}`);
|
|
}
|
|
} catch (error) {
|
|
console.error("[Docker] Error processing video file:", error);
|
|
textChannel.send(
|
|
`⚠️ ${VIDEO_CONFIG.ERRORS.PROCESSING_FAILED} File: ${recording.videoFilename}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const videoRecordingService = new VideoRecordingService();
|
|
export default videoRecordingService;
|