293 lines
5.6 KiB
Go
293 lines
5.6 KiB
Go
package gopeg
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"image/png"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const testVideo = "testdata/small.webm"
|
||
|
|
|
||
|
|
func TestNewDecoder(t *testing.T) {
|
||
|
|
f, err := os.Open(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
d, err := NewDecoder(f)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder:", err)
|
||
|
|
}
|
||
|
|
defer d.Close()
|
||
|
|
|
||
|
|
if d.fmtCtx == nil {
|
||
|
|
t.Fatal("format context is nil")
|
||
|
|
}
|
||
|
|
if d.videoIdx < 0 {
|
||
|
|
t.Fatal("no video stream found")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMeta(t *testing.T) {
|
||
|
|
f, err := os.Open(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
d, err := NewDecoder(f)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder:", err)
|
||
|
|
}
|
||
|
|
defer d.Close()
|
||
|
|
|
||
|
|
m := d.Meta()
|
||
|
|
|
||
|
|
if m.Duration <= 0 {
|
||
|
|
t.Error("duration is zero or negative")
|
||
|
|
}
|
||
|
|
if m.Video == nil {
|
||
|
|
t.Fatal("video meta is nil")
|
||
|
|
}
|
||
|
|
if m.Video.Width == 0 || m.Video.Height == 0 {
|
||
|
|
t.Errorf("video dimensions are zero: %dx%d", m.Video.Width, m.Video.Height)
|
||
|
|
}
|
||
|
|
if m.Video.CodecName == "" {
|
||
|
|
t.Error("video codec name is empty")
|
||
|
|
}
|
||
|
|
if m.Video.FPSNum == 0 || m.Video.FPSDen == 0 {
|
||
|
|
t.Errorf("frame rate is zero: %d/%d", m.Video.FPSNum, m.Video.FPSDen)
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Logf("Duration: %v", m.Duration)
|
||
|
|
t.Logf("Video: %dx%d %s @ %d/%d",
|
||
|
|
m.Video.Width, m.Video.Height,
|
||
|
|
m.Video.CodecName, m.Video.FPSNum, m.Video.FPSDen)
|
||
|
|
if m.Audio != nil {
|
||
|
|
t.Logf("Audio: %dHz %dch %s",
|
||
|
|
m.Audio.SampleRate, m.Audio.Channels, m.Audio.CodecName)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDecodeFrames(t *testing.T) {
|
||
|
|
f, err := os.Open(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
d, err := NewDecoder(f)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder:", err)
|
||
|
|
}
|
||
|
|
defer d.Close()
|
||
|
|
|
||
|
|
var (
|
||
|
|
videoFrames int
|
||
|
|
audioFrames int
|
||
|
|
firstPTS time.Duration
|
||
|
|
lastPTS time.Duration
|
||
|
|
maxFrames = 100 // limit to avoid timeout
|
||
|
|
)
|
||
|
|
|
||
|
|
for videoFrames < maxFrames {
|
||
|
|
frame, err := d.DecodeFrame()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("DecodeFrame:", err)
|
||
|
|
}
|
||
|
|
if frame == nil {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
|
||
|
|
if frame.Video != nil {
|
||
|
|
videoFrames++
|
||
|
|
if firstPTS == 0 {
|
||
|
|
firstPTS = frame.Video.PTS
|
||
|
|
}
|
||
|
|
lastPTS = frame.Video.PTS
|
||
|
|
|
||
|
|
if frame.Video.Img == nil {
|
||
|
|
t.Error("video frame image is nil")
|
||
|
|
}
|
||
|
|
if frame.Video.Img.Bounds().Dx() == 0 {
|
||
|
|
t.Error("video frame has zero width")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if frame.Audio != nil {
|
||
|
|
audioFrames++
|
||
|
|
if len(frame.Audio.Samples) == 0 {
|
||
|
|
t.Error("audio frame has no samples")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Logf("Decoded %d video frames and %d audio frames", videoFrames, audioFrames)
|
||
|
|
t.Logf("First PTS: %v, Last PTS: %v", firstPTS, lastPTS)
|
||
|
|
|
||
|
|
if videoFrames == 0 {
|
||
|
|
t.Error("no video frames decoded")
|
||
|
|
}
|
||
|
|
if lastPTS <= firstPTS && videoFrames > 1 {
|
||
|
|
t.Error("PTS is not monotonically increasing")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDecodeFrameImages(t *testing.T) {
|
||
|
|
f, err := os.Open(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
d, err := NewDecoder(f)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder:", err)
|
||
|
|
}
|
||
|
|
defer d.Close()
|
||
|
|
|
||
|
|
// Decode first 3 video frames and save them as PNG
|
||
|
|
dir := t.TempDir()
|
||
|
|
saved := 0
|
||
|
|
for saved < 3 {
|
||
|
|
frame, err := d.DecodeFrame()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("DecodeFrame:", err)
|
||
|
|
}
|
||
|
|
if frame == nil || frame.Video == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
outPath := filepath.Join(dir, "frame_"+pad(saved)+".png")
|
||
|
|
out, err := os.Create(outPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("create png:", err)
|
||
|
|
}
|
||
|
|
if err := png.Encode(out, frame.Video.Img); err != nil {
|
||
|
|
out.Close()
|
||
|
|
t.Fatal("encode png:", err)
|
||
|
|
}
|
||
|
|
out.Close()
|
||
|
|
|
||
|
|
saved++
|
||
|
|
t.Logf("Saved frame %d (%dx%d) to %s",
|
||
|
|
saved,
|
||
|
|
frame.Video.Img.Bounds().Dx(),
|
||
|
|
frame.Video.Img.Bounds().Dy(),
|
||
|
|
outPath)
|
||
|
|
}
|
||
|
|
|
||
|
|
if saved == 0 {
|
||
|
|
t.Error("no video frames saved")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestClose(t *testing.T) {
|
||
|
|
f, err := os.Open(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
d, err := NewDecoder(f)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder:", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close should be safe to call multiple times
|
||
|
|
d.Close()
|
||
|
|
d.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFrameImagesAreValid(t *testing.T) {
|
||
|
|
f, err := os.Open(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
|
||
|
|
d, err := NewDecoder(f)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder:", err)
|
||
|
|
}
|
||
|
|
defer d.Close()
|
||
|
|
|
||
|
|
// Decode up to 50 frames and verify each is valid
|
||
|
|
checked := 0
|
||
|
|
for checked < 50 {
|
||
|
|
frame, err := d.DecodeFrame()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("DecodeFrame:", err)
|
||
|
|
}
|
||
|
|
if frame == nil || frame.Video == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
img := frame.Video.Img
|
||
|
|
if img == nil {
|
||
|
|
t.Fatal("image is nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
bounds := img.Bounds()
|
||
|
|
if bounds.Dx() == 0 || bounds.Dy() == 0 {
|
||
|
|
t.Errorf("frame %d has zero dimensions", checked)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check that not all pixels are black
|
||
|
|
pix := img.Pix
|
||
|
|
allBlack := true
|
||
|
|
for _, b := range pix {
|
||
|
|
if b != 0 {
|
||
|
|
allBlack = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if allBlack {
|
||
|
|
t.Errorf("frame %d is completely black", checked)
|
||
|
|
}
|
||
|
|
|
||
|
|
checked++
|
||
|
|
}
|
||
|
|
|
||
|
|
if checked == 0 {
|
||
|
|
t.Error("no frames checked")
|
||
|
|
}
|
||
|
|
t.Logf("Checked %d frames", checked)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSeekableReader(t *testing.T) {
|
||
|
|
// Use bytes.Reader which implements io.ReadSeeker
|
||
|
|
data, err := os.ReadFile(testVideo)
|
||
|
|
if err != nil {
|
||
|
|
t.Skipf("test video not found: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
reader := strings.NewReader(string(data))
|
||
|
|
d, err := NewDecoder(reader)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal("NewDecoder with strings.Reader:", err)
|
||
|
|
}
|
||
|
|
defer d.Close()
|
||
|
|
|
||
|
|
m := d.Meta()
|
||
|
|
if m.Video == nil {
|
||
|
|
t.Fatal("no video stream from seekable reader")
|
||
|
|
}
|
||
|
|
t.Logf("Decoded from seekable reader: %dx%d %s",
|
||
|
|
m.Video.Width, m.Video.Height, m.Video.CodecName)
|
||
|
|
}
|
||
|
|
|
||
|
|
func pad(n int) string {
|
||
|
|
if n < 10 {
|
||
|
|
return "00" + string(rune('0'+n))
|
||
|
|
}
|
||
|
|
if n < 100 {
|
||
|
|
return "0" + string(rune('0'+n/10)) + string(rune('0'+n%10))
|
||
|
|
}
|
||
|
|
return "999"
|
||
|
|
}
|