fix(whatsapp): corrige content-type audio/opus e extensão para OGG

- MediaHandler: adiciona sanitize_content_type que normaliza audio/opus → audio/ogg
- MediaHandler: detect_extension retorna .ogg (não .mp3) para áudios WhatsApp
- MediaHandler: final_filename força extensão .ogg em áudios que chegam com .mp3
- Attachment: normalize_opus_blob_content_type! agora verifica apenas content_type
  (remove checagem de extensão de filename que impedia normalização de blobs .mp3)
- Attachment: audio_metadata chama normalize_opus_blob_content_type! para corrigir
  blobs existentes na primeira vez que são acessados (lazy fix)

WhatsApp envia áudio como container OGG/Opus (bytes OggS = 4f 67 67 53),
mas declarava mimetype audio/opus. Browsers não conseguem reproduzir container
OGG via MIME audio/opus — precisam de audio/ogg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rodrigo Borba 2026-02-28 18:40:06 -03:00
parent ec6cfc317d
commit 5d3ce4e565
2 changed files with 17 additions and 6 deletions

View File

@ -114,6 +114,7 @@ class Attachment < ApplicationRecord
end
def audio_metadata
normalize_opus_blob_content_type!
audio_file_data = base_data.merge(file_metadata)
audio_file_data.merge(
{
@ -205,12 +206,12 @@ class Attachment < ApplicationRecord
{ host: host, protocol: uri.scheme }
end
# Marcel gem may detect OGG/Opus files as audio/opus instead of audio/ogg.
# Lazily normalize existing blobs so presigned URLs serve the correct Content-Type.
# Only applies to .ogg files — .opus files legitimately use audio/opus.
# WhatsApp audio arrives as OGG/Opus container. Some paths save it as audio/opus
# (raw Opus MIME type) which browsers cannot play via <audio> tag.
# Lazily normalise to audio/ogg on first access, regardless of filename extension.
def normalize_opus_blob_content_type!
blob = file.blob
return unless blob.content_type == 'audio/opus' && blob.filename.to_s.end_with?('.ogg')
return unless blob.content_type == 'audio/opus'
blob.update_column(:content_type, 'audio/ogg') # rubocop:disable Rails/SkipsModelValidations
end

View File

@ -14,7 +14,7 @@ class Whatsapp::Wuzapi::MediaHandler
file: {
io: file_io,
filename: final_filename(attachment_data),
content_type: attachment_data[:mimetype] || 'application/octet-stream'
content_type: sanitize_content_type(attachment_data[:mimetype], @parser.message_type)
}
)
end
@ -68,12 +68,22 @@ class Whatsapp::Wuzapi::MediaHandler
name = data[:file_name] || "wuzapi_#{Time.now.to_i}"
ext = File.extname(name)
ext = detect_extension(data[:mimetype], @parser.message_type) if ext.blank?
# Normalise audio extension: WhatsApp always sends OGG/Opus, never MP3
ext = '.ogg' if @parser.message_type == :audio && ext == '.mp3'
"#{File.basename(name, '.*')}#{ext}"
end
def sanitize_content_type(mimetype, type)
# WhatsApp audio is OGG-wrapped Opus. audio/opus is raw Opus (wrong container header).
# Saving as audio/ogg ensures browsers can play via <audio> without issues.
return 'audio/ogg' if type == :audio && mimetype.to_s.include?('opus')
mimetype || 'application/octet-stream'
end
def detect_extension(mimetype, type)
return '.jpg' if type == :image || type == :sticker
return '.mp3' if type == :audio
return '.ogg' if type == :audio
return '.mp4' if type == :video
case mimetype