diff --git a/format_go112.go b/format_go112.go index 4665fe3..d1044e5 100644 --- a/format_go112.go +++ b/format_go112.go @@ -380,6 +380,40 @@ func (ctx *FmtCtx) GetNextPacketForStreamIndex(streamIndex int) (*Packet, error) } } +func (ctx *FmtCtx) GetFirstPacketForStreamIndex(streamIndex int) (*Packet, error) { + currentPacket, err := ctx.GetNextPacketForStreamIndex(streamIndex) + if err != nil { + return nil, fmt.Errorf("failed to get current packet: %s", err) + } + originalPosition := currentPacket.Pos() + + err = ctx.SeekFrameAt(0, streamIndex) + if err != nil { + return nil, fmt.Errorf("failed to seek to first frame: %s", err) + } + + packet, err := ctx.GetNextPacketForStreamIndex(streamIndex) + if err != nil { + return nil, fmt.Errorf("failed to get next packet: %s", err) + } + + err = ctx.SeekFrameAt(originalPosition, streamIndex) + if err != nil { + return nil, fmt.Errorf("failed to seek to original position: %s", err) + } + + return packet, nil +} + +func (ctx *FmtCtx) GetLastPacketForStreamIndex(streamIndex int) (*Packet, error) { + err := ctx.SeekFrameAt(int64(ctx.Duration()), streamIndex) + if err != nil { + return nil, fmt.Errorf("failed to seek to last frame: %s", err) + } + + return ctx.GetNextPacketForStreamIndex(streamIndex) +} + func (ctx *FmtCtx) GetNewPackets() chan *Packet { yield := make(chan *Packet) @@ -572,25 +606,76 @@ func (ctx *FmtCtx) SeekFrameAtTimeCode(timecode string, streamIndex int) error { istCodecCtx := ist.CodecCtx() istAvgFrameRate := ist.GetAvgFrameRate() - sec := int64(0) + // check if timecode is valid (i.e. if the timecode is between the first and last frame of the video) + firstPacket, err := ctx.GetFirstPacketForStreamIndex(streamIndex) + if err != nil { + return err + } + lastPacket, err := ctx.GetLastPacketForStreamIndex(streamIndex) + if err != nil { + return err + } + firstFrameTimeCode, err := getPacketTimeCode(firstPacket, istCodecCtx, istAvgFrameRate) + if err != nil { + return err + } + lastFrameTimeCode, err := getPacketTimeCode(lastPacket, istCodecCtx, istAvgFrameRate) + if err != nil { + return err + } + + tcBetween, err := IsTimeCodeBetween(timecode, firstFrameTimeCode, lastFrameTimeCode) + if err != nil { + return err + } + if !tcBetween { + return errors.New("timecode is not between the first and last frame of the video") + } + + // get comparable timecodes + hhToTest, hhStart, hhEnd, err := TimeCodeToComparable(timecode, firstFrameTimeCode, lastFrameTimeCode) + if err != nil { + return err + } + + // calculate a good approximation of the position of the needed frame + var relativePos float64 = float64(float64(hhToTest-hhStart) / float64(hhEnd-hhStart)) + + // rewind a bit to be sure we are not too far + if relativePos-0.001 > 0 { + relativePos -= 0.001 + } + + sec := ctx.Duration() * relativePos + err = ctx.SeekFrameAt(int64(sec), streamIndex) + if err != nil { + return err + } found := false for !found { - packet, err := ctx.GetNextPacket() + packet, err := ctx.GetNextPacketForStreamIndex(streamIndex) if err != nil { return err } - if packet.StreamIndex() != streamIndex { - continue - } - found, err = isPacketLaterThanTimeCode(packet, timecode, istCodecCtx, istAvgFrameRate) + frameTimeCode, err := getPacketTimeCode(packet, istCodecCtx, istAvgFrameRate) if err != nil { return err } - if !found { - sec += 5 - err = ctx.SeekFrameAt(sec, streamIndex) + + hhFrameTimeCode, _, _, err := TimeCodeToComparable(frameTimeCode, firstFrameTimeCode, lastFrameTimeCode) + if err != nil { + return err + } + + if hhFrameTimeCode >= hhToTest { + return nil + } + + if hhFrameTimeCode < hhToTest { + sec += 1 + err = ctx.SeekFrameAt(int64(sec), streamIndex) if err != nil { return err } diff --git a/go.mod b/go.mod index a358606..a011dd5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/3d0c/gmf go 1.12 + +require github.com/stretchr/testify v1.7.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2dca7c9 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/utils.go b/utils.go index 5acf3de..6241a66 100644 --- a/utils.go +++ b/utils.go @@ -27,6 +27,8 @@ import ( "bytes" "errors" "fmt" + "strconv" + "strings" "syscall" "unsafe" ) @@ -184,3 +186,69 @@ func GenSyntVideoN(N, w, h int, fmt int32) chan *Frame { }() return yield } + +func TimeCodeToComparable(timeCodeToTest, timeCodeStart, timeCodeEnd string) (hhToTest int, hhStart int, hhEnd int, err error) { + hhStart, err = TimeCodeToHundredths(timeCodeStart) + if err != nil { + return + } + hhEnd, err = TimeCodeToHundredths(timeCodeEnd) + if err != nil { + return + } + hhToTest, err = TimeCodeToHundredths(timeCodeToTest) + if err != nil { + return + } + + if hhEnd < hhStart { + hhStart -= hhEnd + hhToTest -= hhEnd + if hhToTest <= 0 { + hhToTest += 24 * 60 * 60 * 100 + } + hhEnd = 24 * 60 * 60 * 100 + } + return +} + +func IsTimeCodeBetween(timeCodeToTest, timeCodeStart, timeCodeEnd string) (bool, error) { + hhToTest, hhStart, hhEnd, err := TimeCodeToComparable(timeCodeToTest, timeCodeStart, timeCodeEnd) + if err != nil { + return false, err + } + return hhToTest >= hhStart && hhToTest <= hhEnd, nil +} + +// TimeCodeToHHMMSS splits a timeCode = "20:15:31:00" to return 20, 15, 31 +func TimeCodeToHHMMSS(timeCode string) (int, int, int, int, error) { + parts := strings.Split(timeCode, ":") + if len(parts) != 4 { + return -1, -1, -1, -1, errors.New("invalid timecode") + } + hh, err := strconv.Atoi(parts[0]) + if err != nil { + return -1, -1, -1, -1, errors.New("invalid timecode") + } + mm, err := strconv.Atoi(parts[1]) + if err != nil { + return -1, -1, -1, -1, errors.New("invalid timecode") + } + ss, err := strconv.Atoi(parts[2]) + if err != nil { + return -1, -1, -1, -1, errors.New("invalid timecode") + } + hs, err := strconv.Atoi(parts[3]) + if err != nil { + return -1, -1, -1, -1, errors.New("invalid timecode") + } + return hh, mm, ss, hs, nil +} + +func TimeCodeToHundredths(timeCode string) (int, error) { + hh, mm, ss, hs, err := TimeCodeToHHMMSS(timeCode) + if err != nil { + return -1, err + } + return (hh * 60 * 60 * 100) + (mm * 60 * 100) + (ss * 100) + hs, nil +} diff --git a/utils_test.go b/utils_test.go index e873d52..bf2cdb5 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,8 +1,10 @@ package gmf_test import ( - "github.com/3d0c/gmf" "testing" + + "github.com/3d0c/gmf" + "github.com/stretchr/testify/require" ) func TestAvError(t *testing.T) { @@ -10,3 +12,34 @@ func TestAvError(t *testing.T) { t.Fatalf("Expected error is 'No such file or directory', '%s' got\n", err.Error()) } } + +func TestIsTimeCodeBetween(t *testing.T) { + timeCodeStart := "20:31:00:62" + timeCodeEnd := "04:56:12:00" + tests := []struct { + timeCodeToTest string + expectedBool bool + errorExpected bool + }{ + {"20:15:31:00", false, false}, + {"20:30:59:99", false, false}, + {"20:30:59:99", false, false}, + {"20:31:00:00", false, false}, + {"20:31:00:61", false, false}, + {"20:31:31:62", true, false}, + {"04:31:31:00", true, false}, + {"04:56:12:00", true, false}, + {"04:56:12:01", false, false}, + {"05:31:31:00", false, false}, + {"05:31:31", false, true}, + } + for _, test := range tests { + isTimeCodeBetween, err := gmf.IsTimeCodeBetween(test.timeCodeToTest, timeCodeStart, timeCodeEnd) + if test.errorExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, test.expectedBool, isTimeCodeBetween, "hourToTest: %s", test.timeCodeToTest) + } +}