Skip to content

Commit fe65fda

Browse files
committed
feat: Implement Nginx for HLS stream serving with optimized caching and updated application performance settings.
1 parent fa46563 commit fe65fda

3 files changed

Lines changed: 114 additions & 33 deletions

File tree

app.js

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,25 @@ app.locals.appVersion = process.env.APP_VERSION || `v${pkg.version}`;
3939

4040
// =================== Security & Middleware ===================
4141

42+
// Disable ETag to reduce server overhead
43+
app.set("etag", false);
44+
45+
// Global connection optimization
46+
app.use((req, res, next) => {
47+
res.setHeader("Connection", "keep-alive");
48+
next();
49+
});
50+
4251
// =================== Global Performance & Security ===================
4352
app.use(compression()); // Compress all responses
4453

4554
// Optimize nonce generation: Only for HTML/EJS requests to reduce crypto overhead
4655
app.use((req, res, next) => {
47-
const isHtml = req.accepts('html');
56+
const isHtml = req.accepts("html");
4857
if (isHtml) {
4958
res.locals.nonce = crypto.randomBytes(16).toString("base64");
5059
} else {
51-
res.locals.nonce = '';
60+
res.locals.nonce = "";
5261
}
5362
next();
5463
});
@@ -70,15 +79,19 @@ const CACHE_DURATION = 30000; // 30 seconds
7079
app.use(async (req, res, next) => {
7180
try {
7281
// Only fetch for pages that might render the navbar
73-
if (req.method === 'GET' && !req.path.startsWith('/api/') && !req.path.startsWith('/public/dvr/')) {
74-
const now = Date.now();
75-
if (!cachedActivities || (now - lastFetchTime > CACHE_DURATION)) {
76-
cachedActivities = await getRecentActivities(5);
77-
lastFetchTime = now;
78-
}
79-
res.locals.recentActivities = cachedActivities;
82+
if (
83+
req.method === "GET" &&
84+
!req.path.startsWith("/api/") &&
85+
!req.path.startsWith("/public/dvr/")
86+
) {
87+
const now = Date.now();
88+
if (!cachedActivities || now - lastFetchTime > CACHE_DURATION) {
89+
cachedActivities = await getRecentActivities(5);
90+
lastFetchTime = now;
91+
}
92+
res.locals.recentActivities = cachedActivities;
8093
}
81-
} catch(err) {
94+
} catch (err) {
8295
res.locals.recentActivities = [];
8396
}
8497
next();
@@ -90,7 +103,7 @@ const skipVideo = (req, res) => {
90103

91104
app.use(
92105
morgan(
93-
process.env.NODE_ENV === "production" ? "combined" : "dev", // Use faster 'combined' or 'tiny' in prod
106+
process.env.NODE_ENV === "production" ? "tiny" : "dev", // Use faster 'tiny' in prod
94107
{
95108
skip: skipVideo,
96109
stream: {
@@ -116,7 +129,7 @@ app.use(
116129
"'self'",
117130
"https://cdn.jsdelivr.net",
118131
"https://cdnjs.cloudflare.com",
119-
(req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : '',
132+
(req, res) => (res.locals.nonce ? `'nonce-${res.locals.nonce}'` : ""),
120133
].filter(Boolean),
121134
"worker-src": ["'self'", "blob:"],
122135
"style-src": [
@@ -157,8 +170,8 @@ app.use(
157170
})
158171
);
159172

160-
app.use(express.json({ limit: "10mb" }));
161-
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
173+
app.use(express.json({ limit: "1mb" }));
174+
app.use(express.urlencoded({ extended: true, limit: "1mb" }));
162175
app.use(cookieParser());
163176

164177
// CSRF Protection Initialization (Cookie-based)
@@ -213,29 +226,26 @@ app.use(
213226
"/hls",
214227
express.static(streamDir, {
215228
setHeaders: (res, filePath) => {
216-
// Prevent caching of HLS files to avoid stale footage
217-
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
218-
res.setHeader("Pragma", "no-cache");
219-
res.setHeader("Expires", "0");
220-
res.setHeader("Surrogate-Control", "no-store");
221-
222229
if (filePath.endsWith(".m3u8")) {
230+
res.setHeader("Cache-Control", "no-cache");
223231
res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
224232
} else if (filePath.endsWith(".ts")) {
233+
res.setHeader("Cache-Control", "public, max-age=60");
225234
res.setHeader("Content-Type", "video/mp2t");
226235
}
227236
},
228237
})
229238
);
230239

231-
// =================== Disable HLS Cache ===================
240+
// =================== Optimal HLS Caching strategy ===================
232241
app.use(
233242
"/streams",
234243
(req, res, next) => {
235-
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
236-
res.setHeader("Pragma", "no-cache");
237-
res.setHeader("Expires", "0");
238-
res.setHeader("Surrogate-Control", "no-store");
244+
if (req.path.endsWith(".m3u8")) {
245+
res.setHeader("Cache-Control", "no-cache");
246+
} else if (req.path.endsWith(".ts")) {
247+
res.setHeader("Cache-Control", "public, max-age=60");
248+
}
239249
next();
240250
},
241251
express.static(path.join(__dirname, "public", "streams"))
@@ -245,8 +255,10 @@ app.use(
245255
app.use(publicRoutes);
246256

247257
// 2. Specific Security Middleware for Admin/API
248-
// Apply CSRF protection to routes that render forms or handle POSTs
249258
app.use("/", (req, res, next) => {
259+
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
260+
return next();
261+
}
250262
// Skip CSRF for purely public streaming APIs if they are GET only
251263
if (req.path.startsWith("/api/public") && req.method === "GET") {
252264
return next();
@@ -275,7 +287,7 @@ app.use((req, res, next) => {
275287

276288
// 3. Admin/Protected Routes
277289
// Note: Sensitive rate limiting is now applied inside userRouter specifically for login/password
278-
app.use("/", userRouter);
290+
app.use("/", userRouter);
279291
app.use("/camera", checkAuth, cameraRoutes);
280292
app.use("/dvr", checkAuth, dvrRoutes);
281293
app.use(settingsRoutes);
@@ -298,7 +310,6 @@ app.post("/api/start-stream", async (req, res) => {
298310
}
299311
});
300312

301-
302313
app.post("/api/stop-stream", (req, res) => {
303314
const { rtspUrl } = req.body;
304315
if (!rtspUrl) return res.status(400).json({ error: "Missing RTSP URL" });
@@ -340,10 +351,6 @@ app.get("/api/public/camera/:id/hls", async (req, res) => {
340351
}
341352
});
342353

343-
344-
345-
346-
347354
// =================== Start Server ===================
348355
const server = http.createServer(app);
349356

@@ -364,7 +371,7 @@ const gracefulShutdown = () => {
364371
logger.info("HTTP server closed.");
365372
process.exit(0);
366373
});
367-
374+
368375
// Force exit if server doesn't close in 5 seconds
369376
setTimeout(() => {
370377
logger.error("Could not close connections in time, forcefully shutting down");

docker-compose.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@ services:
2626
- "traefik.http.routers.streamvision.tls.certresolver=letsencrypt"
2727
- "traefik.http.services.streamvision.loadbalancer.server.port=3000"
2828

29+
nginx_hls:
30+
image: nginx:alpine
31+
container_name: nginx_hls
32+
restart: unless-stopped
33+
volumes:
34+
- ./streams:/usr/share/nginx/html/hls:ro
35+
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
36+
networks:
37+
- proxy
38+
labels:
39+
- "traefik.enable=true"
40+
- "traefik.http.routers.nginx-hls.rule=Host(`cctvcameralive.in`) && PathPrefix(`/hls`)"
41+
- "traefik.http.routers.nginx-hls.priority=20"
42+
- "traefik.http.routers.nginx-hls.entrypoints=websecure"
43+
- "traefik.http.routers.nginx-hls.tls.certresolver=letsencrypt"
44+
- "traefik.http.middlewares.hls-headers.headers.customResponseHeaders.Access-Control-Allow-Origin=*"
45+
- "traefik.http.routers.nginx-hls.middlewares=hls-headers"
46+
- "traefik.http.services.nginx-hls.loadbalancer.server.port=80"
47+
2948

3049
networks:
3150

nginx.conf

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
server {
2+
listen 80;
3+
server_name _;
4+
5+
# TCP / Connection optimization
6+
keepalive_timeout 65;
7+
sendfile on;
8+
tcp_nopush on;
9+
tcp_nodelay on;
10+
11+
# GZIP compression for playlists
12+
gzip on;
13+
gzip_types application/vnd.apple.mpegurl;
14+
15+
# Directory mapping: ./streams mounted to /usr/share/nginx/html/hls
16+
root /usr/share/nginx/html;
17+
18+
# Handle CORS for video chunks
19+
add_header Access-Control-Allow-Origin *;
20+
21+
# Serve bulky .ts chunks directly from Nginx avoiding Node JS single thread
22+
location ~ \.ts$ {
23+
expires 60s;
24+
add_header Cache-Control "public, max-age=60";
25+
add_header Access-Control-Allow-Origin *;
26+
types {
27+
video/mp2t ts;
28+
}
29+
try_files $uri =404;
30+
}
31+
32+
# Proxy lightweight .m3u8 playlists back to Express
33+
# Ensures Express heartbeat middleware keeps streams alive
34+
location ~ \.m3u8$ {
35+
proxy_pass http://streamvision_app:3000;
36+
37+
# Explicit cache and CORS headers for proxy
38+
add_header Cache-Control "no-cache";
39+
add_header Access-Control-Allow-Origin *;
40+
41+
proxy_set_header Host $host;
42+
proxy_set_header X-Real-IP $remote_addr;
43+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44+
proxy_set_header X-Forwarded-Proto $scheme;
45+
}
46+
47+
# Proxy anything else routed to this container back to Express just in case
48+
location / {
49+
proxy_pass http://streamvision_app:3000;
50+
proxy_set_header Host $host;
51+
proxy_set_header X-Real-IP $remote_addr;
52+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
53+
proxy_set_header X-Forwarded-Proto $scheme;
54+
}
55+
}

0 commit comments

Comments
 (0)