You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 18:17:48