mirror of
https://github.com/jehiah/TrafficSpeed.git
synced 2025-09-27 12:42:23 +08:00
migrate to github.com/asticode/go-astiav
This commit is contained in:
6
go.mod
6
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
|
||||
)
|
||||
|
16
go.sum
16
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=
|
||||
|
1
internal/project/.gitignore
vendored
Normal file
1
internal/project/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
testdata
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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 }
|
||||
|
50
internal/project/video_iterator_test.go
Normal file
50
internal/project/video_iterator_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user