Skip to content

fix(audio): AAC/G.711 playback; docs: README in EN/RU/ZH, device report form - #6

Merged
compot-dev merged 4 commits into
v2from
fix/audio
Oct 12, 2026
Merged

compot-dev merged 4 commits into
v2from
fix/audio

Conversation

@compot-dev

@compot-dev compot-dev commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • AAC never played. The AAC decoder could only be started from playAudio(), which reads the PCM queue. For AAC streams that queue stays empty, so the decoder was never created.
  • mpeg4-generic (RFC 3640) was not recognised as AAC. The SDP check only looked for MP4A/AAC, so this audio went out as raw PCM, which is noise.
  • AAC could not restart (regression from the v3 commit). closeAudio() called shutdownNow() on the final executor. After the first pause or camera switch, starting AAC would throw RejectedExecutionException.
  • G.711 was advertised in the README but not implemented ("not yet implemented").
  • Starting audio crashed on Android 5.x. AudioTrack.Builder needs API 23, but minSdk is 21. Lint reported this as NewApi.

Changes

  • New AudioConfig class with the audio parameters from the SDP: codec, PT, rate, channels, config=, SizeLength/IndexLength/IndexDeltaLength. Static PTs 0/8/10/11 work without rtpmap. Only rtpmap/fmtp lines from the audio section are read.
  • AAC:
    • RTP payload handling: AU headers per RFC 3640; ADTS split into frames; a raw AU uses csd-0 taken from config= or built from the rate and channel count.
    • The decoder starts on the first AAC packet.
    • The output format (rate/channels) drives the AudioTrack.
  • G.711 μ-law/A-law decoding through lookup tables.
  • L16 stereo support.
  • Unsupported codecs (Opus, LATM, ...) are dropped with a single log line.
  • RtspClient.rtpHeaderLength() / rtpPayloadEnd() account for CSRC, extension and padding. They are only used for audio for now.
  • Only the playback thread writes to the AudioTrack. The track is recreated when the rate or channel count changes. closeAudio() also clears the PCM queue.
  • AudioTrack is created with the API 21 constructor. AudioManager.shutdown() is called in onDestroy.
  • The status overlay shows nothing for the audio codec when there is no audio track.

Verification

  • assembleDebug builds locally (AGP 8.13.1, SDK 36). lintDebug: 9 errors on v2, 3 on this branch; the remaining 3 predate it and are unrelated to audio.
  • JVM checks of the pure logic: 33/33 pass. Covered: G.711 against reference values, AudioSpecificConfig (1188, 1210, 1588), AU-header bit reading, RTP header with CSRC/extension/padding, SDP parsing (mpeg4-generic + fmtp, static PCMA, a video and audio track with the same PT, Opus, no audio).
  • Not tested on a device or with real cameras. It needs a check with an OpenIPC camera on AAC and on G.711.

Out of scope, coming in separate PRs: SETUP of a missing audio track, a=control:*, video codec detection by PT.


Also in this PR: documentation and license headers

- AAC never started: the decoder was only created from playAudio(), which
  is fed by the PCM queue that stays empty for AAC. The decoder is now
  created on the first AAC packet.
- mpeg4-generic (RFC 3640) was not recognised and was played as raw PCM.
  AU headers are now parsed using SizeLength/IndexLength from fmtp, and
  config= is passed as csd-0. ADTS payloads are split per frame.
- closeAudio() shut down the AAC executor for good, so AAC could not
  restart after the first pause or camera switch.
- G.711 (PCMU/PCMA) is decoded through lookup tables, including static
  payload types 0/8 without rtpmap.
- Sample rate and channel count come from rtpmap and from the AAC output
  format; L16 stereo is supported.
- Unsupported codecs (Opus, LATM, ...) are dropped instead of being played
  as noise.
- The RTP header length now accounts for CSRC, extension and padding.
- AudioTrack.Builder (API 23) is replaced with the API 21 constructor, so
  starting audio no longer crashes on Android 5.x (minSdk 21).
- Only the playback thread writes to the AudioTrack.
- Audio parameters move into AudioConfig.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix AAC playback and add G.711 audio decoding

🐞 Bug fix ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Parse audio SDP parameters so AAC, G.711, and stereo L16 use the correct playback formats.
• Start AAC decoding on incoming packets and route decoded audio through the PCM playback queue.
• Make audio restartable and restore AudioTrack compatibility with Android 5.x.
Diagram

graph TD
  SDP["Audio SDP"] --> RTSP["RTSP client"] --> CFG["Audio config"] --> MGR["Audio manager"] --> AAC["AAC MediaCodec"] --> Q[("PCM queue")] --> TRACK["AudioTrack"]
  MGR --> G711["G.711 and L16"] --> Q
  RTSP --> MGR
Loading
High-Level Assessment

Extending the existing RTSP-to-AudioTrack pipeline is proportionate to this playback fix; replacing it with a separate media player would require substantially broader integration. Before claiming support for RTP packets with CSRCs or extensions, address the existing processPacket prefilter that still rejects them, and verify AAC and G.711 on a device.

Files changed (5) +494 / -250

Enhancement (1) +108 / -0
AudioConfig.javaRepresent audio SDP parameters in one configuration object +108/-0

Represent audio SDP parameters in one configuration object

• Adds codec and payload-type defaults, including static G.711 and L16 types. Parses audio rtpmap and fmtp values needed for sample format and AAC access-unit decoding.

android/app/src/main/java/com/openipc/decoder/AudioConfig.java

Bug fix (3) +385 / -246
AudioManager.javaDecode AAC and G.711 into a single PCM playback path +319/-173

Decode AAC and G.711 into a single PCM playback path

• Adds AAC access-unit and ADTS handling, G.711 lookup-table decoding, and stereo-aware L16 conversion. Starts AAC on packet arrival, queues decoded PCM for playback-thread-only writes, recreates AudioTrack when its format changes, and keeps AAC restartable after close.

android/app/src/main/java/com/openipc/decoder/AudioManager.java

Decoder.javaReflect audio availability and release resources on destruction +19/-7

Reflect audio availability and release resources on destruction

• Passes AudioConfig to the audio manager and shows no audio codec in the status overlay when none is supported. Shuts down audio resources in onDestroy.

android/app/src/main/java/com/openipc/decoder/Decoder.java

RtspClient.javaParse audio-track configuration and RTP payload boundaries +47/-66

Parse audio-track configuration and RTP payload boundaries

• Replaces separate audio fields with AudioConfig and confines audio rtpmap and fmtp parsing to the audio media section. Adds RTP header-length and payload-end helpers for audio processing, including CSRC, extension, and padding offsets.

android/app/src/main/java/com/openipc/decoder/RtspClient.java

Refactor (1) +1 / -4
QuadCell.javaAdapt video-only SDP parsing to the new interface +1/-4

Adapt video-only SDP parsing to the new interface

• Calls the revised SDP parser without requesting audio configuration, preserving QuadCell's video-only use.

android/app/src/main/java/com/openipc/decoder/QuadCell.java

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. AAC stays silent after some reconnects ✓ Resolved
Description
AudioManager.configure() replaces the audio configuration without resetting an existing AAC
decoder, and ensureAacDecoder() skips creation whenever that decoder is non-null. When RTSP
reconnects with a different AAC rate, channel count, or config=, new access units are fed to the
decoder configured for the previous stream.
Code

android/app/src/main/java/com/openipc/decoder/AudioManager.java[R79-84]

+    void configure(AudioConfig audio) {
+        config = audio;
+        if (!AudioConfig.AAC.equals(audio.codec)) {
+            pcmSampleRate = audio.sampleRate;
+            pcmChannels = audio.channels;
+        }
Evidence
RTSP parses and publishes configuration on each connection without closing audio during its
reconnect loop; configuration replacement does not touch the decoder, whose non-null check prevents
reconfiguration.

android/app/src/main/java/com/openipc/decoder/RtspClient.java[100-108]
android/app/src/main/java/com/openipc/decoder/RtspClient.java[459-468]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[79-84]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[353-356]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An RTSP reconnect can supply new AAC parameters while the previous decoder remains active.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/AudioManager.java[79-84]
- android/app/src/main/java/com/openipc/decoder/AudioManager.java[353-357]
## Recommended Fix
Compare incoming AAC parameters with the active decoder configuration and, when they differ, stop and release the old decoder and clear its queued access units before accepting packets for the new stream.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Valid SDP ordering silences AAC ✓ Resolved
Description
AudioConfig.applyRtpmap() resets sizeLength to zero even if an earlier a=fmtp line supplied
it. When an audio section lists fmtp before rtpmap, processAac() treats RFC 3640 AU headers as
a raw access unit and sends the wrong bytes to the decoder.
Code

android/app/src/main/java/com/openipc/decoder/AudioConfig.java[R46-47]

+        bigEndian = false;
+        sizeLength = 0;
Evidence
The SDP parser applies matching attributes in their encountered order. applyFmtp() sets
sizeLength, applyRtpmap() erases it, and AAC packet parsing chooses its AU-header branch solely
from that field.

android/app/src/main/java/com/openipc/decoder/RtspClient.java[513-518]
android/app/src/main/java/com/openipc/decoder/AudioConfig.java[46-49]
android/app/src/main/java/com/openipc/decoder/AudioConfig.java[85-89]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[283-317]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Parsing an `rtpmap` line after `fmtp` erases the AAC AU-header layout.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/AudioConfig.java[46-47]
- android/app/src/main/java/com/openipc/decoder/RtspClient.java[513-518]
## Recommended Fix
Do not reset an already parsed AU-header length in `applyRtpmap()`; initialize defaults when starting a new audio media section instead, and test both attribute orders.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Malformed AAC headers interrupt streaming ✓ Resolved
Description
AudioConfig.applyFmtp() accepts negative indexLength and indexDeltaLength, which
processAac() adds to its bit position without validation. With a negative index length and an AU
header followed by payload, a later readBits() can index before the packet and throw on the RTSP
processing thread.
Code

android/app/src/main/java/com/openipc/decoder/AudioConfig.java[R87-89]

+                    case "sizelength":       sizeLength = Integer.parseInt(value); break;
+                    case "indexlength":      indexLength = Integer.parseInt(value); break;
+                    case "indexdeltalength": indexDeltaLength = Integer.parseInt(value); break;
Evidence
Only sizeLength is bounded after parsing. The other parsed lengths can move bitPos negative,
while readBits() indexes the byte array directly and the RTP callback has no exception guard.

android/app/src/main/java/com/openipc/decoder/AudioConfig.java[84-95]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[287-300]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[325-330]
android/app/src/main/java/com/openipc/decoder/RtspClient.java[322-330]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Unvalidated SDP index lengths can make AAC bit parsing access outside the packet.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/AudioConfig.java[87-95]
- android/app/src/main/java/com/openipc/decoder/AudioManager.java[287-300]
## Recommended Fix
Reject negative or excessive index lengths during SDP parsing and verify each complete AU header fits within the declared header bits before reading it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Busy AAC decoders discard access units ✓ Resolved
Description
aacLoop() removes an access unit from aacQueue before requesting an input buffer, then proceeds
without requeuing it when dequeueInputBuffer() returns a negative result. If the decoder has no
input buffer available within the timeout, that access unit is lost even though output buffers are
drained immediately afterward.
Code

android/app/src/main/java/com/openipc/decoder/AudioManager.java[R418-420]

                 int inputId = codec.dequeueInputBuffer(10_000);
-                    if (inputId < 0) continue;
-
-                    ByteBuffer inBuf = codec.getInputBuffer(inputId);
-                    if (inBuf == null) continue;
-                    inBuf.clear();
-                    if (hasAdts) {
-                        inBuf.put(aacData, 0, aacData.length);
-                    } else {
-                        inBuf.put(aacData);
+                    if (inputId >= 0) {
+                        ByteBuffer inBuf = codec.getInputBuffer(inputId);
Evidence
The worker polls and removes an AU, but only queues it into MediaCodec inside the
nonnegative-input-ID branch; no alternate path retains it before the next queue poll.

android/app/src/main/java/com/openipc/decoder/AudioManager.java[403-425]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[429-446]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A temporarily unavailable codec input buffer causes the current AAC access unit to be discarded.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/AudioManager.java[403-425]
## Recommended Fix
Keep the current access unit pending when no input buffer is available, drain output as needed, and retry it before polling another unit.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (1)
5. Final AAC frames can remain unheard ✓ Resolved
Description
aacLoop() polls codec output only after it has taken another access unit from aacQueue; its
empty-queue branch skips output polling. If decoded output becomes available after the immediate
zero-timeout drain for the final packet, playback never receives that output until another AAC
packet arrives.
Code

android/app/src/main/java/com/openipc/decoder/AudioManager.java[R410-413]

+            if (au == null) {
+                if (aacDecoder != codec) break;
+                continue;
+            }
Evidence
An empty input poll immediately continues the loop, whereas the only output dequeue is inside the
branch that received an AU and uses a zero timeout.

android/app/src/main/java/com/openipc/decoder/AudioManager.java[400-413]
android/app/src/main/java/com/openipc/decoder/AudioManager.java[429-446]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
AAC output that becomes ready after the last input packet is not polled while the input queue is empty.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/AudioManager.java[400-446]
## Recommended Fix
Poll and drain available MediaCodec output on idle iterations as well as after input submissions, preserving the decoder-lock and shutdown checks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can commit Qodo's fix in one click with committable suggestions (GitHub & GitLab)

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/AudioManager.java
Comment thread android/app/src/main/java/com/openipc/decoder/AudioConfig.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/AudioConfig.java
Comment thread android/app/src/main/java/com/openipc/decoder/AudioManager.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/AudioManager.java Outdated
LICENSE.md and README already state Prosperity 3.0.0; the per-file headers
still said MIT. Also adds a Device report issue form.
- Fix menu and quick-start descriptions to match the app (4 slots, Q,
  Settings = URL editor, no transport option, quality colours)
- Add Download, Branches, Known limitations (linked to issues #7-#16),
  Building (debug without keys, CI behaviour), honest Security notes
- Device list as a table with app version; cropped picture moved to #17
- Device report issue form link; README.ru.md and README.zh-CN.md with
  cross-links; tables padded for plain-text reading
@compot-dev compot-dev changed the title fix(audio): make AAC and G.711 playback work fix(audio): AAC/G.711 playback; docs: README in EN/RU/ZH, device report form Oct 12, 2026
- Recreate the AAC decoder on every RTSP (re)configure, so a reconnect
  with a different rate, channel count or config= is not fed to the old
  decoder.
- Do not reset the AU-header layout in applyRtpmap(): fmtp may come
  before rtpmap in the SDP.
- Disable AAC when SizeLength/IndexLength/IndexDeltaLength are outside
  0..16, and only read AU headers that fit completely in the declared
  header bits.
- Keep an access unit when no codec input buffer is free, and retry it
  instead of dropping it.
- Drain codec output on idle iterations as well, so the last decoded
  frame is not held back until the next packet arrives.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Thanks @qodo — all 5 findings were valid and are fixed in f18fa67 (replies inline).

On the high-level note about the CSRC/extension prefilter: audio arriving on interleaved channel 2 goes straight to AudioManager.onAudioFrame() and does not pass through processPacket(), so audio with CSRC or extensions is handled. The video path still drops such packets; that is tracked in #10 and is deliberately out of scope here.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Thanks @qodo — all 5 findings were valid and are fixed in f18fa67 (replies inline).

On the high-level note about the CSRC/extension prefilter: audio arriving on interleaved channel 2 goes straight to AudioManager.onAudioFrame() and does not pass through processPacket(), so audio with CSRC or extensions is handled. The video path still drops such packets; that is tracked in #10 and is deliberately out of scope here.

Thanks for the clarification. Findings 1–5 are confirmed fixed in f18fa67. The CSRC/extension prefilter does not affect interleaved audio on channel 2, while the corresponding video-path limitation is correctly out of scope and tracked in #10.

@compot-dev
compot-dev merged commit faa11db into v2 Oct 12, 2026
1 check passed
@compot-dev
compot-dev deleted the fix/audio branch October 13, 2026 17:16
compot-dev added a commit that referenced this pull request Oct 15, 2026
fix(audio): AAC/G.711 playback; docs: README in EN/RU/ZH, device report form
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant