* fix(playback): last-write-wins progress and DV P7 RPU strip on remux Progress: UpdateProgress (the live playback-session path) clamped position_seconds to GREATEST(new, old), so a deliberate backward seek could never persist — "rewind and stop" resumed at the stale later position on every client. Position is now last-write-wins, matching the /sync/progress path that was always unconditional. The completed latch and rewatch re-entry semantics are unchanged. Remux: profile 7 Dolby Vision remuxes drop the enhancement-layer track (-map 0:v:0 keeps only the base layer) but previously left the dangling dual-layer RPUs on the BL — broken metadata that a DV-honoring display can mis-render. Remuxes of P7 files now strip DV RPUs via the dovi_rpu bitstream filter, yielding a clean HDR10 stream (the same fallback presentation the Apple client's P7 HDR10 toggle produces). Profile 8 RPUs are kept: the BL is self-contained and DV clients render it. Adds MediaFile.PrimaryDVProfile() and threads the profile through ServeRemux callers; the proxy path (no track metadata in claims) keeps prior behavior. True P7->8.1 DV conversion needs dovi_tool alongside FFmpeg (the dovi_rpu bsf only strips/recompresses); the remux plumbing now carries the DV profile so that can slot in later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(userdb): apply last-write-wins progress to the SQLite backend too Review follow-up (P2): the LWW change only covered pgstore; the SQLite userdb UpdateProgress kept the MAX clamp, so rewind-and-stop still resumed at the stale later position for sqlite-backed installs. The conflict clause now matches Postgres (position last-write-wins, completed latch and rewatch re-entry unchanged), with a backward-seek regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): harden LWW progress and DV RPU strip from review - Probe ffmpeg for the dovi_rpu bitstream filter once per process and fall back to a no-strip remux (the pre-existing behavior) when it is missing: on pre-7.1 ffmpeg the unknown filter aborted the process, turning every Dolby Vision profile 7 remux into a hard playback failure. - Skip zero-position heartbeats in persistProgress, mirroring the stop path and the jellycompat report path. Under last-write-wins an early zero heartbeat (e.g. before the client seeks to its resume point) would wipe the stored resume position; GREATEST previously masked this. - Carry the DV profile in stream token claims (dvp, omitempty) so standalone proxy nodes strip profile 7 RPUs the same way integrated mode does. Old tokens decode as 0 and keep prior behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
91 lines
3.5 KiB
Go
91 lines
3.5 KiB
Go
package streamtoken
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// Claims holds everything a stateless proxy or transcode node needs
|
|
// to serve a streaming session without database access.
|
|
//
|
|
// Under token-carried reconstruction (TR-lease) the token is also the durable
|
|
// reconstruction descriptor: its claims carry the full set of byte-affecting
|
|
// encode parameters (the former Postgres "recipe card"), so a front-end that has
|
|
// lost its in-memory session can rebuild ffmpeg from the token the client
|
|
// re-presents — no shared per-session store. The ownership claims (uid/pid/mfid)
|
|
// are lookup keys re-resolved against the authority on reconstruct; they are
|
|
// never trusted on their own.
|
|
type Claims struct {
|
|
SessionID string `json:"sid"`
|
|
MediaPath string `json:"path"`
|
|
PlayMethod string `json:"method"`
|
|
TranscodeAudio bool `json:"ta,omitempty"`
|
|
TranscodeNode string `json:"tnode,omitempty"`
|
|
TargetCodec string `json:"tc,omitempty"`
|
|
TargetRes string `json:"tres,omitempty"`
|
|
AudioCodec string `json:"ac,omitempty"`
|
|
AudioChannels int `json:"ach,omitempty"`
|
|
AudioTrackIndex int `json:"ati,omitempty"`
|
|
// DVProfile is the file's Dolby Vision profile (0 = none); remux nodes
|
|
// use it to strip dangling profile 7 RPUs. Absent in older tokens, which
|
|
// decodes as 0 (no strip — the pre-existing behavior).
|
|
DVProfile int `json:"dvp,omitempty"`
|
|
|
|
// Ownership / authorization lookup keys (re-resolved at reconstruct).
|
|
// Not trust assertions.
|
|
UserID int `json:"uid,omitempty"`
|
|
ProfileID string `json:"pid,omitempty"`
|
|
MediaFileID int `json:"mfid,omitempty"`
|
|
|
|
// Reconstruction recipe — the byte-affecting encode parameters, mirroring the
|
|
// former playback.RecipeCard. Zero for direct/remux tokens, which reconstruct
|
|
// from identity alone plus the client-supplied position.
|
|
SourceVideoCodec string `json:"svc,omitempty"`
|
|
SeekSeconds float64 `json:"seek,omitempty"`
|
|
SegmentDuration int `json:"segd,omitempty"`
|
|
StartSegmentNumber int `json:"ssn,omitempty"`
|
|
SubtitleTrackIndex int `json:"sti,omitempty"`
|
|
SubtitleBurnIn bool `json:"sbi,omitempty"`
|
|
TargetBitrateKbps int `json:"tbr,omitempty"`
|
|
TotalDuration float64 `json:"dur,omitempty"`
|
|
FastStart bool `json:"fs,omitempty"`
|
|
TargetCodecAudio string `json:"tca,omitempty"`
|
|
|
|
// Recipe staleness hint, bumped on each re-mint after a recipe mutation
|
|
// (audio/quality/seek switch). An optional client-side hint only.
|
|
Version int `json:"ver,omitempty"`
|
|
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// Sign creates a signed JWT string from the given claims.
|
|
func Sign(c Claims, secret string, ttl time.Duration) (string, error) {
|
|
now := time.Now()
|
|
c.RegisteredClaims = jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
|
|
return token.SignedString([]byte(secret))
|
|
}
|
|
|
|
// Verify parses and validates a stream token JWT string.
|
|
func Verify(tokenString, secret string) (*Claims, error) {
|
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
}
|
|
return []byte(secret), nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid stream token: %w", err)
|
|
}
|
|
claims, ok := token.Claims.(*Claims)
|
|
if !ok || !token.Valid {
|
|
return nil, fmt.Errorf("invalid stream token claims")
|
|
}
|
|
return claims, nil
|
|
}
|