Skip to content

Instantly share code, notes, and snippets.

@pbatey
Created March 28, 2024 22:18
Show Gist options
  • Save pbatey/2f738b5426d91eec5289c1046fcb56b2 to your computer and use it in GitHub Desktop.
Save pbatey/2f738b5426d91eec5289c1046fcb56b2 to your computer and use it in GitHub Desktop.
express route to serve mp4 video files from AWS s3
// 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