diff --git a/CHANGELOG.md b/CHANGELOG.md index 66746cd..921af57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG - apigo.cc/go/vision +## v1.5.4 (2026-07-18) +- **视频预览修复**: 采样避开低帧率视频无帧的尾部区间,测试在 FFmpeg 未提供 `libwebp` 编码器时正确跳过相关集成项。 +- **依赖校验修复**: 重新整理模块图并补齐 checksum。 + ## v1.5.3 (2026-06-21) - **JS 对齐**: 重构 JS 导出为具名函数,并引入 `jsmod.MakeError` 动态包装错误以获取调用栈。 - **依赖更新**: 升级依赖 `jsmod` 至 `v1.5.3`,`cast` 至 `v1.5.3`,`rand` 至 `v1.5.3`,`encoding` 至 `v1.5.4`,`safe` 至 `v1.5.2`,`file` 至 `v1.5.5`。 diff --git a/TEST.md b/TEST.md index 03c13c3..f8b8179 100644 --- a/TEST.md +++ b/TEST.md @@ -1,5 +1,8 @@ # Testing @go/vision +## v1.5.4 验证 +- 全量测试与图像处理基准测试通过;FFmpeg 能力相关用例按本机编码器能力执行。 + `go/vision` 拥有完善的单元测试覆盖,确保在各种图像处理场景下的稳定性。 ## 运行测试 diff --git a/go.mod b/go.mod index 4b66179..91f55f5 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( ) require ( - apigo.cc/go/encoding v1.5.4 - apigo.cc/go/safe v1.5.2 + apigo.cc/go/encoding v1.5.4 // indirect + apigo.cc/go/safe v1.5.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/kr/text v0.2.0 // indirect golang.org/x/crypto v0.52.0 // indirect diff --git a/go.sum b/go.sum index 47698d6..5fb6c2c 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,15 @@ -apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE= -apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8= -apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I= -apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA= -apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw= -apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0= -apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec= -apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= -apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E= -apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk= -apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk= -apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM= +apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw= +apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4= +apigo.cc/go/encoding v1.5.4 h1:Fk8TrveZATyy8SHukC4ZiqdTSp+QIfsRHtt55xmMK7w= +apigo.cc/go/encoding v1.5.4/go.mod h1:dShEsZ3gKqBINz7TSOYf4e7/fBCqCY9VzlenoGUQUFM= +apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs= +apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo= +apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU= +apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= +apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius= +apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4= +apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg= +apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -32,10 +32,12 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= diff --git a/preview.go b/preview.go index 6b7d589..40668a0 100644 --- a/preview.go +++ b/preview.go @@ -83,7 +83,7 @@ func GenerateVideoPreview(videoPath, outPath string, width, height int, frameInt times := make([]float64, frameCount) for i := 0; i < frameCount; i++ { - times[i] = duration * (0.10 + 0.80*(float64(i)/float64(frameCount-1))) + times[i] = duration * (0.10 + 0.65*(float64(i)/float64(frameCount-1))) } // 2a. 动画模式 (.webp, .gif) @@ -152,7 +152,7 @@ func GenerateAudioPreview(mediaPath, outPath string) error { } func getVideoDuration(videoPath string) (float64, error) { - out, err := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath).Output() + out, err := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath).Output() if err != nil { return 0, err } @@ -160,4 +160,3 @@ func getVideoDuration(videoPath string) (float64, error) { _, err = fmt.Sscanf(strings.TrimSpace(string(out)), "%f", &duration) return duration, err } - diff --git a/preview_test.go b/preview_test.go index dce03b9..952644d 100644 --- a/preview_test.go +++ b/preview_test.go @@ -1,6 +1,7 @@ package vision import ( + "bytes" "fmt" "image/color" "os" @@ -37,20 +38,21 @@ func TestPreviewer(t *testing.T) { if err != nil { t.Fatal(err) } - + // 这里通过 ffmpeg 拼接图片并添加一个静音音频产生视频 err = v.CreateVideoFromImages(filepath.Join(tmpDir, "frame_%d.png"), 1, videoPath) if err != nil { t.Skip("FFmpeg video generation failed, skipping integration test") } - + // 为测试视频添加静音音频,否则提取音频会失败 audioPath := filepath.Join(tmpDir, "silent.aac") err = exec.Command("ffmpeg", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono", "-t", "5", "-c:a", "aac", audioPath).Run() if err == nil { finalVideo := filepath.Join(tmpDir, "final.mp4") - exec.Command("ffmpeg", "-i", videoPath, "-i", audioPath, "-c", "copy", finalVideo).Run() - videoPath = finalVideo + if err = exec.Command("ffmpeg", "-i", videoPath, "-i", audioPath, "-c", "copy", finalVideo).Run(); err == nil { + videoPath = finalVideo + } } // 3. 测试 Preview 功能 @@ -67,7 +69,11 @@ func TestPreviewer(t *testing.T) { }) t.Run("GenerateVideoPreview", func(t *testing.T) { - err := GenerateVideoPreview(videoPath, webPPath, 160, 120) + encoders, err := exec.Command("ffmpeg", "-hide_banner", "-encoders").Output() + if err != nil || !bytes.Contains(encoders, []byte("libwebp")) { + t.Skip("FFmpeg does not provide the libwebp encoder") + } + err = GenerateVideoPreview(videoPath, webPPath, 160, 120) if err != nil { t.Errorf("GenerateVideoPreview failed: %v", err) }