// Package rtspgrab captures single frames from the DWARF telescope's RTSP // server. The telescope's RTSP implementation is non-standard: ffmpeg can // connect and capture frames but does not terminate cleanly (it hangs after // writing the output file). This package works around that by launching ffmpeg // in the background, watching for the output file to appear, then killing ffmpeg. package rtspgrab import ( "context" "fmt" "image" _ "image/jpeg" "os" "os/exec" "time" ) // GrabFrame connects to the RTSP URL, captures one MJPEG frame, saves it to // outFile, and returns the decoded image. It uses ffmpeg with low-latency // options required by the DWARF telescope's non-standard RTSP server, then // kills ffmpeg once the output file is written (ffmpeg hangs after capture). func GrabFrame(rtspURL, outFile string) (image.Image, error) { // Remove any stale output. os.Remove(outFile) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "ffmpeg", "-rtsp_transport", "tcp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "4096", "-analyzeduration", "1000000", "-i", rtspURL, "-frames:v", "1", "-q:v", "2", "-y", outFile) cmd.Stdout = nil cmd.Stderr = nil if err := cmd.Start(); err != nil { return nil, fmt.Errorf("start ffmpeg: %w", err) } // Poll for the output file. Once it appears with non-zero size, kill ffmpeg. deadline := time.Now().Add(25 * time.Second) for time.Now().Before(deadline) { if info, err := os.Stat(outFile); err == nil && info.Size() > 0 { // File written — kill ffmpeg (it won't exit on its own). cmd.Process.Kill() cmd.Wait() break } select { case <-ctx.Done(): cmd.Process.Kill() return nil, fmt.Errorf("timeout waiting for frame") case <-time.After(200 * time.Millisecond): } } // Check if we got the file. info, err := os.Stat(outFile) if err != nil || info.Size() == 0 { cmd.Process.Kill() return nil, fmt.Errorf("no frame captured (ffmpeg produced no output)") } // Decode the JPEG. f, err := os.Open(outFile) if err != nil { return nil, err } defer f.Close() img, _, err := image.Decode(f) if err != nil { return nil, fmt.Errorf("decode JPEG: %w", err) } return img, nil }