Initial commit

This commit is contained in:
florian 2023-07-07 22:46:50 +02:00
commit f637301f6a
9 changed files with 2489 additions and 0 deletions

18
.env.sample Normal file
View 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
View File

@ -0,0 +1,2 @@
.env
node_modules

16
.prettierrc Normal file
View 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

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View 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
View 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
View 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
View 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
View 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;
};