@similie/hyphen-rtsp-tunnel 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +83 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/services/day.d.ts +3 -0
  8. package/dist/services/day.d.ts.map +1 -0
  9. package/dist/services/day.js +26 -0
  10. package/dist/services/day.js.map +1 -0
  11. package/dist/services/device-auth.d.ts +26 -0
  12. package/dist/services/device-auth.d.ts.map +1 -0
  13. package/dist/services/device-auth.js +95 -0
  14. package/dist/services/device-auth.js.map +1 -0
  15. package/dist/services/events.d.ts +29 -0
  16. package/dist/services/events.d.ts.map +1 -0
  17. package/dist/services/events.js +14 -0
  18. package/dist/services/events.js.map +1 -0
  19. package/dist/services/index.d.ts +9 -0
  20. package/dist/services/index.d.ts.map +1 -0
  21. package/dist/services/index.js +9 -0
  22. package/dist/services/index.js.map +1 -0
  23. package/dist/services/notifier.d.ts +20 -0
  24. package/dist/services/notifier.d.ts.map +1 -0
  25. package/dist/services/notifier.js +12 -0
  26. package/dist/services/notifier.js.map +1 -0
  27. package/dist/services/rtsp-tunnel-gateway.d.ts +58 -0
  28. package/dist/services/rtsp-tunnel-gateway.d.ts.map +1 -0
  29. package/dist/services/rtsp-tunnel-gateway.js +532 -0
  30. package/dist/services/rtsp-tunnel-gateway.js.map +1 -0
  31. package/dist/services/sqs-notifier.d.ts +23 -0
  32. package/dist/services/sqs-notifier.d.ts.map +1 -0
  33. package/dist/services/sqs-notifier.js +109 -0
  34. package/dist/services/sqs-notifier.js.map +1 -0
  35. package/dist/services/storage-s3.d.ts +12 -0
  36. package/dist/services/storage-s3.d.ts.map +1 -0
  37. package/dist/services/storage-s3.js +49 -0
  38. package/dist/services/storage-s3.js.map +1 -0
  39. package/dist/services/storage-worker.d.ts +22 -0
  40. package/dist/services/storage-worker.d.ts.map +1 -0
  41. package/dist/services/storage-worker.js +100 -0
  42. package/dist/services/storage-worker.js.map +1 -0
  43. package/dist/services/storage.d.ts +28 -0
  44. package/dist/services/storage.d.ts.map +1 -0
  45. package/dist/services/storage.js +41 -0
  46. package/dist/services/storage.js.map +1 -0
  47. package/package.json +48 -0
@@ -0,0 +1,109 @@
1
+ import { SQSClient, SendMessageCommand, } from "@aws-sdk/client-sqs";
2
+ /**
3
+ * Env:
4
+ * - SQS_QUEUE_URL (required)
5
+ * - AWS_REGION (required)
6
+ *
7
+ * Credentials:
8
+ * - resolved automatically by AWS SDK (env / instance role / task role / etc.)
9
+ */
10
+ export class SqsNotifier {
11
+ enabled;
12
+ sqs = null;
13
+ queueUrl = null;
14
+ isFifo = false;
15
+ ctx = null;
16
+ constructor() {
17
+ const queueUrl = process.env.SQS_QUEUE_URL?.trim();
18
+ const region = process.env.AWS_REGION?.trim();
19
+ if (!queueUrl || !region) {
20
+ this.enabled = false;
21
+ return;
22
+ }
23
+ this.enabled = true;
24
+ this.queueUrl = queueUrl;
25
+ this.isFifo = queueUrl.endsWith(".fifo");
26
+ this.sqs = new SQSClient({ region });
27
+ }
28
+ init(ctx) {
29
+ this.ctx = ctx;
30
+ if (!this.enabled) {
31
+ return this.ctx.log("[rtsp-tunnel][sqs] disabled (missing SQS_QUEUE_URL or AWS_REGION)");
32
+ }
33
+ this.ctx.log("[rtsp-tunnel][sqs] enabled queue=", {
34
+ queueUrl: this.queueUrl,
35
+ });
36
+ }
37
+ async shutdown() {
38
+ // AWS SDK v3 clients have destroy() to close sockets
39
+ try {
40
+ this.sqs?.destroy();
41
+ }
42
+ catch { }
43
+ this.sqs = null;
44
+ }
45
+ async send(eventName, payload) {
46
+ this.ctx?.log("[rtsp-tunnel][sqs] send called", { eventName, payload });
47
+ if (!this.enabled || !this.sqs || !this.queueUrl)
48
+ return;
49
+ // Build a consistent message envelope
50
+ const msg = this.buildMessage(eventName, payload);
51
+ const params = {
52
+ QueueUrl: this.queueUrl,
53
+ MessageBody: JSON.stringify(msg),
54
+ };
55
+ // FIFO-safe handling
56
+ if (this.isFifo) {
57
+ params.MessageDeduplicationId = msg.deduplicationId;
58
+ params.MessageGroupId = msg.groupId;
59
+ }
60
+ try {
61
+ await this.sqs.send(new SendMessageCommand(params));
62
+ this.ctx?.log("[rtsp-tunnel][sqs] sent", {
63
+ eventName,
64
+ groupId: msg.groupId,
65
+ });
66
+ }
67
+ catch (err) {
68
+ this.ctx?.log("[rtsp-tunnel][sqs] send failed", {
69
+ eventName,
70
+ error: err?.message ?? err,
71
+ });
72
+ throw err;
73
+ }
74
+ }
75
+ // ----------------- internals -----------------
76
+ buildMessage(eventName, payload) {
77
+ // Prefer payloadId for grouping, then deviceId.
78
+ const groupId = payload.payloadId || payload.deviceId || "hyphen";
79
+ // Dedup: prefer payloadId if present; otherwise sessionId; otherwise a stable fallback.
80
+ // (SQS FIFO requires <= 128 chars; we keep it simple.)
81
+ const deduplicationId = payload.payloadId ||
82
+ payload.sessionId ||
83
+ `${payload.deviceId || "unknown"}:${eventName}:${payload.capturedAt || payload.at || ""}`;
84
+ // For stored snapshots, include storedUri if your storage worker attaches it.
85
+ // For now, it may be undefined and that’s fine.
86
+ const storedUri = payload.storedUri;
87
+ return {
88
+ version: 1,
89
+ source: "hyphen-rtsp-tunnel",
90
+ event: eventName,
91
+ capturedAt: payload.capturedAt || null,
92
+ at: payload.at || null,
93
+ deviceId: payload.deviceId || "unknown",
94
+ payloadId: payload.payloadId ?? null,
95
+ sessionId: payload.sessionId || null,
96
+ remote: payload.remote || null,
97
+ // file paths/uris
98
+ localPath: payload.localPath || null,
99
+ storedUri: storedUri || null,
100
+ // failure details (for snapshot:failed)
101
+ stage: payload.stage || null,
102
+ error: payload.error || null,
103
+ // FIFO helpers
104
+ groupId,
105
+ deduplicationId,
106
+ };
107
+ }
108
+ }
109
+ //# sourceMappingURL=sqs-notifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqs-notifier.js","sourceRoot":"","sources":["../../src/services/sqs-notifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { StorageAdapter, StoreInput, StoreResult } from "./storage.js";
2
+ import { S3Client } from "@aws-sdk/client-s3";
3
+ export declare class S3StorageAdapter implements StorageAdapter {
4
+ private readonly bucket;
5
+ private readonly prefix;
6
+ private readonly deleteOnMove;
7
+ name: string;
8
+ private readonly s3;
9
+ constructor(bucket: string, prefix?: string, s3Client?: S3Client, deleteOnMove?: boolean);
10
+ store(input: StoreInput): Promise<StoreResult>;
11
+ }
12
+ //# sourceMappingURL=storage-s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-s3.d.ts","sourceRoot":"","sources":["../../src/services/storage-s3.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG5E,OAAO,EAAE,QAAQ,EAAoB,MAAM,oBAAoB,CAAC;AAMhE,qBAAa,gBAAiB,YAAW,cAAc;IAKnD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAP/B,IAAI,SAAQ;IACZ,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;gBAGX,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAsB,EAC/C,QAAQ,CAAC,EAAE,QAAQ,EACF,YAAY,GAAE,OAAc;IAMzC,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC;CAmCrD"}
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
4
+ function safeSegment(s) {
5
+ return (s || "unknown").replace(/[^a-zA-Z0-9._/-]/g, "_").slice(0, 128);
6
+ }
7
+ export class S3StorageAdapter {
8
+ bucket;
9
+ prefix;
10
+ deleteOnMove;
11
+ name = "s3";
12
+ s3;
13
+ constructor(bucket, prefix = "hyphen/rtsp", s3Client, deleteOnMove = true) {
14
+ this.bucket = bucket;
15
+ this.prefix = prefix;
16
+ this.deleteOnMove = deleteOnMove;
17
+ this.s3 = s3Client ?? new S3Client({});
18
+ if (!bucket)
19
+ throw new Error("S3StorageAdapter requires bucket");
20
+ }
21
+ async store(input) {
22
+ const fileName = path.basename(input.localPath);
23
+ const day = input.day ?? new Date(input.capturedAt).toISOString().slice(0, 10); // "2025-12-28"
24
+ const keyParts = [
25
+ safeSegment(this.prefix),
26
+ safeSegment(input.deviceId),
27
+ input.payloadId ? safeSegment(input.payloadId) : null,
28
+ // safeSegment(input.capturedAt.replace(/[:.]/g, "-")),
29
+ day,
30
+ fileName,
31
+ ].filter(Boolean);
32
+ const key = keyParts.join("/");
33
+ const body = fs.createReadStream(input.localPath);
34
+ await this.s3.send(new PutObjectCommand({
35
+ Bucket: this.bucket,
36
+ Key: key,
37
+ Body: body,
38
+ ContentType: "image/jpeg",
39
+ }));
40
+ if (this.deleteOnMove) {
41
+ fs.unlinkSync(input.localPath);
42
+ }
43
+ return {
44
+ storage: "s3",
45
+ storedUri: `s3://${this.bucket}/${key}`,
46
+ };
47
+ }
48
+ }
49
+ //# sourceMappingURL=storage-s3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-s3.js","sourceRoot":"","sources":["../../src/services/storage-s3.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEhE,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC1E,CAAC;AAED,MAAM,OAAO,gBAAgB;IAKR;IACA;IAEA;IAPnB,IAAI,GAAG,IAAI,CAAC;IACK,EAAE,CAAW;IAE9B,YACmB,MAAc,EACd,SAAiB,aAAa,EAC/C,QAAmB,EACF,eAAwB,IAAI;QAH5B,WAAM,GAAN,MAAM,CAAQ;QACd,WAAM,GAAN,MAAM,CAAwB;QAE9B,iBAAY,GAAZ,YAAY,CAAgB;QAE7C,IAAI,CAAC,EAAE,GAAG,QAAQ,IAAI,IAAI,QAAQ,CAAC,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAiB;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAChD,MAAM,GAAG,GACP,KAAK,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe;QACrF,MAAM,QAAQ,GAAG;YACf,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;YACxB,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC;YAC3B,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI;YACrD,yDAAyD;YACzD,GAAG;YACH,QAAQ;SACT,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAElB,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE/B,MAAM,IAAI,GAAG,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAElD,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAChB,IAAI,gBAAgB,CAAC;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,IAAI;YACV,WAAW,EAAE,YAAY;SAC1B,CAAC,CACH,CAAC;QAEF,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACjC,CAAC;QAED,OAAO;YACL,OAAO,EAAE,IAAI;YACb,SAAS,EAAE,QAAQ,IAAI,CAAC,MAAM,IAAI,GAAG,EAAE;SACxC,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,22 @@
1
+ import type { RtspTunnelEvents } from "./events.js";
2
+ import type { StorageAdapter } from "./storage.js";
3
+ type CtxLike = {
4
+ log: (...args: any[]) => void;
5
+ };
6
+ export declare class StorageWorker {
7
+ private readonly ctx;
8
+ private readonly events;
9
+ private readonly storage;
10
+ private readonly concurrency;
11
+ private readonly deleteAfterStore;
12
+ private running;
13
+ private inFlight;
14
+ private queue;
15
+ private onCapturedBound;
16
+ constructor(ctx: CtxLike, events: RtspTunnelEvents, storage: StorageAdapter, concurrency?: number, deleteAfterStore?: boolean);
17
+ start(): void;
18
+ stop(): Promise<void>;
19
+ private pump;
20
+ }
21
+ export {};
22
+ //# sourceMappingURL=storage-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-worker.d.ts","sourceRoot":"","sources":["../../src/services/storage-worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAyB,MAAM,aAAa,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,KAAK,OAAO,GAAG;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,CAAC;AAEjD,qBAAa,aAAa;IAOtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAG5B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAZnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,eAAe,CAAqD;gBAGzD,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,cAAc,EACvB,WAAW,SAE3B,EACgB,gBAAgB,UACnB;IAGhB,KAAK;IAgBC,IAAI;IAmBV,OAAO,CAAC,IAAI;CAqDb"}
@@ -0,0 +1,100 @@
1
+ // storage-worker.ts
2
+ import fs from "node:fs";
3
+ import { dayFromCapturedAt } from "./day.js";
4
+ export class StorageWorker {
5
+ ctx;
6
+ events;
7
+ storage;
8
+ concurrency;
9
+ deleteAfterStore;
10
+ running = false;
11
+ inFlight = 0;
12
+ queue = [];
13
+ onCapturedBound = null;
14
+ constructor(ctx, events, storage, concurrency = Number(process.env.STORAGE_CONCURRENCY ?? "2"), deleteAfterStore = (process.env.STORAGE_DELETE_LOCAL ??
15
+ "1") === "1") {
16
+ this.ctx = ctx;
17
+ this.events = events;
18
+ this.storage = storage;
19
+ this.concurrency = concurrency;
20
+ this.deleteAfterStore = deleteAfterStore;
21
+ }
22
+ start() {
23
+ if (this.running)
24
+ return;
25
+ this.running = true;
26
+ this.onCapturedBound = (e) => {
27
+ // cheap enqueue; never await here
28
+ this.queue.push(e);
29
+ this.pump();
30
+ };
31
+ this.events.on("snapshot:captured", this.onCapturedBound);
32
+ this.ctx.log(`[rtsp-tunnel] storage worker started storage=${this.storage.name} concurrency=${this.concurrency}`);
33
+ }
34
+ async stop() {
35
+ if (!this.running)
36
+ return;
37
+ this.running = false;
38
+ if (this.onCapturedBound) {
39
+ this.events.off("snapshot:captured", this.onCapturedBound);
40
+ this.onCapturedBound = null;
41
+ }
42
+ // wait a short window for in-flight tasks (best effort)
43
+ const start = Date.now();
44
+ while (this.inFlight > 0 && Date.now() - start < 5000) {
45
+ await new Promise((r) => setTimeout(r, 50));
46
+ }
47
+ this.queue = [];
48
+ this.ctx.log("[rtsp-tunnel] storage worker stopped");
49
+ }
50
+ pump() {
51
+ if (!this.running)
52
+ return;
53
+ while (this.inFlight < this.concurrency && this.queue.length > 0) {
54
+ const job = this.queue.shift();
55
+ this.inFlight++;
56
+ (async () => {
57
+ try {
58
+ const day = dayFromCapturedAt(job.capturedAt, job.tzOffsetHours ?? null);
59
+ const stored = await this.storage.store({
60
+ localPath: job.localPath,
61
+ deviceId: job.deviceId,
62
+ payloadId: job.payloadId,
63
+ capturedAt: job.capturedAt,
64
+ day,
65
+ });
66
+ // delete local file by default (configurable), unless adapter says otherwise
67
+ const shouldDelete = this.deleteAfterStore && (stored.deleteLocal ?? true);
68
+ if (shouldDelete) {
69
+ try {
70
+ fs.unlinkSync(job.localPath);
71
+ }
72
+ catch { }
73
+ }
74
+ this.events.emitStored({
75
+ ...job,
76
+ storage: stored.storage,
77
+ storedUri: stored.storedUri,
78
+ day,
79
+ tzOffsetHours: job.tzOffsetHours ?? null,
80
+ });
81
+ }
82
+ catch (e) {
83
+ this.events.emitFailed({
84
+ sessionId: job.sessionId,
85
+ deviceId: job.deviceId,
86
+ payloadId: job.payloadId,
87
+ remote: job.remote,
88
+ stage: "store",
89
+ error: e?.message ?? String(e),
90
+ });
91
+ }
92
+ finally {
93
+ this.inFlight--;
94
+ this.pump();
95
+ }
96
+ })();
97
+ }
98
+ }
99
+ }
100
+ //# sourceMappingURL=storage-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-worker.js","sourceRoot":"","sources":["../../src/services/storage-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ export type StoreInput = {
2
+ localPath: string;
3
+ deviceId: string;
4
+ payloadId: string | null;
5
+ capturedAt: string;
6
+ day?: string;
7
+ };
8
+ export type StoreResult = {
9
+ storage: string;
10
+ storedUri: string;
11
+ deleteLocal?: boolean;
12
+ };
13
+ export interface StorageAdapter {
14
+ name: string;
15
+ store(input: StoreInput): Promise<StoreResult>;
16
+ }
17
+ export declare class LocalStorageAdapter implements StorageAdapter {
18
+ name: string;
19
+ store(input: StoreInput): Promise<StoreResult>;
20
+ }
21
+ export declare class LocalMoveStorageAdapter implements StorageAdapter {
22
+ private readonly rootDir;
23
+ private readonly mode;
24
+ name: string;
25
+ constructor(rootDir: string, mode?: "copy" | "move");
26
+ store(input: StoreInput): Promise<StoreResult>;
27
+ }
28
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/services/storage.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAChD;AAGD,qBAAa,mBAAoB,YAAW,cAAc;IACxD,IAAI,SAAW;IACT,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC;CAOrD;AAGD,qBAAa,uBAAwB,YAAW,cAAc;IAG1D,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHvB,IAAI,SAAgB;gBAED,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAAM,GAAG,MAAe;IAK3C,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC;CAiBrD"}
@@ -0,0 +1,41 @@
1
+ // storage.ts
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ // -------- Default: keep local file in place (no-op) --------
5
+ export class LocalStorageAdapter {
6
+ name = "local";
7
+ async store(input) {
8
+ return {
9
+ storage: "local",
10
+ storedUri: `file://${input.localPath}`,
11
+ deleteLocal: false,
12
+ };
13
+ }
14
+ }
15
+ // -------- Optional: move/copy to a new root folder --------
16
+ export class LocalMoveStorageAdapter {
17
+ rootDir;
18
+ mode;
19
+ name = "local-move";
20
+ constructor(rootDir, mode = "move") {
21
+ this.rootDir = rootDir;
22
+ this.mode = mode;
23
+ fs.mkdirSync(rootDir, { recursive: true });
24
+ }
25
+ async store(input) {
26
+ const safeDev = input.deviceId;
27
+ const sub = input.payloadId ? path.join(safeDev, input.payloadId) : safeDev;
28
+ const destDir = path.join(this.rootDir, sub);
29
+ fs.mkdirSync(destDir, { recursive: true });
30
+ const filename = path.basename(input.localPath);
31
+ const destPath = path.join(destDir, filename);
32
+ if (this.mode === "move") {
33
+ fs.renameSync(input.localPath, destPath);
34
+ }
35
+ else {
36
+ fs.copyFileSync(input.localPath, destPath);
37
+ }
38
+ return { storage: this.name, storedUri: `file://${destPath}` };
39
+ }
40
+ }
41
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/services/storage.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAqBzB,8DAA8D;AAC9D,MAAM,OAAO,mBAAmB;IAC9B,IAAI,GAAG,OAAO,CAAC;IACf,KAAK,CAAC,KAAK,CAAC,KAAiB;QAC3B,OAAO;YACL,OAAO,EAAE,OAAO;YAChB,SAAS,EAAE,UAAU,KAAK,CAAC,SAAS,EAAE;YACtC,WAAW,EAAE,KAAK;SACnB,CAAC;IACJ,CAAC;CACF;AAED,6DAA6D;AAC7D,MAAM,OAAO,uBAAuB;IAGf;IACA;IAHnB,IAAI,GAAG,YAAY,CAAC;IACpB,YACmB,OAAe,EACf,OAAwB,MAAM;QAD9B,YAAO,GAAP,OAAO,CAAQ;QACf,SAAI,GAAJ,IAAI,CAA0B;QAE/C,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,KAAiB;QAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC7C,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAE9C,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC3C,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC7C,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,QAAQ,EAAE,EAAE,CAAC;IACjE,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@similie/hyphen-rtsp-tunnel",
3
+ "version": "1.1.0",
4
+ "description": "API module for Hyphen Command Center. Provides REST and WebSocket interfaces for managing RTSP streams and related services.",
5
+ "license": "MIT",
6
+ "author": "adam.smith@similie.org",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "dev": "tsc -p tsconfig.json -w",
22
+ "prepare": "pnpm run build",
23
+ "docs": "./node_modules/typedoc/bin/typedoc --out docs src",
24
+ "release": "npm run build && standard-version"
25
+ },
26
+ "dependencies": {
27
+ "@aws-sdk/client-s3": "^3.958.0",
28
+ "@aws-sdk/client-sqs": "^3.958.0",
29
+ "@similie/ellipsies": "^1.0.16",
30
+ "@similie/hyphen-command-server-types": "^1.0.6",
31
+ "ws": "^8.18.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^24.9.2",
35
+ "@types/ws": "^8.18.1",
36
+ "standard-version": "^9.5.0",
37
+ "ts-node": "^10.9.2",
38
+ "tsx": "^4.20.6",
39
+ "typedoc": "^0.28.14",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "files": [
43
+ "dist",
44
+ "package.json",
45
+ "README.md",
46
+ "LICENSE"
47
+ ]
48
+ }