117 lines
2.7 KiB
Go
117 lines
2.7 KiB
Go
package av
|
|
|
|
import (
|
|
"fmt"
|
|
"image/jpeg"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/asticode/go-astiav"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func ExtractVideoThumbnail(videoPath string, imagePath string) error {
|
|
astiav.SetLogLevel(astiav.LogLevelQuiet)
|
|
|
|
formatCtx := astiav.AllocFormatContext()
|
|
defer formatCtx.Free()
|
|
|
|
err := formatCtx.OpenInput(videoPath, nil, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed opening input: %w", err)
|
|
}
|
|
|
|
err = formatCtx.FindStreamInfo(nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed finding stream info: %w", err)
|
|
}
|
|
|
|
stream, codec, err := formatCtx.FindBestStream(
|
|
astiav.MediaTypeVideo, -1, -1)
|
|
if err != nil {
|
|
return fmt.Errorf("failed finding best video stream: %w", err)
|
|
}
|
|
|
|
codecParameters := stream.CodecParameters()
|
|
|
|
decoder := astiav.FindDecoder(codec.ID())
|
|
if decoder == nil {
|
|
return fmt.Errorf("no decoder found for codec %s", codec.String())
|
|
}
|
|
|
|
codecCtx := astiav.AllocCodecContext(decoder)
|
|
defer codecCtx.Free()
|
|
|
|
err = codecParameters.ToCodecContext(codecCtx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed setting codec parameters: %w", err)
|
|
}
|
|
|
|
err = codecCtx.Open(decoder, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed opening codec: %w", err)
|
|
}
|
|
|
|
packet := astiav.AllocPacket()
|
|
defer packet.Free()
|
|
frame := astiav.AllocFrame()
|
|
defer frame.Free()
|
|
|
|
startTime := time.Now()
|
|
timeout := 5 * time.Second
|
|
|
|
// read frames until we find a video frame or timeout
|
|
for time.Since(startTime) < timeout {
|
|
err := formatCtx.ReadFrame(packet)
|
|
if err != nil {
|
|
if errors.Is(err, astiav.ErrEof) {
|
|
return errors.New("end of file reached before finding video frame")
|
|
}
|
|
return fmt.Errorf("failed reading frame: %w", err)
|
|
}
|
|
|
|
if packet.StreamIndex() != stream.Index() {
|
|
packet.Unref()
|
|
continue
|
|
}
|
|
|
|
err = codecCtx.SendPacket(packet)
|
|
if err != nil {
|
|
return fmt.Errorf("failed sending packet to decoder: %w", err)
|
|
}
|
|
|
|
err = codecCtx.ReceiveFrame(frame)
|
|
if err != nil {
|
|
if errors.Is(err, astiav.ErrEagain) || errors.Is(err, astiav.ErrEof) {
|
|
packet.Unref()
|
|
continue
|
|
}
|
|
return fmt.Errorf("failed receiving frame from decoder: %w", err)
|
|
}
|
|
|
|
img, err := frame.Data().GuessImageFormat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed guessing image format: %w", err)
|
|
}
|
|
err = frame.Data().ToImage(img)
|
|
if err != nil {
|
|
return fmt.Errorf("failed converting frame to image: %w", err)
|
|
}
|
|
packet.Unref()
|
|
|
|
file, err := os.Create(imagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed creating image file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
err = jpeg.Encode(file, img, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed encoding image: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return errors.New("timeout while waiting for video frame")
|
|
}
|