mirror of
https://github.com/kerberos-io/agent.git
synced 2026-08-23 15:08:32 +00:00
Add LPCM audio support and enhance PCM_MULAW to AAC transcoding
This commit is contained in:
@@ -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)
|
||||
@@ -525,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) {
|
||||
@@ -1263,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
|
||||
@@ -112,6 +130,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): start recording")
|
||||
|
||||
start := false
|
||||
rolloverRequested := false
|
||||
rolloverMaxLogged := false
|
||||
|
||||
// If continuous record the full length
|
||||
postRecording = maxRecordingPeriod
|
||||
@@ -136,9 +156,18 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
nextPkt, cursorError = recordingCursor.ReadPacket()
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
hardMaxReached := now-startRecording > maxRecordingPeriod-500
|
||||
postRecordingElapsed := startRecording+postRecording-now <= 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) {
|
||||
rolloverRequested && nextPkt.IsKeyFrame {
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
@@ -177,6 +206,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,7 +336,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
} else if videoCodec == "H265" {
|
||||
videoTrack = mp4Video.AddVideoTrack("H265")
|
||||
}
|
||||
audioWriter = newRecordingAudioWriter(mp4Video, audioCodec, config.Capture.IPCamera.SampleRate, config.Capture.IPCamera.Channels, "capture.main.HandleRecordStream(continuous)")
|
||||
audioWriter = newRecordingAudioWriter(mp4Video, audioCodec, config.Capture.IPCamera.SampleRate, config.Capture.IPCamera.Channels, audioBitDepth, "capture.main.HandleRecordStream(continuous)")
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
@@ -435,6 +466,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
motionTimestamp := now
|
||||
|
||||
start := false
|
||||
rolloverRequested := false
|
||||
rolloverMaxLogged := false
|
||||
|
||||
if cursorError == nil {
|
||||
pkt, cursorError = recordingCursor.ReadPacket()
|
||||
@@ -526,7 +559,17 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
default:
|
||||
}
|
||||
|
||||
if start && (motionTimestamp+postRecording-now < 0 || now-startRecording > maxRecordingPeriod-500) && nextPkt.IsKeyFrame {
|
||||
hardMaxReached := now-startRecording > maxRecordingPeriod-500
|
||||
postRecordingElapsed := motionTimestamp+postRecording-now < 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
|
||||
}
|
||||
}
|
||||
|
||||
if start && rolloverRequested && 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))
|
||||
@@ -551,7 +594,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
} else if videoCodec == "H265" {
|
||||
videoTrack = mp4Video.AddVideoTrack("H265")
|
||||
}
|
||||
audioWriter = newRecordingAudioWriter(mp4Video, audioCodec, config.Capture.IPCamera.SampleRate, config.Capture.IPCamera.Channels, "capture.main.HandleRecordStream(motiondetection)")
|
||||
audioWriter = newRecordingAudioWriter(mp4Video, audioCodec, config.Capture.IPCamera.SampleRate, config.Capture.IPCamera.Channels, audioBitDepth, "capture.main.HandleRecordStream(motiondetection)")
|
||||
start = true
|
||||
}
|
||||
if start {
|
||||
|
||||
@@ -20,12 +20,18 @@ const (
|
||||
defaultPCMUChannels = 1
|
||||
)
|
||||
|
||||
type audioToAACTranscoder interface {
|
||||
Transcode([]byte) ([]byte, error)
|
||||
Flush() ([]byte, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
func PCMUToAACTranscodingAvailable() bool {
|
||||
_, err := exec.LookPath("ffmpeg")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type PCMUToAACTranscoder struct {
|
||||
type ffmpegToAACTranscoder struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
@@ -34,22 +40,16 @@ type PCMUToAACTranscoder struct {
|
||||
mu sync.Mutex
|
||||
outMu sync.Mutex
|
||||
outBuf bytes.Buffer
|
||||
adtsBuf []byte
|
||||
closed bool
|
||||
stdinClosed bool
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func NewPCMUToAACTranscoder(sampleRate int, channels int) (*PCMUToAACTranscoder, error) {
|
||||
func newFFmpegToAACTranscoder(inputFormat string, sampleRate int, channels int) (*ffmpegToAACTranscoder, error) {
|
||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
return nil, errors.New("PCM_MULAW to AAC transcoding not available: ffmpeg binary not found in PATH")
|
||||
}
|
||||
|
||||
if sampleRate <= 0 {
|
||||
sampleRate = defaultPCMUSampleRate
|
||||
}
|
||||
if channels <= 0 {
|
||||
channels = defaultPCMUChannels
|
||||
return nil, errors.New("audio to AAC transcoding not available: ffmpeg binary not found in PATH")
|
||||
}
|
||||
|
||||
cmd := exec.Command(
|
||||
@@ -58,7 +58,7 @@ func NewPCMUToAACTranscoder(sampleRate int, channels int) (*PCMUToAACTranscoder,
|
||||
"-loglevel", "error",
|
||||
"-fflags", "+nobuffer",
|
||||
"-flags", "low_delay",
|
||||
"-f", "mulaw",
|
||||
"-f", inputFormat,
|
||||
"-ar", intToString(sampleRate),
|
||||
"-ac", intToString(channels),
|
||||
"-i", "pipe:0",
|
||||
@@ -87,7 +87,7 @@ func NewPCMUToAACTranscoder(sampleRate int, channels int) (*PCMUToAACTranscoder,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &PCMUToAACTranscoder{
|
||||
t := &ffmpegToAACTranscoder{
|
||||
cmd: cmd,
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
@@ -112,12 +112,38 @@ func NewPCMUToAACTranscoder(sampleRate int, channels int) (*PCMUToAACTranscoder,
|
||||
}
|
||||
}()
|
||||
|
||||
log.Log.Info("capture.pcmu_to_aac: PCM_MULAW -> AAC transcoder initialised (ffmpeg process)")
|
||||
log.Log.Info("capture.audio_to_aac: " + strings.ToUpper(inputFormat) + " -> AAC transcoder initialised (ffmpeg process)")
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *PCMUToAACTranscoder) Transcode(mulawData []byte) ([]byte, error) {
|
||||
if t == nil || len(mulawData) == 0 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -125,13 +151,13 @@ func (t *PCMUToAACTranscoder) Transcode(mulawData []byte) ([]byte, error) {
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.closed {
|
||||
return nil, errors.New("PCM_MULAW to AAC transcoder is closed")
|
||||
return nil, errors.New("audio to AAC transcoder is closed")
|
||||
}
|
||||
if t.stdinClosed {
|
||||
return nil, errors.New("PCM_MULAW to AAC transcoder input is closed")
|
||||
return nil, errors.New("audio to AAC transcoder input is closed")
|
||||
}
|
||||
|
||||
if _, err := t.stdin.Write(mulawData); err != nil {
|
||||
if _, err := t.stdin.Write(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -148,7 +174,7 @@ func (t *PCMUToAACTranscoder) Transcode(mulawData []byte) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *PCMUToAACTranscoder) Flush() ([]byte, error) {
|
||||
func (t *ffmpegToAACTranscoder) Flush() ([]byte, error) {
|
||||
if t == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -189,7 +215,7 @@ func (t *PCMUToAACTranscoder) Flush() ([]byte, error) {
|
||||
return t.readAvailable(), nil
|
||||
}
|
||||
|
||||
func (t *PCMUToAACTranscoder) Close() {
|
||||
func (t *ffmpegToAACTranscoder) Close() {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
@@ -213,47 +239,64 @@ func (t *PCMUToAACTranscoder) Close() {
|
||||
}
|
||||
|
||||
if stderr := t.stderrString(); stderr != "" {
|
||||
log.Log.Info("capture.pcmu_to_aac: ffmpeg stderr on close: " + stderr)
|
||||
log.Log.Info("capture.audio_to_aac: ffmpeg stderr on close: " + stderr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t *PCMUToAACTranscoder) readAvailable() []byte {
|
||||
func (t *ffmpegToAACTranscoder) readAvailable() []byte {
|
||||
t.outMu.Lock()
|
||||
defer t.outMu.Unlock()
|
||||
|
||||
if t.outBuf.Len() == 0 {
|
||||
return nil
|
||||
if t.outBuf.Len() > 0 {
|
||||
t.adtsBuf = append(t.adtsBuf, t.outBuf.Bytes()...)
|
||||
t.outBuf.Reset()
|
||||
}
|
||||
|
||||
out := make([]byte, t.outBuf.Len())
|
||||
copy(out, t.outBuf.Bytes())
|
||||
t.outBuf.Reset()
|
||||
return out
|
||||
return drainCompleteADTSFrames(&t.adtsBuf)
|
||||
}
|
||||
|
||||
func (t *PCMUToAACTranscoder) bufferedLen() int {
|
||||
func (t *ffmpegToAACTranscoder) bufferedLen() int {
|
||||
t.outMu.Lock()
|
||||
defer t.outMu.Unlock()
|
||||
return t.outBuf.Len()
|
||||
}
|
||||
|
||||
func (t *PCMUToAACTranscoder) stderrString() string {
|
||||
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 *PCMUToAACTranscoder
|
||||
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, logPrefix string) *recordingAudioWriter {
|
||||
func newRecordingAudioWriter(mp4Video *video.MP4, audioCodec string, sampleRate int, channels int, bitDepth int, logPrefix string) *recordingAudioWriter {
|
||||
writer := &recordingAudioWriter{
|
||||
mp4: mp4Video,
|
||||
logPrefix: logPrefix,
|
||||
@@ -283,7 +326,19 @@ func newRecordingAudioWriter(mp4Video *video.MP4, audioCodec string, sampleRate
|
||||
|
||||
writer.trackID = mp4Video.AddAudioTrack("AAC")
|
||||
writer.transcoder = transcoder
|
||||
log.Log.Info(logPrefix + ": recording PCM_MULAW audio as AAC.")
|
||||
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
|
||||
@@ -309,10 +364,14 @@ func (w *recordingAudioWriter) WritePacket(pkt packets.Packet) error {
|
||||
switch pkt.Codec {
|
||||
case "AAC":
|
||||
return w.mp4.AddSampleToTrack(w.trackID, pkt.IsKeyFrame, pkt.Data, pts)
|
||||
case "PCM_MULAW":
|
||||
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 {
|
||||
@@ -322,7 +381,7 @@ func (w *recordingAudioWriter) WritePacket(pkt packets.Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.mp4.AddSampleToTrack(w.trackID, false, adts, pts)
|
||||
return w.writeTranscodedADTS(adts)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -341,12 +400,7 @@ func (w *recordingAudioWriter) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
pts := w.lastPTS
|
||||
if pts == 0 {
|
||||
pts = 1
|
||||
}
|
||||
|
||||
return w.mp4.AddSampleToTrack(w.trackID, false, adts, pts)
|
||||
return w.writeTranscodedADTS(adts)
|
||||
}
|
||||
|
||||
func (w *recordingAudioWriter) Close() {
|
||||
@@ -356,6 +410,121 @@ func (w *recordingAudioWriter) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -49,6 +50,8 @@ type MP4 struct {
|
||||
VideoTotalDuration uint64
|
||||
AudioTotalDuration uint64
|
||||
AudioPTS uint64
|
||||
AudioSampleRate uint32
|
||||
AudioChannels uint16
|
||||
Start bool
|
||||
SPSNALUs [][]byte // SPS NALUs for H264
|
||||
PPSNALUs [][]byte // PPS NALUs for H264
|
||||
@@ -385,16 +388,14 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
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
|
||||
dts := aacFrameDurationSamples(aac)
|
||||
if dts == 0 {
|
||||
dts = mp4.LastAudioSampleDTS
|
||||
if dts == 0 {
|
||||
dts = 1024
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -418,6 +419,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"
|
||||
}
|
||||
@@ -449,9 +456,12 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
if mp4.AudioFullSample != nil && mp4.AudioTrack > 0 {
|
||||
SplitAACFrame(mp4.AudioFullSample.Data, func(started bool, aac []byte) {
|
||||
sampleToAdd := *mp4.AudioFullSample
|
||||
dts := mp4.LastAudioSampleDTS
|
||||
dts := aacFrameDurationSamples(aac)
|
||||
if dts == 0 {
|
||||
dts = 1024 // Default AAC frame duration
|
||||
dts = mp4.LastAudioSampleDTS
|
||||
if dts == 0 {
|
||||
dts = 1024 // Default fallback when ADTS is malformed.
|
||||
}
|
||||
}
|
||||
mp4.AudioTotalDuration += dts
|
||||
mp4.AudioPTS += dts
|
||||
@@ -526,15 +536,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 +630,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 +1289,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 +1331,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