Flutter just_audio在iOS下无法处理206部分内容响应问题
问题:iOS平台使用just_audio播放返回206状态码的音频异常
问题背景
开发的Flutter音乐应用基于just_audio包开发,Android平台运行正常,但iOS平台播放返回206部分内容状态码的音频时,出现无法播放、无法获取正确时长的问题。点击歌曲调用setNewPlaylist方法时抛出错误:Unhandled Exception: (-1) unknown error。
Song Handler核心代码
import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; import 'package:just_audio/just_audio.dart'; import 'package:http/http.dart' as http; import '../app_export.dart'; class SongHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final AudioPlayer audioPlayer = AudioPlayer(); final playlist = ConcatenatingAudioSource(children: [], useLazyPreparation: true); SongHandler() { initializeAudioSession(); } // 音频会话配置 Future<void> initializeAudioSession() async { final session = await AudioSession.instance; try { await session.configure(const AudioSessionConfiguration.music()); session.interruptionEventStream.listen((event) async { if (event.begin) { switch (event.type) { case AudioInterruptionType.duck: await audioPlayer.setVolume(0.5); break; case AudioInterruptionType.pause: case AudioInterruptionType.unknown: await audioPlayer.pause(); break; } } else { switch (event.type) { case AudioInterruptionType.duck: await audioPlayer.setVolume(1); break; case AudioInterruptionType.pause: await audioPlayer.play(); break; case AudioInterruptionType.unknown: break; } } }); } catch (e, stackTrace) { debugPrint('Error initializing audio session: $stackTrace'); } } // 创建音频源 UriAudioSource createAudioSource(MediaItem mediaItem) { return AudioSource.uri(Uri.parse(mediaItem.extras!['url'] as String), tag: mediaItem); } List<UriAudioSource> createAudioSources(List<MediaItem> mediaItems) { return mediaItems .map( (item) => AudioSource.uri(Uri.parse(item.extras!['url'] as String), tag: item, headers: {}), ) .toList(); } // 设置新播放列表 Future<void> setNewPlaylist(List<MediaItem> mediaItems, int index) async { if (!Platform.isAndroid && audioPlayer.playing) { await audioPlayer.stop(); } var getCount = queue.value.length; await playlist.removeRange(0, getCount); final audioSource = createAudioSources(mediaItems); await playlist.addAll(audioSource); final newQueue = queue.value..addAll(mediaItems); queue.add(newQueue); try { audioPlayer.setAudioSource(playlist, initialIndex: index, initialPosition: Duration.zero); } catch (e) { print("Error loading audio source: $e"); } } }
SongBloc时长更新核心逻辑
class SongBloc extends Bloc<SongEvent, SongState> { final NetworkInfo _networkInfo; final SongHandler songHandler; SongBloc(this._networkInfo, this.songHandler) : super(const SongState().copyWith( songsStatus: SongsStatus.initial, isPlaying: [], mediaItems: [], selectedSongId: "")) { _initializeListeners(); on<SelectSong>(onSelectSong); // 其他事件处理逻辑省略 } void _initializeListeners() { songHandler.audioPlayer.durationStream.listen((duration) { _handleDurationChange(duration); }); // 其他监听器逻辑省略 } Future<void> _handleDurationChange(Duration? duration) async { var index = songHandler.audioPlayer.currentIndex; final newQueue = state.mediaItems; if (index == null || newQueue!.isEmpty || newQueue.length < index) { return; } if (songHandler.audioPlayer.shuffleModeEnabled) { index = songHandler.audioPlayer.shuffleIndices!.indexOf(index); } final oldMediaItem = newQueue[index]; final newMediaItem = oldMediaItem.copyWith(duration: duration); newQueue[index] = newMediaItem; emit(state.copyWith(songDuration: duration, mediaItem: newMediaItem)); } // 选择歌曲事件处理 Future<void> onSelectSong(SelectSong event, Emitter<SongState> emit) async { try { final mediaItems = event.songs.map(MediaConverter.mediaToMediaItem).toList(); final MediaItem mediaItem = mediaItems[event.index]; emit(state.copyWith( selectedSongId: mediaItem.id, selectedIndexSong: event.index, )); if (MediaConverter.areMediaItemsEqual(state.mediaItems!, mediaItems)) { await songHandler.skipToQueueItem(event.index); } else { await songHandler.setNewPlaylist(mediaItems, event.index); List<bool> isPlaying = List.filled(event.songs.length, false); emit(state.copyWith(isPlaying: isPlaying)); } // 主题色生成逻辑省略 songHandler.play(); emit(state.copyWith( songsListEntity: event.songs, currentSong: event.songs[event.index], mediaItem: mediaItem, mediaItems: mediaItems, songsStatus: SongsStatus.selectSong, )); } catch (e) { emit(state.copyWith( songsStatus: SongsStatus.error, error: const PlaybackFailure('Failed to select song', ""), )); } } }
服务器响应头信息
Accept-Ranges: bytes Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept Access-Control-Allow-Origin: * Connection: keep-alive Content-Length: 1000002 Content-Range: bytes 965921-1965921/11279288 Content-Type: audio/mpeg3 Date: Tue, 24 Sep 2024 08:05:29 GMT Server: nginx/1.22.0 X-Powered-By: Express
尝试过的方案及遗留问题
尝试在请求头中添加Range字段,仅当Range设为空时iOS能播放,但此时时长显示为0,进度监听器异常,无法拖动进度条或正常流式播放:
List<UriAudioSource> createAudioSources(List<MediaItem> mediaItems) { return mediaItems .map( (item) => AudioSource.uri(Uri.parse(item.extras!['url'] as String), tag: item, headers: { "Range": "", }), ) .toList(); }
进度条组件代码
import 'package:audio_service/audio_service.dart'; import 'package:audio_video_progress_bar/audio_video_progress_bar.dart'; import '../../../../core/app_export.dart'; import '../bloc/song_bloc.dart'; class SongProgress extends StatelessWidget { final Duration? totalDuration; final Color? color; const SongProgress({ super.key, this.totalDuration, this.color, }); @override Widget build(BuildContext context) { return StreamBuilder<Duration>( stream: AudioService.position, builder: (context, positionSnapshot) { Duration? position = positionSnapshot.data; return ProgressBar( progress: position ?? Duration.zero, buffered: const Duration(seconds: 1), total: totalDuration ?? Duration.zero, onSeek: (position) { BlocProvider.of<SongBloc>(context).songHandler.seek(position); }, progressBarColor: color, baseBarColor: Colors.white.withOpacity(0.24), bufferedBarColor: Colors.white.withOpacity(0.24), thumbColor: Colors.white, barHeight: 3.h, thumbRadius: 5.h, timeLabelTextStyle: Styles.textStyle14.copyWith(fontWeight: FontWeight.bold), ); }, ); } }
解决方案建议
1. 修复Range请求头,强制获取完整音频元数据
iOS底层依赖的AVPlayer对206响应处理逻辑与Android不同,需明确请求完整音频内容来获取元数据。修改createAudioSources方法:
List<UriAudioSource> createAudioSources(List<MediaItem> mediaItems) { return mediaItems.map((item) { final headers = Platform.isIOS ? {"Range": "bytes=0-"} // 请求从开头到结尾的完整内容 : {}; return AudioSource.uri( Uri.parse(item.extras!['url'] as String), tag: item, headers: headers, ); }).toList(); }
该请求头会让服务器返回完整音频(或包含完整元数据的响应),确保iOS能正确解析时长。
2. 手动预获取音频时长并填充MediaItem
如果just_audio仍无法从流中解析时长,可提前通过HEAD请求获取音频总长度:
在SongHandler中添加时长获取方法:
Future<Duration> fetchAudioDuration(String url) async { try { final response = await http.head(Uri.parse(url)); if (response.statusCode == 200 || response.statusCode == 206) { // 从Content-Range提取总字节数 final contentRange = response.headers['content-range']; if (contentRange != null) { final parts = contentRange.split('/'); if (parts.length == 2) { final totalBytes = int.tryParse(parts[1]); if (totalBytes != null) { // 按128kbps比特率计算时长(可根据实际音频调整) const bitRate = 128000; return Duration(seconds: (totalBytes * 8) ~/ bitRate); } } } // 备用:从Content-Length获取 final contentLength = int.tryParse(response.headers['content-length'] ?? ''); if (contentLength != null) { const bitRate = 128000; return Duration(seconds: (contentLength * 8) ~/ bitRate); } } } catch (e) { debugPrint('Failed to fetch duration: $e'); } return Duration.zero; }
然后在创建MediaItem时预填充时长:
// 在MediaConverter.mediaToMediaItem方法中调用 final duration = await songHandler.fetchAudioDuration(song.url); return MediaItem( id: song.id, title: song.title, extras: {'url': song.url}, duration: duration, );
3. 修复进度条的buffered值
当前进度条buffered值硬编码为1秒,需改为从audioPlayer的bufferedPositionStream获取:
class SongProgress extends StatelessWidget { final Duration? totalDuration; final Color? color; final SongHandler songHandler; const SongProgress({ super.key, this.totalDuration, this.color, required this.songHandler, }); @override Widget build(BuildContext context) { return StreamBuilder<Duration>( stream: AudioService.position, builder: (context, positionSnapshot) { final position = positionSnapshot.data ?? Duration.zero; return StreamBuilder<Duration>( stream: songHandler.audioPlayer.bufferedPositionStream, builder: (context, bufferedSnapshot) { final buffered = bufferedSnapshot.data ?? Duration.zero; return ProgressBar( progress: position, buffered: buffered, total: totalDuration ?? Duration.zero, onSeek: (position) => songHandler.seek(position), progressBarColor: color, baseBarColor: Colors.white.withOpacity(0.24), bufferedBarColor: Colors.white.withOpacity(0.24), thumbColor: Colors.white, barHeight: 3.h, thumbRadius: 5.h, timeLabelTextStyle: Styles.textStyle14.copyWith(fontWeight: FontWeight.bold), ); }, ); }, ); } }
4. 强制服务器返回标准MIME类型
服务器返回的Content-Type为audio/mpeg3,iOS更兼容audio/mpeg或audio/mp3,可在请求头中添加Accept字段:
headers: { "Accept": "audio/mpeg", if (Platform.isIOS) "Range": "bytes=0-", },
内容的提问来源于stack exchange,提问作者Bassam Jawish
相关产品推荐
相关产品推荐

