mirror of
https://github.com/kerberos-io/agent.git
synced 2026-08-23 15:08:32 +00:00
Compare commits
4 Commits
v3.10.0
...
feature/pc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642a89a989 | ||
|
|
204ae3f05c | ||
|
|
945ceea61c | ||
|
|
a7e68951dc |
@@ -339,6 +339,40 @@ func (g *Golibrtsp) Connect(ctx context.Context, ctxOtel context.Context) (err e
|
||||
}
|
||||
}
|
||||
|
||||
// Look for audio stream.
|
||||
// find the LPCM media and format
|
||||
audioFormaLPCM, audioMediLPCM := FindLPCM(desc, false)
|
||||
g.AudioLPCMMedia = audioMediLPCM
|
||||
g.AudioLPCMForma = audioFormaLPCM
|
||||
if audioMediLPCM == nil {
|
||||
log.Log.Debug("capture.golibrtsp.Connect(LPCM): " + "audio media not found")
|
||||
} else {
|
||||
_, err = g.Client.Setup(desc.BaseURL, audioMediLPCM, 0, 0)
|
||||
if err != nil {
|
||||
log.Log.Error("capture.golibrtsp.Connect(LPCM): " + err.Error())
|
||||
} else {
|
||||
audiortpDec, err := audioFormaLPCM.CreateDecoder()
|
||||
if err != nil {
|
||||
log.Log.Error("capture.golibrtsp.Connect(LPCM): " + err.Error())
|
||||
} else {
|
||||
g.AudioLPCMDecoder = audiortpDec
|
||||
streamIndex := len(g.Streams)
|
||||
g.Streams = append(g.Streams, packets.Stream{
|
||||
Index: streamIndex,
|
||||
Name: "LPCM",
|
||||
IsVideo: false,
|
||||
IsAudio: true,
|
||||
IsBackChannel: false,
|
||||
SampleRate: audioFormaLPCM.SampleRate,
|
||||
Channels: audioFormaLPCM.ChannelCount,
|
||||
BitDepth: audioFormaLPCM.BitDepth,
|
||||
})
|
||||
|
||||
g.AudioLPCMIndex = int8(len(g.Streams)) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for audio stream.
|
||||
// find the G711 media and format
|
||||
audioForma, audioMedi := FindPCMU(desc, false)
|
||||
@@ -367,6 +401,8 @@ func (g *Golibrtsp) Connect(ctx context.Context, ctxOtel context.Context) (err e
|
||||
IsVideo: false,
|
||||
IsAudio: true,
|
||||
IsBackChannel: false,
|
||||
SampleRate: defaultPCMUSampleRate,
|
||||
Channels: defaultPCMUChannels,
|
||||
})
|
||||
|
||||
// Set the index for the audio
|
||||
@@ -509,6 +545,8 @@ func (g *Golibrtsp) ConnectBackChannel(ctx context.Context, ctxRunAgent context.
|
||||
IsVideo: false,
|
||||
IsAudio: true,
|
||||
IsBackChannel: true,
|
||||
SampleRate: defaultPCMUSampleRate,
|
||||
Channels: defaultPCMUChannels,
|
||||
})
|
||||
// Set the index for the audio
|
||||
g.AudioG711IndexBackChannel = int8(len(g.Streams)) - 1
|
||||
@@ -521,6 +559,39 @@ func (g *Golibrtsp) ConnectBackChannel(ctx context.Context, ctxRunAgent context.
|
||||
func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets.Queue, configuration *models.Configuration, communication *models.Communication) (err error) {
|
||||
log.Log.Debug("capture.golibrtsp.Start(): started")
|
||||
|
||||
// called when a MULAW audio RTP packet arrives
|
||||
if g.AudioLPCMMedia != nil && g.AudioLPCMForma != nil {
|
||||
g.Client.OnPacketRTP(g.AudioLPCMMedia, g.AudioLPCMForma, func(rtppkt *rtp.Packet) {
|
||||
pts, ok := g.Client.PacketPTS(g.AudioLPCMMedia, rtppkt)
|
||||
pts2, ok := g.Client.PacketPTS2(g.AudioLPCMMedia, rtppkt)
|
||||
if !ok {
|
||||
log.Log.Debug("capture.golibrtsp.Start(): " + "unable to get PTS")
|
||||
return
|
||||
}
|
||||
|
||||
op, err := g.AudioLPCMDecoder.Decode(rtppkt)
|
||||
if err != nil {
|
||||
log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pkt := packets.Packet{
|
||||
IsKeyFrame: false,
|
||||
Packet: rtppkt,
|
||||
Data: op,
|
||||
Time: pts2,
|
||||
TimeLegacy: pts,
|
||||
CompositionTime: pts2,
|
||||
CurrentTime: time.Now().UnixMilli(),
|
||||
Idx: g.AudioLPCMIndex,
|
||||
IsVideo: false,
|
||||
IsAudio: true,
|
||||
Codec: "LPCM",
|
||||
}
|
||||
queue.WritePacket(pkt)
|
||||
})
|
||||
}
|
||||
|
||||
// called when a MULAW audio RTP packet arrives
|
||||
if g.AudioG711Media != nil && g.AudioG711Forma != nil {
|
||||
g.Client.OnPacketRTP(g.AudioG711Media, g.AudioG711Forma, func(rtppkt *rtp.Packet) {
|
||||
@@ -1259,6 +1330,21 @@ func FindPCMU(desc *description.Session, isBackChannel bool) (*format.G711, *des
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func FindLPCM(desc *description.Session, isBackChannel bool) (*format.LPCM, *description.Media) {
|
||||
for _, media := range desc.Medias {
|
||||
if media.IsBackChannel == isBackChannel {
|
||||
for _, forma := range media.Formats {
|
||||
if lpcm, ok := forma.(*format.LPCM); ok {
|
||||
if lpcm.SampleRate > 0 && lpcm.ChannelCount > 0 && lpcm.BitDepth > 0 {
|
||||
return lpcm, media
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func FindOPUS(desc *description.Session, isBackChannel bool) (*format.Opus, *description.Media) {
|
||||
for _, media := range desc.Medias {
|
||||
if media.IsBackChannel == isBackChannel {
|
||||
|
||||
@@ -87,13 +87,31 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// We only expect one audio and one video codec.
|
||||
// If there are multiple audio or video streams, we will use the first one.
|
||||
audioCodec := ""
|
||||
audioBitDepth := 0
|
||||
videoCodec := ""
|
||||
configuredAudioSampleRate := config.Capture.IPCamera.SampleRate
|
||||
configuredAudioChannels := config.Capture.IPCamera.Channels
|
||||
audioStreams, _ := rtspClient.GetAudioStreams()
|
||||
videoStreams, _ := rtspClient.GetVideoStreams()
|
||||
if len(audioStreams) > 0 {
|
||||
audioCodec = audioStreams[0].Name
|
||||
config.Capture.IPCamera.SampleRate = audioStreams[0].SampleRate
|
||||
config.Capture.IPCamera.Channels = audioStreams[0].Channels
|
||||
resolvedSampleRate := audioStreams[0].SampleRate
|
||||
resolvedChannels := audioStreams[0].Channels
|
||||
|
||||
if audioCodec == "LPCM" {
|
||||
if configuredAudioSampleRate > 0 && configuredAudioSampleRate != resolvedSampleRate {
|
||||
log.Log.Warning("capture.main.HandleRecordStream(): LPCM sample rate mismatch between configuration and RTSP stream; using configured value " + strconv.Itoa(configuredAudioSampleRate) + " instead of detected value " + strconv.Itoa(resolvedSampleRate))
|
||||
resolvedSampleRate = configuredAudioSampleRate
|
||||
}
|
||||
if configuredAudioChannels > 0 && configuredAudioChannels != resolvedChannels {
|
||||
log.Log.Warning("capture.main.HandleRecordStream(): LPCM channel count mismatch between configuration and RTSP stream; using configured value " + strconv.Itoa(configuredAudioChannels) + " instead of detected value " + strconv.Itoa(resolvedChannels))
|
||||
resolvedChannels = configuredAudioChannels
|
||||
}
|
||||
}
|
||||
|
||||
config.Capture.IPCamera.SampleRate = resolvedSampleRate
|
||||
config.Capture.IPCamera.Channels = resolvedChannels
|
||||
audioBitDepth = audioStreams[0].BitDepth
|
||||
}
|
||||
if len(videoStreams) > 0 {
|
||||
videoCodec = videoStreams[0].Name
|
||||
@@ -105,13 +123,15 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
//var cws *cacheWriterSeeker
|
||||
var mp4Video *video.MP4
|
||||
var videoTrack uint32
|
||||
var audioTrack uint32
|
||||
var audioWriter *recordingAudioWriter
|
||||
var name string
|
||||
|
||||
// Do not do anything!
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): start recording")
|
||||
|
||||
start := false
|
||||
rolloverRequested := false
|
||||
rolloverMaxLogged := false
|
||||
|
||||
// If continuous record the full length
|
||||
postRecording = maxRecordingPeriod
|
||||
@@ -135,29 +155,42 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
|
||||
nextPkt, cursorError = recordingCursor.ReadPacket()
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
packetTime := pkt.CurrentTime
|
||||
hardMaxReached := packetTime-startRecording > maxRecordingPeriod-500
|
||||
postRecordingElapsed := startRecording+postRecording-packetTime <= 0
|
||||
if start && (postRecordingElapsed || hardMaxReached) {
|
||||
rolloverRequested = true
|
||||
if hardMaxReached && !rolloverMaxLogged {
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): max recording period reached, waiting for next keyframe to roll over without dropping frames")
|
||||
rolloverMaxLogged = true
|
||||
}
|
||||
}
|
||||
|
||||
if start && // If already recording and current frame is a keyframe and we should stop recording
|
||||
nextPkt.IsKeyFrame && (startRecording+postRecording-now <= 0 || now-startRecording > maxRecordingPeriod-500) {
|
||||
closeOnCurrentKeyframe := rolloverRequested && pkt.IsKeyFrame && pkt.CurrentTime > startRecording
|
||||
closeOnNextKeyframe := rolloverRequested && nextPkt.IsKeyFrame
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
// Write the last packet
|
||||
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.IsAudio {
|
||||
// Write the last packet
|
||||
if pkt.Codec == "AAC" {
|
||||
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
|
||||
if start && (closeOnCurrentKeyframe || closeOnNextKeyframe) {
|
||||
|
||||
if !closeOnCurrentKeyframe {
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
// Write the last packet before the rollover keyframe.
|
||||
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.IsAudio {
|
||||
if err := audioWriter.WritePacket(pkt); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.Codec == "PCM_MULAW" {
|
||||
// TODO: transcode to AAC, some work to do..
|
||||
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
|
||||
}
|
||||
}
|
||||
|
||||
if err := audioWriter.Flush(); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
audioWriter.Close()
|
||||
audioWriter = nil
|
||||
|
||||
// Close mp4
|
||||
if len(mp4Video.SPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.SPSNALUs) > 0 {
|
||||
mp4Video.SPSNALUs = configuration.Config.Capture.IPCamera.SPSNALUs
|
||||
@@ -177,6 +210,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
|
||||
// Cleanup muxer
|
||||
start = false
|
||||
rolloverRequested = false
|
||||
rolloverMaxLogged = false
|
||||
|
||||
// Update the name of the recording with the duration.
|
||||
// We will update the name of the recording with the duration in milliseconds.
|
||||
@@ -305,11 +340,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
} else if videoCodec == "H265" {
|
||||
videoTrack = mp4Video.AddVideoTrack("H265")
|
||||
}
|
||||
if audioCodec == "AAC" {
|
||||
audioTrack = mp4Video.AddAudioTrack("AAC")
|
||||
} else if audioCodec == "PCM_MULAW" {
|
||||
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
|
||||
}
|
||||
audioWriter = newRecordingAudioWriter(mp4Video, audioCodec, config.Capture.IPCamera.SampleRate, config.Capture.IPCamera.Channels, audioBitDepth, "capture.main.HandleRecordStream(continuous)")
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
@@ -317,15 +348,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.IsAudio {
|
||||
if pkt.Codec == "AAC" {
|
||||
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.Codec == "PCM_MULAW" {
|
||||
// TODO: transcode to AAC, some work to do..
|
||||
// We might need to use ffmpeg to transcode the audio to AAC.
|
||||
// For now we will skip the audio track.
|
||||
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
|
||||
if err := audioWriter.WritePacket(pkt); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
}
|
||||
recordingStatus = "started"
|
||||
@@ -339,13 +363,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.IsAudio {
|
||||
if pkt.Codec == "AAC" {
|
||||
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
} else if pkt.Codec == "PCM_MULAW" {
|
||||
// TODO: transcode to AAC, some work to do..
|
||||
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
|
||||
if err := audioWriter.WritePacket(pkt); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +374,10 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// We might have interrupted the recording while restarting the agent.
|
||||
// If this happens we need to check to properly close the recording.
|
||||
if cursorError != nil {
|
||||
if audioWriter != nil {
|
||||
audioWriter.Close()
|
||||
audioWriter = nil
|
||||
}
|
||||
if recordingStatus == "started" {
|
||||
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): Recording finished: file save: " + name)
|
||||
@@ -434,7 +457,6 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
var displayTime int64 = 0 // display time in milliseconds
|
||||
|
||||
var videoTrack uint32
|
||||
var audioTrack uint32
|
||||
|
||||
for motion := range communication.HandleMotion {
|
||||
|
||||
@@ -448,6 +470,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
motionTimestamp := now
|
||||
|
||||
start := false
|
||||
rolloverRequested := false
|
||||
rolloverMaxLogged := false
|
||||
|
||||
if cursorError == nil {
|
||||
pkt, cursorError = recordingCursor.ReadPacket()
|
||||
@@ -520,6 +544,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
}
|
||||
// Create the MP4 only once the first keyframe arrives.
|
||||
var mp4Video *video.MP4
|
||||
var audioWriter *recordingAudioWriter
|
||||
|
||||
for cursorError == nil {
|
||||
|
||||
@@ -528,20 +553,33 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + cursorError.Error())
|
||||
}
|
||||
|
||||
now = time.Now().UnixMilli()
|
||||
select {
|
||||
case motion := <-communication.HandleMotion:
|
||||
motionTimestamp = now
|
||||
motionTimestamp = pkt.CurrentTime
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): motion detected while recording. Expanding recording.")
|
||||
numberOfChanges := motion.NumberOfChanges
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
|
||||
default:
|
||||
}
|
||||
|
||||
if start && (motionTimestamp+postRecording-now < 0 || now-startRecording > maxRecordingPeriod-500) && nextPkt.IsKeyFrame {
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): timestamp+postRecording-now < 0 - " + strconv.FormatInt(motionTimestamp+postRecording-now, 10) + " < 0")
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): now-startRecording > maxRecordingPeriod-500 - " + strconv.FormatInt(now-startRecording, 10) + " > " + strconv.FormatInt(maxRecordingPeriod-500, 10))
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(motionTimestamp, 10) + ", postRecording: " + strconv.FormatInt(postRecording, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
|
||||
packetTime := pkt.CurrentTime
|
||||
hardMaxReached := packetTime-startRecording > maxRecordingPeriod-500
|
||||
postRecordingElapsed := motionTimestamp+postRecording-packetTime < 0
|
||||
if start && (postRecordingElapsed || hardMaxReached) {
|
||||
rolloverRequested = true
|
||||
if hardMaxReached && !rolloverMaxLogged {
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): max recording period reached, waiting for next keyframe to close without dropping frames")
|
||||
rolloverMaxLogged = true
|
||||
}
|
||||
}
|
||||
|
||||
closeOnCurrentKeyframe := rolloverRequested && pkt.IsKeyFrame && pkt.CurrentTime > startRecording
|
||||
closeOnNextKeyframe := rolloverRequested && nextPkt.IsKeyFrame
|
||||
|
||||
if start && (closeOnCurrentKeyframe || closeOnNextKeyframe) {
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): timestamp+postRecording-packetTime < 0 - " + strconv.FormatInt(motionTimestamp+postRecording-packetTime, 10) + " < 0")
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): packetTime-startRecording > maxRecordingPeriod-500 - " + strconv.FormatInt(packetTime-startRecording, 10) + " > " + strconv.FormatInt(maxRecordingPeriod-500, 10))
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(motionTimestamp, 10) + ", postRecording: " + strconv.FormatInt(postRecording, 10) + ", packetTime: " + strconv.FormatInt(packetTime, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
|
||||
break
|
||||
}
|
||||
if pkt.IsKeyFrame && !start && pkt.CurrentTime >= startRecording {
|
||||
@@ -550,7 +588,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): write frames")
|
||||
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): recording started on keyframe")
|
||||
|
||||
// Align duration timers with the first keyframe.
|
||||
// Align duration timers with the first keyframe so audio/video start together.
|
||||
startRecording = pkt.CurrentTime
|
||||
|
||||
// Create a video file, and set the dimensions.
|
||||
@@ -563,11 +601,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
} else if videoCodec == "H265" {
|
||||
videoTrack = mp4Video.AddVideoTrack("H265")
|
||||
}
|
||||
if audioCodec == "AAC" {
|
||||
audioTrack = mp4Video.AddAudioTrack("AAC")
|
||||
} else if audioCodec == "PCM_MULAW" {
|
||||
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
|
||||
}
|
||||
audioWriter = newRecordingAudioWriter(mp4Video, audioCodec, config.Capture.IPCamera.SampleRate, config.Capture.IPCamera.Channels, audioBitDepth, "capture.main.HandleRecordStream(motiondetection)")
|
||||
start = true
|
||||
}
|
||||
if start {
|
||||
@@ -581,17 +615,10 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
}
|
||||
} else if pkt.IsAudio {
|
||||
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): add audio sample")
|
||||
if pkt.Codec == "AAC" {
|
||||
if mp4Video != nil {
|
||||
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
|
||||
}
|
||||
if mp4Video != nil {
|
||||
if err := audioWriter.WritePacket(pkt); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
|
||||
}
|
||||
} else if pkt.Codec == "PCM_MULAW" {
|
||||
// TODO: transcode to AAC, some work to do..
|
||||
// We might need to use ffmpeg to transcode the audio to AAC.
|
||||
// For now we will skip the audio track.
|
||||
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): no AAC audio codec detected, skipping audio track.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,10 +631,18 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
lastRecordingTime = pkt.CurrentTime
|
||||
|
||||
if mp4Video == nil {
|
||||
if audioWriter != nil {
|
||||
audioWriter.Close()
|
||||
}
|
||||
log.Log.Warning("capture.main.HandleRecordStream(motiondetection): recording closed without keyframe; no MP4 created")
|
||||
continue
|
||||
}
|
||||
|
||||
if err := audioWriter.Flush(); err != nil {
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
|
||||
}
|
||||
audioWriter.Close()
|
||||
|
||||
// This will close the recording and write the last packet.
|
||||
if len(mp4Video.SPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.SPSNALUs) > 0 {
|
||||
mp4Video.SPSNALUs = configuration.Config.Capture.IPCamera.SPSNALUs
|
||||
|
||||
566
machinery/src/capture/pcmu_to_aac_transcoder.go
Normal file
566
machinery/src/capture/pcmu_to_aac_transcoder.go
Normal file
@@ -0,0 +1,566 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/packets"
|
||||
"github.com/kerberos-io/agent/machinery/src/video"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPCMUSampleRate = 8000
|
||||
defaultPCMUChannels = 1
|
||||
)
|
||||
|
||||
type audioToAACTranscoder interface {
|
||||
Transcode([]byte) ([]byte, error)
|
||||
Flush() ([]byte, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
func PCMUToAACTranscodingAvailable() bool {
|
||||
_, err := exec.LookPath("ffmpeg")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type ffmpegToAACTranscoder struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderr *bytes.Buffer
|
||||
|
||||
mu sync.Mutex
|
||||
outMu sync.Mutex
|
||||
outBuf bytes.Buffer
|
||||
adtsBuf []byte
|
||||
closed bool
|
||||
stdinClosed bool
|
||||
closeOnce sync.Once
|
||||
stdoutDone chan struct{}
|
||||
waitDone chan struct{}
|
||||
waitErr error
|
||||
}
|
||||
|
||||
func newFFmpegToAACTranscoder(inputFormat string, sampleRate int, channels int) (*ffmpegToAACTranscoder, error) {
|
||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
return nil, errors.New("audio to AAC transcoding not available: ffmpeg binary not found in PATH")
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
ffmpegPath,
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-fflags", "+nobuffer",
|
||||
"-flags", "low_delay",
|
||||
"-f", inputFormat,
|
||||
"-ar", intToString(sampleRate),
|
||||
"-ac", intToString(channels),
|
||||
"-i", "pipe:0",
|
||||
"-vn",
|
||||
"-ac", "1",
|
||||
"-ar", intToString(sampleRate),
|
||||
"-c:a", "aac",
|
||||
"-profile:a", "aac_low",
|
||||
"-b:a", "32k",
|
||||
"-f", "adts",
|
||||
"pipe:1",
|
||||
)
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stderr := &bytes.Buffer{}
|
||||
cmd.Stderr = stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &ffmpegToAACTranscoder{
|
||||
cmd: cmd,
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
stdoutDone: make(chan struct{}),
|
||||
waitDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(t.stdoutDone)
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, readErr := stdout.Read(buf)
|
||||
if n > 0 {
|
||||
t.outMu.Lock()
|
||||
_, _ = t.outBuf.Write(buf[:n])
|
||||
t.outMu.Unlock()
|
||||
}
|
||||
if readErr != nil {
|
||||
if readErr != io.EOF {
|
||||
log.Log.Warning("capture.pcmu_to_aac: stdout reader stopped: " + readErr.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
t.waitErr = cmd.Wait()
|
||||
close(t.waitDone)
|
||||
}()
|
||||
|
||||
log.Log.Info("capture.audio_to_aac: " + strings.ToUpper(inputFormat) + " -> AAC transcoder initialised (ffmpeg process)")
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func NewPCMUToAACTranscoder(sampleRate int, channels int) (*ffmpegToAACTranscoder, error) {
|
||||
if sampleRate <= 0 {
|
||||
sampleRate = defaultPCMUSampleRate
|
||||
}
|
||||
if channels <= 0 {
|
||||
channels = defaultPCMUChannels
|
||||
}
|
||||
|
||||
return newFFmpegToAACTranscoder("mulaw", sampleRate, channels)
|
||||
}
|
||||
|
||||
func NewLPCMToAACTranscoder(sampleRate int, channels int, bitDepth int) (*ffmpegToAACTranscoder, error) {
|
||||
inputFormat, err := lpcmFFmpegInputFormat(bitDepth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sampleRate <= 0 {
|
||||
return nil, errors.New("LPCM to AAC transcoding requires a valid sample rate")
|
||||
}
|
||||
if channels <= 0 {
|
||||
return nil, errors.New("LPCM to AAC transcoding requires a valid channel count")
|
||||
}
|
||||
|
||||
return newFFmpegToAACTranscoder(inputFormat, sampleRate, channels)
|
||||
}
|
||||
|
||||
func (t *ffmpegToAACTranscoder) Transcode(input []byte) ([]byte, error) {
|
||||
if t == nil || len(input) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.closed {
|
||||
return nil, errors.New("audio to AAC transcoder is closed")
|
||||
}
|
||||
if t.stdinClosed {
|
||||
return nil, errors.New("audio to AAC transcoder input is closed")
|
||||
}
|
||||
|
||||
if _, err := t.stdin.Write(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do not block the recording loop waiting for the encoder to emit output.
|
||||
// FFmpeg can buffer for a while, and polling here per RTP packet causes the
|
||||
// recorder to fall behind real time and drop trailing media before close.
|
||||
return t.readAvailable(), nil
|
||||
}
|
||||
|
||||
func (t *ffmpegToAACTranscoder) Flush() ([]byte, error) {
|
||||
if t == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
if t.closed {
|
||||
defer t.mu.Unlock()
|
||||
return t.readAvailable(), nil
|
||||
}
|
||||
if !t.stdinClosed && t.stdin != nil {
|
||||
if err := t.stdin.Close(); err != nil {
|
||||
t.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
t.stdinClosed = true
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
processExited := false
|
||||
readerFinished := false
|
||||
deadline := time.Now().Add(15 * time.Second)
|
||||
timedOut := false
|
||||
for !processExited || !readerFinished {
|
||||
if !processExited {
|
||||
select {
|
||||
case <-t.waitDone:
|
||||
processExited = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
if !readerFinished {
|
||||
select {
|
||||
case <-t.stdoutDone:
|
||||
readerFinished = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if processExited && readerFinished {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
timedOut = true
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(15 * time.Millisecond)
|
||||
}
|
||||
|
||||
if timedOut {
|
||||
log.Log.Warning("capture.audio_to_aac: flush timed out before ffmpeg fully drained (process_exited=" + strconv.FormatBool(processExited) + ", stdout_done=" + strconv.FormatBool(readerFinished) + ", buffered=" + intToString(t.bufferedLen()) + ")")
|
||||
}
|
||||
|
||||
if processExited && t.waitErr != nil {
|
||||
return t.readAvailable(), t.waitErr
|
||||
}
|
||||
|
||||
return t.readAvailable(), nil
|
||||
}
|
||||
|
||||
func (t *ffmpegToAACTranscoder) Close() {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
t.closeOnce.Do(func() {
|
||||
t.mu.Lock()
|
||||
t.closed = true
|
||||
if t.stdin != nil && !t.stdinClosed {
|
||||
_ = t.stdin.Close()
|
||||
t.stdinClosed = true
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
processExited := false
|
||||
waitTimeout := 250 * time.Millisecond
|
||||
if t.stdinClosed {
|
||||
waitTimeout = 2 * time.Second
|
||||
}
|
||||
select {
|
||||
case <-t.waitDone:
|
||||
processExited = true
|
||||
case <-time.After(waitTimeout):
|
||||
}
|
||||
|
||||
if !processExited {
|
||||
if t.stdout != nil {
|
||||
_ = t.stdout.Close()
|
||||
}
|
||||
if t.cmd != nil && t.cmd.Process != nil {
|
||||
_ = t.cmd.Process.Kill()
|
||||
<-t.waitDone
|
||||
}
|
||||
}
|
||||
|
||||
if stderr := t.stderrString(); stderr != "" {
|
||||
log.Log.Info("capture.audio_to_aac: ffmpeg stderr on close: " + stderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ffmpegToAACTranscoder) readAvailable() []byte {
|
||||
t.outMu.Lock()
|
||||
defer t.outMu.Unlock()
|
||||
|
||||
if t.outBuf.Len() > 0 {
|
||||
t.adtsBuf = append(t.adtsBuf, t.outBuf.Bytes()...)
|
||||
t.outBuf.Reset()
|
||||
}
|
||||
|
||||
return drainCompleteADTSFrames(&t.adtsBuf)
|
||||
}
|
||||
|
||||
func (t *ffmpegToAACTranscoder) bufferedLen() int {
|
||||
t.outMu.Lock()
|
||||
defer t.outMu.Unlock()
|
||||
return t.outBuf.Len()
|
||||
}
|
||||
|
||||
func (t *ffmpegToAACTranscoder) stderrString() string {
|
||||
if t == nil || t.stderr == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(t.stderr.String())
|
||||
}
|
||||
|
||||
func lpcmFFmpegInputFormat(bitDepth int) (string, error) {
|
||||
switch bitDepth {
|
||||
case 8:
|
||||
return "u8", nil
|
||||
case 16:
|
||||
return "s16be", nil
|
||||
case 24:
|
||||
return "s24be", nil
|
||||
default:
|
||||
return "", errors.New("unsupported LPCM bit depth: " + intToString(bitDepth))
|
||||
}
|
||||
}
|
||||
|
||||
type recordingAudioWriter struct {
|
||||
mp4 *video.MP4
|
||||
trackID uint32
|
||||
transcoder audioToAACTranscoder
|
||||
lastPTS uint64
|
||||
logPrefix string
|
||||
|
||||
transcodedSampleRate int
|
||||
aacBasePTS uint64
|
||||
aacFrameCursor uint64
|
||||
aacClockStarted bool
|
||||
loggedAACParams bool
|
||||
}
|
||||
|
||||
func newRecordingAudioWriter(mp4Video *video.MP4, audioCodec string, sampleRate int, channels int, bitDepth int, logPrefix string) *recordingAudioWriter {
|
||||
writer := &recordingAudioWriter{
|
||||
mp4: mp4Video,
|
||||
logPrefix: logPrefix,
|
||||
}
|
||||
|
||||
switch audioCodec {
|
||||
case "AAC":
|
||||
writer.trackID = mp4Video.AddAudioTrack("AAC")
|
||||
case "PCM_MULAW":
|
||||
if sampleRate <= 0 {
|
||||
sampleRate = defaultPCMUSampleRate
|
||||
}
|
||||
if channels <= 0 {
|
||||
channels = defaultPCMUChannels
|
||||
}
|
||||
|
||||
if !PCMUToAACTranscodingAvailable() {
|
||||
log.Log.Warning(logPrefix + ": ffmpeg not available, skipping PCM_MULAW audio recording.")
|
||||
return writer
|
||||
}
|
||||
|
||||
transcoder, err := NewPCMUToAACTranscoder(sampleRate, channels)
|
||||
if err != nil {
|
||||
log.Log.Error(logPrefix + ": failed to create PCM_MULAW to AAC transcoder: " + err.Error())
|
||||
return writer
|
||||
}
|
||||
|
||||
writer.trackID = mp4Video.AddAudioTrack("AAC")
|
||||
writer.transcoder = transcoder
|
||||
writer.transcodedSampleRate = sampleRate
|
||||
log.Log.Info(logPrefix + ": recording PCM_MULAW audio as AAC (input_rate=" + intToString(sampleRate) + ", channels=" + intToString(channels) + ").")
|
||||
case "LPCM":
|
||||
transcoder, err := NewLPCMToAACTranscoder(sampleRate, channels, bitDepth)
|
||||
if err != nil {
|
||||
log.Log.Error(logPrefix + ": failed to create LPCM to AAC transcoder: " + err.Error())
|
||||
return writer
|
||||
}
|
||||
|
||||
writer.trackID = mp4Video.AddAudioTrack("AAC")
|
||||
writer.transcoder = transcoder
|
||||
writer.transcodedSampleRate = sampleRate
|
||||
log.Log.Info(logPrefix + ": recording LPCM audio as AAC (input_rate=" + intToString(sampleRate) + ", channels=" + intToString(channels) + ", bit_depth=" + intToString(bitDepth) + ").")
|
||||
}
|
||||
|
||||
return writer
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) TrackID() uint32 {
|
||||
if w == nil {
|
||||
return 0
|
||||
}
|
||||
return w.trackID
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) WritePacket(pkt packets.Packet) error {
|
||||
if w == nil || w.mp4 == nil || !pkt.IsAudio || w.trackID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pts > 0 {
|
||||
w.lastPTS = pts
|
||||
}
|
||||
|
||||
switch pkt.Codec {
|
||||
case "AAC":
|
||||
return w.mp4.AddSampleToTrack(w.trackID, pkt.IsKeyFrame, pkt.Data, pts)
|
||||
case "PCM_MULAW", "LPCM":
|
||||
if w.transcoder == nil {
|
||||
return nil
|
||||
}
|
||||
if !w.aacClockStarted {
|
||||
w.aacBasePTS = pts
|
||||
w.aacClockStarted = true
|
||||
}
|
||||
|
||||
adts, err := w.transcoder.Transcode(pkt.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(adts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.writeTranscodedADTS(adts)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) Flush() error {
|
||||
if w == nil || w.transcoder == nil || w.mp4 == nil || w.trackID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
adts, err := w.transcoder.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(adts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.writeTranscodedADTS(adts)
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) Close() {
|
||||
if w != nil && w.transcoder != nil {
|
||||
w.transcoder.Close()
|
||||
w.transcoder = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) writeTranscodedADTS(adts []byte) error {
|
||||
if w == nil || w.mp4 == nil || w.trackID == 0 || len(adts) == 0 {
|
||||
return nil
|
||||
}
|
||||
if w.transcodedSampleRate <= 0 {
|
||||
return errors.New("transcoded AAC sample rate is not set")
|
||||
}
|
||||
|
||||
var writeErr error
|
||||
video.SplitAACFrame(adts, func(started bool, aac []byte) {
|
||||
if writeErr != nil || len(aac) < 7 {
|
||||
return
|
||||
}
|
||||
|
||||
if !w.loggedAACParams {
|
||||
log.Log.Info(w.logPrefix + ": first AAC frame parameters (aac_rate=" + intToString(int(video.AACSampleRateFromADTS(aac))) + ", channels=" + intToString(int(video.AACChannelCountFromADTS(aac))) + ", samples_per_frame=" + intToString(aacSamplesPerFrame(aac)) + ").")
|
||||
w.loggedAACParams = true
|
||||
}
|
||||
|
||||
pts := w.transcodedPTS()
|
||||
if err := w.mp4.AddSampleToTrack(w.trackID, false, aac, pts); err != nil {
|
||||
writeErr = err
|
||||
return
|
||||
}
|
||||
|
||||
w.lastPTS = pts
|
||||
w.aacFrameCursor += uint64(aacSamplesPerFrame(aac))
|
||||
})
|
||||
|
||||
return writeErr
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) transcodedPTS() uint64 {
|
||||
if w == nil {
|
||||
return 0
|
||||
}
|
||||
if !w.aacClockStarted {
|
||||
w.aacClockStarted = true
|
||||
}
|
||||
return w.aacBasePTS + (w.aacFrameCursor*1000)/uint64(w.transcodedSampleRate)
|
||||
}
|
||||
|
||||
func aacSamplesPerFrame(aac []byte) int {
|
||||
if len(aac) < 7 {
|
||||
return 1024
|
||||
}
|
||||
rawBlocks := int(aac[6]&0x03) + 1
|
||||
return rawBlocks * 1024
|
||||
}
|
||||
|
||||
func intToString(v int) string {
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
|
||||
func drainCompleteADTSFrames(buffer *[]byte) []byte {
|
||||
if buffer == nil || len(*buffer) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := *buffer
|
||||
start := video.FindSyncword(data, 0)
|
||||
if start < 0 {
|
||||
// Keep the tail in case the syncword is split across reads.
|
||||
if len(data) > 1 {
|
||||
*buffer = append([]byte{}, data[len(data)-1:]...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if start > 0 {
|
||||
data = data[start:]
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
offset := 0
|
||||
for {
|
||||
if len(data[offset:]) < 7 {
|
||||
break
|
||||
}
|
||||
|
||||
var adts video.ADTS_Frame_Header
|
||||
adts.Decode(data[offset:])
|
||||
frameLen := int(adts.Variable_Header.Frame_length)
|
||||
if frameLen < 7 {
|
||||
next := video.FindSyncword(data, offset+1)
|
||||
if next < 0 {
|
||||
break
|
||||
}
|
||||
offset = next
|
||||
continue
|
||||
}
|
||||
if offset+frameLen > len(data) {
|
||||
break
|
||||
}
|
||||
|
||||
_, _ = out.Write(data[offset : offset+frameLen])
|
||||
offset += frameLen
|
||||
|
||||
next := video.FindSyncword(data, offset)
|
||||
if next < 0 {
|
||||
break
|
||||
}
|
||||
if next > offset {
|
||||
offset = next
|
||||
}
|
||||
}
|
||||
|
||||
if offset < len(data) {
|
||||
*buffer = append([]byte{}, data[offset:]...)
|
||||
} else {
|
||||
*buffer = nil
|
||||
}
|
||||
|
||||
if out.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
@@ -49,6 +49,9 @@ type Stream struct {
|
||||
// Channels is the number of audio channels.
|
||||
Channels int
|
||||
|
||||
// BitDepth is the number of bits per audio sample for PCM-based streams.
|
||||
BitDepth int
|
||||
|
||||
// GopSize is the size of the GOP (Group of Pictures).
|
||||
GopSize int
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Eyevinn/mp4ff/aac"
|
||||
"github.com/Eyevinn/mp4ff/avc"
|
||||
mp4ff "github.com/Eyevinn/mp4ff/mp4"
|
||||
"github.com/kerberos-io/agent/machinery/src/encryption"
|
||||
@@ -46,9 +47,12 @@ type MP4 struct {
|
||||
SegmentCount int
|
||||
SampleCount int
|
||||
StartPTS uint64
|
||||
MediaStartPTS uint64
|
||||
VideoTotalDuration uint64
|
||||
AudioTotalDuration uint64
|
||||
AudioPTS uint64
|
||||
AudioSampleRate uint32
|
||||
AudioChannels uint16
|
||||
Start bool
|
||||
SPSNALUs [][]byte // SPS NALUs for H264
|
||||
PPSNALUs [][]byte // PPS NALUs for H264
|
||||
@@ -266,6 +270,49 @@ func (mp4 *MP4) flushPendingVideoSample(nextPTS uint64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (mp4 *MP4) alignAudioTimeline(rawPTS uint64) {
|
||||
if mp4 == nil || mp4.AudioSampleRate == 0 || mp4.AudioPTS != 0 {
|
||||
return
|
||||
}
|
||||
if rawPTS <= mp4.MediaStartPTS {
|
||||
return
|
||||
}
|
||||
|
||||
offsetMs := rawPTS - mp4.MediaStartPTS
|
||||
mp4.AudioPTS = (offsetMs*uint64(mp4.AudioSampleRate) + 500) / 1000
|
||||
}
|
||||
|
||||
func (mp4 *MP4) appendPendingAudioSample(trackID uint32) {
|
||||
if mp4 == nil || mp4.AudioFullSample == nil {
|
||||
return
|
||||
}
|
||||
|
||||
SplitAACFrame(mp4.AudioFullSample.Data, func(started bool, aac []byte) {
|
||||
sampleToAdd := *mp4.AudioFullSample
|
||||
dts := aacFrameDurationSamples(aac)
|
||||
if dts == 0 {
|
||||
dts = mp4.LastAudioSampleDTS
|
||||
if dts == 0 {
|
||||
dts = 1024
|
||||
}
|
||||
}
|
||||
|
||||
mp4.alignAudioTimeline(sampleToAdd.DecodeTime)
|
||||
mp4.LastAudioSampleDTS = dts
|
||||
sampleToAdd.Data = aac[7:]
|
||||
sampleToAdd.DecodeTime = mp4.AudioPTS
|
||||
sampleToAdd.Sample.Dur = uint32(dts)
|
||||
sampleToAdd.Sample.Size = uint32(len(aac[7:]))
|
||||
mp4.AudioTotalDuration += dts
|
||||
mp4.AudioPTS += dts
|
||||
|
||||
err := mp4.MultiTrackFragment.AddFullSampleToTrack(sampleToAdd, trackID)
|
||||
if err != nil {
|
||||
log.Log.Error("mp4.appendPendingAudioSample(): error adding sample to track " + fmt.Sprintf("%d: %v", trackID, err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, pts uint64) error {
|
||||
|
||||
if isKeyframe && trackID == uint32(mp4.VideoTrack) {
|
||||
@@ -320,6 +367,9 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
|
||||
// Increment the segment count
|
||||
mp4.SegmentCount = mp4.SegmentCount + 1
|
||||
if mp4.MediaStartPTS == 0 {
|
||||
mp4.MediaStartPTS = pts
|
||||
}
|
||||
|
||||
// Create a new media segment
|
||||
seg := mp4ff.NewMediaSegment()
|
||||
@@ -383,29 +433,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
}
|
||||
} else if trackID == uint32(mp4.AudioTrack) {
|
||||
if mp4.AudioFullSample != nil {
|
||||
SplitAACFrame(mp4.AudioFullSample.Data, func(started bool, aac []byte) {
|
||||
sampleToAdd := *mp4.AudioFullSample
|
||||
dts := pts - mp4.AudioFullSample.DecodeTime
|
||||
if pts < mp4.AudioFullSample.DecodeTime {
|
||||
//log.Printf("Warning: PTS %d is less than previous sample's DecodeTime %d, resetting AudioFullSample", pts, mp4.AudioFullSample.DecodeTime)
|
||||
dts = 1
|
||||
}
|
||||
if started {
|
||||
dts = 1
|
||||
}
|
||||
mp4.LastAudioSampleDTS = dts
|
||||
//fmt.Printf("Adding sample to track %d, PTS: %d, Duration: %d, size: %d\n", trackID, pts, dts, len(aac[7:]))
|
||||
mp4.AudioTotalDuration += dts
|
||||
mp4.AudioPTS += dts
|
||||
sampleToAdd.Data = aac[7:] // Remove the ADTS header (first 7 bytes)
|
||||
sampleToAdd.DecodeTime = mp4.AudioPTS - dts
|
||||
sampleToAdd.Sample.Dur = uint32(dts)
|
||||
sampleToAdd.Sample.Size = uint32(len(aac[7:]))
|
||||
err := mp4.MultiTrackFragment.AddFullSampleToTrack(sampleToAdd, trackID)
|
||||
if err != nil {
|
||||
log.Log.Error("mp4.AddSampleToTrack(): error adding sample to track " + fmt.Sprintf("%d: %v", trackID, err))
|
||||
}
|
||||
})
|
||||
mp4.appendPendingAudioSample(trackID)
|
||||
}
|
||||
|
||||
// Set the sample data
|
||||
@@ -418,6 +446,12 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
Flags: 0,
|
||||
CompositionTimeOffset: 0, // No composition time offset for audio
|
||||
}
|
||||
if mp4.AudioSampleRate == 0 {
|
||||
mp4.AudioSampleRate = aacSampleRate(data)
|
||||
}
|
||||
if mp4.AudioChannels == 0 {
|
||||
mp4.AudioChannels = aacChannelCount(data)
|
||||
}
|
||||
mp4.AudioFullSample = &fullSample
|
||||
mp4.SampleType = "audio"
|
||||
}
|
||||
@@ -447,23 +481,7 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
|
||||
// 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.appendPendingAudioSample(uint32(mp4.AudioTrack))
|
||||
mp4.AudioFullSample = nil
|
||||
}
|
||||
}
|
||||
@@ -526,15 +544,26 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
// 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)
|
||||
audioTimescale := mp4.AudioSampleRate
|
||||
if audioTimescale == 0 {
|
||||
if config.Capture.IPCamera.SampleRate > 0 {
|
||||
audioTimescale = uint32(config.Capture.IPCamera.SampleRate)
|
||||
} else {
|
||||
audioTimescale = uint32(1000)
|
||||
}
|
||||
}
|
||||
macTime := mp4.StartTime + MacEpochOffset
|
||||
nextTrackID := uint32(len(mp4.TrackIDs) + 1)
|
||||
audioMovieDuration := mp4.AudioTotalDuration
|
||||
if audioTimescale != 0 && audioTimescale != videoTimescale {
|
||||
audioMovieDuration = (mp4.AudioTotalDuration*uint64(videoTimescale) + uint64(audioTimescale/2)) / uint64(audioTimescale)
|
||||
}
|
||||
|
||||
// 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
|
||||
if audioMovieDuration > movDuration {
|
||||
movDuration = audioMovieDuration
|
||||
}
|
||||
|
||||
mvhd := &mp4ff.MvhdBox{
|
||||
@@ -609,16 +638,23 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
// Add an audio track to the moov box
|
||||
init.AddEmptyTrack(audioTimescale, "audio", "und")
|
||||
|
||||
// Check if the same sample rate is set, otherwise we default to 48000
|
||||
audioSampleRate := 48000
|
||||
if config.Capture.IPCamera.SampleRate > 0 {
|
||||
audioSampleRate = config.Capture.IPCamera.SampleRate
|
||||
audioSampleRate := int(audioTimescale)
|
||||
if audioSampleRate == 0 {
|
||||
audioSampleRate = 48000
|
||||
}
|
||||
// Set the audio descriptor
|
||||
err := init.Moov.Traks[1].SetAACDescriptor(29, audioSampleRate)
|
||||
audioChannels := mp4.AudioChannels
|
||||
if audioChannels == 0 {
|
||||
if config.Capture.IPCamera.Channels > 0 {
|
||||
audioChannels = uint16(config.Capture.IPCamera.Channels)
|
||||
} else {
|
||||
audioChannels = 1
|
||||
}
|
||||
}
|
||||
// Set the audio descriptor to match the AAC-LC stream actually produced by ffmpeg/camera.
|
||||
err := setAACLCDescriptor(init.Moov.Traks[1], audioSampleRate, audioChannels)
|
||||
if err != nil {
|
||||
}
|
||||
init.Moov.Traks[1].Tkhd.Duration = mp4.AudioTotalDuration
|
||||
init.Moov.Traks[1].Tkhd.Duration = audioMovieDuration
|
||||
init.Moov.Traks[1].Tkhd.CreationTime = macTime
|
||||
init.Moov.Traks[1].Tkhd.ModificationTime = macTime
|
||||
init.Moov.Traks[1].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
@@ -1261,7 +1297,7 @@ func (frame *ADTS_Frame_Header) Decode(aac []byte) {
|
||||
frame.Fix_Header.Profile = aac[2] >> 6 & 0x03
|
||||
frame.Fix_Header.Sampling_frequency_index = aac[2] >> 2 & 0x0F
|
||||
frame.Fix_Header.Private_bit = aac[2] >> 1 & 0x01
|
||||
frame.Fix_Header.Channel_configuration = (aac[2] & 0x01 << 2) | (aac[3] >> 6)
|
||||
frame.Fix_Header.Channel_configuration = ((aac[2] & 0x01) << 2) | (aac[3] >> 6)
|
||||
frame.Fix_Header.Originalorcopy = aac[3] >> 5 & 0x01
|
||||
frame.Fix_Header.Home = aac[3] >> 4 & 0x01
|
||||
frame.Variable_Header.Copyright_identification_bit = aac[3] >> 3 & 0x01
|
||||
@@ -1303,6 +1339,89 @@ func AACSampleIdxToSample(idx int) int {
|
||||
return AAC_Sampling_Idx[idx]
|
||||
}
|
||||
|
||||
func aacFrameDurationSamples(aac []byte) uint64 {
|
||||
if len(aac) < 7 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var header ADTS_Frame_Header
|
||||
header.Decode(aac)
|
||||
|
||||
rawBlocks := uint64(header.Variable_Header.Number_of_raw_data_blocks_in_frame) + 1
|
||||
return rawBlocks * 1024
|
||||
}
|
||||
|
||||
func aacSampleRate(aac []byte) uint32 {
|
||||
if len(aac) < 7 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var header ADTS_Frame_Header
|
||||
header.Decode(aac)
|
||||
|
||||
sampleRateIdx := int(header.Fix_Header.Sampling_frequency_index)
|
||||
if sampleRateIdx < 0 || sampleRateIdx >= len(AAC_Sampling_Idx) {
|
||||
return 0
|
||||
}
|
||||
|
||||
sampleRate := AACSampleIdxToSample(sampleRateIdx)
|
||||
if sampleRate <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint32(sampleRate)
|
||||
}
|
||||
|
||||
func aacChannelCount(aacBytes []byte) uint16 {
|
||||
if len(aacBytes) < 7 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var header ADTS_Frame_Header
|
||||
header.Decode(aacBytes)
|
||||
if header.Fix_Header.Channel_configuration == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return uint16(header.Fix_Header.Channel_configuration)
|
||||
}
|
||||
|
||||
func AACSampleRateFromADTS(aac []byte) uint32 {
|
||||
return aacSampleRate(aac)
|
||||
}
|
||||
|
||||
func AACChannelCountFromADTS(aac []byte) uint16 {
|
||||
return aacChannelCount(aac)
|
||||
}
|
||||
|
||||
func setAACLCDescriptor(trak *mp4ff.TrakBox, sampleRate int, channels uint16) error {
|
||||
if trak == nil {
|
||||
return errors.New("nil trak for AAC descriptor")
|
||||
}
|
||||
if sampleRate <= 0 {
|
||||
return errors.New("invalid AAC sample rate")
|
||||
}
|
||||
if channels == 0 {
|
||||
channels = 1
|
||||
}
|
||||
|
||||
asc := &aac.AudioSpecificConfig{
|
||||
ObjectType: aac.AAClc,
|
||||
ChannelConfiguration: byte(channels),
|
||||
SamplingFrequency: sampleRate,
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := asc.Encode(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
esds := mp4ff.CreateEsdsBox(buf.Bytes())
|
||||
mp4a := mp4ff.CreateAudioSampleEntryBox("mp4a", channels, 16, uint16(sampleRate), esds)
|
||||
trak.Mdia.Minf.Stbl.Stsd.AddChild(mp4a)
|
||||
return nil
|
||||
}
|
||||
|
||||
// +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
// | audio object type(5 bits) | sampling frequency index(4 bits) | channel configuration(4 bits) | GA framelength flag(1 bits) | GA Depends on core coder(1 bits) | GA Extension Flag(1 bits) |
|
||||
// +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Reference in New Issue
Block a user