mirror of https://github.com/stashapp/stash.git
Added streaming quality options (#790)
This commit is contained in:
parent
a31c8ccd33
commit
71c814c116
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue