From 361c322bc844e11438d16790e4ca731dc3f03aff Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Thu, 7 May 2026 21:25:10 +0200 Subject: [PATCH] [call-me] - feat: use RINGING_TIMEOUT env var for call timeout, dynamic progress bar, and looping ring sound --- .env.template | 6 +++- app/server.js | 2 ++ package-lock.json | 87 ++++++++++++----------------------------------- package.json | 4 +-- public/client.js | 47 ++++++++++++++++++++++--- public/style.css | 2 +- 6 files changed, 74 insertions(+), 74 deletions(-) diff --git a/.env.template b/.env.template index 423916d..ea98a29 100644 --- a/.env.template +++ b/.env.template @@ -76,4 +76,8 @@ PUSH_VAPID_EMAIL='mailto:admin@example.com' SENTRY_ENABLED=false # true or false SENTRY_LOG_LEVELS=error # Log levels to capture in Sentry (e.g., error,warn) SENTRY_DSN= -SENTRY_TRACES_SAMPLE_RATE=0.5 # Adjust the sample rate for performance monitoring (0.0 to 1.0) \ No newline at end of file +SENTRY_TRACES_SAMPLE_RATE=0.5 # Adjust the sample rate for performance monitoring (0.0 to 1.0) + +# Time in seconds before a call is considered unanswered (default 30 seconds) + +RINGING_TIMEOUT=30 \ No newline at end of file diff --git a/app/server.js b/app/server.js index cf6143f..dec2f7b 100755 --- a/app/server.js +++ b/app/server.js @@ -92,6 +92,7 @@ const config = { pushVapidPrivateKey: process.env.PUSH_VAPID_PRIVATE_KEY || '', pushVapidEmail: process.env.PUSH_VAPID_EMAIL || 'mailto:admin@example.com', randomImageUrl: process.env.RANDOM_IMAGE_URL || '', + ringTimeout: parseInt(process.env.RINGING_TIMEOUT, 10) || 30, apiBasePath: '/api/v1', swaggerDocument: yaml.load(fs.readFileSync(path.join(__dirname, '/api/swagger.yaml'), 'utf8')), }; @@ -529,6 +530,7 @@ function handleConnection(socket) { message: 'Hello Client!', iceServers: config.iceServers, pushEnabled: config.pushEnabled, + ringTimeout: config.ringTimeout, }); } diff --git a/package-lock.json b/package-lock.json index 3d30b7d..6e75961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "call-me", - "version": "1.3.44", + "version": "1.3.45", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "call-me", - "version": "1.3.44", + "version": "1.3.45", "license": "AGPLv3", "dependencies": { "@ngrok/ngrok": "1.7.0", - "@sentry/node": "^10.51.0", + "@sentry/node": "^10.52.0", "axios": "^1.16.0", "colors": "^1.4.0", "cors": "^2.8.6", @@ -522,23 +522,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.62.0.tgz", - "integrity": "sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/redis-common": "^0.38.2", - "@opentelemetry/semantic-conventions": "^1.33.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-kafkajs": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz", @@ -690,23 +673,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.62.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz", - "integrity": "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/redis-common": "^0.38.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-tedious": { "version": "0.33.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz", @@ -724,15 +690,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.38.3", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz", - "integrity": "sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - } - }, "node_modules/@opentelemetry/resources": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", @@ -844,18 +801,18 @@ } }, "node_modules/@sentry/core": { - "version": "10.51.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.51.0.tgz", - "integrity": "sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==", + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.52.0.tgz", + "integrity": "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/node": { - "version": "10.51.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.51.0.tgz", - "integrity": "sha512-2yZLRZwS1dKG8/4eOTpGSo/gO/EgmT9aPj6lAzUkRa7bZCTTdW4BraaHU0leX5T94909Qfhbr3W5AVTfDOCKiQ==", + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.52.0.tgz", + "integrity": "sha512-9+p3KJUk3rHO1HOEZuSknP2RgKCJZONDm4HWgkVDtVBtocb66KLtVlMjc59d2/bWP7tM3wc877tpG30quFfU9g==", "license": "MIT", "dependencies": { "@fastify/otel": "0.18.0", @@ -870,7 +827,6 @@ "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", - "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", @@ -880,14 +836,13 @@ "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", - "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", - "@sentry/core": "10.51.0", - "@sentry/node-core": "10.51.0", - "@sentry/opentelemetry": "10.51.0", + "@sentry/core": "10.52.0", + "@sentry/node-core": "10.52.0", + "@sentry/opentelemetry": "10.52.0", "import-in-the-middle": "^3.0.0" }, "engines": { @@ -895,13 +850,13 @@ } }, "node_modules/@sentry/node-core": { - "version": "10.51.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.51.0.tgz", - "integrity": "sha512-VP9DMEzBEuauABrfDHYz/pRYa74M09uRJLz0ls3yel3sKhYHMyCB29ZxbKcciUhD4d33dwgi8DbaPZV2H/wnfQ==", + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.52.0.tgz", + "integrity": "sha512-IG7MBtLRPQ2LuU+kbD14AFZroZgAeUmJQTP1FI/F8n56O31+p+9R703LuBTpvZr6sm+eRYDMWcGYYkfLHRVjwg==", "license": "MIT", "dependencies": { - "@sentry/core": "10.51.0", - "@sentry/opentelemetry": "10.51.0", + "@sentry/core": "10.52.0", + "@sentry/opentelemetry": "10.52.0", "import-in-the-middle": "^3.0.0" }, "engines": { @@ -937,12 +892,12 @@ } }, "node_modules/@sentry/opentelemetry": { - "version": "10.51.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.51.0.tgz", - "integrity": "sha512-Qc7AlCE4uhB+SvHLqah4RgR1WdY7wmmr/hx9g/prDP9R1ocshmUEMrZK9qjuwaklW7/fmkFCXI8ETxo5L1bHIA==", + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.52.0.tgz", + "integrity": "sha512-Sc7StsvC0bwhMcgDfTRWUIexO5cNzzKUurvUwtpgQUnxO7AzexU3lkY3yHYDsCbWYAEQMXAgQYQtbcqoh+Ie7g==", "license": "MIT", "dependencies": { - "@sentry/core": "10.51.0" + "@sentry/core": "10.52.0" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 13058ff..70c4144 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "call-me", - "version": "1.3.44", + "version": "1.3.45", "description": "Your Go-To for Instant Video Calls", "author": "Miroslav Pejic - miroslav.pejic.85@gmail.com", "license": "AGPLv3", @@ -21,7 +21,7 @@ }, "dependencies": { "@ngrok/ngrok": "1.7.0", - "@sentry/node": "^10.51.0", + "@sentry/node": "^10.52.0", "axios": "^1.16.0", "colors": "^1.4.0", "cors": "^2.8.6", diff --git a/public/client.js b/public/client.js index 331ded2..3b6ec34 100755 --- a/public/client.js +++ b/public/client.js @@ -90,6 +90,8 @@ let callingTimerId = null; // Timer for calling overlay let callingElapsed = 0; // Seconds elapsed while calling let incomingCallData = null; // Pending incoming call data let incomingCallTimerId = null; // Auto-decline timer for incoming call +let ringTimeout = 30; // Seconds before unanswered call is auto-cancelled/declined (from server) +let ringingAudio = null; // Looping ring sound for incoming call let thisConnection; let camera = 'user'; let stream; @@ -777,6 +779,9 @@ function showCallingOverlay(targetUser) { callingTimerId = setInterval(() => { callingElapsed++; if (callingTimer) callingTimer.textContent = callingElapsed + 's'; + if (callingElapsed >= ringTimeout) { + handleCancelCall(); + } }, 1000); } @@ -1375,6 +1380,9 @@ function handlePing(data) { if (data.pushEnabled !== undefined) { pushEnabled = data.pushEnabled; } + if (data.ringTimeout !== undefined) { + ringTimeout = data.ringTimeout; + } sendMsg({ type: 'pong', message: { @@ -1717,7 +1725,6 @@ function offerAccept(data) { incomingCallData = data; showIncomingCallOverlay(data.from); - sound('ring'); } // Show incoming call overlay @@ -1725,20 +1732,47 @@ function showIncomingCallOverlay(callerName) { if (!incomingCallOverlay) return; if (incomingCallUsername) incomingCallUsername.textContent = callerName; - // Reset timer bar animation + // Reset timer bar animation with dynamic duration if (incomingCallTimer) { + incomingCallTimer.style.setProperty('--ring-duration', ringTimeout + 's'); incomingCallTimer.style.animation = 'none'; incomingCallTimer.offsetHeight; // Force reflow incomingCallTimer.style.animation = ''; } + // Start looping ring sound with a 1s gap between plays + if (ringingAudio) { + ringingAudio.pause(); + ringingAudio = null; + } + let ringingDelayTimer = null; + function playRing() { + if (!ringingAudio) return; + ringingAudio.currentTime = 0; + ringingAudio.play().catch(() => {}); + } + ringingAudio = new Audio('./assets/ring.wav'); + ringingAudio.volume = 0.5; + ringingAudio.addEventListener('ended', () => { + ringingDelayTimer = setTimeout(playRing, 3000); + }); + // Store the delay timer on the audio object so hideIncomingCallOverlay can clear it + ringingAudio._delayTimer = null; + Object.defineProperty(ringingAudio, '_delayTimer', { + get: () => ringingDelayTimer, + set: (v) => { + ringingDelayTimer = v; + }, + }); + playRing(); + incomingCallOverlay.style.display = 'flex'; - // Auto-decline after 10 seconds + // Auto-decline after ringTimeout seconds if (incomingCallTimerId) clearTimeout(incomingCallTimerId); incomingCallTimerId = setTimeout(() => { handleDeclineIncomingCall(); - }, 10000); + }, ringTimeout * 1000); } // Hide incoming call overlay @@ -1749,6 +1783,11 @@ function hideIncomingCallOverlay() { clearTimeout(incomingCallTimerId); incomingCallTimerId = null; } + if (ringingAudio) { + clearTimeout(ringingAudio._delayTimer); + ringingAudio.pause(); + ringingAudio = null; + } incomingCallData = null; } diff --git a/public/style.css b/public/style.css index dcef56c..825cdd8 100644 --- a/public/style.css +++ b/public/style.css @@ -3010,7 +3010,7 @@ z-index: height: 100%; width: 100%; background: linear-gradient(90deg, var(--success-color), var(--primary-color)); - animation: incomingTimerBar 10s linear forwards; + animation: incomingTimerBar var(--ring-duration, 30s) linear forwards; transform-origin: left; }