From aa11bba08fe27294cc0dda308ebdd9b33aaf94d0 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Wed, 22 Jul 2026 13:20:10 -0700 Subject: [PATCH] media: diagnose legacy MKV pushes; default mist-http-port to 28080 Field report from the first live test of the fMP4 ingest: a MistServer container still running the legacy MKVExec process config POSTed MKV to /live on a 4s restart loop, and each attempt died as an instant cryptic qtdemux failure (a pile of ~200-byte truncated debug recordings). Meanwhile the pull ingest dialed the old default port 18080 while Mist listened on 28080, so it never connected at all. - buildMP4IngestPipeline now peeks the stream and rejects the EBML magic with an error that names the actual problem (legacy MKVExec config) instead of letting qtdemux die confusingly. - mist-http-port default 18080 -> 28080, matching docker/mistserver.json (the generated dev config derives Mist's listener from the same flag, so both worlds stay consistent). Co-Authored-By: Claude Fable 5 --- pkg/config/config.go | 4 ++-- pkg/media/mist_mp4_ingest_test.go | 12 ++++++++++++ pkg/media/mp4_ingest.go | 32 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 1ba3931b..03a78af3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1032,8 +1032,8 @@ func (cli *CLI) NewCommand(name string) *urfavecli.Command { }) cmd.Flags = append(cmd.Flags, &urfavecli.IntFlag{ Name: "mist-http-port", - Usage: "MistServer HTTP port (internal use only)", - Value: 18080, + Usage: "MistServer HTTP port (internal use only) — ingest pulls Mist's live fMP4 output from this port, so it must match the running Mist config (docker/mistserver.json uses 28080, the default here)", + Value: 28080, Destination: &cli.MistHTTPPort, Sources: urfavecli.EnvVars("SP_MIST_HTTP_PORT"), }) diff --git a/pkg/media/mist_mp4_ingest_test.go b/pkg/media/mist_mp4_ingest_test.go index 13a60bcc..d933ca18 100644 --- a/pkg/media/mist_mp4_ingest_test.go +++ b/pkg/media/mist_mp4_ingest_test.go @@ -282,3 +282,15 @@ func TestMP4IngestBFramesValidate(t *testing.T) { require.True(t, sawReorderOffset, "B-frame stream keeps its real PTS−DTS reorder offsets through ingest") t.Logf("B-frame stream: %d segments, all validated", len(segs)) } + +// TestMP4IngestRejectsMatroskaWithDiagnosis: an MKV stream landing on the +// fMP4 ingest (a MistServer still running the legacy MKVExec process config) +// must fail immediately with a message that names the actual problem — not a +// generic qtdemux parse error on an endless Mist-side restart loop. +func TestMP4IngestRejectsMatroskaWithDiagnosis(t *testing.T) { + mkvish := append(append([]byte{}, matroskaMagic...), make([]byte, 1024)...) + _, err := runMP4ThroughIngestWorker(t, mkvish, false) + require.Error(t, err) + require.Contains(t, err.Error(), "Matroska") + require.Contains(t, err.Error(), "MKVExec") +} diff --git a/pkg/media/mp4_ingest.go b/pkg/media/mp4_ingest.go index 77314fda..776e804b 100644 --- a/pkg/media/mp4_ingest.go +++ b/pkg/media/mp4_ingest.go @@ -1,7 +1,10 @@ package media import ( + "bufio" + "bytes" "context" + "errors" "fmt" "io" "strings" @@ -81,7 +84,36 @@ func (mm *MediaManager) MP4Ingest(ctx context.Context, input io.Reader, ms Media // offset that pushed every GoP's presentation past its segment's declared // window and broke WebRTC playback at every keyframe. Real DTS in the // container means no reconstruction and no guessing. +// matroskaMagic is the EBML header every Matroska/WebM stream opens with. +var matroskaMagic = []byte{0x1A, 0x45, 0xDF, 0xA3} + +// rejectMatroska peeks at the ingest stream and fails fast with a diagnosis if +// it's Matroska. MKV was this pipeline's previous bridge format, so the most +// likely stray MKV source is a MistServer still running the legacy MKVExec +// process config (`streamplace live` POSTing MKV to /live on a restart loop) — +// without the sniff that just looks like qtdemux dying instantly, over and +// over, which is a miserable thing to debug. Returns a reader that includes +// the peeked bytes. +func rejectMatroska(input io.Reader) (io.Reader, error) { + br := bufio.NewReader(input) + head, err := br.Peek(len(matroskaMagic)) + if err != nil { + if errors.Is(err, io.EOF) { + return br, nil // shorter than the magic; let the pipeline EOS/complain + } + return nil, fmt.Errorf("peek ingest stream: %w", err) + } + if bytes.Equal(head, matroskaMagic) { + return nil, fmt.Errorf("ingest input is Matroska (MKV), but this node ingests fragmented MP4 — a MistServer running the legacy MKVExec process config is probably still pushing MKV to /live; update its config (see docker/mistserver.json)") + } + return br, nil +} + func buildMP4IngestPipeline(ctx context.Context, input io.Reader, signerElem *gst.Element) (*gst.Pipeline, error) { + input, err := rejectMatroska(input) + if err != nil { + return nil, err + } // Queue sizing: qtdemux feeds both branches from one thread, and the fMP4 // muxer downstream is an aggregator — it consumes NOTHING until every pad // has data. If the video track goes sparse (e.g. MistServer drops all delta -- 2.51.2