fix(jellycompat): preserve video color range
This commit is contained in:
@@ -2580,6 +2580,7 @@ func toVideoTrackRecords(tracks []models.VideoTrack) []VideoTrackRecord {
|
||||
Bitrate: track.Bitrate,
|
||||
VideoRange: track.VideoRange,
|
||||
VideoRangeType: track.VideoRangeType,
|
||||
ColorRange: track.ColorRange,
|
||||
ColorPrimaries: track.ColorPrimaries,
|
||||
ColorSpace: track.ColorSpace,
|
||||
ColorTransfer: track.ColorTransfer,
|
||||
|
||||
@@ -3,8 +3,24 @@ package catalogseed
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Silo-Server/silo-server/internal/models"
|
||||
)
|
||||
|
||||
func TestToVideoTrackRecordsPreservesColorRange(t *testing.T) {
|
||||
got := toVideoTrackRecords([]models.VideoTrack{
|
||||
{ColorRange: "tv"},
|
||||
{ColorRange: "pc"},
|
||||
})
|
||||
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("records length = %d, want 2", len(got))
|
||||
}
|
||||
if got[0].ColorRange != "tv" || got[1].ColorRange != "pc" {
|
||||
t.Fatalf("ColorRange values = [%q, %q], want [tv, pc]", got[0].ColorRange, got[1].ColorRange)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogSeedSearchUpsertIDsIncludesChangedItemsAndEmbeddings(t *testing.T) {
|
||||
itemStates := map[string]bool{
|
||||
"movie-1": true,
|
||||
|
||||
@@ -156,6 +156,7 @@ type VideoTrackRecord struct {
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
VideoRange string `json:"video_range,omitempty"`
|
||||
VideoRangeType string `json:"video_range_type,omitempty"`
|
||||
ColorRange string `json:"color_range,omitempty"`
|
||||
ColorPrimaries string `json:"color_primaries,omitempty"`
|
||||
ColorSpace string `json:"color_space,omitempty"`
|
||||
ColorTransfer string `json:"color_transfer,omitempty"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package jellycompat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -273,6 +274,39 @@ func TestBuildMediaStreamsUsesJellyfinVideoRangeType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMediaStreamsPreservesColorRange(t *testing.T) {
|
||||
for _, colorRange := range []string{"tv", "pc"} {
|
||||
t.Run(colorRange, func(t *testing.T) {
|
||||
version := catalog.FileVersion{
|
||||
VideoTracks: []models.VideoTrack{{
|
||||
Codec: "h264",
|
||||
ColorRange: colorRange,
|
||||
}},
|
||||
}
|
||||
|
||||
streams := buildMediaStreams("item", "source", version)
|
||||
if len(streams) != 1 {
|
||||
t.Fatalf("streams length = %d, want 1", len(streams))
|
||||
}
|
||||
if got := streams[0].ColorRange; got != colorRange {
|
||||
t.Fatalf("ColorRange = %q, want %q", got, colorRange)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(streams[0])
|
||||
if err != nil {
|
||||
t.Fatalf("marshal media stream: %v", err)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal media stream: %v", err)
|
||||
}
|
||||
if got := decoded["ColorRange"]; got != colorRange {
|
||||
t.Fatalf("JSON ColorRange = %#v, want %q", got, colorRange)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodecProfileAVCRefFramesConstraint(t *testing.T) {
|
||||
version := catalog.FileVersion{
|
||||
FileID: 1,
|
||||
|
||||
@@ -264,6 +264,7 @@ type mediaStreamDTO struct {
|
||||
AspectRatio string `json:"AspectRatio,omitempty"`
|
||||
VideoRange string `json:"VideoRange,omitempty"`
|
||||
VideoRangeType string `json:"VideoRangeType,omitempty"`
|
||||
ColorRange string `json:"ColorRange,omitempty"`
|
||||
ColorPrimaries string `json:"ColorPrimaries,omitempty"`
|
||||
ColorSpace string `json:"ColorSpace,omitempty"`
|
||||
ColorTransfer string `json:"ColorTransfer,omitempty"`
|
||||
|
||||
@@ -890,6 +890,7 @@ func buildMediaStreamsWithSelection(routeItemID, mediaSourceID string, version c
|
||||
AspectRatio: track.AspectRatio,
|
||||
VideoRange: compatVideoRange(track, version.HDR),
|
||||
VideoRangeType: compatVideoRangeType(track, version.HDR),
|
||||
ColorRange: track.ColorRange,
|
||||
ColorPrimaries: track.ColorPrimaries,
|
||||
ColorSpace: track.ColorSpace,
|
||||
ColorTransfer: track.ColorTransfer,
|
||||
|
||||
@@ -219,6 +219,7 @@ type VideoTrack struct {
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
VideoRange string `json:"video_range,omitempty"`
|
||||
VideoRangeType string `json:"video_range_type,omitempty"`
|
||||
ColorRange string `json:"color_range,omitempty"`
|
||||
ColorPrimaries string `json:"color_primaries,omitempty"`
|
||||
ColorSpace string `json:"color_space,omitempty"`
|
||||
ColorTransfer string `json:"color_transfer,omitempty"`
|
||||
|
||||
@@ -76,6 +76,7 @@ type ffprobeStream struct {
|
||||
StartTime string `json:"start_time"`
|
||||
Duration string `json:"duration"`
|
||||
BitRate string `json:"bit_rate"`
|
||||
ColorRange string `json:"color_range"`
|
||||
ColorTransfer string `json:"color_transfer"`
|
||||
ColorPrimaries string `json:"color_primaries"`
|
||||
ColorSpace string `json:"color_space"`
|
||||
@@ -205,6 +206,7 @@ func convertProbeData(raw *ffprobeOutput) *ProbeData {
|
||||
Bitrate: parseNumeric(s.BitRate) / 1000,
|
||||
VideoRange: videoRangeLabel(s),
|
||||
VideoRangeType: videoRangeType(s),
|
||||
ColorRange: firstNonEmpty(s.ColorRange, "unknown"),
|
||||
ColorPrimaries: s.ColorPrimaries,
|
||||
ColorSpace: s.ColorSpace,
|
||||
ColorTransfer: s.ColorTransfer,
|
||||
|
||||
@@ -54,6 +54,9 @@ func NeedsCriticalProbeRepair(file *models.MediaFile) bool {
|
||||
if strings.TrimSpace(file.CodecVideo) == "" || strings.TrimSpace(file.Resolution) == "" {
|
||||
return true
|
||||
}
|
||||
if videoTracksMissingColorRange(file.VideoTracks) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if file.Chapters == nil {
|
||||
return true
|
||||
@@ -61,6 +64,15 @@ func NeedsCriticalProbeRepair(file *models.MediaFile) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func videoTracksMissingColorRange(tracks []models.VideoTrack) bool {
|
||||
for _, track := range tracks {
|
||||
if strings.TrimSpace(track.ColorRange) == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PlaybackProbeEnsurer repairs missing playback-critical probe metadata on
|
||||
// demand by running a local ffprobe and persisting the result.
|
||||
type PlaybackProbeEnsurer struct {
|
||||
|
||||
@@ -47,6 +47,31 @@ func TestNeedsCriticalProbeRepair_ProbedVideoMissingResolutionStillRepairs(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsCriticalProbeRepair_ProbedVideoMissingColorRangeRepairsOnce(t *testing.T) {
|
||||
now := time.Now()
|
||||
f := &models.MediaFile{
|
||||
ProbeSource: "local",
|
||||
ProbeUpdatedAt: &now,
|
||||
Duration: 7200,
|
||||
Container: "mkv",
|
||||
CodecAudio: "aac",
|
||||
AudioTracks: []models.AudioTrack{{Language: "eng"}},
|
||||
CodecVideo: "h264",
|
||||
Resolution: "1080p",
|
||||
VideoTracks: []models.VideoTrack{{Codec: "h264"}},
|
||||
Chapters: []models.MediaChapter{},
|
||||
}
|
||||
|
||||
if !NeedsCriticalProbeRepair(f) {
|
||||
t.Fatal("a legacy video without color range should need probe repair")
|
||||
}
|
||||
|
||||
f.VideoTracks[0].ColorRange = "unknown"
|
||||
if NeedsCriticalProbeRepair(f) {
|
||||
t.Fatal("a reprobed video with unknown color range should not repair again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsCriticalProbeRepair_UnprobedFileRepairs(t *testing.T) {
|
||||
if !NeedsCriticalProbeRepair(&models.MediaFile{}) {
|
||||
t.Fatal("an unprobed file must need probe repair")
|
||||
@@ -64,7 +89,7 @@ func implausiblyShortLargeVideoFile(probedAt time.Time) *models.MediaFile {
|
||||
AudioTracks: []models.AudioTrack{{Language: "eng"}},
|
||||
CodecVideo: "h264",
|
||||
Resolution: "720p",
|
||||
VideoTracks: []models.VideoTrack{{Codec: "h264"}},
|
||||
VideoTracks: []models.VideoTrack{{Codec: "h264", ColorRange: "unknown"}},
|
||||
Chapters: []models.MediaChapter{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,44 @@
|
||||
package scanner
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/Silo-Server/silo-server/internal/models"
|
||||
)
|
||||
|
||||
func TestProbePipelinePreservesVideoColorRange(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
colorRange string
|
||||
wantColorRange string
|
||||
}{
|
||||
{name: "limited", colorRange: "tv", wantColorRange: "tv"},
|
||||
{name: "full", colorRange: "pc", wantColorRange: "pc"},
|
||||
{name: "unspecified", wantColorRange: "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rawJSON := `{"streams":[{"codec_type":"video","codec_name":"h264","color_range":"` + tt.colorRange + `"}]}`
|
||||
var raw ffprobeOutput
|
||||
if err := json.Unmarshal([]byte(rawJSON), &raw); err != nil {
|
||||
t.Fatalf("unmarshal ffprobe output: %v", err)
|
||||
}
|
||||
|
||||
probe := convertProbeData(&raw)
|
||||
file := &models.MediaFile{}
|
||||
applyProbeData(file, probe, "local")
|
||||
|
||||
if len(file.VideoTracks) != 1 {
|
||||
t.Fatalf("VideoTracks length = %d, want 1", len(file.VideoTracks))
|
||||
}
|
||||
if got := file.VideoTracks[0].ColorRange; got != tt.wantColorRange {
|
||||
t.Fatalf("ColorRange = %q, want %q", got, tt.wantColorRange)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertProbeDataVideoRangeTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
||||
@@ -3186,6 +3186,7 @@ func applyProbeData(mf *models.MediaFile, probe *ProbeData, probeSource string)
|
||||
Bitrate: vt.Bitrate,
|
||||
VideoRange: vt.VideoRange,
|
||||
VideoRangeType: vt.VideoRangeType,
|
||||
ColorRange: vt.ColorRange,
|
||||
ColorPrimaries: vt.ColorPrimaries,
|
||||
ColorSpace: vt.ColorSpace,
|
||||
ColorTransfer: vt.ColorTransfer,
|
||||
|
||||
@@ -67,6 +67,7 @@ type VideoTrackInfo struct {
|
||||
Bitrate int
|
||||
VideoRange string
|
||||
VideoRangeType string
|
||||
ColorRange string
|
||||
ColorPrimaries string
|
||||
ColorSpace string
|
||||
ColorTransfer string
|
||||
|
||||
@@ -970,6 +970,7 @@ export interface VersionVideoTrack {
|
||||
bitrate?: number;
|
||||
video_range?: string;
|
||||
video_range_type?: string;
|
||||
color_range?: string;
|
||||
color_primaries?: string;
|
||||
color_space?: string;
|
||||
color_transfer?: string;
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface PlayerVideoTrack {
|
||||
frame_rate?: string;
|
||||
bitrate?: number;
|
||||
video_range?: string;
|
||||
color_range?: string;
|
||||
color_primaries?: string;
|
||||
color_space?: string;
|
||||
color_transfer?: string;
|
||||
|
||||
Reference in New Issue
Block a user