Skip to content

Commit

Permalink
Merge pull request #4 from abema/support-NDF-timecode
Browse files Browse the repository at this point in the history
feat: support 29.97, 59.94 NDF
  • Loading branch information
lomavkin authored Jun 20, 2024
2 parents 9ebb1b2 + a00cc54 commit d8dea53
Show file tree
Hide file tree
Showing 2 changed files with 407 additions and 190 deletions.
249 changes: 174 additions & 75 deletions timecode/timecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (

// rate represents frame rate.
type rate struct {
fps int
roundFPS int
actualFPS float64
numerator int32
denominator int32
dropFrames int
Expand All @@ -19,19 +20,25 @@ type rate struct {
}

var (
// supportedRates represents supported frame rates 23.976, 24, 25, 29.97DF, 30, 48, 50, 59.94DF, 60.
supportedRates = []*rate{
{fps: 10, numerator: 10, denominator: 1, dropFrames: 0, framesPer1Min: 10 * 60, framesPer10Min: 10 * 600}, // 10
{fps: 15, numerator: 15, denominator: 1, dropFrames: 0, framesPer1Min: 15 * 60, framesPer10Min: 15 * 600}, // 15
{fps: 24, numerator: 24000, denominator: 1001, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 23.976
{fps: 24, numerator: 24, denominator: 1, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 24
{fps: 25, numerator: 25, denominator: 1, dropFrames: 0, framesPer1Min: 25 * 60, framesPer10Min: 25 * 600}, // 25
{fps: 30, numerator: 30000, denominator: 1001, dropFrames: 2, framesPer1Min: 30*60 - 2, framesPer10Min: 30*600 - 9*2}, // 29.97DF
{fps: 30, numerator: 30, denominator: 1, dropFrames: 0, framesPer1Min: 30 * 60, framesPer10Min: 30 * 600}, // 30
{fps: 48, numerator: 48, denominator: 1, dropFrames: 0, framesPer1Min: 48 * 60, framesPer10Min: 48 * 600}, // 48
{fps: 50, numerator: 50, denominator: 1, dropFrames: 0, framesPer1Min: 50 * 60, framesPer10Min: 50 * 600}, // 50
{fps: 60, numerator: 60000, denominator: 1001, dropFrames: 4, framesPer1Min: 60*60 - 4, framesPer10Min: 60*600 - 9*4}, // 59.94DF
{fps: 60, numerator: 60, denominator: 1, dropFrames: 0, framesPer1Min: 60 * 60, framesPer10Min: 60 * 600}, // 60
// supportedNDFRates represents supported frame rates 23.976, 24, 25, 29.97NDF, 30, 48, 50, 59.94NDF, 60.
supportedNDFRates = []*rate{
{roundFPS: 10, actualFPS: 10, numerator: 10, denominator: 1, dropFrames: 0, framesPer1Min: 10 * 60, framesPer10Min: 10 * 600}, // 10
{roundFPS: 15, actualFPS: 15, numerator: 15, denominator: 1, dropFrames: 0, framesPer1Min: 15 * 60, framesPer10Min: 15 * 600}, // 15
{roundFPS: 24, actualFPS: 23.976, numerator: 24000, denominator: 1001, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 23.976
{roundFPS: 24, actualFPS: 24, numerator: 24, denominator: 1, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 24
{roundFPS: 25, actualFPS: 25, numerator: 25, denominator: 1, dropFrames: 0, framesPer1Min: 25 * 60, framesPer10Min: 25 * 600}, // 25
{roundFPS: 30, actualFPS: 29.97, numerator: 30000, denominator: 1001, dropFrames: 0, framesPer1Min: 30 * 60, framesPer10Min: 30 * 600}, // 29.97NDF (optional)
{roundFPS: 30, actualFPS: 30, numerator: 30, denominator: 1, dropFrames: 0, framesPer1Min: 30 * 60, framesPer10Min: 30 * 600}, // 30
{roundFPS: 48, actualFPS: 48, numerator: 48, denominator: 1, dropFrames: 0, framesPer1Min: 48 * 60, framesPer10Min: 48 * 600}, // 48
{roundFPS: 50, actualFPS: 50, numerator: 50, denominator: 1, dropFrames: 0, framesPer1Min: 50 * 60, framesPer10Min: 50 * 600}, // 50
{roundFPS: 60, actualFPS: 59.94, numerator: 60000, denominator: 1001, dropFrames: 0, framesPer1Min: 60 * 60, framesPer10Min: 60 * 600}, // 59.94NDF (optional)
{roundFPS: 60, actualFPS: 60, numerator: 60, denominator: 1, dropFrames: 0, framesPer1Min: 60 * 60, framesPer10Min: 60 * 600}, // 60
}

// supportedDFRates represents supported frame rates 29.97DF, 59.94DF.
supportedDFRates = []*rate{
{roundFPS: 30, actualFPS: 29.97, numerator: 30000, denominator: 1001, dropFrames: 2, framesPer1Min: 30*60 - 2, framesPer10Min: 30*600 - 9*2}, // 29.97DF (preferred)
{roundFPS: 60, actualFPS: 59.94, numerator: 60000, denominator: 1001, dropFrames: 4, framesPer1Min: 60*60 - 4, framesPer10Min: 60*600 - 9*4}, // 59.94DF (preferred)
}

// timecodePattern represents timecode pattern.
Expand All @@ -49,84 +56,176 @@ var (

// Timecode represents timecode.
type Timecode struct {
optp TimecodeOptionParam
r *rate
HH uint64
MM uint64
SS uint64
FF uint64
}

// TimecodeOptionParam represents timecode option parameter.
type TimecodeOptionParam struct {
Sep string
SepDF string
preferDF bool
sep string
lastSep string
r *rate
HH uint64
MM uint64
SS uint64
FF uint64
}

// TimecodeOption represents timecode option.
type TimecodeOption func(*TimecodeOptionParam)

// newTimecodeOptionParam returns new TimecodeOptionParam.
func newTimecodeOptionParam() TimecodeOptionParam {
return TimecodeOptionParam{
Sep: ":",
SepDF: ":",
}
}

// applyTimecodeOption applies TimecodeOption to TimecodeOptionParam.
func (p *TimecodeOptionParam) applyTimecodeOption(opts ...TimecodeOption) {
for _, opt := range opts {
opt(p)
// newNDFRate returns new NDF rate.
func newNDFRate(num, den int32) (*rate, error) {
fps := float64(num) / float64(den)
for _, r := range supportedNDFRates {
if float64(r.numerator)/float64(r.denominator) == fps {
return r, nil
}
}
return nil, ErrUnsupportedFrameRate
}

// newRate returns new rate.
func newRate(num, den int32) (*rate, error) {
// newDFRate returns new DF rate.
func newDFRate(num, den int32) (*rate, error) {
fps := float64(num) / float64(den)
for _, r := range supportedRates {
for _, r := range supportedDFRates {
if float64(r.numerator)/float64(r.denominator) == fps {
return r, nil
}
}
return nil, ErrUnsupportedFrameRate
}

// newRate returns new rate.
func newRate(num, den int32, preferDF bool) (*rate, error) {
if preferDF {
r, err := newDFRate(num, den)
if err != nil {
if errors.Is(err, ErrUnsupportedFrameRate) {
return newNDFRate(num, den)
}
return nil, err
}
return r, nil
}
return newNDFRate(num, den)
}

// IsSupportedFrameRate returns whether frame rate is supported.
func IsSupportedFrameRate(num, den int32) bool {
_, err := newRate(num, den)
_, err := newNDFRate(num, den)
return err == nil
}

// IsRepresentableFramesOptionParam represents IsRepresentableFrames option parameter.
type IsRepresentableFramesOptionParam struct {
PreferDF bool
}

// IsRepresentableFramesOption represents IsRepresentableFrames option.
type IsRepresentableFramesOption func(*IsRepresentableFramesOptionParam)

// newIsRepresentableFramesOptionParam returns new IsRepresentableFramesOptionParam.
func newIsRepresentableFramesOptionParam() IsRepresentableFramesOptionParam {
return IsRepresentableFramesOptionParam{
PreferDF: true, // if frame rate is DF or NDF, assume DF
}
}

// applyIsRepresentableFramesOption applies IsRepresentableFramesOption to IsRepresentableFramesOptionParam.
func (p *IsRepresentableFramesOptionParam) applyIsRepresentableFramesOption(opts ...IsRepresentableFramesOption) {
for _, opt := range opts {
opt(p)
}
}

// IsRepresentableFrames returns whether frames is representable.
func IsRepresentableFrames(frames uint64, num, den int32) bool {
r, err := newRate(num, den)
func IsRepresentableFrames(frames uint64, num, den int32, opts ...IsRepresentableFramesOption) bool {
p := newIsRepresentableFramesOptionParam()
p.applyIsRepresentableFramesOption(opts...)

r, err := newRate(num, den, p.PreferDF)
if err != nil {
return false
}
return r.isRepresentableFrames(frames)
}

// TimecodeOptionParam represents timecode option parameter.
type TimecodeOptionParam struct {
PreferDF bool
Sep string
LastSep string
}

// TimecodeOption represents timecode option.
type TimecodeOption func(*TimecodeOptionParam)

// newTimecodeOptionParam returns new TimecodeOptionParam.
func newTimecodeOptionParam() TimecodeOptionParam {
return TimecodeOptionParam{
PreferDF: true, // if frame rate is 29.97 or 59.94, assume DF. otherwise, assume NDF
Sep: ":",
LastSep: ":",
}
}

// applyTimecodeOption applies TimecodeOption to TimecodeOptionParam.
func (p *TimecodeOptionParam) applyTimecodeOption(opts ...TimecodeOption) {
for _, opt := range opts {
opt(p)
}
}

// NewTimecode returns new Timecode.
func NewTimecode(frames uint64, num, den int32, opts ...TimecodeOption) (*Timecode, error) {
r, err := newRate(num, den)
p := newTimecodeOptionParam()
p.applyTimecodeOption(opts...)

r, err := newRate(num, den, p.PreferDF)
if err != nil {
return nil, err
}

p := newTimecodeOptionParam()
p.applyTimecodeOption(opts...)
lastSep := p.LastSep
if r.dropFrames == 0 {
lastSep = p.Sep
}

tc, err := Reset(&Timecode{r: r, optp: p}, frames)
tc, err := Reset(&Timecode{
preferDF: p.PreferDF,
sep: p.Sep,
lastSep: lastSep,
r: r,
}, frames)
if err != nil {
return nil, err
}
return tc, nil
}

// TimecodeOptionParam represents timecode option parameter.
type ParseTimecodeOptionParam struct {
PreferDF bool
Sep string
LastSep string
}

// ParseTimecodeOption represents parse timecode option.
type ParseTimecodeOption func(*ParseTimecodeOptionParam)

// newParseTimecodeOptionParam returns new ParseTimecodeOptionParam.
func newParseTimecodeOptionParam() ParseTimecodeOptionParam {
return ParseTimecodeOptionParam{
PreferDF: true, // if frame rate is 29.97 or 59.94, assume DF. otherwise, assume NDF
}
}

// applyParseTimecodeOption applies ParseTimecodeOption to ParseTimecodeOptionParam.
func (p *ParseTimecodeOptionParam) applyParseTimecodeOption(opts ...ParseTimecodeOption) {
for _, opt := range opts {
opt(p)
}
}

// ParseTimecode returns new Timecode from formatted string.
func ParseTimecode(s string, num, den int32) (*Timecode, error) {
r, err := newRate(num, den)
func ParseTimecode(s string, num, den int32, opts ...ParseTimecodeOption) (*Timecode, error) {
p := newParseTimecodeOptionParam()
p.applyParseTimecodeOption(opts...)

r, err := newRate(num, den, p.PreferDF)
if err != nil {
return nil, err
}
Expand All @@ -142,20 +241,25 @@ func ParseTimecode(s string, num, den int32) (*Timecode, error) {
sep := match[2]
mm, _ := strconv.Atoi(match[3])
ss, _ := strconv.Atoi(match[5])
sepDF := match[6]
lastSep := match[6]
ff, _ := strconv.Atoi(match[7])

if ff < r.dropFrames && mm%10 != 0 {
ff = r.dropFrames
}
if r.dropFrames == 0 {
lastSep = sep
}

return &Timecode{
r: r,
optp: TimecodeOptionParam{Sep: sep, SepDF: sepDF},
HH: uint64(hh),
MM: uint64(mm),
SS: uint64(ss),
FF: uint64(ff),
preferDF: p.PreferDF,
sep: sep,
lastSep: lastSep,
r: r,
HH: uint64(hh),
MM: uint64(mm),
SS: uint64(ss),
FF: uint64(ff),
}, nil
}

Expand All @@ -179,7 +283,7 @@ func Reset(tc *Timecode, frames uint64) (*Timecode, error) {
f += df * ((m - df) / uint64(new.r.framesPer1Min))
}

fps := uint64(new.r.fps)
fps := uint64(new.r.roundFPS)
new.FF = f % fps
new.SS = f / fps % 60
new.MM = f / (fps * 60) % 60
Expand All @@ -193,7 +297,7 @@ func (r *rate) equal(other *rate) bool {
if r == nil || other == nil {
return false
}
return r.numerator == other.numerator && r.denominator == other.denominator
return r.numerator == other.numerator && r.denominator == other.denominator && r.dropFrames == other.dropFrames
}

// isRepresentableFrames returns whether frames is representable.
Expand All @@ -204,12 +308,12 @@ func (r *rate) isRepresentableFrames(frames uint64) bool {
// Frames returns number of frames.
func (tc *Timecode) Frames() uint64 {
var frames uint64
frames += tc.HH * 3600 * uint64(tc.r.fps)
frames += tc.MM * 60 * uint64(tc.r.fps)
frames += tc.SS * uint64(tc.r.fps)
frames += tc.HH * 3600 * uint64(tc.r.roundFPS)
frames += tc.MM * 60 * uint64(tc.r.roundFPS)
frames += tc.SS * uint64(tc.r.roundFPS)
frames += tc.FF

framesPer10Min := uint64(tc.r.fps) * 60 * 10
framesPer10Min := uint64(tc.r.roundFPS) * 60 * 10
framesPer1Min := framesPer10Min / 10

var df uint64
Expand All @@ -221,7 +325,7 @@ func (tc *Timecode) Frames() uint64 {

// Duration returns duration from zero-origin.
func (tc *Timecode) Duration() time.Duration {
return time.Duration((float64(tc.Frames()) * float64(tc.r.denominator) / float64(tc.r.numerator)) * float64(time.Second))
return time.Duration((float64(tc.Frames()) / float64(tc.r.actualFPS)) * float64(time.Second))
}

// Framerate denominator.
Expand Down Expand Up @@ -269,19 +373,14 @@ func (tc *Timecode) SubFrames(frames uint64) (*Timecode, error) {
// String returns Timecode formatted string.
// e.g. 01:23:45:28
func (tc *Timecode) String() string {
sep := tc.optp.Sep
lastSep := sep
if tc.r.dropFrames > 0 {
lastSep = tc.optp.SepDF
}
return fmt.Sprintf(
"%02d%s%02d%s%02d%s%02d",
tc.HH,
sep,
tc.sep,
tc.MM,
sep,
tc.sep,
tc.SS,
lastSep,
tc.lastSep,
tc.FF,
)
}
Loading

0 comments on commit d8dea53

Please sign in to comment.