Initial commit
This commit is contained in:
commit
f637301f6a
18
.env.sample
Normal file
18
.env.sample
Normal file
@ -0,0 +1,18 @@
|
||||
# The port to listen on
|
||||
PORT=3000
|
||||
|
||||
# Endpoint for S3-compatible storage. Cloudflare uses an endpoint that contains
|
||||
# the account ID.
|
||||
S3_ENDPOINT=https://xxxxxxxxxxxxxxxxxxx.r2.cloudflarestorage.com
|
||||
|
||||
# Credentials for the S3 bucket to store the stream in.
|
||||
S3_ACCESS_KEY_ID=xx
|
||||
S3_ACCESS_KEY_SECRET=xxx
|
||||
S3_BUCKET_NAME=bucketname
|
||||
|
||||
# The public where the bucket is accessible from. This is used to generate a link
|
||||
# to the stream but is for informational purposes only.
|
||||
PUBLIC_BASE_URL=https://yourdomain.com
|
||||
|
||||
# The maximum number of files to keep in the TLS directory. This is used to
|
||||
MAX_TS_FILES_TO_KEEP=20
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.env
|
||||
node_modules
|
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"rangeStart": 0,
|
||||
"rangeEnd": 9007199254740991,
|
||||
"requirePragma": false,
|
||||
"insertPragma": false,
|
||||
"proseWrap": "preserve"
|
||||
}
|
2291
package-lock.json
generated
Normal file
2291
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "srs-s3",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.363.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
}
|
9
src/env.ts
Normal file
9
src/env.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
export const S3_ENDPOINT = process.env.S3_ENDPOINT;
|
||||
export const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID;
|
||||
export const S3_ACCESS_KEY_SECRET = process.env.S3_ACCESS_KEY_SECRET;
|
||||
export const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || 'bucket';
|
||||
export const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || 'https://your.public.domain';
|
||||
export const MAX_TS_FILES_TO_KEEP = parseInt(process.env.MAX_TS_FILES_TO_KEEP || '20', 10);
|
||||
export const PORT = process.env.PORT || 3000;
|
49
src/index.ts
Normal file
49
src/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import express from 'express';
|
||||
import { HLSUpdateEvent } from './types';
|
||||
import { createS3Client, deleteFile, uploadFile } from './s3';
|
||||
import { S3_BUCKET_NAME, PORT, MAX_TS_FILES_TO_KEEP } from './env';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json()); // for parsing application/json
|
||||
|
||||
const s3Client = createS3Client();
|
||||
|
||||
const processOnHlsEvent = async (hlsEvent: HLSUpdateEvent) => {
|
||||
console.log(
|
||||
`Server ID: ${hlsEvent.server_id}, App: ${hlsEvent.app}, Stream: ${hlsEvent.stream}, SEQ: ${hlsEvent.seq_no}`
|
||||
);
|
||||
|
||||
// Upload ts file
|
||||
await uploadFile(
|
||||
s3Client,
|
||||
`${hlsEvent.cwd}/${hlsEvent.file}`,
|
||||
S3_BUCKET_NAME,
|
||||
`${hlsEvent.stream}/${hlsEvent.url}`,
|
||||
'video/mp2t'
|
||||
);
|
||||
|
||||
// Upload m3u8 file
|
||||
const m3u8S3Name = `${hlsEvent.stream}/${hlsEvent.m3u8_url}`;
|
||||
await uploadFile(s3Client, `${hlsEvent.cwd}/${hlsEvent.m3u8}`, S3_BUCKET_NAME, m3u8S3Name, 'application/x-mpegURL');
|
||||
|
||||
// console.log(`- Stream available at: ${PUBLIC_BASE_URL}/${m3u8S3Name}`);
|
||||
|
||||
// Delete OLD ts files
|
||||
const current = hlsEvent.seq_no;
|
||||
if (current > MAX_TS_FILES_TO_KEEP) {
|
||||
const fileToDelete = hlsEvent.url.replace(/[0-9]+\.ts$/, `${current - MAX_TS_FILES_TO_KEEP}.ts`);
|
||||
await deleteFile(s3Client, S3_BUCKET_NAME, `${hlsEvent.stream}/${fileToDelete}`);
|
||||
}
|
||||
};
|
||||
|
||||
app.post('/api/v1/hls', async (req, res, next) => {
|
||||
// console.debug('POST /api/v1/hls called: Received HLS update event.');
|
||||
const hlsEvent = req.body as HLSUpdateEvent;
|
||||
await processOnHlsEvent(hlsEvent);
|
||||
res.send('0'); // srs needs 0 == OK
|
||||
});
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`SRS S3 Uploader started on port ${PORT}. Waiting for events...`);
|
||||
console.log(`Uploading to S3 Bucket: ${S3_BUCKET_NAME}`);
|
||||
});
|
53
src/s3.ts
Normal file
53
src/s3.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import fs from 'fs';
|
||||
import { S3_ACCESS_KEY_ID, S3_ACCESS_KEY_SECRET, S3_ENDPOINT } from './env';
|
||||
|
||||
export const createS3Client = () => {
|
||||
if (!S3_ACCESS_KEY_ID) {
|
||||
throw new Error('S3_ACCESS_KEY_ID is not set.');
|
||||
}
|
||||
if (!S3_ACCESS_KEY_SECRET) {
|
||||
throw new Error('S3_ACCESS_KEY_SECRET is not set.');
|
||||
}
|
||||
|
||||
return new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: S3_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: S3_ACCESS_KEY_SECRET,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadFile = async (
|
||||
s3Client: S3Client,
|
||||
sourceFilePath: string,
|
||||
bucket: string,
|
||||
targetKey: string,
|
||||
contentType: string
|
||||
) => {
|
||||
const fileStream = fs.createReadStream(sourceFilePath);
|
||||
console.log(`- Uploading File: ${sourceFilePath} to ${bucket}:${targetKey}`);
|
||||
|
||||
return await s3Client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: targetKey,
|
||||
Body: fileStream,
|
||||
ACL: 'public-read',
|
||||
ContentType: 'video/mp2t',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteFile = async (s3Client: S3Client, bucket: string, targetKey: string) => {
|
||||
console.log(`- Deleting File in S3: ${bucket}:${targetKey}`);
|
||||
|
||||
await s3Client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: targetKey,
|
||||
})
|
||||
);
|
||||
};
|
30
src/types.ts
Normal file
30
src/types.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
{"server_id":"vid-34g09wb","service_id":"4l00gw4n","action":"on_hls",
|
||||
"client_id":"7lgl6020","ip":"127.0.0.1","vhost":"__defaultVhost__","app":
|
||||
"live","tcUrl":"rtmp://localhost/live","stream":"livestream","param":"",
|
||||
"duration":12.01,"cwd":"/Users/t3adminfmau/dev/srs/trunk",
|
||||
"file":"./objs/nginx/html/live/livestream-80.ts","url":"live/livestream-80.ts",
|
||||
"m3u8":"./objs/nginx/html/live/livestream.m3u8","m3u8_url":"live/livestream.m3u8",
|
||||
"seq_no":80,"stream_url":"/live/livestream","stream_id":"vid-q015v6y"}
|
||||
*/
|
||||
export type HLSUpdateEvent = {
|
||||
server_id: string;
|
||||
service_id: string;
|
||||
action: string;
|
||||
client_id: string;
|
||||
ip: string;
|
||||
vhost: string;
|
||||
app: string;
|
||||
tcUrl: string;
|
||||
stream: string;
|
||||
param: string;
|
||||
duration: number;
|
||||
cwd: string;
|
||||
file: string;
|
||||
url: string;
|
||||
m3u8: string;
|
||||
m3u8_url: string;
|
||||
seq_no: number;
|
||||
stream_url: string;
|
||||
stream_id: string;
|
||||
};
|
Loading…
Reference in New Issue
Block a user