stash/internal/manager/scene.go

240 lines
8.4 KiB
Go

package manager
import (
"fmt"
"net/url"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/models"
)
type SceneStreamEndpoint struct {
URL string `json:"url"`
MimeType *string `json:"mime_type"`
Label *string `json:"label"`
}
type endpointType struct {
label string
mimeType string
extension string
}
var (
directEndpointType = endpointType{
label: "Direct stream",
mimeType: ffmpeg.MimeMp4Video,
extension: "",
}
mp4EndpointType = endpointType{
label: "MP4",
mimeType: ffmpeg.MimeMp4Video,
extension: ".mp4",
}
mkvEndpointType = endpointType{
label: "MKV",
// use mp4 mimetype to trick the client, since many clients won't try mkv
mimeType: ffmpeg.MimeMp4Video,
extension: ".mkv",
}
webmEndpointType = endpointType{
label: "WEBM",
mimeType: ffmpeg.MimeWebmVideo,
extension: ".webm",
}
hlsEndpointType = endpointType{
label: "HLS",
mimeType: ffmpeg.MimeHLS,
extension: ".m3u8",
}
dashEndpointType = endpointType{
label: "DASH",
mimeType: ffmpeg.MimeDASH,
extension: ".mpd",
}
)
func GetVideoFileContainer(file *file.VideoFile) (ffmpeg.Container, error) {
var container ffmpeg.Container
format := file.Format
if format != "" {
container = ffmpeg.Container(format)
} else { // container isn't in the DB
// shouldn't happen, fallback to ffprobe
ffprobe := GetInstance().FFProbe
tmpVideoFile, err := ffprobe.NewVideoFile(file.Path)
if err != nil {
return ffmpeg.Container(""), fmt.Errorf("error reading video file: %v", err)
}
return ffmpeg.MatchContainer(tmpVideoFile.Container, file.Path)
}
return container, nil
}
func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStreamingTranscodeSize models.StreamingResolutionEnum) ([]*SceneStreamEndpoint, error) {
if scene == nil {
return nil, fmt.Errorf("nil scene")
}
pf := scene.Files.Primary()
if pf == nil {
return nil, nil
}
// convert StreamingResolutionEnum to ResolutionEnum
maxStreamingResolution := models.ResolutionEnum(maxStreamingTranscodeSize)
sceneResolution := pf.GetMinResolution()
includeSceneStreamPath := func(streamingResolution models.StreamingResolutionEnum) bool {
var minResolution int
if streamingResolution == models.StreamingResolutionEnumOriginal {
minResolution = sceneResolution
} else {
// convert StreamingResolutionEnum to ResolutionEnum so we can get the min
// resolution
convertedRes := models.ResolutionEnum(streamingResolution)
minResolution = convertedRes.GetMinResolution()
// don't include if scene resolution is smaller than the streamingResolution
if sceneResolution != 0 && sceneResolution < minResolution {
return false
}
}
// if we always allow everything, then return true
if maxStreamingTranscodeSize == models.StreamingResolutionEnumOriginal {
return true
}
return maxStreamingResolution.GetMinResolution() >= minResolution
}
makeStreamEndpoint := func(t endpointType, resolution models.StreamingResolutionEnum) *SceneStreamEndpoint {
url := *directStreamURL
url.Path += t.extension
label := t.label
if resolution != "" {
v := url.Query()
v.Set("resolution", resolution.String())
url.RawQuery = v.Encode()
switch resolution {
case models.StreamingResolutionEnumFourK:
label += " 4K (2160p)"
case models.StreamingResolutionEnumFullHd:
label += " Full HD (1080p)"
case models.StreamingResolutionEnumStandardHd:
label += " HD (720p)"
case models.StreamingResolutionEnumStandard:
label += " Standard (480p)"
case models.StreamingResolutionEnumLow:
label += " Low (240p)"
}
}
return &SceneStreamEndpoint{
URL: url.String(),
MimeType: &t.mimeType,
Label: &label,
}
}
var endpoints []*SceneStreamEndpoint
// direct stream should only apply when the audio codec is supported
audioCodec := ffmpeg.MissingUnsupported
if pf.AudioCodec != "" {
audioCodec = ffmpeg.ProbeAudioCodec(pf.AudioCodec)
}
// don't care if we can't get the container
container, _ := GetVideoFileContainer(pf)
if HasTranscode(scene, config.GetInstance().GetVideoFileNamingAlgorithm()) || ffmpeg.IsValidAudioForContainer(audioCodec, container) {
endpoints = append(endpoints, makeStreamEndpoint(directEndpointType, ""))
}
// only add mkv stream endpoint if the scene container is an mkv already
if container == ffmpeg.Matroska {
endpoints = append(endpoints, makeStreamEndpoint(mkvEndpointType, ""))
}
mp4Streams := []*SceneStreamEndpoint{}
webmStreams := []*SceneStreamEndpoint{}
hlsStreams := []*SceneStreamEndpoint{}
dashStreams := []*SceneStreamEndpoint{}
if includeSceneStreamPath(models.StreamingResolutionEnumOriginal) {
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumOriginal))
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumOriginal))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumOriginal))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumOriginal))
}
if includeSceneStreamPath(models.StreamingResolutionEnumFourK) {
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumFourK))
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumFourK))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumFourK))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumFourK))
}
if includeSceneStreamPath(models.StreamingResolutionEnumFullHd) {
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumFullHd))
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumFullHd))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumFullHd))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumFullHd))
}
if includeSceneStreamPath(models.StreamingResolutionEnumStandardHd) {
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumStandardHd))
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumStandardHd))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumStandardHd))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumStandardHd))
}
if includeSceneStreamPath(models.StreamingResolutionEnumStandard) {
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumStandard))
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumStandard))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumStandard))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumStandard))
}
if includeSceneStreamPath(models.StreamingResolutionEnumLow) {
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumLow))
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumLow))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumLow))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumLow))
}
endpoints = append(endpoints, mp4Streams...)
endpoints = append(endpoints, webmStreams...)
endpoints = append(endpoints, hlsStreams...)
endpoints = append(endpoints, dashStreams...)
return endpoints, nil
}
// HasTranscode returns true if a transcoded video exists for the provided
// scene. It will check using the OSHash of the scene first, then fall back
// to the checksum.
func HasTranscode(scene *models.Scene, fileNamingAlgo models.HashAlgorithm) bool {
if scene == nil {
return false
}
sceneHash := scene.GetHash(fileNamingAlgo)
if sceneHash == "" {
return false
}
transcodePath := instance.Paths.Scene.GetTranscodePath(sceneHash)
ret, _ := fsutil.FileExists(transcodePath)
return ret
}