diff --git a/go.mod b/go.mod index ee8874f..42467e2 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,19 @@ go 1.23 require ( github.com/anthonynsimon/bild v0.11.1 + github.com/asticode/go-astiav v0.37.0 github.com/chai2010/webp v1.1.0 - github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/spf13/cobra v0.0.7 - github.com/stretchr/testify v1.2.2 + github.com/stretchr/testify v1.7.0 gopkg.in/gographics/imagick.v3 v3.3.0 ) require ( + github.com/asticode/go-astikit v0.42.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index 13e9767..fa89656 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/anthonynsimon/bild v0.11.1 h1:gsfSwed1Zlk3lwQTA202qwJM6mzzXCX/i0Pbv2igfDY= github.com/anthonynsimon/bild v0.11.1/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asticode/go-astiav v0.37.0 h1:Ph4usW4lulotVvne8hqZ1JCOHX1f8ces6yVKdg+PnyQ= +github.com/asticode/go-astiav v0.37.0/go.mod h1:GI0pHw6K2/pl/o8upCtT49P/q4KCwhv/8nGLlCsZLdA= +github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= +github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -20,6 +24,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -52,16 +57,16 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431 h1:nWhrOsCKdV6bivw03k7MROF2tYzCFGfYBYFrTEHyucs= -github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431/go.mod h1:aFJ1ZwLjvHN4yEzE5Bkz8rD8/d8Vlj3UIuvz2yfET7I= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -96,9 +101,11 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -140,6 +147,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/gographics/imagick.v3 v3.3.0 h1:MI9qkquiNDmrSGnTG2241GZqOUgEfR7KQpVsVLxiylk= gopkg.in/gographics/imagick.v3 v3.3.0/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA= @@ -147,4 +155,6 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/project/.gitignore b/internal/project/.gitignore new file mode 100644 index 0000000..d383c56 --- /dev/null +++ b/internal/project/.gitignore @@ -0,0 +1 @@ +testdata diff --git a/internal/project/mask.go b/internal/project/mask.go index 31adec2..a910a80 100644 --- a/internal/project/mask.go +++ b/internal/project/mask.go @@ -68,7 +68,7 @@ func (m Masks) Apply(i image.Image) { var ii draw.Image var ok bool if ii, ok = i.(draw.Image); !ok { - log.Printf("%T does not implement draw.Image") + log.Printf("%T does not implement draw.Image", i) return } for _, mm := range m { diff --git a/internal/project/project.go b/internal/project/project.go index a24b2dd..8508fbc 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -1,8 +1,5 @@ package project -// #cgo LDFLAGS="-L/usr/local/Cellar/ffmpeg/4.2.2_2/lib" -// #cgo CGO_CFLAGS="-I/usr/local/Cellar/ffmpeg/4.2.2_2/include" - import ( "encoding/json" "fmt" @@ -233,17 +230,10 @@ func (p *Project) Run() (Response, error) { interested = false } - var rgbImg *image.RGBA - var img *image.RGBA + img := imgutils.RGBA(iterator.Image().(*image.YCbCr)) + rgbImg := img if interested { log.Printf("interested in frame %d time %s", frame, iterator.Duration()) - if yi := iterator.Image(); yi == nil { - log.Printf("no image. continuing") - continue - } else { - rgbImg = imgutils.RGBA(yi) - } - img = rgbImg } if frame == 0 { diff --git a/internal/project/video_iterator.go b/internal/project/video_iterator.go index 3a81b7f..3a834e8 100644 --- a/internal/project/video_iterator.go +++ b/internal/project/video_iterator.go @@ -1,38 +1,48 @@ package project import ( + "errors" "fmt" "image" - "io" "log" "time" - "github.com/nareix/joy4/av" - "github.com/nareix/joy4/av/avutil" - "github.com/nareix/joy4/cgo/ffmpeg" - "github.com/nareix/joy4/format" - "github.com/nareix/joy4/format/mp4" + "github.com/asticode/go-astiav" ) func init() { - format.RegisterAll() + astiav.SetLogLevel(astiav.LogLevelError) } type Iterator struct { - err error - demuxer av.DemuxCloser - decoders []*ffmpeg.VideoDecoder - rect image.Rectangle - packet av.Packet - frame int - decoded bool - vf *ffmpeg.VideoFrame + err error + formatContext *astiav.FormatContext + codecContext *astiav.CodecContext + videoStreamIdx int + rect image.Rectangle + packet *astiav.Packet + frame int + decoded bool + currentFrame *astiav.Frame + img image.Image } func (p *Iterator) Close() { - if p.demuxer != nil { - p.demuxer.Close() - p.demuxer = nil + if p.formatContext != nil { + p.formatContext.Free() + p.formatContext = nil + } + if p.codecContext != nil { + p.codecContext.Free() + p.codecContext = nil + } + if p.currentFrame != nil { + p.currentFrame.Free() + p.currentFrame = nil + } + if p.packet != nil { + p.packet.Free() + p.packet = nil } } @@ -40,89 +50,180 @@ func NewIterator(filename string) (iter *Iterator, err error) { if filename == "" { panic("missing filename") } + iter = &Iterator{frame: -1} - iter.demuxer, err = avutil.Open(filename) - if err != nil { - return nil, err + + // Open input file + formatContext := astiav.AllocFormatContext() + if formatContext == nil { + return nil, fmt.Errorf("allocating format context failed") + } + iter.formatContext = formatContext + + // Get stream info + if err := formatContext.OpenInput(filename, nil, nil); err != nil { + iter.Close() + return nil, fmt.Errorf("opening input: %w", err) } - streams, err := iter.demuxer.Streams() - if err != nil { - iter.Close() - return nil, err - } - iter.decoders = make([]*ffmpeg.VideoDecoder, len(streams)) - for i, stream := range streams { - // if stream.Type().IsAudio() { - // astream := stream.(av.AudioCodecData) - // fmt.Println(astream.Type(), astream.SampleRate(), astream.SampleFormat(), astream.ChannelLayout()) - // } else if stream.Type().IsVideo() { - fmt.Printf("stream[%d] = %s (video:%v)\n", i, stream.Type(), stream.Type().IsVideo()) - if stream.Type().IsVideo() { - vstream := stream.(av.VideoCodecData) - r := image.Rect(0, 0, vstream.Width(), vstream.Height()) - if iter.rect.Empty() { - iter.rect = r - } else if !iter.rect.Eq(r) { - return nil, fmt.Errorf("video stream %d(%v) doesn't match expected %v", i, r, iter.rect) - } - // fmt.Printf("stream[%d] = %s\n", i, vstream.Type()) - // fmt.Printf("stream[%d] %#v\n", i, vstream) - iter.decoders[i], err = ffmpeg.NewVideoDecoder(vstream) - if err != nil { - log.Fatalf("NewVideoDecoder error: %s", err) - } + // Find the first video stream + iter.videoStreamIdx = -1 + for i, stream := range formatContext.Streams() { + if stream.CodecParameters().MediaType() != astiav.MediaTypeVideo { + continue } + iter.videoStreamIdx = i + width := stream.CodecParameters().Width() + height := stream.CodecParameters().Height() + iter.rect = image.Rect(0, 0, width, height) + fmt.Printf("stream[%d] = video (%dx%d)\n", i, width, height) + + iter.currentFrame = astiav.AllocFrame() + // iter.currentFrame.SetWidth(width) + // iter.currentFrame.SetHeight(height) + // iter.currentFrame.SetPixelFormat(astiav.PixelFormatYuv420P) + break } - if iter.rect.Empty() { + + if iter.videoStreamIdx == -1 { + iter.Close() return nil, fmt.Errorf("no video stream found") } + + // Find decoder + stream := formatContext.Streams()[iter.videoStreamIdx] + codec := astiav.FindDecoder(stream.CodecParameters().CodecID()) + if codec == nil { + iter.Close() + return nil, fmt.Errorf("no codec found for stream %d", iter.videoStreamIdx) + } + log.Printf("codec found: %s", codec.Name()) + + // Create codec context + iter.codecContext = astiav.AllocCodecContext(codec) + if iter.codecContext == nil { + iter.Close() + return nil, fmt.Errorf("failed to create codec context") + } + log.Printf("codec parameters for stream %d: %#v", iter.videoStreamIdx, stream.CodecParameters()) + + // Copy codec parameters + if err := iter.codecContext.FromCodecParameters(stream.CodecParameters()); err != nil { + iter.Close() + return nil, fmt.Errorf("copying codec parameters: %w", err) + } + // iter.codecContext.SetPixelFormat(astiav.PixelFormatYuv420P) + + // Open codec + if err := iter.codecContext.Open(codec, nil); err != nil { + iter.Close() + return nil, fmt.Errorf("opening codec: %w", err) + } + + // Allocate packet + iter.packet = astiav.AllocPacket() + + log.Printf("codecContext: %s pixelFormat=%#v", iter.codecContext.String(), iter.codecContext.PixelFormat()) + return iter, nil } func (i *Iterator) Seek(d time.Duration) error { - dm := i.demuxer.(*avutil.HandlerDemuxer).Demuxer.(*mp4.Demuxer) - log.Printf("should seek %s", d) - return dm.SeekToTime(d) + // Convert time.Duration to AV timestamp + ts := int64(d / time.Microsecond) + timeBase := i.formatContext.Streams()[i.videoStreamIdx].TimeBase() + timestamp := astiav.RescaleQ(ts, astiav.NewRational(1, 1000000), timeBase) + + log.Printf("should seek %s (timestamp: %d)", d, timestamp) + + // Seek to timestamp + seekFlags := astiav.NewSeekFlags(astiav.SeekFlagBackward, astiav.SeekFlagAny) + if err := i.formatContext.SeekFrame(i.videoStreamIdx, timestamp, seekFlags); err != nil { + return fmt.Errorf("seeking: %w", err) + } + + // Reset frame counter + i.frame = -1 + + // Send empty packet to flush the decoder + if err := i.codecContext.SendPacket(nil); err != nil { + return fmt.Errorf("flushing decoder: %w", err) + } + + return nil } func (i *Iterator) VideoResolution() string { return fmt.Sprintf("%dx%d", i.rect.Dx(), i.rect.Dy()) } -func (i *Iterator) NextWithImage() bool { - for i.Next() { - i.err = i.DecodeFrame() - if i.err != nil { - return false - } - if i.vf == nil { - continue - } - return true - } - return false +// func (i *Iterator) NextWithImage() bool { +// for i.Next() { +// i.err = i.DecodeFrame() +// if i.err != nil { +// return false +// } +// if i.currentFrame == nil { +// continue +// } +// return true +// } +// return false +// } -} func (i *Iterator) Next() bool { - var err error - var pkt av.Packet + i.decoded = false + for { - i.vf = nil - i.decoded = false - if pkt, err = i.demuxer.ReadPacket(); err != nil { - if err == io.EOF { + // Read frame + err := i.formatContext.ReadFrame(i.packet) + if err != nil { + if errors.Is(err, astiav.ErrEof) { return false } - i.err = err + i.err = fmt.Errorf("reading frame: %w", err) return false } - // skip packets we don't have a decoder for - if i.decoders[pkt.Idx] == nil { + + // Skip if not a video packet + if i.packet.StreamIndex() != i.videoStreamIdx { + log.Printf("Skipping packet from stream %d (not video)", i.packet.StreamIndex()) + i.packet.Unref() continue } - i.packet = pkt i.frame++ + + // Send packet to decoder + if i.err = i.codecContext.SendPacket(i.packet); i.err != nil { + log.Printf("SendPacket() error: %v", i.err) + i.packet.Unref() + return false + } + + // Get frame from decoder + i.err = i.codecContext.ReceiveFrame(i.currentFrame) + if errors.Is(i.err, astiav.ErrEagain) { + i.packet.Unref() + log.Printf("ReceiveFrame() Eagain for frame %d, waiting for more data", i.frame) + i.err = nil // No frame available yet, continue to read more packets + continue + } + defer i.packet.Unref() + if i.err != nil { + // Check if we need more data or reached EOF + log.Printf("ReceiveFrame() error: %v", i.err) + return false + } + + defer i.currentFrame.Unref() + + img, _ := i.currentFrame.Data().GuessImageFormat() + i.err = i.currentFrame.Data().ToImage(img) + if i.err != nil { + log.Printf("ToImage() error: %v", i.err) + return false + } + i.img = img return true } } @@ -131,41 +232,28 @@ func (i *Iterator) Frame() int { return i.frame } -func (i *Iterator) DecodeFrame() error { - if i.decoded { - return i.err - } - // decode - decoder := i.decoders[i.packet.Idx] - var err error - if len(i.packet.Data) == 0 { - log.Printf("no packet at frame %d", i.frame) - return nil - } - i.vf, err = decoder.Decode(i.packet.Data) - if i.vf == nil { - log.Printf("no image at frame %d", i.frame) - i.frame-- - } - i.decoded = true - return err +func (i *Iterator) Image() image.Image { + return i.img } -func (i *Iterator) Image() *image.YCbCr { - if i.frame == -1 { - if !i.NextWithImage() { - panic("no image") - } - } - i.err = i.DecodeFrame() - if i.vf == nil { - return nil - } - return &i.vf.Image +func (i *Iterator) Error() error { + return i.err } -func (i *Iterator) Error() error { return i.err } -func (i *Iterator) Duration() time.Duration { return i.packet.Time } + +func (i *Iterator) Duration() time.Duration { + if i.packet == nil || i.packet.Pts() == astiav.NoPtsValue { + return 0 + } + + timeBase := i.formatContext.Streams()[i.videoStreamIdx].TimeBase() + durationMicros := astiav.RescaleQ(i.packet.Pts(), timeBase, astiav.NewRational(1, 1000000)) + return time.Duration(durationMicros) * time.Microsecond +} + func (i *Iterator) DurationMs() time.Duration { - return (i.packet.Time / time.Millisecond) * time.Millisecond + return i.Duration().Round(time.Millisecond) +} + +func (i *Iterator) IsKeyFrame() bool { + return i.currentFrame.KeyFrame() } -func (i *Iterator) IsKeyFrame() bool { return i.packet.IsKeyFrame } diff --git a/internal/project/video_iterator_test.go b/internal/project/video_iterator_test.go new file mode 100644 index 0000000..44277a4 --- /dev/null +++ b/internal/project/video_iterator_test.go @@ -0,0 +1,50 @@ +package project + +import ( + "fmt" + "image" + "image/png" + "log" + "os" + "testing" +) + +func SavePNG(img image.Image, filename string) error { + if img == nil { + return nil + } + // Save the image to a file + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + return png.Encode(file, img) +} + +func TestNewIterator(t *testing.T) { + i, err := NewIterator("../../IMG_2399_1024.MOV") + if err != nil { + t.Fatalf("NewIterator() error = %v", err) + } + defer i.Close() + os.Mkdir("testdata", 0755) + + n := 0 + for i.Next() && n < 10 { + log.Printf("Next() returned true for frame %d err:%s", i.Frame(), i.Error()) + n++ + frame := i.Frame() + img := i.Image() + log.Printf("Frame %d: Img:%T img.bounds=%#v duration=%s", frame, img, img.Bounds(), i.Duration()) + // write out to testdata/%d.png + if img != nil { + err = SavePNG(img, fmt.Sprintf("testdata/%d.png", frame)) + if err != nil { + t.Errorf("SavePNG() error = %v", err) + } + } else { + log.Printf("No image at frame %d", frame) + } + } +}