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
This commit is contained in:
79
dwarfctl/internal/rtspgrab/grabber.go
Normal file
79
dwarfctl/internal/rtspgrab/grabber.go
Normal file
@ -0,0 +1,79 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user