Skip to content

fix(media): repacketize PTT Opus to code 3 so voice notes play on iOS#2648

Open
alessandropcostabr wants to merge 2 commits into
WhiskeySockets:masterfrom
alessandropcostabr:fix/ptt-ios-opus-code3
Open

fix(media): repacketize PTT Opus to code 3 so voice notes play on iOS#2648
alessandropcostabr wants to merge 2 commits into
WhiskeySockets:masterfrom
alessandropcostabr:fix/ptt-ios-opus-code3

Conversation

@alessandropcostabr

@alessandropcostabr alessandropcostabr commented Jun 18, 2026

Copy link
Copy Markdown

Problem

Voice notes sent with ptt: true play 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:

  • WhatsApp's native voice notes use Opus code 3 (multiple 20 ms SILK frames per packet, e.g. 3×20 ms = 60 ms).
  • libopus/ffmpeg — the usual way to produce the .ogg passed to sendMessage — emit code 0 (one frame per packet).
  • iOS WhatsApp accepts code 3 and rejects code 0 for PTT; Android tolerates both.

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 0 stream is rejected. Code 3 framing is the decisive factor.

TOC reminder (RFC 6716 §3.1): config = toc >> 3, code = toc & 0x03.

How it was isolated (controlled A/B on a real iPhone)

  1. Decrypt a genuine native voice noteconfig 9 / code 3. Re-upload those exact bytes via Baileys (sendMessage({ audio, ptt:true })) → plays on iOS → proves Baileys' transport/upload is correct.
  2. ffmpeg 16 kHz mono opus (config 9 / code 0) via Baileys → iOS rejects.
  3. Re-packetize that same ffmpeg output to code 3 → via Baileys → plays on iOS.

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 in prepareWAMessageMedia only on the ptt path:

  • Regroups consecutive same-config code 0 frames into code 3 packets (default 3 frames/packet), rewriting OGG pages with recomputed granulepos and CRC-32.
  • Idempotent — returns the input untouched if it is already code 3 or not OGG/Opus.
  • SaferepacketizePttOpus buffers the media and falls back to the original on any error (worst case = current behaviour).
  • Grouping respects the per-frame config (libopus can switch SILK↔CELT between frames; a code 3 packet requires a uniform config).

Alternatives (happy to switch)

  • (this PR) default on the ptt path — fixes it for everyone, no API change.
  • opt-in — gate behind a media option if you'd rather not change default behaviour (one line).
  • utility onlyrepacketizeOggOpusToCode3 is exported, so callers can apply it themselves.

Tests

src/__tests__/Utils/repacketize-opus.test.ts: a real code 0 fixture → asserts code 3 output (config preserved, OpusHead intact), 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.

  • Bug Fixes
    • Adds repacketizeOggOpusToCode3 to regroup code 0 frames into code 3, updating OGG granulepos and CRC.
    • Applies repacketization in prepareWAMessageMedia only when ptt: true; no-op for code 1/2/3 or non-OGG/Opus, and falls back to the original on any error.
    • Groups only same-config frames to avoid SILK/CELT mixing; defaults to 3 frames/packet (validated 1–48); preserves OpusHead and config.
    • Adds a unit test verifying conversion to code 3, idempotency, and non-OGG passthrough.

Written for commit ae88776. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved Push-to-Talk (PTT) voice note handling by repacketizing Opus audio for better compatibility and more reliable playback, especially on iOS.
    • Automatically applies the update only to PTT voice notes; other audio types are unaffected.
  • Bug Fixes

    • Added resilient fallback behavior: if repacketization isn’t possible, the original voice note audio is used to avoid transmission failures.
  • Tests

    • Added Jest coverage to verify correct repacketization, idempotency, and non-OGG no-op behavior.

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>
@whiskeysockets-bot

whiskeysockets-bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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 works

If you’ve tested this PR, please comment below with:

Tested and working ✅

This helps us speed up the review and merge process.

📦 To test this PR locally:

# NPM
npm install @whiskeysockets/baileys@alessandropcostabr/Baileys#fix/ptt-ios-opus-code3

# Yarn (v2+)
yarn add @whiskeysockets/baileys@alessandropcostabr/Baileys#fix/ptt-ios-opus-code3

# PNPM
pnpm add @whiskeysockets/baileys@alessandropcostabr/Baileys#fix/ptt-ios-opus-code3

If you encounter any issues or have feedback, feel free to comment as well.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d986b2a6-050b-40ad-9f78-0e54b782ebc2

📥 Commits

Reviewing files that changed from the base of the PR and between fd3ecf9 and ae88776.

📒 Files selected for processing (1)
  • src/Utils/messages-media.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Utils/messages-media.ts

📝 Walkthrough

Walkthrough

Adds 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 repacketizeOggOpusToCode3 and repacketizePttOpus are exposed, the latter is wired into prepareWAMessageMedia for PTT audio, and Jest tests validate conversion, idempotency, and pass-through behavior.

Changes

PTT Opus Repacketization

Layer / File(s) Summary
OGG/Opus repacketization core and PTT helper
src/Utils/messages-media.ts
Adds OGG CRC computation, page parser, OpusHead pre-skip extractor, Opus TOC frame-duration calculator, OGG page builder, repacketizeOggOpusToCode3 (idempotent, guards for non-OGG/non-Opus/already-code-3 input), and repacketizePttOpus (buffers upload, applies repacketization, falls back on error).
PTT integration in prepareWAMessageMedia
src/Utils/messages.ts
Imports repacketizePttOpus and adds a conditional call inside prepareWAMessageMedia that overwrites uploadData.media when mediaType === 'audio' and ptt is true.
Jest tests for repacketizeOggOpusToCode3
src/__tests__/Utils/repacketize-opus.test.ts
Adds a suite with a base64 real 16kHz mono Ogg/Opus fixture, a page-scanning TOC helper, and three tests: code-0-to-code-3 conversion with config preservation, idempotency, and pass-through for non-OGG input.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 Hop hop, the packets align,
From code zero to code three they shine!
The Ogg pages rebuild anew,
With CRC checksums fresh and true.
iOS ears now hear the call—
One bunny's fix benefits all! 🎵

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: repacketizing PTT Opus packets to code 3 for iOS voice note compatibility.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 75d6588 and fd3ecf9.

📒 Files selected for processing (3)
  • src/Utils/messages-media.ts
  • src/Utils/messages.ts
  • src/__tests__/Utils/repacketize-opus.test.ts

Comment thread src/Utils/messages-media.ts
Comment thread src/Utils/messages-media.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/Utils/messages.ts
// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread src/Utils/messages.ts
// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread src/Utils/messages-media.ts
Comment thread src/Utils/messages-media.ts
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>
@alessandropcostabr

Copy link
Copy Markdown
Author

Thanks @coderabbitai — both addressed in the latest commit:

  1. framesPerPacket validation: now throws for non-integers / values outside 1..48 (RFC 6716 caps frames-per-packet at 48), preventing the non-terminating loop.
  2. code 1/2 packets: the function now only repacketizes a stream where every audio packet is code 0 ((toc & 0x03) === 0); any code 1/2/3 stream is returned untouched, so non-code 0 layouts are never misinterpreted. This also subsumes the previous code-3 idempotency check.

Tests still green (code 0 → code 3, idempotency, non-OGG passthrough).

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@alessandropcostabr: Both fixes look solid — thanks for the detailed follow-up!

  • The framesPerPacket guard (1..48, integer-only) correctly mirrors the RFC 6716 constraint and eliminates the infinite-loop risk.
  • Requiring all audio packets to be code 0 before repacketizing is the right conservative approach: it keeps the function strictly correct for the targeted case (ffmpeg/libopus output) while leaving anything more exotic untouched, and it neatly folds in the old idempotency shortcut.

(๑˃ᴗ˂)ﻭ 🐇


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot added the Stale label Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants