From 8ed3c5f71d847e870ea5a5e915f98fccc416cba7 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 29 Jul 2019 13:42:00 +1000 Subject: [PATCH 1/7] Add seeking for live transcodes via video.js --- pkg/api/routes_scene.go | 21 +- pkg/ffmpeg/encoder_transcode.go | 13 +- ui/v2/package.json | 8 +- .../scenes/ScenePlayer/ScenePlayer.tsx | 110 +++++++++- ui/v2/src/index.scss | 5 + ui/v2/yarn.lock | 201 +++++++++++++++++- 6 files changed, 336 insertions(+), 22 deletions(-) diff --git a/pkg/api/routes_scene.go b/pkg/api/routes_scene.go index 965a6cb1a..ebc36b7fc 100644 --- a/pkg/api/routes_scene.go +++ b/pkg/api/routes_scene.go @@ -1,17 +1,18 @@ package api import ( - "io" "context" + "io" + "net/http" + "strconv" + "strings" + "github.com/go-chi/chi" + "github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" - "github.com/stashapp/stash/pkg/ffmpeg" - "net/http" - "strconv" - "strings" ) type sceneRoutes struct{} @@ -41,7 +42,7 @@ func (rs sceneRoutes) Routes() chi.Router { func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) { scene := r.Context().Value(sceneKey).(*models.Scene) - + // detect if not a streamable file and try to transcode it instead filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, scene.Checksum) @@ -58,10 +59,14 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) { logger.Errorf("[stream] error reading video file: %s", err.Error()) return } - + + // start stream based on query param, if provided + r.ParseForm() + startTime := r.Form.Get("start") + encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath) - stream, process, err := encoder.StreamTranscode(*videoFile) + stream, process, err := encoder.StreamTranscode(*videoFile, startTime) if err != nil { logger.Errorf("[stream] error transcoding video file: %s", err.Error()) return diff --git a/pkg/ffmpeg/encoder_transcode.go b/pkg/ffmpeg/encoder_transcode.go index d8942b36b..32f8d1cca 100644 --- a/pkg/ffmpeg/encoder_transcode.go +++ b/pkg/ffmpeg/encoder_transcode.go @@ -26,8 +26,14 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) { _, _ = e.run(probeResult, args) } -func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Process, error) { - args := []string{ +func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) { + args := []string{} + + if startTime != "" { + args = append(args, "-ss", startTime) + } + + args = append(args, "-i", probeResult.Path, "-c:v", "libvpx-vp9", "-vf", "scale=iw:-2", @@ -37,6 +43,7 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile) (io.ReadCloser, *os.Pro "-b:v", "0", "-f", "webm", "pipe:", - } + ) + return e.stream(probeResult, args) } diff --git a/ui/v2/package.json b/ui/v2/package.json index b2309d2e1..4e5c65601 100644 --- a/ui/v2/package.json +++ b/ui/v2/package.json @@ -12,6 +12,7 @@ "@types/react": "16.8.18", "@types/react-dom": "16.8.4", "@types/react-router-dom": "4.3.3", + "@types/video.js": "^7.2.11", "apollo-boost": "0.4.0", "axios": "0.18.0", "bulma": "0.7.5", @@ -30,7 +31,8 @@ "react-photo-gallery": "7.0.2", "react-router-dom": "5.0.0", "react-scripts": "3.0.1", - "react-use": "9.1.2" + "react-use": "9.1.2", + "video.js": "^7.6.0" }, "scripts": { "start": "react-scripts start", @@ -53,12 +55,12 @@ "devDependencies": { "graphql-code-generator": "0.18.2", "graphql-codegen-add": "0.18.2", + "graphql-codegen-time": "0.18.2", "graphql-codegen-typescript-client": "0.18.2", "graphql-codegen-typescript-common": "0.18.2", "graphql-codegen-typescript-react-apollo": "0.18.2", - "graphql-codegen-time": "0.18.2", "tslint": "5.16.0", "tslint-react": "4.0.0", "typescript": "3.4.5" } -} \ No newline at end of file +} diff --git a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx index e1ce9a4b6..f05a3537a 100644 --- a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx @@ -4,15 +4,95 @@ import ReactJWPlayer from "react-jw-player"; import * as GQL from "../../../core/generated-graphql"; import { SceneHelpers } from "../helpers"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; +import videojs from "video.js"; +import "video.js/dist/video-js.css"; interface IScenePlayerProps { scene: GQL.SceneDataFragment; timestamp: number; + onReady?: any; + onSeeked?: any; + onTime?: any; } interface IScenePlayerState { scrubberPosition: number; } +export class VideoJSPlayer extends React.Component { + private player: any; + private videoNode: any; + + constructor(props: IScenePlayerProps) { + super(props); + } + + componentDidMount() { + this.player = videojs(this.videoNode); + + this.player.src(this.props.scene.paths.stream); + + // hack duration + this.player.duration = () => { return this.props.scene.file.duration; }; + this.player.start = 0; + this.player.oldCurrentTime = this.player.currentTime; + this.player.currentTime = (time: any) => { + if( time == undefined ) + { + return this.player.oldCurrentTime() + this.player.start; + } + this.player.start = time; + this.player.oldCurrentTime(0); + this.player.src(this.props.scene.paths.stream + "?start=" + time); + this.player.play(); + + return this; + }; + + this.player.ready(() => { + // dirty hack - make this player look like JWPlayer + this.player.seek = this.player.currentTime; + this.player.getPosition = this.player.currentTime; + + // hook it into the window function + (window as any).jwplayer = () => { + return this.player; + } + + this.player.on("timeupdate", () => { + this.props.onTime(); + }); + + this.player.on("seeked", () => { + this.props.onSeeked(); + }); + + this.props.onReady(); + }); + } + + componentWillUnmount() { + if (this.player) { + this.player.dispose(); + } + } + + render() { + return ( +
+
+ +
+
+ ); + } +} + @HotkeysTarget export class ScenePlayer extends React.Component { private player: any; @@ -36,12 +116,11 @@ export class ScenePlayer extends React.Component -
- + ); + } else { + return ( + + + ) + } + } + + public render() { + return ( + <> +
+ {this.renderPlayer()} Date: Thu, 1 Aug 2019 11:27:53 +1000 Subject: [PATCH 2/7] Fix viewing jwplayer after non-jwplayer video --- .../scenes/SceneDetails/SceneMarkersPanel.tsx | 2 +- .../scenes/ScenePlayer/ScenePlayer.tsx | 18 ++++++++---------- ui/v2/src/components/scenes/helpers.tsx | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx index 63f4e71b4..bf74b6281 100644 --- a/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2/src/components/scenes/SceneDetails/SceneMarkersPanel.tsx @@ -40,7 +40,7 @@ export const SceneMarkersPanel: FunctionComponent = (pr const sceneMarkerUpdate = StashService.useSceneMarkerUpdate(); const sceneMarkerDestroy = StashService.useSceneMarkerDestroy(); - const jwplayer = SceneHelpers.getJWPlayer(); + const jwplayer = SceneHelpers.getPlayer(); function onOpenEditor(marker: GQL.SceneMarkerDataFragment | null = null) { setIsEditorOpen(true); diff --git a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx index f05a3537a..1b33408d5 100644 --- a/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2/src/components/scenes/ScenePlayer/ScenePlayer.tsx @@ -29,6 +29,12 @@ export class VideoJSPlayer extends React.Component { componentDidMount() { this.player = videojs(this.videoNode); + // dirty hack - make this player look like JWPlayer + this.player.seek = this.player.currentTime; + this.player.getPosition = this.player.currentTime; + + SceneHelpers.registerJSPlayer(this.player); + this.player.src(this.props.scene.paths.stream); // hack duration @@ -49,15 +55,6 @@ export class VideoJSPlayer extends React.Component { }; this.player.ready(() => { - // dirty hack - make this player look like JWPlayer - this.player.seek = this.player.currentTime; - this.player.getPosition = this.player.currentTime; - - // hook it into the window function - (window as any).jwplayer = () => { - return this.player; - } - this.player.on("timeupdate", () => { this.props.onTime(); }); @@ -73,6 +70,7 @@ export class VideoJSPlayer extends React.Component { componentWillUnmount() { if (this.player) { this.player.dispose(); + SceneHelpers.deregisterJSPlayer(); } } @@ -225,7 +223,7 @@ export class ScenePlayer extends React.Component 0) { this.player.seek(this.props.timestamp); } diff --git a/ui/v2/src/components/scenes/helpers.tsx b/ui/v2/src/components/scenes/helpers.tsx index 6749be137..3d7b0e3af 100644 --- a/ui/v2/src/components/scenes/helpers.tsx +++ b/ui/v2/src/components/scenes/helpers.tsx @@ -3,9 +3,12 @@ import { } from "@blueprintjs/core"; import React, { } from "react"; import { Link } from "react-router-dom"; +import videojs from "video.js"; import * as GQL from "../../core/generated-graphql"; export class SceneHelpers { + private static videoJSPlayer: videojs.Player | null; + public static maybeRenderStudio( scene: GQL.SceneDataFragment | GQL.SlimSceneDataFragment, height: number, @@ -33,8 +36,21 @@ export class SceneHelpers { ); } + public static registerJSPlayer(player : videojs.Player) { + this.videoJSPlayer = player; + } + + public static deregisterJSPlayer() { + this.videoJSPlayer = null; + } + public static getJWPlayerId(): string { return "main-jwplayer"; } - public static getJWPlayer(): any { + public static getPlayer(): any { + // return videoJSPlayer if it is set, otherwise use jwplayer() + if (this.videoJSPlayer) { + return this.videoJSPlayer; + } + return (window as any).jwplayer("main-jwplayer"); } } From aeef01a64c8482ff50372828d2e20b1c31009e5d Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 1 Aug 2019 11:36:29 +1000 Subject: [PATCH 3/7] Add row-based multithreading for live transcodes --- pkg/ffmpeg/encoder_transcode.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/ffmpeg/encoder_transcode.go b/pkg/ffmpeg/encoder_transcode.go index 32f8d1cca..5f336d439 100644 --- a/pkg/ffmpeg/encoder_transcode.go +++ b/pkg/ffmpeg/encoder_transcode.go @@ -39,6 +39,7 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.R "-vf", "scale=iw:-2", "-deadline", "realtime", "-cpu-used", "5", + "-row-mt", "1", "-crf", "30", "-b:v", "0", "-f", "webm", From d082580ee04dee2fbac71a8186e6fe7a78e4d275 Mon Sep 17 00:00:00 2001 From: ExceptionalError <43562640+ExceptionalError@users.noreply.github.com> Date: Wed, 9 Oct 2019 06:15:00 +0200 Subject: [PATCH 4/7] modified args for screenshot --- pkg/ffmpeg/encoder_screenshot.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/ffmpeg/encoder_screenshot.go b/pkg/ffmpeg/encoder_screenshot.go index 47853249d..cb018e33b 100644 --- a/pkg/ffmpeg/encoder_screenshot.go +++ b/pkg/ffmpeg/encoder_screenshot.go @@ -18,14 +18,14 @@ func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) { options.Quality = 1 } args := []string{ - "-v", options.Verbosity, - "-ss", fmt.Sprintf("%v", options.Time), + "-v " + options.Verbosity, + fmt.Sprintf("-ss %v", options.Time), "-y", - "-i", `"` + probeResult.Path + `"`, - "-vframes", "1", - "-q:v", fmt.Sprintf("%v", options.Quality), - "-vf", fmt.Sprintf("scale=%v:-1", options.Width), - "-f", "image2", + "-i \"" + probeResult.Path + "\"", + "-vframes 1", + fmt.Sprintf("-q:v %v", options.Quality), + fmt.Sprintf("-vf scale=%v:-1", options.Width), + "-f image2", options.OutputPath, } _, _ = e.run(probeResult, args) From 10af75a6701b50fabc0ce1b273da2d43df8b10d4 Mon Sep 17 00:00:00 2001 From: ExceptionalError <43562640+ExceptionalError@users.noreply.github.com> Date: Wed, 9 Oct 2019 06:16:17 +0200 Subject: [PATCH 5/7] Added output of error message --- pkg/ffmpeg/encoder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ffmpeg/encoder.go b/pkg/ffmpeg/encoder.go index caf6413b6..2cf03d483 100644 --- a/pkg/ffmpeg/encoder.go +++ b/pkg/ffmpeg/encoder.go @@ -57,7 +57,7 @@ func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) { stdoutString := string(stdoutData) if err := cmd.Wait(); err != nil { - logger.Errorf("ffmpeg error when running command <%s>", strings.Join(cmd.Args, " ")) + logger.Errorf("ffmpeg error when running command <%s>: %s", strings.Join(cmd.Args, " "), stdoutString) return stdoutString, err } From 4eb843d83ea5d087c4cb9fe532b762288e20a070 Mon Sep 17 00:00:00 2001 From: ExceptionalError <43562640+ExceptionalError@users.noreply.github.com> Date: Sat, 12 Oct 2019 16:04:00 +0200 Subject: [PATCH 6/7] revert changes #117 --- pkg/ffmpeg/encoder_screenshot.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/ffmpeg/encoder_screenshot.go b/pkg/ffmpeg/encoder_screenshot.go index cb018e33b..14ced4d18 100644 --- a/pkg/ffmpeg/encoder_screenshot.go +++ b/pkg/ffmpeg/encoder_screenshot.go @@ -18,14 +18,14 @@ func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) { options.Quality = 1 } args := []string{ - "-v " + options.Verbosity, - fmt.Sprintf("-ss %v", options.Time), + "-v", options.Verbosity, + "-ss", fmt.Sprintf("%v", options.Time), "-y", - "-i \"" + probeResult.Path + "\"", - "-vframes 1", - fmt.Sprintf("-q:v %v", options.Quality), - fmt.Sprintf("-vf scale=%v:-1", options.Width), - "-f image2", + "-i", probeResult.Path, // TODO: Wrap in quotes? + "-vframes", "1", + "-q:v", fmt.Sprintf("%v", options.Quality), + "-vf", fmt.Sprintf("scale=%v:-1", options.Width), + "-f", "image2", options.OutputPath, } _, _ = e.run(probeResult, args) From 87f81f79c11c51dcea940ea08606f8e44ff3eaed Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 14 Oct 2019 10:58:46 +1100 Subject: [PATCH 7/7] Make IsStreamable return using codec not MIME type --- pkg/manager/utils.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/manager/utils.go b/pkg/manager/utils.go index 9967aa0d1..af767a291 100644 --- a/pkg/manager/utils.go +++ b/pkg/manager/utils.go @@ -2,6 +2,8 @@ package manager import ( "fmt" + + "github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" ) @@ -10,15 +12,11 @@ func IsStreamable(scene *models.Scene) (bool, error) { if scene == nil { return false, fmt.Errorf("nil scene") } - fileType, err := utils.FileType(scene.Path) - if err != nil { - return false, err - } - switch fileType.MIME.Value { - case "video/quicktime", "video/mp4", "video/webm", "video/x-m4v": + videoCodec := scene.VideoCodec.String + if ffmpeg.IsValidCodec(videoCodec) { return true, nil - default: + } else { hasTranscode, _ := HasTranscode(scene) return hasTranscode, nil }