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"]
|
||||
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 ---
|
||||
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
|
||||
COPY package*.json ./
|
||||
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
|
||||
- .:/opt/bot
|
||||
# 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:
|
||||
- "5901:5901" # VNC
|
||||
- "3000:3000" # optional HTTP server if you add one
|
||||
tmpfs:
|
||||
- /tmp:size=512M
|
||||
# volumes:
|
||||
# node_modules:
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
node_modules:
|
||||
|
|
@ -164,4 +164,4 @@ services:
|
|||
networks:
|
||||
- backend
|
||||
```
|
||||
This allows the Teto bot to communicate with `vllm-openai`, `wyoming-whisper`, and `wyoming-piper` using their service names as hostnames.
|
||||
This allows the Teto bot to communicate with `vllm-openai`, `wyoming-whisper`, and `wyoming-piper` using their service names as hostnames.
|
||||
12
entry.sh
12
entry.sh
|
|
@ -13,12 +13,12 @@ openbox &
|
|||
x11vnc -display :99 -shared -forever -nopw -rfbport 5901 -bg &
|
||||
|
||||
# Launch discord in the background. THIS IS THE FIX.
|
||||
# discord \
|
||||
# --no-sandbox \
|
||||
# --disable-dev-shm-usage \
|
||||
# --disable-gpu \
|
||||
# --disable-background-timer-throttling \
|
||||
# --disable-renderer-backgrounding &
|
||||
discord \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--disable-gpu \
|
||||
--disable-background-timer-throttling \
|
||||
--disable-renderer-backgrounding &
|
||||
|
||||
# Give Discord a moment to start before launching the controller script
|
||||
sleep 10
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import videoRecordingService from "./videoRecording.js";
|
||||
import webcamRecordingService from "./webcamRecording.js";
|
||||
import { VIDEO_CONFIG } from "../config/videoConfig.js";
|
||||
|
||||
class CommandHandler {
|
||||
|
|
@ -115,50 +114,37 @@ class CommandHandler {
|
|||
this.commands.set("status", {
|
||||
trigger: (msg) => msg.content.toLowerCase() === "teto status",
|
||||
execute: async (msg, context) => {
|
||||
const videoStatus = videoRecordingService.getStatus();
|
||||
const webcamStatus = webcamRecordingService.getStatus();
|
||||
const status = videoRecordingService.getStatus();
|
||||
|
||||
const statusMsg = [
|
||||
VIDEO_CONFIG.STATUS_TEMPLATE.TITLE,
|
||||
VIDEO_CONFIG.STATUS_TEMPLATE.ACTIVE_RECORDINGS.replace(
|
||||
"{count}",
|
||||
videoStatus.activeRecordings
|
||||
status.activeRecordings
|
||||
),
|
||||
`🎥 Active webcam recordings: ${webcamStatus.activeWebcamRecordings}`,
|
||||
VIDEO_CONFIG.STATUS_TEMPLATE.CONTAINER_OUTPUT.replace(
|
||||
"{path}",
|
||||
videoStatus.outputDir
|
||||
status.outputDir
|
||||
),
|
||||
VIDEO_CONFIG.STATUS_TEMPLATE.HOST_OUTPUT,
|
||||
VIDEO_CONFIG.STATUS_TEMPLATE.VIDEO_FILES.replace(
|
||||
"{count}",
|
||||
videoStatus.videoFiles
|
||||
status.videoFiles
|
||||
),
|
||||
`🎥 Webcam files: ${webcamStatus.webcamFiles}`,
|
||||
videoStatus.videoFiles > 0
|
||||
status.videoFiles > 0
|
||||
? VIDEO_CONFIG.STATUS_TEMPLATE.RECENT_FILES.replace(
|
||||
"{files}",
|
||||
videoStatus.recentFiles.join(", ")
|
||||
status.recentFiles.join(", ")
|
||||
)
|
||||
: "",
|
||||
webcamStatus.webcamFiles > 0
|
||||
? `📋 Recent webcam files: ${webcamStatus.recentWebcamFiles.join(
|
||||
", "
|
||||
)}`
|
||||
: "",
|
||||
videoStatus.activeRecordings > 0
|
||||
status.activeRecordings > 0
|
||||
? VIDEO_CONFIG.STATUS_TEMPLATE.CURRENTLY_RECORDING.replace(
|
||||
"{channels}",
|
||||
videoStatus.activeRecordingsList
|
||||
status.activeRecordingsList
|
||||
.map((r) => r.voiceChannelName)
|
||||
.join(", ")
|
||||
)
|
||||
: "",
|
||||
webcamStatus.activeWebcamRecordings > 0
|
||||
? `🎥 Currently recording webcams in: ${webcamStatus.activeWebcamRecordingsList
|
||||
.map((r) => r.voiceChannelName)
|
||||
.join(", ")}`
|
||||
: "",
|
||||
VIDEO_CONFIG.STATUS_TEMPLATE.UPTIME.replace(
|
||||
"{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
|
||||
this.commands.set("hello", {
|
||||
trigger: (msg) => msg.content.toLowerCase().includes("hello teto"),
|
||||
|
|
|
|||
|
|
@ -64,14 +64,6 @@ class VideoRecordingService {
|
|||
// 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,
|
||||
|
|
@ -84,7 +76,6 @@ class VideoRecordingService {
|
|||
textChannelId: textChannel.id,
|
||||
voiceChannelId: voiceChannel.id,
|
||||
voiceChannelName: voiceChannel.name,
|
||||
targetUserId: user.id,
|
||||
};
|
||||
|
||||
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) {
|
||||
setTimeout(() => {
|
||||
if (activeRecordings.has(voiceChannelId)) {
|
||||
|
|
@ -309,13 +252,6 @@ class VideoRecordingService {
|
|||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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