- 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
80 lines
2.2 KiB
Go
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
|
|
}
|