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,