diff --git a/db/knex_migrations/2025-06-27-0001-add-rtsp.js b/db/knex_migrations/2025-06-27-0001-add-rtsp.js new file mode 100644 index 000000000..ca43423e0 --- /dev/null +++ b/db/knex_migrations/2025-06-27-0001-add-rtsp.js @@ -0,0 +1,18 @@ +// Add new columns and alter 'manual_status' to smallint +// migration file: add_rtsp_fields_to_monitor.js + +exports.up = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.string("rtsp_username"); + table.string("rtsp_password"); + table.string("rtsp_path"); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.dropColumn("rtsp_username"); + table.dropColumn("rtsp_password"); + table.dropColumn("rtsp_path"); + }); +}; diff --git a/package-lock.json b/package-lock.json index ccb72dee3..03e832d7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "uptime-kuma", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "uptime-kuma", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "license": "MIT", "dependencies": { "@grpc/grpc-js": "~1.8.22", @@ -75,6 +75,7 @@ "qs": "~6.10.4", "redbean-node": "~0.3.0", "redis": "~4.5.1", + "rtsp-client": "^1.4.5", "semver": "~7.5.4", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", @@ -14850,6 +14851,16 @@ "rtlcss": "bin/rtlcss.js" } }, + "node_modules/rtsp-client": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/rtsp-client/-/rtsp-client-1.4.5.tgz", + "integrity": "sha512-21ZjCoGZdCPOTZOME1BzZ+OCXJIU6SQoGEAKwlbuVPU/jhAX9S8wTr+ZkBMR074xK28UfFx1KQvTV1/wECU3MA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15", + "www-authenticate": "^0.6.2" + } + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -17852,6 +17863,14 @@ } } }, + "node_modules/www-authenticate": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/www-authenticate/-/www-authenticate-0.6.3.tgz", + "integrity": "sha512-8VkdLBJiBh5aXlJvcVaPykwSI//OA+Sxw7g84vIyCqoqlXtLupGNhyXxbgVuZ7g5ZS+lCJ4bTtcw/gJciqEuAg==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/xml-js": { "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", diff --git a/package.json b/package.json index 97b7bc339..327be7598 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "qs": "~6.10.4", "redbean-node": "~0.3.0", "redis": "~4.5.1", + "rtsp-client": "^1.4.5", "semver": "~7.5.4", "socket.io": "~4.8.0", "socket.io-client": "~4.8.0", diff --git a/server/model/monitor.js b/server/model/monitor.js index 0ddfa924c..aa5a05a7c 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -198,6 +198,10 @@ class Monitor extends BeanModel { kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), rabbitmqUsername: this.rabbitmqUsername, rabbitmqPassword: this.rabbitmqPassword, + rtspUsername: this.rtspUsername, + rtspPassword: this.rtspPassword, + rtspPath: this.rtspPath + }; } diff --git a/server/monitor-types/rtsp.js b/server/monitor-types/rtsp.js new file mode 100644 index 000000000..92f21501e --- /dev/null +++ b/server/monitor-types/rtsp.js @@ -0,0 +1,68 @@ +const { MonitorType } = require("./monitor-type"); +const RTSPClient = require("rtsp-client"); +const { log, UP, DOWN } = require("../../src/util"); + +class RtspMonitorType extends MonitorType { + name = "rtsp"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + const { rtspUsername, rtspPassword, hostname, port, rtspPath } = monitor; + + // Construct RTSP URL + let url = `rtsp://${hostname}:${port}${rtspPath}`; + if (rtspUsername && rtspPassword !== undefined) { + url = `rtsp://${rtspUsername}:${rtspPassword}@${hostname}:${port}${rtspPath}`; + } + + // Validate URL + if (!url || !url.startsWith("rtsp://")) { + heartbeat.status = DOWN; + heartbeat.msg = "Invalid RTSP URL"; + return; + } + + const client = new RTSPClient(); + client.on("error", (err) => { + log.debug("monitor", `RTSP client emitted error: ${err.message}`); + }); + + try { + log.debug("monitor", `Connecting to RTSP URL: ${url}`); + await client.connect(url); + + const res = await client.describe(); + log.debug("monitor", `RTSP DESCRIBE response: ${JSON.stringify(res)}`); + + const statusCode = res?.statusCode; + const statusMessage = res?.statusMessage || "Unknown"; + + if (statusCode === 200) { + heartbeat.status = UP; + heartbeat.msg = "RTSP stream is accessible"; + } else if (statusCode === 503) { + heartbeat.status = DOWN; + heartbeat.msg = res.body?.reason || "Service Unavailable"; + } else { + heartbeat.status = DOWN; + heartbeat.msg = `${statusCode} - ${statusMessage}`; + } + } catch (error) { + log.debug("monitor", `[${monitor.name}] RTSP check failed: ${error.message}`); + heartbeat.status = DOWN; + heartbeat.msg = `RTSP check failed: ${error.message}`; + } finally { + try { + await client.close(); + } catch (closeError) { + log.debug("monitor", `Error closing RTSP client: ${closeError.message}`); + } + } + } +} + +module.exports = { + RtspMonitorType, +}; diff --git a/server/server.js b/server/server.js index b7025464b..532a93c64 100644 --- a/server/server.js +++ b/server/server.js @@ -878,6 +878,9 @@ let needSetup = false; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.manual_status = monitor.manual_status; + bean.rtspUsername = monitor.rtspUsername; + bean.rtspPassword = monitor.rtspPassword; + bean.rtspPath = monitor.rtspPath; // ping advanced options bean.ping_numeric = monitor.ping_numeric; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index a04e6bd49..bad37f7f4 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -119,6 +119,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); + UptimeKumaServer.monitorTypeList["rtsp"] = new RtspMonitorType(); // Allow all CORS origins (polling) in development let cors = undefined; @@ -560,4 +561,5 @@ const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); const { ManualMonitorType } = require("./monitor-types/manual"); +const { RtspMonitorType } = require("./monitor-types/rtsp"); const Monitor = require("./model/monitor"); diff --git a/src/lang/en.json b/src/lang/en.json index b6449371b..846729940 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1125,5 +1125,9 @@ "Staged Tags for Batch Add": "Staged Tags for Batch Add", "Clear Form": "Clear Form", "pause": "Pause", - "Manual": "Manual" + "Manual": "Manual", + "RTSP Username": "RTSP Username", + "RTSP Password": "RTSP Password", + "RTSP Path": "RTSP Path", + "Path": "Path" } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 1b7af4184..2f2bd1535 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -97,6 +97,9 @@ + @@ -300,7 +303,7 @@ -
+
-
+
@@ -515,6 +518,13 @@
+ + + +