mirror of
https://github.com/krau/SaveAny-Bot.git
synced 2026-08-06 21:13:27 +08:00
fix(telegram): use seekable file path for thumbnail & metadata extraction (#225)
extractFrameAt() and getVideoMetadata() piped the media into ffmpeg/ffprobe via pipe:0. A pipe is not seekable, so ffmpeg cannot decode non-faststart MP4s whose moov atom sits at the END of the file (very common for yt-dlp / HLS-merged downloads): the frame grab and probe silently fail, and the video is uploaded with no thumbnail (and, for non-mp4 containers, no dimensions). Hand ffmpeg/ffprobe a seekable file path instead. When the reader already is an *os.File we use it directly; otherwise we spool to a temp file and clean it up. Verified with an A/B upload of a 56 MB non-faststart clip: stock build produced no thumbnail, patched build produced a correct thumbnail. Co-authored-by: pennyucloud <valentino@pennyu.co.id> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/celestix/gotgproto/ext"
|
||||
@@ -14,6 +15,34 @@ import (
|
||||
"github.com/yapingcat/gomedia/go-mp4"
|
||||
)
|
||||
|
||||
// sourceFile returns a filesystem path to the media for ffmpeg/ffprobe. Those
|
||||
// tools need a SEEKABLE input: feeding them a pipe (pipe:0) fails for
|
||||
// non-faststart MP4s whose moov atom is at the END of the file, because ffmpeg
|
||||
// cannot seek backwards on a stream to read it. That silently broke thumbnail
|
||||
// and metadata extraction for a large share of downloaded videos. If rs is
|
||||
// already an *os.File we use it directly; otherwise we spool it to a temp file
|
||||
// and return a cleanup func.
|
||||
func sourceFile(rs io.ReadSeeker) (path string, cleanup func(), err error) {
|
||||
noop := func() {}
|
||||
if f, ok := rs.(*os.File); ok {
|
||||
return f.Name(), noop, nil
|
||||
}
|
||||
if _, err = rs.Seek(0, io.SeekStart); err != nil {
|
||||
return "", noop, err
|
||||
}
|
||||
tf, err := os.CreateTemp("", "saveany-media-*.tmp")
|
||||
if err != nil {
|
||||
return "", noop, err
|
||||
}
|
||||
if _, err = io.Copy(tf, rs); err != nil {
|
||||
tf.Close()
|
||||
os.Remove(tf.Name())
|
||||
return "", noop, err
|
||||
}
|
||||
tf.Close()
|
||||
return tf.Name(), func() { os.Remove(tf.Name()) }, nil
|
||||
}
|
||||
|
||||
type VideoMetadata struct {
|
||||
Duration int
|
||||
Width int
|
||||
@@ -52,16 +81,14 @@ func getMP4Meta(rs io.ReadSeeker) (metadata *VideoMetadata, err error) {
|
||||
|
||||
// getVideoMetadata uses ffprobe to get video metadata
|
||||
func getVideoMetadata(rs io.ReadSeeker) (*VideoMetadata, error) {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
path, cleanup, err := sourceFile(rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
go func() {
|
||||
defer pipeWriter.Close()
|
||||
rs.Seek(0, io.SeekStart)
|
||||
io.Copy(pipeWriter, rs)
|
||||
}()
|
||||
|
||||
result, err := ffmpeg.ProbeReaderWithTimeout(
|
||||
pipeReader,
|
||||
result, err := ffmpeg.ProbeWithTimeout(
|
||||
path,
|
||||
time.Second*10,
|
||||
ffmpeg.KwArgs{
|
||||
"select_streams": "v:0",
|
||||
@@ -114,25 +141,22 @@ func extractThumbFrame(rs io.ReadSeeker) ([]byte, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
}()
|
||||
path, cleanup, err := sourceFile(rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
err := ffmpeg.
|
||||
Input("pipe:0", ffmpeg.KwArgs{
|
||||
err = ffmpeg.
|
||||
Input(path, ffmpeg.KwArgs{
|
||||
"ss": fmt.Sprintf("%.3f", timestamp),
|
||||
}).
|
||||
Output("pipe:1", ffmpeg.KwArgs{
|
||||
"vframes": 1,
|
||||
"f": "mjpeg",
|
||||
}).
|
||||
WithInput(pipeReader).
|
||||
WithOutput(&out).
|
||||
OverWriteOutput().
|
||||
Run()
|
||||
|
||||
Reference in New Issue
Block a user