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" }