Files
silo-server/internal/playback/executable_recipe_v3.go
T
fa1d7ba2b4 fix(playback): freeze v3 seek reanchor recipes (#548)
* fix(playback): freeze v3 seek reanchor recipes

* fix(playback): address v3 recipe review feedback

* test(playback): isolate v3 fallback route fixture

* fix(database): split v3 recipe constraint validation

* fix(playback): preserve frozen seek recipe identity

---------

Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
2026-08-06 10:43:21 -04:00

111 lines
4.7 KiB
Go

package playback
// ExecutableRecipeV3 is the frozen operational half of a protocol-v3 plan.
// PlanV3 describes the client-visible route identity; these fields are the
// additional inputs needed to open another transport for that same route.
// Keeping them with the durable attempt prevents seek reanchoring from
// reverse-engineering execution details from presentation fields or mutable
// planner inputs.
type ExecutableRecipeV3 struct {
Version int `json:"version"`
PlanID string `json:"plan_id"`
PlayMethod PlayMethod `json:"play_method"`
TranscodeAudio bool `json:"transcode_audio"`
TargetVideoCodec string `json:"target_video_codec,omitempty"`
TargetAudioCodec string `json:"target_audio_codec,omitempty"`
TargetAudioChannels int `json:"target_audio_channels,omitempty"`
TargetResolution string `json:"target_resolution,omitempty"`
TargetBitrateKbps int `json:"target_bitrate_kbps,omitempty"`
SourceVideoCodec string `json:"source_video_codec,omitempty"`
SourceDurationSeconds float64 `json:"source_duration_seconds,omitempty"`
SubtitleTrackIndex int `json:"subtitle_track_index"`
SubtitleTransportTrackIndex int `json:"subtitle_transport_track_index"`
SubtitleBurnIn bool `json:"subtitle_burn_in"`
SubtitleCodec string `json:"subtitle_codec,omitempty"`
// SubtitleSource pins which sidecar inventory segment SubtitleTrackIndex
// pointed into when the plan was accepted, and the identity fields below
// pin the exact entry. The combined index space (externals, then embedded,
// then downloaded) shifts when any segment grows or shrinks; a seek
// reanchor must detect that drift and fail rather than silently resolve
// the frozen index to a different artifact.
SubtitleSource string `json:"subtitle_source,omitempty"`
ExternalSubtitlePath string `json:"external_subtitle_path,omitempty"`
EmbeddedStreamIndex int `json:"embedded_stream_index,omitempty"`
DownloadedSubtitleID int `json:"downloaded_subtitle_id,omitempty"`
}
const executableRecipeVersionV3 = 1
const (
SubtitleSourceExternalV3 = "external"
SubtitleSourceEmbeddedV3 = "embedded"
SubtitleSourceDownloadedV3 = "downloaded"
)
func FreezeExecutableRecipeV3(result PlannerResultV3) ExecutableRecipeV3 {
planID := ""
if result.Plan != nil {
planID = result.Plan.PlanID
}
sourceMetadata := SourceExecutionMetadataV3{}
if result.FrozenSourceMetadata != nil {
sourceMetadata = *result.FrozenSourceMetadata
}
return ExecutableRecipeV3{
Version: executableRecipeVersionV3,
PlanID: planID,
PlayMethod: result.PlayMethod,
TranscodeAudio: result.TranscodeAudio,
TargetVideoCodec: result.TargetVideoCodec,
TargetAudioCodec: result.TargetAudioCodec,
TargetAudioChannels: result.TargetAudioChannels,
TargetResolution: result.TargetResolution,
TargetBitrateKbps: result.TargetBitrateKbps,
SourceVideoCodec: sourceMetadata.VideoCodec,
SourceDurationSeconds: sourceMetadata.DurationSeconds,
SubtitleTrackIndex: result.SubtitleTrackIndex,
SubtitleTransportTrackIndex: result.SubtitleTransportTrackIndex,
SubtitleBurnIn: result.SubtitleBurnIn,
SubtitleCodec: result.SubtitleCodec,
DownloadedSubtitleID: result.DownloadedSubtitleID,
}
}
func (r ExecutableRecipeV3) Valid() bool {
if r.Version != executableRecipeVersionV3 || r.PlanID == "" {
return false
}
switch r.PlayMethod {
case PlayDirect, PlayRemux, PlayTranscode:
return true
default:
return false
}
}
func (r ExecutableRecipeV3) ValidFor(plan PlanV3) bool {
return r.Valid() && r.PlanID == plan.PlanID
}
func (r ExecutableRecipeV3) PlannerResult(plan *PlanV3) PlannerResultV3 {
return PlannerResultV3{
Plan: plan,
PlayMethod: r.PlayMethod,
TranscodeAudio: r.TranscodeAudio,
TargetVideoCodec: r.TargetVideoCodec,
TargetAudioCodec: r.TargetAudioCodec,
TargetAudioChannels: r.TargetAudioChannels,
TargetResolution: r.TargetResolution,
TargetBitrateKbps: r.TargetBitrateKbps,
FrozenSourceMetadata: &SourceExecutionMetadataV3{
VideoCodec: r.SourceVideoCodec,
DurationSeconds: r.SourceDurationSeconds,
},
SubtitleTrackIndex: r.SubtitleTrackIndex,
SubtitleTransportTrackIndex: r.SubtitleTransportTrackIndex,
SubtitleBurnIn: r.SubtitleBurnIn,
SubtitleCodec: r.SubtitleCodec,
DownloadedSubtitleID: r.DownloadedSubtitleID,
}
}