mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Added streaming quality options (#790)
This commit is contained in:
@@ -145,6 +145,7 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, vi
|
|||||||
// start stream based on query param, if provided
|
// start stream based on query param, if provided
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
startTime := r.Form.Get("start")
|
startTime := r.Form.Get("start")
|
||||||
|
requestedSize := r.Form.Get("resolution")
|
||||||
|
|
||||||
var stream *ffmpeg.Stream
|
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 := ffmpeg.GetTranscodeStreamOptions(*videoFile, videoCodec, audioCodec)
|
||||||
options.StartTime = startTime
|
options.StartTime = startTime
|
||||||
options.MaxTranscodeSize = config.GetMaxStreamingTranscodeSize()
|
options.MaxTranscodeSize = config.GetMaxStreamingTranscodeSize()
|
||||||
|
if requestedSize != "" {
|
||||||
|
options.MaxTranscodeSize = models.StreamingResolutionEnum(requestedSize)
|
||||||
|
}
|
||||||
|
|
||||||
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath)
|
||||||
stream, err = encoder.GetTranscodeStream(options)
|
stream, err = encoder.GetTranscodeStream(options)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ var CodecH264 = Codec{
|
|||||||
format: "mp4",
|
format: "mp4",
|
||||||
MimeType: MimeMp4,
|
MimeType: MimeMp4,
|
||||||
extraArgs: []string{
|
extraArgs: []string{
|
||||||
"-movflags", "frag_keyframe",
|
"-movflags", "frag_keyframe+empty_moov",
|
||||||
"-pix_fmt", "yuv420p",
|
"-pix_fmt", "yuv420p",
|
||||||
"-preset", "veryfast",
|
"-preset", "veryfast",
|
||||||
"-crf", "25",
|
"-crf", "25",
|
||||||
|
|||||||
@@ -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{
|
defaultStreams := []*models.SceneStreamEndpoint{
|
||||||
{
|
{
|
||||||
URL: directStreamURL + ".webm",
|
URL: directStreamURL + ".webm",
|
||||||
MimeType: &mimeWebm,
|
MimeType: &mimeWebm,
|
||||||
Label: &labelWebm,
|
Label: &labelWebm,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
URL: directStreamURL + ".m3u8",
|
|
||||||
MimeType: &mimeHLS,
|
|
||||||
Label: &labelHLS,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, defaultStreams...)
|
ret = append(ret, defaultStreams...)
|
||||||
|
|
||||||
// TODO - at some point, look at streaming at various resolutions
|
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Add selectable streaming quality profiles in the scene player.
|
||||||
* Add scrapers list setting page.
|
* Add scrapers list setting page.
|
||||||
* Add support for individual images and manual creation of galleries.
|
* Add support for individual images and manual creation of galleries.
|
||||||
* Add various fields to galleries.
|
* Add various fields to galleries.
|
||||||
|
|||||||
@@ -32,13 +32,8 @@ export class ScenePlayerImpl extends React.Component<
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startIndex = src.lastIndexOf("?start=");
|
const url = new URL(src);
|
||||||
let srcCopy = src;
|
return url.pathname.endsWith("/stream");
|
||||||
if (startIndex !== -1) {
|
|
||||||
srcCopy = srcCopy.substring(0, startIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return srcCopy.endsWith("/stream");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Typings for jwplayer are, unfortunately, very lacking
|
// Typings for jwplayer are, unfortunately, very lacking
|
||||||
@@ -59,6 +54,9 @@ export class ScenePlayerImpl extends React.Component<
|
|||||||
scrubberPosition: 0,
|
scrubberPosition: 0,
|
||||||
config: this.makeJWPlayerConfig(props.scene),
|
config: this.makeJWPlayerConfig(props.scene),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Default back to Direct Streaming
|
||||||
|
localStorage.removeItem("jwplayer.qualityLabel");
|
||||||
}
|
}
|
||||||
|
|
||||||
public UNSAFE_componentWillReceiveProps(props: IScenePlayerProps) {
|
public UNSAFE_componentWillReceiveProps(props: IScenePlayerProps) {
|
||||||
@@ -98,18 +96,27 @@ export class ScenePlayerImpl extends React.Component<
|
|||||||
|
|
||||||
this.player.on("error", (err: any) => {
|
this.player.on("error", (err: any) => {
|
||||||
if (err && err.code === 224003) {
|
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) => {
|
this.player.on("meta", (metadata: any) => {
|
||||||
if (
|
if (
|
||||||
metadata.metadataType === "media" &&
|
metadata.metadataType === "media" &&
|
||||||
!metadata.width &&
|
!metadata.width &&
|
||||||
!metadata.height
|
!metadata.height
|
||||||
) {
|
) {
|
||||||
// treat this as a decoding error and try the next source
|
// Occurs during preload when videos with supported audio/unsupported video are preloaded.
|
||||||
this.handleError();
|
// 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();
|
this.player.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError() {
|
private handleError(play: boolean) {
|
||||||
const currentFile = this.player.getPlaylistItem();
|
const currentFile = this.player.getPlaylistItem();
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@@ -152,8 +159,13 @@ export class ScenePlayerImpl extends React.Component<
|
|||||||
|
|
||||||
if (this.tryNextStream()) {
|
if (this.tryNextStream()) {
|
||||||
// eslint-disable-next-line no-console
|
// 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);
|
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) => {
|
const seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => {
|
||||||
if (
|
if (!_videoTag.src || _videoTag.src.endsWith(".m3u8")) {
|
||||||
!_videoTag.src ||
|
return false;
|
||||||
ScenePlayerImpl.isDirectStream(_videoTag.src) ||
|
}
|
||||||
_videoTag.src.endsWith(".m3u8")
|
|
||||||
) {
|
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
|
// direct stream - fall back to default
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the start parameter
|
// remove the start parameter
|
||||||
let { src } = _videoTag;
|
const srcUrl = new URL(_videoTag.src);
|
||||||
|
srcUrl.searchParams.delete("start");
|
||||||
const startIndex = src.lastIndexOf("?start=");
|
|
||||||
if (startIndex !== -1) {
|
|
||||||
src = src.substring(0, startIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
_videoTag.dataset.start = seekToPosition.toString();
|
_videoTag.dataset.start = seekToPosition.toString();
|
||||||
|
srcUrl.searchParams.append("start", seekToPosition.toString());
|
||||||
_videoTag.src = `${src}?start=${seekToPosition}`;
|
_videoTag.src = srcUrl.toString();
|
||||||
/* eslint-enable no-param-reassign */
|
/* eslint-enable no-param-reassign */
|
||||||
|
|
||||||
_videoTag.play();
|
_videoTag.play();
|
||||||
|
|
||||||
// return true to indicate not to fall through to default
|
// return true to indicate not to fall through to default
|
||||||
|
|||||||
Reference in New Issue
Block a user