From eb0972084fbea8be17d990142cfafd311a3ef6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Mon, 9 Mar 2026 16:34:52 +0000 Subject: [PATCH] Implement AAC transcoding for WebRTC using FFmpeg; update Dockerfiles and launch configuration --- .vscode/launch.json | 2 +- Dockerfile | 2 +- Dockerfile.arm64 | 2 +- machinery/src/webrtc/aac_transcoder_ffmpeg.go | 270 ++++++++++++++++++ machinery/src/webrtc/aac_transcoder_stub.go | 203 +++++++++++++ machinery/src/webrtc/main.go | 113 +++++++- 6 files changed, 576 insertions(+), 16 deletions(-) create mode 100644 machinery/src/webrtc/aac_transcoder_ffmpeg.go create mode 100644 machinery/src/webrtc/aac_transcoder_stub.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 219c74d..a4d07b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "-action", "run", "-port", - "8080" + "8089" ], "envFile": "${workspaceFolder}/machinery/.env.local", "buildFlags": "--tags dynamic", diff --git a/Dockerfile b/Dockerfile index e794040..28ff43b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,7 @@ RUN addgroup -S kerberosio && adduser -S agent -G kerberosio && addgroup agent v COPY --chown=0:0 --from=build-machinery /dist / COPY --chown=0:0 --from=build-ui /dist / -RUN apk update && apk add ca-certificates curl libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/* +RUN apk update && apk add ca-certificates curl ffmpeg libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/* ################## # Try running agent diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 16e58b2..a898c08 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -95,7 +95,7 @@ RUN addgroup -S kerberosio && adduser -S agent -G kerberosio && addgroup agent v COPY --chown=0:0 --from=build-machinery /dist / COPY --chown=0:0 --from=build-ui /dist / -RUN apk update && apk add ca-certificates curl libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/* +RUN apk update && apk add ca-certificates curl ffmpeg libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/* ################## # Try running agent diff --git a/machinery/src/webrtc/aac_transcoder_ffmpeg.go b/machinery/src/webrtc/aac_transcoder_ffmpeg.go new file mode 100644 index 0000000..67264be --- /dev/null +++ b/machinery/src/webrtc/aac_transcoder_ffmpeg.go @@ -0,0 +1,270 @@ +// AAC to G.711 µ-law transcoder using FFmpeg (libavcodec + libswresample). +// Build with: go build -tags ffmpeg ... +// +// Requires: libavcodec-dev, libavutil-dev, libswresample-dev (FFmpeg ≥ 5.x) +// and an AAC decoder compiled into the FFmpeg build (usually the default). +// +//go:build ffmpeg + +package webrtc + +/* +#cgo pkg-config: libavcodec libavutil libswresample +#cgo CFLAGS: -Wno-deprecated-declarations + +#include +#include +#include +#include +#include +#include +#include +#include + +// ── Transcoder handle ─────────────────────────────────────────────────── + +typedef struct { + AVCodecContext *codec_ctx; + AVCodecParserContext *parser; + SwrContext *swr_ctx; + AVFrame *frame; + AVPacket *pkt; + int swr_initialized; + int in_sample_rate; + int in_channels; +} aac_transcoder_t; + +// ── Create / Destroy ──────────────────────────────────────────────────── + +static aac_transcoder_t* aac_transcoder_create(void) { + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); + if (!codec) return NULL; + + aac_transcoder_t *t = (aac_transcoder_t*)calloc(1, sizeof(aac_transcoder_t)); + if (!t) return NULL; + + t->codec_ctx = avcodec_alloc_context3(codec); + if (!t->codec_ctx) { free(t); return NULL; } + + if (avcodec_open2(t->codec_ctx, codec, NULL) < 0) { + avcodec_free_context(&t->codec_ctx); + free(t); + return NULL; + } + + t->parser = av_parser_init(AV_CODEC_ID_AAC); + if (!t->parser) { + avcodec_free_context(&t->codec_ctx); + free(t); + return NULL; + } + + t->frame = av_frame_alloc(); + t->pkt = av_packet_alloc(); + if (!t->frame || !t->pkt) { + if (t->frame) av_frame_free(&t->frame); + if (t->pkt) av_packet_free(&t->pkt); + av_parser_close(t->parser); + avcodec_free_context(&t->codec_ctx); + free(t); + return NULL; + } + + return t; +} + +static void aac_transcoder_destroy(aac_transcoder_t *t) { + if (!t) return; + if (t->swr_ctx) swr_free(&t->swr_ctx); + if (t->frame) av_frame_free(&t->frame); + if (t->pkt) av_packet_free(&t->pkt); + if (t->parser) av_parser_close(t->parser); + if (t->codec_ctx) avcodec_free_context(&t->codec_ctx); + free(t); +} + +// ── Lazy resampler init (called after the first decoded frame) ────────── + +static int aac_init_swr(aac_transcoder_t *t) { + int64_t in_ch_layout = (int64_t)t->codec_ctx->channel_layout; + if (in_ch_layout == 0) + in_ch_layout = av_get_default_channel_layout(t->codec_ctx->channels); + + t->swr_ctx = swr_alloc_set_opts( + NULL, + AV_CH_LAYOUT_MONO, // out: mono + AV_SAMPLE_FMT_S16, // out: signed 16-bit + 8000, // out: 8 kHz + in_ch_layout, // in: from decoder + t->codec_ctx->sample_fmt, // in: from decoder + t->codec_ctx->sample_rate, // in: from decoder + 0, NULL); + + if (!t->swr_ctx) return -1; + if (swr_init(t->swr_ctx) < 0) { + swr_free(&t->swr_ctx); + return -1; + } + + t->in_sample_rate = t->codec_ctx->sample_rate; + t->in_channels = t->codec_ctx->channels; + t->swr_initialized = 1; + return 0; +} + +// ── Transcode ADTS → 8 kHz mono S16 PCM ──────────────────────────────── +// Caller must free *out_pcm with av_free() when non-NULL. + +static int aac_transcode_to_pcm(aac_transcoder_t *t, + const uint8_t *data, int data_size, + uint8_t **out_pcm, int *out_size) { + *out_pcm = NULL; + *out_size = 0; + if (!data || data_size <= 0) return 0; + + int buf_cap = 8192; + uint8_t *buf = (uint8_t*)av_malloc(buf_cap); + if (!buf) return -1; + int buf_len = 0; + + while (data_size > 0) { + uint8_t *pout = NULL; + int pout_size = 0; + + int used = av_parser_parse2(t->parser, t->codec_ctx, + &pout, &pout_size, + data, data_size, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); + if (used < 0) break; + data += used; + data_size -= used; + if (pout_size == 0) continue; + + // Feed parsed frame to decoder + t->pkt->data = pout; + t->pkt->size = pout_size; + if (avcodec_send_packet(t->codec_ctx, t->pkt) < 0) continue; + + // Pull all decoded frames + while (avcodec_receive_frame(t->codec_ctx, t->frame) == 0) { + if (!t->swr_initialized) { + if (aac_init_swr(t) < 0) { + av_frame_unref(t->frame); + av_free(buf); + return -1; + } + } + + int out_samples = swr_get_out_samples(t->swr_ctx, + t->frame->nb_samples); + if (out_samples <= 0) out_samples = t->frame->nb_samples; + + int needed = buf_len + out_samples * 2; // S16 = 2 bytes/sample + if (needed > buf_cap) { + buf_cap = needed * 2; + uint8_t *tmp = (uint8_t*)av_realloc(buf, buf_cap); + if (!tmp) { av_frame_unref(t->frame); av_free(buf); return -1; } + buf = tmp; + } + + uint8_t *dst = buf + buf_len; + int converted = swr_convert(t->swr_ctx, + &dst, out_samples, + (const uint8_t**)t->frame->extended_data, + t->frame->nb_samples); + if (converted > 0) + buf_len += converted * 2; + + av_frame_unref(t->frame); + } + } + + if (buf_len == 0) { + av_free(buf); + return 0; + } + + *out_pcm = buf; + *out_size = buf_len; + return 0; +} +*/ +import "C" + +import ( + "errors" + "fmt" + "unsafe" + + "github.com/kerberos-io/agent/machinery/src/log" + "github.com/zaf/g711" +) + +// AACTranscodingAvailable reports whether AAC→PCMU transcoding +// is compiled in (requires the "ffmpeg" build tag). +func AACTranscodingAvailable() bool { return true } + +// AACTranscoder decodes ADTS-wrapped AAC audio to 8 kHz mono PCM +// and encodes it as G.711 µ-law for WebRTC transport. +type AACTranscoder struct { + handle *C.aac_transcoder_t +} + +// NewAACTranscoder creates a transcoder backed by FFmpeg's AAC decoder. +func NewAACTranscoder() (*AACTranscoder, error) { + h := C.aac_transcoder_create() + if h == nil { + return nil, errors.New("failed to create AAC transcoder (FFmpeg AAC decoder not available?)") + } + log.Log.Info("webrtc.aac_transcoder: AAC → G.711 µ-law transcoder initialised (FFmpeg)") + return &AACTranscoder{handle: h}, nil +} + +// Transcode converts an ADTS buffer (one or more AAC frames) into +// G.711 µ-law encoded audio suitable for a PCMU WebRTC track. +func (t *AACTranscoder) Transcode(adtsData []byte) ([]byte, error) { + if t == nil || t.handle == nil || len(adtsData) == 0 { + return nil, nil + } + + var outPCM *C.uint8_t + var outSize C.int + + ret := C.aac_transcode_to_pcm( + t.handle, + (*C.uint8_t)(unsafe.Pointer(&adtsData[0])), + C.int(len(adtsData)), + &outPCM, &outSize, + ) + if ret < 0 { + return nil, errors.New("AAC decode/resample failed") + } + if outSize == 0 || outPCM == nil { + return nil, nil // decoder buffering, no output yet + } + defer C.av_free(unsafe.Pointer(outPCM)) + + // Copy S16LE PCM to Go slice, then encode to µ-law. + pcm := C.GoBytes(unsafe.Pointer(outPCM), outSize) + ulaw := g711.EncodeUlaw(pcm) + + // Log resampler details once. + if t.handle.swr_initialized == 1 && t.handle.in_sample_rate != 0 { + log.Log.Info(fmt.Sprintf( + "webrtc.aac_transcoder: first output – resampling %d Hz / %d ch → 8000 Hz mono → µ-law", + int(t.handle.in_sample_rate), int(t.handle.in_channels))) + // Prevent repeated logging by zeroing the field we check. + t.handle.in_sample_rate = 0 + } + + return ulaw, nil +} + +// Close releases all FFmpeg resources held by the transcoder. +func (t *AACTranscoder) Close() { + if t != nil && t.handle != nil { + C.aac_transcoder_destroy(t.handle) + t.handle = nil + log.Log.Info("webrtc.aac_transcoder: transcoder closed") + } +} diff --git a/machinery/src/webrtc/aac_transcoder_stub.go b/machinery/src/webrtc/aac_transcoder_stub.go new file mode 100644 index 0000000..d56cd62 --- /dev/null +++ b/machinery/src/webrtc/aac_transcoder_stub.go @@ -0,0 +1,203 @@ +// AAC transcoding fallback that uses the ffmpeg binary at runtime. +// Build with -tags ffmpeg to use the in-process CGO implementation instead. +// +//go:build !ffmpeg + +package webrtc + +import ( + "bytes" + "errors" + "io" + "os/exec" + "sync" + "time" + + "github.com/kerberos-io/agent/machinery/src/log" +) + +// AACTranscodingAvailable reports whether AAC→PCMU transcoding +// is available in the current runtime. +func AACTranscodingAvailable() bool { + _, err := exec.LookPath("ffmpeg") + return err == nil +} + +// AACTranscoder uses an ffmpeg subprocess to convert ADTS AAC to raw PCMU. +type AACTranscoder struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout io.ReadCloser + stderrBuf bytes.Buffer + + mu sync.Mutex + outMu sync.Mutex + outBuf bytes.Buffer + closed bool + closeOnce sync.Once +} + +// NewAACTranscoder creates a runtime ffmpeg-based transcoder. +func NewAACTranscoder() (*AACTranscoder, error) { + ffmpegPath, err := exec.LookPath("ffmpeg") + if err != nil { + return nil, errors.New("AAC transcoding not available: ffmpeg binary not found in PATH") + } + log.Log.Info("webrtc.aac_transcoder: using ffmpeg binary at " + ffmpegPath) + + cmd := exec.Command( + ffmpegPath, + "-hide_banner", + "-loglevel", "error", + "-fflags", "+nobuffer", + "-flags", "low_delay", + "-f", "aac", + "-i", "pipe:0", + "-vn", + "-ac", "1", + "-ar", "8000", + "-acodec", "pcm_mulaw", + "-f", "mulaw", + "pipe:1", + ) + + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + cmd.Stderr = &bytes.Buffer{} + + if err := cmd.Start(); err != nil { + return nil, err + } + + t := &AACTranscoder{ + cmd: cmd, + stdin: stdin, + stdout: stdout, + } + if stderrBuf, ok := cmd.Stderr.(*bytes.Buffer); ok { + t.stderrBuf = *stderrBuf + } + + go func() { + buf := make([]byte, 4096) + for { + n, readErr := stdout.Read(buf) + if n > 0 { + t.outMu.Lock() + _, _ = t.outBuf.Write(buf[:n]) + buffered := t.outBuf.Len() + t.outMu.Unlock() + if buffered <= 8192 || buffered%16000 == 0 { + log.Log.Info("webrtc.aac_transcoder: ffmpeg produced PCMU bytes, buffered=" + strconv.Itoa(buffered)) + } + } + if readErr != nil { + if readErr != io.EOF { + log.Log.Warning("webrtc.aac_transcoder: stdout reader stopped: " + readErr.Error()) + } + return + } + } + }() + + log.Log.Info("webrtc.aac_transcoder: AAC → PCMU transcoder initialised (ffmpeg process)") + return t, nil +} + +// Transcode writes ADTS AAC to ffmpeg and returns any PCMU bytes produced. +func (t *AACTranscoder) Transcode(adtsData []byte) ([]byte, error) { + if t == nil || len(adtsData) == 0 { + return nil, nil + } + + t.mu.Lock() + defer t.mu.Unlock() + + if t.closed { + return nil, errors.New("AAC transcoder is closed") + } + + if _, err := t.stdin.Write(adtsData); err != nil { + return nil, err + } + if len(adtsData) <= 512 || len(adtsData)%1024 == 0 { + log.Log.Info("webrtc.aac_transcoder: wrote AAC bytes to ffmpeg, input=" + strconv.Itoa(len(adtsData))) + } + + deadline := time.Now().Add(75 * time.Millisecond) + for { + data := t.readAvailable() + if len(data) > 0 { + log.Log.Info("webrtc.aac_transcoder: returning PCMU bytes=" + strconv.Itoa(len(data))) + return data, nil + } + + if time.Now().After(deadline) { + if stderr := t.stderrString(); stderr != "" { + log.Log.Warning("webrtc.aac_transcoder: no output before deadline, ffmpeg stderr: " + stderr) + } else { + log.Log.Info("webrtc.aac_transcoder: no PCMU output before deadline") + } + return nil, nil + } + time.Sleep(5 * time.Millisecond) + } +} + +func (t *AACTranscoder) readAvailable() []byte { + t.outMu.Lock() + defer t.outMu.Unlock() + + if t.outBuf.Len() == 0 { + return nil + } + + out := make([]byte, t.outBuf.Len()) + copy(out, t.outBuf.Bytes()) + t.outBuf.Reset() + return out +} + +func (t *AACTranscoder) stderrString() string { + if t == nil { + return "" + } + if stderrBuf, ok := t.cmd.Stderr.(*bytes.Buffer); ok { + return strings.TrimSpace(stderrBuf.String()) + } + return strings.TrimSpace(t.stderrBuf.String()) +} + +// Close stops the ffmpeg subprocess. +func (t *AACTranscoder) Close() { + if t == nil { + return + } + + t.closeOnce.Do(func() { + t.mu.Lock() + t.closed = true + if t.stdin != nil { + _ = t.stdin.Close() + } + t.mu.Unlock() + + if t.stdout != nil { + _ = t.stdout.Close() + } + + if t.cmd != nil { + _ = t.cmd.Process.Kill() + _, _ = t.cmd.Process.Wait() + if stderr := t.stderrString(); stderr != "" { + log.Log.Info("webrtc.aac_transcoder: ffmpeg stderr on close: " + stderr) + } + } + }) +} diff --git a/machinery/src/webrtc/main.go b/machinery/src/webrtc/main.go index a6759e6..a9201df 100644 --- a/machinery/src/webrtc/main.go +++ b/machinery/src/webrtc/main.go @@ -4,13 +4,14 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "io" "strconv" + "strings" "sync" "sync/atomic" "time" - //"github.com/izern/go-fdkaac/fdkaac" "github.com/kerberos-io/agent/machinery/src/capture" "github.com/kerberos-io/agent/machinery/src/log" "github.com/kerberos-io/agent/machinery/src/models" @@ -640,7 +641,12 @@ func NewVideoBroadcaster(streams []packets.Stream) *TrackBroadcaster { } func NewAudioBroadcaster(streams []packets.Stream) *TrackBroadcaster { + var audioCodecNames []string + hasAAC := false for _, s := range streams { + if s.IsAudio { + audioCodecNames = append(audioCodecNames, s.Name) + } switch s.Name { case "OPUS": return NewTrackBroadcaster(pionWebRTC.MimeTypeOpus, "audio", trackStreamID) @@ -648,9 +654,18 @@ func NewAudioBroadcaster(streams []packets.Stream) *TrackBroadcaster { return NewTrackBroadcaster(pionWebRTC.MimeTypePCMU, "audio", trackStreamID) case "PCM_ALAW": return NewTrackBroadcaster(pionWebRTC.MimeTypePCMA, "audio", trackStreamID) + case "AAC": + hasAAC = true } } - log.Log.Error("webrtc.main.NewAudioBroadcaster(): no supported audio codec found") + if hasAAC { + log.Log.Info("webrtc.main.NewAudioBroadcaster(): AAC detected, creating PCMU audio track for transcoded output") + return NewTrackBroadcaster(pionWebRTC.MimeTypePCMU, "audio", trackStreamID) + } else if len(audioCodecNames) > 0 { + log.Log.Error(fmt.Sprintf("webrtc.main.NewAudioBroadcaster(): no supported audio codec found (detected: %s; supported: OPUS, PCM_MULAW, PCM_ALAW)", strings.Join(audioCodecNames, ", "))) + } else { + log.Log.Info("webrtc.main.NewAudioBroadcaster(): no audio stream found in camera feed") + } return nil } @@ -666,18 +681,33 @@ func NewVideoTrack(streams []packets.Stream) *pionWebRTC.TrackLocalStaticSample func NewAudioTrack(streams []packets.Stream) *pionWebRTC.TrackLocalStaticSample { var mimeType string + var audioCodecNames []string + hasAAC := false for _, stream := range streams { + if stream.IsAudio { + audioCodecNames = append(audioCodecNames, stream.Name) + } if stream.Name == "OPUS" { mimeType = pionWebRTC.MimeTypeOpus } else if stream.Name == "PCM_MULAW" { mimeType = pionWebRTC.MimeTypePCMU } else if stream.Name == "PCM_ALAW" { mimeType = pionWebRTC.MimeTypePCMA + } else if stream.Name == "AAC" { + hasAAC = true } } if mimeType == "" { - log.Log.Error("webrtc.main.NewAudioTrack(): no supported audio codec found") - return nil + if hasAAC { + mimeType = pionWebRTC.MimeTypePCMU + log.Log.Info("webrtc.main.NewAudioTrack(): AAC detected, creating PCMU audio track for transcoded output") + } else if len(audioCodecNames) > 0 { + log.Log.Error(fmt.Sprintf("webrtc.main.NewAudioTrack(): no supported audio codec found (detected: %s; supported: OPUS, PCM_MULAW, PCM_ALAW)", strings.Join(audioCodecNames, ", "))) + return nil + } else { + log.Log.Info("webrtc.main.NewAudioTrack(): no audio stream found in camera feed") + return nil + } } outboundAudioTrack, err := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "audio", trackStreamID) if err != nil { @@ -696,6 +726,11 @@ type streamState struct { receivedKeyFrame bool lastAudioSample *pionMedia.Sample lastVideoSample *pionMedia.Sample + audioPacketsSeen int64 + aacPacketsSeen int64 + audioSamplesSent int64 + aacNoOutput int64 + aacErrors int64 } // codecSupport tracks which codecs are available in the stream @@ -843,22 +878,54 @@ func processVideoPacket(pkt packets.Packet, state *streamState, videoBroadcaster state.lastVideoSample = &sample } -// processAudioPacket processes an audio packet and writes samples to the broadcaster -func processAudioPacket(pkt packets.Packet, state *streamState, audioBroadcaster *TrackBroadcaster, hasAAC bool) { +// processAudioPacket processes an audio packet and writes samples to the broadcaster. +// When the packet carries AAC and a transcoder is provided, the audio is transcoded +// to G.711 µ-law on the fly so it can be sent over a PCMU WebRTC track. +func processAudioPacket(pkt packets.Packet, state *streamState, audioBroadcaster *TrackBroadcaster, transcoder *AACTranscoder) { if audioBroadcaster == nil { return } - if hasAAC { - // AAC transcoding not yet implemented - // TODO: Implement AAC to PCM_MULAW transcoding - return + state.audioPacketsSeen++ + + audioData := pkt.Data + + if pkt.Codec == "AAC" { + state.aacPacketsSeen++ + if transcoder == nil { + state.aacErrors++ + if state.aacErrors <= 3 || state.aacErrors%100 == 0 { + log.Log.Warning(fmt.Sprintf("webrtc.main.processAudioPacket(): AAC packet dropped because transcoder is nil (aac_packets=%d, input_bytes=%d)", state.aacPacketsSeen, len(pkt.Data))) + } + return // no transcoder – silently drop + } + pcmu, err := transcoder.Transcode(pkt.Data) + if err != nil { + state.aacErrors++ + log.Log.Error("webrtc.main.processAudioPacket(): AAC transcode error: " + err.Error()) + return + } + if len(pcmu) == 0 { + state.aacNoOutput++ + if state.aacNoOutput <= 5 || state.aacNoOutput%100 == 0 { + log.Log.Info(fmt.Sprintf("webrtc.main.processAudioPacket(): AAC packet produced no PCMU output yet (aac_packets=%d, no_output=%d, input_bytes=%d)", state.aacPacketsSeen, state.aacNoOutput, len(pkt.Data))) + } + return // decoder still buffering + } + if state.aacPacketsSeen <= 5 || state.aacPacketsSeen%100 == 0 { + log.Log.Info(fmt.Sprintf("webrtc.main.processAudioPacket(): AAC transcoded to PCMU (aac_packets=%d, input_bytes=%d, output_bytes=%d, peers=%d)", state.aacPacketsSeen, len(pkt.Data), len(pcmu), audioBroadcaster.PeerCount())) + } + audioData = pcmu } - sample := pionMedia.Sample{Data: pkt.Data, PacketTimestamp: sampleTimestamp(pkt)} + sample := pionMedia.Sample{Data: audioData, PacketTimestamp: sampleTimestamp(pkt)} if state.lastAudioSample != nil { state.lastAudioSample.Duration = sampleDuration(pkt, state.lastAudioSample.PacketTimestamp, 20*time.Millisecond) + state.audioSamplesSent++ + if state.audioSamplesSent <= 5 || state.audioSamplesSent%100 == 0 { + log.Log.Info(fmt.Sprintf("webrtc.main.processAudioPacket(): queueing audio sample (samples=%d, codec=%s, bytes=%d, duration_ms=%d, peers=%d)", state.audioSamplesSent, pkt.Codec, len(state.lastAudioSample.Data), state.lastAudioSample.Duration.Milliseconds(), audioBroadcaster.PeerCount())) + } audioBroadcaster.WriteSample(*state.lastAudioSample) } @@ -892,8 +959,22 @@ func WriteToTrack(livestreamCursor *packets.QueueCursor, configuration *models.C return } + // Create AAC transcoder if needed (AAC → G.711 µ-law). + var aacTranscoder *AACTranscoder + if codecs.hasAAC && audioBroadcaster != nil { + log.Log.Info(fmt.Sprintf("webrtc.main.WriteToTrack(): AAC audio detected, creating transcoder (audio_peers=%d)", audioBroadcaster.PeerCount())) + t, err := NewAACTranscoder() + if err != nil { + log.Log.Error("webrtc.main.WriteToTrack(): failed to create AAC transcoder: " + err.Error()) + } else { + aacTranscoder = t + log.Log.Info("webrtc.main.WriteToTrack(): AAC transcoder created successfully") + defer aacTranscoder.Close() + } + } + if config.Capture.TranscodingWebRTC == "true" { - log.Log.Info("webrtc.main.WriteToTrack(): transcoding enabled but not yet implemented") + log.Log.Info("webrtc.main.WriteToTrack(): transcoding config enabled") } // Initialize streaming state @@ -903,6 +984,12 @@ func WriteToTrack(livestreamCursor *packets.QueueCursor, configuration *models.C } defer func() { + log.Log.Info(fmt.Sprintf("webrtc.main.WriteToTrack(): audio summary packets=%d aac_packets=%d sent=%d aac_no_output=%d aac_errors=%d peers=%d", state.audioPacketsSeen, state.aacPacketsSeen, state.audioSamplesSent, state.aacNoOutput, state.aacErrors, func() int { + if audioBroadcaster == nil { + return 0 + } + return audioBroadcaster.PeerCount() + }())) writeFinalSamples(state, videoBroadcaster, audioBroadcaster) log.Log.Info("webrtc.main.WriteToTrack(): stopped writing to track") }() @@ -971,7 +1058,7 @@ func WriteToTrack(livestreamCursor *packets.QueueCursor, configuration *models.C if pkt.IsVideo { processVideoPacket(pkt, state, videoBroadcaster, config) } else if pkt.IsAudio { - processAudioPacket(pkt, state, audioBroadcaster, codecs.hasAAC) + processAudioPacket(pkt, state, audioBroadcaster, aacTranscoder) } } }