* fix(scanner): reject durations that imply an impossible bitrate The duration-plausibility rule only rejected videos of 10 seconds or less, so a feature film that probed as 61 seconds passed untouched and persisted. Clients then had nothing trustworthy to anchor on: Android's grow-only duration ratchet has no floor to hold when the catalog value is wrong, so the playback engine's growing-HLS-window duration won and a 90-minute movie displayed as ~1 minute. Size and duration together pin an implied bitrate, which separates the two cases the absolute floor conflates. A genuine short clip has an ordinary bitrate; a 100 GB file claiming 61 seconds implies ~13 Gbps. The ceiling sits far above any real medium, so legitimate content cannot trip it — and unlike the absolute floor, it does not false-positive on a genuine high-bitrate short. Also bump the repair-rule revision marker so rows judged by the previous, weaker rule are re-checked once under this one. Without that bump an improved rule never reaches the rows it was written for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(playback): publish source runtime in v3 plans and stop faking the copy seek window Two defects with one root: a v3 plan described where playback sits without ever stating how long the media is. Add source.duration_seconds. It is the file's full runtime, never `total - source_start` and never adjusted by timeline_offset_seconds, and it is omitted rather than null when unknown — clients that coerce null to a numeric default would read it as zero, the exact value this field exists to stop them inventing. It is set in SourceDescriptorFromFileV3, the single place every delivery already flows through, so direct play, progressive remux, HLS remux and HLS transcode all carry it. Until now the v3 plan omitted duration entirely, so clients fell back to the playback engine. On an HLS copy remux the server intentionally serves FFmpeg's still-growing playlist, so the engine reports the length produced so far. With no server-supplied runtime to anchor on, a feature film played back as a couple of minutes. The legacy protocol already answered this correctly via fileDurationSeconds; this restores parity. Separately, the copy branch published seek_window_end_seconds as the media runtime. That made the window look *complete*, which clients read as proof that any target inside it is locally seekable, so they native-seek past the produced head of a growing playlist instead of asking for a reanchor. Leave the end open: an incomplete window plus can_seek_anywhere=false routes every seek through the server, which is what legacy did before v3 added the bound. Advertise plan_source_duration_v1 so a client can distinguish "this server does not populate the field" from "this server knows the runtime is genuinely unknown" — without it, both look like an absent field and a client cannot tell whether its own catalog fallback is still required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(web): pair the exit position with the media runtime, not the element duration The player's exit state converts its position to media time but took the duration from the video element, which is player-local. On a remux or transcode stream the element only covers the window produced so far, so the two values live in different coordinate systems. Resuming a movie 50 minutes in makes that concrete: the exit position is ~3060s of media time while the element reports ~120s. The progress cache then evaluates `position >= duration`, marks the item completed, latches the watched badge, and — because completion clears the resume point — resets position to 0. Exiting a resumed movie destroyed the resume point and claimed it had been watched. The server's runtime is authoritative and already expressed in media time, so prefer it and fall back to the element only when no server value exists. The rule moves into mediaTimeline.ts next to the coordinate conversions it depends on, which is also what makes it testable — VideoPlayer itself has no test harness. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1007 lines
52 KiB
Go
1007 lines
52 KiB
Go
package playback
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
)
|
|
|
|
func hasDegradationWarningV3(warnings []DegradationWarningV3, code string) bool {
|
|
for _, warning := range warnings {
|
|
if warning.Code == code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestServerFeaturesV3ReturnsCompleteIndependentSlices(t *testing.T) {
|
|
first := ServerFeaturesV3()
|
|
second := ServerFeaturesV3()
|
|
expected := map[string]struct{}{
|
|
FeaturePlaybackPlanV3: {},
|
|
FeatureMedia3Only: {},
|
|
FeatureDetailedDecodeV3: {},
|
|
FeatureLayoutPassthrough: {},
|
|
FeatureRouteDiagnostics: {},
|
|
FeatureDeviceQuirksV3: {},
|
|
FeatureSeekReanchorV3: {},
|
|
FeatureDirectStreamResumeV3: {},
|
|
FeaturePlanSourceDurationV3: {},
|
|
}
|
|
if len(first) != len(expected) {
|
|
t.Fatalf("server features = %v, want %d entries", first, len(expected))
|
|
}
|
|
seen := make(map[string]struct{}, len(first))
|
|
for _, feature := range first {
|
|
if _, ok := expected[feature]; !ok {
|
|
t.Fatalf("server features contain unexpected %q: %v", feature, first)
|
|
}
|
|
if _, duplicate := seen[feature]; duplicate {
|
|
t.Fatalf("server features contain duplicate %q: %v", feature, first)
|
|
}
|
|
seen[feature] = struct{}{}
|
|
}
|
|
for feature := range expected {
|
|
if _, ok := seen[feature]; !ok {
|
|
t.Fatalf("server features omitted %q: %v", feature, first)
|
|
}
|
|
}
|
|
|
|
first[0] = "mutated"
|
|
if second[0] != FeaturePlaybackPlanV3 {
|
|
t.Fatalf("feature slices share backing storage: %v", second)
|
|
}
|
|
}
|
|
|
|
func TestStartRequestV3Validation(t *testing.T) {
|
|
index := 1
|
|
req := validStartRequestV3()
|
|
req.AudioTrackIndex = &index
|
|
req.AudioTrackID = TrackIDV3(req.FileID, "audio", index)
|
|
warnings, err := req.NormalizeAndValidate()
|
|
if err != nil {
|
|
t.Fatalf("NormalizeAndValidate: %v", err)
|
|
}
|
|
if len(warnings) != 0 {
|
|
t.Fatalf("warnings = %#v, want none", warnings)
|
|
}
|
|
|
|
req.AudioTrackID = TrackIDV3(req.FileID, "audio", 2)
|
|
if _, err := req.NormalizeAndValidate(); err == nil {
|
|
t.Fatal("mismatched track id/index accepted")
|
|
}
|
|
}
|
|
|
|
func TestStartRequestV3UnknownQualityFallsBackToAuto(t *testing.T) {
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "future-super-quality"
|
|
warnings, err := req.NormalizeAndValidate()
|
|
if err != nil {
|
|
t.Fatalf("NormalizeAndValidate: %v", err)
|
|
}
|
|
if req.QualityPreference != "auto" || len(warnings) != 1 || warnings[0].Code != "quality_preference_normalized" {
|
|
t.Fatalf("quality=%q warnings=%#v", req.QualityPreference, warnings)
|
|
}
|
|
}
|
|
|
|
func TestReplanRequestV3OperationDefaultsAndValidates(t *testing.T) {
|
|
start := validStartRequestV3()
|
|
request := ReplanRequestV3{
|
|
ProtocolVersion: ProtocolV3,
|
|
PlaybackAttemptID: start.PlaybackAttemptID,
|
|
ReplanRequestID: "replan-operation-0001",
|
|
FailedPlanID: "plan:operation-0001",
|
|
PlanAttemptID: "plan-attempt-operation-0001",
|
|
PlanAttemptKey: "v3:0000000000000001",
|
|
AttemptCount: 1,
|
|
QualityPreference: start.QualityPreference,
|
|
OutputRouteGeneration: start.OutputRouteGeneration,
|
|
Failure: FailureV3{Classification: "parser_failure"},
|
|
Capabilities: start.Capabilities,
|
|
ClientPlaybackContext: start.ClientPlaybackContext,
|
|
}
|
|
if request.EffectiveOperation() != ReplanOperationFailureRecoveryV3 {
|
|
t.Fatalf("missing operation = %q", request.EffectiveOperation())
|
|
}
|
|
if err := request.Validate(); err != nil {
|
|
t.Fatalf("backward-compatible default operation: %v", err)
|
|
}
|
|
|
|
request.Operation = ReplanOperationSeekReanchorV3
|
|
request.Failure.Classification = ""
|
|
if err := request.Validate(); err != nil {
|
|
t.Fatalf("seek reanchor operation without fake failure: %v", err)
|
|
}
|
|
body, err := json.Marshal(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wire map[string]any
|
|
if err := json.Unmarshal(body, &wire); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wire["operation"] != string(ReplanOperationSeekReanchorV3) {
|
|
t.Fatalf("serialized operation = %#v", wire["operation"])
|
|
}
|
|
|
|
request.Operation = ReplanOperationSeekFailureRecoveryV3
|
|
if err := request.Validate(); err == nil {
|
|
t.Fatal("seek failure recovery without a classification was accepted")
|
|
}
|
|
request.Failure.Classification = "decoder_failure"
|
|
if err := request.Validate(); err != nil {
|
|
t.Fatalf("seek failure recovery operation: %v", err)
|
|
}
|
|
|
|
request.Operation = "future_operation"
|
|
if err := request.Validate(); err == nil {
|
|
t.Fatal("unknown replan operation was accepted")
|
|
}
|
|
}
|
|
|
|
func TestReplanRequestV3RejectsInvalidNetworkAndTrackEvidence(t *testing.T) {
|
|
start := validStartRequestV3()
|
|
request := ReplanRequestV3{
|
|
ProtocolVersion: ProtocolV3,
|
|
PlaybackAttemptID: start.PlaybackAttemptID,
|
|
ReplanRequestID: "replan-validation-0001",
|
|
FailedPlanID: "plan:validation-0001",
|
|
PlanAttemptID: "plan-attempt-validation-0001",
|
|
PlanAttemptKey: "v3:0000000000000001",
|
|
AttemptCount: 1,
|
|
QualityPreference: start.QualityPreference,
|
|
OutputRouteGeneration: start.OutputRouteGeneration,
|
|
Failure: FailureV3{Classification: "parser_failure"},
|
|
Capabilities: start.Capabilities,
|
|
ClientPlaybackContext: start.ClientPlaybackContext,
|
|
}
|
|
|
|
negative := -1
|
|
request.SelectedTracks.Subtitle = &TrackIdentityV3{ID: "file:42:subtitle:-1", Index: &negative}
|
|
if err := request.Validate(); err == nil {
|
|
t.Fatal("negative subtitle index was accepted")
|
|
}
|
|
|
|
request.SelectedTracks.Subtitle = nil
|
|
tooLow := 99
|
|
request.BandwidthEstimateKbps = &tooLow
|
|
if err := request.Validate(); err == nil {
|
|
t.Fatal("out-of-range bandwidth estimate was accepted")
|
|
}
|
|
}
|
|
|
|
func TestPlanAttemptKeyV3KotlinFixture(t *testing.T) {
|
|
type fixture struct {
|
|
Name string `json:"name"`
|
|
PlanID string `json:"plan_id"`
|
|
Delivery DeliveryV3 `json:"delivery"`
|
|
StreamProtocol StreamProtocolV3 `json:"stream_protocol"`
|
|
Container string `json:"container"`
|
|
VideoCodec string `json:"video_codec"`
|
|
AudioCodec string `json:"audio_codec"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
BitrateKbps int `json:"bitrate_kbps"`
|
|
DynamicRange string `json:"dynamic_range"`
|
|
SubtitleMode SubtitleModeV3 `json:"subtitle_mode"`
|
|
Transformations []TransformationV3 `json:"transformations"`
|
|
AppliedQuirks []AppliedQuirkV3 `json:"applied_quirks"`
|
|
RuntimeCorrections []string `json:"runtime_corrections"`
|
|
OutputRouteGeneration int64 `json:"output_route_generation"`
|
|
LocalMutations []string `json:"local_mutations"`
|
|
Expected string `json:"expected"`
|
|
}
|
|
body, err := os.ReadFile("testdata/protocol_v3/attempt_keys.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fixtures []fixture
|
|
if err := json.Unmarshal(body, &fixtures); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, value := range fixtures {
|
|
t.Run(value.Name, func(t *testing.T) {
|
|
plan := PlanV3{
|
|
PlanID: value.PlanID,
|
|
Delivery: value.Delivery,
|
|
Stream: StreamV3{Protocol: value.StreamProtocol, Container: value.Container},
|
|
EffectiveRecipe: EffectiveRecipeV3{VideoCodec: value.VideoCodec, AudioCodec: value.AudioCodec, Width: &value.Width, Height: &value.Height, BitrateKbps: &value.BitrateKbps, DynamicRange: value.DynamicRange},
|
|
Subtitle: SubtitleDecisionV3{Mode: value.SubtitleMode},
|
|
}
|
|
plan.Transformations = append(plan.Transformations, value.Transformations...)
|
|
// The quirk identity is conditionally omitted from the preimage
|
|
// when no quirks or runtime corrections apply; fixtures pin both
|
|
// arities so the Kotlin client reproduces the omission exactly.
|
|
plan.AppliedQuirks = append(plan.AppliedQuirks, value.AppliedQuirks...)
|
|
plan.RuntimeCorrections = append(plan.RuntimeCorrections, value.RuntimeCorrections...)
|
|
if got := PlanAttemptKeyV3(plan, value.OutputRouteGeneration, value.LocalMutations); got != value.Expected {
|
|
t.Fatalf("key = %q, want %q", got, value.Expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProtocolV3GoldenWireFixtures(t *testing.T) {
|
|
startBody, err := os.ReadFile("testdata/protocol_v3/start_request.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var start StartRequestV3
|
|
if err := json.Unmarshal(startBody, &start); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := start.NormalizeAndValidate(); err != nil {
|
|
t.Fatalf("golden start request: %v", err)
|
|
}
|
|
replanBody, err := os.ReadFile("testdata/protocol_v3/replan_request.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var replan ReplanRequestV3
|
|
if err := json.Unmarshal(replanBody, &replan); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := replan.Validate(); err != nil {
|
|
t.Fatalf("golden replan request: %v", err)
|
|
}
|
|
if replan.BandwidthEstimateKbps == nil || *replan.BandwidthEstimateKbps != 3_500 ||
|
|
replan.BandwidthCapKbps == nil || *replan.BandwidthCapKbps != 4_000 || !replan.Metered {
|
|
t.Fatalf("golden replan network evidence = %#v", replan)
|
|
}
|
|
responseBody, err := os.ReadFile("testdata/protocol_v3/decision_response.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var response DecisionResponseV3
|
|
if err := json.Unmarshal(responseBody, &response); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.ProtocolVersion != ProtocolV3 || response.Outcome != OutcomePlayableV3 || response.PlaybackPlan == nil || response.PlaybackPlan.Stream.Protocol != StreamHTTPProgressiveV3 {
|
|
t.Fatalf("golden response = %#v", response)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3DirectRequiresDetailedEvidence(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3, FeatureLayoutPassthrough)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3, FeatureLayoutPassthrough)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
req.ClientPlaybackContext.Output.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
|
|
req.ClientFeatures = []string{FeaturePlaybackPlanV3, FeatureMedia3Only}
|
|
req.ClientPlaybackContext.Features = req.ClientFeatures
|
|
result = PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: false, Allow4KTranscode: true}})
|
|
if result.Terminal == nil || result.Terminal.Reason != "transcoding_disabled" {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
}
|
|
|
|
func TestSourceDescriptorV3NormalizesLegacyHEVCMetadata(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].BitDepth = 0
|
|
file.VideoTracks[0].PixelFormat = "yuv420p10le"
|
|
file.VideoTracks[0].DVProfile = 7
|
|
file.VideoTracks[0].DVELPresent = false
|
|
file.VideoTracks[0].DVEnhancementLayer = ""
|
|
file.VideoTracks[0].VideoRangeType = "DOVIWithEL"
|
|
|
|
source := SourceDescriptorFromFileV3(file, 0)
|
|
if source.BitDepth != 10 {
|
|
t.Fatalf("bit depth = %d, want inferred 10", source.BitDepth)
|
|
}
|
|
if source.DVEnhancementLayer != EnhancementUnknownV3 {
|
|
t.Fatalf("enhancement layer = %q, want unknown", source.DVEnhancementLayer)
|
|
}
|
|
}
|
|
|
|
func TestSourceDescriptorV3PreservesCanonicalColorRange(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{name: "limited", input: "tv", want: "tv"},
|
|
{name: "full", input: "pc", want: "pc"},
|
|
{name: "unspecified", input: "unknown", want: "unknown"},
|
|
{name: "normalizes case and whitespace", input: " PC ", want: "pc"},
|
|
{name: "rejects non-ffmpeg value", input: "limited", want: ""},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].ColorRange = test.input
|
|
if got := SourceDescriptorFromFileV3(file, 0).ColorRange; got != test.want {
|
|
t.Fatalf("color range = %q, want %q", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3DirectPlaysLegacyHDR10WithInferredBitDepth(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].BitDepth = 0
|
|
file.VideoTracks[0].PixelFormat = "yuv420p10le"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
req.ClientPlaybackContext.Output.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 || !result.Plan.Claims.Video.HDR10 {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3DirectPlaysLegacyDolbyVisionProfile8(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].BitDepth = 0
|
|
file.VideoTracks[0].PixelFormat = "yuv420p10le"
|
|
file.VideoTracks[0].DVProfile = 8
|
|
file.VideoTracks[0].DVBLCompatID = 1
|
|
file.VideoTracks[0].VideoRange = "DolbyVision"
|
|
file.VideoTracks[0].VideoRangeType = "DOVIWithHDR10"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{8}}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 || !result.Plan.Claims.Video.DolbyVision {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
if result.Plan.RequestedMediaFileID != file.ID || result.Plan.EffectiveMediaFileID != file.ID {
|
|
t.Fatalf("source ids = requested %d effective %d", result.Plan.RequestedMediaFileID, result.Plan.EffectiveMediaFileID)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3RejectsTrulyIncompleteVideoMetadata(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].BitDepth = 0
|
|
file.VideoTracks[0].PixelFormat = ""
|
|
file.VideoTracks[0].Profile = ""
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Terminal == nil || result.Terminal.Reason != "source_metadata_incomplete" {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3BlocksUltrawide4KTranscode(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.Resolution = "2160p"
|
|
file.VideoTracks[0].Width = 3840
|
|
file.VideoTracks[0].Height = 1626
|
|
file.VideoTracks[0].VideoRange = "SDR"
|
|
file.VideoTracks[0].VideoRangeType = "SDR"
|
|
file.VideoTracks[0].ColorTransfer = "bt709"
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "1080p"
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: false}, Registry: testTransformationRegistryV3()})
|
|
if result.Terminal == nil || result.Terminal.Reason != "no_alternate_version" {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3Profile7FallsBackToHDR10WithoutNativeP7(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].DVProfile = 7
|
|
file.VideoTracks[0].DVBLCompatID = 6
|
|
file.VideoTracks[0].DVELPresent = false
|
|
file.VideoTracks[0].DVEnhancementLayer = ""
|
|
file.VideoTracks[0].VideoRange = "DolbyVision"
|
|
file.VideoTracks[0].VideoRangeType = "DOVIWithEL"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{5, 8}}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
registry := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "server_dv7_to_hdr10", Available: true}})
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true}, Registry: registry})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryRemuxProgressiveV3 || !result.Plan.Claims.Video.HDR10 || result.Plan.Claims.Video.DolbyVision {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
if len(result.Plan.Transformations) != 1 || result.Plan.Transformations[0].Name != "server_dv7_to_hdr10" {
|
|
t.Fatalf("transformations = %#v", result.Plan.Transformations)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3Profile7UsesVersionedClientTransformationsOnSameFile(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].DVProfile = 7
|
|
file.VideoTracks[0].DVBLCompatID = 1
|
|
file.VideoTracks[0].DVELPresent = true
|
|
file.VideoTracks[0].DVEnhancementLayer = "unknown"
|
|
file.VideoTracks[0].VideoRange = "DolbyVision"
|
|
file.VideoTracks[0].VideoRangeType = "DOVIWithEL"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3, FeatureClientVideoTransforms)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3, FeatureClientVideoTransforms)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{8}}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
direct := req.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)]
|
|
direct.Transformations = []TransformationV3{
|
|
{Name: ClientDV7ToDV81V3, Executor: "client", RecipeVersion: ClientDVTransformVersionV3},
|
|
{Name: ClientDV7ToHDR10V3, Executor: "client", RecipeVersion: ClientDVTransformVersionV3},
|
|
}
|
|
req.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)] = direct
|
|
|
|
first := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true}, Registry: testTransformationRegistryV3()})
|
|
if first.Plan == nil || first.Plan.Delivery != DeliveryOriginalHTTPV3 || first.Plan.EffectiveMediaFileID != file.ID || first.Plan.DecisionReason != "client_dv7_to_dv81" {
|
|
t.Fatalf("first = %#v", first)
|
|
}
|
|
if got := first.Plan.Transformations; len(got) != 1 || got[0].Name != ClientDV7ToDV81V3 || got[0].Executor != "client" || got[0].RecipeVersion != "1" {
|
|
t.Fatalf("first transformations = %#v", got)
|
|
}
|
|
|
|
failedKey := PlanAttemptKeyV3(*first.Plan, req.OutputRouteGeneration, nil)
|
|
second := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true}, Registry: testTransformationRegistryV3(), AttemptedKeys: []string{failedKey}})
|
|
if second.Plan == nil || second.Plan.Delivery != DeliveryOriginalHTTPV3 || second.Plan.EffectiveMediaFileID != file.ID || second.Plan.DecisionReason != "client_dv7_to_hdr10" || !second.Plan.Claims.Video.HDR10 {
|
|
t.Fatalf("second = %#v", second)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3Profile7DoesNotInferClientTransformFromHDRProfile(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].DVProfile = 7
|
|
file.VideoTracks[0].VideoRange = "DolbyVision"
|
|
file.VideoTracks[0].VideoRangeType = "DOVIWithEL"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{7, 8}}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: false}})
|
|
if result.Plan != nil && result.Plan.Delivery == DeliveryOriginalHTTPV3 {
|
|
t.Fatalf("Profile 7 must not direct-play from codec claims alone: %#v", result.Plan)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3AudioAdaptationCopiesVideo(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.AudioTracks[0] = models.AudioTrack{Codec: "truehd", Channels: 8, Layout: "7.1"}
|
|
file.CodecAudio = "truehd"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryRemuxProgressiveV3 || !result.TranscodeAudio || result.TargetVideoCodec != "" {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3FallsBackFromProgressiveToHLSWithoutRepeatingKey(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].VideoRange = "SDR"
|
|
file.VideoTracks[0].VideoRangeType = "SDR"
|
|
file.VideoTracks[0].ColorTransfer = "bt709"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.Containers = []string{"mp4"}
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
first := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}})
|
|
if first.Plan == nil || first.Plan.Delivery != DeliveryRemuxProgressiveV3 {
|
|
t.Fatalf("first = %s", ExplainPlannerResultV3(first))
|
|
}
|
|
failedKey := PlanAttemptKeyV3(*first.Plan, req.OutputRouteGeneration, nil)
|
|
second := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, AttemptedKeys: []string{failedKey}})
|
|
if second.Plan == nil || second.Plan.Delivery != DeliveryRemuxHLSV3 || second.TargetVideoCodec != "copy" {
|
|
t.Fatalf("second = %#v", second)
|
|
}
|
|
secondKey := PlanAttemptKeyV3(*second.Plan, req.OutputRouteGeneration, nil)
|
|
third := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3(), AttemptedKeys: []string{failedKey, secondKey}})
|
|
if third.Plan == nil || third.Plan.Delivery != DeliveryTranscodeHLSV3 || third.Plan.DecisionReason != "copy_routes_exhausted" {
|
|
t.Fatalf("third = %#v", third)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3NeverClaimsUnimplementedHDRTranscode(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "1080p"
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}})
|
|
if result.Terminal == nil || result.Terminal.Reason != "hdr_transcode_unsupported" {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3Profile7StripFallsBackToValidatedHLSCopy(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].DVProfile = 7
|
|
file.VideoTracks[0].DVBLCompatID = 1
|
|
file.VideoTracks[0].DVELPresent = true
|
|
file.VideoTracks[0].DVEnhancementLayer = "mel"
|
|
file.VideoTracks[0].VideoRange = "DolbyVision"
|
|
file.VideoTracks[0].VideoRangeType = "DOVIWithEL"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.Containers = []string{"mp4"}
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{7}}
|
|
registry := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "server_dv7_to_hdr10", Available: true}})
|
|
first := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: registry})
|
|
if first.Plan == nil || first.Plan.Delivery != DeliveryRemuxProgressiveV3 || len(first.Plan.Transformations) != 1 {
|
|
t.Fatalf("first = %#v", first)
|
|
}
|
|
failedKey := PlanAttemptKeyV3(*first.Plan, req.OutputRouteGeneration, nil)
|
|
second := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: registry, AttemptedKeys: []string{failedKey}})
|
|
if second.Plan == nil || second.Plan.Delivery != DeliveryRemuxHLSV3 || second.TargetVideoCodec != "copy" || len(second.Plan.Transformations) != 1 || second.Plan.Transformations[0].Name != "server_dv7_to_hdr10" {
|
|
t.Fatalf("second = %#v", second)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3Profile8CompatibleBaseLayerStripsToHDR10(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].DVProfile = 8
|
|
file.VideoTracks[0].DVBLCompatID = 1
|
|
file.VideoTracks[0].VideoRange = "DolbyVision"
|
|
file.VideoTracks[0].VideoRangeType = "DOVI"
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
registry := NewTransformationRegistryV3([]TransformationSpecV3{{Name: "server_dv7_to_hdr10", Available: true}})
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: registry})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryRemuxProgressiveV3 || result.Plan.EffectiveRecipe.DynamicRange != "hdr10" || len(result.Plan.Transformations) != 1 || result.Plan.Transformations[0].Name != "server_dv7_to_hdr10" {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3PassthroughRequiresExactLayoutEntry(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.CodecAudio = "truehd"
|
|
file.AudioTracks[0] = models.AudioTrack{Codec: "truehd", Channels: 8, Layout: "7.1"}
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3, FeatureLayoutPassthrough)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3, FeatureLayoutPassthrough)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
req.Capabilities.AudioPassthrough = &AudioPassthroughV3{PassthroughCodecs: []string{"truehd"}, MaxChannels: 8, Entries: []AudioPassthroughEntryV3{{Codec: "truehd"}}}
|
|
withoutLayout := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if withoutLayout.Plan == nil || !withoutLayout.TranscodeAudio {
|
|
t.Fatalf("without exact layout = %#v", withoutLayout)
|
|
}
|
|
req.Capabilities.AudioPassthrough.Entries[0].ChannelCounts = []int{8}
|
|
req.Capabilities.AudioPassthrough.Entries[0].Layouts = []string{"7.1"}
|
|
withLayout := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}})
|
|
if withLayout.Plan == nil || withLayout.Plan.Delivery != DeliveryOriginalHTTPV3 || !withLayout.Plan.Claims.Audio.Passthrough {
|
|
t.Fatalf("with exact layout = %#v", withLayout)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3DownloadedSubtitleUsesFrozenCombinedOrdinal(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
index := 0
|
|
req.SubtitleTrackIndex = &index
|
|
req.SubtitleTrackID = TrackIDV3(file.ID, "subtitle", index)
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, AdditionalSubtitles: []SubtitleInventoryEntryV3{{CombinedIndex: 0, Codec: "srt", Source: "downloaded"}}})
|
|
if result.Plan == nil || result.Plan.Subtitle.Mode != SubtitleRenderV3 || result.Plan.SelectedTracks.Subtitle == nil || result.Plan.SelectedTracks.Subtitle.ID != req.SubtitleTrackID {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestSubtitleBurnInUsesEmbeddedOrdinalAndRejectsUnsupportedSources(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.ExternalSubtitles = []models.ExternalSubtitle{{Format: "ass"}}
|
|
file.SubtitleTracks = []models.SubtitleTrack{{Codec: "hdmv_pgs_subtitle"}}
|
|
req := validStartRequestV3()
|
|
embeddedCombinedIndex := 1
|
|
req.SubtitleTrackIndex = &embeddedCombinedIndex
|
|
req.SubtitleTrackID = TrackIDV3(file.ID, "subtitle", embeddedCombinedIndex)
|
|
result := ResolveSubtitlePolicyV3(file, req, true, EngineMedia3DirectV3, nil)
|
|
if !result.RequiresBurn || result.SelectedIndex != 1 || result.TransportIndex != 0 {
|
|
t.Fatalf("embedded burn-in result = %#v", result)
|
|
}
|
|
|
|
externalIndex := 0
|
|
req.SubtitleTrackIndex = &externalIndex
|
|
req.SubtitleTrackID = TrackIDV3(file.ID, "subtitle", externalIndex)
|
|
req.SubtitleFidelityPreference = SubtitleFidelityPreserveV3
|
|
result = ResolveSubtitlePolicyV3(file, req, true, EngineMedia3DirectV3, nil)
|
|
if result.Terminal == nil || result.Terminal.Reason != "subtitle_burn_in_source_unsupported" {
|
|
t.Fatalf("external burn-in result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveQualityPolicyV3HonorsBandwidthCapInAllModes(t *testing.T) {
|
|
source := SourceDescriptorV3{Width: 3840, Height: 2160, BitrateKbps: 20_000}
|
|
cap := 5_000
|
|
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "original"
|
|
req.BandwidthCapKbps = &cap
|
|
result := ResolveQualityPolicyV3(req, source)
|
|
if !result.RequiresTranscode || result.Height != 720 || result.Reason != "quality_bandwidth_cap" || result.ExplicitRung {
|
|
t.Fatalf("original over cap = %#v", result)
|
|
}
|
|
if !hasDegradationWarningV3(result.Warnings, "bandwidth_cap_applied") {
|
|
t.Fatalf("missing cap warning: %#v", result.Warnings)
|
|
}
|
|
|
|
lowBitrateSource := SourceDescriptorV3{Width: 1920, Height: 1080, BitrateKbps: 4_000}
|
|
result = ResolveQualityPolicyV3(req, lowBitrateSource)
|
|
if !result.PreservesSource || result.RequiresTranscode || len(result.Warnings) != 0 {
|
|
t.Fatalf("original under cap = %#v", result)
|
|
}
|
|
|
|
req.QualityPreference = "1080p"
|
|
result = ResolveQualityPolicyV3(req, source)
|
|
if result.Height != 720 || result.Reason != "quality_bandwidth_cap" || !result.ExplicitRung || !result.RequiresTranscode {
|
|
t.Fatalf("fixed rung over cap = %#v", result)
|
|
}
|
|
if !hasDegradationWarningV3(result.Warnings, "bandwidth_cap_applied") {
|
|
t.Fatalf("missing cap warning: %#v", result.Warnings)
|
|
}
|
|
|
|
req.QualityPreference = "480p"
|
|
result = ResolveQualityPolicyV3(req, source)
|
|
if result.Height != 480 || result.Reason != "quality_fixed_rung" || hasDegradationWarningV3(result.Warnings, "bandwidth_cap_applied") {
|
|
t.Fatalf("fixed rung under cap = %#v", result)
|
|
}
|
|
|
|
req.QualityPreference = "auto"
|
|
result = ResolveQualityPolicyV3(req, source)
|
|
if !result.RequiresTranscode || result.Height != 720 || result.PreservesSource {
|
|
t.Fatalf("auto with cap = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestResolveQualityPolicyV3MeteredWithoutEvidencePrefersConservativeRung(t *testing.T) {
|
|
source := SourceDescriptorV3{Width: 3840, Height: 2160, BitrateKbps: 20_000}
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "auto"
|
|
req.Metered = true
|
|
result := ResolveQualityPolicyV3(req, source)
|
|
if result.Height != 720 || !result.RequiresTranscode || result.Reason != "quality_metered_limit" || result.ExplicitRung {
|
|
t.Fatalf("metered auto = %#v", result)
|
|
}
|
|
|
|
estimate := 30_000
|
|
req.BandwidthEstimateKbps = &estimate
|
|
result = ResolveQualityPolicyV3(req, source)
|
|
if !result.PreservesSource || result.Reason != "quality_bandwidth_limit" {
|
|
t.Fatalf("metered with estimate = %#v", result)
|
|
}
|
|
|
|
req.BandwidthEstimateKbps = nil
|
|
req.Metered = false
|
|
result = ResolveQualityPolicyV3(req, source)
|
|
if !result.PreservesSource || result.RequiresTranscode {
|
|
t.Fatalf("unmetered auto = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3HDRDeviceCapFallsBackToOriginalQuality(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "auto"
|
|
req.Capabilities.MaxResolution = "1080p"
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 || !result.Plan.Claims.Video.HDR10 {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
if result.Plan.EffectiveRecipe.Height == nil || *result.Plan.EffectiveRecipe.Height != 2160 {
|
|
t.Fatalf("recipe = %#v", result.Plan.EffectiveRecipe)
|
|
}
|
|
if !hasDegradationWarningV3(result.Plan.DegradationWarnings, "quality_reduction_unavailable") {
|
|
t.Fatalf("warnings = %#v", result.Plan.DegradationWarnings)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3HDRBandwidthCapFallsBackToOriginalQuality(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "auto"
|
|
cap := 5_000
|
|
req.BandwidthCapKbps = &cap
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 || !result.Plan.Claims.Video.HDR10 {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
if !hasDegradationWarningV3(result.Plan.DegradationWarnings, "quality_reduction_unavailable") {
|
|
t.Fatalf("warnings = %#v", result.Plan.DegradationWarnings)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3CapWithoutTranscodeRouteFallsBackToOriginal(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.Resolution = "1080p"
|
|
file.VideoTracks[0].Width = 1920
|
|
file.VideoTracks[0].Height = 1080
|
|
file.VideoTracks[0].Bitrate = 8_000
|
|
file.VideoTracks[0].VideoRange = "SDR"
|
|
file.VideoTracks[0].VideoRangeType = "SDR"
|
|
file.VideoTracks[0].ColorTransfer = "bt709"
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "original"
|
|
cap := 4_000
|
|
req.BandwidthCapKbps = &cap
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
// The client has no HLS engine, so the cap-induced transcode cannot run.
|
|
delete(req.ClientPlaybackContext.Engines, string(EngineMedia3HLSV3))
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
if !hasDegradationWarningV3(result.Plan.DegradationWarnings, "quality_reduction_unavailable") {
|
|
t.Fatalf("warnings = %#v", result.Plan.DegradationWarnings)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3LegacyHDRUnknownAssumesHDR10ForCapableClients(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.HDR = true
|
|
file.VideoTracks[0].VideoRange = ""
|
|
file.VideoTracks[0].VideoRangeType = ""
|
|
file.VideoTracks[0].ColorTransfer = ""
|
|
req := validStartRequestV3()
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.Capabilities.HDRDetails = &HDRCapabilitiesV3{HDR10: true}
|
|
req.ClientPlaybackContext.Output.HDRDetails = req.Capabilities.HDRDetails
|
|
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryOriginalHTTPV3 || !result.Plan.Claims.Video.HDR10 {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
if !hasDegradationWarningV3(result.Plan.DegradationWarnings, "hdr_range_assumed_hdr10") {
|
|
t.Fatalf("warnings = %#v", result.Plan.DegradationWarnings)
|
|
}
|
|
|
|
// Without HDR10 output support the legacy row keeps the previous
|
|
// (ineligible) behavior instead of guessing at the client's range.
|
|
req.Capabilities.HDRDetails = nil
|
|
req.ClientPlaybackContext.Output.HDRDetails = nil
|
|
result = PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Terminal == nil || result.Terminal.Reason != "hdr_transcode_unsupported" {
|
|
t.Fatalf("result = %s", ExplainPlannerResultV3(result))
|
|
}
|
|
}
|
|
|
|
func TestSourceDescriptorV3NormalizesLegacyFileBitrateFallback(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.VideoTracks[0].Bitrate = 0
|
|
file.Bitrate = 60_000_000 // legacy rows stored bps
|
|
|
|
source := SourceDescriptorFromFileV3(file, 0)
|
|
if source.BitrateKbps != 60_000 {
|
|
t.Fatalf("file-level bitrate = %d, want normalized 60000", source.BitrateKbps)
|
|
}
|
|
|
|
file.VideoTracks[0].Bitrate = 45_000_000
|
|
source = SourceDescriptorFromFileV3(file, 0)
|
|
if source.BitrateKbps != 45_000 {
|
|
t.Fatalf("track-level bitrate = %d, want normalized 45000", source.BitrateKbps)
|
|
}
|
|
}
|
|
|
|
func TestPlanAttemptedV3RequiresExactKeyMatch(t *testing.T) {
|
|
plan := PlanV3{PlanID: "plan:exact", Delivery: DeliveryOriginalHTTPV3, Stream: StreamV3{Protocol: StreamHTTPProgressiveV3, Container: "mkv"}, Subtitle: SubtitleDecisionV3{Mode: SubtitleOffV3}}
|
|
key := PlanAttemptKeyV3(plan, 1, nil)
|
|
if !planAttemptedV3(plan, 1, []string{" " + key + " "}) {
|
|
t.Fatal("whitespace-trimmed exact key must match")
|
|
}
|
|
if planAttemptedV3(plan, 1, []string{strings.ToUpper(key)}) {
|
|
t.Fatal("case-folded attempt key must not match an exact hash")
|
|
}
|
|
}
|
|
|
|
func TestStartRequestV3ValidationBoundsInnerLists(t *testing.T) {
|
|
longValue := strings.Repeat("x", 65)
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*StartRequestV3)
|
|
}{
|
|
{"video_decode_profile_count", func(r *StartRequestV3) {
|
|
r.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "h264", Hardware: true, Profiles: make([]string, 65)}}
|
|
}},
|
|
{"video_decode_profile_length", func(r *StartRequestV3) {
|
|
r.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "h264", Hardware: true, Profiles: []string{longValue}}}
|
|
}},
|
|
{"video_decode_level_count", func(r *StartRequestV3) {
|
|
r.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "h264", Hardware: true, Levels: make([]int, 65)}}
|
|
}},
|
|
{"video_decode_bit_depth_count", func(r *StartRequestV3) {
|
|
r.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "h264", Hardware: true, BitDepths: make([]int, 65)}}
|
|
}},
|
|
{"capability_dolby_vision_profiles", func(r *StartRequestV3) {
|
|
r.Capabilities.HDRDetails = &HDRCapabilitiesV3{DolbyVisionProfiles: make([]int, 17)}
|
|
}},
|
|
{"output_dolby_vision_profiles", func(r *StartRequestV3) {
|
|
r.ClientPlaybackContext.Output.HDRDetails = &HDRCapabilitiesV3{DolbyVisionProfiles: make([]int, 17)}
|
|
}},
|
|
{"engine_dolby_vision_profiles", func(r *StartRequestV3) {
|
|
engine := r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)]
|
|
engine.HDRDetails = &HDRCapabilitiesV3{DolbyVisionProfiles: make([]int, 17)}
|
|
r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)] = engine
|
|
}},
|
|
{"engine_container_length", func(r *StartRequestV3) {
|
|
engine := r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)]
|
|
engine.Containers = []string{longValue}
|
|
r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)] = engine
|
|
}},
|
|
{"engine_validated_claim_length", func(r *StartRequestV3) {
|
|
engine := r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)]
|
|
engine.ValidatedClaims = []string{longValue}
|
|
r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)] = engine
|
|
}},
|
|
{"engine_feature_length", func(r *StartRequestV3) {
|
|
engine := r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)]
|
|
engine.Features = []string{longValue}
|
|
r.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)] = engine
|
|
}},
|
|
{"audio_track_id_length", func(r *StartRequestV3) {
|
|
r.AudioTrackID = strings.Repeat("a", 129)
|
|
}},
|
|
{"subtitle_track_id_length", func(r *StartRequestV3) {
|
|
r.SubtitleTrackID = strings.Repeat("s", 129)
|
|
}},
|
|
}
|
|
for _, value := range cases {
|
|
t.Run(value.name, func(t *testing.T) {
|
|
req := validStartRequestV3()
|
|
value.mutate(&req)
|
|
if _, err := req.NormalizeAndValidate(); err == nil {
|
|
t.Fatal("oversized capability accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveQualityPolicyV3PreservesNonStandardSourceHeight(t *testing.T) {
|
|
req := validStartRequestV3()
|
|
req.QualityPreference = "auto"
|
|
result := ResolveQualityPolicyV3(req, SourceDescriptorV3{Width: 2560, Height: 1440, BitrateKbps: 12_000})
|
|
if !result.PreservesSource || result.RequiresTranscode || result.Width != 2560 || result.Height != 1440 || result.Label != "1440p" {
|
|
t.Fatalf("quality result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3SeekedHLSCopyPreservesVideo(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
file.Resolution = "1080p"
|
|
file.VideoTracks[0].Width = 1920
|
|
file.VideoTracks[0].Height = 1080
|
|
file.VideoTracks[0].BitDepth = 8
|
|
file.VideoTracks[0].VideoRange = "SDR"
|
|
file.VideoTracks[0].VideoRangeType = "SDR"
|
|
file.VideoTracks[0].ColorTransfer = "bt709"
|
|
req := validStartRequestV3()
|
|
start := 20.0
|
|
req.StartPosition = &start
|
|
req.ClientFeatures = append(req.ClientFeatures, FeatureDetailedDecodeV3)
|
|
req.ClientPlaybackContext.Features = append(req.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
req.Capabilities.Containers = []string{"mp4"}
|
|
req.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{8}, MaxWidth: 1920, MaxHeight: 1080, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
req.ClientPlaybackContext.Engines[string(EngineMedia3DirectV3)] = EngineCapabilityV3{}
|
|
req.ClientPlaybackContext.Engines[string(EngineMedia3ProgressiveRemuxV3)] = EngineCapabilityV3{}
|
|
result := PlanPlaybackV3(PlannerInputV3{Request: req, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0, Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3()})
|
|
if result.Plan == nil || result.Plan.Delivery != DeliveryRemuxHLSV3 || result.TargetVideoCodec != "copy" || result.Plan.Timeline.SourceStartSeconds != start {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestPlanPlaybackV3TimelineChangePreservesRouteIdentity(t *testing.T) {
|
|
file := detailedFixtureFileV3()
|
|
request := validStartRequestV3()
|
|
request.ClientFeatures = append(request.ClientFeatures, FeatureDetailedDecodeV3)
|
|
request.ClientPlaybackContext.Features = append(request.ClientPlaybackContext.Features, FeatureDetailedDecodeV3)
|
|
request.Capabilities.VideoDecode = []VideoDecodeCapabilityV3{{Codec: "hevc", Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10}, MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true}}
|
|
hdr := &HDRCapabilitiesV3{HDR10: true}
|
|
request.Capabilities.HDR = true
|
|
request.Capabilities.HDRDetails = hdr
|
|
request.ClientPlaybackContext.Output.HDRDetails = hdr
|
|
startAtZero := 0.0
|
|
request.StartPosition = &startAtZero
|
|
first := PlanPlaybackV3(PlannerInputV3{
|
|
Request: request, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0,
|
|
Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3(),
|
|
})
|
|
if first.Plan == nil {
|
|
t.Fatalf("initial plan = %#v", first)
|
|
}
|
|
|
|
startAtSeek := 321.25
|
|
request.StartPosition = &startAtSeek
|
|
second := PlanPlaybackV3(PlannerInputV3{
|
|
Request: request, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0,
|
|
Settings: PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}, Registry: testTransformationRegistryV3(),
|
|
})
|
|
if second.Plan == nil {
|
|
t.Fatalf("seeked plan = %#v", second)
|
|
}
|
|
if first.Plan.PlanID != second.Plan.PlanID ||
|
|
PlanAttemptKeyV3(*first.Plan, request.OutputRouteGeneration, nil) != PlanAttemptKeyV3(*second.Plan, request.OutputRouteGeneration, nil) {
|
|
t.Fatalf("timeline changed route identity: first=%#v second=%#v", first.Plan, second.Plan)
|
|
}
|
|
if first.Plan.Timeline.SourceStartSeconds != 0 || second.Plan.Timeline.SourceStartSeconds != startAtSeek {
|
|
t.Fatalf("timeline positions: first=%#v second=%#v", first.Plan.Timeline, second.Plan.Timeline)
|
|
}
|
|
}
|
|
|
|
func validStartRequestV3() StartRequestV3 {
|
|
return StartRequestV3{
|
|
ProtocolVersion: ProtocolV3,
|
|
ClientFeatures: []string{FeaturePlaybackPlanV3, FeatureMedia3Only},
|
|
FileID: 42,
|
|
ProfileID: "profile-1",
|
|
PlaybackAttemptID: "attempt-0001",
|
|
QualityPreference: "original",
|
|
SubtitleFidelityPreference: SubtitleFidelityCompatibleV3,
|
|
OutputRouteGeneration: 1,
|
|
Capabilities: ClientCodecCapabilitiesV3{CodecsVideo: []string{"hevc"}, CodecsAudio: []string{"aac"}, Containers: []string{"mkv"}, MaxResolution: "2160p"},
|
|
ClientPlaybackContext: ClientPlaybackContextV3{ProtocolVersion: ProtocolV3, Features: []string{FeaturePlaybackPlanV3, FeatureMedia3Only}, Platform: "android", FormFactor: "tv", AppVersion: "test", Output: OutputContextV3{OutputRouteGeneration: 1}, Engines: map[string]EngineCapabilityV3{
|
|
string(EngineMedia3DirectV3): {Enabled: true, SupportedOnDevice: true, Subtitles: EngineSubtitleCapabilitiesV3{EmbeddedText: true, SidecarText: true}},
|
|
string(EngineMedia3ProgressiveRemuxV3): {Enabled: true, SupportedOnDevice: true},
|
|
string(EngineMedia3HLSV3): {Enabled: true, SupportedOnDevice: true},
|
|
}},
|
|
}
|
|
}
|
|
|
|
func detailedFixtureFileV3() *models.MediaFile {
|
|
return &models.MediaFile{ID: 42, FilePath: "/media/movie.mkv", Container: "mkv", CodecVideo: "hevc", CodecAudio: "aac", Resolution: "2160p", Bitrate: 60_000, AudioChannels: 2, VideoTracks: []models.VideoTrack{{Codec: "hevc", Profile: "Main 10", Level: 153, Width: 3840, Height: 2160, FrameRate: "24000/1001", Bitrate: 60_000, BitDepth: 10, VideoRange: "HDR", VideoRangeType: "HDR10", ColorRange: "tv"}}, AudioTracks: []models.AudioTrack{{Codec: "aac", Channels: 2, Layout: "stereo"}}}
|
|
}
|
|
|
|
func testTransformationRegistryV3() *TransformationRegistryV3 {
|
|
return NewTransformationRegistryV3([]TransformationSpecV3{
|
|
{Name: "audio_to_aac", Available: true},
|
|
{Name: "video_to_h264", Available: true},
|
|
{Name: "server_dv7_to_hdr10", Available: true},
|
|
})
|
|
}
|