Merge pull request #148 from mheers/feature/timecode

massive speedup at seeking by timecode by approximating the first jump
This commit is contained in:
alex
2022-04-25 11:42:53 +04:00
committed by GitHub
5 changed files with 209 additions and 10 deletions

View File

@@ -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
}

2
go.mod
View File

@@ -1,3 +1,5 @@
module github.com/3d0c/gmf
go 1.12
require github.com/stretchr/testify v1.7.1

11
go.sum Normal file
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}
}