diff --git a/README.md b/README.md index 6ebe16d..b87ab35 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Examples are located in the [examples](examples) directory and mirror as much as |Custom IO Demuxing|[see](examples/custom_io_demuxing/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/avio_reading.c) |Demuxing/Decoding|[see](examples/demuxing_decoding/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/demuxing_decoding.c) |Filtering|[see](examples/filtering/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/filtering_video.c) +|Frame data manipulating|[see](examples/frame_data_manipulating/main.go)|X |Hardware Decoding|[see](examples/hardware_decoding/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/hw_decode.c) |Remuxing|[see](examples/remuxing/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/remuxing.c) |Scaling|[see](examples/scaling/main.go)|[see](https://github.com/FFmpeg/FFmpeg/blob/n7.0/doc/examples/scaling_video.c) diff --git a/examples/demuxing_decoding/main.go b/examples/demuxing_decoding/main.go index c2b941d..5fce9e0 100644 --- a/examples/demuxing_decoding/main.go +++ b/examples/demuxing_decoding/main.go @@ -4,7 +4,6 @@ import ( "errors" "flag" "fmt" - "image" "log" "strings" @@ -107,7 +106,6 @@ func main() { } // Loop through packets - var i image.Image for { // Read frame if err := inputFormatContext.ReadFrame(pkt); err != nil { @@ -138,35 +136,8 @@ func main() { log.Fatal(fmt.Errorf("main: receiving frame failed: %w", err)) } - // Do something with decoded frame - if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeVideo { - // In this example, we'll process the frame data but you can do whatever you feel like - // with the decoded frame - fd := f.Data() - - // Image has not yet been initialized - // If the image format can change in the stream, you'll need to guess image format for every frame - if i == nil { - // Guess image format - // It might not return an image.Image in the proper format for your use case, in that case - // you can skip this step and provide .ToImage() with your own image.Image - var err error - if i, err = fd.GuessImageFormat(); err != nil { - log.Fatal(fmt.Errorf("main: guessing image format failed: %w", err)) - } - } - - // Copy frame data to the image - if err := fd.ToImage(i); err != nil { - log.Fatal(fmt.Errorf("main: copying frame data to the image failed: %w", err)) - } - - // Log - log.Printf("new video frame: stream %d - pts: %d - size: %dx%d - color at (0,0): %+v", pkt.StreamIndex(), f.Pts(), i.Bounds().Dx(), i.Bounds().Dy(), i.At(0, 0)) - } else { - // Log - log.Printf("new audio frame: stream %d - pts: %d", pkt.StreamIndex(), f.Pts()) - } + // Log + log.Printf("new %s frame: stream %d - pts: %d", s.inputStream.CodecParameters().MediaType(), pkt.StreamIndex(), f.Pts()) } } diff --git a/examples/frame_data_manipulating/main.go b/examples/frame_data_manipulating/main.go new file mode 100644 index 0000000..ab0df60 --- /dev/null +++ b/examples/frame_data_manipulating/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "image/png" + "log" + "os" + "strings" + + "github.com/asticode/go-astiav" +) + +const ( + align = 1 + pngPath = "testdata/image-rgba.png" + rawBufferPath = "testdata/image-rgba.rgba" +) + +func main() { + // Handle ffmpeg logs + astiav.SetLogLevel(astiav.LogLevelDebug) + astiav.SetLogCallback(func(c astiav.Classer, l astiav.LogLevel, fmt, msg string) { + var cs string + if c != nil { + if cl := c.Class(); cl != nil { + cs = " - class: " + cl.String() + } + } + log.Printf("ffmpeg log: %s%s - level: %d\n", strings.TrimSpace(msg), cs, l) + }) + + // Alloc frames + f1 := astiav.AllocFrame() + defer f1.Free() + f2 := astiav.AllocFrame() + defer f2.Free() + + // To write data manually into a frame, proper attributes need to be set and allocated + for _, f := range []*astiav.Frame{f1, f2} { + // Set attributes + f.SetHeight(256) + f.SetPixelFormat(astiav.PixelFormatRgba) + f.SetWidth(256) + + // Alloc buffer + if err := f.AllocBuffer(align); err != nil { + log.Fatal(fmt.Errorf("main: allocating buffer failed: %w", err)) + } + + // Alloc image + if err := f.AllocImage(align); err != nil { + log.Fatal(fmt.Errorf("main: allocating image failed: %w", err)) + } + } + + // When writing data manually into a frame, you usually need to make sure the frame is writable + // Don't forget this step above all if the frame's buffer is referenced elsewhere + for _, f := range []*astiav.Frame{f1, f2} { + // Make writable + if err := f.MakeWritable(); err != nil { + log.Fatal(fmt.Errorf("main: making frame writable failed: %w", err)) + } + } + + // As an example, we're going to write data manually into the first frame based on a buffer (i.e. raw data) + b, err := os.ReadFile(rawBufferPath) + if err != nil { + log.Fatal(fmt.Errorf("main: reading %s failed: %w", rawBufferPath, err)) + } + if err := f1.Data().SetBytes(b, align); err != nil { + log.Fatal(fmt.Errorf("main: setting frame's data based on bytes failed: %w", err)) + } + + // As an example, we're going to write data manually into the second frame based on a Go image + fl1, err := os.Open(pngPath) + if err != nil { + log.Fatal(fmt.Errorf("main: opening %s failed: %w", pngPath, err)) + } + defer fl1.Close() + i1, err := png.Decode(fl1) + if err != nil { + log.Fatal(fmt.Errorf("main: decoding %s failed: %w", pngPath, err)) + } + if err := f2.Data().FromImage(i1); err != nil { + log.Fatal(fmt.Errorf("main: setting frame's data based on Go image failed: %w", err)) + } + + // This is the place where you do stuff with the frames + + // As an example, we're going to read the first frame's data as a buffer (i.e. raw data) + if _, err = f1.Data().Bytes(align); err != nil { + log.Fatal(fmt.Errorf("main: getting frame's data as bytes failed: %w", err)) + } + + // As an example, we're going to read the second frame's data as a Go image + // For that we first need to guess the Go image format based on the frame's attributes before providing + // it to .ToImage(). You may not need this and can provide your own image.Image to .ToImage() + i2, err := f2.Data().GuessImageFormat() + if err != nil { + log.Fatal(fmt.Errorf("main: guessing image format failed: %w", err)) + } + if err := f2.Data().ToImage(i2); err != nil { + log.Fatal(fmt.Errorf("main: getting frame's data as Go image failed: %w", err)) + } + + // Success + log.Println("success") +} diff --git a/frame.go b/frame.go index 649be50..28eded4 100644 --- a/frame.go +++ b/frame.go @@ -90,7 +90,7 @@ func (f *Frame) SetKeyFrame(k bool) { } func (f *Frame) ImageBufferSize(align int) (int, error) { - ret := C.av_image_get_buffer_size((C.enum_AVSampleFormat)(f.c.format), f.c.width, f.c.height, C.int(align)) + ret := C.av_image_get_buffer_size((C.enum_AVPixelFormat)(f.c.format), f.c.width, f.c.height, C.int(align)) if err := newError(ret); err != nil { return 0, err } @@ -98,7 +98,7 @@ func (f *Frame) ImageBufferSize(align int) (int, error) { } func (f *Frame) ImageCopyToBuffer(b []byte, align int) (int, error) { - ret := C.av_image_copy_to_buffer((*C.uint8_t)(unsafe.Pointer(&b[0])), C.int(len(b)), &f.c.data[0], &f.c.linesize[0], (C.enum_AVSampleFormat)(f.c.format), f.c.width, f.c.height, C.int(align)) + ret := C.av_image_copy_to_buffer((*C.uint8_t)(unsafe.Pointer(&b[0])), C.int(len(b)), &f.c.data[0], &f.c.linesize[0], (C.enum_AVPixelFormat)(f.c.format), f.c.width, f.c.height, C.int(align)) if err := newError(ret); err != nil { return 0, err } @@ -224,3 +224,11 @@ func (f *Frame) MoveRef(src *Frame) { func (f *Frame) UnsafePointer() unsafe.Pointer { return unsafe.Pointer(f.c) } + +func (f *Frame) IsWritable() bool { + return C.av_frame_is_writable(f.c) > 0 +} + +func (f *Frame) MakeWritable() error { + return newError(C.av_frame_make_writable(f.c)) +} diff --git a/frame_data.go b/frame_data.go index 2571b05..30e795e 100644 --- a/frame_data.go +++ b/frame_data.go @@ -1,6 +1,7 @@ package astiav //#include +//#include //#include "macros.h" import "C" import ( @@ -8,6 +9,7 @@ import ( "fmt" "image" "strings" + "unsafe" ) type FrameData struct { @@ -16,9 +18,10 @@ type FrameData struct { type frameDataFramer interface { bytes(align int) ([]byte, error) + copyPlanes(ps []frameDataPlane) error height() int pixelFormat() PixelFormat - planes() ([]frameDataPlane, error) + planes(b []byte, align int) ([]frameDataPlane, error) width() int } @@ -35,6 +38,21 @@ func (d *FrameData) Bytes(align int) ([]byte, error) { return d.f.bytes(align) } +// It's the developer's responsibility to handle frame's writability +func (d *FrameData) SetBytes(b []byte, align int) error { + // Get planes + planes, err := d.f.planes(b, align) + if err != nil { + return fmt.Errorf("astiav: getting planes failed: %w", err) + } + + // Copy planes + if err := d.f.copyPlanes(planes); err != nil { + return fmt.Errorf("astiav: copying planes failed: %w", err) + } + return nil +} + // Always returns non-premultiplied formats when dealing with alpha channels, however this might not // always be accurate. In this case, use your own format in .ToImage() func (d *FrameData) GuessImageFormat() (image.Image, error) { @@ -118,41 +136,113 @@ func (d *FrameData) toImageYCbCrA(y, cb, cr, a *[]uint8, yStride, cStride, aStri } func (d *FrameData) ToImage(dst image.Image) error { + // Get bytes + // Using bytesFromC on f.c.data caused random segfaults + const align = 1 + b, err := d.f.bytes(align) + if err != nil { + return fmt.Errorf("astiav: getting bytes failed: %w", err) + } + // Get planes - planes, err := d.f.planes() + planes, err := d.f.planes(b, align) if err != nil { return fmt.Errorf("astiav: getting planes failed: %w", err) } // Update image - if v, ok := dst.(*image.Alpha); ok { + switch v := dst.(type) { + case *image.Alpha: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.Alpha16); ok { + case *image.Alpha16: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.CMYK); ok { + case *image.CMYK: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.Gray); ok { + case *image.Gray: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.Gray16); ok { + case *image.Gray16: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.NRGBA); ok { + case *image.NRGBA: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.NRGBA64); ok { + case *image.NRGBA64: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.NYCbCrA); ok { + case *image.NYCbCrA: d.toImageYCbCrA(&v.Y, &v.Cb, &v.Cr, &v.A, &v.YStride, &v.CStride, &v.AStride, &v.SubsampleRatio, &v.Rect, planes) - } else if v, ok := dst.(*image.RGBA); ok { + case *image.RGBA: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.RGBA64); ok { + case *image.RGBA64: d.toImagePix(&v.Pix, &v.Stride, &v.Rect, planes) - } else if v, ok := dst.(*image.YCbCr); ok { + case *image.YCbCr: d.toImageYCbCr(&v.Y, &v.Cb, &v.Cr, &v.YStride, &v.CStride, &v.SubsampleRatio, &v.Rect, planes) - } else { + default: return errors.New("astiav: image format is not handled") } return nil } +func (d *FrameData) fromImagePix(pix []uint8, stride int) error { + // Copy planes + if err := d.f.copyPlanes([]frameDataPlane{{bytes: pix, linesize: stride}}); err != nil { + return fmt.Errorf("astiav: copying planes failed: %w", err) + } + return nil +} + +func (d *FrameData) fromImageYCbCr(y, cb, cr []uint8, yStride, cStride int) error { + // Copy planes + if err := d.f.copyPlanes([]frameDataPlane{ + {bytes: y, linesize: yStride}, + {bytes: cb, linesize: cStride}, + {bytes: cr, linesize: cStride}, + }); err != nil { + return fmt.Errorf("astiav: copying planes failed: %w", err) + } + return nil +} + +func (d *FrameData) fromImageYCbCrA(y, cb, cr, a []uint8, yStride, cStride, aStride int) error { + // Copy planes + if err := d.f.copyPlanes([]frameDataPlane{ + {bytes: y, linesize: yStride}, + {bytes: cb, linesize: cStride}, + {bytes: cr, linesize: cStride}, + {bytes: a, linesize: aStride}, + }); err != nil { + return fmt.Errorf("astiav: copying planes failed: %w", err) + } + return nil +} + +// It's the developer's responsibility to handle frame's writability +func (d *FrameData) FromImage(src image.Image) error { + // Copy planes + switch v := src.(type) { + case *image.Alpha: + return d.fromImagePix(v.Pix, v.Stride) + case *image.Alpha16: + return d.fromImagePix(v.Pix, v.Stride) + case *image.CMYK: + return d.fromImagePix(v.Pix, v.Stride) + case *image.Gray: + return d.fromImagePix(v.Pix, v.Stride) + case *image.Gray16: + return d.fromImagePix(v.Pix, v.Stride) + case *image.NRGBA: + return d.fromImagePix(v.Pix, v.Stride) + case *image.NRGBA64: + return d.fromImagePix(v.Pix, v.Stride) + case *image.NYCbCrA: + return d.fromImageYCbCrA(v.Y, v.Cb, v.Cr, v.A, v.YStride, v.CStride, v.AStride) + case *image.RGBA: + return d.fromImagePix(v.Pix, v.Stride) + case *image.RGBA64: + return d.fromImagePix(v.Pix, v.Stride) + case *image.YCbCr: + return d.fromImageYCbCr(v.Y, v.Cb, v.Cr, v.YStride, v.CStride) + } + return errors.New("astiav: image format is not handled") +} + var _ frameDataFramer = (*frameDataFrame)(nil) type frameDataFrame struct { @@ -190,6 +280,31 @@ func (f *frameDataFrame) bytes(align int) ([]byte, error) { return nil, errors.New("astiav: frame type not implemented") } +func (f *frameDataFrame) copyPlanes(ps []frameDataPlane) (err error) { + switch { + // Video + case f.height() > 0 && f.width() > 0: + // Loop through planes + var cdata [8]*C.uint8_t + var clinesizes [8]C.int + for i, p := range ps { + // Convert data + if len(p.bytes) > 0 { + cdata[i] = (*C.uint8_t)(C.CBytes(p.bytes)) + defer C.free(unsafe.Pointer(cdata[i])) + } + + // Convert linesize + clinesizes[i] = C.int(p.linesize) + } + + // Copy image + C.av_image_copy(&f.f.c.data[0], &f.f.c.linesize[0], &cdata[0], &clinesizes[0], (C.enum_AVPixelFormat)(f.f.c.format), f.f.c.width, f.f.c.height) + return + } + return +} + func (f *frameDataFrame) height() int { return f.f.Height() } @@ -198,44 +313,36 @@ func (f *frameDataFrame) pixelFormat() PixelFormat { return f.f.PixelFormat() } -// Using bytesFromC on f.c.data caused random segfaults -func (f *frameDataFrame) planes() ([]frameDataPlane, error) { - // Get bytes - const align = 1 - b, err := f.bytes(align) - if err != nil { - return nil, fmt.Errorf("astiav: getting bytes failed: %w", err) - } - +func (f *frameDataFrame) planes(b []byte, align int) ([]frameDataPlane, error) { switch { // Video case f.height() > 0 && f.width() > 0: // Below is mostly inspired by https://github.com/FFmpeg/FFmpeg/blob/n5.1.2/libavutil/imgutils.c#L466 // Get linesize - var linesize [4]C.int - if err := newError(C.av_image_fill_linesizes(&linesize[0], (C.enum_AVPixelFormat)(f.f.c.format), f.f.c.width)); err != nil { + var linesizes [4]C.int + if err := newError(C.av_image_fill_linesizes(&linesizes[0], (C.enum_AVPixelFormat)(f.f.c.format), f.f.c.width)); err != nil { return nil, fmt.Errorf("astiav: getting linesize failed: %w", err) } // Align linesize - var alignedLinesize [4]C.ptrdiff_t + var alignedLinesizes [4]C.ptrdiff_t for i := 0; i < 4; i++ { - alignedLinesize[i] = C.astiavFFAlign(linesize[i], C.int(align)) + alignedLinesizes[i] = C.astiavFFAlign(linesizes[i], C.int(align)) } // Get plane sizes var planeSizes [4]C.size_t - if err := newError(C.av_image_fill_plane_sizes(&planeSizes[0], (C.enum_AVPixelFormat)(f.f.c.format), f.f.c.height, &alignedLinesize[0])); err != nil { + if err := newError(C.av_image_fill_plane_sizes(&planeSizes[0], (C.enum_AVPixelFormat)(f.f.c.format), f.f.c.height, &alignedLinesizes[0])); err != nil { return nil, fmt.Errorf("astiav: getting plane sizes failed: %w", err) } - // Loop through plane sizes + // Loop through planes var ps []frameDataPlane start := 0 - for idx, planeSize := range planeSizes { + for i := range planeSizes { // Get end - end := start + int(planeSize) + end := start + int(planeSizes[i]) if len(b) < end { return nil, fmt.Errorf("astiav: buffer length %d is invalid for [%d:%d]", len(b), start, end) } @@ -243,15 +350,16 @@ func (f *frameDataFrame) planes() ([]frameDataPlane, error) { // Append plane ps = append(ps, frameDataPlane{ bytes: b[start:end], - linesize: int(alignedLinesize[idx]), + linesize: int(linesizes[i]), }) // Update start - start += int(planeSize) + start = end } return ps, nil + default: + return nil, errors.New("astiav: frame type not implemented") } - return nil, errors.New("astiav: frame type not implemented") } func (f *frameDataFrame) width() int { diff --git a/frame_data_test.go b/frame_data_test.go index a44dee4..db0f30a 100644 --- a/frame_data_test.go +++ b/frame_data_test.go @@ -10,17 +10,23 @@ import ( ) type mockedFrameDataFrame struct { - h int - imageBytes []byte - pf PixelFormat - planes_ []frameDataPlane - w int + copiedPlanes []frameDataPlane + h int + onBytes func(align int) ([]byte, error) + onPlanes func(b []byte, align int) ([]frameDataPlane, error) + pf PixelFormat + w int } var _ frameDataFramer = (*mockedFrameDataFrame)(nil) func (f *mockedFrameDataFrame) bytes(align int) ([]byte, error) { - return f.imageBytes, nil + return f.onBytes(align) +} + +func (f *mockedFrameDataFrame) copyPlanes(ps []frameDataPlane) error { + f.copiedPlanes = ps + return nil } func (f *mockedFrameDataFrame) height() int { @@ -31,8 +37,8 @@ func (f *mockedFrameDataFrame) pixelFormat() PixelFormat { return f.pf } -func (f *mockedFrameDataFrame) planes() ([]frameDataPlane, error) { - return f.planes_, nil +func (f *mockedFrameDataFrame) planes(b []byte, align int) ([]frameDataPlane, error) { + return f.onPlanes(b, align) } func (f *mockedFrameDataFrame) width() int { @@ -114,15 +120,15 @@ func TestFrameDataInternal(t *testing.T) { } fdf.h = 1 - fdf.imageBytes = []byte{0, 1, 2, 3} + b1 := []byte{0, 1, 2, 3} + fdf.onBytes = func(align int) ([]byte, error) { return b1, nil } fdf.w = 2 - b, err := fd.Bytes(0) + b2, err := fd.Bytes(0) require.NoError(t, err) - require.Equal(t, fdf.imageBytes, b) + require.Equal(t, b1, b2) for _, v := range []struct { e image.Image - err bool i image.Image pixelFormat PixelFormat planes []frameDataPlane @@ -326,13 +332,133 @@ func TestFrameDataInternal(t *testing.T) { }, } { fdf.pf = v.pixelFormat - fdf.planes_ = v.planes - err = fd.ToImage(v.i) - if v.err { - require.Error(t, err) - } else { - require.Equal(t, v.e, v.i) - } + fdf.onPlanes = func(b []byte, align int) ([]frameDataPlane, error) { return v.planes, nil } + require.NoError(t, fd.ToImage(v.i)) + require.Equal(t, v.e, v.i) + } + + b1 = []byte{1, 2, 3, 4} + fdf.onPlanes = func(b []byte, align int) ([]frameDataPlane, error) { + return []frameDataPlane{ + { + bytes: b1[:2], + linesize: 1, + }, + { + bytes: b1[2:], + linesize: 2, + }, + }, nil + } + require.NoError(t, fd.SetBytes(b1, 0)) + require.Equal(t, []frameDataPlane{ + {bytes: b1[:2], linesize: 1}, + {bytes: b1[2:], linesize: 2}, + }, fdf.copiedPlanes) + + for _, v := range []struct { + expectedCopiedPlanes []frameDataPlane + i image.Image + }{ + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.Alpha{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.Alpha16{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.CMYK{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.Gray{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.Gray16{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.NRGBA{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.NRGBA64{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{ + {bytes: []byte{0, 1}, linesize: 1}, + {bytes: []byte{2, 3}, linesize: 2}, + {bytes: []byte{4, 5}, linesize: 2}, + {bytes: []byte{6, 7}, linesize: 4}, + }, + i: &image.NYCbCrA{ + A: []byte{6, 7}, + AStride: 4, + YCbCr: image.YCbCr{ + Y: []byte{0, 1}, + Cb: []byte{2, 3}, + Cr: []byte{4, 5}, + YStride: 1, + CStride: 2, + }, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.RGBA{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{{bytes: []byte{0, 1, 2, 3}, linesize: 1}}, + i: &image.RGBA64{ + Pix: []byte{0, 1, 2, 3}, + Stride: 1, + }, + }, + { + expectedCopiedPlanes: []frameDataPlane{ + {bytes: []byte{0, 1}, linesize: 1}, + {bytes: []byte{2, 3}, linesize: 2}, + {bytes: []byte{4, 5}, linesize: 2}, + }, + i: &image.YCbCr{ + Y: []byte{0, 1}, + Cb: []byte{2, 3}, + Cr: []byte{4, 5}, + YStride: 1, + CStride: 2, + }, + }, + } { + require.NoError(t, fd.FromImage(v.i)) + require.Equal(t, v.expectedCopiedPlanes, fdf.copiedPlanes) } } @@ -350,24 +476,47 @@ func TestFrameData(t *testing.T) { name: "video-yuv420p", }, } { - f, err := globalHelper.inputLastFrame(v.name+"."+v.ext, MediaTypeVideo) + f1, err := globalHelper.inputLastFrame(v.name+"."+v.ext, MediaTypeVideo) require.NoError(t, err) - fd := f.Data() + fd1 := f1.Data() - b1, err := fd.Bytes(1) + b1, err := fd1.Bytes(1) require.NoError(t, err) b2 := []byte(fmt.Sprintf("%+v", b1)) b3, err := os.ReadFile("testdata/" + v.name + "-bytes") require.NoError(t, err) - require.Equal(t, b2, b3) + require.Equal(t, b3, b2) - i1, err := fd.GuessImageFormat() + i1, err := fd1.GuessImageFormat() require.NoError(t, err) - require.NoError(t, fd.ToImage(i1)) + require.NoError(t, fd1.ToImage(i1)) b4 := []byte(fmt.Sprintf("%+v", i1)) b5, err := os.ReadFile("testdata/" + v.name + "-struct") require.NoError(t, err) - require.Equal(t, b4, b5) + require.Equal(t, b5, b4) + f2 := AllocFrame() + defer f2.Free() + f2.SetHeight(f1.Height()) + f2.SetPixelFormat(f1.PixelFormat()) + f2.SetWidth(f1.Width()) + const align = 1 + require.NoError(t, f2.AllocBuffer(align)) + require.NoError(t, f2.AllocImage(align)) + fd2 := f2.Data() + + require.NoError(t, fd2.FromImage(i1)) + b6, err := fd2.Bytes(align) + require.NoError(t, err) + b7 := []byte(fmt.Sprintf("%+v", b6)) + require.Equal(t, b3, b7) + + require.NoError(t, f2.ImageFillBlack()) + require.NoError(t, fd2.SetBytes(b1, align)) + b1[0] -= 1 + b8, err := fd2.Bytes(align) + require.NoError(t, err) + b9 := []byte(fmt.Sprintf("%+v", b8)) + require.Equal(t, b3, b9) } } diff --git a/frame_test.go b/frame_test.go index 8a942b9..3707b6c 100644 --- a/frame_test.go +++ b/frame_test.go @@ -115,4 +115,10 @@ func TestFrame(t *testing.T) { require.NoError(t, err) require.Equal(t, 12, n) require.Equal(t, []byte{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x80, 0x80, 0x80, 0x80}, b) + + require.True(t, f6.IsWritable()) + require.NoError(t, f5.Ref(f6)) + require.False(t, f6.IsWritable()) + require.NoError(t, f6.MakeWritable()) + require.True(t, f6.IsWritable()) } diff --git a/testdata/image-rgba.rgba b/testdata/image-rgba.rgba new file mode 100644 index 0000000..55092c8 Binary files /dev/null and b/testdata/image-rgba.rgba differ