Files
Jacquin Antoine d319ef6b4a Improve odometry with rotation estimation, daemon stability, and RTSP fixes
- Add rotation estimation via log-polar phase correlation (EstimateRotation,
  rotateGray) — de-rotate before translation to avoid aliasing
- Track cumulative field rotation in Tracker, reject low-confidence frames
- Extract RTSP grabber into internal/rtspgrab module
- Daemon: pause odometry during slew (motion blur), auto-resume after settle
  delay, add /pause and /resume HTTP endpoints, switch shooting mode before
  opening camera, use ReqOpenCamera with rtsp_encode_type=1
- WebSocket heartbeat (ping/pong every 10s) to prevent idle disconnects
- FFmpeg low-latency flags (-fflags nobuffer, -probesize, -analyzeduration)
- Add SendRaw API for custom payloads, increase daemon HTTP timeout to 60s

💘 Generated with Crush

Assisted-by: Crush:/models/Qwen3.6-27B-uncensored-heretic-v2-Native-MTP-Preserved-Q4_K_M.gguf
2026-07-13 23:00:30 +02:00

80 lines
2.2 KiB
Go

// 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
}