// backend\record_stream_mfc.go package main import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/grafov/m3u8" ) // RecordStreamMFC nimmt vorerst die URL 1:1 und ruft ffmpeg direkt darauf auf. // In der Praxis musst du hier meist erst eine HLS-URL aus dem HTML extrahieren. // RecordStreamMFC ist jetzt nur noch ein Wrapper um den bewährten MFC-Flow (runMFC). func RecordStreamMFC( ctx context.Context, hc *HTTPClient, username string, outputPath string, job *RecordJob, ) error { mfc := NewMyFreeCams(username) const waitPublicMax = 20 * time.Second deadline := time.Now().Add(waitPublicMax) for { if err := ctx.Err(); err != nil { return err } st, err := mfc.GetStatus() if err == nil && st == StatusPublic { break } if time.Now().After(deadline) { return context.DeadlineExceeded } time.Sleep(5 * time.Second) } // ✅ erst jetzt die Video URL holen (weil public) stream, err := mfc.GetSelectedStream(false) if err != nil { return appErrorf("mfc get video url: %w", err) } if stream == nil || strings.TrimSpace(stream.VideoURL) == "" { return appErrorf("mfc: keine m3u8 URL gefunden") } // ✅ WICHTIG: fMP4 live preview (/api/preview/live) braucht job.PreviewM3U8 als Input if job != nil { previewURL := strings.TrimSpace(stream.VideoURL) if stream.HasSeparateAudio && strings.TrimSpace(stream.MasterURL) != "" { previewURL = strings.TrimSpace(stream.MasterURL) } jobsMu.Lock() job.PreviewM3U8 = previewURL job.PreviewCookie = "" job.PreviewUA = hc.userAgent jobsMu.Unlock() } if job != nil { _ = publishJob(job.ID) } return handleM3U8Mode(ctx, stream, outputPath, job, "", hc.userAgent, mfc.GetWebsiteURL()) } type MyFreeCams struct { Username string Attrs map[string]string VideoURL string } func NewMyFreeCams(username string) *MyFreeCams { return &MyFreeCams{ Username: username, Attrs: map[string]string{}, } } func (m *MyFreeCams) GetWebsiteURL() string { return "https://www.myfreecams.com/#" + m.Username } func (m *MyFreeCams) GetVideoURL(refresh bool) (string, error) { if !refresh && m.VideoURL != "" { return m.VideoURL, nil } // Prüfen, ob alle benötigten Attribute vorhanden sind if _, ok := m.Attrs["data-cam-preview-model-id-value"]; !ok { return "", nil } sid := m.Attrs["data-cam-preview-server-id-value"] midBase := m.Attrs["data-cam-preview-model-id-value"] isWzobs := strings.ToLower(m.Attrs["data-cam-preview-is-wzobs-value"]) == "true" midInt, err := strconv.Atoi(midBase) if err != nil { return "", appErrorf("model-id parse error: %w", err) } mid := 100000000 + midInt a := "" if isWzobs { a = "a_" } playlistURL := fmt.Sprintf( "https://previews.myfreecams.com/hls/NxServer/%s/ngrp:mfc_%s%d.f4v_mobile_mhp1080_previewurl/playlist.m3u8", sid, a, mid, ) // Validieren (HTTP 200) & ggf. auf gewünschte Auflösung verlinken u, err := getWantedResolutionPlaylist(playlistURL) if err != nil { return "", err } m.VideoURL = u return m.VideoURL, nil } func (m *MyFreeCams) GetSelectedStream(refresh bool) (*selectedHLSStream, error) { m3u8URL, err := m.GetVideoURL(refresh) if err != nil { return nil, err } if strings.TrimSpace(m3u8URL) == "" { return nil, errors.New("keine m3u8 URL gefunden") } return getWantedResolutionPlaylistWithHeaders( context.Background(), m3u8URL, "", // MFC normalerweise ohne Cookies "", // optionaler UA, hier leer lassen oder später ergänzen m.GetWebsiteURL(), ) } func (m *MyFreeCams) GetStatus() (Status, error) { // 1) share-Seite prüfen (existiert/nicht existiert) shareURL := "https://share.myfreecams.com/" + m.Username resp, err := http.Get(shareURL) if err != nil { return StatusUnknown, err } defer resp.Body.Close() if resp.StatusCode == 404 { return StatusNotExist, nil } if resp.StatusCode != 200 { return StatusUnknown, appErrorf("HTTP %d", resp.StatusCode) } // wir brauchen sowohl Bytes (für Suche) als auch Reader (für HTML) bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return StatusUnknown, err } // 2) „tracking.php?“ suchen und prüfen, ob model_id vorhanden ist start := bytes.Index(bodyBytes, []byte("https://www.myfreecams.com/php/tracking.php?")) if start == -1 { // ohne tracking Parameter -> behandeln wie nicht existent return StatusNotExist, nil } end := bytes.IndexByte(bodyBytes[start:], '"') if end == -1 { return StatusUnknown, errors.New("tracking url parse failed") } raw := string(bodyBytes[start : start+end]) u, err := url.Parse(raw) if err != nil { return StatusUnknown, appErrorf("tracking url invalid: %w", err) } qs := u.Query() if qs.Get("model_id") == "" { return StatusNotExist, nil } // 3) HTML parsen und