Estimate missing audio bitrate in metadata

This commit is contained in:
Stu Leak 2025-12-20 13:29:09 -05:00
parent 5b76da0fdf
commit 43efc84bf6
3 changed files with 81 additions and 1 deletions

View File

@ -766,6 +766,7 @@ This file tracks completed features, fixes, and milestones.
### Recent Fixes ### Recent Fixes
- ✅ Fixed aspect ratio default from 16:9 to Source (dev7) - ✅ Fixed aspect ratio default from 16:9 to Source (dev7)
- ✅ Ranked benchmark results by score and added cancel confirmation - ✅ Ranked benchmark results by score and added cancel confirmation
- ✅ Added estimated audio bitrate fallback when metadata is missing
- ✅ Stabilized video seeking and embedded rendering - ✅ Stabilized video seeking and embedded rendering
- ✅ Improved player window positioning - ✅ Improved player window positioning
- ✅ Fixed clear video functionality - ✅ Fixed clear video functionality

View File

@ -41,6 +41,7 @@ This file tracks upcoming features, improvements, and known issues.
- Lossless option only for H.265/AV1 - Lossless option only for H.265/AV1
- Dynamic dropdown based on codec - Dynamic dropdown based on codec
- Lossless + Target Size mode support - Lossless + Target Size mode support
- Audio bitrate estimation when metadata is missing
## Priority Features for dev20+ ## Priority Features for dev20+

80
main.go
View File

@ -7180,7 +7180,11 @@ func buildMetadataPanel(state *appState, src *videoSource, min fyne.Size) (fyne.
audioBitrate := "--" audioBitrate := "--"
if src.AudioBitrate > 0 { if src.AudioBitrate > 0 {
audioBitrate = fmt.Sprintf("%d kbps", src.AudioBitrate/1000) prefix := ""
if src.AudioBitrateEstimated {
prefix = "~"
}
audioBitrate = fmt.Sprintf("%s%d kbps", prefix, src.AudioBitrate/1000)
} }
// Format advanced metadata // Format advanced metadata
@ -10386,6 +10390,7 @@ type videoSource struct {
AudioCodec string AudioCodec string
Bitrate int // Video bitrate in bits per second Bitrate int // Video bitrate in bits per second
AudioBitrate int // Audio bitrate in bits per second AudioBitrate int // Audio bitrate in bits per second
AudioBitrateEstimated bool
FrameRate float64 FrameRate float64
PixelFormat string PixelFormat string
AudioRate int AudioRate int
@ -10459,6 +10464,11 @@ func probeVideo(path string) (*videoSource, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
var fileSize int64
if info, err := os.Stat(path); err == nil {
fileSize = info.Size()
}
cmd := exec.CommandContext(ctx, "ffprobe", cmd := exec.CommandContext(ctx, "ffprobe",
"-v", "quiet", "-v", "quiet",
"-print_format", "json", "-print_format", "json",
@ -10495,6 +10505,7 @@ func probeVideo(path string) (*videoSource, error) {
Channels int `json:"channels"` Channels int `json:"channels"`
AvgFrameRate string `json:"avg_frame_rate"` AvgFrameRate string `json:"avg_frame_rate"`
FieldOrder string `json:"field_order"` FieldOrder string `json:"field_order"`
Tags map[string]interface{} `json:"tags"`
Disposition struct { Disposition struct {
AttachedPic int `json:"attached_pic"` AttachedPic int `json:"attached_pic"`
} `json:"disposition"` } `json:"disposition"`
@ -10512,7 +10523,9 @@ func probeVideo(path string) (*videoSource, error) {
DisplayName: filepath.Base(path), DisplayName: filepath.Base(path),
Format: utils.FirstNonEmpty(result.Format.Format, result.Format.FormatName), Format: utils.FirstNonEmpty(result.Format.Format, result.Format.FormatName),
} }
var formatBitrate int
if rate, err := utils.ParseInt(result.Format.BitRate); err == nil { if rate, err := utils.ParseInt(result.Format.BitRate); err == nil {
formatBitrate = rate
src.Bitrate = rate src.Bitrate = rate
} }
if durStr := result.Format.Duration; durStr != "" { if durStr := result.Format.Duration; durStr != "" {
@ -10537,6 +10550,7 @@ func probeVideo(path string) (*videoSource, error) {
// Track if we've found the main video stream (not cover art) // Track if we've found the main video stream (not cover art)
foundMainVideo := false foundMainVideo := false
var coverArtStreamIndex int = -1 var coverArtStreamIndex int = -1
var videoStreamBitrate int
for _, stream := range result.Streams { for _, stream := range result.Streams {
switch stream.CodecType { switch stream.CodecType {
@ -10567,6 +10581,9 @@ func probeVideo(path string) (*videoSource, error) {
if stream.PixFmt != "" { if stream.PixFmt != "" {
src.PixelFormat = stream.PixFmt src.PixelFormat = stream.PixFmt
} }
if br, err := utils.ParseInt(stream.BitRate); err == nil && br > 0 {
videoStreamBitrate = br
}
} }
if src.Bitrate == 0 { if src.Bitrate == 0 {
if br, err := utils.ParseInt(stream.BitRate); err == nil { if br, err := utils.ParseInt(stream.BitRate); err == nil {
@ -10582,10 +10599,41 @@ func probeVideo(path string) (*videoSource, error) {
if stream.Channels > 0 { if stream.Channels > 0 {
src.Channels = stream.Channels src.Channels = stream.Channels
} }
if br, err := utils.ParseInt(stream.BitRate); err == nil && br > 0 {
src.AudioBitrate = br
} else if br := parseBitrateTag(stream.Tags); br > 0 {
src.AudioBitrate = br
}
} }
} }
} }
if src.AudioCodec != "" && src.AudioBitrate == 0 {
totalBps := 0
if formatBitrate > 0 {
totalBps = formatBitrate
} else if src.Duration > 0 && fileSize > 0 {
totalBps = int(float64(fileSize*8) / src.Duration)
}
baseVideo := videoStreamBitrate
if baseVideo == 0 && formatBitrate == 0 && src.Bitrate > 0 {
baseVideo = src.Bitrate
}
estimated := 0
if totalBps > 0 && baseVideo > 0 && totalBps > baseVideo {
estimated = totalBps - baseVideo
}
if estimated == 0 {
estimated = defaultAudioBitrate(src.Channels)
}
if estimated > 0 {
src.AudioBitrate = estimated
src.AudioBitrateEstimated = true
}
}
// Extract embedded cover art if present // Extract embedded cover art if present
if coverArtStreamIndex >= 0 { if coverArtStreamIndex >= 0 {
coverPath := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano())) coverPath := filepath.Join(os.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano()))
@ -10608,6 +10656,36 @@ func probeVideo(path string) (*videoSource, error) {
return src, nil return src, nil
} }
func parseBitrateTag(tags map[string]interface{}) int {
if len(tags) == 0 {
return 0
}
keys := []string{"BPS", "BPS-eng", "bit_rate", "variant_bitrate"}
for _, key := range keys {
if val, ok := tags[key]; ok {
if rate, err := utils.ParseInt(fmt.Sprint(val)); err == nil && rate > 0 {
return rate
}
}
}
return 0
}
func defaultAudioBitrate(channels int) int {
switch channels {
case 1:
return 96000
case 2:
return 128000
case 6:
return 256000
case 8:
return 320000
default:
return 128000
}
}
func normalizeTags(tags map[string]interface{}) map[string]string { func normalizeTags(tags map[string]interface{}) map[string]string {
normalized := make(map[string]string, len(tags)) normalized := make(map[string]string, len(tags))
for k, v := range tags { for k, v := range tags {