Transcode stream refactor (#609)

* Remove forceMkv and forceHEVC
* Add HLS support and refactor
* Add new streaming endpoints
This commit is contained in:
WithoutPants
2020-07-23 11:56:08 +10:00
committed by GitHub
parent 274d84ce93
commit 37be146a9d
40 changed files with 769 additions and 374 deletions

View File

@@ -2,9 +2,7 @@ package api
import (
"context"
"io"
"net/http"
"os"
"strconv"
"strings"
@@ -24,8 +22,15 @@ func (rs sceneRoutes) Routes() chi.Router {
r.Route("/{sceneId}", func(r chi.Router) {
r.Use(SceneCtx)
r.Get("/stream", rs.Stream)
r.Get("/stream.mp4", rs.Stream)
// streaming endpoints
r.Get("/stream", rs.StreamDirect)
r.Get("/stream.mkv", rs.StreamMKV)
r.Get("/stream.webm", rs.StreamWebM)
r.Get("/stream.m3u8", rs.StreamHLS)
r.Get("/stream.ts", rs.StreamTS)
r.Get("/stream.mp4", rs.StreamMp4)
r.Get("/screenshot", rs.Screenshot)
r.Get("/preview", rs.Preview)
r.Get("/webp", rs.Webp)
@@ -42,41 +47,94 @@ func (rs sceneRoutes) Routes() chi.Router {
// region Handlers
func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
container := ""
func getSceneFileContainer(scene *models.Scene) ffmpeg.Container {
var container ffmpeg.Container
if scene.Format.Valid {
container = scene.Format.String
container = ffmpeg.Container(scene.Format.String)
} else { // container isn't in the DB
// shouldn't happen, fallback to ffprobe
tmpVideoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
if err != nil {
logger.Errorf("[transcode] error reading video file: %s", err.Error())
return
return ffmpeg.Container("")
}
container = string(ffmpeg.MatchContainer(tmpVideoFile.Container, scene.Path))
container = ffmpeg.MatchContainer(tmpVideoFile.Container, scene.Path)
}
// detect if not a streamable file and try to transcode it instead
return container
}
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
videoCodec := scene.VideoCodec.String
audioCodec := ffmpeg.MissingUnsupported
if scene.AudioCodec.Valid {
audioCodec = ffmpeg.AudioCodec(scene.AudioCodec.String)
}
hasTranscode, _ := manager.HasTranscode(scene)
if ffmpeg.IsValidCodec(videoCodec) && ffmpeg.IsValidCombo(videoCodec, ffmpeg.Container(container)) && ffmpeg.IsValidAudioForContainer(audioCodec, ffmpeg.Container(container)) || hasTranscode {
manager.RegisterStream(filepath, &w)
http.ServeFile(w, r, filepath)
manager.WaitAndDeregisterStream(filepath, &w, r)
manager.RegisterStream(filepath, &w)
http.ServeFile(w, r, filepath)
manager.WaitAndDeregisterStream(filepath, &w, r)
}
func (rs sceneRoutes) StreamMKV(w http.ResponseWriter, r *http.Request) {
// only allow mkv streaming if the scene container is an mkv already
scene := r.Context().Value(sceneKey).(*models.Scene)
container := getSceneFileContainer(scene)
if container != ffmpeg.Matroska {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("not an mkv file"))
return
}
rs.streamTranscode(w, r, ffmpeg.CodecMKVAudio)
}
func (rs sceneRoutes) StreamWebM(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.CodecVP9)
}
func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.CodecH264)
}
func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
if err != nil {
logger.Errorf("[stream] error reading video file: %s", err.Error())
return
}
logger.Debug("Returning HLS playlist")
// getting the playlist manifest only
w.Header().Set("Content-Type", ffmpeg.MimeHLS)
var str strings.Builder
ffmpeg.WriteHLSPlaylist(*videoFile, r.URL.String(), &str)
requestByteRange := utils.CreateByteRange(r.Header.Get("Range"))
if requestByteRange.RawString != "" {
logger.Debugf("Requested range: %s", requestByteRange.RawString)
}
ret := requestByteRange.Apply([]byte(str.String()))
rangeStr := requestByteRange.ToHeaderValue(int64(str.Len()))
w.Header().Set("Content-Range", rangeStr)
w.Write(ret)
}
func (rs sceneRoutes) StreamTS(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.CodecHLS)
}
func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, videoCodec ffmpeg.Codec) {
logger.Debugf("Streaming as %s", videoCodec.MimeType)
scene := r.Context().Value(sceneKey).(*models.Scene)
// needs to be transcoded
videoFile, err := ffmpeg.NewVideoFile(manager.GetInstance().FFProbePath, scene.Path)
if err != nil {
logger.Errorf("[stream] error reading video file: %s", err.Error())
@@ -87,65 +145,28 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
startTime := r.Form.Get("start")
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
var stream *ffmpeg.Stream
var stream io.ReadCloser
var process *os.Process
mimeType := ffmpeg.MimeWebm
if audioCodec == ffmpeg.MissingUnsupported {
//ffmpeg fails if it trys to transcode a non supported audio codec
stream, process, err = encoder.StreamTranscodeVideo(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
} else {
copyVideo := false // try to be smart if the video to be transcoded is in a Matroska container
// mp4 has always supported audio so it doesn't need to be checked
// while mpeg_ts has seeking issues if we don't reencode the video
if config.GetForceMKV() { // If MKV is forced as supported and video codec is also supported then only transcode audio
if ffmpeg.Container(container) == ffmpeg.Matroska {
switch videoCodec {
case ffmpeg.H264, ffmpeg.Vp9, ffmpeg.Vp8:
copyVideo = true
case ffmpeg.Hevc:
if config.GetForceHEVC() {
copyVideo = true
}
}
}
}
if copyVideo { // copy video stream instead of transcoding it
stream, process, err = encoder.StreamMkvTranscodeAudio(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
mimeType = ffmpeg.MimeMkv
} else {
stream, process, err = encoder.StreamTranscode(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
}
audioCodec := ffmpeg.MissingUnsupported
if scene.AudioCodec.Valid {
audioCodec = ffmpeg.AudioCodec(scene.AudioCodec.String)
}
options := ffmpeg.GetTranscodeStreamOptions(*videoFile, videoCodec, audioCodec)
options.StartTime = startTime
options.MaxTranscodeSize = config.GetMaxStreamingTranscodeSize()
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
stream, err = encoder.GetTranscodeStream(options)
if err != nil {
logger.Errorf("[stream] error transcoding video file: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", mimeType)
logger.Infof("[stream] transcoding video file to %s", mimeType)
// handle if client closes the connection
notify := r.Context().Done()
go func() {
<-notify
logger.Info("[stream] client closed the connection. Killing stream process.")
process.Kill()
}()
_, err = io.Copy(w, stream)
if err != nil {
logger.Errorf("[stream] error serving transcoded video file: %s", err.Error())
}
stream.Serve(w, r)
}
func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {