diff --git a/internal/copier/copier.go b/internal/copier/copier.go
index d235d11..675cbe6 100644
--- a/internal/copier/copier.go
+++ b/internal/copier/copier.go
@@ -393,23 +393,26 @@ func (c *Copier) processVideo(ctx context.Context, taskID string, database *db.D
ext := transcoder.OutputExt(profile.OutputFormat)
dstTranscoded := strings.TrimSuffix(dst, filepath.Ext(dst)) + ext
- srcFPS := fmt.Sprintf("%.2f", info.FPS)
- msg := fmt.Sprintf("Transcoding %s (%s/%dch/%sfps → %s/%s/%dfps %s)",
- filepath.Base(src),
- info.Codec, info.AudioChannels, srcFPS,
- profile.VideoCodec, profile.AudioCodec, profile.MaxFPS, profile.OutputFormat,
- )
+ srcInfo := fmt.Sprintf("%s/%dch/%.0ffps", info.Codec, info.AudioChannels, info.FPS)
+ dstInfo := fmt.Sprintf("%s/%s/%dfps %s", profile.VideoCodec, profile.AudioCodec, profile.MaxFPS, profile.OutputFormat)
+ baseMsg := fmt.Sprintf("Transcoding %s (%s → %s)", filepath.Base(src), srcInfo, dstInfo)
+
c.tasks.Update(taskID, func(t *task.Task) {
t.Phase = task.PhaseTranscoding
- t.Message = msg
+ t.Message = baseMsg
})
if t, ok := c.tasks.Get(taskID); ok {
_ = database.UpdateTask(*t)
}
- progressFn := func(pct float64) {
+ progressFn := func(p transcoder.Progress) {
c.tasks.Update(taskID, func(t *task.Task) {
- t.Progress = int(pct * 100)
+ t.Progress = int(p.Pct * 100)
+ t.SpeedBPS = 0
+ t.ETASec = 0
+ if p.EncodeFPS > 0 {
+ t.Message = fmt.Sprintf("%s @ %.1f fps", baseMsg, p.EncodeFPS)
+ }
})
}
diff --git a/internal/transcoder/detect.go b/internal/transcoder/detect.go
index 4264534..f9a7b8e 100644
--- a/internal/transcoder/detect.go
+++ b/internal/transcoder/detect.go
@@ -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
}
diff --git a/internal/transcoder/transcoder.go b/internal/transcoder/transcoder.go
index 5ea48f4..bab83ef 100644
--- a/internal/transcoder/transcoder.go
+++ b/internal/transcoder/transcoder.go
@@ -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)
}
}
}
diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html
index 6b6856c..87bc00a 100644
--- a/web/templates/dashboard.html
+++ b/web/templates/dashboard.html
@@ -1,20 +1,16 @@
{{define "content"}}
Mounted Disk
+ Disks