From cbe5a74624ed2aa51a1afbafa6d5fd59b3a0239e Mon Sep 17 00:00:00 2001 From: Cacsjep Date: Tue, 23 Jan 2024 18:11:54 +0100 Subject: [PATCH] Follow scaling example from libav, update readme, improve sws Change scaling example to an similar libav example Update readme Add func to UpdateScalingParameters Rename AllocSwsContext to SwsGetContext Using a type for scaling algos/flags --- README.md | 1 + examples/scaling/main.go | 178 +++++++++++---------------------------- sws_context.go | 60 ++++++++++--- sws_context_test.go | 32 +++---- 4 files changed, 113 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 1758ea3..2bd212d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Examples are located in the [examples](examples) directory and mirror as much as |Filtering|[see](examples/filtering/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n5.1.2/doc/examples/filtering_video.c) |Remuxing|[see](examples/remuxing/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n5.1.2/doc/examples/remuxing.c) |Transcoding|[see](examples/transcoding/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n5.1.2/doc/examples/transcoding.c) +|Scaling|[see](examples/scaling/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n5.1.2/doc/examples/scaling_video.c) *Tip: you can use the video sample located in the `testdata` directory for your tests* diff --git a/examples/scaling/main.go b/examples/scaling/main.go index 1538b17..6f2acf5 100644 --- a/examples/scaling/main.go +++ b/examples/scaling/main.go @@ -1,158 +1,76 @@ package main import ( - "errors" "flag" "fmt" + "image/png" "log" - "strings" + "os" "github.com/asticode/go-astiav" ) -var ( - input = flag.String("i", "", "the input path") -) - -type stream struct { - decCodec *astiav.Codec - decCodecContext *astiav.CodecContext - inputStream *astiav.Stream -} - func main() { - // Handle ffmpeg logs - astiav.SetLogLevel(astiav.LogLevelDebug) - astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) { - log.Printf("ffmpeg log: %s (level: %d)\n", strings.TrimSpace(msg), l) - }) - - // Parse flags + var ( + dstFilename string + dstWidth int + dstHeight int + ) + + flag.StringVar(&dstFilename, "output", "", "Output file name") + flag.IntVar(&dstWidth, "w", 0, "Destination width") + flag.IntVar(&dstHeight, "h", 0, "Destination height") flag.Parse() - // Usage - if *input == "" { - log.Println("Usage: -i ") - return - } - - // Alloc packet - pkt := astiav.AllocPacket() - defer pkt.Free() - - // Alloc frame - f := astiav.AllocFrame() - defer f.Free() - - // Alloc input format context - inputFormatContext := astiav.AllocFormatContext() - if inputFormatContext == nil { - log.Fatal(errors.New("main: input format context is nil")) - } - defer inputFormatContext.Free() - - // Open input - if err := inputFormatContext.OpenInput(*input, nil, nil); err != nil { - log.Fatal(fmt.Errorf("main: opening input failed: %w", err)) - } - defer inputFormatContext.CloseInput() - - // Find stream info - if err := inputFormatContext.FindStreamInfo(nil); err != nil { - log.Fatal(fmt.Errorf("main: finding stream info failed: %w", err)) + if dstFilename == "" || dstWidth <= 0 || dstHeight <= 0 { + fmt.Fprintf(os.Stderr, "Usage: %s -output output_file -w W -h H\n", os.Args[0]) + flag.PrintDefaults() + os.Exit(1) } - // Loop through streams - streams := make(map[int]*stream) // Indexed by input stream index - for _, is := range inputFormatContext.Streams() { - // Only process audio or video - if is.CodecParameters().MediaType() != astiav.MediaTypeVideo { - continue - } - - // Create stream - s := &stream{inputStream: is} - - // Find decoder - if s.decCodec = astiav.FindDecoder(is.CodecParameters().CodecID()); s.decCodec == nil { - log.Fatal(errors.New("main: codec is nil")) - } - - // Alloc codec context - if s.decCodecContext = astiav.AllocCodecContext(s.decCodec); s.decCodecContext == nil { - log.Fatal(errors.New("main: codec context is nil")) - } - defer s.decCodecContext.Free() - - // Update codec context - if err := is.CodecParameters().ToCodecContext(s.decCodecContext); err != nil { - log.Fatal(fmt.Errorf("main: updating codec context failed: %w", err)) - } - - // Open codec context - if err := s.decCodecContext.Open(s.decCodec, nil); err != nil { - log.Fatal(fmt.Errorf("main: opening codec context failed: %w", err)) - } - - // Add stream - streams[is.Index()] = s + dstFile, err := os.Create(dstFilename) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not open destination file %s\n", dstFilename) + os.Exit(1) } - - sws_created := false - var sws *astiav.SWSContext + defer dstFile.Close() + + srcW, srcH := 320, 240 + srcPixFmt, dstPixFmt := astiav.PixelFormatYuv420P, astiav.PixelFormatRgba + srcFrame := astiav.AllocFrame() + srcFrame.SetHeight(srcH) + srcFrame.SetWidth(srcW) + srcFrame.SetPixelFormat(srcPixFmt) + srcFrame.AllocBuffer(1) + srcFrame.ImageFillBlack() + defer srcFrame.Free() dstFrame := astiav.AllocFrame() defer dstFrame.Free() - // Loop through packets - for { - // Read frame - if err := inputFormatContext.ReadFrame(pkt); err != nil { - if errors.Is(err, astiav.ErrEof) { - break - } - log.Fatal(fmt.Errorf("main: reading frame failed: %w", err)) - } - - // Get stream - s, ok := streams[pkt.StreamIndex()] - if !ok { - continue - } + swsCtx := astiav.SwsGetContext(srcW, srcH, srcPixFmt, dstWidth, dstHeight, dstPixFmt, astiav.SWS_POINT, dstFrame) + if swsCtx == nil { + fmt.Fprintln(os.Stderr, "Unable to create scale context") + os.Exit(1) + } + defer swsCtx.Free() - // Send packet - if err := s.decCodecContext.SendPacket(pkt); err != nil { - log.Fatal(fmt.Errorf("main: sending packet failed: %w", err)) - } + err = swsCtx.Scale(srcFrame, dstFrame) - // Loop - for { - // Receive frame - if err := s.decCodecContext.ReceiveFrame(f); err != nil { - if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) { - break - } - log.Fatal(fmt.Errorf("main: receiving frame failed: %w", err)) - } + if err != nil { + log.Fatalf("Unable to scale: %w", err) + } - // SWS context alloc on first frame otherwise we dont no the frame w,h and pixel format - if !sws_created { - sws = astiav.AllocSwsContext(f.Width(), f.Height(), f.PixelFormat(), 480, 270, astiav.PixelFormatRgba, astiav.SWS_BILINEAR, dstFrame) - sws_created = true - } + img, err := dstFrame.Data().Image() - err := sws.Scale(f, dstFrame) - if err != nil { - log.Println("Scaling fails") - } else { - // Do something with decoded frame - log.Printf("orig frame: %dx%d %s", f.Width(), f.Height(), f.PixelFormat().String()) - log.Printf("scaled frame: %dx%d %s", dstFrame.Width(), dstFrame.Height(), dstFrame.PixelFormat().String()) - } + if err != nil { + log.Fatalf("Unable to get image: %w", err) + } - } + err = png.Encode(dstFile, img) + if err != nil { + log.Fatalf("Unable to encode image to png: %w", err) } - // Success - log.Println("success") + log.Printf("Successfully scale to %dx%d and write image to: %s", dstWidth, dstHeight, dstFilename) } diff --git a/sws_context.go b/sws_context.go index 654522c..d65242b 100644 --- a/sws_context.go +++ b/sws_context.go @@ -16,25 +16,28 @@ type SWSContext struct { srcH int dstW int dstH int - flags int + flags ScalingAlgorithm dstFrame *Frame } +// https://github.com/FFmpeg/FFmpeg/blob/n5.0/libswscale/swscale.h#L59 +type ScalingAlgorithm int + const ( - SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR - SWS_BILINEAR = C.SWS_BILINEAR - SWS_BICUBIC = C.SWS_BICUBIC - SWS_X = C.SWS_X - SWS_POINT = C.SWS_POINT - SWS_AREA = C.SWS_AREA - SWS_BICUBLIN = C.SWS_BICUBLIN - SWS_GAUSS = C.SWS_GAUSS - SWS_SINC = C.SWS_SINC - SWS_LANCZOS = C.SWS_LANCZOS - SWS_SPLINE = C.SWS_SPLINE + SWS_FAST_BILINEAR ScalingAlgorithm = ScalingAlgorithm(C.SWS_FAST_BILINEAR) + SWS_BILINEAR ScalingAlgorithm = ScalingAlgorithm(C.SWS_BILINEAR) + SWS_BICUBIC ScalingAlgorithm = ScalingAlgorithm(C.SWS_BICUBIC) + SWS_X ScalingAlgorithm = ScalingAlgorithm(C.SWS_X) + SWS_POINT ScalingAlgorithm = ScalingAlgorithm(C.SWS_POINT) + SWS_AREA ScalingAlgorithm = ScalingAlgorithm(C.SWS_AREA) + SWS_BICUBLIN ScalingAlgorithm = ScalingAlgorithm(C.SWS_BICUBLIN) + SWS_GAUSS ScalingAlgorithm = ScalingAlgorithm(C.SWS_GAUSS) + SWS_SINC ScalingAlgorithm = ScalingAlgorithm(C.SWS_SINC) + SWS_LANCZOS ScalingAlgorithm = ScalingAlgorithm(C.SWS_LANCZOS) + SWS_SPLINE ScalingAlgorithm = ScalingAlgorithm(C.SWS_SPLINE) ) -func AllocSwsContext(srcW, srcH int, srcFormat PixelFormat, dstW, dstH int, dstFormat PixelFormat, flags int, dstFrame *Frame) *SWSContext { +func SwsGetContext(srcW, srcH int, srcFormat PixelFormat, dstW, dstH int, dstFormat PixelFormat, flags ScalingAlgorithm, dstFrame *Frame) *SWSContext { dstFrame.SetPixelFormat(dstFormat) dstFrame.SetWidth(dstW) dstFrame.SetHeight(dstH) @@ -72,6 +75,37 @@ func (sc *SWSContext) Scale(srcFrame, dstFrame *Frame) error { return nil } +func (sc *SWSContext) UpdateScalingParameters(dstW, dstH int, dstFormat PixelFormat) error { + if sc.dstW != dstW || sc.dstH != dstH || sc.dstFormat != dstFormat { + sc.dstW = dstW + sc.dstH = dstH + sc.dstFormat = dstFormat + + // Reallocate the destination frame buffer + sc.dstFrame.SetPixelFormat(dstFormat) + sc.dstFrame.SetWidth(dstW) + sc.dstFrame.SetHeight(dstH) + sc.dstFrame.AllocBuffer(1) + + // Update the sws context + sc.c = C.sws_getCachedContext( + sc.c, + C.int(sc.srcW), + C.int(sc.srcH), + C.enum_AVPixelFormat(sc.srcFormat), + C.int(dstW), + C.int(dstH), + C.enum_AVPixelFormat(dstFormat), + C.int(sc.flags), + nil, nil, nil, + ) + if sc.c == nil { + return fmt.Errorf("failed to update sws context") + } + } + return nil +} + func (sc *SWSContext) Free() { C.sws_freeContext(sc.c) } diff --git a/sws_context_test.go b/sws_context_test.go index a67b42c..c966921 100644 --- a/sws_context_test.go +++ b/sws_context_test.go @@ -11,14 +11,10 @@ import ( // Test constants for source and destination dimensions and formats const ( - srcW = 100 - srcH = 100 - dstW = 200 - dstH = 200 - secondDstW = 300 - secondDstH = 300 - srcFormat = astiav.PixelFormatYuv420P - dstFormat = astiav.PixelFormatRgba + srcW = 100 + srcH = 100 + dstW = 200 + dstH = 200 ) // assertImageType is a helper function to check the type of an image. @@ -37,22 +33,18 @@ func TestSWS(t *testing.T) { srcFrame.SetHeight(srcH) srcFrame.SetWidth(srcW) - srcFrame.SetPixelFormat(srcFormat) + srcFrame.SetPixelFormat(astiav.PixelFormatYuv420P) srcFrame.AllocBuffer(1) - srcFrame.ImageFillBlack() // Fill the source frame with black for testing - // Create SWSContext for scaling and verify it's not nil - swsc := astiav.AllocSwsContext(srcW, srcH, srcFormat, dstW, dstH, dstFormat, astiav.SWS_BILINEAR, dstFrame) + swsc := astiav.SwsGetContext(srcW, srcH, astiav.PixelFormatYuv420P, dstW, dstH, astiav.PixelFormatRgba, astiav.SWS_BILINEAR, dstFrame) require.NotNil(t, swsc) - // Perform scaling and verify no errors err := swsc.Scale(srcFrame, dstFrame) require.NoError(t, err) - // Verify the dimensions and format of the destination frame require.Equal(t, dstW, dstFrame.Height()) require.Equal(t, dstH, dstFrame.Width()) - require.Equal(t, dstFormat, dstFrame.PixelFormat()) + require.Equal(t, astiav.PixelFormatRgba, dstFrame.PixelFormat()) // Convert frame data to image and perform additional verifications i1, err := dstFrame.Data().Image() @@ -60,5 +52,15 @@ func TestSWS(t *testing.T) { require.Equal(t, dstW, i1.Bounds().Dx()) require.Equal(t, dstH, i1.Bounds().Dy()) assertImageType(t, i1, reflect.TypeOf((*image.NRGBA)(nil))) + + // Update sws ctx tests + err = swsc.UpdateScalingParameters(50, 50, astiav.PixelFormatRgb24) + require.NoError(t, err) + require.Equal(t, astiav.PixelFormatRgb24, dstFrame.PixelFormat()) + err = swsc.Scale(srcFrame, dstFrame) + require.NoError(t, err) + require.Equal(t, dstFrame.Width(), 50) + require.Equal(t, dstFrame.Height(), 50) + swsc.Free() }