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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user