Compare commits
No commits in common. "603032f1dbeebef014122f1ecb62f241e7da2d46" and "2e94820164e7955b86fe574d0cb6573b0ad41f33" have entirely different histories.
603032f1db
...
2e94820164
10 changed files with 22 additions and 1128 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
[submodule "discord.js-selfbot-v13"]
|
[submodule "discord.js-selfbot-v13"]
|
||||||
path = discord.js-selfbot-v13
|
path = discord.js-selfbot-v13
|
||||||
url = https://git.opencodebox.work/MikolajG/discord.js-selfbot-v13.git
|
url = ssh://git@ssh.opencodebox.work:23/MikolajG/discord.js-selfbot-v13.git
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,6 @@
|
||||||
# --- Stage 1: Install node_modules ---
|
# --- Stage 1: Install node_modules ---
|
||||||
FROM node:20-slim as dependencies
|
FROM node:20-slim as dependencies
|
||||||
|
|
||||||
# Install build tools needed for native modules like sodium
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
build-essential libtool autoconf automake libsodium-dev python3 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /opt/bot
|
WORKDIR /opt/bot
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install --no-audit --no-fund
|
RUN npm install --no-audit --no-fund
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 174c89f18e2171461d838b0c1586acbabb9be7e1
|
Subproject commit 683b280067068958a22411d414d0a581b65d3813
|
||||||
|
|
@ -20,11 +20,13 @@ services:
|
||||||
# Mount the current directory into the container for live-reloading
|
# Mount the current directory into the container for live-reloading
|
||||||
- .:/opt/bot
|
- .:/opt/bot
|
||||||
# Use a named volume to keep the container's node_modules from being overwritten
|
# Use a named volume to keep the container's node_modules from being overwritten
|
||||||
# - node_modules:/opt/bot/node_modules
|
- node_modules:/opt/bot/node_modules
|
||||||
ports:
|
ports:
|
||||||
- "5901:5901" # VNC
|
- "5901:5901" # VNC
|
||||||
- "3000:3000" # optional HTTP server if you add one
|
- "3000:3000" # optional HTTP server if you add one
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp:size=512M
|
- /tmp:size=512M
|
||||||
# volumes:
|
restart: unless-stopped
|
||||||
# node_modules:
|
|
||||||
|
volumes:
|
||||||
|
node_modules:
|
||||||
12
entry.sh
12
entry.sh
|
|
@ -13,12 +13,12 @@ openbox &
|
||||||
x11vnc -display :99 -shared -forever -nopw -rfbport 5901 -bg &
|
x11vnc -display :99 -shared -forever -nopw -rfbport 5901 -bg &
|
||||||
|
|
||||||
# Launch discord in the background. THIS IS THE FIX.
|
# Launch discord in the background. THIS IS THE FIX.
|
||||||
# discord \
|
discord \
|
||||||
# --no-sandbox \
|
--no-sandbox \
|
||||||
# --disable-dev-shm-usage \
|
--disable-dev-shm-usage \
|
||||||
# --disable-gpu \
|
--disable-gpu \
|
||||||
# --disable-background-timer-throttling \
|
--disable-background-timer-throttling \
|
||||||
# --disable-renderer-backgrounding &
|
--disable-renderer-backgrounding &
|
||||||
|
|
||||||
# Give Discord a moment to start before launching the controller script
|
# Give Discord a moment to start before launching the controller script
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import videoRecordingService from "./videoRecording.js";
|
import videoRecordingService from "./videoRecording.js";
|
||||||
import webcamRecordingService from "./webcamRecording.js";
|
|
||||||
import { VIDEO_CONFIG } from "../config/videoConfig.js";
|
import { VIDEO_CONFIG } from "../config/videoConfig.js";
|
||||||
|
|
||||||
class CommandHandler {
|
class CommandHandler {
|
||||||
|
|
@ -115,50 +114,37 @@ class CommandHandler {
|
||||||
this.commands.set("status", {
|
this.commands.set("status", {
|
||||||
trigger: (msg) => msg.content.toLowerCase() === "teto status",
|
trigger: (msg) => msg.content.toLowerCase() === "teto status",
|
||||||
execute: async (msg, context) => {
|
execute: async (msg, context) => {
|
||||||
const videoStatus = videoRecordingService.getStatus();
|
const status = videoRecordingService.getStatus();
|
||||||
const webcamStatus = webcamRecordingService.getStatus();
|
|
||||||
|
|
||||||
const statusMsg = [
|
const statusMsg = [
|
||||||
VIDEO_CONFIG.STATUS_TEMPLATE.TITLE,
|
VIDEO_CONFIG.STATUS_TEMPLATE.TITLE,
|
||||||
VIDEO_CONFIG.STATUS_TEMPLATE.ACTIVE_RECORDINGS.replace(
|
VIDEO_CONFIG.STATUS_TEMPLATE.ACTIVE_RECORDINGS.replace(
|
||||||
"{count}",
|
"{count}",
|
||||||
videoStatus.activeRecordings
|
status.activeRecordings
|
||||||
),
|
),
|
||||||
`🎥 Active webcam recordings: ${webcamStatus.activeWebcamRecordings}`,
|
|
||||||
VIDEO_CONFIG.STATUS_TEMPLATE.CONTAINER_OUTPUT.replace(
|
VIDEO_CONFIG.STATUS_TEMPLATE.CONTAINER_OUTPUT.replace(
|
||||||
"{path}",
|
"{path}",
|
||||||
videoStatus.outputDir
|
status.outputDir
|
||||||
),
|
),
|
||||||
VIDEO_CONFIG.STATUS_TEMPLATE.HOST_OUTPUT,
|
VIDEO_CONFIG.STATUS_TEMPLATE.HOST_OUTPUT,
|
||||||
VIDEO_CONFIG.STATUS_TEMPLATE.VIDEO_FILES.replace(
|
VIDEO_CONFIG.STATUS_TEMPLATE.VIDEO_FILES.replace(
|
||||||
"{count}",
|
"{count}",
|
||||||
videoStatus.videoFiles
|
status.videoFiles
|
||||||
),
|
),
|
||||||
`🎥 Webcam files: ${webcamStatus.webcamFiles}`,
|
status.videoFiles > 0
|
||||||
videoStatus.videoFiles > 0
|
|
||||||
? VIDEO_CONFIG.STATUS_TEMPLATE.RECENT_FILES.replace(
|
? VIDEO_CONFIG.STATUS_TEMPLATE.RECENT_FILES.replace(
|
||||||
"{files}",
|
"{files}",
|
||||||
videoStatus.recentFiles.join(", ")
|
status.recentFiles.join(", ")
|
||||||
)
|
)
|
||||||
: "",
|
: "",
|
||||||
webcamStatus.webcamFiles > 0
|
status.activeRecordings > 0
|
||||||
? `📋 Recent webcam files: ${webcamStatus.recentWebcamFiles.join(
|
|
||||||
", "
|
|
||||||
)}`
|
|
||||||
: "",
|
|
||||||
videoStatus.activeRecordings > 0
|
|
||||||
? VIDEO_CONFIG.STATUS_TEMPLATE.CURRENTLY_RECORDING.replace(
|
? VIDEO_CONFIG.STATUS_TEMPLATE.CURRENTLY_RECORDING.replace(
|
||||||
"{channels}",
|
"{channels}",
|
||||||
videoStatus.activeRecordingsList
|
status.activeRecordingsList
|
||||||
.map((r) => r.voiceChannelName)
|
.map((r) => r.voiceChannelName)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
)
|
)
|
||||||
: "",
|
: "",
|
||||||
webcamStatus.activeWebcamRecordings > 0
|
|
||||||
? `🎥 Currently recording webcams in: ${webcamStatus.activeWebcamRecordingsList
|
|
||||||
.map((r) => r.voiceChannelName)
|
|
||||||
.join(", ")}`
|
|
||||||
: "",
|
|
||||||
VIDEO_CONFIG.STATUS_TEMPLATE.UPTIME.replace(
|
VIDEO_CONFIG.STATUS_TEMPLATE.UPTIME.replace(
|
||||||
"{uptime}",
|
"{uptime}",
|
||||||
Math.round(process.uptime())
|
Math.round(process.uptime())
|
||||||
|
|
@ -180,362 +166,6 @@ class CommandHandler {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start webcam recording command
|
|
||||||
this.commands.set("start_webcam_recording", {
|
|
||||||
trigger: (msg) =>
|
|
||||||
msg.content.toLowerCase() === "record webcam" ||
|
|
||||||
msg.content.toLowerCase() === "xbox record webcam",
|
|
||||||
execute: async (msg, context) => {
|
|
||||||
const { client } = context;
|
|
||||||
|
|
||||||
// Check if user is in a voice channel
|
|
||||||
const member = msg.guild?.members.cache.get(msg.author.id);
|
|
||||||
const voiceChannel = member?.voice?.channel;
|
|
||||||
|
|
||||||
if (!voiceChannel) {
|
|
||||||
msg.channel.send(VIDEO_CONFIG.ERRORS.NO_VOICE_CHANNEL);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.channel.send(
|
|
||||||
`🎥 Trying to start webcam recording... in ${voiceChannel.name}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await webcamRecordingService.startRecording({
|
|
||||||
client,
|
|
||||||
voiceChannel,
|
|
||||||
user: msg.author,
|
|
||||||
textChannel: msg.channel,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
msg.channel.send("🎥 Starting webcam recording... 📹");
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Started webcam recording ${msg.author.tag} in ${voiceChannel.name}`,
|
|
||||||
channel: `#${msg.channel.name}`,
|
|
||||||
icon: "🎥",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
if (result.message.includes("Already recording")) {
|
|
||||||
msg.channel.send(result.message);
|
|
||||||
} else if (result.message.includes("does not have webcam")) {
|
|
||||||
msg.channel.send(result.message);
|
|
||||||
} else {
|
|
||||||
msg.channel.send(
|
|
||||||
"Failed to start webcam recording. Check container logs for details!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stop webcam recording command
|
|
||||||
this.commands.set("stop_webcam_recording", {
|
|
||||||
trigger: (msg) =>
|
|
||||||
msg.content.toLowerCase() === "stop webcam" ||
|
|
||||||
msg.content.toLowerCase() === "xbox stop webcam",
|
|
||||||
execute: async (msg, context) => {
|
|
||||||
const member = msg.guild?.members.cache.get(msg.author.id);
|
|
||||||
const voiceChannel = member?.voice?.channel;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!voiceChannel ||
|
|
||||||
!webcamRecordingService.isRecording(voiceChannel.id)
|
|
||||||
) {
|
|
||||||
msg.channel.send("No active webcam recording in your voice channel!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await webcamRecordingService.stopRecording(
|
|
||||||
voiceChannel.id,
|
|
||||||
msg.channel,
|
|
||||||
"🎥 Webcam recording stopped by user."
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Stopped webcam recording by ${msg.author.tag} in ${voiceChannel.name}`,
|
|
||||||
channel: `#${msg.channel.name}`,
|
|
||||||
icon: "⏹️",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enhanced status command to include webcam recordings
|
|
||||||
this.commands.set("webcam_status", {
|
|
||||||
trigger: (msg) => msg.content.toLowerCase() === "webcam status",
|
|
||||||
execute: async (msg, context) => {
|
|
||||||
const videoStatus = videoRecordingService.getStatus();
|
|
||||||
const webcamStatus = webcamRecordingService.getStatus();
|
|
||||||
|
|
||||||
const statusMsg = [
|
|
||||||
"🤖 **Teto Recording Status (Docker)**",
|
|
||||||
`📹 Active stream recordings: ${videoStatus.activeRecordings}`,
|
|
||||||
`🎥 Active webcam recordings: ${webcamStatus.activeWebcamRecordings}`,
|
|
||||||
`📁 Container output: ${videoStatus.outputDir}`,
|
|
||||||
"📁 Host output: ./output (volume mounted)",
|
|
||||||
`🎬 Stream video files: ${videoStatus.videoFiles}`,
|
|
||||||
`🎥 Webcam video files: ${webcamStatus.webcamFiles}`,
|
|
||||||
videoStatus.videoFiles > 0
|
|
||||||
? `📋 Recent stream files: ${videoStatus.recentFiles.join(", ")}`
|
|
||||||
: "",
|
|
||||||
webcamStatus.webcamFiles > 0
|
|
||||||
? `📋 Recent webcam files: ${webcamStatus.recentWebcamFiles.join(
|
|
||||||
", "
|
|
||||||
)}`
|
|
||||||
: "",
|
|
||||||
videoStatus.activeRecordings > 0
|
|
||||||
? `🔴 Currently recording streams in: ${videoStatus.activeRecordingsList
|
|
||||||
.map((r) => r.voiceChannelName)
|
|
||||||
.join(", ")}`
|
|
||||||
: "",
|
|
||||||
webcamStatus.activeWebcamRecordings > 0
|
|
||||||
? `🎥 Currently recording webcams in: ${webcamStatus.activeWebcamRecordingsList
|
|
||||||
.map((r) => r.voiceChannelName)
|
|
||||||
.join(", ")}`
|
|
||||||
: "",
|
|
||||||
`⏰ Bot uptime: ${Math.round(process.uptime())}s`,
|
|
||||||
"🐳 Running in Docker container",
|
|
||||||
]
|
|
||||||
.filter((line) => line)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
msg.channel.send(statusMsg);
|
|
||||||
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Webcam status requested by ${msg.author.tag}`,
|
|
||||||
channel: `#${msg.channel.name || "DM"}`,
|
|
||||||
icon: "📊",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug voice state command
|
|
||||||
this.commands.set("debug_voice", {
|
|
||||||
trigger: (msg) => msg.content.toLowerCase() === "debug voice",
|
|
||||||
execute: async (msg, context) => {
|
|
||||||
const member = msg.guild?.members.cache.get(msg.author.id);
|
|
||||||
const voiceChannel = member?.voice?.channel;
|
|
||||||
|
|
||||||
if (!voiceChannel) {
|
|
||||||
msg.channel.send(
|
|
||||||
"❌ You need to be in a voice channel for voice state debugging!"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const voiceState =
|
|
||||||
voiceChannel.guild?.voiceStates.cache.get(msg.author.id) ||
|
|
||||||
context.client.voiceStates.cache.get(msg.author.id);
|
|
||||||
|
|
||||||
// Get voice connection and SSRC info if available
|
|
||||||
const voiceConnection = context.client.voice.connections.get(
|
|
||||||
voiceChannel.guild.id
|
|
||||||
);
|
|
||||||
let ssrcInfo = [];
|
|
||||||
|
|
||||||
if (voiceConnection && voiceConnection.ssrcMap) {
|
|
||||||
ssrcInfo = [
|
|
||||||
"",
|
|
||||||
"**🔊 SSRC Mapping:**",
|
|
||||||
`• Connection status: ${voiceConnection.status}`,
|
|
||||||
`• Total mapped SSRCs: ${voiceConnection.ssrcMap.size}`,
|
|
||||||
...Array.from(voiceConnection.ssrcMap.entries()).map(
|
|
||||||
([ssrc, data]) => {
|
|
||||||
const streamType = data.streamType
|
|
||||||
? ` (${data.streamType})`
|
|
||||||
: "";
|
|
||||||
const quality = data.quality ? ` Q:${data.quality}` : "";
|
|
||||||
return `• SSRC ${ssrc}: ${data.userId}${streamType}${quality}`;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const debugInfo = [
|
|
||||||
"🔍 **Voice State Debug Info**",
|
|
||||||
`📍 Channel: ${voiceChannel.name} (${voiceChannel.id})`,
|
|
||||||
`👤 User: ${msg.author.tag} (${msg.author.id})`,
|
|
||||||
"",
|
|
||||||
"**Voice State Properties:**",
|
|
||||||
`• Has voice state: ${!!voiceState}`,
|
|
||||||
`• Self video: ${voiceState?.selfVideo ?? "undefined"}`,
|
|
||||||
`• Streaming: ${voiceState?.streaming ?? "undefined"}`,
|
|
||||||
`• Server mute: ${voiceState?.serverMute ?? "undefined"}`,
|
|
||||||
`• Server deaf: ${voiceState?.serverDeaf ?? "undefined"}`,
|
|
||||||
`• Self mute: ${voiceState?.selfMute ?? "undefined"}`,
|
|
||||||
`• Self deaf: ${voiceState?.selfDeaf ?? "undefined"}`,
|
|
||||||
`• Channel ID: ${voiceState?.channelId ?? "undefined"}`,
|
|
||||||
`• Session ID: ${voiceState?.sessionId ?? "undefined"}`,
|
|
||||||
"",
|
|
||||||
"**Other Users in Channel:**",
|
|
||||||
...voiceChannel.members.map((m) => {
|
|
||||||
const otherState = voiceChannel.guild?.voiceStates.cache.get(m.id);
|
|
||||||
return `• ${m.user.tag}: selfVideo=${otherState?.selfVideo}, streaming=${otherState?.streaming}`;
|
|
||||||
}),
|
|
||||||
...ssrcInfo,
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
msg.channel.send(debugInfo);
|
|
||||||
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Voice debug requested by ${msg.author.tag}`,
|
|
||||||
channel: `#${msg.channel.name}`,
|
|
||||||
icon: "🔍",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test voice connectivity command
|
|
||||||
this.commands.set("test_voice", {
|
|
||||||
trigger: (msg) => msg.content.toLowerCase() === "test voice",
|
|
||||||
execute: async (msg, context) => {
|
|
||||||
const member = msg.guild?.members.cache.get(msg.author.id);
|
|
||||||
const voiceChannel = member?.voice?.channel;
|
|
||||||
|
|
||||||
if (!voiceChannel) {
|
|
||||||
msg.channel.send(
|
|
||||||
"❌ You need to be in a voice channel to test voice connectivity!"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.channel.send("🔧 Testing basic voice server connectivity...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const testStart = Date.now();
|
|
||||||
|
|
||||||
// Simple connection test with timeout promise
|
|
||||||
const connectionPromise = context.client.voice.joinChannel(
|
|
||||||
voiceChannel.id,
|
|
||||||
{
|
|
||||||
selfMute: true,
|
|
||||||
selfDeaf: true,
|
|
||||||
selfVideo: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error("Manual timeout after 45 seconds")),
|
|
||||||
45000
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const connection = await Promise.race([
|
|
||||||
connectionPromise,
|
|
||||||
timeoutPromise,
|
|
||||||
]);
|
|
||||||
const connectTime = Date.now() - testStart;
|
|
||||||
|
|
||||||
msg.channel.send(
|
|
||||||
`✅ Voice connection successful!\n` +
|
|
||||||
`• Connect time: ${connectTime}ms\n` +
|
|
||||||
`• Status: ${connection.status}\n` +
|
|
||||||
`• Channel: ${voiceChannel.name}\n` +
|
|
||||||
`• Authentication: ${
|
|
||||||
connection.authentication ? "Ready" : "Pending"
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Disconnect after test
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
connection.disconnect();
|
|
||||||
} catch (e) {
|
|
||||||
console.log(
|
|
||||||
"[Docker] Error disconnecting test connection:",
|
|
||||||
e.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Voice connectivity test by ${msg.author.tag}`,
|
|
||||||
channel: `#${msg.channel.name}`,
|
|
||||||
icon: "🔧",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const connectTime = Date.now() - testStart;
|
|
||||||
msg.channel.send(
|
|
||||||
`❌ Voice connection failed after ${connectTime}ms!\n` +
|
|
||||||
`• Error: ${error.message}\n` +
|
|
||||||
`• This indicates Discord voice server or network issues\n` +
|
|
||||||
`• Try switching voice channels or try again later`
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Voice connectivity test failed by ${msg.author.tag}`,
|
|
||||||
channel: `#${msg.channel.name}`,
|
|
||||||
icon: "❌",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Help command
|
|
||||||
this.commands.set("help", {
|
|
||||||
trigger: (msg) =>
|
|
||||||
msg.content.toLowerCase() === "teto help" ||
|
|
||||||
msg.content.toLowerCase() === "help",
|
|
||||||
execute: async (msg, context) => {
|
|
||||||
const helpMsg = [
|
|
||||||
"🤖 **Teto Recording Bot Commands**",
|
|
||||||
"",
|
|
||||||
"**📹 Stream Recording:**",
|
|
||||||
"`xbox record that` - Start recording screen share",
|
|
||||||
"`stop recording` or `xbox stop` - Stop stream recording",
|
|
||||||
"",
|
|
||||||
"**🎥 Webcam Recording:**",
|
|
||||||
"`record webcam` or `xbox record webcam` - Start recording webcam",
|
|
||||||
"`stop webcam` or `xbox stop webcam` - Stop webcam recording",
|
|
||||||
"",
|
|
||||||
"**📊 Status & Info:**",
|
|
||||||
"`teto status` - Show overall recording status",
|
|
||||||
"`webcam status` - Show detailed webcam status",
|
|
||||||
"`debug voice` - Show voice state debug info",
|
|
||||||
"`test voice` - Test voice server connectivity",
|
|
||||||
"`teto help` or `help` - Show this help message",
|
|
||||||
"",
|
|
||||||
"**💬 General:**",
|
|
||||||
"`hello teto` - Say hello",
|
|
||||||
"",
|
|
||||||
"**Notes:**",
|
|
||||||
"• You must be in a voice channel to start recordings",
|
|
||||||
"• For webcam recording, you must have your camera turned ON in Discord",
|
|
||||||
"• Recordings auto-stop after 30 seconds (demo limit)",
|
|
||||||
"• Both stream and webcam can be recorded simultaneously",
|
|
||||||
"• Files are saved to ./output/ directory",
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
msg.channel.send(helpMsg);
|
|
||||||
|
|
||||||
return {
|
|
||||||
action: {
|
|
||||||
message: `Help requested by ${msg.author.tag}`,
|
|
||||||
channel: `#${msg.channel.name || "DM"}`,
|
|
||||||
icon: "❓",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hello teto command
|
// Hello teto command
|
||||||
this.commands.set("hello", {
|
this.commands.set("hello", {
|
||||||
trigger: (msg) => msg.content.toLowerCase().includes("hello teto"),
|
trigger: (msg) => msg.content.toLowerCase().includes("hello teto"),
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,6 @@ class VideoRecordingService {
|
||||||
// Set up video stream event handlers
|
// Set up video stream event handlers
|
||||||
this._setupVideoStreamHandlers(videoStream, textChannel, voiceChannel.id);
|
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
|
// Store recording info
|
||||||
const recordingInfo = {
|
const recordingInfo = {
|
||||||
connection,
|
connection,
|
||||||
|
|
@ -84,7 +76,6 @@ class VideoRecordingService {
|
||||||
textChannelId: textChannel.id,
|
textChannelId: textChannel.id,
|
||||||
voiceChannelId: voiceChannel.id,
|
voiceChannelId: voiceChannel.id,
|
||||||
voiceChannelName: voiceChannel.name,
|
voiceChannelName: voiceChannel.name,
|
||||||
targetUserId: user.id,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
activeRecordings.set(voiceChannel.id, recordingInfo);
|
activeRecordings.set(voiceChannel.id, recordingInfo);
|
||||||
|
|
@ -248,54 +239,6 @@ class VideoRecordingService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_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 }) => {
|
|
||||||
// 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) {
|
_setupAutoStop(voiceChannelId, textChannel) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (activeRecordings.has(voiceChannelId)) {
|
if (activeRecordings.has(voiceChannelId)) {
|
||||||
|
|
@ -309,13 +252,6 @@ class VideoRecordingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanupRecording(recording) {
|
_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) {
|
if (recording.videoStream) {
|
||||||
recording.videoStream.destroy();
|
recording.videoStream.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,668 +0,0 @@
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { createWriteStream } from "fs";
|
|
||||||
import { VIDEO_CONFIG } from "../config/videoConfig.js";
|
|
||||||
|
|
||||||
// Store active webcam recordings
|
|
||||||
const activeWebcamRecordings = new Map();
|
|
||||||
|
|
||||||
class WebcamRecordingService {
|
|
||||||
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 webcam 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 webcam
|
|
||||||
if (activeWebcamRecordings.has(voiceChannel.id)) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Already recording webcam in this channel!",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure output directory exists
|
|
||||||
this._ensureOutputDirectory();
|
|
||||||
|
|
||||||
// Create unique filename with timestamp
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
||||||
const webcamFilename = `webcam-${timestamp}${VIDEO_CONFIG.VIDEO_EXTENSION}`;
|
|
||||||
const webcamPath = path.join(this.outputDir, webcamFilename);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[Docker] Attempting to join voice channel for webcam recording: ${voiceChannel.name}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Join the voice channel with retry logic
|
|
||||||
let connection;
|
|
||||||
let connectionRetries = 0;
|
|
||||||
const maxConnectionRetries = 3;
|
|
||||||
|
|
||||||
while (connectionRetries < maxConnectionRetries) {
|
|
||||||
try {
|
|
||||||
connection = await client.voice.joinChannel(
|
|
||||||
voiceChannel.id,
|
|
||||||
VIDEO_CONFIG.VOICE_SETTINGS
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[Docker] Successfully joined voice channel for webcam recording`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
connectionRetries++;
|
|
||||||
console.log(
|
|
||||||
`[Docker] Voice connection failed (attempt ${connectionRetries}/${maxConnectionRetries}):`,
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
|
|
||||||
if (connectionRetries === maxConnectionRetries) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to join voice channel after ${maxConnectionRetries} attempts: ${error.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait before retry with exponential backoff
|
|
||||||
const delay = Math.min(
|
|
||||||
5000 * Math.pow(2, connectionRetries - 1),
|
|
||||||
30000
|
|
||||||
);
|
|
||||||
console.log(`[Docker] Retrying voice connection in ${delay}ms...`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait longer for connection and SSRC mapping to be fully established
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 8000));
|
|
||||||
|
|
||||||
// Wait for SSRC mapping to be populated
|
|
||||||
let ssrcRetries = 0;
|
|
||||||
const maxSSRCRetries = 15;
|
|
||||||
while (ssrcRetries < maxSSRCRetries) {
|
|
||||||
if (connection.ssrcMap && connection.ssrcMap.size > 0) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] SSRC mapping ready with ${connection.ssrcMap.size} entries after ${ssrcRetries} retries`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`[Docker] Waiting for SSRC mapping... (${
|
|
||||||
ssrcRetries + 1
|
|
||||||
}/${maxSSRCRetries})`
|
|
||||||
);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
ssrcRetries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for user's camera to be detected in voice state
|
|
||||||
let retries = 0;
|
|
||||||
const maxRetries = 10;
|
|
||||||
while (retries < maxRetries) {
|
|
||||||
const voiceState = voiceChannel.guild?.voiceStates.cache.get(user.id);
|
|
||||||
if (voiceState && voiceState.selfVideo) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Camera detected for user ${user.id} after ${retries} retries`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`[Docker] Waiting for camera to be detected for user ${user.id}... (${
|
|
||||||
retries + 1
|
|
||||||
}/${maxRetries})`
|
|
||||||
);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
retries++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retries === maxRetries) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Camera not detected after ${maxRetries} retries, proceeding anyway...`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log final SSRC state before starting recording
|
|
||||||
console.log(
|
|
||||||
`[Docker] Starting webcam recording with SSRC map:`,
|
|
||||||
Array.from(connection.ssrcMap.entries()).map(([ssrc, data]) => ({
|
|
||||||
ssrc,
|
|
||||||
userId: data.userId?.slice(-4),
|
|
||||||
streamType: data.streamType,
|
|
||||||
hasVideo: data.hasVideo,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[Docker] Creating webcam stream for user ${user.id}`);
|
|
||||||
|
|
||||||
// Create webcam stream with retry logic and connection monitoring
|
|
||||||
let webcamStream;
|
|
||||||
let streamRetries = 0;
|
|
||||||
const maxStreamRetries = 3;
|
|
||||||
|
|
||||||
while (streamRetries < maxStreamRetries) {
|
|
||||||
try {
|
|
||||||
webcamStream = connection.receiver.createWebcamStream(
|
|
||||||
user.id,
|
|
||||||
createWriteStream(webcamPath)
|
|
||||||
);
|
|
||||||
console.log(`[Docker] Webcam stream created successfully`);
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
streamRetries++;
|
|
||||||
console.log(
|
|
||||||
`[Docker] Failed to create webcam stream (attempt ${streamRetries}/${maxStreamRetries}):`,
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
|
|
||||||
if (streamRetries === maxStreamRetries) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait before retry and check connection health
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Check if connection is still healthy
|
|
||||||
if (!this._isConnectionHealthy(connection)) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Connection status changed to: ${connection.status}`
|
|
||||||
);
|
|
||||||
throw new Error("Voice connection lost during stream creation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up webcam stream event handlers
|
|
||||||
this._setupWebcamStreamHandlers(
|
|
||||||
webcamStream,
|
|
||||||
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,
|
|
||||||
webcamStream,
|
|
||||||
webcamFilename,
|
|
||||||
webcamPath,
|
|
||||||
startTime: Date.now(),
|
|
||||||
userId: user.id,
|
|
||||||
textChannelId: textChannel.id,
|
|
||||||
voiceChannelId: voiceChannel.id,
|
|
||||||
voiceChannelName: voiceChannel.name,
|
|
||||||
targetUserId: user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
activeWebcamRecordings.set(voiceChannel.id, recordingInfo);
|
|
||||||
|
|
||||||
console.log(`[Docker] Started webcam recording in ${voiceChannel.name}`);
|
|
||||||
console.log(`[Docker] Webcam output will be saved to: ${this.outputDir}`);
|
|
||||||
|
|
||||||
// Set up auto-stop timeout
|
|
||||||
this._setupAutoStop(voiceChannel.id, textChannel);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Webcam recording started successfully",
|
|
||||||
recordingInfo,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Docker] Error starting webcam recording:", error);
|
|
||||||
|
|
||||||
// Handle specific error types
|
|
||||||
if (error.message === "VOICE_USER_NO_WEBCAM") {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "❌ User does not have webcam/camera enabled.",
|
|
||||||
error: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("VOICE_CONNECTION_TIMEOUT")) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
"❌ Voice connection timed out. Discord voice servers may be slow or unavailable.",
|
|
||||||
error: "VOICE_CONNECTION_TIMEOUT",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("Failed to join voice channel")) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
"❌ Could not connect to voice channel after multiple attempts. Try again later.",
|
|
||||||
error: "VOICE_CONNECTION_FAILED",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("Voice connection lost")) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "❌ Voice connection was lost during setup.",
|
|
||||||
error: "VOICE_CONNECTION_LOST",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Failed to start webcam recording",
|
|
||||||
error: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop webcam 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 = "Webcam recording stopped."
|
|
||||||
) {
|
|
||||||
const recording = activeWebcamRecordings.get(voiceChannelId);
|
|
||||||
|
|
||||||
if (!recording) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "No active webcam recording found",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Stopping webcam recording for user: ${recording.userId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up streams and connections
|
|
||||||
this._cleanupRecording(recording);
|
|
||||||
|
|
||||||
// Remove from active recordings
|
|
||||||
activeWebcamRecordings.delete(voiceChannelId);
|
|
||||||
|
|
||||||
const duration = Math.round((Date.now() - recording.startTime) / 1000);
|
|
||||||
|
|
||||||
textChannel.send(
|
|
||||||
`${stopMessage} Duration: ${duration}s. Processing webcam file...`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Process the webcam file after a delay
|
|
||||||
setTimeout(() => {
|
|
||||||
this._processWebcamFile(recording, textChannel, duration);
|
|
||||||
}, this.processingDelay);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Webcam recording stopped successfully",
|
|
||||||
duration,
|
|
||||||
filename: recording.webcamFilename,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Docker] Error stopping webcam recording:", error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
"Error stopping webcam recording, but file may have been saved",
|
|
||||||
error: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get status of all active webcam recordings and saved files
|
|
||||||
* @returns {Object} Status information
|
|
||||||
*/
|
|
||||||
getStatus() {
|
|
||||||
const activeCount = activeWebcamRecordings.size;
|
|
||||||
let webcamFileCount = 0;
|
|
||||||
let webcamFiles = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(this.outputDir)) {
|
|
||||||
const dirContents = fs.readdirSync(this.outputDir);
|
|
||||||
webcamFiles = dirContents.filter(
|
|
||||||
(file) =>
|
|
||||||
file.startsWith("webcam-") &&
|
|
||||||
file.endsWith(VIDEO_CONFIG.VIDEO_EXTENSION)
|
|
||||||
);
|
|
||||||
webcamFileCount = webcamFiles.length;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading output directory for webcam files:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeWebcamRecordings: activeCount,
|
|
||||||
outputDir: this.outputDir,
|
|
||||||
webcamFiles: webcamFileCount,
|
|
||||||
recentWebcamFiles: webcamFiles.slice(-3),
|
|
||||||
activeWebcamRecordingsList: Array.from(
|
|
||||||
activeWebcamRecordings.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 webcam recording
|
|
||||||
* @param {string} voiceChannelId - Voice channel ID
|
|
||||||
* @returns {boolean} Whether webcam recording is active
|
|
||||||
*/
|
|
||||||
isRecording(voiceChannelId) {
|
|
||||||
return activeWebcamRecordings.has(voiceChannelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get webcam 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 activeWebcamRecordings.get(voiceChannelId) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private methods
|
|
||||||
|
|
||||||
_ensureOutputDirectory() {
|
|
||||||
if (!fs.existsSync(this.outputDir)) {
|
|
||||||
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupWebcamStreamHandlers(webcamStream, textChannel, voiceChannelId) {
|
|
||||||
let frameCount = 0;
|
|
||||||
let lastFrameTime = Date.now();
|
|
||||||
let dataReceived = false;
|
|
||||||
let connectionTimeouts = 0;
|
|
||||||
const maxConnectionTimeouts = 3;
|
|
||||||
|
|
||||||
webcamStream.on("ready", () => {
|
|
||||||
console.log("[Docker] FFmpeg process ready for webcam recording!");
|
|
||||||
textChannel.send("🎥 Webcam recording started successfully!");
|
|
||||||
|
|
||||||
webcamStream.stream.stderr.on("data", (data) => {
|
|
||||||
const dataStr = data.toString();
|
|
||||||
dataReceived = true;
|
|
||||||
|
|
||||||
// Only log important FFmpeg messages to reduce spam
|
|
||||||
if (
|
|
||||||
dataStr.includes("frame=") ||
|
|
||||||
dataStr.includes("error") ||
|
|
||||||
dataStr.includes("timeout")
|
|
||||||
) {
|
|
||||||
console.log(`[Docker] Webcam FFmpeg: ${dataStr}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track frame production
|
|
||||||
const frameMatch = dataStr.match(/frame=\s*(\d+)/);
|
|
||||||
if (frameMatch) {
|
|
||||||
frameCount = parseInt(frameMatch[1]);
|
|
||||||
lastFrameTime = Date.now();
|
|
||||||
connectionTimeouts = 0; // Reset timeout counter on successful frames
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for connection timeouts
|
|
||||||
if (
|
|
||||||
dataStr.includes("Connection timed out") ||
|
|
||||||
dataStr.includes("pipe:")
|
|
||||||
) {
|
|
||||||
connectionTimeouts++;
|
|
||||||
console.log(
|
|
||||||
`[Docker] FFmpeg connection timeout detected (${connectionTimeouts}/${maxConnectionTimeouts})`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (connectionTimeouts >= maxConnectionTimeouts) {
|
|
||||||
console.log(
|
|
||||||
"[Docker] Too many connection timeouts, stopping webcam recording"
|
|
||||||
);
|
|
||||||
textChannel.send(
|
|
||||||
"⚠️ Webcam recording stopped - connection unstable."
|
|
||||||
);
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - connection timeouts."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Monitor for frame production - stop if no frames after 45 seconds (increased from 30)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (frameCount === 0) {
|
|
||||||
console.log(
|
|
||||||
"[Docker] No frames produced after 45 seconds, stopping webcam recording"
|
|
||||||
);
|
|
||||||
textChannel.send(
|
|
||||||
"⚠️ Webcam recording stopped - no video frames were captured. User may not have camera enabled."
|
|
||||||
);
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - no frames detected."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 45000);
|
|
||||||
|
|
||||||
// Check for stalled recording every 30 seconds (reduced from 60)
|
|
||||||
const stallCheckInterval = setInterval(() => {
|
|
||||||
const timeSinceLastFrame = Date.now() - lastFrameTime;
|
|
||||||
|
|
||||||
// More aggressive stall detection
|
|
||||||
if (frameCount > 0 && timeSinceLastFrame > 45000) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Webcam recording stalled - no frames for ${timeSinceLastFrame}ms`
|
|
||||||
);
|
|
||||||
textChannel.send("⚠️ Webcam recording appears stalled - stopping.");
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - stream stalled."
|
|
||||||
);
|
|
||||||
clearInterval(stallCheckInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're receiving any data at all
|
|
||||||
if (!dataReceived && Date.now() - lastFrameTime > 30000) {
|
|
||||||
console.log(
|
|
||||||
"[Docker] No FFmpeg data received, connection may be broken"
|
|
||||||
);
|
|
||||||
textChannel.send("⚠️ Webcam recording stopped - no data received.");
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - no data received."
|
|
||||||
);
|
|
||||||
clearInterval(stallCheckInterval);
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
// Store interval for cleanup
|
|
||||||
const recording = activeWebcamRecordings.get(voiceChannelId);
|
|
||||||
if (recording) {
|
|
||||||
recording.stallCheckInterval = stallCheckInterval;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
webcamStream.on("error", (error) => {
|
|
||||||
console.error("[Docker] Webcam recording error:", error);
|
|
||||||
textChannel.send("🎥 Webcam recording encountered an error!");
|
|
||||||
this._handleRecordingError(voiceChannelId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupVoiceConnectionHandlers(
|
|
||||||
connection,
|
|
||||||
targetUserId,
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel
|
|
||||||
) {
|
|
||||||
// Listen for streaming events to detect when camera is turned off
|
|
||||||
connection.on(
|
|
||||||
"startStreaming",
|
|
||||||
({ video_ssrc, user_id, audio_ssrc, streams }) => {
|
|
||||||
// Check if this is our target user
|
|
||||||
if (user_id === targetUserId) {
|
|
||||||
// Check if any stream is active for video
|
|
||||||
const hasActiveVideo =
|
|
||||||
streams &&
|
|
||||||
streams.some((stream) => stream.active && stream.type === "video");
|
|
||||||
|
|
||||||
// Stop recording if no active video streams or video_ssrc is 0
|
|
||||||
if (!hasActiveVideo || video_ssrc === 0 || !video_ssrc) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Target user ${targetUserId} turned off camera (active video: ${hasActiveVideo}, video_ssrc: ${video_ssrc}), stopping webcam recording`
|
|
||||||
);
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - user turned off camera."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Listen for user disconnect events
|
|
||||||
connection.on("disconnect", (userId) => {
|
|
||||||
if (userId === targetUserId) {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Target user ${targetUserId} disconnected, stopping webcam recording`
|
|
||||||
);
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - user disconnected."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for connection close
|
|
||||||
connection.on("close", () => {
|
|
||||||
console.log(
|
|
||||||
`[Docker] Voice connection closed, stopping webcam recording`
|
|
||||||
);
|
|
||||||
if (this.isRecording(voiceChannelId)) {
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped - voice connection closed."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupAutoStop(voiceChannelId, textChannel) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (activeWebcamRecordings.has(voiceChannelId)) {
|
|
||||||
this.stopRecording(
|
|
||||||
voiceChannelId,
|
|
||||||
textChannel,
|
|
||||||
"🎥 Webcam recording stopped automatically after 30 seconds (demo limit)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, this.autoStopTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
_cleanupRecording(recording) {
|
|
||||||
// Clear any monitoring intervals
|
|
||||||
if (recording.stallCheckInterval) {
|
|
||||||
clearInterval(recording.stallCheckInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all event listeners first
|
|
||||||
if (recording.connection) {
|
|
||||||
recording.connection.removeAllListeners("startStreaming");
|
|
||||||
recording.connection.removeAllListeners("disconnect");
|
|
||||||
recording.connection.removeAllListeners("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recording.webcamStream) {
|
|
||||||
recording.webcamStream.destroy();
|
|
||||||
}
|
|
||||||
if (recording.connection) {
|
|
||||||
recording.connection.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleRecordingError(voiceChannelId) {
|
|
||||||
const recording = activeWebcamRecordings.get(voiceChannelId);
|
|
||||||
if (recording) {
|
|
||||||
this._cleanupRecording(recording);
|
|
||||||
activeWebcamRecordings.delete(voiceChannelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_processWebcamFile(recording, textChannel, duration) {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(recording.webcamPath)) {
|
|
||||||
const stats = fs.statSync(recording.webcamPath);
|
|
||||||
if (stats.size > 0) {
|
|
||||||
textChannel.send(
|
|
||||||
`🎥 Webcam recording saved as: ${recording.webcamFilename}`
|
|
||||||
);
|
|
||||||
textChannel.send(
|
|
||||||
"📁 File location: Container: /tmp/output/ | Host: ./output/"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[Docker] Webcam recording completed: ${recording.webcamFilename}, Duration: ${duration}s`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
textChannel.send(
|
|
||||||
`❌ No webcam video data was captured during the recording.`
|
|
||||||
);
|
|
||||||
fs.unlinkSync(recording.webcamPath); // Delete empty file
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
textChannel.send(
|
|
||||||
`❌ Webcam recording file was not created in container.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Docker] Error processing webcam file:", error);
|
|
||||||
textChannel.send(
|
|
||||||
`⚠️ Webcam recording may have been saved but processing failed. File: ${recording.webcamFilename}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if voice connection is healthy
|
|
||||||
* @param {Object} connection - Voice connection object
|
|
||||||
* @returns {boolean} Whether connection is healthy
|
|
||||||
*/
|
|
||||||
_isConnectionHealthy(connection) {
|
|
||||||
if (!connection) return false;
|
|
||||||
|
|
||||||
const healthyStatuses = ["connected", "connecting", "ready"];
|
|
||||||
return healthyStatuses.includes(connection.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
export const webcamRecordingService = new WebcamRecordingService();
|
|
||||||
export default webcamRecordingService;
|
|
||||||
Loading…
Reference in a new issue