Add LPCM audio support and enhance PCM_MULAW to AAC transcoding

This commit is contained in:
Cédric Verstraeten
2026-03-19 14:51:10 +00:00
parent a7e68951dc
commit 945ceea61c
5 changed files with 477 additions and 69 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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) |
// +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+