Created
March 28, 2024 22:18
-
-
Save pbatey/2f738b5426d91eec5289c1046fcb56b2 to your computer and use it in GitHub Desktop.
express route to serve mp4 video files from AWS s3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// npm i express@^4.15.2 @aws-sdk/client-s3@^3.540.0 | |
import { Request, Response, Router } from 'express' | |
import { GetObjectCommand, HeadObjectCommand, S3Client } from '@aws-sdk/client-s3' | |
const router = Router() | |
/** stream a video from S3 */ | |
router.get('/:filepath(*.mp4)', async (req: Request, res: Response) => { | |
const filepath = req.params.filepath || req.params[0] | |
const range = req.headers.range; | |
if (!range) { | |
res.status(416).send({err: 'Wrong range'}) | |
return | |
} | |
const credentials = { | |
accessKeyId: process.env.S3_ACCESS_KEY_ID || '', | |
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '', | |
} | |
const region = process.env.S3_REGION | |
const bucket = process.env.S3_BUCKET | |
if (!credentials.accessKeyId || credentials.secretAccessKey || !region || !bucket) { | |
console.error('Unexpected error. At least one of S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION, or S3_BUCKET is unset.') | |
res.status(500).send({err: 'Unexpected error. See server log for details.'}) | |
return | |
} | |
const client = new S3Client({region, credentials}) | |
const headRes = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: filepath })) | |
const contentLength = headRes.ContentLength | |
const lastModified = headRes.LastModified?.toUTCString() | |
const etag = headRes.ETag | |
if (!contentLength) { | |
res.status(404).send({err: 'Not found'}) | |
return | |
} | |
const [starts, ends] = range.replace(/bytes=/, '').split('-') | |
const start = parseInt(starts, 10) | |
const end = ends ? parseInt(ends, 10) : contentLength - 1 | |
const getRes = await client.send(new GetObjectCommand({ Bucket: bucket, Key: filepath, Range: range })) | |
res.status(206) | |
Object.entries({ | |
'Accept-Ranges': 'bytes', | |
'Cache-Control': 'no-cache', | |
'Connection': 'keep-alive', | |
'Content-Length': (end-start)+1, | |
'Content-Range': `bytes ${start}-${end}/${contentLength}`, | |
'Content-Type': 'video/mp4', | |
'ETag': etag, | |
'Keep-Alive': 'timeout=5', | |
'Last-Modified': lastModified, | |
}).forEach(([k,v])=>v && res.setHeader(k,v)) | |
if (!getRes.Body) { | |
res.status(502).send({err: 'Failed to get content from S3.'}) | |
return | |
} | |
// casting since pipe is unexpectedly undefined for GetObjectCommandOutput.Body | |
(getRes.Body as ({pipe:(res:Response)=>void})).pipe(res) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment