mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-09-07 16:46:37 +08:00
feat: auto generate thumbnail for video uploaded to telegram storage
This commit is contained in:
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/goccy/go-yaml v1.18.0
|
github.com/goccy/go-yaml v1.18.0
|
||||||
github.com/gotd/contrib v0.21.1
|
github.com/gotd/contrib v0.21.1
|
||||||
github.com/gotd/td v0.132.0
|
github.com/gotd/td v0.132.0
|
||||||
|
github.com/krau/ffmpeg-go v0.6.0
|
||||||
github.com/minio/minio-go/v7 v7.0.95
|
github.com/minio/minio-go/v7 v7.0.95
|
||||||
github.com/playwright-community/playwright-go v0.5200.1
|
github.com/playwright-community/playwright-go v0.5200.1
|
||||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/krau/ffmpeg-go v0.6.0 h1:F4HWvOrKXQsfLsFTOnUfP0HY6WISJqOrsAFGSIzkKto=
|
||||||
|
github.com/krau/ffmpeg-go v0.6.0/go.mod h1:sa7/bWHB6fO9j4lhmxnWQ1U07o+dE1leFjhctotxU7A=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
@@ -249,6 +251,7 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
|||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
@@ -290,8 +293,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
@@ -309,8 +310,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@@ -372,6 +371,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
tests/
|
||||||
@@ -137,29 +137,44 @@ func (t *Telegram) Save(ctx context.Context, r io.Reader, storagePath string) er
|
|||||||
if strings.HasPrefix(mtype.String(), "image/") && size >= tglimit.MaxPhotoSize {
|
if strings.HasPrefix(mtype.String(), "image/") && size >= tglimit.MaxPhotoSize {
|
||||||
forceFile = true
|
forceFile = true
|
||||||
}
|
}
|
||||||
docb := message.UploadedDocument(file, caption).
|
doc := message.UploadedDocument(file, caption).
|
||||||
Filename(filename).
|
Filename(filename).
|
||||||
ForceFile(forceFile).
|
ForceFile(forceFile).
|
||||||
MIME(mtype.String())
|
MIME(mtype.String())
|
||||||
|
|
||||||
var media message.MediaOption = docb
|
var media message.MediaOption = doc
|
||||||
|
|
||||||
switch mtypeStr := mtype.String(); {
|
switch mtypeStr := mtype.String(); {
|
||||||
case strings.HasPrefix(mtypeStr, "video/"):
|
case strings.HasPrefix(mtypeStr, "video/"):
|
||||||
media = docb.Video().SupportsStreaming()
|
media = doc.Video().SupportsStreaming()
|
||||||
|
thumb, err := extractThumbFrame(rs)
|
||||||
|
if err == nil {
|
||||||
|
thumb, err := upler.FromBytes(ctx, "thumb.jpg", thumb)
|
||||||
|
if err == nil {
|
||||||
|
doc = doc.Thumb(thumb)
|
||||||
|
}
|
||||||
|
}
|
||||||
rs.Seek(0, io.SeekStart)
|
rs.Seek(0, io.SeekStart)
|
||||||
switch mtypeStr {
|
switch mtypeStr {
|
||||||
case "video/mp4":
|
case "video/mp4":
|
||||||
info, err := getMP4Info(rs)
|
info, err := getMP4Meta(rs)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
media = docb.Video().
|
media = doc.Video().
|
||||||
|
Duration(time.Duration(info.Duration)*time.Second).
|
||||||
|
Resolution(info.Width, info.Height).
|
||||||
|
SupportsStreaming()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
info, err := getVideoMetadata(rs)
|
||||||
|
if err == nil {
|
||||||
|
media = doc.Video().
|
||||||
Duration(time.Duration(info.Duration)*time.Second).
|
Duration(time.Duration(info.Duration)*time.Second).
|
||||||
Resolution(info.Width, info.Height).
|
Resolution(info.Width, info.Height).
|
||||||
SupportsStreaming()
|
SupportsStreaming()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(mtypeStr, "audio/"):
|
case strings.HasPrefix(mtypeStr, "audio/"):
|
||||||
media = docb.Audio().Title(filename)
|
media = doc.Audio().Title(filename)
|
||||||
case strings.HasPrefix(mtypeStr, "image/") && !strings.HasSuffix(mtypeStr, "webp"):
|
case strings.HasPrefix(mtypeStr, "image/") && !strings.HasSuffix(mtypeStr, "webp"):
|
||||||
media = message.UploadedPhoto(file, caption)
|
media = message.UploadedPhoto(file, caption)
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-4
@@ -1,20 +1,25 @@
|
|||||||
package telegram
|
package telegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/krau/ffmpeg-go"
|
||||||
"github.com/yapingcat/gomedia/go-mp4"
|
"github.com/yapingcat/gomedia/go-mp4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MP4Info struct {
|
type VideoMetadata struct {
|
||||||
Duration int
|
Duration int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
// a go native way to get mp4 video metadata
|
||||||
d := mp4.CreateMp4Demuxer(r)
|
func getMP4Meta(rs io.ReadSeeker) (*VideoMetadata, error) {
|
||||||
|
d := mp4.CreateMp4Demuxer(rs)
|
||||||
|
|
||||||
tracks, err := d.ReadHead()
|
tracks, err := d.ReadHead()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -24,7 +29,7 @@ func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
|||||||
for _, track := range tracks {
|
for _, track := range tracks {
|
||||||
if track.Cid == mp4.MP4_CODEC_H264 {
|
if track.Cid == mp4.MP4_CODEC_H264 {
|
||||||
info := d.GetMp4Info()
|
info := d.GetMp4Info()
|
||||||
return &MP4Info{
|
return &VideoMetadata{
|
||||||
Duration: int(info.Duration / info.Timescale),
|
Duration: int(info.Duration / info.Timescale),
|
||||||
Width: int(track.Width),
|
Width: int(track.Width),
|
||||||
Height: int(track.Height),
|
Height: int(track.Height),
|
||||||
@@ -34,3 +39,97 @@ func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
|||||||
|
|
||||||
return nil, fmt.Errorf("no h264 track found")
|
return nil, fmt.Errorf("no h264 track found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getVideoMetadata uses ffprobe to get video metadata
|
||||||
|
func getVideoMetadata(rs io.ReadSeeker) (*VideoMetadata, error) {
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer pipeWriter.Close()
|
||||||
|
rs.Seek(0, io.SeekStart)
|
||||||
|
io.Copy(pipeWriter, rs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
result, err := ffmpeg.ProbeReaderWithTimeout(
|
||||||
|
pipeReader,
|
||||||
|
time.Second*10,
|
||||||
|
ffmpeg.KwArgs{
|
||||||
|
"select_streams": "v:0",
|
||||||
|
"show_entries": "stream=width,height:format=duration",
|
||||||
|
"of": "json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Streams []struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
} `json:"streams"`
|
||||||
|
Format struct {
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
} `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(result), &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换 duration
|
||||||
|
var durationFloat float64
|
||||||
|
if data.Format.Duration != "" {
|
||||||
|
fmt.Sscanf(data.Format.Duration, "%f", &durationFloat)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := &VideoMetadata{
|
||||||
|
Duration: int(durationFloat),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.Streams) > 0 {
|
||||||
|
meta.Width = data.Streams[0].Width
|
||||||
|
meta.Height = data.Streams[0].Height
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractThumbFrame(rs io.ReadSeeker) ([]byte, error) {
|
||||||
|
data, err := extractFrameAt(rs, 1.0)
|
||||||
|
if err == nil && len(data) > 0 {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return extractFrameAt(rs, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFrameAt(rs io.ReadSeeker, timestamp float64) ([]byte, error) {
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer pipeWriter.Close()
|
||||||
|
rs.Seek(0, io.SeekStart)
|
||||||
|
io.Copy(pipeWriter, rs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
|
||||||
|
err := ffmpeg.
|
||||||
|
Input("pipe:0", ffmpeg.KwArgs{
|
||||||
|
"ss": fmt.Sprintf("%.3f", timestamp),
|
||||||
|
}).
|
||||||
|
Output("pipe:1", ffmpeg.KwArgs{
|
||||||
|
"vframes": 1,
|
||||||
|
"f": "mjpeg",
|
||||||
|
}).
|
||||||
|
WithInput(pipeReader).
|
||||||
|
WithOutput(&out).
|
||||||
|
OverWriteOutput().
|
||||||
|
Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractThumbFrame(t *testing.T) {
|
||||||
|
file, err := os.Open("tests/testvideo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open test video: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
thumb, err := extractThumbFrame(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to extract thumb frame: %v", err)
|
||||||
|
}
|
||||||
|
os.WriteFile("tests/testthumb.jpg", thumb, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetVideoMetadata(t *testing.T) {
|
||||||
|
file, err := os.Open("tests/testvideo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open test video: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
meta, err := getVideoMetadata(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get video metadata: %v", err)
|
||||||
|
}
|
||||||
|
if meta.Duration == 0 || meta.Width == 0 || meta.Height == 0 {
|
||||||
|
t.Fatalf("invalid video metadata: %+v", meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user