GetVideoFrame() buffer param added
This commit is contained in:
parent
1813715282
commit
957b8dc191
2 changed files with 60 additions and 46 deletions
82
frame.go
82
frame.go
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue