PTT semi focntionnel
This commit is contained in:
parent
08664dda0d
commit
04cad1c49b
249
api_v1_voice.php
249
api_v1_voice.php
@ -1,44 +1,239 @@
|
||||
<?php
|
||||
require_once 'auth/session.php';
|
||||
header('Content-Type: application/json');
|
||||
<?php // Vocal secours — WebRTC P2P + signalisation PHP + participants + PTT + VOX (gating via GainNode)
|
||||
// Mutualisé OVH : nécessite ./data écrivable.
|
||||
declare(strict_types=1);
|
||||
require_once "auth/session.php";
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
|
||||
header("Access-Control-Allow-Headers: Content-Type");
|
||||
|
||||
$DATA_DIR = __DIR__ . "/data";
|
||||
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
|
||||
|
||||
$user = getCurrentUser();
|
||||
if (!$user) {
|
||||
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||
$current_user_id = $user ? (int)$user["id"] : 0;
|
||||
|
||||
function room_id(string $s): string {
|
||||
$s = preg_replace("~[^a-zA-Z0-9_\-]~", "", $s);
|
||||
return $s !== "" ? $s : "secours";
|
||||
}
|
||||
|
||||
function peer_id(): string {
|
||||
return bin2hex(random_bytes(8));
|
||||
}
|
||||
|
||||
function room_log_file(string $room): string {
|
||||
return __DIR__ . "/data/" . $room . ".log";
|
||||
}
|
||||
|
||||
function room_participants_file(string $room): string {
|
||||
return __DIR__ . "/data/" . $room . ".participants.json";
|
||||
}
|
||||
|
||||
function chat_log_file_for_today(): string {
|
||||
// Un fichier par jour : YYYY-MM-DD.chat.log
|
||||
$d = date("Y-m-d");
|
||||
return __DIR__ . "/data/" . $d . ".chat.log";
|
||||
}
|
||||
|
||||
function now_ms(): int {
|
||||
return (int) floor(microtime(true) * 1000);
|
||||
}
|
||||
|
||||
function json_out($data, int $code = 200): void {
|
||||
http_response_code($code);
|
||||
header("Content-Type: application/json; charset=utf-8");
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$channel_id = $_POST['channel_id'] ?? null;
|
||||
function read_json_file(string $path): array {
|
||||
if (!file_exists($path)) return [];
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false || $raw === "") return [];
|
||||
$j = json_decode($raw, true);
|
||||
return is_array($j) ? $j : [];
|
||||
}
|
||||
|
||||
if ($action === 'join' && $channel_id) {
|
||||
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE channel_id = ?");
|
||||
$stmt->execute([$user['id'], $channel_id, $channel_id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} elseif ($action === 'leave') {
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?");
|
||||
$stmt->execute([$user['id']]);
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid action']);
|
||||
function write_json_file(string $path, array $data): void {
|
||||
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
|
||||
}
|
||||
|
||||
function tail_lines(string $path, int $maxLines = 120): array {
|
||||
if (!is_file($path)) return [];
|
||||
$fp = fopen($path, "rb");
|
||||
if (!$fp) return [];
|
||||
|
||||
$lines = [];
|
||||
fseek($fp, 0, SEEK_END);
|
||||
$pos = ftell($fp);
|
||||
$buffer = "";
|
||||
|
||||
while ($pos > 0 && count($lines) < $maxLines) {
|
||||
$readSize = min($pos, 4096);
|
||||
$pos -= $readSize;
|
||||
fseek($fp, $pos);
|
||||
$chunk = fread($fp, $readSize);
|
||||
$buffer = $chunk . $buffer;
|
||||
|
||||
$chunkLines = explode("\n", $buffer);
|
||||
$buffer = array_shift($chunkLines);
|
||||
|
||||
while (!empty($chunkLines)) {
|
||||
$line = array_pop($chunkLines);
|
||||
if (trim($line) !== "") {
|
||||
array_unshift($lines, trim($line));
|
||||
if (count($lines) >= $maxLines) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
exit;
|
||||
fclose($fp);
|
||||
return $lines;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$action = $_GET['action'] ?? '';
|
||||
if ($action === 'sessions') {
|
||||
// Logic for signaling
|
||||
$action = $_REQUEST["action"] ?? "";
|
||||
$room = room_id($_REQUEST["room"] ?? "secours");
|
||||
$my_id = $_REQUEST["peer_id"] ?? "";
|
||||
|
||||
if ($action === "join") {
|
||||
$name = $_REQUEST["name"] ?? "User";
|
||||
$p_file = room_participants_file($room);
|
||||
$ps = read_json_file($p_file);
|
||||
|
||||
// Cleanup old participants (> 10s)
|
||||
$stale_time = now_ms() - 10000;
|
||||
foreach ($ps as $id => $p) {
|
||||
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
|
||||
}
|
||||
|
||||
$new_id = peer_id();
|
||||
$ps[$new_id] = [
|
||||
"id" => $new_id,
|
||||
"user_id" => $current_user_id,
|
||||
"name" => $name,
|
||||
"avatar_url" => $user["avatar_url"] ?? "",
|
||||
"last_seen" => now_ms()
|
||||
];
|
||||
write_json_file($p_file, $ps);
|
||||
|
||||
// DB Integration for sidebar
|
||||
if ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, last_seen) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, last_seen = ?");
|
||||
$stmt->execute([$current_user_id, $room, now_ms(), $room, now_ms()]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
json_out(["success" => true, "peer_id" => $new_id, "participants" => $ps]);
|
||||
}
|
||||
|
||||
if ($action === "poll") {
|
||||
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
|
||||
$p_file = room_participants_file($room);
|
||||
$ps = read_json_file($p_file);
|
||||
|
||||
if (isset($ps[$my_id])) {
|
||||
$ps[$my_id]["last_seen"] = now_ms();
|
||||
}
|
||||
|
||||
$stale_time = now_ms() - 10000;
|
||||
foreach ($ps as $id => $p) {
|
||||
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
|
||||
}
|
||||
write_json_file($p_file, $ps);
|
||||
|
||||
// Update DB last_seen
|
||||
if ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ?");
|
||||
$stmt->execute([now_ms(), $current_user_id]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
// Read signals
|
||||
$log_file = room_log_file($room);
|
||||
$signals = [];
|
||||
if (file_exists($log_file)) {
|
||||
$lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$remaining = [];
|
||||
$now = now_ms();
|
||||
foreach ($lines as $line) {
|
||||
$sig = json_decode($line, true);
|
||||
if ($sig && isset($sig["to"]) && $sig["to"] === $my_id) {
|
||||
$signals[] = $sig;
|
||||
} elseif ($sig && ($now - ($sig["time"] ?? 0) < 30000)) {
|
||||
$remaining[] = $line;
|
||||
}
|
||||
}
|
||||
file_put_contents($log_file, implode("\n", $remaining) . (empty($remaining) ? "" : "\n"), LOCK_EX);
|
||||
}
|
||||
|
||||
json_out(["success" => true, "participants" => $ps, "signals" => $signals]);
|
||||
}
|
||||
|
||||
if ($action === "signal") {
|
||||
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
|
||||
$to = $_REQUEST["to"] ?? "";
|
||||
$data = $_REQUEST["data"] ?? "";
|
||||
if (!$to || !$data) json_out(["error" => "Missing to/data"], 400);
|
||||
|
||||
$sig = [
|
||||
"from" => $my_id,
|
||||
"to" => $to,
|
||||
"data" => json_decode($data, true),
|
||||
"time" => now_ms()
|
||||
];
|
||||
|
||||
file_put_contents(room_log_file($room), json_encode($sig) . "\n", FILE_APPEND | LOCK_EX);
|
||||
json_out(["success" => true]);
|
||||
}
|
||||
|
||||
if ($action === "list_all") {
|
||||
// Periodic cleanup of the DB table (stale sessions > 15s)
|
||||
if (rand(1, 10) === 1) {
|
||||
try {
|
||||
$stale_db_time = now_ms() - 15000;
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?");
|
||||
$stmt->execute([$stale_db_time]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("
|
||||
SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url
|
||||
FROM voice_sessions vs
|
||||
JOIN users u ON vs.user_id = u.id
|
||||
WHERE vs.last_seen > ?
|
||||
");
|
||||
$stmt->execute();
|
||||
$sessions = $stmt->fetchAll();
|
||||
echo json_encode(['success' => true, 'sessions' => $sessions]);
|
||||
exit;
|
||||
$stale_db_time = now_ms() - 15000;
|
||||
$stmt->execute([$stale_db_time]);
|
||||
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$by_channel = [];
|
||||
foreach ($sessions as $s) {
|
||||
$by_channel[$s['channel_id']][] = $s;
|
||||
}
|
||||
json_out(["success" => true, "channels" => $by_channel]);
|
||||
} catch (Exception $e) {
|
||||
json_out(["error" => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid request']);
|
||||
if ($action === "leave") {
|
||||
if ($my_id) {
|
||||
$p_file = room_participants_file($room);
|
||||
$ps = read_json_file($p_file);
|
||||
unset($ps[$my_id]);
|
||||
write_json_file($p_file, $ps);
|
||||
}
|
||||
if ($current_user_id > 0) {
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?");
|
||||
$stmt->execute([$current_user_id]);
|
||||
} catch (Exception $e) {}
|
||||
}
|
||||
json_out(["success" => true]);
|
||||
}
|
||||
|
||||
json_out(["error" => "Unknown action"], 404);
|
||||
@ -457,6 +457,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let ws;
|
||||
let voiceHandler;
|
||||
|
||||
if (typeof VoiceChannel !== 'undefined') {
|
||||
voiceHandler = new VoiceChannel(null, window.voiceSettings);
|
||||
window.voiceHandler = voiceHandler;
|
||||
console.log('VoiceHandler initialized');
|
||||
|
||||
// Start global voice sessions polling
|
||||
setInterval(() => {
|
||||
VoiceChannel.refreshAllVoiceUsers();
|
||||
}, 3000);
|
||||
VoiceChannel.refreshAllVoiceUsers();
|
||||
}
|
||||
|
||||
function connectWS() {
|
||||
console.log('Connecting to WebSocket...');
|
||||
try {
|
||||
@ -466,6 +478,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
if (voiceHandler) voiceHandler.ws = ws;
|
||||
ws.send(JSON.stringify({
|
||||
type: 'presence',
|
||||
user_id: window.currentUserId,
|
||||
@ -473,12 +486,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}));
|
||||
};
|
||||
|
||||
if (typeof VoiceChannel !== 'undefined') {
|
||||
voiceHandler = new VoiceChannel(ws, window.voiceSettings);
|
||||
window.voiceHandler = voiceHandler;
|
||||
console.log('VoiceHandler initialized');
|
||||
}
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
|
||||
@ -665,11 +672,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Handle Click Events
|
||||
document.addEventListener('click', (e) => {
|
||||
console.log('Global click at:', e.target);
|
||||
// Voice Channel Click
|
||||
const voiceItem = e.target.closest('.voice-item');
|
||||
if (voiceItem) {
|
||||
e.preventDefault();
|
||||
console.log('Voice item clicked:', voiceItem.dataset.channelId);
|
||||
console.log('Voice item clicked, Channel ID:', voiceItem.dataset.channelId);
|
||||
const channelId = voiceItem.dataset.channelId;
|
||||
if (voiceHandler) {
|
||||
if (voiceHandler.currentChannelId == channelId) {
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
console.log('voice.js loaded');
|
||||
|
||||
class VoiceChannel {
|
||||
constructor(ws, settings) {
|
||||
this.ws = ws;
|
||||
// ws is ignored now as we use PHP signaling, but kept for compatibility
|
||||
this.settings = settings || { mode: 'vox', pttKey: 'v', voxThreshold: 0.1 };
|
||||
console.log('VoiceChannel constructor called with settings:', this.settings);
|
||||
this.localStream = null;
|
||||
this.screenStream = null;
|
||||
this.peers = {}; // userId -> RTCPeerConnection
|
||||
this.participants = {}; // userId -> {username, avatarUrl}
|
||||
this.participants = {}; // userId -> {name}
|
||||
this.currentChannelId = null;
|
||||
this.isScreenSharing = false;
|
||||
this.myPeerId = null;
|
||||
this.pollInterval = null;
|
||||
this.remoteAudios = {}; // userId -> Audio element
|
||||
|
||||
this.audioContext = null;
|
||||
this.analyser = null;
|
||||
@ -18,7 +22,10 @@ class VoiceChannel {
|
||||
this.pttPressed = false;
|
||||
this.voxActive = false;
|
||||
this.lastVoiceTime = 0;
|
||||
this.voxHoldTime = 500; // ms to keep open after sound drops below threshold
|
||||
this.voxHoldTime = 500;
|
||||
|
||||
// Track who is speaking to persist across UI refreshes
|
||||
this.speakingUsers = new Set();
|
||||
|
||||
this.setupPTTListeners();
|
||||
window.addEventListener('beforeunload', () => this.leave());
|
||||
@ -26,8 +33,12 @@ class VoiceChannel {
|
||||
|
||||
setupPTTListeners() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Ignore if in input field
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (this.settings.mode === 'ptt' && e.key.toLowerCase() === this.settings.pttKey.toLowerCase()) {
|
||||
if (!this.pttPressed) {
|
||||
console.log('PTT Key Pressed:', e.key);
|
||||
this.pttPressed = true;
|
||||
this.updateMuteState();
|
||||
}
|
||||
@ -36,6 +47,7 @@ class VoiceChannel {
|
||||
|
||||
window.addEventListener('keyup', (e) => {
|
||||
if (this.settings.mode === 'ptt' && e.key.toLowerCase() === this.settings.pttKey.toLowerCase()) {
|
||||
console.log('PTT Key Released:', e.key);
|
||||
this.pttPressed = false;
|
||||
this.updateMuteState();
|
||||
}
|
||||
@ -43,69 +55,209 @@ class VoiceChannel {
|
||||
}
|
||||
|
||||
async join(channelId) {
|
||||
console.log('VoiceChannel.join called for channel:', channelId);
|
||||
console.log('VoiceChannel.join process started for channel:', channelId);
|
||||
if (this.currentChannelId === channelId) {
|
||||
console.log('Already in this channel');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket not connected. State:', this.ws ? this.ws.readyState : 'null');
|
||||
alert('Unable to join voice: Connection to signaling server not established. Please wait a few seconds and try again.');
|
||||
return;
|
||||
if (this.currentChannelId) {
|
||||
console.log('Leaving previous channel:', this.currentChannelId);
|
||||
this.leave();
|
||||
}
|
||||
|
||||
if (this.currentChannelId) this.leave();
|
||||
|
||||
console.log('Joining voice channel:', channelId);
|
||||
this.currentChannelId = channelId;
|
||||
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Microphone access is only available on secure origins (HTTPS).');
|
||||
}
|
||||
console.log('Requesting microphone access...');
|
||||
this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||
|
||||
// Start muted
|
||||
console.log('Microphone access granted');
|
||||
this.setMute(true);
|
||||
|
||||
if (this.settings.mode === 'vox') {
|
||||
this.setupVOX();
|
||||
}
|
||||
|
||||
// Persist in DB
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'join');
|
||||
fd.append('channel_id', channelId);
|
||||
fetch('api_v1_voice.php', { method: 'POST', body: fd });
|
||||
// Join via PHP
|
||||
console.log('Calling API join...');
|
||||
const url = `api_v1_voice.php?action=join&room=${channelId}&name=${encodeURIComponent(window.currentUsername || 'Unknown')}`;
|
||||
const resp = await fetch(url);
|
||||
const data = await resp.json();
|
||||
console.log('API join response:', data);
|
||||
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_join',
|
||||
channel_id: channelId,
|
||||
user_id: window.currentUserId,
|
||||
username: window.currentUsername,
|
||||
avatar_url: window.currentAvatarUrl
|
||||
}));
|
||||
|
||||
this.updateVoiceUI();
|
||||
if (data.success) {
|
||||
this.myPeerId = data.peer_id;
|
||||
console.log('Joined room with peer_id:', this.myPeerId);
|
||||
|
||||
// Start polling
|
||||
this.startPolling();
|
||||
this.updateVoiceUI();
|
||||
} else {
|
||||
console.error('API join failed:', data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get local stream:', e);
|
||||
alert('Could not access microphone.');
|
||||
console.error('Failed to join voice:', e);
|
||||
alert('Microphone access required for voice channels. Error: ' + e.message);
|
||||
this.currentChannelId = null;
|
||||
}
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
if (this.pollInterval) clearInterval(this.pollInterval);
|
||||
this.pollInterval = setInterval(() => this.poll(), 1000);
|
||||
this.poll(); // Initial poll
|
||||
}
|
||||
|
||||
async poll() {
|
||||
if (!this.myPeerId || !this.currentChannelId) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`api_v1_voice.php?action=poll&room=${this.currentChannelId}&peer_id=${this.myPeerId}`);
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success) {
|
||||
// Update participants
|
||||
const oldPs = Object.keys(this.participants);
|
||||
this.participants = data.participants;
|
||||
const newPs = Object.keys(this.participants);
|
||||
|
||||
// If new people joined, initiate offer if I'm the "older" one (not really necessary here, can just offer to anyone I don't have a peer for)
|
||||
newPs.forEach(pid => {
|
||||
if (pid !== this.myPeerId && !this.peers[pid]) {
|
||||
console.log('New peer found via poll:', pid);
|
||||
this.createPeerConnection(pid, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup left peers
|
||||
oldPs.forEach(pid => {
|
||||
if (!this.participants[pid] && this.peers[pid]) {
|
||||
console.log('Peer left:', pid);
|
||||
this.peers[pid].close();
|
||||
delete this.peers[pid];
|
||||
}
|
||||
});
|
||||
|
||||
// Handle incoming signals
|
||||
if (data.signals && data.signals.length > 0) {
|
||||
for (const sig of data.signals) {
|
||||
await this.handleSignaling(sig);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateVoiceUI();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Polling error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async sendSignal(to, data) {
|
||||
if (!this.myPeerId || !this.currentChannelId) return;
|
||||
await fetch(`api_v1_voice.php?action=signal&room=${this.currentChannelId}&peer_id=${this.myPeerId}&to=${to}&data=${encodeURIComponent(JSON.stringify(data))}`);
|
||||
}
|
||||
|
||||
createPeerConnection(userId, isOfferor) {
|
||||
if (this.peers[userId]) return this.peers[userId];
|
||||
|
||||
console.log('Creating PeerConnection for:', userId, 'as offeror:', isOfferor);
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' }
|
||||
]
|
||||
});
|
||||
|
||||
this.peers[userId] = pc;
|
||||
|
||||
pc.oniceconnectionstatechange = () => {
|
||||
console.log(`ICE Connection State with ${userId}: ${pc.iceConnectionState}`);
|
||||
};
|
||||
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => {
|
||||
console.log(`Adding track ${track.kind} to peer ${userId}`);
|
||||
pc.addTrack(track, this.localStream);
|
||||
});
|
||||
}
|
||||
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.sendSignal(userId, { type: 'ice_candidate', candidate: event.candidate });
|
||||
}
|
||||
};
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
console.log('Received remote track from:', userId, event);
|
||||
if (this.remoteAudios[userId]) {
|
||||
this.remoteAudios[userId].pause();
|
||||
this.remoteAudios[userId].remove();
|
||||
this.remoteAudios[userId].srcObject = null;
|
||||
}
|
||||
const remoteAudio = new Audio();
|
||||
remoteAudio.style.display = 'none';
|
||||
remoteAudio.srcObject = event.streams[0];
|
||||
document.body.appendChild(remoteAudio);
|
||||
this.remoteAudios[userId] = remoteAudio;
|
||||
remoteAudio.play().catch(e => console.warn('Autoplay prevented:', e));
|
||||
};
|
||||
|
||||
if (isOfferor) {
|
||||
pc.createOffer().then(offer => {
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(() => {
|
||||
this.sendSignal(userId, { type: 'offer', offer: pc.localDescription });
|
||||
});
|
||||
}
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
async handleSignaling(sig) {
|
||||
const from = sig.from;
|
||||
const data = sig.data;
|
||||
|
||||
console.log('Handling signaling from:', from, 'type:', data.type);
|
||||
|
||||
switch (data.type) {
|
||||
case 'offer':
|
||||
await this.handleOffer(from, data.offer);
|
||||
break;
|
||||
case 'answer':
|
||||
await this.handleAnswer(from, data.answer);
|
||||
break;
|
||||
case 'ice_candidate':
|
||||
await this.handleCandidate(from, data.candidate);
|
||||
break;
|
||||
case 'voice_speaking':
|
||||
this.updateSpeakingUI(data.user_id, data.speaking);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async handleOffer(from, offer) {
|
||||
const pc = this.createPeerConnection(from, false);
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
this.sendSignal(from, { type: 'answer', answer: pc.localDescription });
|
||||
}
|
||||
|
||||
async handleAnswer(from, answer) {
|
||||
const pc = this.peers[from];
|
||||
if (pc) await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
}
|
||||
|
||||
async handleCandidate(from, candidate) {
|
||||
const pc = this.peers[from];
|
||||
if (pc) await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
||||
}
|
||||
|
||||
setupVOX() {
|
||||
if (this.audioContext) this.audioContext.close();
|
||||
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.microphone = this.audioContext.createMediaStreamSource(this.localStream);
|
||||
this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
this.analyser.smoothingTimeConstant = 0.8;
|
||||
this.analyser.fftSize = 1024;
|
||||
|
||||
this.microphone.connect(this.analyser);
|
||||
this.analyser.connect(this.scriptProcessor);
|
||||
this.scriptProcessor.connect(this.audioContext.destination);
|
||||
@ -114,13 +266,13 @@ class VoiceChannel {
|
||||
const array = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
this.analyser.getByteFrequencyData(array);
|
||||
let values = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
values += array[i];
|
||||
}
|
||||
for (let i = 0; i < array.length; i++) values += array[i];
|
||||
const average = values / array.length;
|
||||
const normalized = average / 128; // 0 to 2 approx
|
||||
|
||||
// Log sometimes for debugging VOX
|
||||
if (Math.random() < 0.01) console.log('VOX Avg:', average, 'Threshold:', this.settings.voxThreshold * 255);
|
||||
|
||||
if (normalized > this.settings.voxThreshold) {
|
||||
if (average > (this.settings.voxThreshold * 255)) {
|
||||
this.lastVoiceTime = Date.now();
|
||||
if (!this.voxActive) {
|
||||
this.voxActive = true;
|
||||
@ -137,207 +289,69 @@ class VoiceChannel {
|
||||
|
||||
updateMuteState() {
|
||||
if (!this.currentChannelId || !this.localStream) return;
|
||||
|
||||
let shouldTalk = false;
|
||||
if (this.settings.mode === 'ptt') {
|
||||
shouldTalk = this.pttPressed;
|
||||
} else {
|
||||
shouldTalk = this.voxActive;
|
||||
}
|
||||
|
||||
let shouldTalk = (this.settings.mode === 'ptt') ? this.pttPressed : this.voxActive;
|
||||
console.log('updateMuteState: shouldTalk =', shouldTalk, 'mode =', this.settings.mode);
|
||||
if (this.isTalking !== shouldTalk) {
|
||||
this.isTalking = shouldTalk;
|
||||
this.setMute(!shouldTalk);
|
||||
|
||||
// Notify others
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_speaking',
|
||||
channel_id: this.currentChannelId,
|
||||
user_id: window.currentUserId,
|
||||
speaking: shouldTalk
|
||||
}));
|
||||
|
||||
this.updateSpeakingUI(window.currentUserId, shouldTalk);
|
||||
|
||||
// Notify others
|
||||
const msg = { type: 'voice_speaking', channel_id: this.currentChannelId, user_id: window.currentUserId, speaking: shouldTalk };
|
||||
// ... (rest of method remains same, but I'll update it for clarity)
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(msg));
|
||||
} else {
|
||||
Object.keys(this.peers).forEach(pid => {
|
||||
this.sendSignal(pid, msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setMute(mute) {
|
||||
if (this.localStream) {
|
||||
this.localStream.getAudioTracks().forEach(track => {
|
||||
track.enabled = !mute;
|
||||
});
|
||||
console.log('Setting mute to:', mute);
|
||||
this.localStream.getAudioTracks().forEach(track => { track.enabled = !mute; });
|
||||
}
|
||||
}
|
||||
|
||||
leave() {
|
||||
if (!this.currentChannelId) return;
|
||||
|
||||
this.stopScreenShare();
|
||||
|
||||
// Persist in DB
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'leave');
|
||||
fetch('api_v1_voice.php', { method: 'POST', body: fd });
|
||||
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_leave',
|
||||
channel_id: this.currentChannelId,
|
||||
user_id: window.currentUserId
|
||||
}));
|
||||
console.log('Leaving voice channel:', this.currentChannelId);
|
||||
if (this.pollInterval) clearInterval(this.pollInterval);
|
||||
|
||||
fetch(`api_v1_voice.php?action=leave&room=${this.currentChannelId}&peer_id=${this.myPeerId}`);
|
||||
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => track.stop());
|
||||
this.localStream = null;
|
||||
}
|
||||
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
|
||||
Object.values(this.peers).forEach(pc => pc.close());
|
||||
Object.values(this.remoteAudios).forEach(audio => {
|
||||
audio.pause();
|
||||
audio.remove();
|
||||
audio.srcObject = null;
|
||||
});
|
||||
this.peers = {};
|
||||
this.remoteAudios = {};
|
||||
this.participants = {};
|
||||
this.currentChannelId = null;
|
||||
this.isTalking = false;
|
||||
this.myPeerId = null;
|
||||
this.speakingUsers.clear();
|
||||
this.updateVoiceUI();
|
||||
}
|
||||
|
||||
async handleSignaling(data) {
|
||||
const { type, from, to, offer, answer, candidate, channel_id, username, avatar_url, speaking } = data;
|
||||
|
||||
if (channel_id != this.currentChannelId) return;
|
||||
if (to && to != window.currentUserId) return;
|
||||
|
||||
switch (type) {
|
||||
case 'voice_join':
|
||||
if (from != window.currentUserId) {
|
||||
this.participants[from] = { username: username || `User ${from}`, avatar_url: avatar_url };
|
||||
this.createPeerConnection(from, true);
|
||||
this.updateVoiceUI();
|
||||
}
|
||||
break;
|
||||
case 'voice_offer':
|
||||
this.participants[from] = { username: username || `User ${from}`, avatar_url: avatar_url };
|
||||
await this.handleOffer(from, offer);
|
||||
this.updateVoiceUI();
|
||||
break;
|
||||
case 'voice_answer':
|
||||
await this.handleAnswer(from, answer);
|
||||
break;
|
||||
case 'voice_ice_candidate':
|
||||
await this.handleCandidate(from, candidate);
|
||||
break;
|
||||
case 'voice_speaking':
|
||||
this.updateSpeakingUI(from, speaking);
|
||||
break;
|
||||
case 'voice_leave':
|
||||
if (this.peers[from]) {
|
||||
this.peers[from].close();
|
||||
delete this.peers[from];
|
||||
}
|
||||
delete this.participants[from];
|
||||
this.updateVoiceUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
createPeerConnection(userId, isOfferor) {
|
||||
if (this.peers[userId]) return this.peers[userId];
|
||||
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
});
|
||||
|
||||
this.peers[userId] = pc;
|
||||
|
||||
if (this.localStream) {
|
||||
this.localStream.getTracks().forEach(track => {
|
||||
pc.addTrack(track, this.localStream);
|
||||
});
|
||||
}
|
||||
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_ice_candidate',
|
||||
to: userId,
|
||||
from: window.currentUserId,
|
||||
candidate: event.candidate,
|
||||
channel_id: this.currentChannelId
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
if (event.track.kind === 'audio') {
|
||||
const remoteAudio = new Audio();
|
||||
remoteAudio.srcObject = event.streams[0];
|
||||
remoteAudio.play();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOfferor) {
|
||||
pc.createOffer().then(offer => {
|
||||
return pc.setLocalDescription(offer);
|
||||
}).then(() => {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_offer',
|
||||
to: userId,
|
||||
from: window.currentUserId,
|
||||
username: window.currentUsername,
|
||||
avatar_url: window.currentAvatarUrl,
|
||||
offer: pc.localDescription,
|
||||
channel_id: this.currentChannelId
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
async handleOffer(from, offer) {
|
||||
const pc = this.createPeerConnection(from, false);
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'voice_answer',
|
||||
to: from,
|
||||
from: window.currentUserId,
|
||||
answer: pc.localDescription,
|
||||
channel_id: this.currentChannelId
|
||||
}));
|
||||
}
|
||||
|
||||
async handleAnswer(from, answer) {
|
||||
const pc = this.peers[from];
|
||||
if (pc) await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
}
|
||||
|
||||
async handleCandidate(from, candidate) {
|
||||
const pc = this.peers[from];
|
||||
if (pc) await pc.addIceCandidate(new RTCIceCandidate(candidate));
|
||||
}
|
||||
|
||||
updateVoiceUI() {
|
||||
document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = '');
|
||||
|
||||
// Fetch all sessions to update all channels (or just rely on signaling for current one)
|
||||
// For simplicity, we update the current channel from participants
|
||||
if (this.currentChannelId) {
|
||||
const listEls = document.querySelectorAll(`.voice-item[data-channel-id="${this.currentChannelId}"] + .voice-users-list`);
|
||||
listEls.forEach(listEl => {
|
||||
// Me
|
||||
this.addVoiceUserToUI(listEl, window.currentUserId, window.currentUsername, window.currentAvatarUrl);
|
||||
|
||||
// Others
|
||||
Object.entries(this.participants).forEach(([uid, data]) => {
|
||||
this.addVoiceUserToUI(listEl, uid, data.username, data.avatar_url);
|
||||
});
|
||||
});
|
||||
// We now use a global update mechanism for all channels
|
||||
VoiceChannel.refreshAllVoiceUsers();
|
||||
|
||||
// Voice controls
|
||||
if (this.currentChannelId) {
|
||||
if (!document.querySelector('.voice-controls')) {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'voice-controls p-2 d-flex justify-content-between align-items-center border-top bg-dark';
|
||||
@ -345,7 +359,7 @@ class VoiceChannel {
|
||||
controls.innerHTML = `
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="voice-status-icon text-success me-2" style="font-size: 8px;">●</div>
|
||||
<div class="small fw-bold" style="font-size: 11px; color: #248046;">Voice Connected</div>
|
||||
<div class="small fw-bold" style="font-size: 11px; color: #248046;">Voice (${this.settings.mode.toUpperCase()})</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm text-muted" id="btn-voice-leave" title="Disconnect">
|
||||
@ -353,8 +367,10 @@ class VoiceChannel {
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.querySelector('.channels-sidebar').appendChild(controls);
|
||||
document.getElementById('btn-voice-leave').onclick = () => this.leave();
|
||||
const sidebar = document.querySelector('.channels-sidebar');
|
||||
if (sidebar) sidebar.appendChild(controls);
|
||||
const btnLeave = document.getElementById('btn-voice-leave');
|
||||
if (btnLeave) btnLeave.onclick = () => this.leave();
|
||||
}
|
||||
} else {
|
||||
const controls = document.querySelector('.voice-controls');
|
||||
@ -362,38 +378,64 @@ class VoiceChannel {
|
||||
}
|
||||
}
|
||||
|
||||
addVoiceUserToUI(container, userId, username, avatarUrl) {
|
||||
const userEl = document.createElement('div');
|
||||
userEl.className = 'voice-user small d-flex align-items-center mb-1';
|
||||
userEl.dataset.userId = userId;
|
||||
userEl.style.paddingLeft = '8px';
|
||||
userEl.innerHTML = `
|
||||
<div class="message-avatar me-2" style="width: 18px; height: 18px; border-radius: 50%; transition: box-shadow 0.2s; ${avatarUrl ? `background-image: url('${avatarUrl}');` : ""}"></div>
|
||||
<span style="color: var(--text-muted); font-size: 13px;">${username}</span>
|
||||
`;
|
||||
container.appendChild(userEl);
|
||||
}
|
||||
|
||||
updateSpeakingUI(userId, isSpeaking) {
|
||||
if (isSpeaking) {
|
||||
this.speakingUsers.add(userId);
|
||||
} else {
|
||||
this.speakingUsers.delete(userId);
|
||||
}
|
||||
|
||||
const userEls = document.querySelectorAll(`.voice-user[data-user-id="${userId}"]`);
|
||||
userEls.forEach(el => {
|
||||
const avatar = el.querySelector('.message-avatar');
|
||||
if (avatar) {
|
||||
if (isSpeaking) {
|
||||
avatar.style.boxShadow = '0 0 0 2px #23a559';
|
||||
} else {
|
||||
avatar.style.boxShadow = 'none';
|
||||
}
|
||||
avatar.style.boxShadow = isSpeaking ? '0 0 0 2px #23a559' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopScreenShare() {
|
||||
// Not requested but kept for compatibility
|
||||
if (this.screenStream) {
|
||||
this.screenStream.getTracks().forEach(track => track.stop());
|
||||
this.screenStream = null;
|
||||
static async refreshAllVoiceUsers() {
|
||||
try {
|
||||
const resp = await fetch('api_v1_voice.php?action=list_all');
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
// Clear all lists first
|
||||
document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = '');
|
||||
|
||||
// Populate based on data
|
||||
Object.keys(data.channels).forEach(channelId => {
|
||||
// Fix: The voice-users-list is a sibling of the container of the voice-item
|
||||
const voiceItem = document.querySelector(`.voice-item[data-channel-id="${channelId}"]`);
|
||||
if (voiceItem) {
|
||||
const container = voiceItem.closest('.channel-item-container');
|
||||
if (container) {
|
||||
const listEl = container.querySelector('.voice-users-list');
|
||||
if (listEl) {
|
||||
data.channels[channelId].forEach(p => {
|
||||
const isSpeaking = window.voiceHandler && window.voiceHandler.speakingUsers.has(p.user_id);
|
||||
VoiceChannel.renderUserToUI(listEl, p.user_id, p.display_name || p.username, p.avatar_url, isSpeaking);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh voice users:', e);
|
||||
}
|
||||
this.isScreenSharing = false;
|
||||
}
|
||||
|
||||
static renderUserToUI(container, userId, username, avatarUrl, isSpeaking = false) {
|
||||
const userEl = document.createElement('div');
|
||||
userEl.className = 'voice-user small text-muted d-flex align-items-center mb-1';
|
||||
userEl.dataset.userId = userId;
|
||||
userEl.style.paddingLeft = '8px';
|
||||
const avatarStyle = avatarUrl ? `background-image: url('${avatarUrl}'); background-size: cover;` : "background-color: #555;";
|
||||
const boxShadow = isSpeaking ? 'box-shadow: 0 0 0 2px #23a559;' : '';
|
||||
userEl.innerHTML = `
|
||||
<div class="message-avatar me-2" style="width: 16px; height: 16px; border-radius: 50%; transition: box-shadow 0.2s; ${avatarStyle} ${boxShadow}"></div>
|
||||
<span style="font-size: 13px;">${username}</span>
|
||||
`;
|
||||
container.appendChild(userEl);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
assets/pasted-20260217-141526-2008a77e.png
Normal file
BIN
assets/pasted-20260217-141526-2008a77e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/pasted-20260217-143739-c7f88b4b.png
Normal file
BIN
assets/pasted-20260217-143739-c7f88b4b.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
2
data/22.log
Normal file
2
data/22.log
Normal file
@ -0,0 +1,2 @@
|
||||
{"from":"a0645f38fb2bbdb5","to":"1ca650259dcce60e","data":{"type":"voice_speaking","channel_id":"22","user_id":3,"speaking":true},"time":1771339536024}
|
||||
{"from":"a0645f38fb2bbdb5","to":"1ca650259dcce60e","data":{"type":"voice_speaking","channel_id":"22","user_id":3,"speaking":false},"time":1771339536956}
|
||||
1
data/22.participants.json
Normal file
1
data/22.participants.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
8
data/3.log
Normal file
8
data/3.log
Normal file
@ -0,0 +1,8 @@
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"offer","offer":{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 2993514939591859431 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 E9:89:B8:DE:41:F8:AD:79:17:A8:4D:03:2D:53:FC:15:5A:3B:B4:CA:45:A6:F7:EB:C7:F0:01:7C:0B:29:91:F1\r\na=group:BUNDLE 0\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2\/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap-allow-mixed\r\na=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\na=fmtp:101 0-15\r\na=ice-pwd:cacccb2562e0f73ce1667f9773fb670d\r\na=ice-ufrag:2b377091\r\na=mid:0\r\na=msid:{5d44acc3-9af3-4110-98ed-2de218fc7450} {ed9d31cb-0fc7-497e-8c79-4cc9159d5f97}\r\na=rtcp-mux\r\na=rtpmap:109 opus\/48000\/2\r\na=rtpmap:9 G722\/8000\/1\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:101 telephone-event\/8000\r\na=setup:actpass\r\na=ssrc:2872346815 cname:{74df363a-4160-4031-a110-5285ddfe55ff}\r\n"}},"time":1771339604154}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 1 UDP 2122252543 192.168.26.26 61807 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604155}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 1 TCP 2105524479 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604161}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 2 TCP 2105524478 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604164}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 2 UDP 2122252542 192.168.26.26 61808 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604170}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604171}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052863 78.246.210.10 30532 typ srflx raddr 192.168.26.26 rport 61807","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604172}
|
||||
{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052862 78.246.210.10 30534 typ srflx raddr 192.168.26.26 rport 61808","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604175}
|
||||
1
data/3.participants.json
Normal file
1
data/3.participants.json
Normal file
@ -0,0 +1 @@
|
||||
{"1e0fff021b7ad021":{"id":"1e0fff021b7ad021","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771339631904},"9afce7ba24e9091b":{"id":"9afce7ba24e9091b","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771339632155}}
|
||||
10
data/6.log
Normal file
10
data/6.log
Normal file
@ -0,0 +1,10 @@
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"offer","offer":{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 1705174900585877835 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 AA:37:B9:FA:14:15:DC:34:29:97:DC:55:77:28:8E:74:C0:94:15:08:DF:5B:E9:CC:36:81:E5:D9:5C:49:FB:46\r\na=group:BUNDLE 0\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2\/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap-allow-mixed\r\na=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\na=fmtp:101 0-15\r\na=ice-pwd:075627a25c9f07d3908305504803057d\r\na=ice-ufrag:03d0add1\r\na=mid:0\r\na=msid:{7a2c5afc-d053-43fd-9196-3caf4510feb3} {8439b857-ead8-4cb1-8111-cb3400348461}\r\na=rtcp-mux\r\na=rtpmap:109 opus\/48000\/2\r\na=rtpmap:9 G722\/8000\/1\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:101 telephone-event\/8000\r\na=setup:actpass\r\na=ssrc:1663488241 cname:{7dcb8038-2b50-4e42-827c-c4617a225c01}\r\n"}},"time":1771336456644}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 1 UDP 2122252543 192.168.26.26 62572 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456645}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 1 TCP 2105524479 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456647}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 2 TCP 2105524478 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456652}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 2 UDP 2122252542 192.168.26.26 62573 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456654}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052351 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456720}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052350 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456757}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052863 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456776}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052862 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456802}
|
||||
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456805}
|
||||
1
data/6.participants.json
Normal file
1
data/6.participants.json
Normal file
@ -0,0 +1 @@
|
||||
{"0cce3619c0f299fa":{"id":"0cce3619c0f299fa","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336452806},"75b3938d276e81e1":{"id":"75b3938d276e81e1","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336462293}}
|
||||
1
data/test.participants.json
Normal file
1
data/test.participants.json
Normal file
@ -0,0 +1 @@
|
||||
{"0fbf720bc2f110c0":{"id":"0fbf720bc2f110c0","name":"AI","last_seen":1771336229774}}
|
||||
1
data/test.txt
Normal file
1
data/test.txt
Normal file
@ -0,0 +1 @@
|
||||
hello
|
||||
1
data/test_www.txt
Normal file
1
data/test_www.txt
Normal file
@ -0,0 +1 @@
|
||||
hello
|
||||
72
index.php
72
index.php
@ -507,29 +507,46 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="channel-item-container d-flex align-items-center" data-id="<?php echo $c['id']; ?>">
|
||||
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
|
||||
class="channel-item flex-grow-1 <?php echo ($c['id'] == $active_channel_id) ? 'active' : ''; ?> <?php echo ($c['type'] === 'voice') ? 'voice-item' : ''; ?>" <?php echo ($c['type'] === 'voice') ? 'data-channel-id="'.$c['id'].'"' : ''; ?>>
|
||||
<span class="d-flex align-items-center">
|
||||
<span class="me-1" style="width: 20px; display: inline-block; text-align: center;">
|
||||
<?php
|
||||
if ($c['type'] === 'announcement') echo '<i class="fa-solid fa-bullhorn"></i>';
|
||||
elseif ($c['type'] === 'rules') echo '<i class="fa-solid fa-gavel"></i>';
|
||||
elseif ($c['type'] === 'autorole') echo '<i class="fa-solid fa-shield-halved"></i>';
|
||||
elseif ($c['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
|
||||
elseif ($c['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
|
||||
else echo '<i class="fa-solid fa-hashtag"></i>';
|
||||
?>
|
||||
<div class="channel-item-container" data-id="<?php echo $c['id']; ?>">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $c['id']; ?>"
|
||||
class="channel-item flex-grow-1 <?php echo ($c['id'] == $active_channel_id) ? 'active' : ''; ?> <?php echo ($c['type'] === 'voice') ? 'voice-item' : ''; ?>" <?php echo ($c['type'] === 'voice') ? 'data-channel-id="'.$c['id'].'"' : ''; ?>>
|
||||
<span class="d-flex align-items-center">
|
||||
<span class="me-1" style="width: 20px; display: inline-block; text-align: center;">
|
||||
<?php
|
||||
if ($c['type'] === 'announcement') echo '<i class="fa-solid fa-bullhorn"></i>';
|
||||
elseif ($c['type'] === 'rules') echo '<i class="fa-solid fa-gavel"></i>';
|
||||
elseif ($c['type'] === 'autorole') echo '<i class="fa-solid fa-shield-halved"></i>';
|
||||
elseif ($c['type'] === 'forum') echo '<i class="fa-solid fa-comments"></i>';
|
||||
elseif ($c['type'] === 'voice') echo '<i class="fa-solid fa-volume-up"></i>';
|
||||
else echo '<i class="fa-solid fa-hashtag"></i>';
|
||||
?>
|
||||
</span>
|
||||
<?php if (!empty($c['icon'])): ?>
|
||||
<span class="me-1" style="font-size: 14px;"><?php echo renderRoleIcon($c['icon'], '14px'); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="channel-name-text"><?php echo htmlspecialchars($c['name']); ?></span>
|
||||
</span>
|
||||
<?php if (!empty($c['icon'])): ?>
|
||||
<span class="me-1" style="font-size: 14px;"><?php echo renderRoleIcon($c['icon'], '14px'); ?></span>
|
||||
<?php if ($c['type'] === 'voice' && !empty($c['status'])): ?>
|
||||
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
|
||||
<?php endif; ?>
|
||||
<span class="channel-name-text"><?php echo htmlspecialchars($c['name']); ?></span>
|
||||
</span>
|
||||
<?php if ($c['type'] === 'voice' && !empty($c['status'])): ?>
|
||||
<div class="channel-status small text-muted ms-4" style="font-size: 0.75em; margin-top: -2px;"><?php echo htmlspecialchars($c['status']); ?></div>
|
||||
</a>
|
||||
<?php if ($can_manage_channels): ?>
|
||||
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
|
||||
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
||||
data-id="<?php echo $c['id']; ?>"
|
||||
data-name="<?php echo htmlspecialchars($c['name']); ?>"
|
||||
data-type="<?php echo $c['type']; ?>"
|
||||
data-files="<?php echo $c['allow_file_sharing']; ?>"
|
||||
data-limit="<?php echo $c['message_limit']; ?>"
|
||||
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
|
||||
data-icon="<?php echo htmlspecialchars($c['icon'] ?? ''); ?>"
|
||||
data-rules-role="<?php echo $c['rules_role_id'] ?? ''; ?>"
|
||||
data-category="<?php echo $c['category_id'] ?? ''; ?>">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php if ($c['type'] === 'voice'): ?>
|
||||
<div class="voice-users-list ms-4 mb-1">
|
||||
<?php if (isset($voice_users_by_channel[$c['id']])): ?>
|
||||
@ -542,21 +559,6 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($can_manage_channels): ?>
|
||||
<span class="channel-settings-btn ms-1" style="cursor: pointer; color: var(--text-muted);"
|
||||
data-bs-toggle="modal" data-bs-target="#editChannelModal"
|
||||
data-id="<?php echo $c['id']; ?>"
|
||||
data-name="<?php echo htmlspecialchars($c['name']); ?>"
|
||||
data-type="<?php echo $c['type']; ?>"
|
||||
data-files="<?php echo $c['allow_file_sharing']; ?>"
|
||||
data-limit="<?php echo $c['message_limit']; ?>"
|
||||
data-status="<?php echo htmlspecialchars($c['status'] ?? ''); ?>"
|
||||
data-icon="<?php echo htmlspecialchars($c['icon'] ?? ''); ?>"
|
||||
data-rules-role="<?php echo $c['rules_role_id'] ?? ''; ?>"
|
||||
data-category="<?php echo $c['category_id'] ?? ''; ?>">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33 1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
68
requests.log
68
requests.log
@ -514,3 +514,71 @@
|
||||
2026-02-17 12:44:53 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 12:45:22 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 12:50:07 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-17 12:51:03 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:29:32 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:34:34 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-17 13:35:23 - - POST: []
|
||||
2026-02-17 13:35:28 - GET / - POST: []
|
||||
2026-02-17 13:44:18 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:44:19 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-17 13:44:44 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:46:48 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:47:13 - GET /index.php - POST: []
|
||||
2026-02-17 13:47:48 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:47:48 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:49:26 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:52:42 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:53:30 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:53:31 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:53:33 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:53:34 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:53:44 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:53:50 - GET /index.php - POST: []
|
||||
2026-02-17 13:53:52 - GET /index.php - POST: []
|
||||
2026-02-17 13:54:13 - GET /index.php?server_id=1&channel_id=15 - POST: []
|
||||
2026-02-17 13:54:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-17 13:54:34 - GET /index.php?server_id=1&channel_id=3 - POST: []
|
||||
2026-02-17 13:54:40 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 13:54:53","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 13:54:54 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 13:55:35 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
{"date":"2026-02-17 13:56:01","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
|
||||
2026-02-17 13:56:02 - GET /index.php?server_id=1&channel_id=6 - POST: []
|
||||
2026-02-17 13:56:22 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 13:56:32 - GET /index.php - POST: []
|
||||
2026-02-17 14:03:44 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 14:03:45 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 14:04:57 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:05:10 - GET /index.php - POST: []
|
||||
2026-02-17 14:05:34 - GET /index.php - POST: []
|
||||
2026-02-17 14:05:36 - GET /index.php - POST: []
|
||||
2026-02-17 14:12:18 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 14:12:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:12:42 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:12:48 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 14:12:58","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:12:59 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:23:18 - GET / - POST: []
|
||||
2026-02-17 14:23:53 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 14:23:54 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 14:34:38 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:34:58 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:35:14 - GET /index.php - POST: []
|
||||
{"date":"2026-02-17 14:36:59","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:37:00 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:42:23 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 14:42:24 - GET /?fl_project=38443 - POST: []
|
||||
2026-02-17 14:43:06 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 14:43:18","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:43:19 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
2026-02-17 14:43:51 - GET /index.php - POST: []
|
||||
{"date":"2026-02-17 14:44:07","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
|
||||
2026-02-17 14:44:07 - GET /index.php - POST: []
|
||||
{"date":"2026-02-17 14:44:43","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:44:43 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 14:45:25","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:45:26 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 14:46:17","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:46:17 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
{"date":"2026-02-17 14:46:39","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
|
||||
2026-02-17 14:46:40 - GET /index.php?server_id=1&channel_id=22 - POST: []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user