Switch dashboard to watcher-based multi-disk view, fix transcoding FPS display

- dashboard.html: remove standalone "Mounted Disk" input panel; show all disks
  from GET /api/disks (watcher), auto-refresh every 5s
- detect.go: use avg_frame_rate when r_frame_rate is unrealistic (>120 fps or 0),
  fixes MJPEG/mjpeg showing 90000fps
- transcoder.go: parse fps= from ffmpeg progress output and expose in Progress struct
- copier.go: update task message with real-time encoding fps (@ 45.3 fps),
  clear speed_bps/eta during transcoding to avoid showing stale copy speed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:51:39 +03:00
parent 2bad23da3a
commit 839ff494a4
4 changed files with 198 additions and 185 deletions

View File

@@ -34,13 +34,14 @@ func ProbeVideo(path string) (VideoInfo, error) {
var raw struct {
Streams []struct {
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Width int `json:"width"`
Height int `json:"height"`
RFrameRate string `json:"r_frame_rate"`
BitRate string `json:"bit_rate"`
Channels int `json:"channels"`
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Width int `json:"width"`
Height int `json:"height"`
RFrameRate string `json:"r_frame_rate"`
AvgFrameRate string `json:"avg_frame_rate"`
BitRate string `json:"bit_rate"`
Channels int `json:"channels"`
} `json:"streams"`
Format struct {
Duration string `json:"duration"`
@@ -58,7 +59,12 @@ func ProbeVideo(path string) (VideoInfo, error) {
info.Codec = s.CodecName
info.Width = s.Width
info.Height = s.Height
info.FPS = parseFraction(s.RFrameRate)
// avg_frame_rate надёжнее для MJPEG и кодеков с нестандартным таймбейсом
fps := parseFraction(s.RFrameRate)
if avg := parseFraction(s.AvgFrameRate); avg > 0 && (fps <= 0 || fps > 120) {
fps = avg
}
info.FPS = fps
if br, err := strconv.ParseInt(s.BitRate, 10, 64); err == nil {
info.VideoBitrate = br
}

View File

@@ -21,8 +21,13 @@ type Options struct {
SourceInfo VideoInfo
}
// Transcode запускает ffmpeg. progress вызывается с 0..1 по мере работы.
func Transcode(ctx context.Context, opts Options, progress func(float64)) error {
type Progress struct {
Pct float64 // 0..1
EncodeFPS float64 // текущая скорость кодирования, кадр/с
}
// Transcode запускает ffmpeg. progress вызывается при каждом обновлении прогресса.
func Transcode(ctx context.Context, opts Options, progress func(Progress)) error {
if err := os.MkdirAll(filepath.Dir(opts.Output), 0o755); err != nil {
return err
}
@@ -42,20 +47,29 @@ func Transcode(ctx context.Context, opts Options, progress func(float64)) error
}
// Парсим прогресс из stdout (-progress pipe:1)
if opts.SourceInfo.DurationSec > 0 && progress != nil {
if progress != nil {
var cur Progress
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "out_time_us=") {
switch {
case strings.HasPrefix(line, "out_time_us="):
val := strings.TrimPrefix(line, "out_time_us=")
if us, err := strconv.ParseInt(val, 10, 64); err == nil && us > 0 {
if us, err := strconv.ParseInt(val, 10, 64); err == nil && us > 0 && opts.SourceInfo.DurationSec > 0 {
sec := float64(us) / 1e6
pct := sec / opts.SourceInfo.DurationSec
if pct > 1 {
pct = 1
}
progress(pct)
cur.Pct = pct
}
case strings.HasPrefix(line, "fps="):
val := strings.TrimPrefix(line, "fps=")
if fps, err := strconv.ParseFloat(strings.TrimSpace(val), 64); err == nil && fps > 0 {
cur.EncodeFPS = fps
}
case line == "progress=continue" || line == "progress=end":
progress(cur)
}
}
}