mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Transcode stream refactor (#609)
* Remove forceMkv and forceHEVC * Add HLS support and refactor * Add new streaming endpoints
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from "react";
|
||||
import ReactJWPlayer from "react-jw-player";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -8,6 +9,7 @@ import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||
interface IScenePlayerProps {
|
||||
className?: string;
|
||||
scene: GQL.SceneDataFragment;
|
||||
sceneStreams: GQL.SceneStreamEndpoint[];
|
||||
timestamp: number;
|
||||
autoplay?: boolean;
|
||||
onReady?: () => void;
|
||||
@@ -25,9 +27,23 @@ export class ScenePlayerImpl extends React.Component<
|
||||
IScenePlayerProps,
|
||||
IScenePlayerState
|
||||
> {
|
||||
private static isDirectStream(src?: string) {
|
||||
if (!src) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startIndex = src.lastIndexOf("?start=");
|
||||
let srcCopy = src;
|
||||
if (startIndex !== -1) {
|
||||
srcCopy = srcCopy.substring(0, startIndex);
|
||||
}
|
||||
|
||||
return srcCopy.endsWith("/stream");
|
||||
}
|
||||
|
||||
// Typings for jwplayer are, unfortunately, very lacking
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private player: any;
|
||||
private playlist: any;
|
||||
private lastTime = 0;
|
||||
|
||||
constructor(props: IScenePlayerProps) {
|
||||
@@ -82,6 +98,23 @@ export class ScenePlayerImpl extends React.Component<
|
||||
if (this.props.timestamp > 0) {
|
||||
this.player.seek(this.props.timestamp);
|
||||
}
|
||||
|
||||
this.player.on("error", (err: any) => {
|
||||
if (err && err.code === 224003) {
|
||||
this.handleError();
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onSeeked() {
|
||||
@@ -107,6 +140,21 @@ export class ScenePlayerImpl extends React.Component<
|
||||
this.player.pause();
|
||||
}
|
||||
|
||||
private handleError() {
|
||||
const currentFile = this.player.getPlaylistItem();
|
||||
if (currentFile) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Source failed: ${currentFile.file}`);
|
||||
}
|
||||
|
||||
if (this.tryNextStream()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Trying next source in playlist");
|
||||
this.player.load(this.playlist);
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRepeat(scene: GQL.SceneDataFragment) {
|
||||
const maxLoopDuration = this.props?.config?.maximumLoopDuration ?? 0;
|
||||
return (
|
||||
@@ -116,52 +164,85 @@ export class ScenePlayerImpl extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
private tryNextStream() {
|
||||
if (this.playlist.sources.length > 1) {
|
||||
this.playlist.sources.shift();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private makePlaylist() {
|
||||
return {
|
||||
tracks: [
|
||||
{
|
||||
file: this.props.scene.paths.vtt,
|
||||
kind: "thumbnails",
|
||||
},
|
||||
{
|
||||
file: this.props.scene.paths.chapters_vtt,
|
||||
kind: "chapters",
|
||||
},
|
||||
],
|
||||
sources: this.props.sceneStreams.map((s) => {
|
||||
return {
|
||||
file: s.url,
|
||||
type: s.mime_type,
|
||||
label: s.label,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private makeJWPlayerConfig(scene: GQL.SceneDataFragment) {
|
||||
if (!scene.paths.stream) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const repeat = this.shouldRepeat(scene);
|
||||
let getDurationHook: (() => GQL.Maybe<number>) | undefined;
|
||||
let seekHook:
|
||||
| ((seekToPosition: number, _videoTag: HTMLVideoElement) => void)
|
||||
| undefined;
|
||||
let getCurrentTimeHook:
|
||||
| ((_videoTag: HTMLVideoElement) => number)
|
||||
| undefined;
|
||||
const getDurationHook = () => {
|
||||
return this.props.scene.file.duration ?? null;
|
||||
};
|
||||
|
||||
if (!this.props.scene.is_streamable) {
|
||||
getDurationHook = () => {
|
||||
return this.props.scene.file.duration ?? null;
|
||||
};
|
||||
const seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => {
|
||||
if (
|
||||
ScenePlayerImpl.isDirectStream(_videoTag.src) ||
|
||||
_videoTag.src.endsWith(".m3u8")
|
||||
) {
|
||||
// direct stream - fall back to default
|
||||
return false;
|
||||
}
|
||||
|
||||
seekHook = (seekToPosition: number, _videoTag: HTMLVideoElement) => {
|
||||
/* eslint-disable no-param-reassign */
|
||||
_videoTag.dataset.start = seekToPosition.toString();
|
||||
_videoTag.src = `${this.props.scene.paths.stream}?start=${seekToPosition}`;
|
||||
/* eslint-enable no-param-reassign */
|
||||
_videoTag.play();
|
||||
};
|
||||
// remove the start parameter
|
||||
let { src } = _videoTag;
|
||||
|
||||
getCurrentTimeHook = (_videoTag: HTMLVideoElement) => {
|
||||
const start = Number.parseInt(_videoTag.dataset?.start ?? "0", 10);
|
||||
return _videoTag.currentTime + start;
|
||||
};
|
||||
}
|
||||
const startIndex = src.lastIndexOf("?start=");
|
||||
if (startIndex !== -1) {
|
||||
src = src.substring(0, startIndex);
|
||||
}
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
_videoTag.dataset.start = seekToPosition.toString();
|
||||
|
||||
_videoTag.src = `${src}?start=${seekToPosition}`;
|
||||
/* eslint-enable no-param-reassign */
|
||||
_videoTag.play();
|
||||
|
||||
// return true to indicate not to fall through to default
|
||||
return true;
|
||||
};
|
||||
|
||||
const getCurrentTimeHook = (_videoTag: HTMLVideoElement) => {
|
||||
const start = Number.parseFloat(_videoTag.dataset?.start ?? "0");
|
||||
return _videoTag.currentTime + start;
|
||||
};
|
||||
|
||||
this.playlist = this.makePlaylist();
|
||||
|
||||
const ret = {
|
||||
file: scene.paths.stream,
|
||||
playlist: this.playlist,
|
||||
image: scene.paths.screenshot,
|
||||
tracks: [
|
||||
{
|
||||
file: scene.paths.vtt,
|
||||
kind: "thumbnails",
|
||||
},
|
||||
{
|
||||
file: scene.paths.chapters_vtt,
|
||||
kind: "chapters",
|
||||
},
|
||||
],
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
floating: {
|
||||
|
||||
Reference in New Issue
Block a user