mirror of
https://github.com/3d0c/gmf
synced 2025-12-24 10:40:59 +08:00
Merge pull request #148 from mheers/feature/timecode
massive speedup at seeking by timecode by approximating the first jump
This commit is contained in:
103
format_go112.go
103
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
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -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
11
go.sum
Normal 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=
|
||||
68
utils.go
68
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user