diff --git a/graphql/schema/types/scene.graphql b/graphql/schema/types/scene.graphql index 4d99e0a21f..9d445cabfb 100644 --- a/graphql/schema/types/scene.graphql +++ b/graphql/schema/types/scene.graphql @@ -270,6 +270,8 @@ type SceneStreamEndpoint { url: String! mime_type: String label: String + stream_type: String + resolution: String } input AssignSceneFileInput { diff --git a/internal/manager/scene.go b/internal/manager/scene.go index ff551754ed..0784c8b11d 100644 --- a/internal/manager/scene.go +++ b/internal/manager/scene.go @@ -11,15 +11,18 @@ import ( ) type SceneStreamEndpoint struct { - URL string `json:"url"` - MimeType *string `json:"mime_type"` - Label *string `json:"label"` + URL string `json:"url"` + MimeType *string `json:"mime_type"` + Label *string `json:"label"` + StreamType *string `json:"stream_type"` + Resolution *string `json:"resolution"` } type endpointType struct { - label string - mimeType string - extension string + label string + mimeType string + extension string + streamType string } var ( @@ -27,32 +30,38 @@ var ( label: "Direct stream", mimeType: ffmpeg.MimeMp4Video, extension: "", + streamType: "direct", } mp4EndpointType = endpointType{ label: "MP4", mimeType: ffmpeg.MimeMp4Video, extension: ".mp4", + streamType: "mp4", } mkvEndpointType = endpointType{ label: "MKV", // use mp4 mimetype to trick the client, since many clients won't try mkv mimeType: ffmpeg.MimeMp4Video, extension: ".mkv", + streamType: "mkv", } webmEndpointType = endpointType{ label: "WEBM", mimeType: ffmpeg.MimeWebmVideo, extension: ".webm", + streamType: "webm", } hlsEndpointType = endpointType{ label: "HLS", mimeType: ffmpeg.MimeHLS, extension: ".m3u8", + streamType: "hls", } dashEndpointType = endpointType{ label: "DASH", mimeType: ffmpeg.MimeDASH, extension: ".mpd", + streamType: "dash", } ) @@ -117,8 +126,10 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStrea url.Path += t.extension label := t.label + resolutionStr := "ORIGINAL" if resolution != "" { + resolutionStr = string(resolution) v := url.Query() v.Set("resolution", resolution.String()) url.RawQuery = v.Encode() @@ -138,9 +149,11 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStrea } return &SceneStreamEndpoint{ - URL: url.String(), - MimeType: &t.mimeType, - Label: &label, + URL: url.String(), + MimeType: &t.mimeType, + Label: &label, + StreamType: &t.streamType, + Resolution: &resolutionStr, } } diff --git a/ui/v2.5/graphql/data/scene.graphql b/ui/v2.5/graphql/data/scene.graphql index b7378c1da8..a3e5086f7a 100644 --- a/ui/v2.5/graphql/data/scene.graphql +++ b/ui/v2.5/graphql/data/scene.graphql @@ -78,6 +78,8 @@ fragment SceneData on Scene { url mime_type label + stream_type + resolution } custom_fields diff --git a/ui/v2.5/graphql/queries/scene.graphql b/ui/v2.5/graphql/queries/scene.graphql index 0e1a9fa11c..46c894fab1 100644 --- a/ui/v2.5/graphql/queries/scene.graphql +++ b/ui/v2.5/graphql/queries/scene.graphql @@ -94,6 +94,8 @@ query SceneStreams($id: ID!) { url mime_type label + stream_type + resolution } } } diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 6a90396313..106d6dfb6b 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -639,10 +639,14 @@ export const ScenePlayer: React.FC = PatchComponent( src: stream.url, type: stream.mime_type ?? undefined, label: stream.label ?? undefined, + streamType: stream.stream_type ?? undefined, + resolution: stream.resolution ?? undefined, offset: !isDirect(src), duration, }; - }) + }), + uiConfig?.defaultStreamType, + uiConfig?.defaultStreamingResolution ); function getDefaultLanguageCode() { @@ -733,6 +737,8 @@ export const ScenePlayer: React.FC = PatchComponent( interfaceConfig?.autostartVideo, uiConfig?.alwaysStartFromBeginning, uiConfig?.disableMobileMediaAutoRotateEnabled, + uiConfig?.defaultStreamType, + uiConfig?.defaultStreamingResolution, _initialTimestamp, ]); diff --git a/ui/v2.5/src/components/ScenePlayer/live.ts b/ui/v2.5/src/components/ScenePlayer/live.ts index 164f8090f3..2666a84834 100644 --- a/ui/v2.5/src/components/ScenePlayer/live.ts +++ b/ui/v2.5/src/components/ScenePlayer/live.ts @@ -76,7 +76,10 @@ function offsetMiddleware(player: VideoJsPlayer) { tech.trigger("timeupdate"); tech.trigger("pause"); tech.trigger("seeking"); - tech.play(); + tech.play().catch(() => { + // auto-play failed due to browser restrictions + seeking = 0; + }); }, loadDelay, { leading: true } diff --git a/ui/v2.5/src/components/ScenePlayer/source-selector.ts b/ui/v2.5/src/components/ScenePlayer/source-selector.ts index eafb18e2ae..2adad9a0fb 100644 --- a/ui/v2.5/src/components/ScenePlayer/source-selector.ts +++ b/ui/v2.5/src/components/ScenePlayer/source-selector.ts @@ -3,6 +3,23 @@ import videojs, { VideoJsPlayer } from "video.js"; export interface ISource extends videojs.Tech.SourceObject { label?: string; errored?: boolean; + streamType?: string; + resolution?: string; +} + +function preferredSourceIndex( + sources: ISource[], + preferredType: string | undefined, + preferredResolution: string | undefined +): number { + let matchedType = -1; + for (let i = 0; i < sources.length; i++) { + const source = sources[i]; + const matchType = preferredType ? source.streamType === preferredType : true; + if (matchType && source.resolution === preferredResolution) return i; + if (matchType && source.resolution === "ORIGINAL" && matchedType === -1) matchedType = i; + } + return matchedType === -1 ? 0 : matchedType; } class SourceMenuItem extends videojs.getComponent("MenuItem") { @@ -46,11 +63,11 @@ class SourceMenuButton extends videojs.getComponent("MenuButton") { }); } - public setSources(sources: ISource[]) { + public setSources(sources: ISource[], selectedIndex: number) { this.selectedSource = null; this.items = sources.map((source, i) => { - if (i === 0) { + if (i === selectedIndex) { this.selectedSource = source; } @@ -176,8 +193,10 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { const currentSource = player.currentSource() as ISource; console.log(`Source '${currentSource.label}' is unsupported`); - // mark current source as errored - currentSource.errored = true; + // mark current source as errored in this.sources (currentSource() returns a copy) + if (this.selectedIndex >= 0 && this.selectedIndex < this.sources.length) { + this.sources[this.selectedIndex].errored = true; + } this.menu.markSourceErrored(currentSource); // don't auto play next source if user manually selected a source @@ -187,12 +206,10 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { // TODO - make auto play next source configurable // try the next source in the list - if ( - this.selectedIndex !== -1 && - this.selectedIndex + 1 < this.sources.length - ) { - this.selectedIndex += 1; - const newSource = this.sources[this.selectedIndex]; + const nextIndex = (this.selectedIndex + 1) % this.sources.length; + if (this.selectedIndex !== -1 && !this.sources[nextIndex].errored) { + this.selectedIndex = nextIndex; + const newSource = this.sources[nextIndex]; console.log(`Trying next source in playlist: '${newSource.label}'`); this.menu.setSelectedSource(newSource); @@ -209,21 +226,28 @@ class SourceSelectorPlugin extends videojs.getPlugin("plugin") { }); } - setSources(sources: ISource[]) { + setSources( + sources: ISource[], + preferredType: string | undefined, + preferredResolution: string | undefined + ) { const cleanupTracks = this.cleanupTextTracks.splice(0); for (const track of cleanupTracks) { this.player.removeRemoteTextTrack(track); } - this.menu.setSources(sources); + const selectedIndex = preferredSourceIndex(sources, preferredType, preferredResolution); + if (selectedIndex === this.selectedIndex) return; + + this.menu.setSources(sources, selectedIndex); if (sources.length !== 0) { - this.selectedIndex = 0; + this.selectedIndex = selectedIndex; } else { this.selectedIndex = -1; } this.sources = sources; - this.player.src(sources[0]); + this.player.src(sources[selectedIndex]); } get textTracks(): HTMLTrackElement[] { diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 75f1a16950..47dbe9a532 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -490,6 +490,43 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( checked={ui.showAbLoopControls ?? undefined} onChange={(v) => saveUI({ showAbLoopControls: v })} /> + saveUI({ defaultStreamType: v })} + > + + + + + + + + + saveUI({ defaultStreamingResolution: v })} + > + + + + + + +