mirror of
https://github.com/kerberos-io/agent.git
synced 2026-08-30 16:18:33 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
448d4a946d | ||
|
|
61ac314bb7 | ||
|
|
c1b144ca28 | ||
|
|
e16987bf9d | ||
|
|
9991597984 | ||
|
|
2c0314cea4 | ||
|
|
0584e52b98 | ||
|
|
1fc90eaee2 | ||
|
|
aef3eacbc9 | ||
|
|
2843568473 | ||
|
|
53ffc8cae0 | ||
|
|
86e654fe19 | ||
|
|
46d57f7664 | ||
|
|
963d8672eb | ||
|
|
9b7a62816a | ||
|
|
237134fe0e | ||
|
|
c8730e8f26 | ||
|
|
acbbe8b444 | ||
|
|
f690016aa5 |
@@ -36,7 +36,7 @@ func startTracing(agentKey string, otelEndpoint string) (*trace.TracerProvider,
|
||||
exporter, err := otlptrace.New(
|
||||
context.Background(),
|
||||
otlptracehttp.NewClient(
|
||||
otlptracehttp.WithEndpoint("74.241.203.114:4318"),
|
||||
otlptracehttp.WithEndpoint(otelEndpoint),
|
||||
otlptracehttp.WithHeaders(headers),
|
||||
otlptracehttp.WithInsecure(),
|
||||
),
|
||||
|
||||
@@ -84,6 +84,21 @@ type Golibrtsp struct {
|
||||
AudioMPEG4Decoder *rtpmpeg4audio.Decoder
|
||||
|
||||
Streams []packets.Stream
|
||||
|
||||
// FPS calculation fields
|
||||
lastFrameTime time.Time
|
||||
frameTimeBuffer []time.Duration
|
||||
frameBufferSize int
|
||||
frameBufferIndex int
|
||||
fpsMutex sync.Mutex
|
||||
|
||||
// I-frame interval tracking fields
|
||||
packetsSinceLastKeyframe int
|
||||
lastKeyframePacketCount int
|
||||
keyframeIntervals []int
|
||||
keyframeBufferSize int
|
||||
keyframeBufferIndex int
|
||||
keyframeMutex sync.Mutex
|
||||
}
|
||||
|
||||
// Init function
|
||||
@@ -137,8 +152,9 @@ func (g *Golibrtsp) Connect(ctx context.Context, ctxOtel context.Context) (err e
|
||||
return
|
||||
}
|
||||
|
||||
// Iniatlise the mutex.
|
||||
// Initialize the mutex and FPS calculation.
|
||||
g.VideoDecoderMutex = &sync.Mutex{}
|
||||
g.initFPSCalculation()
|
||||
|
||||
// find the H264 media and format
|
||||
var formaH264 *format.H264
|
||||
@@ -574,6 +590,9 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
var sps h264.SPS
|
||||
errSPS := sps.Unmarshal(nalu)
|
||||
if errSPS == nil {
|
||||
// Debug SPS information
|
||||
g.debugSPSInfo(&sps, streamType)
|
||||
|
||||
// Get width
|
||||
g.Streams[g.VideoH264Index].Width = sps.Width()
|
||||
if streamType == "main" {
|
||||
@@ -588,12 +607,14 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
} else if streamType == "sub" {
|
||||
configuration.Config.Capture.IPCamera.SubHeight = sps.Height()
|
||||
}
|
||||
// Get FPS
|
||||
g.Streams[g.VideoH264Index].FPS = sps.FPS()
|
||||
// Get FPS using enhanced method
|
||||
fps := g.getEnhancedFPS(&sps, g.VideoH264Index)
|
||||
g.Streams[g.VideoH264Index].FPS = fps
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.Start(%s): Final FPS=%.2f", streamType, fps))
|
||||
g.VideoH264Forma.SPS = nalu
|
||||
|
||||
}
|
||||
case h264.NALUTypePPS:
|
||||
// Read out pps
|
||||
g.VideoH264Forma.PPS = nalu
|
||||
}
|
||||
filteredAU = append(filteredAU, nalu)
|
||||
@@ -630,6 +651,21 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Codec: "H264",
|
||||
}
|
||||
|
||||
// Track keyframe intervals
|
||||
keyframeInterval := g.trackKeyframeInterval(idrPresent)
|
||||
if idrPresent && keyframeInterval > 0 {
|
||||
avgInterval := g.getAverageKeyframeInterval()
|
||||
gopDuration := float64(keyframeInterval) / g.Streams[g.VideoH265Index].FPS
|
||||
gopSize := int(avgInterval) // Store GOP size in a separate variable
|
||||
g.Streams[g.VideoH264Index].GopSize = gopSize
|
||||
log.Log.Info(fmt.Sprintf("capture.golibrtsp.Start(%s): Keyframe interval=%d packets, Avg=%.1f, GOP=%.1fs, GOPSize=%d",
|
||||
streamType, keyframeInterval, avgInterval, gopDuration, gopSize))
|
||||
preRecording := configuration.Config.Capture.PreRecording
|
||||
if preRecording > 0 && int(gopDuration) > 0 {
|
||||
queue.SetMaxGopCount(int(preRecording)/int(gopDuration) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
pkt.Data = pkt.Data[4:]
|
||||
if pkt.IsKeyFrame {
|
||||
annexbNALUStartCode := func() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
|
||||
@@ -759,6 +795,21 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Codec: "H265",
|
||||
}
|
||||
|
||||
// Track keyframe intervals for H265
|
||||
keyframeInterval := g.trackKeyframeInterval(isRandomAccess)
|
||||
if isRandomAccess && keyframeInterval > 0 {
|
||||
avgInterval := g.getAverageKeyframeInterval()
|
||||
gopDuration := float64(keyframeInterval) / g.Streams[g.VideoH265Index].FPS
|
||||
gopSize := int(avgInterval) // Store GOP size in a separate variable
|
||||
g.Streams[g.VideoH265Index].GopSize = gopSize
|
||||
log.Log.Info(fmt.Sprintf("capture.golibrtsp.Start(%s): Keyframe interval=%d packets, Avg=%.1f, GOP=%.1fs, GOPSize=%d",
|
||||
streamType, keyframeInterval, avgInterval, gopDuration, gopSize))
|
||||
preRecording := configuration.Config.Capture.PreRecording
|
||||
if preRecording > 0 && int(gopDuration) > 0 {
|
||||
queue.SetMaxGopCount(int(preRecording)/int(gopDuration) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
queue.WritePacket(pkt)
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
@@ -1128,3 +1179,185 @@ func WriteMPEG4Audio(forma *format.MPEG4Audio, aus [][]byte) ([]byte, error) {
|
||||
}
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
// Initialize FPS calculation buffers
|
||||
func (g *Golibrtsp) initFPSCalculation() {
|
||||
g.frameBufferSize = 30 // Store last 30 frame intervals
|
||||
g.frameTimeBuffer = make([]time.Duration, g.frameBufferSize)
|
||||
g.frameBufferIndex = 0
|
||||
g.lastFrameTime = time.Time{}
|
||||
|
||||
// Initialize I-frame interval tracking
|
||||
g.keyframeBufferSize = 10 // Store last 10 keyframe intervals
|
||||
g.keyframeIntervals = make([]int, g.keyframeBufferSize)
|
||||
g.keyframeBufferIndex = 0
|
||||
g.packetsSinceLastKeyframe = 0
|
||||
g.lastKeyframePacketCount = 0
|
||||
}
|
||||
|
||||
// Calculate FPS from frame timestamps
|
||||
func (g *Golibrtsp) calculateFPSFromTimestamps() float64 {
|
||||
g.fpsMutex.Lock()
|
||||
defer g.fpsMutex.Unlock()
|
||||
|
||||
if g.lastFrameTime.IsZero() {
|
||||
g.lastFrameTime = time.Now()
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
interval := now.Sub(g.lastFrameTime)
|
||||
g.lastFrameTime = now
|
||||
|
||||
// Store the interval
|
||||
g.frameTimeBuffer[g.frameBufferIndex] = interval
|
||||
g.frameBufferIndex = (g.frameBufferIndex + 1) % g.frameBufferSize
|
||||
|
||||
// Calculate average FPS from stored intervals
|
||||
var totalInterval time.Duration
|
||||
validSamples := 0
|
||||
|
||||
for _, interval := range g.frameTimeBuffer {
|
||||
if interval > 0 {
|
||||
totalInterval += interval
|
||||
validSamples++
|
||||
}
|
||||
}
|
||||
|
||||
if validSamples == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
avgInterval := totalInterval / time.Duration(validSamples)
|
||||
if avgInterval == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(time.Second) / float64(avgInterval)
|
||||
}
|
||||
|
||||
// Get enhanced FPS information from SPS with fallback
|
||||
func (g *Golibrtsp) getEnhancedFPS(sps *h264.SPS, streamIndex int8) float64 {
|
||||
// First try to get FPS from SPS
|
||||
spsFPS := sps.FPS()
|
||||
|
||||
// Check if SPS FPS is reasonable (between 1 and 120 fps)
|
||||
if spsFPS > 0 && spsFPS <= 120 {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.getEnhancedFPS(): SPS FPS: %.2f", spsFPS))
|
||||
return spsFPS
|
||||
}
|
||||
|
||||
// Fallback to timestamp-based calculation
|
||||
timestampFPS := g.calculateFPSFromTimestamps()
|
||||
if timestampFPS > 0 && timestampFPS <= 120 {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.getEnhancedFPS(): Timestamp FPS: %.2f", timestampFPS))
|
||||
return timestampFPS
|
||||
}
|
||||
|
||||
// Return SPS FPS even if it seems unreasonable, or default
|
||||
if spsFPS > 0 {
|
||||
return spsFPS
|
||||
}
|
||||
|
||||
return 25.0 // Default fallback FPS
|
||||
}
|
||||
|
||||
// Track I-frame intervals by counting packets between keyframes
|
||||
func (g *Golibrtsp) trackKeyframeInterval(isKeyframe bool) int {
|
||||
g.keyframeMutex.Lock()
|
||||
defer g.keyframeMutex.Unlock()
|
||||
|
||||
g.packetsSinceLastKeyframe++
|
||||
|
||||
if isKeyframe {
|
||||
// Store the interval since the last keyframe
|
||||
if g.lastKeyframePacketCount > 0 {
|
||||
interval := g.packetsSinceLastKeyframe
|
||||
g.keyframeIntervals[g.keyframeBufferIndex] = interval
|
||||
g.keyframeBufferIndex = (g.keyframeBufferIndex + 1) % g.keyframeBufferSize
|
||||
}
|
||||
|
||||
// Reset counter for next interval
|
||||
g.lastKeyframePacketCount = g.packetsSinceLastKeyframe
|
||||
g.packetsSinceLastKeyframe = 0
|
||||
|
||||
return g.lastKeyframePacketCount
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get average keyframe interval (GOP size)
|
||||
func (g *Golibrtsp) getAverageKeyframeInterval() float64 {
|
||||
g.keyframeMutex.Lock()
|
||||
defer g.keyframeMutex.Unlock()
|
||||
|
||||
var totalInterval int
|
||||
validSamples := 0
|
||||
|
||||
for _, interval := range g.keyframeIntervals {
|
||||
if interval > 0 {
|
||||
totalInterval += interval
|
||||
validSamples++
|
||||
}
|
||||
}
|
||||
|
||||
if validSamples == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(totalInterval) / float64(validSamples)
|
||||
}
|
||||
|
||||
// Calculate GOP size in seconds based on FPS and keyframe interval
|
||||
func (g *Golibrtsp) getGOPDuration(fps float64) float64 {
|
||||
avgInterval := g.getAverageKeyframeInterval()
|
||||
if avgInterval > 0 && fps > 0 {
|
||||
return avgInterval / fps
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get detailed SPS timing information
|
||||
func (g *Golibrtsp) getSPSTimingInfo(sps *h264.SPS) (hasVUI bool, timeScale uint32, numUnitsInTick uint32, fps float64) {
|
||||
// Try to get FPS from SPS
|
||||
fps = sps.FPS()
|
||||
|
||||
// Note: The gortsplib SPS struct may not expose VUI parameters directly
|
||||
// but we can still work with the calculated FPS
|
||||
if fps > 0 {
|
||||
hasVUI = true
|
||||
// These are estimated values based on common patterns
|
||||
if fps == 25.0 {
|
||||
timeScale = 50
|
||||
numUnitsInTick = 1
|
||||
} else if fps == 30.0 {
|
||||
timeScale = 60
|
||||
numUnitsInTick = 1
|
||||
} else if fps == 24.0 {
|
||||
timeScale = 48
|
||||
numUnitsInTick = 1
|
||||
} else {
|
||||
// Generic calculation
|
||||
timeScale = uint32(fps * 2)
|
||||
numUnitsInTick = 1
|
||||
}
|
||||
}
|
||||
|
||||
return hasVUI, timeScale, numUnitsInTick, fps
|
||||
}
|
||||
|
||||
// Debug SPS information
|
||||
func (g *Golibrtsp) debugSPSInfo(sps *h264.SPS, streamType string) {
|
||||
hasVUI, timeScale, numUnitsInTick, fps := g.getSPSTimingInfo(sps)
|
||||
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.debugSPSInfo(%s): Width=%d, Height=%d",
|
||||
streamType, sps.Width(), sps.Height()))
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.debugSPSInfo(%s): HasVUI=%t, FPS=%.2f",
|
||||
streamType, hasVUI, fps))
|
||||
|
||||
if hasVUI {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.debugSPSInfo(%s): TimeScale=%d, NumUnitsInTick=%d",
|
||||
streamType, timeScale, numUnitsInTick))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
if start && // If already recording and current frame is a keyframe and we should stop recording
|
||||
nextPkt.IsKeyFrame && (timestamp+postRecording-now <= 0 || now-startRecording >= maxRecordingPeriod) {
|
||||
nextPkt.IsKeyFrame && (timestamp+postRecording-now <= 0 || now-startRecording >= maxRecordingPeriod-1000) {
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
@@ -159,6 +159,39 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// Cleanup muxer
|
||||
start = false
|
||||
|
||||
// Update the name of the recording with the duration.
|
||||
// We will update the name of the recording with the duration in milliseconds.
|
||||
if mp4Video.VideoTotalDuration > 0 {
|
||||
duration := mp4Video.VideoTotalDuration
|
||||
// Update the name with the duration in milliseconds.
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
startRecordingMilliseconds := startRecording % 1000 // convert to milliseconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
|
||||
strconv.Itoa(len(strconv.FormatInt(startRecordingMilliseconds, 10))) + "-" +
|
||||
strconv.FormatInt(startRecordingMilliseconds, 10) + "_" +
|
||||
config.Name + "_" +
|
||||
"0-0-0-0" + "_" + // region coordinates, we
|
||||
"-1" + "_" + // token
|
||||
strconv.FormatInt(int64(duration), 10) // + "_" + // duration of recording
|
||||
//utils.VERSION // version of the agent
|
||||
|
||||
oldName := name
|
||||
name = s + ".mp4"
|
||||
fullName = configDirectory + "/data/recordings/" + name
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): renamed file from: " + oldName + " to: " + name)
|
||||
|
||||
// Rename the file to the new name.
|
||||
err := os.Rename(
|
||||
configDirectory+"/data/recordings/"+oldName,
|
||||
configDirectory+"/data/recordings/"+s+".mp4")
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error renaming file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): no video data recorded, not renaming file.")
|
||||
}
|
||||
|
||||
// Check if we need to encrypt the recording.
|
||||
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
|
||||
// reopen file into memory 'fullName'
|
||||
@@ -215,13 +248,16 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// - Token
|
||||
|
||||
startRecording = time.Now().UnixMilli()
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
|
||||
"6" + "-" +
|
||||
"967003" + "_" +
|
||||
config.Name + "_" +
|
||||
"200-200-400-400" + "_0_" +
|
||||
"769"
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
startRecordingMilliseconds := startRecording % 1000 // convert to milliseconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" + // start timestamp in seconds
|
||||
strconv.Itoa(len(strconv.FormatInt(startRecordingMilliseconds, 10))) + "-" + // length of milliseconds
|
||||
strconv.FormatInt(startRecordingMilliseconds, 10) + "_" + // milliseconds
|
||||
config.Name + "_" + // device name
|
||||
"0-0-0-0" + "_" + // region coordinates, we will not use this for continuous recording
|
||||
"0" + "_" + // token
|
||||
"0" + "_" //+ // duration of recording in milliseconds
|
||||
//utils.VERSION // version of the agent
|
||||
|
||||
name = s + ".mp4"
|
||||
fullName = configDirectory + "/data/recordings/" + name
|
||||
@@ -305,6 +341,39 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// Cleanup muxer
|
||||
start = false
|
||||
|
||||
// Update the name of the recording with the duration.
|
||||
// We will update the name of the recording with the duration in milliseconds.
|
||||
if mp4Video.VideoTotalDuration > 0 {
|
||||
duration := mp4Video.VideoTotalDuration
|
||||
// Update the name with the duration in milliseconds.
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
startRecordingMilliseconds := startRecording % 1000 // convert to milliseconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
|
||||
strconv.Itoa(len(strconv.FormatInt(startRecordingMilliseconds, 10))) + "-" +
|
||||
strconv.FormatInt(startRecordingMilliseconds, 10) + "_" +
|
||||
config.Name + "_" +
|
||||
"0-0-0-0" + "_" + // region coordinates, we
|
||||
"-1" + "_" + // token
|
||||
strconv.FormatInt(int64(duration), 10) // + "_" + // duration of recording
|
||||
//utils.VERSION // version of the agent
|
||||
|
||||
oldName := name
|
||||
name = s + ".mp4"
|
||||
fullName = configDirectory + "/data/recordings/" + name
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): renamed file from: " + oldName + " to: " + name)
|
||||
|
||||
// Rename the file to the new name.
|
||||
err := os.Rename(
|
||||
configDirectory+"/data/recordings/"+oldName,
|
||||
configDirectory+"/data/recordings/"+s+".mp4")
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error renaming file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): no video data recorded, not renaming file.")
|
||||
}
|
||||
|
||||
// Check if we need to encrypt the recording.
|
||||
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
|
||||
// reopen file into memory 'fullName'
|
||||
@@ -342,28 +411,58 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
|
||||
var lastDuration int64 = 0 // last duration in milliseconds
|
||||
var lastRecordingTime int64 = 0 // last recording time in milliseconds
|
||||
var displayTime int64 = 0 // display time in milliseconds
|
||||
|
||||
var videoTrack uint32
|
||||
var audioTrack uint32
|
||||
|
||||
streams, _ := rtspClient.GetVideoStreams()
|
||||
videoStream := streams[0] // We will use the first video stream, as we only expect one video stream.
|
||||
|
||||
for motion := range communication.HandleMotion {
|
||||
|
||||
// Get as much packets we need.
|
||||
var cursorError error
|
||||
var pkt packets.Packet
|
||||
var nextPkt packets.Packet
|
||||
recordingCursor := queue.Oldest() // Start from the latest packet in the queue)
|
||||
|
||||
timestamp = time.Now().UnixMilli()
|
||||
startRecording = time.Now().UnixMilli() // we mark the current time when the record started.
|
||||
numberOfChanges := motion.NumberOfChanges
|
||||
|
||||
// If we have prerecording we will substract the number of seconds.
|
||||
// Taking into account FPS = GOP size (Keyfram interval)
|
||||
if preRecording > 0 && lastRecordingTime > 0 {
|
||||
var preRecordingDelta int64 = 0
|
||||
if preRecording > 0 {
|
||||
|
||||
fps := videoStream.FPS
|
||||
queueSize := queue.GetSize()
|
||||
|
||||
// Based on the GOP size and FPS we can calculate the pre-recording time.
|
||||
// It might be that the queue size is 0, in that case we will not calculate the pre-recording time.
|
||||
queuedAvailablePreRecording := preRecording
|
||||
if queueSize > 0 && fps > 0 {
|
||||
queuedAvailablePreRecording = int64(queueSize) / int64(fps) * 1000 // convert to milliseconds
|
||||
}
|
||||
timeBetweenNowAndLastRecording := startRecording - lastRecordingTime
|
||||
if lastRecordingTime == 0 {
|
||||
timeBetweenNowAndLastRecording = 0
|
||||
}
|
||||
|
||||
// Might be that recordings are coming short after each other.
|
||||
// Therefore we do some math with the current time and the last recording time.
|
||||
|
||||
timeBetweenNowAndLastRecording := startRecording - lastRecordingTime
|
||||
if timeBetweenNowAndLastRecording > preRecording {
|
||||
startRecording = startRecording - preRecording + 1000 // we add 1000 milliseconds to make sure we have a full second of pre-recording.
|
||||
} else {
|
||||
startRecording = startRecording - timeBetweenNowAndLastRecording
|
||||
if timeBetweenNowAndLastRecording >= preRecording {
|
||||
displayTime = startRecording - preRecording + 1000
|
||||
preRecordingDelta = preRecording
|
||||
} else if timeBetweenNowAndLastRecording < preRecording {
|
||||
// If the time between now and the last recording is less than the pre-recording time,
|
||||
// we will use the pre-recording time.
|
||||
if lastRecordingTime == 0 && queuedAvailablePreRecording < preRecording {
|
||||
displayTime = startRecording - queuedAvailablePreRecording
|
||||
} else {
|
||||
preRecordingDelta = timeBetweenNowAndLastRecording
|
||||
displayTime = startRecording - preRecordingDelta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,20 +474,33 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// - Region
|
||||
// - Number of changes
|
||||
// - Token
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
|
||||
"6" + "-" +
|
||||
"967003" + "_" +
|
||||
config.Name + "_" +
|
||||
"200-200-400-400" + "_" +
|
||||
strconv.Itoa(numberOfChanges) + "_" +
|
||||
"769"
|
||||
|
||||
displayTimeSeconds := displayTime / 1000 // convert to seconds
|
||||
displayTimeMilliseconds := displayTime % 1000 // convert to milliseconds
|
||||
motionRectangleString := "0-0-0-0"
|
||||
if motion.Rectangle.X != 0 || motion.Rectangle.Y != 0 ||
|
||||
motion.Rectangle.Width != 0 || motion.Rectangle.Height != 0 {
|
||||
motionRectangleString = strconv.Itoa(motion.Rectangle.X) + "-" + strconv.Itoa(motion.Rectangle.Y) + "-" +
|
||||
strconv.Itoa(motion.Rectangle.Width) + "-" + strconv.Itoa(motion.Rectangle.Height)
|
||||
}
|
||||
|
||||
// Get the number of changes from the motion detection.
|
||||
numberOfChanges := motion.NumberOfChanges
|
||||
|
||||
s := strconv.FormatInt(displayTimeSeconds, 10) + "_" + // start timestamp in seconds
|
||||
strconv.Itoa(len(strconv.FormatInt(displayTimeMilliseconds, 10))) + "-" + // length of milliseconds
|
||||
strconv.FormatInt(displayTimeMilliseconds, 10) + "_" + // milliseconds
|
||||
config.Name + "_" + // device name
|
||||
motionRectangleString + "_" + // region coordinates, we will not use this for continuous recording
|
||||
strconv.Itoa(numberOfChanges) + "_" + // number of changes
|
||||
"0" // + "_" + // duration of recording in milliseconds
|
||||
//utils.VERSION // version of the agent
|
||||
|
||||
name := s + ".mp4"
|
||||
fullName := configDirectory + "/data/recordings/" + name
|
||||
|
||||
// Running...
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): recording started")
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): recording started (" + name + ")" + " at " + strconv.FormatInt(displayTimeSeconds, 10) + " unix")
|
||||
|
||||
// Get width and height from the camera.
|
||||
width := configuration.Config.Capture.IPCamera.Width
|
||||
@@ -417,12 +529,6 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
|
||||
start := false
|
||||
|
||||
// Get as much packets we need.
|
||||
var cursorError error
|
||||
var pkt packets.Packet
|
||||
var nextPkt packets.Packet
|
||||
recordingCursor := queue.DelayedGopCount(int(config.Capture.PreRecording + 1))
|
||||
|
||||
if cursorError == nil {
|
||||
pkt, cursorError = recordingCursor.ReadPacket()
|
||||
}
|
||||
@@ -435,16 +541,19 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
select {
|
||||
case motion := <-communication.HandleMotion:
|
||||
timestamp = now
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): motion detected while recording. Expanding recording.")
|
||||
numberOfChanges = motion.NumberOfChanges
|
||||
numberOfChanges := motion.NumberOfChanges
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
|
||||
default:
|
||||
}
|
||||
|
||||
if (timestamp+postRecording-now < 0 || now-startRecording > maxRecordingPeriod-1000) && nextPkt.IsKeyFrame {
|
||||
if (timestamp+postRecording+(preRecording-preRecordingDelta)-now < 0 || now-startRecording > maxRecordingPeriod-preRecordingDelta) && nextPkt.IsKeyFrame {
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): timestamp+postRecording-now < 0 - " + strconv.FormatInt(timestamp+postRecording-now, 10) + " < 0")
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): now-startRecording > maxRecordingPeriod-1000 - " + strconv.FormatInt(now-startRecording, 10) + " > " + strconv.FormatInt(maxRecordingPeriod-1000, 10))
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(timestamp, 10) + ", postRecording: " + strconv.FormatInt(postRecording, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
|
||||
break
|
||||
}
|
||||
@@ -479,12 +588,46 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
pkt = nextPkt
|
||||
}
|
||||
|
||||
// Update the last duration and last recording time.
|
||||
// This is used to determine if we need to start a new recording.
|
||||
lastDuration = pkt.Time
|
||||
lastRecordingTime = time.Now().UnixMilli()
|
||||
|
||||
// This will close the recording and write the last packet.
|
||||
mp4Video.Close(&config)
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): file save: " + name)
|
||||
|
||||
lastDuration = pkt.Time
|
||||
lastRecordingTime = time.Now().UnixMilli()
|
||||
// Update the name of the recording with the duration.
|
||||
// We will update the name of the recording with the duration in milliseconds.
|
||||
if mp4Video.VideoTotalDuration > 0 {
|
||||
duration := mp4Video.VideoTotalDuration
|
||||
|
||||
// Update the name with the duration in milliseconds.
|
||||
s := strconv.FormatInt(displayTimeSeconds, 10) + "_" +
|
||||
strconv.Itoa(len(strconv.FormatInt(displayTimeMilliseconds, 10))) + "-" +
|
||||
strconv.FormatInt(displayTimeMilliseconds, 10) + "_" +
|
||||
config.Name + "_" +
|
||||
motionRectangleString + "_" +
|
||||
strconv.Itoa(numberOfChanges) + "_" + // number of changes
|
||||
strconv.FormatInt(int64(duration), 10) // + "_" + // duration of recording in milliseconds
|
||||
//utils.VERSION // version of the agent
|
||||
|
||||
oldName := name
|
||||
name = s + ".mp4"
|
||||
fullName = configDirectory + "/data/recordings/" + name
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): renamed file from: " + oldName + " to: " + name)
|
||||
|
||||
// Rename the file to the new name.
|
||||
err := os.Rename(
|
||||
configDirectory+"/data/recordings/"+oldName,
|
||||
configDirectory+"/data/recordings/"+s+".mp4")
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error renaming file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): no video data recorded, not renaming file.")
|
||||
}
|
||||
|
||||
// Check if we need to encrypt the recording.
|
||||
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
|
||||
|
||||
@@ -235,7 +235,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
|
||||
// Set the maximum GOP count, this is used to determine the pre-recording time.
|
||||
log.Log.Info("components.Kerberos.RunAgent(): SetMaxGopCount was set with: " + strconv.Itoa(int(config.Capture.PreRecording)+1))
|
||||
queue.SetMaxGopCount(int(config.Capture.PreRecording) + 1) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
|
||||
queue.SetMaxGopCount(1) // We will adjust this later on, when we have the GOP size.
|
||||
queue.WriteHeader(videoStreams)
|
||||
go rtspClient.Start(ctx, "main", queue, configuration, communication)
|
||||
|
||||
@@ -254,7 +254,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
if subStreamEnabled && rtspSubClient != nil {
|
||||
subQueue = packets.NewQueue()
|
||||
communication.SubQueue = subQueue
|
||||
subQueue.SetMaxGopCount(3) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
|
||||
subQueue.SetMaxGopCount(1) // GOP time frame is set to 1 for motion detection and livestreaming.
|
||||
subQueue.WriteHeader(videoSubStreams)
|
||||
go rtspSubClient.Start(ctx, "sub", subQueue, configuration, communication)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
|
||||
var isPixelChangeThresholdReached = false
|
||||
var changesToReturn = 0
|
||||
var motionRectangle models.MotionRectangle
|
||||
|
||||
pixelThreshold := config.Capture.PixelChangeThreshold
|
||||
// Might not be set in the config file, so set it to 150
|
||||
@@ -132,7 +133,7 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
if detectMotion {
|
||||
|
||||
// Remember additional information about the result of findmotion
|
||||
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
|
||||
isPixelChangeThresholdReached, changesToReturn, motionRectangle = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
|
||||
if isPixelChangeThresholdReached {
|
||||
|
||||
// If offline mode is disabled, send a message to the hub
|
||||
@@ -164,6 +165,7 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
dataToPass := models.MotionDataPartial{
|
||||
Timestamp: time.Now().Unix(),
|
||||
NumberOfChanges: changesToReturn,
|
||||
Rectangle: motionRectangle,
|
||||
}
|
||||
communication.HandleMotion <- dataToPass //Save data to the channel
|
||||
}
|
||||
@@ -185,24 +187,58 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
log.Log.Debug("computervision.main.ProcessMotion(): stop the motion detection.")
|
||||
}
|
||||
|
||||
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int) {
|
||||
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int, motionRectangle models.MotionRectangle) {
|
||||
image1 := imageArray[0]
|
||||
image2 := imageArray[1]
|
||||
image3 := imageArray[2]
|
||||
threshold := 60
|
||||
changes := AbsDiffBitwiseAndThreshold(image1, image2, image3, threshold, coordinatesToCheck)
|
||||
return changes > pixelChangeThreshold, changes
|
||||
changes, motionRectangle := AbsDiffBitwiseAndThreshold(image1, image2, image3, threshold, coordinatesToCheck)
|
||||
return changes > pixelChangeThreshold, changes, motionRectangle
|
||||
}
|
||||
|
||||
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) int {
|
||||
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) (int, models.MotionRectangle) {
|
||||
changes := 0
|
||||
var pixelList [][]int
|
||||
for i := 0; i < len(coordinatesToCheck); i++ {
|
||||
pixel := coordinatesToCheck[i]
|
||||
diff := int(img3.Pix[pixel]) - int(img1.Pix[pixel])
|
||||
diff2 := int(img3.Pix[pixel]) - int(img2.Pix[pixel])
|
||||
if (diff > threshold || diff < -threshold) && (diff2 > threshold || diff2 < -threshold) {
|
||||
changes++
|
||||
// Store the pixel coordinates where the change is detected
|
||||
pixelList = append(pixelList, []int{pixel % img1.Bounds().Dx(), pixel / img1.Bounds().Dx()})
|
||||
}
|
||||
}
|
||||
return changes
|
||||
|
||||
// Calculate rectangle of pixelList (startX, startY, endX, endY)
|
||||
var motionRectangle models.MotionRectangle
|
||||
if len(pixelList) > 0 {
|
||||
startX := pixelList[0][0]
|
||||
startY := pixelList[0][1]
|
||||
endX := startX
|
||||
endY := startY
|
||||
for _, pixel := range pixelList {
|
||||
if pixel[0] < startX {
|
||||
startX = pixel[0]
|
||||
}
|
||||
if pixel[1] < startY {
|
||||
startY = pixel[1]
|
||||
}
|
||||
if pixel[0] > endX {
|
||||
endX = pixel[0]
|
||||
}
|
||||
if pixel[1] > endY {
|
||||
endY = pixel[1]
|
||||
}
|
||||
}
|
||||
log.Log.Debugf("Rectangle of changes detected: startX: %d, startY: %d, endX: %d, endY: %d", startX, startY, endX, endY)
|
||||
motionRectangle = models.MotionRectangle{
|
||||
X: startX,
|
||||
Y: startY,
|
||||
Width: endX - startX,
|
||||
Height: endY - startY,
|
||||
}
|
||||
log.Log.Debugf("Motion rectangle: %+v", motionRectangle)
|
||||
}
|
||||
return changes, motionRectangle
|
||||
}
|
||||
|
||||
@@ -138,6 +138,16 @@ func (self *Logging) Debug(sentence string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Logging) Debugf(format string, args ...interface{}) {
|
||||
switch self.Logger {
|
||||
case "go-logging":
|
||||
gologging.Debugf(format, args...)
|
||||
case "logrus":
|
||||
logrus.Debugf(format, args...)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Logging) Error(sentence string) {
|
||||
switch self.Logger {
|
||||
case "go-logging":
|
||||
|
||||
@@ -65,6 +65,7 @@ type Capture struct {
|
||||
Continuous string `json:"continuous,omitempty"`
|
||||
PostRecording int64 `json:"postrecording"`
|
||||
PreRecording int64 `json:"prerecording"`
|
||||
GopSize int `json:"gopsize,omitempty" bson:"gopsize,omitempty"` // GOP size in seconds, used for pre-recording
|
||||
MaxLengthRecording int64 `json:"maxlengthrecording"`
|
||||
TranscodingWebRTC string `json:"transcodingwebrtc"`
|
||||
TranscodingResolution int64 `json:"transcodingresolution"`
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package models
|
||||
|
||||
type MotionDataPartial struct {
|
||||
Timestamp int64 `json:"timestamp" bson:"timestamp"`
|
||||
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
|
||||
Timestamp int64 `json:"timestamp" bson:"timestamp"`
|
||||
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
|
||||
Rectangle MotionRectangle `json:"rectangle" bson:"rectangle"`
|
||||
}
|
||||
|
||||
type MotionDataFull struct {
|
||||
@@ -14,3 +15,10 @@ type MotionDataFull struct {
|
||||
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
|
||||
Token int `json:"token" bson:"token"`
|
||||
}
|
||||
|
||||
type MotionRectangle struct {
|
||||
X int `json:"x" bson:"x"`
|
||||
Y int `json:"y" bson:"y"`
|
||||
Width int `json:"width" bson:"width"`
|
||||
Height int `json:"height" bson:"height"`
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ type Packet struct {
|
||||
Time int64 // packet decode time
|
||||
TimeLegacy time.Duration
|
||||
Data []byte // packet data
|
||||
Gopsize int // size of the GOP
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ func (self *Queue) SetMaxGopCount(n int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Queue) GetMaxGopCount() int {
|
||||
n := self.maxgopcount
|
||||
return n
|
||||
}
|
||||
|
||||
func (self *Queue) WriteHeader(streams []Stream) error {
|
||||
self.lock.Lock()
|
||||
|
||||
|
||||
@@ -48,4 +48,7 @@ type Stream struct {
|
||||
|
||||
// Channels is the number of audio channels.
|
||||
Channels int
|
||||
|
||||
// GopSize is the size of the GOP (Group of Pictures).
|
||||
GopSize int
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
if err == nil {
|
||||
if mp4.VideoFullSample != nil {
|
||||
duration := pts - mp4.VideoFullSample.DecodeTime
|
||||
log.Log.Info("Adding sample to track " + fmt.Sprintf("%d, PTS: %d, Duration: %d, size: %d, Keyframe: %t", trackID, pts, duration, len(lengthPrefixed), isKeyframe))
|
||||
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
|
||||
//fmt.Printf("Adding sample to track %d, PTS: %d, Duration: %d, size: %d, Keyframe: %t\n", trackID, pts, duration, len(mp4.VideoFullSample.Data), isKeyframe)
|
||||
@@ -288,10 +288,16 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
mp4.Segments = append(mp4.Segments, mp4.Segment)
|
||||
}*/
|
||||
|
||||
if mp4.VideoTotalDuration == 0 && mp4.AudioTotalDuration == 0 {
|
||||
log.Log.Error("mp4.Close(): no video or audio samples added, cannot create MP4 file")
|
||||
}
|
||||
|
||||
// Encode the last segment
|
||||
err := mp4.Segment.Encode(mp4.Writer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if mp4.Segment != nil {
|
||||
err := mp4.Segment.Encode(mp4.Writer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mp4.Writer.Flush()
|
||||
|
||||
Reference in New Issue
Block a user