proxy.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import express from 'express'
  2. import { PassThrough, pipeline } from 'stream'
  3. import { HttpStatusCode } from '@peertube/peertube-models'
  4. import { buildReinjectVideoFileTokenQuery } from '@server/controllers/shared/m3u8-playlist.js'
  5. import { logger } from '@server/helpers/logger.js'
  6. import { StreamReplacer } from '@server/helpers/stream-replacer.js'
  7. import { MStreamingPlaylist, MVideo } from '@server/types/models/index.js'
  8. import { injectQueryToPlaylistUrls } from '../hls.js'
  9. import { getHLSFileReadStream, getWebVideoFileReadStream } from './videos.js'
  10. import type { GetObjectCommandOutput } from '@aws-sdk/client-s3'
  11. export async function proxifyWebVideoFile (options: {
  12. req: express.Request
  13. res: express.Response
  14. filename: string
  15. }) {
  16. const { req, res, filename } = options
  17. logger.debug('Proxifying Web Video file %s from object storage.', filename)
  18. try {
  19. const { response: s3Response, stream } = await getWebVideoFileReadStream({
  20. filename,
  21. rangeHeader: req.header('range')
  22. })
  23. setS3Headers(res, s3Response)
  24. return stream.pipe(res)
  25. } catch (err) {
  26. return handleObjectStorageFailure(res, err)
  27. }
  28. }
  29. export async function proxifyHLS (options: {
  30. req: express.Request
  31. res: express.Response
  32. playlist: MStreamingPlaylist
  33. video: MVideo
  34. filename: string
  35. reinjectVideoFileToken: boolean
  36. }) {
  37. const { req, res, playlist, video, filename, reinjectVideoFileToken } = options
  38. logger.debug('Proxifying HLS file %s from object storage.', filename)
  39. try {
  40. const { response: s3Response, stream } = await getHLSFileReadStream({
  41. playlist: playlist.withVideo(video),
  42. filename,
  43. rangeHeader: req.header('range')
  44. })
  45. setS3Headers(res, s3Response)
  46. const streamReplacer = reinjectVideoFileToken
  47. ? new StreamReplacer(line => injectQueryToPlaylistUrls(line, buildReinjectVideoFileTokenQuery(req, filename.endsWith('master.m3u8'))))
  48. : new PassThrough()
  49. return pipeline(
  50. stream,
  51. streamReplacer,
  52. res,
  53. err => {
  54. if (!err) return
  55. handleObjectStorageFailure(res, err)
  56. }
  57. )
  58. } catch (err) {
  59. return handleObjectStorageFailure(res, err)
  60. }
  61. }
  62. // ---------------------------------------------------------------------------
  63. // Private
  64. // ---------------------------------------------------------------------------
  65. function handleObjectStorageFailure (res: express.Response, err: Error) {
  66. if (err.name === 'NoSuchKey') {
  67. logger.debug('Could not find key in object storage to proxify private HLS video file.', { err })
  68. return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
  69. }
  70. logger.error('Object storage failure', { err })
  71. return res.fail({
  72. status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
  73. message: err.message,
  74. type: err.name
  75. })
  76. }
  77. function setS3Headers (res: express.Response, s3Response: GetObjectCommandOutput) {
  78. if (s3Response.$metadata.httpStatusCode === HttpStatusCode.PARTIAL_CONTENT_206) {
  79. res.setHeader('Content-Range', s3Response.ContentRange)
  80. res.status(HttpStatusCode.PARTIAL_CONTENT_206)
  81. }
  82. }