Skip to content

Commit

Permalink
Follow scaling example from libav, update readme, improve sws
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Cacsjep committed Jan 23, 2024
1 parent e66d604 commit cbe5a74
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 158 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down
178 changes: 48 additions & 130 deletions examples/scaling/main.go
Original file line number Diff line number Diff line change
@@ -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: <binary path> -i <input path>")
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)

Check failure on line 61 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

log.Fatalf does not support error-wrapping directive %w

Check failure on line 61 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (macos-latest)

log.Fatalf does not support error-wrapping directive %w

Check failure on line 61 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (windows-latest)

log.Fatalf does not support error-wrapping directive %w
}

// 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)

Check failure on line 67 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

log.Fatalf does not support error-wrapping directive %w

Check failure on line 67 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (macos-latest)

log.Fatalf does not support error-wrapping directive %w

Check failure on line 67 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (windows-latest)

log.Fatalf does not support error-wrapping directive %w
}

}
err = png.Encode(dstFile, img)
if err != nil {
log.Fatalf("Unable to encode image to png: %w", err)

Check failure on line 72 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

log.Fatalf does not support error-wrapping directive %w

Check failure on line 72 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (macos-latest)

log.Fatalf does not support error-wrapping directive %w

Check failure on line 72 in examples/scaling/main.go

View workflow job for this annotation

GitHub Actions / test (windows-latest)

log.Fatalf does not support error-wrapping directive %w
}

// Success
log.Println("success")
log.Printf("Successfully scale to %dx%d and write image to: %s", dstWidth, dstHeight, dstFilename)
}
60 changes: 47 additions & 13 deletions sws_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
32 changes: 17 additions & 15 deletions sws_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -37,28 +33,34 @@ 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()
require.NoError(t, err)
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()
}

0 comments on commit cbe5a74

Please sign in to comment.