Flutter中如何获取Icecast2网络电台的流信息与封面?
在Flutter中实现Icecast2电台流的播放与信息展示
嘿,刚好我之前做过类似的Flutter Icecast2电台播放器,结合你给出的流信息示例,我来一步步给你拆解实现步骤~
1. 获取Icecast2的流信息
首先你需要从网页端的接口拿到那个JSON数据,Flutter里用http包就能轻松发起请求。先把依赖加到pubspec.yaml里:
dependencies: http: ^1.1.0 flutter: sdk: flutter
然后写个请求方法获取流信息:
import 'package:http/http.dart' as http; import 'dart:convert'; Future<Map<String, dynamic>> fetchIcecastStreamInfo(String apiUrl) async { final response = await http.get(Uri.parse(apiUrl)); if (response.statusCode == 200) { return json.decode(response.body); } else { throw Exception('加载流信息失败'); } }
这里的apiUrl就是你网页端获取数据的地址,Icecast默认的状态接口一般是http://你的服务器地址:端口/status-json.xsl。
2. 解析并展示流信息
拿到返回的JSON后,就可以提取你需要的字段了,比如服务器名称、当前曲目、在线听众这些。结合你给的示例,提取逻辑可以这么写:
// 假设已经通过fetchIcecastStreamInfo拿到了streamInfo var sourceInfo = streamInfo['source'][0]; String serverName = sourceInfo['server_name']; String currentTitle = sourceInfo['title']; int bitrate = sourceInfo['bitrate']; int listeners = sourceInfo['listeners'];
然后把这些信息展示在UI里:
Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('电台名称: $serverName', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('当前播放: $currentTitle'), SizedBox(height: 8), Text('比特率: ${bitrate}kbps | 在线听众: $listeners'), ], )
3. 播放Icecast2音频流
播放网络音频流推荐用just_audio包,它对各种音频流的支持很稳定,还自带播放控制功能。先加依赖:
dependencies: just_audio: ^0.9.34 rxdart: ^0.27.7 # just_audio依赖这个包
然后实现播放逻辑:
import 'package:just_audio/just_audio.dart'; final AudioPlayer _audioPlayer = AudioPlayer(); // 播放流 Future<void> playStream(String streamUrl) async { try { await _audioPlayer.setUrl(streamUrl); await _audioPlayer.play(); } catch (e) { print('播放出错: $e'); } } // 暂停播放 Future<void> pauseStream() async { await _audioPlayer.pause(); } // 页面销毁时记得释放资源 @override void dispose() { _audioPlayer.dispose(); super.dispose(); }
这里的streamUrl就是你的Icecast2流地址,比如http://你的服务器地址:端口/stream。
4. 获取并展示封面图
你提到网页端能获取封面图,分两种情况处理:
- 如果封面图有单独的接口地址,直接用
Image.network加载就行:
// 假设从接口拿到了coverUrl Image.network( coverUrl, width: 200, height: 200, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Icon(Icons.music_note, size: 100); // 加载失败显示默认图标 }, )
- 如果封面图在Icecast的音频元数据里,可以通过
just_audio的监听来获取:
_audioPlayer.metadataStream.listen((metadata) { String? coverUrl = metadata?.artworkUri?.toString(); if (coverUrl != null) { // 更新UI显示封面图 setState(() { _coverUrl = coverUrl; }); } });
完整示例整合
把上面的逻辑整合起来,一个简单的电台播放器页面大概是这样:
import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:just_audio/just_audio.dart'; class IcecastPlayerPage extends StatefulWidget { const IcecastPlayerPage({super.key}); @override State<IcecastPlayerPage> createState() => _IcecastPlayerPageState(); } class _IcecastPlayerPageState extends State<IcecastPlayerPage> { final AudioPlayer _audioPlayer = AudioPlayer(); Map<String, dynamic>? _streamInfo; bool _isLoading = true; bool _isPlaying = false; String? _coverUrl; // 替换成你的服务器地址 final String _apiUrl = 'http://你的Icecast服务器地址/status-json.xsl'; final String _streamUrl = 'http://你的Icecast服务器地址/stream'; @override void initState() { super.initState(); _fetchStreamInfo(); // 监听播放状态变化 _audioPlayer.playerStateStream.listen((state) { setState(() { _isPlaying = state.playing; }); }); } Future<void> _fetchStreamInfo() async { try { final response = await http.get(Uri.parse(_apiUrl)); if (response.statusCode == 200) { setState(() { _streamInfo = json.decode(response.body); _isLoading = false; // 如果封面图URL在流信息里,这里可以提取 // _coverUrl = _streamInfo['source'][0]['cover_url']; }); } else { throw Exception('加载流信息失败'); } } catch (e) { setState(() { _isLoading = false; }); print('获取信息出错: $e'); } } Future<void> _togglePlay() async { if (_isPlaying) { await _audioPlayer.pause(); } else { await _audioPlayer.setUrl(_streamUrl); await _audioPlayer.play(); } } @override void dispose() { _audioPlayer.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('我的Icecast电台')), body: _isLoading ? const Center(child: CircularProgressIndicator()) : Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ // 封面图区域 _coverUrl != null ? Image.network( _coverUrl!, width: 200, height: 200, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon(Icons.music_note, size: 100); }, ) : const Icon(Icons.music_note, size: 100), const SizedBox(height: 20), // 流信息展示 Text( _streamInfo!['source'][0]['server_name'], style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text('当前播放: ${_streamInfo!['source'][0]['title']}'), const SizedBox(height: 8), Text('比特率: ${_streamInfo!['source'][0]['bitrate']}kbps | 在线听众: ${_streamInfo!['source'][0]['listeners']}'), const SizedBox(height: 30), // 播放控制按钮 ElevatedButton.icon( onPressed: _togglePlay, icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), label: Text(_isPlaying ? '暂停' : '播放'), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), fontSize: 18, ), ), ], ), ), ); } }
内容的提问来源于stack exchange,提问作者Alain Seys
相关产品推荐
相关产品推荐

