almost complete implementation
This commit is contained in:
+292
@@ -0,0 +1,292 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user