mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-07-12 16:02:05 +08:00
feat: auto generate thumbnail for video uploaded to telegram storage
This commit is contained in:
1
storage/telegram/.gitignore
vendored
Normal file
1
storage/telegram/.gitignore
vendored
Normal file
@@ -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 {
|
||||
forceFile = true
|
||||
}
|
||||
docb := message.UploadedDocument(file, caption).
|
||||
doc := message.UploadedDocument(file, caption).
|
||||
Filename(filename).
|
||||
ForceFile(forceFile).
|
||||
MIME(mtype.String())
|
||||
|
||||
var media message.MediaOption = docb
|
||||
var media message.MediaOption = doc
|
||||
|
||||
switch mtypeStr := mtype.String(); {
|
||||
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)
|
||||
switch mtypeStr {
|
||||
case "video/mp4":
|
||||
info, err := getMP4Info(rs)
|
||||
info, err := getMP4Meta(rs)
|
||||
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).
|
||||
Resolution(info.Width, info.Height).
|
||||
SupportsStreaming()
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(mtypeStr, "audio/"):
|
||||
media = docb.Audio().Title(filename)
|
||||
media = doc.Audio().Title(filename)
|
||||
case strings.HasPrefix(mtypeStr, "image/") && !strings.HasSuffix(mtypeStr, "webp"):
|
||||
media = message.UploadedPhoto(file, caption)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/krau/ffmpeg-go"
|
||||
"github.com/yapingcat/gomedia/go-mp4"
|
||||
)
|
||||
|
||||
type MP4Info struct {
|
||||
type VideoMetadata struct {
|
||||
Duration int
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
||||
d := mp4.CreateMp4Demuxer(r)
|
||||
// a go native way to get mp4 video metadata
|
||||
func getMP4Meta(rs io.ReadSeeker) (*VideoMetadata, error) {
|
||||
d := mp4.CreateMp4Demuxer(rs)
|
||||
|
||||
tracks, err := d.ReadHead()
|
||||
if err != nil {
|
||||
@@ -24,7 +29,7 @@ func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
||||
for _, track := range tracks {
|
||||
if track.Cid == mp4.MP4_CODEC_H264 {
|
||||
info := d.GetMp4Info()
|
||||
return &MP4Info{
|
||||
return &VideoMetadata{
|
||||
Duration: int(info.Duration / info.Timescale),
|
||||
Width: int(track.Width),
|
||||
Height: int(track.Height),
|
||||
@@ -34,3 +39,97 @@ func getMP4Info(r io.ReadSeeker) (*MP4Info, error) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
34
storage/telegram/util_test.go
Normal file
34
storage/telegram/util_test.go
Normal file
@@ -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