implement libav in some methods

This commit is contained in:
stefanodvx 2025-04-17 16:21:59 +02:00
parent 9489689dbf
commit d92f02d38e
7 changed files with 259 additions and 56 deletions

View file

@ -2,31 +2,117 @@ package av
import (
"fmt"
"os"
"path/filepath"
"strings"
ffmpeg "github.com/u2takey/ffmpeg-go"
"github.com/asticode/go-astiav"
)
func RemuxFile(
inputFile string,
) error {
tempFileName := inputFile + ".temp"
outputFile := inputFile
err := os.Rename(inputFile, tempFileName)
if err != nil {
return fmt.Errorf("failed to rename file: %w", err)
func RemuxFile(inputFile string) error {
ext := strings.ToLower(filepath.Ext(inputFile))
var muxerName string
switch ext {
case ".mp4":
muxerName = "mp4"
case ".mkv":
muxerName = "matroska"
case ".mov":
muxerName = "mov"
case ".avi":
muxerName = "avi"
default:
return fmt.Errorf("unsupported output container for extension: %s", ext)
}
defer os.Remove(tempFileName)
err = ffmpeg.
Input(tempFileName).
Output(outputFile, ffmpeg.KwArgs{
"c": "copy",
}).
Silent(true).
OverWriteOutput().
Run()
outputFile := strings.TrimSuffix(inputFile, ext) + ".remuxed" + ext
inputCtx := astiav.AllocFormatContext()
if inputCtx == nil {
return fmt.Errorf("failed to alloc input format context")
}
defer inputCtx.Free()
if err := inputCtx.OpenInput(inputFile, nil, nil); err != nil {
return fmt.Errorf("failed to open input: %w", err)
}
defer inputCtx.CloseInput()
if err := inputCtx.FindStreamInfo(nil); err != nil {
return fmt.Errorf("failed to find stream info: %w", err)
}
outCtx, err := astiav.AllocOutputFormatContext(nil, muxerName, outputFile)
if err != nil {
return fmt.Errorf("failed to remux file: %w", err)
return fmt.Errorf("failed to alloc output format context: %w", err)
}
defer outCtx.Free()
inToOutIdx := make(map[int]int)
for inIdx, inStream := range inputCtx.Streams() {
inCP := inStream.CodecParameters()
if inCP.CodecID() == astiav.CodecIDNone {
continue
}
if mt := inCP.MediaType(); mt != astiav.MediaTypeVideo && mt != astiav.MediaTypeAudio && mt != astiav.MediaTypeSubtitle {
continue
}
outStream := outCtx.NewStream(nil)
if outStream == nil {
return fmt.Errorf("failed to create new stream in output context")
}
if err := inCP.Copy(outStream.CodecParameters()); err != nil {
return fmt.Errorf("failed to copy codec parameters: %w", err)
}
outStream.CodecParameters().SetCodecTag(0)
outStream.SetTimeBase(inStream.TimeBase())
inToOutIdx[inIdx] = len(inToOutIdx)
}
if len(inToOutIdx) == 0 {
return fmt.Errorf("no supported streams to remux")
}
if !outCtx.OutputFormat().Flags().Has(astiav.IOFormatFlagNofile) {
ioCtx, err := astiav.OpenIOContext(outputFile, astiav.NewIOContextFlags(astiav.IOContextFlagWrite), nil, nil)
if err != nil {
return fmt.Errorf("failed to open output IO context: %w", err)
}
defer ioCtx.Close()
outCtx.SetPb(ioCtx)
}
if err := outCtx.WriteHeader(nil); err != nil {
return fmt.Errorf("failed to write output header: %w", err)
}
packet := astiav.AllocPacket()
defer packet.Free()
for {
if err := inputCtx.ReadFrame(packet); err != nil {
if err == astiav.ErrEof {
break
}
return fmt.Errorf("error reading frame: %w", err)
}
outIdx, ok := inToOutIdx[packet.StreamIndex()]
if !ok {
packet.Unref()
continue
}
inStream := inputCtx.Streams()[packet.StreamIndex()]
outStream := outCtx.Streams()[outIdx]
packet.SetPts(astiav.RescaleQRnd(packet.Pts(), inStream.TimeBase(), outStream.TimeBase(), astiav.RoundingNearInf))
packet.SetDts(astiav.RescaleQRnd(packet.Dts(), inStream.TimeBase(), outStream.TimeBase(), astiav.RoundingNearInf))
packet.SetDuration(astiav.RescaleQ(packet.Duration(), inStream.TimeBase(), outStream.TimeBase()))
packet.SetStreamIndex(outIdx)
packet.SetPos(-1)
if err := outCtx.WriteInterleavedFrame(packet); err != nil {
packet.Unref()
return fmt.Errorf("error writing frame: %w", err)
}
packet.Unref()
}
if err := outCtx.WriteTrailer(); err != nil {
return fmt.Errorf("failed to write trailer: %w", err)
}
return nil
}

View file

@ -1,24 +1,115 @@
package av
import (
ffmpeg "github.com/u2takey/ffmpeg-go"
"errors"
"fmt"
"image/jpeg"
"os"
"time"
"github.com/asticode/go-astiav"
)
func ExtractVideoThumbnail(
videoPath string,
thumbnailPath string,
) error {
err := ffmpeg.
Input(videoPath).
Output(thumbnailPath, ffmpeg.KwArgs{
"vframes": 1,
"ss": "00:00:01",
}).
Silent(true).
OverWriteOutput().
Run()
func ExtractVideoThumbnail(videoPath string, imagePath string) error {
formatCtx := astiav.AllocFormatContext()
defer formatCtx.Free()
err := formatCtx.OpenInput(videoPath, nil, nil)
if err != nil {
return err
return fmt.Errorf("failed opening input: %w", err)
}
return nil
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 fmt.Errorf("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 fmt.Errorf("timeout while waiting for video frame")
}

View file

@ -1,18 +1,41 @@
package av
import (
"github.com/tidwall/gjson"
ffmpeg "github.com/u2takey/ffmpeg-go"
)
import "github.com/asticode/go-astiav"
func GetVideoInfo(filePath string) (int64, int64, int64) {
probeData, err := ffmpeg.Probe(filePath)
if err != nil {
formatCtx := astiav.AllocFormatContext()
if formatCtx == nil {
return 0, 0, 0
}
duration := gjson.Get(probeData, "format.duration").Float()
width := gjson.Get(probeData, "streams.0.width").Int()
height := gjson.Get(probeData, "streams.0.height").Int()
defer formatCtx.Free()
return int64(duration), width, height
if err := formatCtx.OpenInput(filePath, nil, nil); err != nil {
return 0, 0, 0
}
defer formatCtx.CloseInput()
if err := formatCtx.FindStreamInfo(nil); err != nil {
return 0, 0, 0
}
var width, height int64
found := false
for _, stream := range formatCtx.Streams() {
if stream.CodecParameters().MediaType() == astiav.MediaTypeVideo {
width = int64(stream.CodecParameters().Width())
height = int64(stream.CodecParameters().Height())
found = true
break
}
}
if !found {
return 0, 0, 0
}
// get duration in seconds
duration := formatCtx.Duration()
durationSeconds := duration / int64(astiav.TimeBase)
return durationSeconds, width, height
}