Skip to content

Commit d8dea53

Browse files
authored
Merge pull request #4 from abema/support-NDF-timecode
feat: support 29.97, 59.94 NDF
2 parents 9ebb1b2 + a00cc54 commit d8dea53

File tree

2 files changed

+407
-190
lines changed

2 files changed

+407
-190
lines changed

timecode/timecode.go

Lines changed: 174 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import (
1010

1111
// rate represents frame rate.
1212
type rate struct {
13-
fps int
13+
roundFPS int
14+
actualFPS float64
1415
numerator int32
1516
denominator int32
1617
dropFrames int
@@ -19,19 +20,25 @@ type rate struct {
1920
}
2021

2122
var (
22-
// supportedRates represents supported frame rates 23.976, 24, 25, 29.97DF, 30, 48, 50, 59.94DF, 60.
23-
supportedRates = []*rate{
24-
{fps: 10, numerator: 10, denominator: 1, dropFrames: 0, framesPer1Min: 10 * 60, framesPer10Min: 10 * 600}, // 10
25-
{fps: 15, numerator: 15, denominator: 1, dropFrames: 0, framesPer1Min: 15 * 60, framesPer10Min: 15 * 600}, // 15
26-
{fps: 24, numerator: 24000, denominator: 1001, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 23.976
27-
{fps: 24, numerator: 24, denominator: 1, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 24
28-
{fps: 25, numerator: 25, denominator: 1, dropFrames: 0, framesPer1Min: 25 * 60, framesPer10Min: 25 * 600}, // 25
29-
{fps: 30, numerator: 30000, denominator: 1001, dropFrames: 2, framesPer1Min: 30*60 - 2, framesPer10Min: 30*600 - 9*2}, // 29.97DF
30-
{fps: 30, numerator: 30, denominator: 1, dropFrames: 0, framesPer1Min: 30 * 60, framesPer10Min: 30 * 600}, // 30
31-
{fps: 48, numerator: 48, denominator: 1, dropFrames: 0, framesPer1Min: 48 * 60, framesPer10Min: 48 * 600}, // 48
32-
{fps: 50, numerator: 50, denominator: 1, dropFrames: 0, framesPer1Min: 50 * 60, framesPer10Min: 50 * 600}, // 50
33-
{fps: 60, numerator: 60000, denominator: 1001, dropFrames: 4, framesPer1Min: 60*60 - 4, framesPer10Min: 60*600 - 9*4}, // 59.94DF
34-
{fps: 60, numerator: 60, denominator: 1, dropFrames: 0, framesPer1Min: 60 * 60, framesPer10Min: 60 * 600}, // 60
23+
// supportedNDFRates represents supported frame rates 23.976, 24, 25, 29.97NDF, 30, 48, 50, 59.94NDF, 60.
24+
supportedNDFRates = []*rate{
25+
{roundFPS: 10, actualFPS: 10, numerator: 10, denominator: 1, dropFrames: 0, framesPer1Min: 10 * 60, framesPer10Min: 10 * 600}, // 10
26+
{roundFPS: 15, actualFPS: 15, numerator: 15, denominator: 1, dropFrames: 0, framesPer1Min: 15 * 60, framesPer10Min: 15 * 600}, // 15
27+
{roundFPS: 24, actualFPS: 23.976, numerator: 24000, denominator: 1001, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 23.976
28+
{roundFPS: 24, actualFPS: 24, numerator: 24, denominator: 1, dropFrames: 0, framesPer1Min: 24 * 60, framesPer10Min: 24 * 600}, // 24
29+
{roundFPS: 25, actualFPS: 25, numerator: 25, denominator: 1, dropFrames: 0, framesPer1Min: 25 * 60, framesPer10Min: 25 * 600}, // 25
30+
{roundFPS: 30, actualFPS: 29.97, numerator: 30000, denominator: 1001, dropFrames: 0, framesPer1Min: 30 * 60, framesPer10Min: 30 * 600}, // 29.97NDF (optional)
31+
{roundFPS: 30, actualFPS: 30, numerator: 30, denominator: 1, dropFrames: 0, framesPer1Min: 30 * 60, framesPer10Min: 30 * 600}, // 30
32+
{roundFPS: 48, actualFPS: 48, numerator: 48, denominator: 1, dropFrames: 0, framesPer1Min: 48 * 60, framesPer10Min: 48 * 600}, // 48
33+
{roundFPS: 50, actualFPS: 50, numerator: 50, denominator: 1, dropFrames: 0, framesPer1Min: 50 * 60, framesPer10Min: 50 * 600}, // 50
34+
{roundFPS: 60, actualFPS: 59.94, numerator: 60000, denominator: 1001, dropFrames: 0, framesPer1Min: 60 * 60, framesPer10Min: 60 * 600}, // 59.94NDF (optional)
35+
{roundFPS: 60, actualFPS: 60, numerator: 60, denominator: 1, dropFrames: 0, framesPer1Min: 60 * 60, framesPer10Min: 60 * 600}, // 60
36+
}
37+
38+
// supportedDFRates represents supported frame rates 29.97DF, 59.94DF.
39+
supportedDFRates = []*rate{
40+
{roundFPS: 30, actualFPS: 29.97, numerator: 30000, denominator: 1001, dropFrames: 2, framesPer1Min: 30*60 - 2, framesPer10Min: 30*600 - 9*2}, // 29.97DF (preferred)
41+
{roundFPS: 60, actualFPS: 59.94, numerator: 60000, denominator: 1001, dropFrames: 4, framesPer1Min: 60*60 - 4, framesPer10Min: 60*600 - 9*4}, // 59.94DF (preferred)
3542
}
3643

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

5057
// Timecode represents timecode.
5158
type Timecode struct {
52-
optp TimecodeOptionParam
53-
r *rate
54-
HH uint64
55-
MM uint64
56-
SS uint64
57-
FF uint64
58-
}
59-
60-
// TimecodeOptionParam represents timecode option parameter.
61-
type TimecodeOptionParam struct {
62-
Sep string
63-
SepDF string
59+
preferDF bool
60+
sep string
61+
lastSep string
62+
r *rate
63+
HH uint64
64+
MM uint64
65+
SS uint64
66+
FF uint64
6467
}
6568

66-
// TimecodeOption represents timecode option.
67-
type TimecodeOption func(*TimecodeOptionParam)
68-
69-
// newTimecodeOptionParam returns new TimecodeOptionParam.
70-
func newTimecodeOptionParam() TimecodeOptionParam {
71-
return TimecodeOptionParam{
72-
Sep: ":",
73-
SepDF: ":",
74-
}
75-
}
76-
77-
// applyTimecodeOption applies TimecodeOption to TimecodeOptionParam.
78-
func (p *TimecodeOptionParam) applyTimecodeOption(opts ...TimecodeOption) {
79-
for _, opt := range opts {
80-
opt(p)
69+
// newNDFRate returns new NDF rate.
70+
func newNDFRate(num, den int32) (*rate, error) {
71+
fps := float64(num) / float64(den)
72+
for _, r := range supportedNDFRates {
73+
if float64(r.numerator)/float64(r.denominator) == fps {
74+
return r, nil
75+
}
8176
}
77+
return nil, ErrUnsupportedFrameRate
8278
}
8379

84-
// newRate returns new rate.
85-
func newRate(num, den int32) (*rate, error) {
80+
// newDFRate returns new DF rate.
81+
func newDFRate(num, den int32) (*rate, error) {
8682
fps := float64(num) / float64(den)
87-
for _, r := range supportedRates {
83+
for _, r := range supportedDFRates {
8884
if float64(r.numerator)/float64(r.denominator) == fps {
8985
return r, nil
9086
}
9187
}
9288
return nil, ErrUnsupportedFrameRate
9389
}
9490

91+
// newRate returns new rate.
92+
func newRate(num, den int32, preferDF bool) (*rate, error) {
93+
if preferDF {
94+
r, err := newDFRate(num, den)
95+
if err != nil {
96+
if errors.Is(err, ErrUnsupportedFrameRate) {
97+
return newNDFRate(num, den)
98+
}
99+
return nil, err
100+
}
101+
return r, nil
102+
}
103+
return newNDFRate(num, den)
104+
}
105+
95106
// IsSupportedFrameRate returns whether frame rate is supported.
96107
func IsSupportedFrameRate(num, den int32) bool {
97-
_, err := newRate(num, den)
108+
_, err := newNDFRate(num, den)
98109
return err == nil
99110
}
100111

112+
// IsRepresentableFramesOptionParam represents IsRepresentableFrames option parameter.
113+
type IsRepresentableFramesOptionParam struct {
114+
PreferDF bool
115+
}
116+
117+
// IsRepresentableFramesOption represents IsRepresentableFrames option.
118+
type IsRepresentableFramesOption func(*IsRepresentableFramesOptionParam)
119+
120+
// newIsRepresentableFramesOptionParam returns new IsRepresentableFramesOptionParam.
121+
func newIsRepresentableFramesOptionParam() IsRepresentableFramesOptionParam {
122+
return IsRepresentableFramesOptionParam{
123+
PreferDF: true, // if frame rate is DF or NDF, assume DF
124+
}
125+
}
126+
127+
// applyIsRepresentableFramesOption applies IsRepresentableFramesOption to IsRepresentableFramesOptionParam.
128+
func (p *IsRepresentableFramesOptionParam) applyIsRepresentableFramesOption(opts ...IsRepresentableFramesOption) {
129+
for _, opt := range opts {
130+
opt(p)
131+
}
132+
}
133+
101134
// IsRepresentableFrames returns whether frames is representable.
102-
func IsRepresentableFrames(frames uint64, num, den int32) bool {
103-
r, err := newRate(num, den)
135+
func IsRepresentableFrames(frames uint64, num, den int32, opts ...IsRepresentableFramesOption) bool {
136+
p := newIsRepresentableFramesOptionParam()
137+
p.applyIsRepresentableFramesOption(opts...)
138+
139+
r, err := newRate(num, den, p.PreferDF)
104140
if err != nil {
105141
return false
106142
}
107143
return r.isRepresentableFrames(frames)
108144
}
109145

146+
// TimecodeOptionParam represents timecode option parameter.
147+
type TimecodeOptionParam struct {
148+
PreferDF bool
149+
Sep string
150+
LastSep string
151+
}
152+
153+
// TimecodeOption represents timecode option.
154+
type TimecodeOption func(*TimecodeOptionParam)
155+
156+
// newTimecodeOptionParam returns new TimecodeOptionParam.
157+
func newTimecodeOptionParam() TimecodeOptionParam {
158+
return TimecodeOptionParam{
159+
PreferDF: true, // if frame rate is 29.97 or 59.94, assume DF. otherwise, assume NDF
160+
Sep: ":",
161+
LastSep: ":",
162+
}
163+
}
164+
165+
// applyTimecodeOption applies TimecodeOption to TimecodeOptionParam.
166+
func (p *TimecodeOptionParam) applyTimecodeOption(opts ...TimecodeOption) {
167+
for _, opt := range opts {
168+
opt(p)
169+
}
170+
}
171+
110172
// NewTimecode returns new Timecode.
111173
func NewTimecode(frames uint64, num, den int32, opts ...TimecodeOption) (*Timecode, error) {
112-
r, err := newRate(num, den)
174+
p := newTimecodeOptionParam()
175+
p.applyTimecodeOption(opts...)
176+
177+
r, err := newRate(num, den, p.PreferDF)
113178
if err != nil {
114179
return nil, err
115180
}
116181

117-
p := newTimecodeOptionParam()
118-
p.applyTimecodeOption(opts...)
182+
lastSep := p.LastSep
183+
if r.dropFrames == 0 {
184+
lastSep = p.Sep
185+
}
119186

120-
tc, err := Reset(&Timecode{r: r, optp: p}, frames)
187+
tc, err := Reset(&Timecode{
188+
preferDF: p.PreferDF,
189+
sep: p.Sep,
190+
lastSep: lastSep,
191+
r: r,
192+
}, frames)
121193
if err != nil {
122194
return nil, err
123195
}
124196
return tc, nil
125197
}
126198

199+
// TimecodeOptionParam represents timecode option parameter.
200+
type ParseTimecodeOptionParam struct {
201+
PreferDF bool
202+
Sep string
203+
LastSep string
204+
}
205+
206+
// ParseTimecodeOption represents parse timecode option.
207+
type ParseTimecodeOption func(*ParseTimecodeOptionParam)
208+
209+
// newParseTimecodeOptionParam returns new ParseTimecodeOptionParam.
210+
func newParseTimecodeOptionParam() ParseTimecodeOptionParam {
211+
return ParseTimecodeOptionParam{
212+
PreferDF: true, // if frame rate is 29.97 or 59.94, assume DF. otherwise, assume NDF
213+
}
214+
}
215+
216+
// applyParseTimecodeOption applies ParseTimecodeOption to ParseTimecodeOptionParam.
217+
func (p *ParseTimecodeOptionParam) applyParseTimecodeOption(opts ...ParseTimecodeOption) {
218+
for _, opt := range opts {
219+
opt(p)
220+
}
221+
}
222+
127223
// ParseTimecode returns new Timecode from formatted string.
128-
func ParseTimecode(s string, num, den int32) (*Timecode, error) {
129-
r, err := newRate(num, den)
224+
func ParseTimecode(s string, num, den int32, opts ...ParseTimecodeOption) (*Timecode, error) {
225+
p := newParseTimecodeOptionParam()
226+
p.applyParseTimecodeOption(opts...)
227+
228+
r, err := newRate(num, den, p.PreferDF)
130229
if err != nil {
131230
return nil, err
132231
}
@@ -142,20 +241,25 @@ func ParseTimecode(s string, num, den int32) (*Timecode, error) {
142241
sep := match[2]
143242
mm, _ := strconv.Atoi(match[3])
144243
ss, _ := strconv.Atoi(match[5])
145-
sepDF := match[6]
244+
lastSep := match[6]
146245
ff, _ := strconv.Atoi(match[7])
147246

148247
if ff < r.dropFrames && mm%10 != 0 {
149248
ff = r.dropFrames
150249
}
250+
if r.dropFrames == 0 {
251+
lastSep = sep
252+
}
151253

152254
return &Timecode{
153-
r: r,
154-
optp: TimecodeOptionParam{Sep: sep, SepDF: sepDF},
155-
HH: uint64(hh),
156-
MM: uint64(mm),
157-
SS: uint64(ss),
158-
FF: uint64(ff),
255+
preferDF: p.PreferDF,
256+
sep: sep,
257+
lastSep: lastSep,
258+
r: r,
259+
HH: uint64(hh),
260+
MM: uint64(mm),
261+
SS: uint64(ss),
262+
FF: uint64(ff),
159263
}, nil
160264
}
161265

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

182-
fps := uint64(new.r.fps)
286+
fps := uint64(new.r.roundFPS)
183287
new.FF = f % fps
184288
new.SS = f / fps % 60
185289
new.MM = f / (fps * 60) % 60
@@ -193,7 +297,7 @@ func (r *rate) equal(other *rate) bool {
193297
if r == nil || other == nil {
194298
return false
195299
}
196-
return r.numerator == other.numerator && r.denominator == other.denominator
300+
return r.numerator == other.numerator && r.denominator == other.denominator && r.dropFrames == other.dropFrames
197301
}
198302

199303
// isRepresentableFrames returns whether frames is representable.
@@ -204,12 +308,12 @@ func (r *rate) isRepresentableFrames(frames uint64) bool {
204308
// Frames returns number of frames.
205309
func (tc *Timecode) Frames() uint64 {
206310
var frames uint64
207-
frames += tc.HH * 3600 * uint64(tc.r.fps)
208-
frames += tc.MM * 60 * uint64(tc.r.fps)
209-
frames += tc.SS * uint64(tc.r.fps)
311+
frames += tc.HH * 3600 * uint64(tc.r.roundFPS)
312+
frames += tc.MM * 60 * uint64(tc.r.roundFPS)
313+
frames += tc.SS * uint64(tc.r.roundFPS)
210314
frames += tc.FF
211315

212-
framesPer10Min := uint64(tc.r.fps) * 60 * 10
316+
framesPer10Min := uint64(tc.r.roundFPS) * 60 * 10
213317
framesPer1Min := framesPer10Min / 10
214318

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

222326
// Duration returns duration from zero-origin.
223327
func (tc *Timecode) Duration() time.Duration {
224-
return time.Duration((float64(tc.Frames()) * float64(tc.r.denominator) / float64(tc.r.numerator)) * float64(time.Second))
328+
return time.Duration((float64(tc.Frames()) / float64(tc.r.actualFPS)) * float64(time.Second))
225329
}
226330

227331
// Framerate denominator.
@@ -269,19 +373,14 @@ func (tc *Timecode) SubFrames(frames uint64) (*Timecode, error) {
269373
// String returns Timecode formatted string.
270374
// e.g. 01:23:45:28
271375
func (tc *Timecode) String() string {
272-
sep := tc.optp.Sep
273-
lastSep := sep
274-
if tc.r.dropFrames > 0 {
275-
lastSep = tc.optp.SepDF
276-
}
277376
return fmt.Sprintf(
278377
"%02d%s%02d%s%02d%s%02d",
279378
tc.HH,
280-
sep,
379+
tc.sep,
281380
tc.MM,
282-
sep,
381+
tc.sep,
283382
tc.SS,
284-
lastSep,
383+
tc.lastSep,
285384
tc.FF,
286385
)
287386
}

0 commit comments

Comments
 (0)