Compare commits

...

18 Commits

Author SHA1 Message Date
Cédric Verstraeten
77629ac9b8 Merge pull request #231 from kerberos-io/feature/improve-keyframe-interval
feature/improve-keyframe-interval
2026-02-11 12:28:33 +01:00
cedricve
59608394af Use Warning instead of Warn in mp4.go
Replace call to log.Log.Warn with log.Log.Warning in MP4.flushPendingVideoSample to match the logger API. This is a non-functional change that preserves the original message and behavior while using the correct logging method name.
2026-02-11 12:26:18 +01:00
cedricve
9dfcaa466f Refactor video sample flushing logic into a dedicated function 2026-02-11 11:48:15 +01:00
cedricve
88442e4525 Add pending video sample to segment before flush
Before flushing a segment when mp4.Start is true, add any pending VideoFullSample for the current video track to the current fragment. The change computes and updates LastVideoSampleDTS and VideoTotalDuration, adjusts the sample DecodeTime and Dur, calls AddFullSampleToTrack, logs errors, and clears VideoFullSample so the pending sample is included in the segment before starting a new one. This ensures segments contain all frames up to (but not including) the keyframe that triggered the flush.
2026-02-11 11:38:51 +01:00
Cédric Verstraeten
891ae2e5d5 Merge pull request #230 from kerberos-io/feature/improve-video-format
feature/improve-video-format
2026-02-10 17:25:23 +01:00
Cédric Verstraeten
32b471f570 Update machinery/src/video/mp4.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 17:20:40 +01:00
Cédric Verstraeten
5d745fc989 Update machinery/src/video/mp4.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 17:20:29 +01:00
Cédric Verstraeten
edfa6ec4c6 Update machinery/src/video/mp4.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-10 17:20:16 +01:00
Cédric Verstraeten
0c460efea6 Refactor PR description workflow to include organization variable and correct pull request URL format 2026-02-10 16:17:10 +00:00
Cédric Verstraeten
96df049e59 Enhance MP4 initialization by adding max recording duration parameter, improving placeholder size calculation for segments. 2026-02-10 15:59:59 +00:00
Cédric Verstraeten
2cb454e618 Merge branch 'master' into feature/improve-video-format 2026-02-10 16:57:47 +01:00
Cédric Verstraeten
7f2ebb655e Fix sidx.FirstOffset calculation and re-encode init segment for accurate MP4 structure 2026-02-10 15:56:10 +00:00
Cédric Verstraeten
63857fb5cc Merge pull request #229 from kerberos-io/feature/improve-video-format
feature/improve-video-format
2026-02-10 16:53:34 +01:00
Cédric Verstraeten
f4c75f9aa9 Add environment variables for PR number and project name in workflow 2026-02-10 15:31:37 +00:00
Cédric Verstraeten
c3936dc884 Enhance MP4 segment handling by adding segment durations and base decode times, improving fragment management and data integrity 2026-02-10 14:47:47 +00:00
Cédric Verstraeten
2868ddc499 Add fragment duration handling and improve MP4 segment management 2026-02-10 13:52:58 +00:00
Cédric Verstraeten
176610a694 Update mp4.go 2026-02-10 13:39:55 +01:00
Cédric Verstraeten
f60aff4fd6 Enhance MP4 closing process by adding final video and audio samples, ensuring data integrity and updating track metadata 2026-02-10 12:45:46 +01:00
3 changed files with 296 additions and 199 deletions

View File

@@ -2,6 +2,11 @@ name: Autofill PR description
on: pull_request
env:
ORGANIZATION: uugai
PROJECT: ${{ github.event.repository.name }}
PR_NUMBER: ${{ github.event.number }}
jobs:
openai-pr-description:
runs-on: ubuntu-22.04
@@ -16,4 +21,6 @@ jobs:
azure_openai_api_key: ${{ secrets.AZURE_OPENAI_API_KEY }}
azure_openai_endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
azure_openai_version: ${{ secrets.AZURE_OPENAI_VERSION }}
openai_model: ${{ secrets.OPENAI_MODEL }}
pull_request_url: https://pr${{ env.PR_NUMBER }}.api.kerberos.lol
overwrite_description: true

View File

@@ -280,7 +280,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
// Create a video file, and set the dimensions.
mp4Video = video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS)
mp4Video = video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS, configuration.Config.Capture.MaxLengthRecording)
mp4Video.SetWidth(width)
mp4Video.SetHeight(height)
@@ -500,7 +500,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
// Create a video file, and set the dimensions.
mp4Video := video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS)
mp4Video := video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS, configuration.Config.Capture.MaxLengthRecording)
mp4Video.SetWidth(width)
mp4Video.SetHeight(height)

View File

@@ -22,52 +22,73 @@ import (
var LastPTS uint64 = 0 // Last PTS for the current segment
// FragmentDurationMs is the target duration for each fragment in milliseconds.
// Fragments will be flushed at the first keyframe after this duration has elapsed,
// resulting in ~3 second fragments (assuming a typical GOP interval).
const FragmentDurationMs = 3000
type MP4 struct {
// FileName is the name of the file
FileName string
width int
height int
Segments []*mp4ff.MediaSegment // List of media segments
Segment *mp4ff.MediaSegment
MultiTrackFragment *mp4ff.Fragment
TrackIDs []uint32
FileWriter *os.File
Writer *bufio.Writer
SegmentCount int
SampleCount int
StartPTS uint64
VideoTotalDuration uint64
AudioTotalDuration uint64
AudioPTS uint64
Start bool
SPSNALUs [][]byte // SPS NALUs for H264
PPSNALUs [][]byte // PPS NALUs for H264
VPSNALUs [][]byte // VPS NALUs for H264
FreeBoxSize int64
MoofBoxes int64 // Number of moof boxes in the file
MoofBoxSizes []int64 // Sizes of each moof box
StartTime uint64 // Start time of the MP4 file
VideoTrackName string // Name of the video track
VideoTrack int // Track ID for the video track
AudioTrackName string // Name of the audio track
AudioTrack int // Track ID for the audio track
VideoFullSample *mp4ff.FullSample // Full sample for video track
AudioFullSample *mp4ff.FullSample // Full sample for audio track
LastAudioSampleDTS uint64 // Last PTS for audio sample
LastVideoSampleDTS uint64 // Last PTS for video sample
SampleType string // Type of the sample (e.g., "video", "audio", "subtitle")
FileName string
width int
height int
Segments []*mp4ff.MediaSegment // List of media segments
Segment *mp4ff.MediaSegment
MultiTrackFragment *mp4ff.Fragment
TrackIDs []uint32
FileWriter *os.File
Writer *bufio.Writer
SegmentCount int
SampleCount int
StartPTS uint64
VideoTotalDuration uint64
AudioTotalDuration uint64
AudioPTS uint64
Start bool
SPSNALUs [][]byte // SPS NALUs for H264
PPSNALUs [][]byte // PPS NALUs for H264
VPSNALUs [][]byte // VPS NALUs for H264
FreeBoxSize int64
FragmentStartRawPTS uint64 // Raw PTS for timing when to flush fragments
FragmentStartDTS uint64 // Accumulated VideoTotalDuration at fragment start (matches tfdt)
MoofBoxes int64 // Number of moof boxes in the file
MoofBoxSizes []int64 // Sizes of each moof box
SegmentDurations []uint64 // Duration of each segment in timescale units
SegmentBaseDecTimes []uint64 // Base decode time of each segment
StartTime uint64 // Start time of the MP4 file
VideoTrackName string // Name of the video track
VideoTrack int // Track ID for the video track
AudioTrackName string // Name of the audio track
AudioTrack int // Track ID for the audio track
VideoFullSample *mp4ff.FullSample // Full sample for video track
AudioFullSample *mp4ff.FullSample // Full sample for audio track
LastAudioSampleDTS uint64 // Last PTS for audio sample
LastVideoSampleDTS uint64 // Last PTS for video sample
SampleType string // Type of the sample (e.g., "video", "audio", "subtitle")
}
// NewMP4 creates a new MP4 object
func NewMP4(fileName string, spsNALUs [][]byte, ppsNALUs [][]byte, vpsNALUs [][]byte) *MP4 {
// NewMP4 creates a new MP4 object.
// maxDurationSec is the maximum expected recording duration in seconds,
// used to calculate the free-box placeholder size for ftyp+moov+sidx.
func NewMP4(fileName string, spsNALUs [][]byte, ppsNALUs [][]byte, vpsNALUs [][]byte, maxDurationSec int64) *MP4 {
init := mp4ff.NewMP4Init()
// Add a free box to the init segment
// Prepend a free box to the init segment with a size of 4096 bytes, so we can overwrite it later with the actual init segment.
freeBoxSize := 4096
// Calculate the placeholder size needed at the start of the file.
// Components:
// ftyp: ~32 bytes
// moov: ~1500 bytes (mvhd + mvex + video trak + audio trak + UUID)
// sidx: 24 bytes fixed + 12 bytes per segment reference
// Segments are ~FragmentDurationMs each, so:
// numSegments = ceil(maxDurationSec * 1000 / FragmentDurationMs) + 1 (safety margin)
// sidxSize = 24 + 12 * numSegments
baseSize := int64(2560) // ftyp + moov + extra headroom for large UUID signatures
numSegments := int64(0)
if maxDurationSec > 0 {
// Use integer ceiling division to avoid underestimating the number of segments.
numSegments = ((maxDurationSec*1000)+FragmentDurationMs-1)/FragmentDurationMs + 1
}
sidxSize := int64(24 + 12*numSegments)
freeBoxSize := int(baseSize + sidxSize)
free := mp4ff.NewFreeBox(make([]byte, freeBoxSize))
init.AddChild(free)
// Create a writer
ofd, err := os.Create(fileName)
@@ -77,16 +98,15 @@ func NewMP4(fileName string, spsNALUs [][]byte, ppsNALUs [][]byte, vpsNALUs [][]
// Create a buffered writer
bufferedWriter := bufio.NewWriterSize(ofd, 64*1024) // 64KB buffer
// We will write the empty init segment to the file
// so we can overwrite it later with the actual init segment.
err = init.Encode(bufferedWriter)
// Write the free box placeholder at the start of the file
err = free.Encode(bufferedWriter)
if err != nil {
}
return &MP4{
FileName: fileName,
StartTime: uint64(time.Now().Unix()),
FreeBoxSize: int64(freeBoxSize),
FreeBoxSize: int64(freeBoxSize) + 8, // payload + 8 byte box header
FileWriter: ofd,
Writer: bufferedWriter,
SPSNALUs: spsNALUs,
@@ -130,42 +150,105 @@ func (mp4 *MP4) AddAudioTrack(codec string) uint32 {
func (mp4 *MP4) AddMediaSegment(segNr int) {
}
// flushPendingVideoSample writes the pending video sample to the current fragment.
// If nextPTS is provided (non-zero), it calculates duration from the PTS difference.
// If nextPTS is 0 (e.g., at Close time), it uses the last known duration.
// Returns true if a sample was flushed, false if there was no pending sample.
func (mp4 *MP4) flushPendingVideoSample(nextPTS uint64) bool {
if mp4.VideoFullSample == nil || mp4.MultiTrackFragment == nil {
return false
}
var duration uint64
if nextPTS > 0 && nextPTS > mp4.VideoFullSample.DecodeTime {
duration = nextPTS - mp4.VideoFullSample.DecodeTime
} else {
// No valid nextPTS (Close case) or PTS went backwards (jitter/discontinuity)
if nextPTS > 0 {
log.Log.Warning(fmt.Sprintf("mp4.flushPendingVideoSample(): video PTS went backwards or zero duration (nextPTS=%d, prevDTS=%d), using last known duration", nextPTS, mp4.VideoFullSample.DecodeTime))
}
duration = mp4.LastVideoSampleDTS
if duration == 0 {
duration = 33 // Default ~30fps frame duration
}
}
mp4.LastVideoSampleDTS = duration
mp4.VideoTotalDuration += duration
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
mp4.VideoFullSample.Sample.Dur = uint32(duration)
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, uint32(mp4.VideoTrack))
if err != nil {
log.Log.Error("mp4.flushPendingVideoSample(): error adding sample: " + err.Error())
}
mp4.VideoFullSample = nil
return true
}
func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, pts uint64) error {
if isKeyframe {
// Write the segment to the file
// Determine whether to start a new fragment.
// We only flush at a keyframe boundary once at least FragmentDurationMs
// of content has been accumulated, resulting in ~3 second fragments.
elapsed := uint64(0)
if mp4.Start {
mp4.MoofBoxes = mp4.MoofBoxes + 1
mp4.MoofBoxSizes = append(mp4.MoofBoxSizes, int64(mp4.Segment.Size()))
err := mp4.Segment.Encode(mp4.Writer)
if err != nil {
log.Log.Error("mp4.AddSampleToTrack(): error encoding segment: " + err.Error())
elapsed = pts - mp4.FragmentStartRawPTS
}
shouldFlush := !mp4.Start || elapsed >= FragmentDurationMs
if shouldFlush {
// Write the previous segment to the file
if mp4.Start {
// IMPORTANT: Add any pending video sample to the current segment BEFORE flushing.
// This ensures the segment contains all frames up to (but not including) this keyframe,
// and the new segment will start cleanly with this keyframe.
if trackID == uint32(mp4.VideoTrack) {
mp4.flushPendingVideoSample(pts)
}
mp4.MoofBoxes = mp4.MoofBoxes + 1
mp4.MoofBoxSizes = append(mp4.MoofBoxSizes, int64(mp4.Segment.Size()))
// Track the segment's duration and base decode time for sidx.
// Use accumulated VideoTotalDuration which matches the tfdt values
// in the trun boxes, NOT raw PTS from the camera.
segDuration := mp4.VideoTotalDuration - mp4.FragmentStartDTS
mp4.SegmentDurations = append(mp4.SegmentDurations, segDuration)
mp4.SegmentBaseDecTimes = append(mp4.SegmentBaseDecTimes, mp4.FragmentStartDTS)
err := mp4.Segment.Encode(mp4.Writer)
if err != nil {
log.Log.Error("mp4.AddSampleToTrack(): error encoding segment: " + err.Error())
}
mp4.Segments = append(mp4.Segments, mp4.Segment)
}
mp4.Segments = append(mp4.Segments, mp4.Segment)
mp4.Start = true
// Increment the segment count
mp4.SegmentCount = mp4.SegmentCount + 1
// Create a new media segment
seg := mp4ff.NewMediaSegment()
// Create a video fragment
multiTrackFragment, err := mp4ff.CreateMultiTrackFragment(uint32(mp4.SegmentCount), mp4.TrackIDs)
if err != nil {
log.Log.Error("mp4.AddSampleToTrack(): error creating multi track fragment: " + err.Error())
}
mp4.MultiTrackFragment = multiTrackFragment
seg.AddFragment(multiTrackFragment)
// Set to MP4 struct
mp4.Segment = seg
// Set the start PTS for the next segment
mp4.StartPTS = pts
mp4.FragmentStartRawPTS = pts
mp4.FragmentStartDTS = mp4.VideoTotalDuration
}
mp4.Start = true
// Increment the segment count
mp4.SegmentCount = mp4.SegmentCount + 1
// Create a new media segment
seg := mp4ff.NewMediaSegment()
// Create a video fragment
multiTrackFragment, err := mp4ff.CreateMultiTrackFragment(uint32(mp4.SegmentCount), mp4.TrackIDs) // Assuming 1 for video track and 2 for audio track
if err != nil {
log.Log.Error("mp4.AddSampleToTrack(): error creating multi track fragment: " + err.Error())
}
mp4.MultiTrackFragment = multiTrackFragment
seg.AddFragment(multiTrackFragment)
// Set to MP4 struct
mp4.Segment = seg
// Set the start PTS for the next segment
mp4.StartPTS = pts
}
if mp4.Start {
@@ -182,18 +265,10 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
}
if err == nil {
// Flush previous pending sample before storing the new one
if mp4.VideoFullSample != nil {
duration := pts - mp4.VideoFullSample.DecodeTime
log.Log.Debug("Adding sample to track " + fmt.Sprintf("%d, PTS: %d, Duration: %d, size: %d, Keyframe: %t", trackID, pts, duration, len(lengthPrefixed), isKeyframe))
mp4.LastVideoSampleDTS = duration
mp4.VideoTotalDuration += duration
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
mp4.VideoFullSample.Sample.Dur = uint32(duration)
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, trackID)
if err != nil {
log.Log.Error("mp4.AddSampleToTrack(): error adding sample to track " + fmt.Sprintf("%d: %v", trackID, err))
}
log.Log.Debug("Adding sample to track " + fmt.Sprintf("%d, PTS: %d, size: %d, Keyframe: %t", trackID, pts, len(lengthPrefixed), isKeyframe))
mp4.flushPendingVideoSample(pts)
}
// Set the sample data
@@ -263,8 +338,47 @@ func (mp4 *MP4) Close(config *models.Config) {
log.Log.Error("mp4.Close(): no video or audio samples added, cannot create MP4 file")
}
// Add final pending samples before closing
if mp4.Segment != nil {
// Add final video sample if pending (pass 0 as nextPTS to use last known duration)
mp4.flushPendingVideoSample(0)
// Add final audio sample if pending
if mp4.AudioFullSample != nil && mp4.AudioTrack > 0 {
SplitAACFrame(mp4.AudioFullSample.Data, func(started bool, aac []byte) {
sampleToAdd := *mp4.AudioFullSample
dts := mp4.LastAudioSampleDTS
if dts == 0 {
dts = 1024 // Default AAC frame duration
}
mp4.AudioTotalDuration += dts
mp4.AudioPTS += dts
sampleToAdd.Data = aac[7:]
sampleToAdd.DecodeTime = mp4.AudioPTS - dts
sampleToAdd.Sample.Dur = uint32(dts)
sampleToAdd.Sample.Size = uint32(len(aac[7:]))
err := mp4.MultiTrackFragment.AddFullSampleToTrack(sampleToAdd, uint32(mp4.AudioTrack))
if err != nil {
log.Log.Error("mp4.Close(): error adding final audio sample: " + err.Error())
}
})
mp4.AudioFullSample = nil
}
}
// Encode the last segment
if mp4.Segment != nil {
// Track the last segment's size, duration and base decode time.
// Use accumulated VideoTotalDuration which matches tfdt values.
mp4.MoofBoxes = mp4.MoofBoxes + 1
mp4.MoofBoxSizes = append(mp4.MoofBoxSizes, int64(mp4.Segment.Size()))
lastSegDuration := mp4.VideoTotalDuration - mp4.FragmentStartDTS
if lastSegDuration == 0 {
lastSegDuration = mp4.LastVideoSampleDTS
}
mp4.SegmentDurations = append(mp4.SegmentDurations, lastSegDuration)
mp4.SegmentBaseDecTimes = append(mp4.SegmentBaseDecTimes, mp4.FragmentStartDTS)
err := mp4.Segment.Encode(mp4.Writer)
if err != nil {
log.Log.Error("mp4.Close(): error encoding last segment: " + err.Error())
@@ -272,10 +386,14 @@ func (mp4 *MP4) Close(config *models.Config) {
}
mp4.Writer.Flush()
defer mp4.FileWriter.Close()
// Ensure all segment data is on disk before we overwrite the placeholder at offset 0.
if err := mp4.FileWriter.Sync(); err != nil {
log.Log.Error("mp4.Close(): error syncing file: " + err.Error())
}
// Now we have all the moof and mdat boxes written to the file.
// We can now generate the ftyp and moov boxes, and replace it with the free box we added earlier (size of 2048 bytes).
// We build the ftyp + moov init segment and write it at the start,
// overwriting the free box placeholder we reserved in NewMP4.
init := mp4ff.NewMP4Init()
// Create a new ftyp box
@@ -316,8 +434,10 @@ func (mp4 *MP4) Close(config *models.Config) {
if err != nil {
}
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
init.Moov.Traks[0].Tkhd.Width = mp4ff.Fixed32(uint32(mp4.width) << 16)
init.Moov.Traks[0].Tkhd.Height = mp4ff.Fixed32(uint32(mp4.height) << 16)
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
//init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
case "H265", "HVC1":
init.AddEmptyTrack(videoTimescale, "video", "und")
includePS := true
@@ -325,8 +445,10 @@ func (mp4 *MP4) Close(config *models.Config) {
if err != nil {
}
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
init.Moov.Traks[0].Tkhd.Width = mp4ff.Fixed32(uint32(mp4.width) << 16)
init.Moov.Traks[0].Tkhd.Height = mp4ff.Fixed32(uint32(mp4.height) << 16)
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
//init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
}
// Try adding audio track if available
@@ -345,7 +467,7 @@ func (mp4 *MP4) Close(config *models.Config) {
}
init.Moov.Traks[1].Tkhd.Duration = mp4.AudioTotalDuration
init.Moov.Traks[1].Mdia.Hdlr.Name = "agent " + utils.VERSION
//init.Moov.Traks[1].Mdia.Mdhd.Duration = mp4.AudioTotalDuration
init.Moov.Traks[1].Mdia.Mdhd.Duration = mp4.AudioTotalDuration
}
// Try adding subtitle track if available
@@ -423,126 +545,94 @@ func (mp4 *MP4) Close(config *models.Config) {
}
}
// We will also calculate the SIDX box, which is a segment index box that contains information about the segments in the file.
// This is useful for seeking in the file, and for streaming the file.
/*sidx := &mp4ff.SidxBox{
Version: 0,
Flags: 0,
ReferenceID: 0,
Timescale: videoTimescale,
EarliestPresentationTime: 0,
FirstOffset: 0,
SidxRefs: make([]mp4ff.SidxRef, 0),
}
referenceTrak := init.Moov.Trak
trex, ok := init.Moov.Mvex.GetTrex(referenceTrak.Tkhd.TrackID)
if !ok {
// We have an issue.
// Build a Segment Index (sidx) box so players can seek directly to any
// fragment without scanning the entire file.
if len(mp4.SegmentDurations) > 0 {
sidx := &mp4ff.SidxBox{
Version: 1,
Flags: 0,
ReferenceID: uint32(mp4.VideoTrack),
Timescale: videoTimescale,
EarliestPresentationTime: 0,
FirstOffset: 0,
SidxRefs: make([]mp4ff.SidxRef, 0, len(mp4.SegmentDurations)),
}
for i, dur := range mp4.SegmentDurations {
sidx.SidxRefs = append(sidx.SidxRefs, mp4ff.SidxRef{
ReferenceType: 0, // media reference
ReferencedSize: uint32(mp4.MoofBoxSizes[i]),
SubSegmentDuration: uint32(dur),
StartsWithSAP: 1,
SAPType: 1,
})
}
init.AddChild(sidx)
}
segDatas, err := findSegmentData(mp4.Segments, referenceTrak, trex)
if err != nil {
// We have an issue.
}
fillSidx(sidx, referenceTrak, segDatas, true)
// Add the SIDX box to the moov box
init.AddChild(sidx)*/
// Get a bit slice writer for the init segment
// Get a byte buffer of FreeBoxSize bytes to write the init segment
buffer := bytes.NewBuffer(make([]byte, 0))
init.Encode(buffer)
// The first FreeBoxSize bytes of the file is a free box, so we can read it and replace it with the moov box.
// The init box might not be FreeBoxSize bytes, so we need to read the first FreeBoxSize bytes and then replace it with the moov box.
// while the remaining bytes are for a new free box.
// Write the init segment at the beginning of the file, replacing the free box
if _, err := mp4.FileWriter.WriteAt(buffer.Bytes(), 0); err != nil {
// Encode the ftyp + moov + sidx into a buffer to measure the total size.
// Then compute the correct sidx.FirstOffset (the gap between the end of
// the sidx box and the first moof, occupied by the trailing free box)
// and re-encode with the corrected value.
var initBuf bytes.Buffer
if err := init.Encode(&initBuf); err != nil {
log.Log.Error("mp4.Close(): error encoding init segment: " + err.Error())
}
// Calculate the remaining size for the free box
remainingSize := mp4.FreeBoxSize - int64(buffer.Len())
if remainingSize > 0 {
newFreeBox := mp4ff.NewFreeBox(make([]byte, remainingSize))
initSize := int64(initBuf.Len())
// The sidx.FirstOffset is defined as the distance (in bytes) from the
// anchor point (first byte after the sidx box) to the first byte of
// the first referenced moof/mdat. Since sidx is the last box in init,
// the anchor point is at initSize, and the first moof is at FreeBoxSize.
if len(mp4.SegmentDurations) > 0 {
if mp4.FreeBoxSize < initSize {
// Avoid computing a negative offset and wrapping it to uint64.
log.Log.Error("mp4.Close(): FreeBoxSize is smaller than initSize; skipping sidx FirstOffset adjustment")
} else {
firstOffset := uint64(mp4.FreeBoxSize - initSize)
// Find the sidx we added and update its FirstOffset
for _, child := range init.Children {
if sidxBox, ok := child.(*mp4ff.SidxBox); ok {
sidxBox.FirstOffset = firstOffset
break
}
}
// Re-encode with the corrected FirstOffset (same size, no layout change)
initBuf.Reset()
if err := init.Encode(&initBuf); err != nil {
log.Log.Error("mp4.Close(): error re-encoding init segment: " + err.Error())
}
initSize = int64(initBuf.Len())
}
}
if initSize > mp4.FreeBoxSize {
log.Log.Error(fmt.Sprintf("mp4.Close(): init segment (%d bytes) exceeds reserved space (%d bytes), file may be corrupt", initSize, mp4.FreeBoxSize))
}
// Write the init segment at the beginning of the file, overwriting the free box placeholder.
if _, err := mp4.FileWriter.WriteAt(initBuf.Bytes(), 0); err != nil {
log.Log.Error("mp4.Close(): error writing init segment: " + err.Error())
}
// Fill any remaining reserved space with a new (smaller) free box so
// the byte offsets of the moof/mdat boxes that follow are preserved.
remainingSize := mp4.FreeBoxSize - initSize
if remainingSize >= 8 { // minimum box size is 8 bytes (header only)
newFree := mp4ff.NewFreeBox(make([]byte, remainingSize-8))
var freeBuf bytes.Buffer
if err := newFreeBox.Encode(&freeBuf); err != nil {
if err := newFree.Encode(&freeBuf); err != nil {
log.Log.Error("mp4.Close(): error encoding free box: " + err.Error())
}
if _, err := mp4.FileWriter.WriteAt(freeBuf.Bytes(), int64(buffer.Len())); err != nil {
if _, err := mp4.FileWriter.WriteAt(freeBuf.Bytes(), initSize); err != nil {
log.Log.Error("mp4.Close(): error writing free box: " + err.Error())
}
}
}
type segData struct {
startPos uint64
presentationTime uint64
baseDecodeTime uint64
dur uint32
size uint32
}
func fillSidx(sidx *mp4ff.SidxBox, refTrak *mp4ff.TrakBox, segDatas []segData, nonZeroEPT bool) {
ept := uint64(0)
if nonZeroEPT {
ept = segDatas[0].presentationTime
if err := mp4.FileWriter.Sync(); err != nil {
log.Log.Error("mp4.Close(): error syncing file: " + err.Error())
}
sidx.Version = 1
sidx.Timescale = refTrak.Mdia.Mdhd.Timescale
sidx.ReferenceID = 1
sidx.EarliestPresentationTime = ept
sidx.FirstOffset = 0
sidx.SidxRefs = make([]mp4ff.SidxRef, 0, len(segDatas))
for _, segData := range segDatas {
size := segData.size
sidx.SidxRefs = append(sidx.SidxRefs, mp4ff.SidxRef{
ReferencedSize: size,
SubSegmentDuration: segData.dur,
StartsWithSAP: 1,
SAPType: 1,
})
}
}
// findSegmentData returns a slice of segment media data using a reference track.
func findSegmentData(segs []*mp4ff.MediaSegment, refTrak *mp4ff.TrakBox, trex *mp4ff.TrexBox) ([]segData, error) {
segDatas := make([]segData, 0, len(segs))
for _, seg := range segs {
var firstCompositionTimeOffest int64
dur := uint32(0)
var baseTime uint64
for fIdx, frag := range seg.Fragments {
for _, traf := range frag.Moof.Trafs {
tfhd := traf.Tfhd
if tfhd.TrackID == refTrak.Tkhd.TrackID { // Find track that gives sidx time values
if fIdx == 0 {
baseTime = traf.Tfdt.BaseMediaDecodeTime()
}
for i, trun := range traf.Truns {
trun.AddSampleDefaultValues(tfhd, trex)
samples := trun.GetSamples()
for j, sample := range samples {
if fIdx == 0 && i == 0 && j == 0 {
firstCompositionTimeOffest = int64(sample.CompositionTimeOffset)
}
dur += sample.Dur
}
}
}
}
}
sd := segData{
startPos: seg.StartPos,
presentationTime: uint64(int64(baseTime) + firstCompositionTimeOffest),
baseDecodeTime: baseTime,
dur: dur,
size: uint32(seg.Size()),
}
segDatas = append(segDatas, sd)
}
return segDatas, nil
mp4.FileWriter.Close()
}
// annexBToLengthPrefixed converts Annex B formatted H264 data (with start codes)