GetVideoFrame() buffer param added

This commit is contained in:
Krzysztofz01 2023-08-21 09:55:12 +02:00
parent 1813715282
commit 957b8dc191
2 changed files with 60 additions and 46 deletions

View file

@ -3,7 +3,6 @@ package vidio
import ( import (
"errors" "errors"
"fmt" "fmt"
"image"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@ -13,28 +12,40 @@ import (
"syscall" "syscall"
) )
// Return the N-th frame of the video specified by the filename as a pointer to a RGBA image. The frames are indexed from 0. // Return the N-th frame of the video specified by the filename in the RGBA format and stored it to the provided frame buffer. The frames are indexed from 0.
func GetVideoFrame(filename string, n int) (*image.RGBA, error) { func GetVideoFrame(filename string, n int, frameBuffer []byte) error {
if !exists(filename) { if !exists(filename) {
return nil, fmt.Errorf("vidio: video file %s does not exist", filename) return fmt.Errorf("vidio: video file %s does not exist", filename)
} }
if err := installed("ffmpeg"); err != nil { if err := installed("ffmpeg"); err != nil {
return nil, err return err
} }
if err := installed("ffprobe"); err != nil { if err := installed("ffprobe"); err != nil {
return nil, err return err
}
frameBufferSize, framesCount, err := probeVideo(filename)
if err != nil {
return err
}
if n >= framesCount {
return errors.New("vidio: provided frame index is not in frame count range")
}
if frameBuffer == nil {
frameBuffer = make([]byte, frameBufferSize)
} else {
if len(frameBuffer) < frameBufferSize {
return errors.New("vidio: provided frame buffer size is smaller than the frame size")
}
} }
selectExpression, err := buildSelectExpression(n) selectExpression, err := buildSelectExpression(n)
if err != nil { if err != nil {
return nil, fmt.Errorf("vidio: failed to parse the specified frame index: %w", err) return fmt.Errorf("vidio: failed to parse the specified frame index: %w", err)
}
imageRect, stream, err := probeVideo(filename)
if err != nil {
return nil, err
} }
cmd := exec.Command( cmd := exec.Command(
@ -44,7 +55,7 @@ func GetVideoFrame(filename string, n int) (*image.RGBA, error) {
"-loglevel", "quiet", "-loglevel", "quiet",
"-pix_fmt", "rgba", "-pix_fmt", "rgba",
"-vcodec", "rawvideo", "-vcodec", "rawvideo",
"-map", fmt.Sprintf("0:v:%d", stream), "-map", "0:v:0",
"-vf", selectExpression, "-vf", selectExpression,
"-vsync", "0", "-vsync", "0",
"-", "-",
@ -52,11 +63,11 @@ func GetVideoFrame(filename string, n int) (*image.RGBA, error) {
stdoutPipe, err := cmd.StdoutPipe() stdoutPipe, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return nil, fmt.Errorf("vidio: failed to access the ffmpeg stdout pipe: %w", err) return fmt.Errorf("vidio: failed to access the ffmpeg stdout pipe: %w", err)
} }
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("vidio: failed to start the ffmpeg cmd: %w", err) return fmt.Errorf("vidio: failed to start the ffmpeg cmd: %w", err)
} }
interruptChan := make(chan os.Signal, 1) interruptChan := make(chan os.Signal, 1)
@ -72,59 +83,70 @@ func GetVideoFrame(filename string, n int) (*image.RGBA, error) {
os.Exit(1) os.Exit(1)
}() }()
imageBuffer := image.NewRGBA(imageRect) if _, err := io.ReadFull(stdoutPipe, frameBuffer); err != nil {
if _, err := io.ReadFull(stdoutPipe, imageBuffer.Pix); err != nil { return fmt.Errorf("vidio: failed to read the ffmpeg cmd result to the image buffer: %w", err)
return nil, fmt.Errorf("vidio: failed to read the ffmpeg cmd result to the image buffer: %w", err)
} }
if err := stdoutPipe.Close(); err != nil { if err := stdoutPipe.Close(); err != nil {
return nil, fmt.Errorf("vidio: failed to close the ffmpeg stdout pipe: %w", err) return fmt.Errorf("vidio: failed to close the ffmpeg stdout pipe: %w", err)
} }
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
return nil, fmt.Errorf("vidio: failed to free resources after the ffmpeg cmd: %w", err) return fmt.Errorf("vidio: failed to free resources after the ffmpeg cmd: %w", err)
} }
return imageBuffer, nil return nil
} }
// Helper function used to extract the target frame size and stream index // Helper function used to extract the target frame buffer size and frames count
func probeVideo(filename string) (image.Rectangle, int, error) { func probeVideo(filename string) (int, int, error) {
videoData, err := ffprobe(filename, "v") videoData, err := ffprobe(filename, "v")
if err != nil { if err != nil {
return image.Rectangle{}, 0, fmt.Errorf("vidio: no video data found in %s: %w", filename, err) return 0, 0, fmt.Errorf("vidio: no video data found in %s: %w", filename, err)
} }
if len(videoData) == 0 { if len(videoData) == 0 {
return image.Rectangle{}, 0, fmt.Errorf("vidio: no video streams found in %s", filename) return 0, 0, fmt.Errorf("vidio: no video streams found in %s", filename)
} }
var ( var (
width int = 0 width int = 0
height int = 0 height int = 0
frames int = 0
) )
if widthStr, ok := videoData[0]["width"]; !ok { if widthStr, ok := videoData[0]["width"]; !ok {
return image.Rectangle{}, 0, errors.New("vidio: failed to access the image width") return 0, 0, errors.New("vidio: failed to access the image width")
} else { } else {
if widthParsed, err := strconv.Atoi(widthStr); err != nil { if widthParsed, err := strconv.Atoi(widthStr); err != nil {
return image.Rectangle{}, 0, fmt.Errorf("vidio: failed to parse the image width: %w", err) return 0, 0, fmt.Errorf("vidio: failed to parse the image width: %w", err)
} else { } else {
width = widthParsed width = widthParsed
} }
} }
if heightStr, ok := videoData[0]["height"]; !ok { if heightStr, ok := videoData[0]["height"]; !ok {
return image.Rectangle{}, 0, errors.New("vidio: failed to access the image height") return 0, 0, errors.New("vidio: failed to access the image height")
} else { } else {
if heightParsed, err := strconv.Atoi(heightStr); err != nil { if heightParsed, err := strconv.Atoi(heightStr); err != nil {
return image.Rectangle{}, 0, fmt.Errorf("vidio: failed to parse the image height: %w", err) return 0, 0, fmt.Errorf("vidio: failed to parse the image height: %w", err)
} else { } else {
height = heightParsed height = heightParsed
} }
} }
return image.Rect(0, 0, width, height), 0, nil if framesStr, ok := videoData[0]["nb_frames"]; !ok {
return 0, 0, errors.New("vidio: failed to access the frames count")
} else {
if framesParsed, err := strconv.Atoi(framesStr); err != nil {
return 0, 0, fmt.Errorf("vidio: failed to parse the frames count: %w", err)
} else {
frames = framesParsed
}
}
frameBufferSize := width * height * 4
return frameBufferSize, frames, nil
} }
// Error representing a strings.Builder failure in the buildSelectExpression func. // Error representing a strings.Builder failure in the buildSelectExpression func.

View file

@ -1,6 +1,7 @@
package vidio package vidio
import ( import (
"image"
"image/png" "image/png"
"os" "os"
"testing" "testing"
@ -8,12 +9,9 @@ import (
func TestGetFrameShouldReturnErrorOnInvalidFilePath(t *testing.T) { func TestGetFrameShouldReturnErrorOnInvalidFilePath(t *testing.T) {
path := "test/koala-video-not-present.mp4" path := "test/koala-video-not-present.mp4"
img := image.NewRGBA(image.Rect(0, 0, 480, 270))
frame, err := GetVideoFrame(path, 2) err := GetVideoFrame(path, 2, img.Pix)
if frame != nil {
t.Errorf("Frame was expected to be nil")
}
if err == nil { if err == nil {
t.Error("Error was expected to not be nil") t.Error("Error was expected to not be nil")
@ -22,13 +20,10 @@ func TestGetFrameShouldReturnErrorOnInvalidFilePath(t *testing.T) {
func TestGetFrameShouldReturnErrorOnOutOfRangeFrame(t *testing.T) { func TestGetFrameShouldReturnErrorOnOutOfRangeFrame(t *testing.T) {
path := "test/koala.mp4" path := "test/koala.mp4"
img := image.NewRGBA(image.Rect(0, 0, 480, 270))
framesCount := 101 framesCount := 101
frame, err := GetVideoFrame(path, framesCount+1) err := GetVideoFrame(path, framesCount+1, img.Pix)
if frame != nil {
t.Error("Frames was expected to be nil")
}
if err == nil { if err == nil {
t.Error("Error was expected to not be nil") t.Error("Error was expected to not be nil")
@ -37,17 +32,14 @@ func TestGetFrameShouldReturnErrorOnOutOfRangeFrame(t *testing.T) {
func TestGetFrameShouldReturnCorrectFrame(t *testing.T) { func TestGetFrameShouldReturnCorrectFrame(t *testing.T) {
path := "test/koala.mp4" path := "test/koala.mp4"
img := image.NewRGBA(image.Rect(0, 0, 480, 270))
expectedFrameFile, _ := os.Open("test/koala-frame5.png") expectedFrameFile, _ := os.Open("test/koala-frame5.png")
defer expectedFrameFile.Close() defer expectedFrameFile.Close()
expectedFrame, _ := png.Decode(expectedFrameFile) expectedFrame, _ := png.Decode(expectedFrameFile)
actualFrame, err := GetVideoFrame(path, 5) err := GetVideoFrame(path, 5, img.Pix)
if actualFrame == nil {
t.Error("Frame was expected to not be nil")
}
if err != nil { if err != nil {
t.Error("Error was expected to be nil") t.Error("Error was expected to be nil")
@ -56,7 +48,7 @@ func TestGetFrameShouldReturnCorrectFrame(t *testing.T) {
for xIndex := 0; xIndex < expectedFrame.Bounds().Dx(); xIndex += 1 { for xIndex := 0; xIndex < expectedFrame.Bounds().Dx(); xIndex += 1 {
for yIndex := 0; yIndex < expectedFrame.Bounds().Dy(); yIndex += 1 { for yIndex := 0; yIndex < expectedFrame.Bounds().Dy(); yIndex += 1 {
eR, eG, eB, eA := expectedFrame.At(xIndex, yIndex).RGBA() eR, eG, eB, eA := expectedFrame.At(xIndex, yIndex).RGBA()
aR, aG, aB, aA := actualFrame.At(xIndex, yIndex).RGBA() aR, aG, aB, aA := img.At(xIndex, yIndex).RGBA()
if eR != aR || eG != aG || eB != aB || eA != aA { if eR != aR || eG != aG || eB != aB || eA != aA {
t.Error("The expected and actual frames were expected to be equal") t.Error("The expected and actual frames were expected to be equal")