From d92f02d38e3ca95abf2d8bcb5d9c5a00581bd543 Mon Sep 17 00:00:00 2001 From: stefanodvx <69367859+stefanodvx@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:21:59 +0200 Subject: [PATCH] implement libav in some methods --- ext/instagram/main.go | 10 ++-- ext/instagram/util.go | 7 +-- go.mod | 2 + go.sum | 4 ++ util/av/remux.go | 126 +++++++++++++++++++++++++++++++++++------- util/av/thumbnail.go | 123 +++++++++++++++++++++++++++++++++++------ util/av/videoinfo.go | 43 ++++++++++---- 7 files changed, 259 insertions(+), 56 deletions(-) diff --git a/ext/instagram/main.go b/ext/instagram/main.go index e35ddff..81d06cc 100644 --- a/ext/instagram/main.go +++ b/ext/instagram/main.go @@ -126,12 +126,10 @@ func MediaListFromAPI( } var caption string if !stories { - // caption, err = GetPostCaption(postURL) - // if err != nil { - // return nil, fmt.Errorf("failed to get caption: %w", err) - // } - - // todo: fix this (429 error) + caption, err = GetPostCaption(postURL) + if err != nil { + return nil, fmt.Errorf("failed to get caption: %w", err) + } } for _, item := range details.Items { media := ctx.Extractor.NewMedia( diff --git a/ext/instagram/util.go b/ext/instagram/util.go index 120c79b..6a489d4 100644 --- a/ext/instagram/util.go +++ b/ext/instagram/util.go @@ -56,8 +56,6 @@ func ParseIGramResponse(body []byte) (*IGramResponse, error) { if err := sonic.ConfigFastest.Unmarshal(body, &rawResponse); err != nil { return nil, fmt.Errorf("failed to decode response1: %w", err) } - - switch rawResponse.(type) { case []interface{}: @@ -113,7 +111,6 @@ func GetPostCaption( req.Header.Set("Referer", "https://www.instagram.com/accounts/onetap/?next=%2F") req.Header.Set("Alt-Used", "www.instagram.com") req.Header.Set("Connection", "keep-alive") - req.Header.Set("Cookie", `csrftoken=Ib2Zuvf1y9HkDwXFxkdang; sessionid=8569455296%3AIFQiov2eYfTdSd%3A19%3AAYfVHnaxecWGWhyzxvz60vu5qLn05DyKgN_tTZUXTA; ds_user_id=8569455296; mid=Z_j1vQAEAAGVUE3KuxMR7vBonGBw; ig_did=BC48C8B7-D71B-49EF-8195-F9DE37A57B49; rur="CLN\0548569455296\0541775905137:01f7ebda5b896815e9279bb86a572db6bdc8ebccf3e1f8d5327e2bc5ca187fd5cd932b66"; wd=513x594; datr=x_X4Z_CHqpwtjaRKq7PtCNu3`) req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("Sec-Fetch-Dest", "document") req.Header.Set("Sec-Fetch-Mode", "navigate") @@ -130,7 +127,9 @@ func GetPostCaption( defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to get response: %s", resp.Status) + // return an empty caption + // probably 429 error + return "", nil } body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/go.mod b/go.mod index 184510c..5918598 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( ) require ( + github.com/asticode/go-astikit v0.42.0 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect @@ -29,6 +30,7 @@ require ( require ( github.com/aki237/nscjar v0.0.0-20210417074043-bbb606196143 + github.com/asticode/go-astiav v0.35.1 github.com/aws/aws-sdk-go v1.55.6 // indirect github.com/grafov/m3u8 v0.12.1 github.com/jinzhu/inflection v1.0.0 // indirect diff --git a/go.sum b/go.sum index 598966b..3ac2a7c 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.31 h1:SIkzqC6Nv+znY4NGbWlJceWdns8Q github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.31/go.mod h1:kL1v4iIjlalwm3gCYGvF4NLa3hs+aKEfRkNJvj4aoDU= github.com/aki237/nscjar v0.0.0-20210417074043-bbb606196143 h1:PqRkQZW8lAlK2DnH9iSBfISmDxSChaoNJHwP0p7SD2Y= github.com/aki237/nscjar v0.0.0-20210417074043-bbb606196143/go.mod h1:l0r3UsMujHR1bAYL7R0+6NXkHo/vIe+ja3xLZbUZNb8= +github.com/asticode/go-astiav v0.35.1 h1:jq27Ihf+GXtOTnhzNTcpKrW1iLNRAuPSoarh7/SapYc= +github.com/asticode/go-astiav v0.35.1/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s= +github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= +github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= diff --git a/util/av/remux.go b/util/av/remux.go index b6621a3..89c11b9 100644 --- a/util/av/remux.go +++ b/util/av/remux.go @@ -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 } diff --git a/util/av/thumbnail.go b/util/av/thumbnail.go index 741cff1..dfe23db 100644 --- a/util/av/thumbnail.go +++ b/util/av/thumbnail.go @@ -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") } diff --git a/util/av/videoinfo.go b/util/av/videoinfo.go index d96240c..711507a 100644 --- a/util/av/videoinfo.go +++ b/util/av/videoinfo.go @@ -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 }