Skip to content

Commit abbd30b

Browse files
committed
fix(video): add duration, width and height metadata to VideoMessage
Videos sent via /chat/send/video were missing Seconds, Width, and Height fields in the VideoMessage proto, causing recipients to see 0-second duration and inability to play on WhatsApp Desktop and mobile clients. This change: - Adds getVideoMetadata() helper that uses ffprobe to extract video duration and dimensions from the file data - Sets Seconds, Width, and Height on the VideoMessage proto - Accepts optional Seconds/Width/Height in the API payload, which take priority over auto-detected values - Gracefully degrades to current behavior if ffprobe is unavailable
1 parent 73bfbb4 commit abbd30b

File tree

2 files changed

+106
-0
lines changed

2 files changed

+106
-0
lines changed

handlers.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,9 @@ func (s *server) SendVideo() http.HandlerFunc {
14841484
Id string
14851485
JPEGThumbnail []byte
14861486
MimeType string
1487+
Seconds uint32 `json:"Seconds,omitempty"`
1488+
Width uint32 `json:"Width,omitempty"`
1489+
Height uint32 `json:"Height,omitempty"`
14871490
ContextInfo waE2E.ContextInfo
14881491
QuotedMessage *waE2E.Message `json:"QuotedMessage,omitempty"`
14891492
}
@@ -1571,6 +1574,19 @@ func (s *server) SendVideo() http.HandlerFunc {
15711574
return
15721575
}
15731576

1577+
// Extract video metadata (duration, dimensions) via ffprobe.
1578+
// If caller provided Seconds/Width/Height in payload, those take priority.
1579+
videoMeta := getVideoMetadata(filedata)
1580+
if t.Seconds > 0 {
1581+
videoMeta.DurationSeconds = t.Seconds
1582+
}
1583+
if t.Width > 0 {
1584+
videoMeta.Width = t.Width
1585+
}
1586+
if t.Height > 0 {
1587+
videoMeta.Height = t.Height
1588+
}
1589+
15741590
msg := &waE2E.Message{VideoMessage: &waE2E.VideoMessage{
15751591
Caption: proto.String(t.Caption),
15761592
URL: proto.String(uploaded.URL),
@@ -1586,6 +1602,9 @@ func (s *server) SendVideo() http.HandlerFunc {
15861602
FileSHA256: uploaded.FileSHA256,
15871603
FileLength: proto.Uint64(uint64(len(filedata))),
15881604
JPEGThumbnail: t.JPEGThumbnail,
1605+
Seconds: proto.Uint32(videoMeta.DurationSeconds),
1606+
Width: proto.Uint32(videoMeta.Width),
1607+
Height: proto.Uint32(videoMeta.Height),
15891608
}}
15901609

15911610
if t.ContextInfo.StanzaID != nil {

helpers.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"os/exec"
2424
"regexp"
2525
"runtime/debug"
26+
"strconv"
2627
"strings"
2728
"sync"
2829

@@ -772,6 +773,92 @@ func runFFmpegConversion(input []byte, inputExt string, ffmpegArgs func(inPath,
772773
return os.ReadFile(outPath)
773774
}
774775

776+
// VideoMetadata holds extracted video properties (duration, dimensions).
777+
type VideoMetadata struct {
778+
DurationSeconds uint32
779+
Width uint32
780+
Height uint32
781+
}
782+
783+
// getVideoMetadata uses ffprobe to extract duration, width, and height from video data.
784+
// Returns zero values if extraction fails (graceful degradation).
785+
func getVideoMetadata(filedata []byte) VideoMetadata {
786+
meta := VideoMetadata{}
787+
788+
tmpFile, err := os.CreateTemp("", "video-probe-*.mp4")
789+
if err != nil {
790+
log.Warn().Err(err).Msg("getVideoMetadata: failed to create temp file")
791+
return meta
792+
}
793+
defer os.Remove(tmpFile.Name())
794+
795+
if _, err := tmpFile.Write(filedata); err != nil {
796+
tmpFile.Close()
797+
log.Warn().Err(err).Msg("getVideoMetadata: failed to write temp file")
798+
return meta
799+
}
800+
tmpFile.Close()
801+
802+
cmd := exec.Command("ffprobe",
803+
"-v", "quiet",
804+
"-print_format", "json",
805+
"-show_format",
806+
"-show_streams",
807+
"-select_streams", "v:0",
808+
tmpFile.Name(),
809+
)
810+
811+
var stdout, stderr bytes.Buffer
812+
cmd.Stdout = &stdout
813+
cmd.Stderr = &stderr
814+
815+
if err := cmd.Run(); err != nil {
816+
log.Warn().Err(err).Str("stderr", stderr.String()).Msg("getVideoMetadata: ffprobe failed")
817+
return meta
818+
}
819+
820+
var probeResult struct {
821+
Streams []struct {
822+
Width int `json:"width"`
823+
Height int `json:"height"`
824+
Duration string `json:"duration"`
825+
} `json:"streams"`
826+
Format struct {
827+
Duration string `json:"duration"`
828+
} `json:"format"`
829+
}
830+
831+
if err := json.Unmarshal(stdout.Bytes(), &probeResult); err != nil {
832+
log.Warn().Err(err).Msg("getVideoMetadata: failed to parse ffprobe output")
833+
return meta
834+
}
835+
836+
if len(probeResult.Streams) > 0 {
837+
meta.Width = uint32(probeResult.Streams[0].Width)
838+
meta.Height = uint32(probeResult.Streams[0].Height)
839+
if probeResult.Streams[0].Duration != "" {
840+
if dur, err := strconv.ParseFloat(probeResult.Streams[0].Duration, 64); err == nil && dur > 0 {
841+
meta.DurationSeconds = uint32(dur + 0.5)
842+
}
843+
}
844+
}
845+
846+
// Fallback to format-level duration if stream duration was not available
847+
if meta.DurationSeconds == 0 && probeResult.Format.Duration != "" {
848+
if dur, err := strconv.ParseFloat(probeResult.Format.Duration, 64); err == nil && dur > 0 {
849+
meta.DurationSeconds = uint32(dur + 0.5)
850+
}
851+
}
852+
853+
log.Debug().
854+
Uint32("duration", meta.DurationSeconds).
855+
Uint32("width", meta.Width).
856+
Uint32("height", meta.Height).
857+
Msg("getVideoMetadata: extracted video metadata")
858+
859+
return meta
860+
}
861+
775862
func convertVideoStickerToWebP(input []byte) ([]byte, error) {
776863
return runFFmpegConversion(input, ".mp4", func(inPath, outPath string) []string {
777864
return []string{

0 commit comments

Comments
 (0)