From 71c814c1169f59bf9ef9bb2a7e47b8bf94df8aa8 Mon Sep 17 00:00:00 2001 From: JoeSmithStarkers <60503487+JoeSmithStarkers@users.noreply.github.com> Date: Thu, 22 Oct 2020 15:02:27 +1100 Subject: [PATCH] Added streaming quality options (#790) --- pkg/api/routes_scene.go | 4 + pkg/ffmpeg/stream.go | 2 +- pkg/manager/scene.go | 119 ++++++++++++++++-- .../src/components/Changelog/versions/v040.md | 1 + .../components/ScenePlayer/ScenePlayer.tsx | 64 ++++++---- 5 files changed, 157 insertions(+), 33 deletions(-) diff --git a/pkg/api/routes_scene.go b/pkg/api/routes_scene.go index 8f2cdf9c6..046ac09f9 100644 --- a/pkg/api/routes_scene.go +++ b/pkg/api/routes_scene.go @@ -145,6 +145,7 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi // start stream based on query param, if provided r.ParseForm() startTime := r.Form.Get("start") + requestedSize := r.Form.Get("resolution") var stream *ffmpeg.Stream @@ -156,6 +157,9 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi options := ffmpeg.GetTranscodeStreamOptions(*videoFile, videoCodec, audioCodec) options.StartTime = startTime options.MaxTranscodeSize = config.GetMaxStreamingTranscodeSize() + if requestedSize != "" { + options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize) + } encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath) stream, err = encoder.GetTranscodeStream(options) diff --git a/pkg/ffmpeg/stream.go b/pkg/ffmpeg/stream.go index 6af646500..420667680 100644 --- a/pkg/ffmpeg/stream.go +++ b/pkg/ffmpeg/stream.go @@ -67,7 +67,7 @@ var CodecH264 = Codec{ format: "mp4", MimeType: MimeMp4, extraArgs: []string{ - "-movflags", "frag_keyframe", + "-movflags", "frag_keyframe+empty_moov", "-pix_fmt", "yuv420p", "-preset", "veryfast", "-crf", "25", diff --git a/pkg/manager/scene.go b/pkg/manager/scene.go index 5cb99c9c4..ad8551112 100644 --- a/pkg/manager/scene.go +++ b/pkg/manager/scene.go @@ -228,23 +228,128 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL string) ([]*models }) } + hls := models.SceneStreamEndpoint{ + URL: directStreamURL + ".m3u8", + MimeType: &mimeHLS, + Label: &labelHLS, + } + ret = append(ret, &hls) + + // WEBM quality transcoding options + // Note: These have the wrong mime type intentionally to allow jwplayer to selection between mp4/webm + webmLabelFourK := "WEBM 4K (2160p)" // "FOUR_K" + webmLabelFullHD := "WEBM Full HD (1080p)" // "FULL_HD" + webmLabelStardardHD := "WEBM HD (720p)" // "STANDARD_HD" + webmLabelStandard := "WEBM Standard (480p)" // "STANDARD" + webmLabelLow := "WEBM Low (240p)" // "LOW" + + if !scene.Height.Valid || scene.Height.Int64 >= 2160 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".webm?resolution=FOUR_K", + MimeType: &mimeMp4, + Label: &webmLabelFourK, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 1080 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".webm?resolution=FULL_HD", + MimeType: &mimeMp4, + Label: &webmLabelFullHD, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 720 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".webm?resolution=STANDARD_HD", + MimeType: &mimeMp4, + Label: &webmLabelStardardHD, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 480 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".webm?resolution=STANDARD", + MimeType: &mimeMp4, + Label: &webmLabelStandard, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 240 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".webm?resolution=LOW", + MimeType: &mimeMp4, + Label: &webmLabelLow, + } + ret = append(ret, &new) + } + + // Setup up lower quality transcoding options (MP4) + mp4LabelFourK := "MP4 4K (2160p)" // "FOUR_K" + mp4LabelFullHD := "MP4 Full HD (1080p)" // "FULL_HD" + mp4LabelStardardHD := "MP4 HD (720p)" // "STANDARD_HD" + mp4LabelStandard := "MP4 Standard (480p)" // "STANDARD" + mp4LabelLow := "MP4 Low (240p)" // "LOW" + + if !scene.Height.Valid || scene.Height.Int64 >= 2160 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".mp4?resolution=FOUR_K", + MimeType: &mimeMp4, + Label: &mp4LabelFourK, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 1080 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".mp4?resolution=FULL_HD", + MimeType: &mimeMp4, + Label: &mp4LabelFullHD, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 720 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".mp4?resolution=STANDARD_HD", + MimeType: &mimeMp4, + Label: &mp4LabelStardardHD, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 480 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".mp4?resolution=STANDARD", + MimeType: &mimeMp4, + Label: &mp4LabelStandard, + } + ret = append(ret, &new) + } + + if !scene.Height.Valid || scene.Height.Int64 >= 240 { + new := models.SceneStreamEndpoint{ + URL: directStreamURL + ".mp4?resolution=LOW", + MimeType: &mimeMp4, + Label: &mp4LabelLow, + } + ret = append(ret, &new) + } + defaultStreams := []*models.SceneStreamEndpoint{ { URL: directStreamURL + ".webm", MimeType: &mimeWebm, Label: &labelWebm, }, - { - URL: directStreamURL + ".m3u8", - MimeType: &mimeHLS, - Label: &labelHLS, - }, } ret = append(ret, defaultStreams...) - // TODO - at some point, look at streaming at various resolutions - return ret, nil } diff --git a/ui/v2.5/src/components/Changelog/versions/v040.md b/ui/v2.5/src/components/Changelog/versions/v040.md index cb17a4600..d4f8cd087 100644 --- a/ui/v2.5/src/components/Changelog/versions/v040.md +++ b/ui/v2.5/src/components/Changelog/versions/v040.md @@ -1,4 +1,5 @@ ### ✨ New Features +* Add selectable streaming quality profiles in the scene player. * Add scrapers list setting page. * Add support for individual images and manual creation of galleries. * Add various fields to galleries. diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index b02455557..038358f5d 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -32,13 +32,8 @@ export class ScenePlayerImpl extends React.Component< return false; } - const startIndex = src.lastIndexOf("?start="); - let srcCopy = src; - if (startIndex !== -1) { - srcCopy = srcCopy.substring(0, startIndex); - } - - return srcCopy.endsWith("/stream"); + const url = new URL(src); + return url.pathname.endsWith("/stream"); } // Typings for jwplayer are, unfortunately, very lacking @@ -59,6 +54,9 @@ export class ScenePlayerImpl extends React.Component< scrubberPosition: 0, config: this.makeJWPlayerConfig(props.scene), }; + + // Default back to Direct Streaming + localStorage.removeItem("jwplayer.qualityLabel"); } public UNSAFE_componentWillReceiveProps(props: IScenePlayerProps) { @@ -98,18 +96,27 @@ export class ScenePlayerImpl extends React.Component< this.player.on("error", (err: any) => { if (err && err.code === 224003) { - this.handleError(); + // When jwplayer has been requested to play but the browser doesn't support the video format. + this.handleError(true); } }); + // this.player.on("meta", (metadata: any) => { if ( metadata.metadataType === "media" && !metadata.width && !metadata.height ) { - // treat this as a decoding error and try the next source - this.handleError(); + // Occurs during preload when videos with supported audio/unsupported video are preloaded. + // Treat this as a decoding error and try the next source without playing. + // However on Safari we get an media event when m3u8 is loaded which needs to be ignored. + const currentFile = this.player.getPlaylistItem().file; + if (currentFile != null && !currentFile.includes("m3u8")) { + const state = this.player.getState(); + const play = state === "buffering" || state === "playing"; + this.handleError(play); + } } }); @@ -143,7 +150,7 @@ export class ScenePlayerImpl extends React.Component< this.player.pause(); } - private handleError() { + private handleError(play: boolean) { const currentFile = this.player.getPlaylistItem(); if (currentFile) { // eslint-disable-next-line no-console @@ -152,8 +159,13 @@ export class ScenePlayerImpl extends React.Component< if (this.tryNextStream()) { // eslint-disable-next-line no-console - console.log("Trying next source in playlist"); + console.log( + `Trying next source in playlist: ${this.playlist.sources[0].file}` + ); this.player.load(this.playlist); + if (play) { + this.player.play(); + } } } @@ -211,28 +223,30 @@ export class ScenePlayerImpl extends React.Component< }; const seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => { - if ( - !_videoTag.src || - ScenePlayerImpl.isDirectStream(_videoTag.src) || - _videoTag.src.endsWith(".m3u8") - ) { + if (!_videoTag.src || _videoTag.src.endsWith(".m3u8")) { + return false; + } + + if (ScenePlayerImpl.isDirectStream(_videoTag.src)) { + if (_videoTag.dataset.start) { + /* eslint-disable-next-line no-param-reassign */ + _videoTag.dataset.start = "0"; + } + // direct stream - fall back to default return false; } // remove the start parameter - let { src } = _videoTag; - - const startIndex = src.lastIndexOf("?start="); - if (startIndex !== -1) { - src = src.substring(0, startIndex); - } + const srcUrl = new URL(_videoTag.src); + srcUrl.searchParams.delete("start"); /* eslint-disable no-param-reassign */ _videoTag.dataset.start = seekToPosition.toString(); - - _videoTag.src = `${src}?start=${seekToPosition}`; + srcUrl.searchParams.append("start", seekToPosition.toString()); + _videoTag.src = srcUrl.toString(); /* eslint-enable no-param-reassign */ + _videoTag.play(); // return true to indicate not to fall through to default