From 377ce1af8023d768193c3fb3994f83f2a1c19fca Mon Sep 17 00:00:00 2001 From: lavigarg-simform Date: Thu, 25 Jun 2026 19:17:06 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Play=20mislabeled=20audio?= =?UTF-8?q?=20files=20on=20iOS/macOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AVFoundation routes strictly by file extension on iOS/macOS. Android can record AAC-in-MP4 yet name the file `.mp3`/`.aac`, so the player either fails to load or silently mis-parses it (e.g. an 8ms bogus duration that never throws). Sniff the real container from the file's leading magic bytes in Dart and,when it disagrees with the extension, prepare the player from a temp symlink carrying the correct extension so the right parser is chosen. Doing this once in the Dart layer covers iOS, macOS, and the waveform extractor path without duplicating logic across the native players. --- CHANGELOG.md | 1 + doc/documentation.md | 7 +++ lib/src/base/utils.dart | 64 ++++++++++++++++++++++ lib/src/controllers/player_controller.dart | 4 ++ 4 files changed, 76 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3beb9051..1bd9674c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 2.1.0 (#unreleased) - Feature [#468](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/pull/468) - Add macOS support +- Fixed [#113](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/issues/113) - Play audio files with mislabeled extensions on iOS/macOS ## 2.0.2 diff --git a/doc/documentation.md b/doc/documentation.md index a130d796..d799b20b 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -490,6 +490,13 @@ await playerController.preparePlayer( Currently, playing remote audio files directly isn't supported. You will need to download the file first, then play it locally. +### Mislabeled File Extensions (iOS/macOS) + +iOS/macOS pick the audio parser from the file extension, so a file whose extension doesn't match +its real container (e.g. an Android AAC-in-MP4 recording named `.mp3`/`.aac`) fails to load. +`preparePlayer` detects this from the file's magic bytes and handles it automatically — no action +needed. + ## Start Playing Start playing the audio: diff --git a/lib/src/base/utils.dart b/lib/src/base/utils.dart index 6073b289..a9b6b6f7 100644 --- a/lib/src/base/utils.dart +++ b/lib/src/base/utils.dart @@ -204,3 +204,67 @@ enum WaveformRenderMode { /// Checks if the current platform is iOS or macOS. bool get isIosOrMacOS => Platform.isIOS || Platform.isMacOS; + +/// Sniffs an audio file's container from its leading magic bytes and returns +/// the file extension AVFoundation needs to pick the right parser, or `null` +/// when the container isn't recognised. +Future sniffAudioExtension(String path) async { + RandomAccessFile? raf; + try { + raf = await File(path).open(); + final b = await raf.read(12); + if (b.length < 4) return null; + String ascii(int start, int end) => + end <= b.length ? String.fromCharCodes(b.sublist(start, end)) : ''; + // MP4 / M4A container: bytes 4..8 == "ftyp" (covers AAC-in-MP4). + if (ascii(4, 8) == 'ftyp') return 'm4a'; + if (ascii(0, 4) == 'RIFF' && ascii(8, 12) == 'WAVE') return 'wav'; + if (ascii(0, 4) == 'fLaC') return 'flac'; + if (ascii(0, 4) == 'OggS') return 'ogg'; + if (ascii(0, 3) == 'ID3') return 'mp3'; + // Raw frame sync: 0xFFFx = ADTS AAC, 0xFFEx = MP3. + if (b[0] == 0xFF) { + if ((b[1] & 0xF6) == 0xF0) return 'aac'; + if ((b[1] & 0xE0) == 0xE0) return 'mp3'; + } + return null; + } catch (_) { + return null; + } finally { + await raf?.close(); + } +} + +/// On iOS/macOS, AVFoundation routes strictly by file extension. Android can +/// record AAC-in-MP4 yet name the file `.mp3`/`.aac` (issue #113), which then +/// fails to load or silently mis-parses (e.g. an 8ms bogus duration). When the +/// extension disagrees with the container sniffed from magic bytes, this returns +/// a temp symlink carrying the correct extension so the right parser is chosen; +/// otherwise it returns [path] unchanged. No-op on other platforms. +Future normalizedAudioPath(String path) async { + if (!isIosOrMacOS) return path; + final ext = await sniffAudioExtension(path); + if (ext == null) return path; + final dot = path.lastIndexOf('.'); + final currentExt = dot == -1 ? '' : path.substring(dot + 1).toLowerCase(); + if (currentExt == ext) return path; + try { + final link = + Link('${Directory.systemTemp.path}/awf_sniff_${path.hashCode}.$ext'); + final type = await FileSystemEntity.type(link.path); + if (type == FileSystemEntityType.link) { + // Reuse only when it already targets this exact file; otherwise the + // name collided with another path, so re-point it. + if (await link.target() == path) return link.path; + await link.delete(); + } else if (type != FileSystemEntityType.notFound) { + // A stale regular file/dir is squatting on the name; clear it so the + // link can be created instead of silently disabling the fix. + await File(link.path).delete(); + } + await link.create(path); + return link.path; + } catch (_) { + return path; + } +} diff --git a/lib/src/controllers/player_controller.dart b/lib/src/controllers/player_controller.dart index 08c4b0c6..30b5705e 100644 --- a/lib/src/controllers/player_controller.dart +++ b/lib/src/controllers/player_controller.dart @@ -154,6 +154,10 @@ class PlayerController extends ChangeNotifier { throw ArgumentError('Invalid path format: $path'); } path = uri.path; + // A wrong extension (e.g. Android AAC-in-MP4 named `.mp3`, issue #113) + // makes AVFoundation pick the wrong parser on iOS/macOS. Redirect to a + // correctly-named symlink when the sniffed container disagrees. + path = await normalizedAudioPath(path); } final isPrepared = await AudioWaveformsInterface.instance.preparePlayer( path: path,