Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transcode stream refactor #609

Merged
merged 24 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ebb5e82
Detect supported codecs from browser
WithoutPants Jun 10, 2020
46135bb
Refactor streaming code
WithoutPants Jun 11, 2020
81e8326
Remove forceMkv and forceHEVC
WithoutPants Jun 11, 2020
04ee4d5
Prefer VP9 not VP8
WithoutPants Jun 11, 2020
1f5fbb3
Lint and format
WithoutPants Jun 11, 2020
a7fc9c1
Merge remote-tracking branch 'upstream/develop' into stream-detection
WithoutPants Jun 13, 2020
6c4c2d3
Replace channel map with ac2
WithoutPants Jun 13, 2020
d22cf74
Add HLS support and refactor
WithoutPants Jun 15, 2020
40dd402
Lint and format
WithoutPants Jun 15, 2020
e51e6e6
Adjust codec arguments
WithoutPants Jun 15, 2020
b6e615a
Merge remote-tracking branch 'upstream/develop' into stream-detection
WithoutPants Jun 26, 2020
b7cb057
Replace modernizr with more recent build
WithoutPants Jun 26, 2020
3ede120
Restore forceHEVC
WithoutPants Jun 26, 2020
07f7559
Merge remote-tracking branch 'upstream/develop' into stream-detection
WithoutPants Jul 8, 2020
326982c
Add new streaming endpoints
WithoutPants Jul 9, 2020
1d5cf7c
Major refactor
WithoutPants Jul 9, 2020
240d904
Lint and format
WithoutPants Jul 9, 2020
3b816fe
Fix failover and unsupported video codec
WithoutPants Jul 11, 2020
021d5c3
Lint and format
WithoutPants Jul 11, 2020
a84d07b
Fix incorrect webm settings
WithoutPants Jul 12, 2020
77bfac4
Fix sprites not showing
WithoutPants Jul 12, 2020
866b71b
Remove stream.mp4 from stream list
WithoutPants Jul 12, 2020
7c9b6a9
Merge remote-tracking branch 'upstream/develop' into stream-detection
WithoutPants Jul 22, 2020
fbcc49d
Add changelog entry
WithoutPants Jul 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions graphql/documents/data/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ fragment ConfigGeneralData on ConfigGeneralResult {
cachePath
maxTranscodeSize
maxStreamingTranscodeSize
forceMkv
forceHevc
username
password
maxSessionAge
Expand Down
2 changes: 0 additions & 2 deletions graphql/documents/data/scene.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ fragment SceneData on Scene {
...SceneMarkerData
}

is_streamable

gallery {
...GalleryData
}
Expand Down
4 changes: 4 additions & 0 deletions graphql/documents/queries/scene.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ query ParseSceneFilenames($filter: FindFilterType!, $config: SceneParserInput!)
tag_ids
}
}
}

query IsSceneStreamable($id: ID!, $supportedVideoCodecs: [String!]!) {
isSceneStreamable(id: $id, supportedVideoCodecs: $supportedVideoCodecs)
}
2 changes: 2 additions & 0 deletions graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type Query {

findScenesByPathRegex(filter: FindFilterType): FindScenesResultType!

isSceneStreamable(id: ID, supportedVideoCodecs: [String!]!): Boolean!

parseSceneFilenames(filter: FindFilterType, config: SceneParserInput!): SceneParserResultType!

"""A function which queries SceneMarker objects"""
Expand Down
8 changes: 0 additions & 8 deletions graphql/schema/types/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ input ConfigGeneralInput {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""Force MKV as supported format"""
forceMkv: Boolean!
"""Force HEVC as a supported codec"""
forceHevc: Boolean!
"""Username"""
username: String
"""Password"""
Expand Down Expand Up @@ -57,10 +53,6 @@ type ConfigGeneralResult {
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""Force MKV as supported format"""
forceMkv: Boolean!
"""Force HEVC as a supported codec"""
forceHevc: Boolean!
"""Username"""
username: String!
"""Password"""
Expand Down
1 change: 0 additions & 1 deletion graphql/schema/types/scene.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ type Scene {

file: SceneFileType! # Resolver
paths: ScenePathsType! # Resolver
is_streamable: Boolean! # Resolver

scene_markers: [SceneMarker!]!
gallery: Gallery
Expand Down
7 changes: 0 additions & 7 deletions pkg/api/resolver_model_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"

"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
Expand Down Expand Up @@ -81,12 +80,6 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.S
}, nil
}

func (r *sceneResolver) IsStreamable(ctx context.Context, obj *models.Scene) (bool, error) {
// ignore error
ret, _ := manager.IsStreamable(obj)
return ret, nil
}

func (r *sceneResolver) SceneMarkers(ctx context.Context, obj *models.Scene) ([]*models.SceneMarker, error) {
qb := models.NewSceneMarkerQueryBuilder()
return qb.FindBySceneID(obj.ID, nil)
Expand Down
2 changes: 0 additions & 2 deletions pkg/api/resolver_mutation_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
if input.MaxStreamingTranscodeSize != nil {
config.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String())
}
config.Set(config.ForceMKV, input.ForceMkv)
config.Set(config.ForceHEVC, input.ForceHevc)

if input.Username != nil {
config.Set(config.Username, input.Username)
Expand Down
2 changes: 0 additions & 2 deletions pkg/api/resolver_query_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
CachePath: config.GetCachePath(),
MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
ForceMkv: config.GetForceMKV(),
ForceHevc: config.GetForceHEVC(),
Username: config.GetUsername(),
Password: config.GetPasswordHash(),
MaxSessionAge: config.GetMaxSessionAge(),
Expand Down
22 changes: 22 additions & 0 deletions pkg/api/resolver_query_scene.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package api

import (
"context"
"strconv"

"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
)

func (r *queryResolver) IsSceneStreamable(ctx context.Context, id *string, supportedVideoCodecs []string) (bool, error) {
// find the scene
qb := models.NewSceneQueryBuilder()
idInt, _ := strconv.Atoi(*id)
scene, err := qb.Find(idInt)

if err != nil {
return false, err
}

return manager.IsStreamable(scene, supportedVideoCodecs)
}
56 changes: 31 additions & 25 deletions pkg/api/routes_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package api
import (
"context"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -43,12 +43,15 @@ 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)
r.ParseForm()

supportedStr := r.Form.Get("supported")
supportedVideoCodecs := strings.Split(supportedStr, ",")

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)
Expand All @@ -57,18 +60,19 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
return
}

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
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 {
if ffmpeg.IsStreamable(supportedVideoCodecs, videoCodec, audioCodec, container) || hasTranscode {
filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum)
manager.RegisterStream(filepath, &w)
http.ServeFile(w, r, filepath)
manager.WaitAndDeregisterStream(filepath, &w, r)
Expand All @@ -84,43 +88,36 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
}

// start stream based on query param, if provided
r.ParseForm()
startTime := r.Form.Get("start")

encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)

var stream io.ReadCloser
var process *os.Process
mimeType := ffmpeg.MimeWebm
var stream *ffmpeg.Stream

transcodeCodec := ffmpeg.GetTranscodeCodec(supportedVideoCodecs)
mimeType := transcodeCodec.MimeType

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())
stream, err = encoder.StreamTranscodeVideo(*videoFile, transcodeCodec, 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.IsValidCodec(ffmpeg.Mkv, supportedVideoCodecs) { // If MKV is 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:
if ffmpeg.IsValidCodec(videoCodec, supportedVideoCodecs) {
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())
stream, err = encoder.StreamMkvTranscodeAudio(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
mimeType = ffmpeg.MimeMkv

} else {
stream, process, err = encoder.StreamTranscode(*videoFile, startTime, config.GetMaxStreamingTranscodeSize())
stream, err = encoder.StreamTranscode(*videoFile, transcodeCodec, startTime, config.GetMaxStreamingTranscodeSize())
}
}

Expand All @@ -139,10 +136,19 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) {
go func() {
<-notify
logger.Info("[stream] client closed the connection. Killing stream process.")
process.Kill()
stream.Process.Kill()
}()

// stderr must be consumed or the process deadlocks
go func() {
stderrData, _ := ioutil.ReadAll(stream.Stderr)
stderrString := string(stderrData)
if len(stderrString) > 0 {
logger.Debugf("[stream] ffmpeg stderr: %s", stderrString)
}
}()

_, err = io.Copy(w, stream)
_, err = io.Copy(w, stream.Stdout)
if err != nil {
logger.Errorf("[stream] error serving transcoded video file: %s", err.Error())
}
Expand Down
19 changes: 0 additions & 19 deletions pkg/ffmpeg/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package ffmpeg

import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
Expand Down Expand Up @@ -133,21 +132,3 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {

return stdoutString, nil
}

func (e *Encoder) stream(probeResult VideoFile, args []string) (io.ReadCloser, *os.Process, error) {
cmd := exec.Command(e.Path, args...)

stdout, err := cmd.StdoutPipe()
if nil != err {
logger.Error("FFMPEG stdout not available: " + err.Error())
}

if err = cmd.Start(); err != nil {
return nil, nil, err
}

registerRunningEncoder(probeResult.Path, cmd.Process)
go waitAndDeregister(probeResult.Path, cmd)

return stdout, cmd.Process, nil
}
76 changes: 0 additions & 76 deletions pkg/ffmpeg/encoder_transcode.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package ffmpeg

import (
"io"
"os"
"strconv"

"github.com/stashapp/stash/pkg/models"
Expand Down Expand Up @@ -111,77 +109,3 @@ func (e *Encoder) CopyVideo(probeResult VideoFile, options TranscodeOptions) {
}
_, _ = e.run(probeResult, args)
}

func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) {
scale := calculateTranscodeScale(probeResult, maxTranscodeSize)
args := []string{}

if startTime != "" {
args = append(args, "-ss", startTime)
}

args = append(args,
"-i", probeResult.Path,
"-c:v", "libvpx-vp9",
"-vf", "scale="+scale,
"-deadline", "realtime",
"-cpu-used", "5",
"-row-mt", "1",
"-crf", "30",
"-b:v", "0",
"-f", "webm",
"pipe:",
)

return e.stream(probeResult, args)
}

//transcode the video, remove the audio
//in some videos where the audio codec is not supported by ffmpeg
//ffmpeg fails if you try to transcode the audio
func (e *Encoder) StreamTranscodeVideo(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) {
scale := calculateTranscodeScale(probeResult, maxTranscodeSize)
args := []string{}

if startTime != "" {
args = append(args, "-ss", startTime)
}

args = append(args,
"-i", probeResult.Path,
"-an",
"-c:v", "libvpx-vp9",
"-vf", "scale="+scale,
"-deadline", "realtime",
"-cpu-used", "5",
"-row-mt", "1",
"-crf", "30",
"-b:v", "0",
"-f", "webm",
"pipe:",
)

return e.stream(probeResult, args)
}

//it is very common in MKVs to have just the audio codec unsupported
//copy the video stream, transcode the audio and serve as Matroska
func (e *Encoder) StreamMkvTranscodeAudio(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) {
args := []string{}

if startTime != "" {
args = append(args, "-ss", startTime)
}

args = append(args,
"-i", probeResult.Path,
"-c:v", "copy",
"-c:a", "libopus",
"-b:a", "96k",
"-vbr", "on",
"-f", "matroska",
"pipe:",
)

return e.stream(probeResult, args)
}
Loading