add: processing controls — start/hold, pause/resume, retry

Three lifecycle controls on the dashboard, sharing one /api surface and the
status feed:

- Start/hold gate (#11): the watcher tracks the queue but holds processing
  until POST /api/start. Held by default; AV1DAE_AUTOSTART=1 restores start-
  on-boot. NOTE: flips the previous auto-start default.
- Pause/resume the active encode (#10): SIGSTOP/SIGCONT on the ffmpeg process
  (libsvtav1 is in-process, so one signal suspends all its threads). Resumes
  exactly where it left off; the tracker excludes paused time from elapsed.
- Retry a failed file (#3): POST /api/retry?file=NAME moves it from failed/
  back to input/, with a base-name guard against path traversal.

/status now reports running + the failed list; snapshots carry a paused flag.
Verified live: held queue, retry move, traversal -> 400, and a real encode
suspending to process state T on pause and S on resume.

Closes #3
Closes #10
Closes #11
This commit is contained in:
Esa Kataja
2026-06-21 20:38:21 +03:00
parent 8fc00c96b9
commit 04d6c85712
8 changed files with 349 additions and 29 deletions
+59
View File
@@ -5,12 +5,15 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
"av1dae/internal/logger"
@@ -26,6 +29,59 @@ type Encoder struct {
opusencPath string
log *logger.Logger
tracker *status.Tracker
procMu sync.Mutex
cur *os.Process // the in-flight video encode, for pause/resume
}
// Pause freezes the active video encode in place via SIGSTOP. libsvtav1 runs in
// the ffmpeg process (no forked children), so one signal suspends all its
// threads. No-op if nothing is encoding. The process keeps its memory and
// partial output and resumes exactly where it left off.
func (e *Encoder) Pause() error {
e.procMu.Lock()
defer e.procMu.Unlock()
if e.cur == nil {
return nil
}
if err := e.cur.Signal(syscall.SIGSTOP); err != nil {
return err
}
if e.tracker != nil {
e.tracker.SetPaused(true)
}
return nil
}
// Resume thaws a paused encode via SIGCONT. No-op if nothing is encoding.
func (e *Encoder) Resume() error {
e.procMu.Lock()
defer e.procMu.Unlock()
if e.cur == nil {
return nil
}
if err := e.cur.Signal(syscall.SIGCONT); err != nil {
return err
}
if e.tracker != nil {
e.tracker.SetPaused(false)
}
return nil
}
func (e *Encoder) setProc(p *os.Process) {
e.procMu.Lock()
e.cur = p
e.procMu.Unlock()
}
func (e *Encoder) clearProc() {
e.procMu.Lock()
e.cur = nil
e.procMu.Unlock()
if e.tracker != nil {
e.tracker.SetPaused(false)
}
}
type StreamInfo struct {
@@ -94,6 +150,9 @@ func (e *Encoder) runCmdProgress(ctx context.Context, label, file, name string,
if err := cmd.Start(); err != nil {
return nil, err
}
// Publish the process so Pause/Resume can signal it; clear on exit.
e.setProc(cmd.Process)
defer e.clearProc()
// Reads stdout to EOF (when ffmpeg exits), so Wait below is safe afterwards.
var lastLog time.Time