fix(media): repacketize PTT Opus to code 3 so voice notes play on iOS#2648
fix(media): repacketize PTT Opus to code 3 so voice notes play on iOS#2648alessandropcostabr wants to merge 2 commits into
Conversation
Voice notes sent with ptt:true play on Android but are rejected by iOS
WhatsApp ('this audio is no longer available, ask the sender to resend it').
The message is delivered/downloadable, so it is not a transport issue.
Root cause is the Opus packetization: WhatsApp's native voice notes use
code 3 (multiple 20ms SILK frames per packet); libopus/ffmpeg emit code 0
(one frame per packet). iOS requires code 3 for PTT; Android tolerates both.
Adds a dependency-free OGG/Opus repacketizer (regroups code-0 frames into
code-3 packets, recomputing granulepos + page CRC, grouping by config so
SILK/CELT frames are not mixed) and applies it in prepareWAMessageMedia on
the ptt path only. Idempotent (no-op if already code 3 / not OGG/Opus) and
falls back to the original media on any error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for opening this pull request and contributing to the project! The next step is for the maintainers to review your changes. If everything looks good, it will be approved and merged into the main branch. In the meantime, anyone in the community is encouraged to test this pull request and provide feedback. ✅ How to confirm it worksIf you’ve tested this PR, please comment below with: This helps us speed up the review and merge process. 📦 To test this PR locally:If you encounter any issues or have feedback, feel free to comment as well. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds OGG/Opus repacketization that converts single-frame (code 0) PTT voice-note packets into multi-frame (code 3) packets for iOS playback compatibility. New functions ChangesPTT Opus Repacketization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Utils/messages-media.ts`:
- Line 404: The repacketizeOggOpusToCode3 function accepts a framesPerPacket
parameter with a default value of 3, but does not validate it. When
framesPerPacket is zero, negative, or NaN, the condition at line 436
(group.length < framesPerPacket) will never evaluate to true, causing the
variable i to never advance and creating an infinite loop in the while loop at
line 431. Add validation at the start of the repacketizeOggOpusToCode3 function
to ensure framesPerPacket is a positive number, and either throw an error or
assign a valid default value if the validation fails.
- Around line 414-423: The audioPackets repacketization logic only guards
against code-3 Opus packets but incorrectly assumes all remaining packets are
code-0 format when using p.subarray(1). Packets with code-1 or code-2 have
different layouts and would be misinterpreted. Add an additional filter
condition in the filter method (where p.length >= 1 is checked) to verify that
the packet TOC code is 0 by checking (p[0]! & 0x03) === 0, ensuring only
properly formatted code-0 packets are decoded and repacketized.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: fdc5d0bb-7e1d-4c2d-b6fb-9e236ac387ca
📒 Files selected for processing (3)
src/Utils/messages-media.tssrc/Utils/messages.tssrc/__tests__/Utils/repacketize-opus.test.ts
There was a problem hiding this comment.
4 issues found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/Utils/messages.ts">
<violation number="1" location="src/Utils/messages.ts:153">
P2: Unconditional PTT Opus repacketization buffers the entire media in memory before encryption/upload, adding memory/latency overhead and OOM risk for large inputs without a size guard.</violation>
<violation number="2" location="src/Utils/messages.ts:153">
P2: Repacketization is applied before URL-based media cache key computation, so URL-backed PTT media is converted to a Buffer and the media cache is bypassed for identical sources.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // rejects ("audio no longer available"). Regroup so iOS accepts the voice note | ||
| // (Android already tolerates both). No-op if already code 3 / not OGG/Opus. | ||
| if (mediaType === 'audio' && uploadData.ptt === true) { | ||
| uploadData.media = await repacketizePttOpus(uploadData.media, options.options, logger) |
There was a problem hiding this comment.
P2: Repacketization is applied before URL-based media cache key computation, so URL-backed PTT media is converted to a Buffer and the media cache is bypassed for identical sources.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Utils/messages.ts, line 153:
<comment>Repacketization is applied before URL-based media cache key computation, so URL-backed PTT media is converted to a Buffer and the media cache is bypassed for identical sources.</comment>
<file context>
@@ -143,6 +144,15 @@ export const prepareWAMessageMedia = async (
+ // rejects ("audio no longer available"). Regroup so iOS accepts the voice note
+ // (Android already tolerates both). No-op if already code 3 / not OGG/Opus.
+ if (mediaType === 'audio' && uploadData.ptt === true) {
+ uploadData.media = await repacketizePttOpus(uploadData.media, options.options, logger)
+ }
+
</file context>
| // rejects ("audio no longer available"). Regroup so iOS accepts the voice note | ||
| // (Android already tolerates both). No-op if already code 3 / not OGG/Opus. | ||
| if (mediaType === 'audio' && uploadData.ptt === true) { | ||
| uploadData.media = await repacketizePttOpus(uploadData.media, options.options, logger) |
There was a problem hiding this comment.
P2: Unconditional PTT Opus repacketization buffers the entire media in memory before encryption/upload, adding memory/latency overhead and OOM risk for large inputs without a size guard.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Utils/messages.ts, line 153:
<comment>Unconditional PTT Opus repacketization buffers the entire media in memory before encryption/upload, adding memory/latency overhead and OOM risk for large inputs without a size guard.</comment>
<file context>
@@ -143,6 +144,15 @@ export const prepareWAMessageMedia = async (
+ // rejects ("audio no longer available"). Regroup so iOS accepts the voice note
+ // (Android already tolerates both). No-op if already code 3 / not OGG/Opus.
+ if (mediaType === 'audio' && uploadData.ptt === true) {
+ uploadData.media = await repacketizePttOpus(uploadData.media, options.options, logger)
+ }
+
</file context>
Address review: - guard framesPerPacket (integer 1..48) to avoid a non-terminating loop - only repacketize a stream of single-frame (code 0) packets; leave code 1/2/3 untouched instead of misinterpreting their layout Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @coderabbitai — both addressed in the latest commit:
Tests still green (code 0 → code 3, idempotency, non-OGG passthrough). |
|
Tip For best results, initiate chat on the files or code changes.
(๑˃ᴗ˂)ﻭ 🐇 If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! |
|
This PR is stale because it has been open for 14 days with no activity. Remove the stale label or comment or this will be closed in 14 days |
Problem
Voice notes sent with
ptt: trueplay on Android but are rejected by WhatsApp on iOS — "this audio is no longer available, ask the sender to resend it." The message is delivered and downloadable on Android, so it is not a transport/upload problem. Reported repeatedly (see #1797, and the same media path in #1828).Root cause
The determinant is the Opus packetization (TOC "code") — not the container, mimetype, waveform,
seconds, or transport:libopus/ffmpeg— the usual way to produce the.oggpassed tosendMessage— emit code 0 (one frame per packet).Sample rate matters too (native PTT is 16 kHz mono; 48 kHz is also rejected on iOS) but it is necessary-not-sufficient — even a clean 16 kHz mono
code 0stream is rejected. Code 3 framing is the decisive factor.How it was isolated (controlled A/B on a real iPhone)
config 9 / code 3. Re-upload those exact bytes via Baileys (sendMessage({ audio, ptt:true })) → plays on iOS → proves Baileys' transport/upload is correct.config 9 / code 0) via Baileys → iOS rejects.Same transport, same audio content — only the packet framing changes.
What this PR does
Adds a dependency-free OGG/Opus repacketizer (
repacketizeOggOpusToCode3) and applies it inprepareWAMessageMediaonly on thepttpath:configcode 0frames intocode 3packets (default 3 frames/packet), rewriting OGG pages with recomputedgranuleposand CRC-32.code 3or not OGG/Opus.repacketizePttOpusbuffers the media and falls back to the original on any error (worst case = current behaviour).config(libopus can switch SILK↔CELT between frames; acode 3packet requires a uniform config).Alternatives (happy to switch)
pttpath — fixes it for everyone, no API change.repacketizeOggOpusToCode3is exported, so callers can apply it themselves.Tests
src/__tests__/Utils/repacketize-opus.test.ts: a realcode 0fixture → assertscode 3output (config preserved,OpusHeadintact), idempotency, and non-OGG passthrough. The end-to-end fix was validated by sending a UI-recorded voice note to a real iPhone.Related
🤖 Generated with Claude Code
Summary by cubic
Fixes iOS playback for PTT voice notes by repacketizing Opus to TOC code 3 (multi-frame) during media preparation. No API changes; Android behavior is unchanged.
repacketizeOggOpusToCode3to regroupcode 0frames intocode 3, updating OGG granulepos and CRC.prepareWAMessageMediaonly whenptt: true; no-op forcode 1/2/3or non-OGG/Opus, and falls back to the original on any error.OpusHeadand config.code 3, idempotency, and non-OGG passthrough.Written for commit ae88776. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Tests