2026-08-11 14:30:55 -04:00
|
|
|
// Package transcodeproxy owns the private HTTP completion contract shared by
|
|
|
|
|
// Silo's integrated API proxy and its dedicated proxy node.
|
|
|
|
|
package transcodeproxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// RequestHeader marks a segment hop whose immediate consumer is another
|
|
|
|
|
// Silo server, not the playback client. The transcode node defers download
|
|
|
|
|
// accounting until that server confirms downstream completion.
|
|
|
|
|
RequestHeader = "X-Silo-Transcode-Proxy"
|
|
|
|
|
// GenerationHeader carries an opaque session-incarnation and FFmpeg-timeline
|
|
|
|
|
// token. It is private to Silo hops and must not be forwarded to the client.
|
|
|
|
|
GenerationHeader = "X-Silo-Transcode-Segment-Generation"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var representationRequestHeaders = []string{
|
|
|
|
|
"Range",
|
|
|
|
|
"If-Range",
|
|
|
|
|
"If-Match",
|
|
|
|
|
"If-None-Match",
|
|
|
|
|
"If-Modified-Since",
|
|
|
|
|
"If-Unmodified-Since",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PrepareRequest makes the upstream node serve the same byte representation
|
|
|
|
|
// requested by the downstream client and suppresses completion at this hop.
|
|
|
|
|
func PrepareRequest(upstream, downstream *http.Request) {
|
|
|
|
|
upstream.Header.Set(RequestHeader, "1")
|
|
|
|
|
for _, name := range representationRequestHeaders {
|
|
|
|
|
for _, value := range downstream.Header.Values(name) {
|
|
|
|
|
upstream.Header.Add(name, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CopyResponseHeaders forwards public response metadata while retaining the
|
|
|
|
|
// generation token inside the trusted Silo hop.
|
|
|
|
|
func CopyResponseHeaders(dst, src http.Header) {
|
|
|
|
|
for name, values := range src {
|
|
|
|
|
if http.CanonicalHeaderKey(name) == GenerationHeader {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
dst.Add(name, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FullRepresentationSize resolves the total entity size needed to distinguish
|
|
|
|
|
// a whole-file 206 response from an ordinary partial range.
|
|
|
|
|
func FullRepresentationSize(resp *http.Response) int64 {
|
|
|
|
|
if resp == nil {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
|
return resp.ContentLength
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode != http.StatusPartialContent {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
_, total, ok := strings.Cut(resp.Header.Get("Content-Range"), "/")
|
|
|
|
|
if !ok || total == "*" {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
size, err := strconv.ParseInt(total, 10, 64)
|
|
|
|
|
if err != nil || size <= 0 {
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
return size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Acknowledge reports a completed downstream response to the transcode node.
|
|
|
|
|
// The opaque generation token makes delayed acknowledgements harmless after a
|
|
|
|
|
// restart or reconstruction.
|
|
|
|
|
func Acknowledge(ctx context.Context, client *http.Client, targetURL, jwtSecret, generation string) error {
|
|
|
|
|
if client == nil {
|
|
|
|
|
client = http.DefaultClient
|
|
|
|
|
}
|
2026-08-11 14:45:33 -04:00
|
|
|
// The downstream request is commonly canceled as soon as its response body
|
|
|
|
|
// reaches the client, while the handler is still issuing this acknowledgement.
|
|
|
|
|
// Preserve request-scoped values, but give the post-response hop its own
|
|
|
|
|
// bounded lifetime so a successful delivery is not mistaken for a disconnect.
|
|
|
|
|
ackCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
2026-08-11 14:30:55 -04:00
|
|
|
defer cancel()
|
|
|
|
|
req, err := http.NewRequestWithContext(ackCtx, http.MethodPost, targetURL+"/downloaded", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("build acknowledgement: %w", err)
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Authorization", "Bearer "+jwtSecret)
|
|
|
|
|
req.Header.Set(GenerationHeader, generation)
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("send acknowledgement: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4<<10))
|
|
|
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
|
|
|
return fmt.Errorf("acknowledgement status %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|