* fix(playback): restore Safari Dolby Vision remux * fix(playback): bound web Dolby Vision claims * fix(web): scope Dolby Vision output probes * fix(playback): constrain web Dolby Vision claims * fix(web): refresh playback after output changes * fix(playback): preserve output replan intent * fix(playback): model output capability changes * fix(web): disarm paused HLS startup guard * fix(web): probe progressive HDR10 support * fix(playback): preserve queued output state * fix(playback): bound output refresh evidence * fix(playback): preserve viable routes across replans * fix(playback): carry output evidence through queued replans
864 lines
43 KiB
Go
864 lines
43 KiB
Go
// Command playbackfixtures writes the protocol-v3 golden contract fixtures
|
|
// from the live Go types and planner.
|
|
//
|
|
// The direction of authority is inverted from where it started: the server
|
|
// defines the playback protocol, and clients prove conformance against these
|
|
// files. Android and Apple CI vendor them and compare their own decoders and
|
|
// opaque-token echo behavior against the values here, so a fixture is
|
|
// only trustworthy if it was produced by the same code that serves real
|
|
// traffic — hand-editing one would let the contract and the implementation
|
|
// drift apart silently.
|
|
//
|
|
// Usage:
|
|
//
|
|
// go run ./cmd/playbackfixtures -out internal/playback/testdata/protocol_v3
|
|
//
|
|
// `make playback-fixtures` runs it; `make verify-playback-fixtures`
|
|
// regenerates into a temporary directory and diffs, so a contract change
|
|
// cannot merge without refreshing what every client tests against.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
"github.com/Silo-Server/silo-server/internal/playback"
|
|
)
|
|
|
|
// goldenSessionID is a fixed UUID: fixtures must be byte-stable across runs,
|
|
// so nothing in this generator may read the clock or a random source.
|
|
const (
|
|
goldenSessionID = "11111111-1111-4111-8111-111111111111"
|
|
goldenAttemptID = "attempt-golden-0001"
|
|
goldenExpiresAt = "2030-01-01T00:00:00Z"
|
|
goldenMediaFileID = 42
|
|
// Codec and container tokens the fixtures are built from. Named so the
|
|
// capability lists, the plan recipe, and the source descriptor cannot drift
|
|
// into describing different media by a one-character typo.
|
|
codecH264 = "h264"
|
|
codecHEVC = "hevc"
|
|
codecAAC = "aac"
|
|
codecTrueHD = "truehd"
|
|
containerMP4 = "mp4"
|
|
containerMKV = "mkv"
|
|
containerHLS = "hls"
|
|
profileHigh = "high"
|
|
resolutionHD = "720p"
|
|
resolutionFHD = "1080p"
|
|
qualityOriginal = "original"
|
|
audioLayoutStereo = "stereo"
|
|
audioLayout71 = "7.1"
|
|
filmFrameRate = "24000/1001"
|
|
languageEnglish = "eng"
|
|
videoRangeSDR = "SDR"
|
|
categoryMidSeek = "mid_seek_replan"
|
|
)
|
|
|
|
func main() {
|
|
out := flag.String("out", "", "directory to write fixtures into (required)")
|
|
flag.Parse()
|
|
if *out == "" {
|
|
fail("usage: playbackfixtures -out <dir>")
|
|
}
|
|
if err := os.MkdirAll(*out, 0o755); err != nil {
|
|
fail("create %s: %v", *out, err)
|
|
}
|
|
|
|
write(*out, "start_request.json", goldenStartRequest())
|
|
write(*out, "replan_request.json", goldenReplanRequest())
|
|
write(*out, "decision_response.json", goldenDecisionResponse())
|
|
write(*out, "capability_response.json", goldenCapabilityResponse())
|
|
write(*out, "error_response.json", goldenErrorResponse())
|
|
write(*out, "route_event.json", goldenRouteEvent())
|
|
write(*out, "subtitle_inventory.json", goldenSubtitleInventory())
|
|
write(*out, "attempt_keys.json", goldenAttemptKeys())
|
|
write(*out, "conformance_matrix.json", goldenConformanceMatrix())
|
|
}
|
|
|
|
func goldenErrorResponse() playback.ErrorResponseV3 {
|
|
return playback.LegacyUpgradeErrorV3()
|
|
}
|
|
|
|
func write(dir, name string, value any) {
|
|
body, err := json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
fail("marshal %s: %v", name, err)
|
|
}
|
|
body = append(body, '\n')
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, body, 0o644); err != nil { //nolint:gosec // generated fixture
|
|
fail("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func fail(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// goldenCapabilities is the exact-evidence Android-shaped capability block the
|
|
// request fixtures share. Both request bodies carry the same capability
|
|
// contract, so they are built from one function rather than two drifting
|
|
// literals.
|
|
func goldenCapabilities() playback.ClientCodecCapabilitiesV3 {
|
|
return playback.ClientCodecCapabilitiesV3{
|
|
VideoEvidence: playback.EvidenceExactV3,
|
|
AudioEvidence: playback.EvidenceExactV3,
|
|
CodecsVideo: []string{codecH264},
|
|
CodecsVideoHardware: []string{codecH264},
|
|
CodecsAudio: []string{codecAAC},
|
|
Containers: []string{containerMP4},
|
|
MaxResolution: resolutionFHD,
|
|
VideoDecode: []playback.VideoDecodeCapabilityV3{{
|
|
Codec: codecH264,
|
|
Profiles: []string{profileHigh},
|
|
Levels: []int{41},
|
|
BitDepths: []int{8},
|
|
MaxWidth: 1920,
|
|
MaxHeight: 1080,
|
|
MaxFrameRate: 60,
|
|
MaxBitrateKbps: 20_000,
|
|
Hardware: true,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func goldenPlaybackContext() playback.ClientPlaybackContextV3 {
|
|
return playback.ClientPlaybackContextV3{
|
|
ProtocolVersion: playback.ProtocolV3,
|
|
FormFactor: "tv",
|
|
AppVersion: "3.0-test",
|
|
Device: playback.DeviceContextV3{
|
|
Platform: "android",
|
|
OSVersion: "15",
|
|
Manufacturer: "NVIDIA",
|
|
Model: "SHIELD Android TV",
|
|
// Everything platform-specific travels here as opaque bounded
|
|
// strings; the contract itself stays neutral.
|
|
PlatformDetails: map[string]string{"sdk_int": "35", "abis": "arm64-v8a"},
|
|
},
|
|
Output: playback.OutputContextV3{OutputContextID: "7"},
|
|
Deliveries: map[string]playback.DeliveryCapabilityV3{
|
|
playback.DeliveryClassOriginalHTTPV3: {
|
|
Enabled: true,
|
|
SupportedOnDevice: true,
|
|
Containers: []string{containerMP4},
|
|
VideoCodecs: []string{codecH264},
|
|
AudioDecodeCodecs: []string{codecAAC},
|
|
AudioPassthroughCodecs: []string{},
|
|
Subtitles: playback.DeliverySubtitleCapabilitiesV3{
|
|
EmbeddedText: true,
|
|
SidecarText: true,
|
|
},
|
|
Features: []string{},
|
|
AuthHeaderRefresh: true,
|
|
ValidatedClaims: []string{},
|
|
Transformations: []playback.TransformationV3{},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func goldenStartRequest() playback.StartRequestV3 {
|
|
start := 12.5
|
|
audioIndex := 0
|
|
return playback.StartRequestV3{
|
|
ProtocolVersion: playback.ProtocolV3,
|
|
ClientFeatures: []string{playback.FeaturePlaybackPlanV3},
|
|
FileID: goldenMediaFileID,
|
|
ProfileID: "profile-1",
|
|
PlaybackAttemptID: goldenAttemptID,
|
|
QualityPreference: playback.QualityOriginalV3,
|
|
SubtitleFidelityPreference: playback.SubtitleFidelityCompatibleV3,
|
|
StartPosition: &start,
|
|
ProgressPersistence: playback.ProgressPersistenceClientV3,
|
|
AudioTrackID: playback.TrackIDV3(goldenMediaFileID, "audio", 0),
|
|
AudioTrackIndex: &audioIndex,
|
|
Capabilities: goldenCapabilities(),
|
|
ClientPlaybackContext: goldenPlaybackContext(),
|
|
}
|
|
}
|
|
|
|
func goldenReplanRequest() playback.ReplanRequestV3 {
|
|
estimate := 3_500
|
|
cap := 4_000
|
|
audioIndex := 0
|
|
decision := goldenDecisionResponse()
|
|
if decision.PlaybackPlan == nil {
|
|
fail("golden decision has no plan")
|
|
}
|
|
plan := decision.PlaybackPlan
|
|
return playback.ReplanRequestV3{
|
|
ProtocolVersion: playback.ProtocolV3,
|
|
ClientFeatures: []string{playback.FeaturePlaybackPlanV3},
|
|
Operation: playback.ReplanOperationFailureRecoveryV3,
|
|
PlaybackAttemptID: goldenAttemptID,
|
|
ReplanRequestID: "replan-golden-0001",
|
|
FailedPlanID: plan.PlanID,
|
|
PlanAttemptID: "plan-attempt-golden-0001",
|
|
// Attempt keys are server-owned opaque tokens. A client echoes the
|
|
// values it was handed; it never computes one.
|
|
PlanAttemptKey: plan.PlanAttemptKey,
|
|
AttemptedPlanKeys: []string{plan.PlanAttemptKey},
|
|
AttemptCount: 1,
|
|
QualityPreference: "auto",
|
|
PositionSeconds: 42.5,
|
|
Metered: true,
|
|
BandwidthEstimateKbps: &estimate,
|
|
BandwidthCapKbps: &cap,
|
|
SelectedTracks: playback.SelectedTracksV3{
|
|
Audio: &playback.TrackIdentityV3{ID: playback.TrackIDV3(goldenMediaFileID, "audio", 0), Index: &audioIndex},
|
|
},
|
|
Failure: playback.FailureV3{Classification: "network_degraded"},
|
|
Capabilities: goldenCapabilities(),
|
|
ClientPlaybackContext: goldenPlaybackContext(),
|
|
}
|
|
}
|
|
|
|
// goldenMediaFile is the subtitle-bearing source the inventory fixtures
|
|
// describe. The embedded track list deliberately mixes a text track, a PGS
|
|
// track, and a DVD bitmap track: the DVD track has no sidecar shape the stream
|
|
// handler can serve, and pinning its ordinal is the point of the fixture.
|
|
func goldenMediaFile() *models.MediaFile {
|
|
return &models.MediaFile{
|
|
ID: goldenMediaFileID,
|
|
ExternalSubtitles: []models.ExternalSubtitle{
|
|
{Path: "/library/movie.en.srt", Language: languageEnglish, Format: "srt", Title: "English"},
|
|
},
|
|
SubtitleTracks: []models.SubtitleTrack{
|
|
{Index: 0, Language: languageEnglish, Codec: "ass", Title: "English (Signs)", Forced: true},
|
|
{Index: 1, Language: "jpn", Codec: "pgs", Title: "Japanese"},
|
|
{Index: 2, Language: "fre", Codec: "dvd_subtitle", Title: "French"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func goldenSubtitleAdditional() []playback.SubtitleInventoryEntryV3 {
|
|
return []playback.SubtitleInventoryEntryV3{
|
|
{Codec: "srt", Language: "spa", Label: "Spanish (downloaded)", Source: playback.SubtitleSourceDownloadedV3},
|
|
}
|
|
}
|
|
|
|
// subtitleInventoryFixture is a self-describing conformance vector: the inputs
|
|
// a client would receive for the same file, and the ordinals the server
|
|
// assigns to them. A client reproduces `inventory` from `source` and compares.
|
|
type subtitleInventoryFixture struct {
|
|
Description string `json:"description"`
|
|
SessionID string `json:"session_id"`
|
|
MediaFileID int `json:"media_file_id"`
|
|
Source subtitleInventorySource `json:"source"`
|
|
Inventory []playback.SubtitleInventoryItemV3 `json:"inventory"`
|
|
}
|
|
|
|
type subtitleInventorySource struct {
|
|
ExternalSubtitles []models.ExternalSubtitle `json:"external_subtitles"`
|
|
SubtitleTracks []models.SubtitleTrack `json:"subtitle_tracks"`
|
|
Downloaded []playback.SubtitleInventoryEntryV3 `json:"downloaded"`
|
|
}
|
|
|
|
func goldenSubtitleInventory() subtitleInventoryFixture {
|
|
file := goldenMediaFile()
|
|
additional := goldenSubtitleAdditional()
|
|
return subtitleInventoryFixture{
|
|
Description: "Combined subtitle ordinals are dense and gap-free across externals, embedded tracks, " +
|
|
"then downloaded tracks. A track with no sidecar representation keeps its ordinal and is " +
|
|
"published as burn_in_only without a URL rather than omitted.",
|
|
SessionID: goldenSessionID,
|
|
MediaFileID: goldenMediaFileID,
|
|
Source: subtitleInventorySource{
|
|
ExternalSubtitles: file.ExternalSubtitles,
|
|
SubtitleTracks: file.SubtitleTracks,
|
|
Downloaded: additional,
|
|
},
|
|
Inventory: playback.SubtitleInventoryV3(goldenSessionID, file, additional),
|
|
}
|
|
}
|
|
|
|
func goldenDecisionResponse() playback.DecisionResponseV3 {
|
|
file := conformanceFallbackFile()
|
|
subtitleSource := goldenMediaFile()
|
|
file.ExternalSubtitles = subtitleSource.ExternalSubtitles
|
|
file.SubtitleTracks = subtitleSource.SubtitleTracks
|
|
now, err := time.Parse(time.RFC3339, "2029-12-31T23:55:00Z")
|
|
if err != nil {
|
|
fail("parse golden planner time: %v", err)
|
|
}
|
|
result := playback.PlanPlaybackV3(playback.PlannerInputV3{
|
|
Request: goldenStartRequest(),
|
|
RequestedFile: file,
|
|
EffectiveFile: file,
|
|
AudioTrackIndex: 0,
|
|
Settings: playback.PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true},
|
|
Registry: conformanceRegistry(),
|
|
Now: now,
|
|
AdditionalSubtitles: goldenSubtitleAdditional(),
|
|
})
|
|
if result.Plan == nil {
|
|
fail("golden planner returned terminal: %#v", result.Terminal)
|
|
}
|
|
plan := *result.Plan
|
|
// These values belong to session/transport setup, which runs after the
|
|
// planner in the request handler. Keep the planner authoritative for every
|
|
// decision field and bind only the deterministic fixture runtime values.
|
|
plan.SessionID = goldenSessionID
|
|
plan.ExpiresAt = goldenExpiresAt
|
|
plan.Stream.URL = "/stream/" + goldenSessionID
|
|
plan.Subtitle.Inventory = playback.ScopeSubtitleInventoryV3(goldenSessionID, file, plan.Subtitle.Inventory)
|
|
|
|
return playback.DecisionResponseV3{
|
|
ProtocolVersion: playback.ProtocolV3,
|
|
ServerFeatures: playback.ServerFeaturesV3(),
|
|
Outcome: playback.OutcomePlayableV3,
|
|
SessionID: goldenSessionID,
|
|
PlaybackPlan: &plan,
|
|
}
|
|
}
|
|
|
|
func goldenCapabilityResponse() playback.CapabilityResponseV3 {
|
|
return playback.CapabilityResponseV3{
|
|
Enabled: true,
|
|
ProtocolVersions: []int{playback.ProtocolV3},
|
|
Features: playback.ServerFeaturesV3(),
|
|
Deliveries: []playback.DeliveryV3{
|
|
playback.DeliveryOriginalHTTPV3,
|
|
playback.DeliveryRemuxProgressiveV3,
|
|
playback.DeliveryRemuxHLSV3,
|
|
playback.DeliveryTranscodeHLSV3,
|
|
},
|
|
// A real server advertises only what its installed FFmpeg probed; the
|
|
// fixture pins the full set so a client sees every shape it must parse.
|
|
Transformations: []playback.TransformationV3{
|
|
{Name: playback.TransformationAudioToAACV3, Executor: playback.ExecutorServerV3, RecipeVersion: "1", ValidatedClaims: []string{playback.ClaimAudioDecodeV3}},
|
|
{Name: playback.TransformationServerDV7HDR10V3, Executor: playback.ExecutorServerV3, RecipeVersion: "1", ValidatedClaims: playback.DV7ToHDR10ClaimsV3()},
|
|
{Name: playback.TransformationVideoToH264V3, Executor: playback.ExecutorServerV3, RecipeVersion: playback.TransformationVideoToH264RecipeVersionV3, ValidatedClaims: []string{playback.ClaimH264DecodeV3}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func goldenRouteEvent() playback.RouteEventV3 {
|
|
decision := goldenDecisionResponse()
|
|
if decision.PlaybackPlan == nil {
|
|
fail("golden decision has no plan")
|
|
}
|
|
plan := decision.PlaybackPlan
|
|
return playback.RouteEventV3{
|
|
ProtocolVersion: playback.ProtocolV3,
|
|
PlaybackAttemptID: goldenAttemptID,
|
|
SessionID: goldenSessionID,
|
|
PlanID: plan.PlanID,
|
|
PlanAttemptID: "plan-attempt-golden-0001",
|
|
PlanAttemptKey: plan.PlanAttemptKey,
|
|
Event: playback.RouteEventFirstFrameV3,
|
|
OutputContextID: "7",
|
|
Diagnostics: map[string]string{
|
|
"decoder_name": "c2.android.avc.decoder",
|
|
"first_frame_ms": "412",
|
|
"video_mime": "video/avc",
|
|
},
|
|
}
|
|
}
|
|
|
|
// attemptKeyInput exists only inside the Go fixture generator. The inputs to
|
|
// the server's identity function are deliberately absent from the generated
|
|
// JSON: clients receive a token and prove only that they echo it unchanged.
|
|
type attemptKeyInput struct {
|
|
Name string `json:"name"`
|
|
PlanID string `json:"plan_id"`
|
|
Delivery playback.DeliveryV3 `json:"delivery"`
|
|
StreamProtocol playback.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 playback.SubtitleModeV3 `json:"subtitle_mode"`
|
|
Transformations []playback.TransformationV3 `json:"transformations"`
|
|
AppliedQuirks []playback.AppliedQuirkV3 `json:"applied_quirks,omitempty"`
|
|
RuntimeCorrections []string `json:"runtime_corrections,omitempty"`
|
|
OutputContextID string `json:"output_context_id"`
|
|
LocalMutations []string `json:"local_mutations"`
|
|
}
|
|
|
|
func (f attemptKeyInput) plan() playback.PlanV3 {
|
|
width, height, bitrate := f.Width, f.Height, f.BitrateKbps
|
|
return playback.PlanV3{
|
|
PlanID: f.PlanID,
|
|
Delivery: f.Delivery,
|
|
Stream: playback.StreamV3{Protocol: f.StreamProtocol, Container: f.Container},
|
|
EffectiveRecipe: playback.EffectiveRecipeV3{
|
|
VideoCodec: f.VideoCodec,
|
|
AudioCodec: f.AudioCodec,
|
|
Width: &width,
|
|
Height: &height,
|
|
BitrateKbps: &bitrate,
|
|
DynamicRange: f.DynamicRange,
|
|
},
|
|
Subtitle: playback.SubtitleDecisionV3{Mode: f.SubtitleMode},
|
|
Transformations: f.Transformations,
|
|
AppliedQuirks: f.AppliedQuirks,
|
|
RuntimeCorrections: f.RuntimeCorrections,
|
|
}
|
|
}
|
|
|
|
type opaqueAttemptKeyFixture struct {
|
|
Name string `json:"name"`
|
|
ServerPlanAttemptKey string `json:"server_plan_attempt_key"`
|
|
ReplanEcho string `json:"replan_echo"`
|
|
AttemptedPlanKeys []string `json:"attempted_plan_keys"`
|
|
ExpectedServerAction string `json:"expected_server_action"`
|
|
}
|
|
|
|
func goldenAttemptKeys() []opaqueAttemptKeyFixture {
|
|
inputs := []attemptKeyInput{
|
|
{
|
|
Name: "hls_burn_in_sorted_transformations_and_pcm_mutations",
|
|
PlanID: "plan:fixture",
|
|
Delivery: playback.DeliveryRemuxHLSV3,
|
|
StreamProtocol: playback.StreamHLSV3,
|
|
Container: containerHLS,
|
|
VideoCodec: codecHEVC,
|
|
AudioCodec: codecAAC,
|
|
Width: 3840,
|
|
Height: 2160,
|
|
BitrateKbps: 20_000,
|
|
DynamicRange: playback.DynamicRangeHDR10V3,
|
|
SubtitleMode: playback.SubtitleBurnInV3,
|
|
// Deliberately out of order, and deliberately naming a
|
|
// transformation the registry no longer defines: the key preimage
|
|
// sorts its inputs and never consults the registry, and pinning a
|
|
// retired name proves both properties stay true.
|
|
Transformations: []playback.TransformationV3{
|
|
{Name: "hdr_to_sdr_tonemap", Executor: playback.ExecutorServerV3, RecipeVersion: "1", ValidatedClaims: []string{}},
|
|
{Name: playback.TransformationAudioToAACV3, Executor: playback.ExecutorServerV3, RecipeVersion: "1", ValidatedClaims: []string{}},
|
|
},
|
|
OutputContextID: "7",
|
|
LocalMutations: []string{"transport_reopen", "pcm:truehd:8"},
|
|
},
|
|
{
|
|
Name: "direct_client_dv81_executor_and_version",
|
|
PlanID: "plan:dv81-fixture",
|
|
Delivery: playback.DeliveryOriginalHTTPV3,
|
|
StreamProtocol: playback.StreamHTTPProgressiveV3,
|
|
Container: containerMKV,
|
|
VideoCodec: codecHEVC,
|
|
AudioCodec: codecTrueHD,
|
|
Width: 3840,
|
|
Height: 2160,
|
|
BitrateKbps: 65_000,
|
|
DynamicRange: playback.DynamicRangeDolbyVisionV3,
|
|
SubtitleMode: playback.SubtitleOffV3,
|
|
Transformations: []playback.TransformationV3{
|
|
{Name: playback.ClientDV7ToDV81V3, Executor: playback.ExecutorClientV3, RecipeVersion: playback.ClientDVTransformVersionV3, ValidatedClaims: []string{}},
|
|
},
|
|
OutputContextID: "9",
|
|
LocalMutations: []string{},
|
|
},
|
|
{
|
|
Name: "direct_device_quirk_and_runtime_correction_identity",
|
|
PlanID: "plan:quirk",
|
|
Delivery: playback.DeliveryOriginalHTTPV3,
|
|
StreamProtocol: playback.StreamHTTPProgressiveV3,
|
|
Container: containerMKV,
|
|
VideoCodec: codecHEVC,
|
|
AudioCodec: "eac3",
|
|
Width: 3840,
|
|
Height: 2160,
|
|
BitrateKbps: 60_000,
|
|
DynamicRange: playback.DynamicRangeDolbyVisionV3,
|
|
SubtitleMode: playback.SubtitleOffV3,
|
|
Transformations: []playback.TransformationV3{},
|
|
// The private server implementation includes quirk and runtime
|
|
// correction identity; clients see only its opaque result.
|
|
AppliedQuirks: []playback.AppliedQuirkV3{{
|
|
ID: playback.QuirkFireTVDV8HDR10PlusV3,
|
|
RegistryRevision: playback.DeviceQuirkRegistryRevisionV3,
|
|
Action: "client_runtime_correction",
|
|
}},
|
|
RuntimeCorrections: []string{playback.ClientDV8HDR10PlusSanitizerV3},
|
|
OutputContextID: "9",
|
|
LocalMutations: []string{},
|
|
},
|
|
}
|
|
fixtures := make([]opaqueAttemptKeyFixture, 0, len(inputs))
|
|
for _, input := range inputs {
|
|
key := playback.PlanAttemptKeyV3(input.plan(), input.OutputContextID, input.LocalMutations)
|
|
fixtures = append(fixtures, opaqueAttemptKeyFixture{
|
|
Name: input.Name,
|
|
ServerPlanAttemptKey: key,
|
|
ReplanEcho: key,
|
|
AttemptedPlanKeys: []string{key},
|
|
ExpectedServerAction: "reject_already_attempted_plan",
|
|
})
|
|
}
|
|
return fixtures
|
|
}
|
|
|
|
func goldenConformanceMatrix() playback.ConformanceMatrixV3 {
|
|
videoFile := conformanceVideoFile()
|
|
base := conformanceStartRequest()
|
|
registry := conformanceRegistry()
|
|
settings := playback.PlannerSettingsV3{TranscodeEnabled: true, Allow4KTranscode: true}
|
|
|
|
planner := make([]playback.PlannerScenarioV3, 0, 18)
|
|
for _, tier := range []playback.CapabilityEvidenceV3{
|
|
playback.EvidenceExactV3,
|
|
playback.EvidencePlatformAttestedV3,
|
|
playback.EvidenceDeclaredV3,
|
|
} {
|
|
request := base
|
|
request.PlaybackAttemptID = "attempt-evidence-" + string(tier)
|
|
request.Capabilities.VideoEvidence = tier
|
|
if tier == playback.EvidenceDeclaredV3 {
|
|
request.Capabilities.VideoDecode = nil
|
|
}
|
|
planner = append(planner, makePlannerScenario(
|
|
"evidence_"+string(tier), "evidence_tier_gating", request, videoFile, nil, settings, registry,
|
|
))
|
|
}
|
|
|
|
fallbackRequest := conformanceStartRequest()
|
|
fallbackRequest.Capabilities.VideoEvidence = playback.EvidenceDeclaredV3
|
|
fallbackRequest.Capabilities.VideoDecode = nil
|
|
fallbackRequest.Capabilities.CodecsVideo = []string{codecH264}
|
|
fallbackRequest.Capabilities.CodecsVideoHardware = []string{codecH264}
|
|
fallbackRequest.Capabilities.Containers = []string{containerMP4}
|
|
fallbackFile := conformanceFallbackFile()
|
|
var attempted []string
|
|
for _, name := range []string{qualityOriginal, "progressive", containerHLS} {
|
|
request := fallbackRequest
|
|
request.PlaybackAttemptID = "attempt-delivery-chain"
|
|
scenario := makePlannerScenario("delivery_"+name, "deliveries_negotiation", request, fallbackFile, attempted, settings, registry)
|
|
planner = append(planner, scenario)
|
|
attempted = append(attempted, scenario.Expected.PlanAttemptKey)
|
|
}
|
|
transcodeRequest := fallbackRequest
|
|
transcodeRequest.PlaybackAttemptID = "attempt-delivery-transcode"
|
|
transcodeRequest.QualityPreference = resolutionHD
|
|
planner = append(planner, makePlannerScenario("delivery_transcode", "deliveries_negotiation", transcodeRequest, fallbackFile, nil, settings, registry))
|
|
|
|
audioFile := &models.MediaFile{ID: 77, BaseType: "audiobook", FilePath: "/media/audiobook.m4b", Container: containerMP4, CodecAudio: codecAAC, Bitrate: 128, AudioChannels: 2, Duration: 39_600, AudioTracks: []models.AudioTrack{{Codec: codecAAC, Channels: 2, Layout: audioLayoutStereo}}}
|
|
audioRequest := conformanceStartRequest()
|
|
audioRequest.FileID = audioFile.ID
|
|
audioRequest.PlaybackAttemptID = "attempt-audio-only"
|
|
audioRequest.Capabilities.Containers = []string{containerMP4}
|
|
planner = append(planner, makePlannerScenario("audio_only_original", "audio_only_planning", audioRequest, audioFile, nil, settings, registry))
|
|
|
|
hdr10Request := conformanceHDRRequest()
|
|
hdr10Request.PlaybackAttemptID = "attempt-hdr10-direct"
|
|
planner = append(planner, makePlannerScenario("hdr10_exact_direct", "hdr_dv_matrix", hdr10Request, conformanceHDRFile(), nil, settings, registry))
|
|
|
|
dv8File := conformanceHDRFile()
|
|
dv8File.VideoTracks[0].DVProfile = 8
|
|
dv8File.VideoTracks[0].DVBLCompatID = 1
|
|
dv8File.VideoTracks[0].VideoRange = "DolbyVision"
|
|
dv8File.VideoTracks[0].VideoRangeType = "DOVIWithHDR10"
|
|
dv8Request := conformanceHDRRequest()
|
|
dv8Request.PlaybackAttemptID = "attempt-dv8-direct"
|
|
dv8Request.Capabilities.HDRDetails.DolbyVisionProfiles = []int{8}
|
|
dv8Request.ClientPlaybackContext.Output.HDRDetails.DolbyVisionProfiles = []int{8}
|
|
planner = append(planner, makePlannerScenario("dolby_vision_8_exact_direct", "hdr_dv_matrix", dv8Request, dv8File, nil, settings, registry))
|
|
|
|
dv7File := conformanceHDRFile()
|
|
dv7File.VideoTracks[0].DVProfile = 7
|
|
dv7File.VideoTracks[0].DVBLCompatID = 6
|
|
dv7File.VideoTracks[0].VideoRange = "DolbyVision"
|
|
dv7File.VideoTracks[0].VideoRangeType = "DOVIWithEL"
|
|
dv7Request := conformanceHDRRequest()
|
|
dv7Request.PlaybackAttemptID = "attempt-dv7-hdr10"
|
|
dv7Registry := playback.NewTransformationRegistryV3([]playback.TransformationSpecV3{{Name: playback.TransformationServerDV7HDR10V3, RecipeVersion: "1", Available: true}})
|
|
planner = append(planner, makePlannerScenario("dolby_vision_7_hdr10_fallback", "hdr_dv_matrix", dv7Request, dv7File, nil, settings, dv7Registry))
|
|
|
|
audioAdaptFile := conformanceHDRFile()
|
|
audioAdaptFile.CodecAudio = codecTrueHD
|
|
audioAdaptFile.AudioChannels = 8
|
|
audioAdaptFile.AudioTracks[0] = models.AudioTrack{Codec: codecTrueHD, Channels: 8, Layout: audioLayout71}
|
|
audioAdaptRequest := conformanceHDRRequest()
|
|
audioAdaptRequest.PlaybackAttemptID = "attempt-truehd-aac"
|
|
planner = append(planner, makePlannerScenario("truehd_audio_conversion", "audio_matrix", audioAdaptRequest, audioAdaptFile, nil, settings, registry))
|
|
|
|
audioPassthroughFile := conformanceHDRFile()
|
|
audioPassthroughFile.CodecAudio = codecTrueHD
|
|
audioPassthroughFile.AudioChannels = 8
|
|
audioPassthroughFile.AudioTracks[0] = models.AudioTrack{Codec: codecTrueHD, Channels: 8, Layout: audioLayout71}
|
|
audioPassthroughRequest := conformanceHDRRequest()
|
|
audioPassthroughRequest.PlaybackAttemptID = "attempt-truehd-passthrough"
|
|
audioPassthroughRequest.ClientFeatures = append(audioPassthroughRequest.ClientFeatures, playback.FeatureLayoutPassthrough)
|
|
audioPassthroughRequest.Capabilities.CodecsAudio = []string{codecTrueHD}
|
|
audioPassthroughRequest.Capabilities.AudioPassthrough = &playback.AudioPassthroughV3{
|
|
PassthroughCodecs: []string{codecTrueHD}, MaxChannels: 8,
|
|
Entries: []playback.AudioPassthroughEntryV3{{Codec: codecTrueHD, ChannelCounts: []int{8}, Layouts: []string{audioLayout71}}},
|
|
}
|
|
originalDelivery := audioPassthroughRequest.ClientPlaybackContext.Deliveries[playback.DeliveryClassOriginalHTTPV3]
|
|
originalDelivery.AudioPassthroughCodecs = []string{codecTrueHD}
|
|
audioPassthroughRequest.ClientPlaybackContext.Deliveries[playback.DeliveryClassOriginalHTTPV3] = originalDelivery
|
|
planner = append(planner, makePlannerScenario("truehd_exact_layout_passthrough", "audio_matrix", audioPassthroughRequest, audioPassthroughFile, nil, settings, registry))
|
|
|
|
pgsFile := conformanceHDRFile()
|
|
pgsFile.SubtitleTracks = []models.SubtitleTrack{{Index: 4, Language: "jpn", Codec: "hdmv_pgs_subtitle", Title: "Japanese"}}
|
|
pgsRequest := conformanceHDRRequest()
|
|
pgsRequest.PlaybackAttemptID = "attempt-pgs-sidecar"
|
|
pgsIndex := 0
|
|
pgsRequest.SubtitleTrackIndex = &pgsIndex
|
|
pgsRequest.SubtitleTrackID = playback.TrackIDV3(pgsFile.ID, "subtitle", pgsIndex)
|
|
for _, deliveryClass := range []string{playback.DeliveryClassOriginalHTTPV3, playback.DeliveryClassProgressiveV3, playback.DeliveryClassHLSV3} {
|
|
delivery := pgsRequest.ClientPlaybackContext.Deliveries[deliveryClass]
|
|
delivery.Subtitles.EmbeddedBitmap = true
|
|
pgsRequest.ClientPlaybackContext.Deliveries[deliveryClass] = delivery
|
|
}
|
|
planner = append(planner, makePlannerScenario("embedded_pgs_sidecar", "subtitle_matrix", pgsRequest, pgsFile, nil, settings, registry))
|
|
|
|
assFile := conformanceHDRFile()
|
|
assFile.SubtitleTracks = []models.SubtitleTrack{{Index: 4, Language: languageEnglish, Codec: "ass", Title: "English Signs"}}
|
|
assRequest := conformanceHDRRequest()
|
|
assRequest.PlaybackAttemptID = "attempt-ass-authored"
|
|
assRequest.SubtitleFidelityPreference = playback.SubtitleFidelityPreserveV3
|
|
assIndex := 0
|
|
assRequest.SubtitleTrackIndex = &assIndex
|
|
assRequest.SubtitleTrackID = playback.TrackIDV3(assFile.ID, "subtitle", assIndex)
|
|
for _, deliveryClass := range []string{playback.DeliveryClassOriginalHTTPV3, playback.DeliveryClassProgressiveV3, playback.DeliveryClassHLSV3} {
|
|
delivery := assRequest.ClientPlaybackContext.Deliveries[deliveryClass]
|
|
delivery.Subtitles.EmbeddedText = true
|
|
delivery.Subtitles.ASSStyling = true
|
|
delivery.Subtitles.FontAttachments = true
|
|
assRequest.ClientPlaybackContext.Deliveries[deliveryClass] = delivery
|
|
}
|
|
planner = append(planner, makePlannerScenario("embedded_ass_authored_render", "subtitle_matrix", assRequest, assFile, nil, settings, registry))
|
|
|
|
dvdFile := conformanceVideoFile()
|
|
dvdFile.SubtitleTracks = []models.SubtitleTrack{{Index: 4, Language: languageEnglish, Codec: "dvd_subtitle", Title: "English"}}
|
|
dvdRequest := conformanceStartRequest()
|
|
dvdRequest.PlaybackAttemptID = "attempt-dvd-burn-in"
|
|
dvdIndex := 0
|
|
dvdRequest.SubtitleTrackIndex = &dvdIndex
|
|
dvdRequest.SubtitleTrackID = playback.TrackIDV3(dvdFile.ID, "subtitle", dvdIndex)
|
|
planner = append(planner, makePlannerScenario("embedded_dvd_burn_in", "subtitle_matrix", dvdRequest, dvdFile, nil, settings, registry))
|
|
|
|
qualityRequest := fallbackRequest
|
|
qualityRequest.PlaybackAttemptID = "attempt-available-qualities"
|
|
qualityRequest.QualityPreference = qualityOriginal
|
|
planner = append(planner, makePlannerScenario("available_qualities", "available_qualities", qualityRequest, fallbackFile, nil, settings, registry))
|
|
|
|
decision := goldenDecisionResponse()
|
|
plan := decision.PlaybackPlan
|
|
if plan == nil {
|
|
fail("golden decision has no plan")
|
|
}
|
|
trackIndex := 1
|
|
trackChange := goldenReplanRequest()
|
|
trackChange.Operation = playback.ReplanOperationTrackChangeV3
|
|
trackChange.ReplanRequestID = "replan-track-change-0001"
|
|
trackChange.Failure = playback.FailureV3{}
|
|
trackChange.SelectedTracks.Audio = &playback.TrackIdentityV3{ID: "", Index: &trackIndex}
|
|
qualityChange := goldenReplanRequest()
|
|
qualityChange.Operation = playback.ReplanOperationQualityChangeV3
|
|
qualityChange.ReplanRequestID = "replan-quality-change-0001"
|
|
qualityChange.Failure = playback.FailureV3{}
|
|
qualityChange.QualityPreference = resolutionHD
|
|
outputChange := goldenReplanRequest()
|
|
outputChange.Operation = playback.ReplanOperationOutputChangeV3
|
|
outputChange.ReplanRequestID = "replan-output-change-0001"
|
|
outputChange.Failure = playback.FailureV3{}
|
|
seekReanchor := goldenReplanRequest()
|
|
seekReanchor.Operation = playback.ReplanOperationSeekReanchorV3
|
|
seekReanchor.ReplanRequestID = "replan-seek-reanchor-0001"
|
|
seekReanchor.Failure = playback.FailureV3{}
|
|
seekReanchor.PositionSeconds = 321.25
|
|
|
|
trackChange.PositionSeconds = 321.25
|
|
qualityChange.PositionSeconds = 321.25
|
|
trackDuplicate := trackChange
|
|
trackDuplicate.ReplanRequestID = "replan-track-duplicate-0001"
|
|
qualityDuplicate := qualityChange
|
|
qualityDuplicate.ReplanRequestID = "replan-quality-duplicate-0001"
|
|
trackConcurrent := trackChange
|
|
trackConcurrent.ReplanRequestID = "replan-track-concurrent-0001"
|
|
qualityConcurrent := qualityChange
|
|
qualityConcurrent.ReplanRequestID = "replan-quality-concurrent-0001"
|
|
trackMidSeek := trackChange
|
|
trackMidSeek.ReplanRequestID = "replan-track-mid-seek-0001"
|
|
qualityMidSeek := qualityChange
|
|
qualityMidSeek.ReplanRequestID = "replan-quality-mid-seek-0001"
|
|
replans := []playback.ReplanScenarioV3{
|
|
{Name: "track_change", Category: "track_change_replan", Request: trackChange, Expected: playback.ReplanExpectationV3{HTTPStatus: http.StatusOK, PreserveUnmodifiedTracks: true}},
|
|
{Name: "quality_change", Category: "quality_change_replan", Request: qualityChange, Expected: playback.ReplanExpectationV3{HTTPStatus: http.StatusOK, SelectedQuality: resolutionHD}},
|
|
{Name: "output_change", Category: "output_change_replan", Request: outputChange, Expected: playback.ReplanExpectationV3{HTTPStatus: http.StatusOK, PreserveUnmodifiedTracks: true}},
|
|
{Name: "track_change_idempotent_duplicate", Category: "idempotent_replan", Request: trackDuplicate, Expected: playback.ReplanExpectationV3{SameRequestAndBodyStatus: http.StatusOK, ResponseReplayedVerbatim: true, ChangedBodyStatus: http.StatusConflict, ChangedBodyError: "idempotency_key_reused"}},
|
|
{Name: "quality_change_idempotent_duplicate", Category: "idempotent_replan", Request: qualityDuplicate, Expected: playback.ReplanExpectationV3{SameRequestAndBodyStatus: http.StatusOK, ResponseReplayedVerbatim: true, ChangedBodyStatus: http.StatusConflict, ChangedBodyError: "idempotency_key_reused"}},
|
|
{Name: "track_change_concurrent_duplicate", Category: "concurrent_replan", Request: trackConcurrent, Expected: playback.ReplanExpectationV3{WhileFirstLeaseActiveStatus: http.StatusConflict, ConcurrentError: "replan_in_progress", AfterCompletionStatus: http.StatusOK, ResponseReplayedVerbatim: true}},
|
|
{Name: "quality_change_concurrent_duplicate", Category: "concurrent_replan", Request: qualityConcurrent, Expected: playback.ReplanExpectationV3{WhileFirstLeaseActiveStatus: http.StatusConflict, ConcurrentError: "replan_in_progress", AfterCompletionStatus: http.StatusOK, ResponseReplayedVerbatim: true}},
|
|
{Name: "track_change_mid_seek", Category: categoryMidSeek, Request: trackMidSeek, Expected: playback.ReplanExpectationV3{HTTPStatus: http.StatusOK, PositionSeconds: trackMidSeek.PositionSeconds, PositionPreserved: true}},
|
|
{Name: "quality_change_mid_seek", Category: categoryMidSeek, Request: qualityMidSeek, Expected: playback.ReplanExpectationV3{HTTPStatus: http.StatusOK, PositionSeconds: qualityMidSeek.PositionSeconds, PositionPreserved: true}},
|
|
{Name: "mid_seek_reanchor", Category: categoryMidSeek, Request: seekReanchor, Expected: playback.ReplanExpectationV3{HTTPStatus: http.StatusOK, PositionSeconds: seekReanchor.PositionSeconds, PositionPreserved: true}},
|
|
}
|
|
|
|
outputA := playback.PlanAttemptKeyV3(*plan, "output-a", nil)
|
|
outputB := playback.PlanAttemptKeyV3(*plan, "output-b", nil)
|
|
if outputA == outputB {
|
|
fail("output context must change the opaque plan attempt key")
|
|
}
|
|
recovery := goldenReplanRequest()
|
|
recovery.ReplanRequestID = "replan-failure-matrix-0001"
|
|
recovery.PositionSeconds = 321.25
|
|
subtitleIndex := 2
|
|
recovery.SelectedTracks.Subtitle = &playback.TrackIdentityV3{
|
|
ID: playback.TrackIDV3(goldenMediaFileID, "subtitle", subtitleIndex),
|
|
Index: &subtitleIndex,
|
|
}
|
|
restartStart := goldenStartRequest()
|
|
restartStart.PlaybackAttemptID = "attempt-restart-terminal"
|
|
restartTerminal := playback.NewTerminalResponseV3(
|
|
"transcode_start_failed",
|
|
"The playback transport did not become ready in time.",
|
|
true,
|
|
)
|
|
capacityStart := goldenStartRequest()
|
|
capacityStart.PlaybackAttemptID = "attempt-capacity-unavailable"
|
|
capacityAvailable := false
|
|
zeroCapacityDelta := 0
|
|
limitEvent := goldenRouteEvent()
|
|
limitEvent.PlaybackAttemptID = "attempt-route-limit"
|
|
limitEvent.Diagnostics = make(map[string]string, 33)
|
|
for index := range 33 {
|
|
limitEvent.Diagnostics[fmt.Sprintf("diagnostic_%02d", index)] = "value"
|
|
}
|
|
draftProtocolVersion := playback.ProtocolV3
|
|
protocol := []playback.ProtocolScenarioV3{
|
|
{Name: "legacy_start_requires_upgrade", Category: "legacy_426", Input: playback.ProtocolScenarioInputV3{LegacyStartBody: &playback.LegacyStartBodyV3{FileID: goldenMediaFileID}}, Expected: playback.ProtocolExpectationV3{HTTPStatus: http.StatusUpgradeRequired, Error: "client_upgrade_required"}},
|
|
{Name: "draft_v3_start_requires_upgrade", Category: "draft_v3_426", Input: playback.ProtocolScenarioInputV3{LegacyStartBody: &playback.LegacyStartBodyV3{ProtocolVersion: &draftProtocolVersion, FileID: goldenMediaFileID, ClientCapabilities: &playback.DraftClientCapabilitiesV3{CodecsVideo: []string{codecH264}}}}, Expected: playback.ProtocolExpectationV3{HTTPStatus: http.StatusUpgradeRequired, Error: "client_upgrade_required"}},
|
|
{Name: "output_context_change_invalidates_attempt", Category: "output_context_invalidation", Input: playback.ProtocolScenarioInputV3{PlanID: plan.PlanID, FirstOutputContextID: "output-a", SecondOutputContextID: "output-b", FirstPlanAttemptKey: outputA, SecondPlanAttemptKey: outputB}, Expected: playback.ProtocolExpectationV3{PlanIDUnchanged: true, PlanAttemptKeyChanged: true}},
|
|
{Name: "opaque_attempt_key_loop", Category: "attempt_key_echo_and_loop", Input: playback.ProtocolScenarioInputV3{ServerPlanAttemptKey: plan.PlanAttemptKey, ReplanEcho: plan.PlanAttemptKey, AttemptedPlanKeys: []string{plan.PlanAttemptKey}}, Expected: playback.ProtocolExpectationV3{Action: "reject_already_attempted_plan"}},
|
|
{Name: "failure_recovery_preserves_intent", Category: "recovery_matrix", Input: playback.ProtocolScenarioInputV3{ReplanRequest: &recovery}, Expected: playback.ProtocolExpectationV3{HTTPStatus: http.StatusOK, SelectionPreserved: true, PositionPreserved: true, Action: "preserve_selected_tracks_and_position"}},
|
|
{Name: "restart_replays_terminal_attempt", Category: "restart_matrix", Input: playback.ProtocolScenarioInputV3{StartRequest: &restartStart, PersistedDecision: &restartTerminal, Restarted: true}, Expected: playback.ProtocolExpectationV3{HTTPStatus: http.StatusCreated, Outcome: playback.OutcomeAdaptationUnavailableV3, TerminalReason: "transcode_start_failed", ResponseReplayedVerbatim: true, CapacityDelta: &zeroCapacityDelta}},
|
|
{Name: "capacity_unavailable_cleans_up", Category: "capacity_matrix", Input: playback.ProtocolScenarioInputV3{StartRequest: &capacityStart, CapacityAvailable: &capacityAvailable}, Expected: playback.ProtocolExpectationV3{HTTPStatus: http.StatusCreated, Outcome: playback.OutcomeAdaptationUnavailableV3, TerminalReason: "capacity_unavailable", CapacityDelta: &zeroCapacityDelta, CleanupComplete: true}},
|
|
{Name: "route_event_diagnostic_limit", Category: "route_event_limits", Input: playback.ProtocolScenarioInputV3{RouteEvent: &limitEvent}, Expected: playback.ProtocolExpectationV3{HTTPStatus: http.StatusBadRequest, Error: "bad_request", Action: "reject_without_persisting"}},
|
|
}
|
|
|
|
return playback.ConformanceMatrixV3{SchemaVersion: 1, Planner: planner, Replans: replans, Protocol: protocol}
|
|
}
|
|
|
|
func makePlannerScenario(name, category string, request playback.StartRequestV3, file *models.MediaFile, attempted []string, settings playback.PlannerSettingsV3, registry *playback.TransformationRegistryV3) playback.PlannerScenarioV3 {
|
|
result := playback.PlanPlaybackV3(playback.PlannerInputV3{
|
|
Request: request, RequestedFile: file, EffectiveFile: file, AudioTrackIndex: 0,
|
|
Settings: settings, Registry: registry, AttemptedKeys: attempted,
|
|
})
|
|
expected := playback.PlannerExpectationV3{Outcome: playback.OutcomeAdaptationUnavailableV3}
|
|
if result.Plan != nil {
|
|
selectedTracks := result.Plan.SelectedTracks
|
|
subtitle := result.Plan.Subtitle
|
|
claims := result.Plan.Claims
|
|
expected = playback.PlannerExpectationV3{
|
|
Outcome: playback.OutcomePlayableV3,
|
|
Delivery: result.Plan.Delivery,
|
|
DecisionReason: result.Plan.DecisionReason,
|
|
PlanID: result.Plan.PlanID,
|
|
PlanAttemptKey: result.Plan.PlanAttemptKey,
|
|
SelectedTracks: &selectedTracks,
|
|
Subtitle: &subtitle,
|
|
Claims: &claims,
|
|
Transformations: append([]playback.TransformationV3(nil), result.Plan.Transformations...),
|
|
AvailableQualities: result.Plan.AvailableQualities,
|
|
}
|
|
} else if result.Terminal != nil {
|
|
expected.TerminalReason = result.Terminal.Reason
|
|
}
|
|
return playback.PlannerScenarioV3{
|
|
Name: name, Category: category, Request: request,
|
|
Source: playback.SourceDescriptorFromFileV3(file, 0),
|
|
AttemptedKeys: append([]string(nil), attempted...), Expected: expected,
|
|
}
|
|
}
|
|
|
|
func conformanceStartRequest() playback.StartRequestV3 {
|
|
request := goldenStartRequest()
|
|
request.QualityPreference = qualityOriginal
|
|
request.Capabilities = playback.ClientCodecCapabilitiesV3{
|
|
VideoEvidence: playback.EvidenceExactV3, AudioEvidence: playback.EvidenceExactV3,
|
|
CodecsVideo: []string{codecHEVC}, CodecsVideoHardware: []string{codecHEVC}, CodecsAudio: []string{codecAAC}, Containers: []string{containerMKV}, MaxResolution: resolutionFHD,
|
|
VideoDecode: []playback.VideoDecodeCapabilityV3{{Codec: codecHEVC, Profiles: []string{"main 10"}, Levels: []int{41}, BitDepths: []int{8}, MaxWidth: 1920, MaxHeight: 1080, MaxFrameRate: 60, MaxBitrateKbps: 20_000, Hardware: true}},
|
|
}
|
|
request.ClientPlaybackContext = playback.ClientPlaybackContextV3{
|
|
ProtocolVersion: playback.ProtocolV3, FormFactor: "tv", AppVersion: "3.0-test",
|
|
Device: playback.DeviceContextV3{Platform: "fixture"}, Output: playback.OutputContextV3{OutputContextID: "output-a"},
|
|
Deliveries: map[string]playback.DeliveryCapabilityV3{
|
|
playback.DeliveryClassOriginalHTTPV3: conformanceDelivery([]string{containerMKV, containerMP4}),
|
|
playback.DeliveryClassProgressiveV3: conformanceDelivery([]string{containerMP4}),
|
|
playback.DeliveryClassHLSV3: conformanceDelivery([]string{containerHLS}),
|
|
},
|
|
}
|
|
return request
|
|
}
|
|
|
|
func conformanceDelivery(containers []string) playback.DeliveryCapabilityV3 {
|
|
return playback.DeliveryCapabilityV3{
|
|
Enabled: true,
|
|
SupportedOnDevice: true,
|
|
Containers: append([]string(nil), containers...),
|
|
VideoCodecs: []string{codecHEVC, codecH264},
|
|
AudioDecodeCodecs: []string{codecAAC},
|
|
AudioPassthroughCodecs: []string{},
|
|
Features: []string{},
|
|
ValidatedClaims: []string{},
|
|
Transformations: []playback.TransformationV3{},
|
|
}
|
|
}
|
|
|
|
func conformanceVideoFile() *models.MediaFile {
|
|
return &models.MediaFile{ID: goldenMediaFileID, Container: containerMKV, CodecVideo: codecHEVC, CodecAudio: codecAAC, Resolution: resolutionFHD, Bitrate: 8_000, AudioChannels: 2, Duration: 7_200, VideoTracks: []models.VideoTrack{{Codec: codecHEVC, Profile: "Main", Level: 41, Width: 1920, Height: 1080, FrameRate: filmFrameRate, Bitrate: 8_000, BitDepth: 8, VideoRange: videoRangeSDR, VideoRangeType: videoRangeSDR}}, AudioTracks: []models.AudioTrack{{Codec: codecAAC, Channels: 2, Layout: audioLayoutStereo}}}
|
|
}
|
|
|
|
func conformanceHDRFile() *models.MediaFile {
|
|
return &models.MediaFile{
|
|
ID: goldenMediaFileID,
|
|
Container: containerMKV,
|
|
CodecVideo: codecHEVC,
|
|
CodecAudio: codecAAC,
|
|
Resolution: "2160p",
|
|
Bitrate: 60_000,
|
|
AudioChannels: 2,
|
|
Duration: 7_200,
|
|
VideoTracks: []models.VideoTrack{{
|
|
Codec: codecHEVC, Profile: "Main 10", Level: 153,
|
|
Width: 3840, Height: 2160, FrameRate: filmFrameRate, Bitrate: 60_000, BitDepth: 10,
|
|
VideoRange: "HDR", VideoRangeType: "HDR10", ColorRange: "tv",
|
|
}},
|
|
AudioTracks: []models.AudioTrack{{Codec: codecAAC, Channels: 2, Layout: audioLayoutStereo}},
|
|
}
|
|
}
|
|
|
|
func conformanceHDRRequest() playback.StartRequestV3 {
|
|
request := conformanceStartRequest()
|
|
request.Capabilities.CodecsVideo = []string{codecHEVC}
|
|
request.Capabilities.CodecsVideoHardware = []string{codecHEVC}
|
|
request.Capabilities.CodecsAudio = []string{codecAAC}
|
|
request.Capabilities.Containers = []string{containerMKV}
|
|
request.Capabilities.MaxResolution = "2160p"
|
|
request.Capabilities.HDR = true
|
|
request.Capabilities.HDRDetails = &playback.HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{}}
|
|
request.Capabilities.VideoDecode = []playback.VideoDecodeCapabilityV3{{
|
|
Codec: codecHEVC, Profiles: []string{"main 10"}, Levels: []int{153}, BitDepths: []int{10},
|
|
MaxWidth: 3840, MaxHeight: 2160, MaxFrameRate: 60, MaxBitrateKbps: 80_000, Hardware: true,
|
|
}}
|
|
request.ClientPlaybackContext.Output.HDRDetails = &playback.HDRCapabilitiesV3{HDR10: true, DolbyVisionProfiles: []int{}}
|
|
return request
|
|
}
|
|
|
|
func conformanceFallbackFile() *models.MediaFile {
|
|
return &models.MediaFile{ID: goldenMediaFileID, FilePath: "/media/movie.mp4", Container: containerMP4, CodecVideo: codecH264, CodecAudio: codecAAC, Resolution: resolutionFHD, Bitrate: 8_000, AudioChannels: 2, Duration: 7_200, VideoTracks: []models.VideoTrack{{Codec: codecH264, Profile: profileHigh, Level: 41, Width: 1920, Height: 1080, FrameRate: filmFrameRate, Bitrate: 8_000, BitDepth: 8, VideoRange: videoRangeSDR, VideoRangeType: videoRangeSDR}}, AudioTracks: []models.AudioTrack{{Codec: codecAAC, Channels: 2, Layout: audioLayoutStereo}}}
|
|
}
|
|
|
|
func conformanceRegistry() *playback.TransformationRegistryV3 {
|
|
return playback.NewTransformationRegistryV3([]playback.TransformationSpecV3{
|
|
{Name: playback.TransformationAudioToAACV3, RecipeVersion: "1", Available: true},
|
|
{Name: playback.TransformationVideoToH264V3, RecipeVersion: "1", Available: true},
|
|
{Name: playback.TransformationServerDV7HDR10V3, RecipeVersion: "1", Available: true},
|
|
})
|
|
}
|