hls.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. package mediaserver
  2. /*
  3. hls.go
  4. HLS delivery endpoints for transcoded video.
  5. This module adds support for HLS based streaming to the media server.
  6. Two endpoints make up the format:
  7. /media/hls/ ?file=<vpath>[&res=][&start=] -> the .m3u8 playlist
  8. /media/hls/segment ?sid=<session>&name=<segment> -> one .ts segment
  9. The playlist request creates (or joins) a transcode session; every segment
  10. line inside it points back at the segment endpoint carrying that session id.
  11. */
  12. import (
  13. "encoding/json"
  14. "net/http"
  15. "os"
  16. "path/filepath"
  17. "strconv"
  18. "imuslab.com/arozos/mod/filesystem"
  19. fs "imuslab.com/arozos/mod/filesystem"
  20. "imuslab.com/arozos/mod/media/transcoder"
  21. "imuslab.com/arozos/mod/utils"
  22. )
  23. // HLSSegmentEndpoint is the URL path that serves individual segments. It is
  24. // baked into every playlist ffmpeg writes, so it must match the route
  25. // registered in mediaServer.go.
  26. const HLSSegmentEndpoint = "/media/hls/segment"
  27. // transcodeResolutionFromRequest reads the optional "res" parameter.
  28. // An unrecognised value falls back to the source resolution, matching the
  29. // behaviour the MP4 endpoint has always had.
  30. func transcodeResolutionFromRequest(r *http.Request) transcoder.TranscodeOutputResolution {
  31. resolution, err := utils.GetPara(r, "res")
  32. if err != nil {
  33. return transcoder.TranscodeResolution_original
  34. }
  35. switch resolution {
  36. case "1080p":
  37. return transcoder.TranscodeResolution_1080p
  38. case "720p":
  39. return transcoder.TranscodeResolution_720p
  40. case "360p":
  41. return transcoder.TranscodeResolution_360p
  42. }
  43. return transcoder.TranscodeResolution_original
  44. }
  45. // startTimeFromRequest reads the optional "start" seek offset in seconds.
  46. func startTimeFromRequest(r *http.Request) float64 {
  47. startTimeStr, _ := utils.GetPara(r, "start")
  48. if startTimeStr == "" {
  49. return 0
  50. }
  51. startTime, err := strconv.ParseFloat(startTimeStr, 64)
  52. if err != nil || startTime < 0 {
  53. return 0
  54. }
  55. return startTime
  56. }
  57. // ServeMediaProbe reports the codecs a file contains and whether a mainstream
  58. // browser can play it without transcoding.
  59. //
  60. // The player needs this because a container extension says nothing about
  61. // decodability: an .mp4 holding HEVC or 10-bit H.264 looks directly playable
  62. // and then fails with a bare decode error.
  63. func (s *Instance) ServeMediaProbe(w http.ResponseWriter, r *http.Request) {
  64. targetFsh, _, realFilepath, err := s.ValidateSourceFile(w, r)
  65. if err != nil {
  66. utils.SendErrorResponse(w, err.Error())
  67. return
  68. }
  69. //Native check on purpose: ffprobe has to read the file directly, and
  70. //buffering a remote file in full just to read its header is not worth it.
  71. if targetFsh.RequireBuffer || !filesystem.FileExists(realFilepath) {
  72. utils.SendErrorResponse(w, "codec probe not supported for this file system")
  73. return
  74. }
  75. info, err := transcoder.ProbeMediaCodecs(realFilepath)
  76. if err != nil {
  77. s.options.Logger.PrintAndLog("Media Server",
  78. "Codec probe failed for "+filepath.Base(realFilepath), err)
  79. utils.SendErrorResponse(w, "could not probe media codecs")
  80. return
  81. }
  82. js, _ := json.Marshal(info)
  83. w.Header().Set("Content-Type", "application/json")
  84. w.Header().Set("Cache-Control", "private, max-age=3600")
  85. w.Write(js)
  86. }
  87. // ServeHLSPlaylist starts (or joins) an HLS transcode of the requested file and
  88. // returns its playlist once the first segment is ready.
  89. func (s *Instance) ServeHLSPlaylist(w http.ResponseWriter, r *http.Request) {
  90. if s.hlsManager == nil {
  91. utils.SendErrorResponse(w, "HLS output is not available on this host")
  92. return
  93. }
  94. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  95. if err != nil {
  96. utils.SendErrorResponse(w, "User not logged in")
  97. return
  98. }
  99. //ValidateSourceFile authenticates the request and resolves the vpath
  100. sourceFile, ok := s.resolveLocalTranscodeSource(w, r)
  101. if !ok {
  102. return
  103. }
  104. session, err := s.hlsManager.GetOrCreate(userinfo.Username, sourceFile,
  105. transcodeResolutionFromRequest(r), startTimeFromRequest(r))
  106. if err != nil {
  107. s.options.Logger.PrintAndLog("Media Server", "Unable to start HLS session", err)
  108. utils.SendErrorResponse(w, "Unable to start HLS transcode")
  109. return
  110. }
  111. if err := session.WaitForPlaylist(transcoder.HLSPlaylistWaitTimeout); err != nil {
  112. s.options.Logger.PrintAndLog("Media Server", "HLS session produced no playable segment", err)
  113. utils.SendErrorResponse(w, "Transcode did not produce a playable stream")
  114. return
  115. }
  116. //Served from memory rather than with ServeFile because the init segment URI
  117. //has to be rewritten onto the segment endpoint before the client sees it.
  118. playlist, err := s.hlsManager.ReadPlaylist(session)
  119. if err != nil {
  120. s.options.Logger.PrintAndLog("Media Server", "Unable to read HLS playlist", err)
  121. utils.SendErrorResponse(w, "Unable to read the transcode playlist")
  122. return
  123. }
  124. //The playlist grows as the transcode advances, so it must never be cached.
  125. w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
  126. w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
  127. w.Write(playlist)
  128. }
  129. // ServeHLSSegment serves one segment of a running HLS session. Segments are
  130. // readable only by the user whose session produced them.
  131. func (s *Instance) ServeHLSSegment(w http.ResponseWriter, r *http.Request) {
  132. if s.hlsManager == nil {
  133. http.Error(w, "HLS output is not available on this host", http.StatusNotFound)
  134. return
  135. }
  136. userinfo, err := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  137. if err != nil {
  138. http.Error(w, "User not logged in", http.StatusUnauthorized)
  139. return
  140. }
  141. sessionID, err := utils.GetPara(r, "sid")
  142. if err != nil {
  143. http.Error(w, "Missing parameter 'sid'", http.StatusBadRequest)
  144. return
  145. }
  146. segmentName, err := utils.GetPara(r, "name")
  147. if err != nil {
  148. http.Error(w, "Missing parameter 'name'", http.StatusBadRequest)
  149. return
  150. }
  151. session := s.hlsManager.Session(sessionID)
  152. if session == nil {
  153. //Either a stale playlist from a reaped session, or a guessed id
  154. http.Error(w, "No such HLS session", http.StatusNotFound)
  155. return
  156. }
  157. if session.Owner != userinfo.Username {
  158. http.Error(w, "Permission Denied", http.StatusForbidden)
  159. return
  160. }
  161. segmentPath, err := session.SegmentPath(segmentName)
  162. if err != nil {
  163. http.Error(w, "Invalid segment name", http.StatusBadRequest)
  164. return
  165. }
  166. //A segment never changes once the playlist lists it, so it is safe to cache
  167. //for the lifetime of the session. Segments are fragmented MP4 (including the
  168. //init segment), which is what MediaSource can append without demuxing.
  169. w.Header().Set("Content-Type", "video/mp4")
  170. w.Header().Set("Cache-Control", "private, max-age=3600")
  171. http.ServeFile(w, r, segmentPath)
  172. }
  173. // resolveLocalTranscodeSource validates the request and returns an absolute
  174. // path to the source file on local disk. ffmpeg cannot read a remote file
  175. // system directly, so a file living on one is buffered locally first (reusing
  176. // an existing buffer when its hash still matches).
  177. //
  178. // It writes the error response itself and returns ok=false when the file cannot
  179. // be made available.
  180. func (s *Instance) resolveLocalTranscodeSource(w http.ResponseWriter, r *http.Request) (string, bool) {
  181. userinfo, _ := s.options.UserHandler.GetUserInfoFromRequest(w, r)
  182. targetFsh, vpath, realFilepath, err := s.ValidateSourceFile(w, r)
  183. if err != nil {
  184. utils.SendErrorResponse(w, err.Error())
  185. return "", false
  186. }
  187. if filesystem.FileExists(realFilepath) {
  188. //Already on the local file system
  189. absPath, err := filepath.Abs(realFilepath)
  190. if err != nil {
  191. utils.SendErrorResponse(w, err.Error())
  192. return "", false
  193. }
  194. return absPath, true
  195. }
  196. //Remote file system: reuse the local buffer when it is still current
  197. ps, _ := targetFsh.GetUniquePathHash(vpath, userinfo.Username)
  198. buffpool := filepath.Join(s.options.TmpDirectory, "fsbuffpool")
  199. buffFile := filepath.Join(buffpool, ps)
  200. if fs.FileExists(buffFile) {
  201. remoteFileHash, err := s.GetHashFromRemoteFile(targetFsh.FileSystemAbstraction, realFilepath)
  202. if err == nil {
  203. localFileHash, err := os.ReadFile(buffFile + ".hash")
  204. if err == nil && string(localFileHash) == remoteFileHash {
  205. buffFileAbs, _ := filepath.Abs(buffFile)
  206. return buffFileAbs, true
  207. }
  208. }
  209. }
  210. if !s.options.EnableFileBuffering {
  211. utils.SendErrorResponse(w, "unable to transcode remote file with file buffer disabled")
  212. return "", false
  213. }
  214. os.MkdirAll(buffpool, 0775)
  215. s.options.Logger.PrintAndLog("Media Server", "Buffering video from remote file system handler (might take a while)", nil)
  216. if err := s.BufferRemoteFileToTmp(buffFile, targetFsh, realFilepath); err != nil {
  217. utils.SendErrorResponse(w, err.Error())
  218. return "", false
  219. }
  220. buffFileAbs, _ := filepath.Abs(buffFile)
  221. return buffFileAbs, true
  222. }