mirror of
https://github.com/kerberos-io/agent.git
synced 2026-03-03 05:50:03 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c4ee518d | ||
|
|
51b9d76973 | ||
|
|
f3c1cb9b82 | ||
|
|
a1368361e4 | ||
|
|
abfdea0179 | ||
|
|
8aaeb62fa3 | ||
|
|
e30dd7d4a0 | ||
|
|
ac3f9aa4e8 | ||
|
|
04c568f488 | ||
|
|
e270223968 | ||
|
|
01ab1a9218 | ||
|
|
6f0794b09c | ||
|
|
1ae6a46d88 | ||
|
|
9d83cab5cc | ||
|
|
6f559c2f00 | ||
|
|
c147944f5a | ||
|
|
e8ca776e4e | ||
|
|
de5c4b6e0a | ||
|
|
9ba64de090 | ||
|
|
7ceeebe76e | ||
|
|
bd7dbcfcf2 |
58
.github/workflows/docker-dev.yml
vendored
58
.github/workflows/docker-dev.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Docker development build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [amd64]
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Run Buildx
|
||||
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
- name: Create new and append to manifest
|
||||
run: docker buildx imagetools create -t kerberos/agent-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
- name: Create new and append to latest manifest
|
||||
run: docker buildx imagetools create -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
build-other:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
#architecture: [arm64, arm/v7, arm/v6]
|
||||
architecture: [arm64, arm/v7]
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Run Buildx
|
||||
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
- name: Create new and append to manifest
|
||||
run: docker buildx imagetools create --append -t kerberos/agent-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
- name: Create new and append to manifest latest
|
||||
run: docker buildx imagetools create --append -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
51
.github/workflows/issue-userstory-create.yml
vendored
Normal file
51
.github/workflows/issue-userstory-create.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Create User Story Issue
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_title:
|
||||
description: 'Title for the issue'
|
||||
required: true
|
||||
issue_description:
|
||||
description: 'Brief description of the feature'
|
||||
required: true
|
||||
complexity:
|
||||
description: 'Complexity of the feature'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- 'Low'
|
||||
- 'Medium'
|
||||
- 'High'
|
||||
default: 'Medium'
|
||||
duration:
|
||||
description: 'Estimated duration'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- '1 day'
|
||||
- '3 days'
|
||||
- '1 week'
|
||||
- '2 weeks'
|
||||
- '1 month'
|
||||
default: '1 week'
|
||||
|
||||
jobs:
|
||||
create-issue:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Create Issue with User Story
|
||||
uses: cedricve/llm-create-issue-user-story@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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 }}
|
||||
issue_title: ${{ github.event.inputs.issue_title }}
|
||||
issue_description: ${{ github.event.inputs.issue_description }}
|
||||
complexity: ${{ github.event.inputs.complexity }}
|
||||
duration: ${{ github.event.inputs.duration }}
|
||||
labels: 'user-story,feature'
|
||||
assignees: ${{ github.actor }}
|
||||
@@ -1,12 +1,14 @@
|
||||
name: Docker nightly build
|
||||
name: Nightly build
|
||||
|
||||
on:
|
||||
# Triggers the workflow every day at 9PM (CET).
|
||||
schedule:
|
||||
- cron: "0 22 * * *"
|
||||
# Allows manual triggering from the Actions tab.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
nightly-build-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -18,7 +20,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Checkout
|
||||
run: git clone https://github.com/kerberos-io/agent && cd agent
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
@@ -26,10 +30,10 @@ jobs:
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Run Buildx
|
||||
run: cd agent && docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
- name: Create new and append to manifest
|
||||
run: cd agent && docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
build-other:
|
||||
run: docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
nightly-build-other:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -41,7 +45,9 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Checkout
|
||||
run: git clone https://github.com/kerberos-io/agent && cd agent
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
@@ -49,6 +55,6 @@ jobs:
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Run Buildx
|
||||
run: cd agent && docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
- name: Create new and append to manifest
|
||||
run: cd agent && docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
run: docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
4
.github/workflows/release-create.yml
vendored
4
.github/workflows/release-create.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
length: 7
|
||||
- name: Run Build
|
||||
run: |
|
||||
docker build -t ${{matrix.architecture}} .
|
||||
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
|
||||
CID=$(docker create ${{matrix.architecture}})
|
||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||
docker rm ${CID}
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
length: 7
|
||||
- name: Run Build
|
||||
run: |
|
||||
docker build -t ${{matrix.architecture}} -f Dockerfile.arm64 .
|
||||
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} -f Dockerfile.arm64 .
|
||||
CID=$(docker create ${{matrix.architecture}})
|
||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||
docker rm ${CID}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,4 +14,5 @@ machinery/test*
|
||||
machinery/init-dev.sh
|
||||
machinery/.env.local
|
||||
machinery/vendor
|
||||
deployments/docker/private-docker-compose.yaml
|
||||
deployments/docker/private-docker-compose.yaml
|
||||
video.mp4
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
ARG BASE_IMAGE_VERSION=amd64-ddbe40e
|
||||
ARG VERSION=0.0.0
|
||||
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
|
||||
LABEL AUTHOR=uug.ai
|
||||
|
||||
@@ -34,7 +35,8 @@ RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
|
||||
|
||||
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
|
||||
go mod download && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
|
||||
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "${VERSION}") && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION} -extldflags '-static -latomic'" main.go && \
|
||||
mkdir -p /agent && \
|
||||
mv main /agent && \
|
||||
mv version /agent && \
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
ARG BASE_IMAGE_VERSION=arm64-ddbe40e
|
||||
ARG VERSION=0.0.0
|
||||
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
|
||||
LABEL AUTHOR=uug.ai
|
||||
|
||||
@@ -34,7 +35,8 @@ RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
|
||||
|
||||
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
|
||||
go mod download && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
|
||||
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "${VERSION}") && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION} -extldflags '-static -latomic'" main.go && \
|
||||
mkdir -p /agent && \
|
||||
mv main /agent && \
|
||||
mv version /agent && \
|
||||
|
||||
@@ -637,10 +637,13 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
|
||||
// Frame is complete — update per-stream FPS from PTS.
|
||||
if okPTS {
|
||||
if ft := g.fpsTrackers[g.VideoH264Index]; ft != nil {
|
||||
if ptsFPS := ft.update(pts); ptsFPS > 0 && ptsFPS <= 120 {
|
||||
g.Streams[g.VideoH264Index].FPS = ptsFPS
|
||||
}
|
||||
ft := g.fpsTrackers[g.VideoH264Index]
|
||||
if ft == nil {
|
||||
ft = newFPSTracker(30)
|
||||
g.fpsTrackers[g.VideoH264Index] = ft
|
||||
}
|
||||
if ptsFPS := ft.update(pts); ptsFPS > 0 && ptsFPS <= 120 {
|
||||
g.Streams[g.VideoH264Index].FPS = ptsFPS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,8 +657,10 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
nonIDRPresent := false
|
||||
idrPresent := false
|
||||
|
||||
var naluTypes []string
|
||||
for _, nalu := range au {
|
||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||
naluTypes = append(naluTypes, fmt.Sprintf("%s(%d,sz=%d)", typ.String(), int(typ), len(nalu)))
|
||||
switch typ {
|
||||
case h264.NALUTypeAccessUnitDelimiter:
|
||||
continue
|
||||
@@ -690,18 +695,46 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
g.Streams[g.VideoH264Index].FPS = fps
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.Start(%s): Final FPS=%.2f", streamType, fps))
|
||||
g.VideoH264Forma.SPS = nalu
|
||||
if streamType == "main" && len(nalu) > 0 {
|
||||
// Fallback: store SPS from in-band NALUs when SDP was missing it.
|
||||
configuration.Config.Capture.IPCamera.SPSNALUs = [][]byte{nalu}
|
||||
}
|
||||
|
||||
}
|
||||
case h264.NALUTypePPS:
|
||||
g.VideoH264Forma.PPS = nalu
|
||||
if streamType == "main" && len(nalu) > 0 {
|
||||
// Fallback: store PPS from in-band NALUs when SDP was missing it.
|
||||
configuration.Config.Capture.IPCamera.PPSNALUs = [][]byte{nalu}
|
||||
}
|
||||
}
|
||||
filteredAU = append(filteredAU, nalu)
|
||||
}
|
||||
|
||||
if idrPresent && streamType == "main" {
|
||||
// Ensure config has parameter sets before recordings start.
|
||||
if len(configuration.Config.Capture.IPCamera.SPSNALUs) == 0 && len(g.VideoH264Forma.SPS) > 0 {
|
||||
configuration.Config.Capture.IPCamera.SPSNALUs = [][]byte{g.VideoH264Forma.SPS}
|
||||
log.Log.Warning("capture.golibrtsp.Start(main): fallback SPS set from keyframe")
|
||||
}
|
||||
if len(configuration.Config.Capture.IPCamera.PPSNALUs) == 0 && len(g.VideoH264Forma.PPS) > 0 {
|
||||
configuration.Config.Capture.IPCamera.PPSNALUs = [][]byte{g.VideoH264Forma.PPS}
|
||||
log.Log.Warning("capture.golibrtsp.Start(main): fallback PPS set from keyframe")
|
||||
}
|
||||
if len(configuration.Config.Capture.IPCamera.SPSNALUs) == 0 || len(configuration.Config.Capture.IPCamera.PPSNALUs) == 0 {
|
||||
log.Log.Warning("capture.golibrtsp.Start(main): SPS/PPS still missing after IDR keyframe")
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredAU) <= 1 || (!nonIDRPresent && !idrPresent) {
|
||||
return
|
||||
}
|
||||
|
||||
if idrPresent {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.Start(%s): IDR frame NALUs: [%s]",
|
||||
streamType, fmt.Sprintf("%v", naluTypes)))
|
||||
}
|
||||
|
||||
// Convert to packet.
|
||||
enc, err := h264.AnnexBMarshal(filteredAU)
|
||||
if err != nil {
|
||||
@@ -817,10 +850,13 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
|
||||
// Frame is complete — update per-stream FPS from PTS.
|
||||
if okPTS {
|
||||
if ft := g.fpsTrackers[g.VideoH265Index]; ft != nil {
|
||||
if ptsFPS := ft.update(pts); ptsFPS > 0 && ptsFPS <= 120 {
|
||||
g.Streams[g.VideoH265Index].FPS = ptsFPS
|
||||
}
|
||||
ft := g.fpsTrackers[g.VideoH265Index]
|
||||
if ft == nil {
|
||||
ft = newFPSTracker(30)
|
||||
g.fpsTrackers[g.VideoH265Index] = ft
|
||||
}
|
||||
if ptsFPS := ft.update(pts); ptsFPS > 0 && ptsFPS <= 120 {
|
||||
g.Streams[g.VideoH265Index].FPS = ptsFPS
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -279,6 +279,9 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
ppsNALUS := configuration.Config.Capture.IPCamera.PPSNALUs
|
||||
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
|
||||
|
||||
if len(spsNALUS) == 0 || len(ppsNALUS) == 0 {
|
||||
log.Log.Warning("capture.main.HandleRecordStream(continuous): missing SPS/PPS at recording start")
|
||||
}
|
||||
// Create a video file, and set the dimensions.
|
||||
mp4Video = video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS, configuration.Config.Capture.MaxLengthRecording)
|
||||
mp4Video.SetWidth(width)
|
||||
@@ -499,6 +502,9 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
ppsNALUS := configuration.Config.Capture.IPCamera.PPSNALUs
|
||||
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
|
||||
|
||||
if len(spsNALUS) == 0 || len(ppsNALUS) == 0 {
|
||||
log.Log.Warning("capture.main.HandleRecordStream(motiondetection): missing SPS/PPS at recording start")
|
||||
}
|
||||
// Create a video file, and set the dimensions.
|
||||
mp4Video := video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS, configuration.Config.Capture.MaxLengthRecording)
|
||||
mp4Video.SetWidth(width)
|
||||
|
||||
@@ -25,7 +25,10 @@ import (
|
||||
"github.com/nfnt/resize"
|
||||
)
|
||||
|
||||
const VERSION = "3.5.0"
|
||||
// VERSION is the agent version. It defaults to "0.0.0" for local dev builds
|
||||
// and is overridden at build time via:
|
||||
// go build -ldflags "-X github.com/kerberos-io/agent/machinery/src/utils.VERSION=v1.2.3"
|
||||
var VERSION = "0.0.0"
|
||||
|
||||
const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ import (
|
||||
|
||||
var LastPTS uint64 = 0 // Last PTS for the current segment
|
||||
|
||||
// MacEpochOffset is the number of seconds between Mac HFS epoch (1904-01-01)
|
||||
// and Unix epoch (1970-01-01). QuickTime requires timestamps in Mac HFS format.
|
||||
const MacEpochOffset uint64 = 2082844800
|
||||
|
||||
// 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).
|
||||
@@ -29,42 +33,46 @@ 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
|
||||
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")
|
||||
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")
|
||||
TotalKeyframesReceived int // Total keyframes received by AddSampleToTrack
|
||||
TotalKeyframesWritten int // Total keyframes written to trun boxes
|
||||
FragmentKeyframeCount int // Keyframes in the current fragment
|
||||
PendingSampleIsKeyframe bool // Whether the pending video sample is a keyframe
|
||||
}
|
||||
|
||||
// NewMP4 creates a new MP4 object.
|
||||
@@ -178,17 +186,35 @@ func (mp4 *MP4) flushPendingVideoSample(nextPTS uint64) bool {
|
||||
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
|
||||
mp4.VideoFullSample.Sample.Dur = uint32(duration)
|
||||
|
||||
isKF := mp4.PendingSampleIsKeyframe
|
||||
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, uint32(mp4.VideoTrack))
|
||||
if err != nil {
|
||||
log.Log.Error("mp4.flushPendingVideoSample(): error adding sample: " + err.Error())
|
||||
}
|
||||
if isKF {
|
||||
mp4.TotalKeyframesWritten++
|
||||
mp4.FragmentKeyframeCount++
|
||||
log.Log.Debug(fmt.Sprintf("mp4.flushPendingVideoSample(): KEYFRAME WRITTEN to trun - totalWritten=%d, fragmentKF=%d, flags=0x%08x, dur=%d, DTS=%d",
|
||||
mp4.TotalKeyframesWritten, mp4.FragmentKeyframeCount, mp4.VideoFullSample.Sample.Flags, duration, mp4.VideoFullSample.DecodeTime))
|
||||
}
|
||||
|
||||
mp4.VideoFullSample = nil
|
||||
mp4.PendingSampleIsKeyframe = false
|
||||
return true
|
||||
}
|
||||
|
||||
func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, pts uint64) error {
|
||||
|
||||
if isKeyframe && trackID == uint32(mp4.VideoTrack) {
|
||||
mp4.TotalKeyframesReceived++
|
||||
elapsedDbg := uint64(0)
|
||||
if mp4.Start {
|
||||
elapsedDbg = pts - mp4.FragmentStartRawPTS
|
||||
}
|
||||
log.Log.Debug(fmt.Sprintf("mp4.AddSampleToTrack(): KEYFRAME #%d received - PTS=%d, size=%d, elapsed=%dms, started=%t, segment=%d, fragKF=%d",
|
||||
mp4.TotalKeyframesReceived, pts, len(data), elapsedDbg, mp4.Start, mp4.SegmentCount, mp4.FragmentKeyframeCount))
|
||||
}
|
||||
|
||||
if isKeyframe {
|
||||
|
||||
// Determine whether to start a new fragment.
|
||||
@@ -210,6 +236,8 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
mp4.flushPendingVideoSample(pts)
|
||||
}
|
||||
|
||||
log.Log.Debug(fmt.Sprintf("mp4.AddSampleToTrack(): FLUSHING segment #%d - keyframes_in_fragment=%d, totalKF_received=%d, totalKF_written=%d",
|
||||
mp4.SegmentCount, mp4.FragmentKeyframeCount, mp4.TotalKeyframesReceived, mp4.TotalKeyframesWritten))
|
||||
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.
|
||||
@@ -248,6 +276,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
mp4.StartPTS = pts
|
||||
mp4.FragmentStartRawPTS = pts
|
||||
mp4.FragmentStartDTS = mp4.VideoTotalDuration
|
||||
mp4.FragmentKeyframeCount = 0 // Reset keyframe counter for new fragment
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,6 +314,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
CompositionTimeOffset: 0, // No composition time offset for video
|
||||
}
|
||||
mp4.VideoFullSample = &fullSample
|
||||
mp4.PendingSampleIsKeyframe = isKeyframe
|
||||
mp4.SampleType = "video"
|
||||
}
|
||||
} else if trackID == uint32(mp4.AudioTrack) {
|
||||
@@ -334,6 +364,9 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
|
||||
func (mp4 *MP4) Close(config *models.Config) {
|
||||
|
||||
log.Log.Info(fmt.Sprintf("mp4.Close(): KEYFRAME SUMMARY - totalReceived=%d, totalWritten=%d, segments=%d, lastFragmentKF=%d",
|
||||
mp4.TotalKeyframesReceived, mp4.TotalKeyframesWritten, mp4.SegmentCount, mp4.FragmentKeyframeCount))
|
||||
|
||||
if mp4.VideoTotalDuration == 0 && mp4.AudioTotalDuration == 0 {
|
||||
log.Log.Error("mp4.Close(): no video or audio samples added, cannot create MP4 file")
|
||||
}
|
||||
@@ -407,22 +440,50 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
moov := mp4ff.NewMoovBox()
|
||||
init.AddChild(moov)
|
||||
|
||||
// Set the creation time and modification time for the moov box
|
||||
// Compute the actual video duration by summing segment durations.
|
||||
// This must exactly match the sum of sample durations in the trun boxes
|
||||
// that were written to the file, ensuring QuickTime (which strictly trusts
|
||||
// header durations) displays the correct value.
|
||||
var actualVideoDuration uint64
|
||||
for _, d := range mp4.SegmentDurations {
|
||||
actualVideoDuration += d
|
||||
}
|
||||
if actualVideoDuration != mp4.VideoTotalDuration {
|
||||
log.Log.Warning(fmt.Sprintf("mp4.Close(): duration mismatch: accumulated VideoTotalDuration=%d, sum of segment durations=%d (diff=%d ms)",
|
||||
mp4.VideoTotalDuration, actualVideoDuration, int64(mp4.VideoTotalDuration)-int64(actualVideoDuration)))
|
||||
}
|
||||
|
||||
// Set the creation time and modification time for the moov box.
|
||||
// QuickTime requires timestamps in Mac HFS format (seconds since 1904-01-01),
|
||||
// so we convert from Unix epoch by adding MacEpochOffset.
|
||||
videoTimescale := uint32(1000)
|
||||
audioTimescale := uint32(1000)
|
||||
macTime := mp4.StartTime + MacEpochOffset
|
||||
nextTrackID := uint32(len(mp4.TrackIDs) + 1)
|
||||
|
||||
// mvhd.Duration must be the duration of the longest track.
|
||||
// Start with video; if audio is longer, we update below.
|
||||
movDuration := actualVideoDuration
|
||||
if mp4.AudioTotalDuration > movDuration {
|
||||
movDuration = mp4.AudioTotalDuration
|
||||
}
|
||||
|
||||
mvhd := &mp4ff.MvhdBox{
|
||||
Version: 0,
|
||||
Flags: 0,
|
||||
CreationTime: mp4.StartTime,
|
||||
ModificationTime: mp4.StartTime,
|
||||
CreationTime: macTime,
|
||||
ModificationTime: macTime,
|
||||
Timescale: videoTimescale,
|
||||
Duration: mp4.VideoTotalDuration,
|
||||
Duration: movDuration,
|
||||
Rate: 0x00010000, // 1.0 playback speed (16.16 fixed point)
|
||||
Volume: 0x0100, // 1.0 full volume (8.8 fixed point)
|
||||
NextTrackID: nextTrackID,
|
||||
}
|
||||
init.Moov.AddChild(mvhd)
|
||||
|
||||
// Set the total duration in the moov box
|
||||
mvex := mp4ff.NewMvexBox()
|
||||
mvex.AddChild(&mp4ff.MehdBox{FragmentDuration: int64(mp4.VideoTotalDuration)})
|
||||
mvex.AddChild(&mp4ff.MehdBox{FragmentDuration: int64(movDuration)})
|
||||
init.Moov.AddChild(mvex)
|
||||
|
||||
// Add a track for the video
|
||||
@@ -433,22 +494,34 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
err := init.Moov.Traks[0].SetAVCDescriptor("avc1", mp4.SPSNALUs, mp4.PPSNALUs, includePS)
|
||||
if err != nil {
|
||||
}
|
||||
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
|
||||
init.Moov.Traks[0].Tkhd.Duration = actualVideoDuration
|
||||
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].Tkhd.CreationTime = macTime
|
||||
init.Moov.Traks[0].Tkhd.ModificationTime = macTime
|
||||
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
|
||||
// mdhd.Duration MUST be 0 for fragmented MP4. QuickTime adds mdhd.Duration
|
||||
// to the fragment durations (mehd/sidx), so setting it non-zero doubles the
|
||||
// reported duration. Leave it at 0 so the player derives duration from fragments.
|
||||
init.Moov.Traks[0].Mdia.Mdhd.Duration = 0
|
||||
init.Moov.Traks[0].Mdia.Mdhd.CreationTime = macTime
|
||||
init.Moov.Traks[0].Mdia.Mdhd.ModificationTime = macTime
|
||||
case "H265", "HVC1":
|
||||
init.AddEmptyTrack(videoTimescale, "video", "und")
|
||||
includePS := true
|
||||
err := init.Moov.Traks[0].SetHEVCDescriptor("hvc1", mp4.VPSNALUs, mp4.SPSNALUs, mp4.PPSNALUs, [][]byte{}, includePS)
|
||||
if err != nil {
|
||||
}
|
||||
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
|
||||
init.Moov.Traks[0].Tkhd.Duration = actualVideoDuration
|
||||
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].Tkhd.CreationTime = macTime
|
||||
init.Moov.Traks[0].Tkhd.ModificationTime = macTime
|
||||
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
|
||||
// mdhd.Duration MUST be 0 for fragmented MP4 (see H264 case above).
|
||||
init.Moov.Traks[0].Mdia.Mdhd.Duration = 0
|
||||
init.Moov.Traks[0].Mdia.Mdhd.CreationTime = macTime
|
||||
init.Moov.Traks[0].Mdia.Mdhd.ModificationTime = macTime
|
||||
}
|
||||
|
||||
// Try adding audio track if available
|
||||
@@ -466,8 +539,13 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
if err != nil {
|
||||
}
|
||||
init.Moov.Traks[1].Tkhd.Duration = mp4.AudioTotalDuration
|
||||
init.Moov.Traks[1].Tkhd.CreationTime = macTime
|
||||
init.Moov.Traks[1].Tkhd.ModificationTime = macTime
|
||||
init.Moov.Traks[1].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
init.Moov.Traks[1].Mdia.Mdhd.Duration = mp4.AudioTotalDuration
|
||||
// mdhd.Duration MUST be 0 for fragmented MP4 (see video track comment).
|
||||
init.Moov.Traks[1].Mdia.Mdhd.Duration = 0
|
||||
init.Moov.Traks[1].Mdia.Mdhd.CreationTime = macTime
|
||||
init.Moov.Traks[1].Mdia.Mdhd.ModificationTime = macTime
|
||||
}
|
||||
|
||||
// Try adding subtitle track if available
|
||||
@@ -498,9 +576,11 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
// and encrypted with the public key.
|
||||
|
||||
fingerprint := fmt.Sprintf("%d", init.Moov.Mvhd.CreationTime) + "_" +
|
||||
fmt.Sprintf("%d", init.Moov.Mvhd.Duration) + "_" +
|
||||
init.Moov.Trak.Mdia.Hdlr.Name + "_" +
|
||||
fmt.Sprintf("%d", mp4.MoofBoxes) + "_" // Number of moof boxes
|
||||
fmt.Sprintf("%d", init.Moov.Mvhd.Duration) + "_"
|
||||
if init.Moov.Trak != nil {
|
||||
fingerprint += init.Moov.Trak.Mdia.Hdlr.Name + "_"
|
||||
}
|
||||
fingerprint += fmt.Sprintf("%d", mp4.MoofBoxes) + "_" // Number of moof boxes
|
||||
|
||||
for i, size := range mp4.MoofBoxSizes {
|
||||
fingerprint += fmt.Sprintf("%d", size)
|
||||
@@ -514,7 +594,10 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
}
|
||||
|
||||
// Load the private key from the configuration
|
||||
privateKey := config.Signing.PrivateKey
|
||||
var privateKey string
|
||||
if config.Signing != nil {
|
||||
privateKey = config.Signing.PrivateKey
|
||||
}
|
||||
r := strings.NewReader(privateKey)
|
||||
pemBytes, _ := ioutil.ReadAll(r)
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
|
||||
176
machinery/src/video/mp4_duration_test.go
Normal file
176
machinery/src/video/mp4_duration_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
mp4ff "github.com/Eyevinn/mp4ff/mp4"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
)
|
||||
|
||||
// TestMP4Duration creates an MP4 file simulating a 5-second video recording
|
||||
// and verifies that the durations in all boxes match the sum of sample durations.
|
||||
func TestMP4Duration(t *testing.T) {
|
||||
tmpFile := "/tmp/test_duration.mp4"
|
||||
defer os.Remove(tmpFile)
|
||||
|
||||
// Minimal SPS for H.264 (baseline, 640x480) - proper Annex B format with start code
|
||||
sps := []byte{0x67, 0x42, 0xc0, 0x1e, 0xd9, 0x00, 0xa0, 0x47, 0xfe, 0xc8}
|
||||
pps := []byte{0x68, 0xce, 0x38, 0x80}
|
||||
|
||||
mp4Video := NewMP4(tmpFile, [][]byte{sps}, [][]byte{pps}, nil, 10)
|
||||
mp4Video.SetWidth(640)
|
||||
mp4Video.SetHeight(480)
|
||||
videoTrack := mp4Video.AddVideoTrack("H264")
|
||||
|
||||
// Simulate 5 seconds at 25fps (200 frames, keyframe every 50 frames = 2s)
|
||||
// PTS in milliseconds (timescale=1000)
|
||||
frameDuration := uint64(40) // 40ms per frame = 25fps
|
||||
numFrames := 150
|
||||
gopSize := 50
|
||||
|
||||
// Create a fake Annex B NAL unit (keyframe IDR = type 5, non-keyframe = type 1)
|
||||
makeFrame := func(isKey bool) []byte {
|
||||
nalType := byte(0x01) // non-IDR slice
|
||||
if isKey {
|
||||
nalType = 0x65 // IDR slice
|
||||
}
|
||||
// Start code (4 bytes) + NAL header + some data
|
||||
frame := []byte{0x00, 0x00, 0x00, 0x01, nalType}
|
||||
// Add some padding data
|
||||
for i := 0; i < 100; i++ {
|
||||
frame = append(frame, byte(i))
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
var expectedDuration uint64
|
||||
for i := 0; i < numFrames; i++ {
|
||||
pts := uint64(i) * frameDuration
|
||||
isKeyframe := i%gopSize == 0
|
||||
err := mp4Video.AddSampleToTrack(videoTrack, isKeyframe, makeFrame(isKeyframe), pts)
|
||||
if err != nil {
|
||||
t.Fatalf("AddSampleToTrack failed at frame %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
expectedDuration = uint64(numFrames) * frameDuration // Should be 6000ms (150 * 40)
|
||||
|
||||
// Close with config that has signing key to avoid nil panics
|
||||
config := &models.Config{
|
||||
Signing: &models.Signing{
|
||||
PrivateKey: "",
|
||||
},
|
||||
}
|
||||
mp4Video.Close(config)
|
||||
|
||||
// Log what the code computed
|
||||
t.Logf("VideoTotalDuration: %d ms", mp4Video.VideoTotalDuration)
|
||||
t.Logf("Expected duration: %d ms", expectedDuration)
|
||||
t.Logf("Segments: %d", len(mp4Video.SegmentDurations))
|
||||
var sumSegDur uint64
|
||||
for i, d := range mp4Video.SegmentDurations {
|
||||
t.Logf(" Segment %d: duration=%d ms", i, d)
|
||||
sumSegDur += d
|
||||
}
|
||||
t.Logf("Sum of segment durations: %d ms", sumSegDur)
|
||||
|
||||
// Now read back the file and inspect the boxes
|
||||
f, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open output file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stat output file: %v", err)
|
||||
}
|
||||
|
||||
parsedFile, err := mp4ff.DecodeFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode MP4: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("File size: %d bytes", fi.Size())
|
||||
|
||||
// Check moov box
|
||||
if parsedFile.Moov == nil {
|
||||
t.Fatal("No moov box found")
|
||||
}
|
||||
|
||||
// Check mvhd duration
|
||||
mvhd := parsedFile.Moov.Mvhd
|
||||
t.Logf("mvhd.Duration: %d (timescale=%d) = %.2f seconds", mvhd.Duration, mvhd.Timescale, float64(mvhd.Duration)/float64(mvhd.Timescale))
|
||||
t.Logf("mvhd.Rate: 0x%08x", mvhd.Rate)
|
||||
t.Logf("mvhd.Volume: 0x%04x", mvhd.Volume)
|
||||
|
||||
// Check each trak
|
||||
for i, trak := range parsedFile.Moov.Traks {
|
||||
t.Logf("Track %d:", i)
|
||||
t.Logf(" tkhd.Duration: %d", trak.Tkhd.Duration)
|
||||
t.Logf(" mdhd.Duration: %d (timescale=%d) = %.2f seconds", trak.Mdia.Mdhd.Duration, trak.Mdia.Mdhd.Timescale, float64(trak.Mdia.Mdhd.Duration)/float64(trak.Mdia.Mdhd.Timescale))
|
||||
}
|
||||
|
||||
// Check mvex/mehd
|
||||
if parsedFile.Moov.Mvex != nil && parsedFile.Moov.Mvex.Mehd != nil {
|
||||
t.Logf("mehd.FragmentDuration: %d", parsedFile.Moov.Mvex.Mehd.FragmentDuration)
|
||||
}
|
||||
|
||||
// Sum up actual sample durations from trun boxes in all segments
|
||||
var actualTrunDuration uint64
|
||||
var sampleCount int
|
||||
for _, seg := range parsedFile.Segments {
|
||||
for _, frag := range seg.Fragments {
|
||||
for _, traf := range frag.Moof.Trafs {
|
||||
// Only count video track (track 1)
|
||||
if traf.Tfhd.TrackID == 1 {
|
||||
for _, trun := range traf.Truns {
|
||||
for _, s := range trun.Samples {
|
||||
actualTrunDuration += uint64(s.Dur)
|
||||
sampleCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("Actual trun sample count: %d", sampleCount)
|
||||
t.Logf("Actual trun total duration: %d ms", actualTrunDuration)
|
||||
|
||||
// Check sidx
|
||||
if parsedFile.Sidx != nil {
|
||||
var sidxDuration uint64
|
||||
for _, ref := range parsedFile.Sidx.SidxRefs {
|
||||
sidxDuration += uint64(ref.SubSegmentDuration)
|
||||
}
|
||||
t.Logf("sidx total duration: %d ms", sidxDuration)
|
||||
}
|
||||
|
||||
// VERIFY: All duration values should be consistent
|
||||
// The expected duration for 150 frames at 40ms each:
|
||||
// - The sample-buffering pattern means the LAST sample uses LastVideoSampleDTS as duration
|
||||
// - So all 150 samples should produce 150 * 40ms = 6000ms total
|
||||
// But due to the pending sample pattern, the actual trun durations might differ
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("=== DURATION CONSISTENCY CHECK ===")
|
||||
fmt.Printf("Expected (150 * 40ms): %d ms\n", expectedDuration)
|
||||
fmt.Printf("mvhd.Duration: %d ms\n", mvhd.Duration)
|
||||
fmt.Printf("tkhd.Duration: %d ms\n", parsedFile.Moov.Traks[0].Tkhd.Duration)
|
||||
fmt.Printf("mdhd.Duration: %d ms\n", parsedFile.Moov.Traks[0].Mdia.Mdhd.Duration)
|
||||
fmt.Printf("Actual trun durations sum: %d ms\n", actualTrunDuration)
|
||||
fmt.Printf("VideoTotalDuration: %d ms\n", mp4Video.VideoTotalDuration)
|
||||
fmt.Printf("Sum of SegmentDurations: %d ms\n", sumSegDur)
|
||||
fmt.Println()
|
||||
|
||||
// The key assertion: header duration must equal trun sum
|
||||
if mvhd.Duration != actualTrunDuration {
|
||||
t.Errorf("MISMATCH: mvhd.Duration (%d) != actual trun sum (%d), diff = %d ms",
|
||||
mvhd.Duration, actualTrunDuration, int64(mvhd.Duration)-int64(actualTrunDuration))
|
||||
}
|
||||
if parsedFile.Moov.Traks[0].Mdia.Mdhd.Duration != 0 {
|
||||
t.Errorf("MISMATCH: mdhd.Duration should be 0 for fragmented MP4, got %d",
|
||||
parsedFile.Moov.Traks[0].Mdia.Mdhd.Duration)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user