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