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] 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()}