• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Eyevinn / hi264 / 28188681282

25 Jun 2026 05:31PM UTC coverage: 87.374% (-0.2%) from 87.601%
28188681282

push

github

tobbee
feat: fractional -fps and NTSC drop-frame timecode

Let -fps express fractional rates and add drop-frame timecode counting.

- hi264gen -fps now accepts an integer (25), a rational (30000/1001), or
  an NTSC decimal (29.97/59.94/23.976, snapped to the exact 1001 fraction).
  For mp4 the media timescale is the numerator and each sample lasts the
  denominator ticks, so fractional rates play at the correct speed
  (ffprobe reports r_frame_rate=30000/1001). Timecodes count at the
  nominal integer rate round(fps); -kbps->bpp uses the exact ratio.
- hi264gen -drop-frame: NTSC drop-frame counting, validated to 29.97/59.94.
  Drops the two (rate 30) / four (rate 60) lowest timecode labels at each
  minute except every tenth, and signals it in the pic_timing SEI via
  counting_type=4 and per-frame cnt_dropped_flag. Applies to both the
  burnt-in -text timecode and the SEI.
- yuv.FormatTextTC: text formatter variant taking the counting rate and
  drop-frame; FormatText is the integer/non-drop wrapper.
- encode.PicTimingConfig.DropFrame / PicTiming.Dropped carry the
  counting_type / cnt_dropped_flag into the SEI.
- Docs: cover pic-timing, start-frame, fractional fps, and drop-frame in the
  curated hi264gen usage header and the CLAUDE.md feature list / CLI examples
  (README and CHANGELOG updated alongside the relevant changes).

Verified: 30000/1001 mp4 timescale=30000 dur=1001; NDF rolls at frame 30;
drop-frame frames 1798..1802 -> 59:28, 59:29, 01:00:02, 01:00:03, 01:00:04
(labels ;00/;01 skipped), ffprobe renders ';'; -drop-frame rejected for 25
fps; ffmpeg decodes both; all 12 hi264gen golden checks still pass.

88 of 99 new or added lines in 3 files covered. (88.89%)

264 existing lines in 5 files now uncovered.

8124 of 9298 relevant lines covered (87.37%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.31
/pkg/encode/api.go
1
package encode
2

3
import (
4
        "bytes"
5
        "fmt"
6

7
        "github.com/Eyevinn/mp4ff/avc"
8

9
        "github.com/Eyevinn/hi264/pkg/yuv"
10
)
11

12
// EncodeParams holds H.264 encoding parameters.
13
type EncodeParams struct {
14
        Width          int            // frame width in pixels (must be even)
15
        Height         int            // frame height in pixels (must be even)
16
        QP             int            // quantization parameter (0-51, default 26)
17
        CABAC          bool           // true=Main profile CABAC, false=Baseline CAVLC
18
        DisableDeblock int            // 0=enable, 1=disable
19
        MaxRefFrames   int            // max_num_ref_frames (0=IDR-only, 1+=P-frames)
20
        ColorSpace     yuv.ColorSpace // YCbCr matrix standard (default BT601)
21
        Range          yuv.Range      // sample value range (default LimitedRange)
22
        FPS            int            // frame rate (used for level selection, 0 = ignore MBPS)
23
        Kbps           int            // bitrate in kbit/s (used for level selection, 0 = ignore bitrate)
24
        // PicStructPresent sets vui pic_struct_present_flag in generated SPS. It
25
        // must be true to attach pic_timing SEI clock timestamps (GeneratePicTimingSEI).
26
        PicStructPresent bool
27
}
28

29
func (p *EncodeParams) validate() error {
1✔
30
        if p.Width <= 0 || p.Width%2 != 0 {
1✔
31
                return fmt.Errorf("width must be positive and even, got %d", p.Width)
×
32
        }
×
33
        if p.Height <= 0 || p.Height%2 != 0 {
1✔
34
                return fmt.Errorf("height must be positive and even, got %d", p.Height)
×
35
        }
×
36
        if p.QP < 0 || p.QP > 51 {
1✔
UNCOV
37
                return fmt.Errorf("QP must be 0-51, got %d", p.QP)
×
UNCOV
38
        }
×
39
        return nil
1✔
40
}
41

42
func (p *EncodeParams) qp() int {
1✔
43
        if p.QP == 0 {
1✔
UNCOV
44
                return 26
×
UNCOV
45
        }
×
46
        return p.QP
1✔
47
}
48

49
// GenerateSPS returns SPS NALU bytes in Annex-B format.
50
func GenerateSPS(p EncodeParams) ([]byte, error) {
1✔
51
        if err := p.validate(); err != nil {
1✔
UNCOV
52
                return nil, err
×
UNCOV
53
        }
×
54
        level := ChooseLevel(p.Width, p.Height, p.FPS, p.Kbps, p.CABAC)
1✔
55
        var rbsp []byte
1✔
56
        if p.CABAC {
2✔
57
                rbsp = EncodeSPSMain(p.Width, p.Height, p.MaxRefFrames, level, p.ColorSpace, p.Range, p.PicStructPresent)
1✔
58
        } else {
2✔
59
                rbsp = EncodeSPS(p.Width, p.Height, p.MaxRefFrames, level, p.ColorSpace, p.Range, p.PicStructPresent)
1✔
60
        }
1✔
61
        var buf bytes.Buffer
1✔
62
        if err := WriteNALU(&buf, 7, 3, rbsp); err != nil {
1✔
UNCOV
63
                return nil, fmt.Errorf("write SPS: %w", err)
×
UNCOV
64
        }
×
65
        return buf.Bytes(), nil
1✔
66
}
67

68
// GeneratePPS returns PPS NALU bytes in Annex-B format.
69
func GeneratePPS(p EncodeParams) ([]byte, error) {
1✔
70
        if err := p.validate(); err != nil {
1✔
UNCOV
71
                return nil, err
×
UNCOV
72
        }
×
73
        var rbsp []byte
1✔
74
        if p.CABAC {
2✔
75
                rbsp = EncodePPSCABAC(p.DisableDeblock)
1✔
76
        } else {
2✔
77
                rbsp = EncodePPS(p.DisableDeblock)
1✔
78
        }
1✔
79
        var buf bytes.Buffer
1✔
80
        if err := WriteNALU(&buf, 8, 3, rbsp); err != nil {
1✔
UNCOV
81
                return nil, fmt.Errorf("write PPS: %w", err)
×
UNCOV
82
        }
×
83
        return buf.Bytes(), nil
1✔
84
}
85

86
// GenerateIDR encodes a flat-color IDR frame from a Grid/ColorMap.
87
// Returns the IDR slice NALU in Annex-B format (no SPS/PPS).
88
func GenerateIDR(p EncodeParams, grid *yuv.Grid, colors yuv.ColorMap, idrPicID uint32) ([]byte, error) {
1✔
89
        if err := p.validate(); err != nil {
1✔
UNCOV
90
                return nil, err
×
UNCOV
91
        }
×
92
        enc := &FrameEncoder{
1✔
93
                Grid:            grid,
1✔
94
                Colors:          colors,
1✔
95
                QP:              p.qp(),
1✔
96
                DisableDeblock:  p.DisableDeblock,
1✔
97
                CABAC:           p.CABAC,
1✔
98
                MaxNumRefFrames: p.MaxRefFrames,
1✔
99
                Width:           p.Width,
1✔
100
                Height:          p.Height,
1✔
101
                ColorSpace:      p.ColorSpace,
1✔
102
                Range:           p.Range,
1✔
103
        }
1✔
104
        return enc.EncodeSlice(idrPicID)
1✔
105
}
106

107
// GenerateIDRWithSPSPPS encodes a single IDR slice that references an
108
// existing SPS and PPS (typically extracted from the bitstream you're
109
// extending). The returned NALU has no fresh SPS/PPS — it is meant to be
110
// muxed into a stream that already declares them. Use this when splicing
111
// a fresh IDR onto an arbitrary upstream encoder's output.
112
//
113
// Constraints inherited from the FrameEncoder I_16x16 DC path: the source
114
// PPS must use entropy_coding_mode_flag matching p.CABAC; chroma_qp_offset
115
// is ignored (only matters for non-zero chroma residuals — fine for flat
116
// colors). Returns the IDR slice NALU in Annex-B format.
117
func GenerateIDRWithSPSPPS(p EncodeParams, sps *avc.SPS, pps *avc.PPS,
118
        plane *yuv.PlaneGrid, idrPicID uint32) ([]byte, error) {
1✔
119
        if err := p.validate(); err != nil {
1✔
120
                return nil, err
×
121
        }
×
122
        if sps.PicOrderCntType != 0 && sps.PicOrderCntType != 2 {
1✔
UNCOV
123
                return nil, fmt.Errorf("GenerateIDRWithSPSPPS: pic_order_cnt_type=%d not supported (only 0 and 2)",
×
UNCOV
124
                        sps.PicOrderCntType)
×
UNCOV
125
        }
×
126
        enc := &FrameEncoder{
1✔
127
                Plane:          plane,
1✔
128
                QP:             p.qp(),
1✔
129
                DisableDeblock: p.DisableDeblock,
1✔
130
                CABAC:          pps.EntropyCodingModeFlag,
1✔
131
                Width:          p.Width,
1✔
132
                Height:         p.Height,
1✔
133
                ColorSpace:     p.ColorSpace,
1✔
134
                Range:          p.Range,
1✔
135
                SPS:            sps,
1✔
136
                PPS:            pps,
1✔
137
        }
1✔
138
        return enc.EncodeSlice(idrPicID)
1✔
139
}
140

141
// GenerateIDRFromPlane encodes an IDR frame from a PlaneGrid.
142
// Returns the IDR slice NALU in Annex-B format (no SPS/PPS).
143
func GenerateIDRFromPlane(p EncodeParams, plane *yuv.PlaneGrid, idrPicID uint32) ([]byte, error) {
1✔
144
        if err := p.validate(); err != nil {
1✔
UNCOV
145
                return nil, err
×
UNCOV
146
        }
×
147
        enc := &FrameEncoder{
1✔
148
                Plane:           plane,
1✔
149
                QP:              p.qp(),
1✔
150
                DisableDeblock:  p.DisableDeblock,
1✔
151
                CABAC:           p.CABAC,
1✔
152
                MaxNumRefFrames: p.MaxRefFrames,
1✔
153
                Width:           p.Width,
1✔
154
                Height:          p.Height,
1✔
155
                ColorSpace:      p.ColorSpace,
1✔
156
                Range:           p.Range,
1✔
157
        }
1✔
158
        return enc.EncodeSlice(idrPicID)
1✔
159
}
160

161
// GeneratePSkip returns a P_Skip slice NALU in Annex-B format.
162
func GeneratePSkip(p EncodeParams, frameNum uint32) ([]byte, error) {
1✔
163
        if err := p.validate(); err != nil {
1✔
UNCOV
164
                return nil, err
×
UNCOV
165
        }
×
166
        sps := &avc.SPS{
1✔
167
                Width:            uint(p.Width),
1✔
168
                Height:           uint(p.Height),
1✔
169
                PicOrderCntType:  0,
1✔
170
                FrameMbsOnlyFlag: true,
1✔
171
        }
1✔
172
        pps := &avc.PPS{
1✔
173
                DeblockingFilterControlPresentFlag: true,
1✔
174
                EntropyCodingModeFlag:              p.CABAC,
1✔
175
                PicInitQpMinus26:                   p.qp() - 26,
1✔
176
        }
1✔
177
        // GeneratePSkip is meant for self-generated streams that start fresh
1✔
178
        // from an IDR (POC reset), so picOrderCntLsb = 2*frame_num is correct.
1✔
179
        return EncodePSkipSlice(sps, pps, frameNum, frameNum*2, p.DisableDeblock)
1✔
180
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc