Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
package mpd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// KIDResolver is implemented by rewriters that can recover the live channel from
|
|
// a KID alone. The local server uses it so a rewrite still works after the
|
|
// upstream (ad-stitched) manifest session has expired and returns 410.
|
|
type KIDResolver interface {
|
|
ResolveKID(kidHex string) (channel, originBase string, ok bool)
|
|
}
|
|
|
|
// Primer is implemented by rewriters that want to be told the fallback channel
|
|
// and origin discovered on earlier requests.
|
|
type Primer interface {
|
|
Prime(channel, originBase string)
|
|
}
|
|
|
|
// LocalServer serves a rewritten live MPD on 127.0.0.1 for a downloader to
|
|
// refresh. Every GET re-fetches upstream and re-runs the rewriter.
|
|
type LocalServer struct {
|
|
URL string
|
|
Upstream string
|
|
Key string // KID:KEY
|
|
Headers map[string]string
|
|
|
|
rw Rewriter
|
|
kidHint string
|
|
ln net.Listener
|
|
httpServer *http.Server
|
|
}
|
|
|
|
// StartLocal starts the rewrite proxy. rw must not be nil; pass Passthrough{}
|
|
// to serve upstream unchanged.
|
|
func StartLocal(upstream, key string, headers map[string]string, rw Rewriter) (*LocalServer, error) {
|
|
if rw == nil {
|
|
rw = Passthrough{}
|
|
}
|
|
s := &LocalServer{Upstream: upstream, Key: key, Headers: headers, rw: rw}
|
|
if key != "" && strings.Contains(key, ":") {
|
|
s.kidHint = strings.SplitN(key, ":", 2)[0]
|
|
if r, ok := rw.(KIDResolver); ok {
|
|
if ch, base, found := r.ResolveKID(KIDHex(s.kidHint)); found {
|
|
if p, ok := rw.(Primer); ok {
|
|
p.Prime(ch, base)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.ln = ln
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", s.handle)
|
|
s.httpServer = &http.Server{Handler: mux}
|
|
s.URL = fmt.Sprintf("http://%s/manifest.mpd", ln.Addr().String())
|
|
go func() { _ = s.httpServer.Serve(ln) }()
|
|
return s, nil
|
|
}
|
|
|
|
func (s *LocalServer) Close() {
|
|
if s.httpServer != nil {
|
|
_ = s.httpServer.Close()
|
|
}
|
|
if s.ln != nil {
|
|
_ = s.ln.Close()
|
|
}
|
|
}
|
|
|
|
func (s *LocalServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
body := ""
|
|
req, err := http.NewRequest(http.MethodGet, s.Upstream, nil)
|
|
if err == nil {
|
|
for k, v := range s.Headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
client := &http.Client{Timeout: 20 * time.Second}
|
|
if resp, err := client.Do(req); err == nil {
|
|
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
|
_ = resp.Body.Close()
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 && len(b) > 0 {
|
|
body = string(b)
|
|
}
|
|
}
|
|
}
|
|
// Upstream being gone is not fatal: a rewriter with a KID fallback can still
|
|
// publish the encoder timeline so the downloader keeps pulling segments.
|
|
payload, err := s.rw.Rewrite([]byte(body), s.kidHint)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), 502)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/dash+xml")
|
|
w.WriteHeader(200)
|
|
_, _ = w.Write(payload)
|
|
}
|