mirror of
https://github.com/jehiah/TrafficSpeed.git
synced 2025-10-02 15:12:11 +08:00
260 lines
6.2 KiB
Go
260 lines
6.2 KiB
Go
package project
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/asticode/go-astiav"
|
|
)
|
|
|
|
func init() {
|
|
astiav.SetLogLevel(astiav.LogLevelError)
|
|
}
|
|
|
|
type Iterator struct {
|
|
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.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
|
|
}
|
|
}
|
|
|
|
func NewIterator(filename string) (iter *Iterator, err error) {
|
|
if filename == "" {
|
|
panic("missing filename")
|
|
}
|
|
|
|
iter = &Iterator{frame: -1}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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.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 {
|
|
// 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.currentFrame == nil {
|
|
// continue
|
|
// }
|
|
// return true
|
|
// }
|
|
// return false
|
|
// }
|
|
|
|
func (i *Iterator) Next() bool {
|
|
i.decoded = false
|
|
|
|
for {
|
|
// Read frame
|
|
err := i.formatContext.ReadFrame(i.packet)
|
|
if err != nil {
|
|
if errors.Is(err, astiav.ErrEof) {
|
|
return false
|
|
}
|
|
i.err = fmt.Errorf("reading frame: %w", err)
|
|
return false
|
|
}
|
|
|
|
// 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.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
|
|
}
|
|
}
|
|
|
|
func (i *Iterator) Frame() int {
|
|
return i.frame
|
|
}
|
|
|
|
func (i *Iterator) Image() image.Image {
|
|
return i.img
|
|
}
|
|
|
|
func (i *Iterator) Error() error {
|
|
return i.err
|
|
}
|
|
|
|
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.Duration().Round(time.Millisecond)
|
|
}
|
|
|
|
func (i *Iterator) IsKeyFrame() bool {
|
|
return i.currentFrame.KeyFrame()
|
|
}
|