Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 2.1.0 (#unreleased)

- Feature [#468](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/pull/468) - Add macOS support
- Feature [#54](https://github.com/SimformSolutionsPvtLtd/audio_waveforms/issues/54) - Support waveform extraction from remote URLs on iOS and macOS

## 2.0.2

Expand Down
14 changes: 12 additions & 2 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,8 +487,18 @@ await playerController.preparePlayer(

### Loading from Network

Currently, playing remote audio files directly isn't supported. You will need to download the file
first, then play it locally.
Remote audio files can be loaded directly by passing an `http`/`https` URL as the `path`:

```dart
await playerController.preparePlayer(
path: 'https://example.com/audio.mp3',
shouldExtractWaveform: true,
);
```

Playback streams the URL on all platforms. Waveform extraction also works from a
remote URL: on iOS/macOS the file is downloaded to a temporary file before
decoding, while Android decodes the network stream directly.

## Start Playing

Expand Down
45 changes: 45 additions & 0 deletions lib/src/base/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,48 @@ enum WaveformRenderMode {

/// Checks if the current platform is iOS or macOS.
bool get isIosOrMacOS => Platform.isIOS || Platform.isMacOS;

/// Downloads a remote audio [url] to a temporary file and returns its path.
///
/// Waveform extraction on iOS/macOS uses `AVAudioFile`, which can only read
/// local files, so a remote URL must be downloaded before it can be decoded.
/// The original file extension is preserved so the native decoder can infer
/// the audio format. The caller is responsible for deleting the returned file
/// once extraction is finished.
Future<String> downloadAudioToTempFile(String url) async {
final uri = Uri.parse(url);
final httpClient = HttpClient();
try {
final request = await httpClient.getUrl(uri);
final response = await request.close();
if (response.statusCode != HttpStatus.ok) {
throw HttpException(
'Failed to download audio for waveform extraction '
'(status ${response.statusCode}).',
uri: uri,
);
}
final name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : '';
final extension = name.contains('.') ? '.${name.split('.').last}' : '';
final tempPath = '${Directory.systemTemp.path}/audio_waveforms_'
'${DateTime.now().microsecondsSinceEpoch}$extension';
final file = File(tempPath);
try {
await response.pipe(file.openWrite());
} catch (_) {
// Remove the partially written file so a failed download doesn't leave
// an orphan in the temp directory, then rethrow.
if (file.existsSync()) {
try {
await file.delete();
} catch (_) {
// Best-effort cleanup; ignore failures.
}
}
rethrow;
}
return file.path;
} finally {
httpClient.close();
}
}
1 change: 1 addition & 0 deletions lib/src/controllers/player_controller.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
Expand Down
34 changes: 29 additions & 5 deletions lib/src/controllers/waveform_extraction_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,35 @@ class WaveformExtractionController {
actualNoOfSamples = noOfSamples ?? 100;
}

return await AudioWaveformsInterface.instance.extractWaveformData(
key: _extractorKey,
path: path,
noOfSamples: actualNoOfSamples,
);
// Waveform extraction on iOS/macOS uses AVAudioFile, which can only read
// local files. Remote URLs are downloaded to a temporary file first and
// cleaned up afterwards. Android's MediaExtractor handles network URIs
// directly, so the path is passed through unchanged there.
var effectivePath = path;
File? tempFile;
final uri = Uri.tryParse(path);
final isRemote =
uri != null && (uri.isScheme('http') || uri.isScheme('https'));
if (isIosOrMacOS && isRemote) {
effectivePath = await downloadAudioToTempFile(path);
tempFile = File(effectivePath);
}

try {
return await AudioWaveformsInterface.instance.extractWaveformData(
key: _extractorKey,
path: effectivePath,
noOfSamples: actualNoOfSamples,
);
} finally {
if (tempFile != null && tempFile.existsSync()) {
try {
await tempFile.delete();
} catch (_) {
// Best-effort cleanup; ignore failures.
}
}
}
}

/// Stops current waveform extraction, if any.
Expand Down