hls.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. package controllers
  2. import (
  3. "net/http"
  4. "path"
  5. "path/filepath"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/owncast/owncast/config"
  10. "github.com/owncast/owncast/core"
  11. "github.com/owncast/owncast/core/data"
  12. "github.com/owncast/owncast/models"
  13. "github.com/owncast/owncast/router/middleware"
  14. "github.com/owncast/owncast/utils"
  15. log "github.com/sirupsen/logrus"
  16. cache "github.com/victorspringer/http-cache"
  17. "github.com/victorspringer/http-cache/adapter/memory"
  18. )
  19. var (
  20. hlsCacheAdapter *cache.Adapter
  21. hlsResponseCache *cache.Client
  22. )
  23. type FileServerHandler struct {
  24. HLSPath string
  25. }
  26. func (fsh *FileServerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
  27. http.ServeFile(rw, r, fsh.HLSPath)
  28. }
  29. // HandleHLSRequest will manage all requests to HLS content.
  30. func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
  31. // Sanity check to limit requests to HLS file types.
  32. if filepath.Ext(r.URL.Path) != ".m3u8" && filepath.Ext(r.URL.Path) != ".ts" {
  33. w.WriteHeader(http.StatusNotFound)
  34. return
  35. }
  36. if hlsCacheAdapter == nil {
  37. ca, err := memory.NewAdapter(
  38. memory.AdapterWithAlgorithm(memory.LRU),
  39. memory.AdapterWithCapacity(50),
  40. memory.AdapterWithStorageCapacity(104_857_600),
  41. )
  42. hlsCacheAdapter = &ca
  43. if err != nil {
  44. log.Warn("unable to create web cache", err)
  45. }
  46. }
  47. // Since HLS segments cannot be changed once they're rendered, we can cache
  48. // individual segments for a long time.
  49. if hlsResponseCache == nil {
  50. rc, err := cache.NewClient(
  51. cache.ClientWithAdapter(*hlsCacheAdapter),
  52. cache.ClientWithTTL(30*time.Second),
  53. cache.ClientWithExpiresHeader(),
  54. )
  55. hlsResponseCache = rc
  56. if err != nil {
  57. log.Warn("unable to create web cache client", err)
  58. }
  59. }
  60. requestedPath := r.URL.Path
  61. relativePath := strings.Replace(requestedPath, "/hls/", "", 1)
  62. fullPath := filepath.Join(config.HLSStoragePath, relativePath)
  63. // If using external storage then only allow requests for the
  64. // master playlist at stream.m3u8, no variants or segments.
  65. if data.GetS3Config().Enabled && relativePath != "stream.m3u8" {
  66. w.WriteHeader(http.StatusNotFound)
  67. return
  68. }
  69. // Handle playlists
  70. if path.Ext(r.URL.Path) == ".m3u8" {
  71. // Playlists should never be cached.
  72. middleware.DisableCache(w)
  73. // Force the correct content type
  74. w.Header().Set("Content-Type", "application/x-mpegURL")
  75. // Use this as an opportunity to mark this viewer as active.
  76. viewer := models.GenerateViewerFromRequest(r)
  77. core.SetViewerActive(&viewer)
  78. } else {
  79. cacheTime := utils.GetCacheDurationSecondsForPath(relativePath)
  80. w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime))
  81. fileServer := &FileServerHandler{HLSPath: fullPath}
  82. hlsResponseCache.Middleware(fileServer).ServeHTTP(w, r)
  83. return
  84. }
  85. middleware.EnableCors(w)
  86. http.ServeFile(w, r, fullPath)
  87. }