import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:agora_rtc_engine/rtc_engine.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../utils/settings.dart'; // 实现语音主要五个步骤逻辑: // 初始化引擎 // 开启音频权限,启用音频模块 // 创建房间 // 设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) // 布局实现 // 退出语音(根据需要销毁引擎,释放资源) class PalRoomPage extends StatefulWidget { final String userName; final String channelName; final ClientRole? role; PalRoomPage({Key? key, this.userName = '', this.channelName = '', this.role}) : super(key: key); @override State createState() => _PalRoomPageState(); } class _PalRoomPageState extends State { final _users = []; // 用户id数组 final _infoStrings = []; // 状态文字信息列表 bool viewPanel = true; // 是否显示状态文字信息列表 bool muted = false; // 是否开麦 bool volume = true; // 音量 late RtcEngine _engine; // 声网实例变量 // 房间座位 List list = [ {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, {'name': '', 'content': false, 'image': 'images/palRoom/voice2.png'}, ]; @override void initState() { super.initState(); _infoStrings.insert( 0, '最新插入的item-${widget.userName}-${widget.channelName}-${widget.role}'); if (widget.role == ClientRole.Broadcaster) { // 判断若是主播,进入就初始化引擎并连接上麦 initialize(); _infoStrings.insert(0, '您身份为主播,已经上麦状态'); } else if (widget.role == ClientRole.Audience) { _infoStrings.insert(0, '您身份为观众,点击麦克风图标上麦'); // 判断若非主播,进入提示连麦 } } @override void dispose() { super.dispose(); _users.clear(); _engine.leaveChannel(); _engine.destroy(); } Future initialize() async { // 判断appid是否存在 if (APP_ID.isEmpty) { setState(() { _infoStrings.insert(0, 'APP ID缺失,请在settings.dart中提供您的APP ID'); _infoStrings.insert(0, 'Agora引擎没有启动'); }); return; } _engine = await RtcEngine.create(APP_ID); // 初始化引擎 await _engine.enableAudio(); // 启用音频模块 await _engine.setDefaultAudioRouteToSpeakerphone( true); // 设置默认的音频路由:该方法设置接收到的音频从听筒或扬声器出声。如果用户不调用本方法,音频默认从听筒出声。 await _engine.setAudioProfile(AudioProfile.SpeechStandard, AudioScenario.ChatRoomEntertainment); // 设置音频编码属性和音频场景 await _engine.setChannelProfile(ChannelProfile.LiveBroadcasting); await _engine.setClientRole(widget.role!); _addAgoraEventHandlers(); // 调用状态文字信息列表方法 _engine.joinChannel(Token, widget.channelName, null, 0); // 加入频道 } void _addAgoraEventHandlers() { _engine.setEventHandler(RtcEngineEventHandler( error: (code) { // 错误 setState(() { final info = '错误 $code'; _infoStrings.insert(0, info); }); }, joinChannelSuccess: (channel, uid, elapsed) { // 加入频道成功 setState(() { final info = '加入频道: $channel, uid: $uid'; _infoStrings.insert(0, info); }); }, leaveChannel: (stats) { // 离开频道 setState(() { _infoStrings.insert(0, '离开频道'); _users.clear(); }); }, userJoined: (uid, elapsed) { // 用户加入 setState(() { final info = '用户加入: $uid'; _infoStrings.insert(0, info); _users.add(uid); }); }, userOffline: (uid, reason) { // 用户离线 setState(() { final info = '用户离线: $uid , reason: $reason'; _infoStrings.insert(0, info); _users.remove(uid); }); }, firstRemoteVideoFrame: (uid, width, height, elapsed) { // 远程视频第一帧 setState(() { final info = 'First Remote Video Frame: $uid'; _infoStrings.insert(0, info); }); }, )); } Widget _head() { return Container( child: Column( children: [ Container( padding: EdgeInsets.fromLTRB(0, 27, 0, 5), alignment: Alignment.topCenter, child: Image.asset( 'images/palRoom/photo.png', width: 70, height: 70, ), ), Container( margin: EdgeInsets.only(bottom: 30), alignment: Alignment.topCenter, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 18, height: 18, alignment: Alignment.center, decoration: BoxDecoration( color: Color.fromRGBO(245, 173, 29, 1), borderRadius: BorderRadius.circular(2), ), child: Text( '房', style: TextStyle( color: Colors.white, fontSize: 12, ), ), ), SizedBox(width: 5), Text('以冬', style: TextStyle(color: Colors.white, fontSize: 14)), ], ), ), Container( child: GridView.builder( padding: EdgeInsets.all(15), shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisSpacing: 40, mainAxisSpacing: 35, crossAxisCount: 4, childAspectRatio: 1 / 1.2, ), itemCount: list.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( onTap: () { setState(() { if (widget.role == ClientRole.Audience) { // 观众身份的时候点击上麦克图标 _modelBottomSheet(index); // 调用底部弹框方法 } else { _infoStrings.insert(0, '您身份为主播,已经上麦状态'); } }); }, child: Column( children: [ Container( width: 55, height: 55, margin: EdgeInsets.only(bottom: 5), decoration: BoxDecoration( color: Color.fromRGBO(52, 51, 69, 1), borderRadius: BorderRadius.circular(50), ), child: Center( child: Image.asset( list[index]['image'], width: list[index]['content'] ? 55 : 14, height: list[index]['content'] ? 55 : 21, ), ), ), Text( list[index]['name'], textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 12), ), ], ), ); ; }, ), ), ], ), ); } Widget _planel() { return Visibility( visible: viewPanel, child: Container( padding: EdgeInsets.symmetric(vertical: 48), alignment: Alignment.bottomCenter, child: FractionallySizedBox( heightFactor: 0.5, child: Container( // decoration: BoxDecoration( // color: Colors.black, // ), // padding: EdgeInsets.symmetric(vertical: 48), child: ListView.builder( reverse: true, itemCount: _infoStrings.length, itemBuilder: (BuildContext context, int index) { if (_infoStrings.isEmpty) { return Text('null'); } return Padding( padding: EdgeInsets.symmetric(vertical: 5, horizontal: 15), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Container( padding: EdgeInsets.symmetric( vertical: 10, horizontal: 10, ), decoration: BoxDecoration( color: Color.fromRGBO(50, 51, 71, 1), borderRadius: BorderRadius.circular(5), ), child: Text( _infoStrings[index], style: TextStyle( color: Color.fromRGBO(155, 154, 170, 1), ), ), ), ), ], ), ); }, ), ), ), ), ); } Widget _footer() { final _input = TextEditingController(); return Container( width: double.infinity, alignment: Alignment.bottomCenter, padding: EdgeInsets.symmetric( vertical: 18, horizontal: 15, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ GestureDetector( // 音量图标 onTap: () { setState(() { volume = !volume; }); _engine.adjustPlaybackSignalVolume( volume ? 100 : 0); // 调节本地播放的所有远端用户信号音量 }, child: Icon(volume ? Icons.volume_up : Icons.volume_off, color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), GestureDetector( // 麦克风图标 onTap: () { setState(() { muted = !muted; }); _engine.muteLocalAudioStream(muted); // 取消或恢复发布本地音频流(是否静音) }, child: Icon(muted ? Icons.mic_off : Icons.mic, color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), GestureDetector( child: Icon(Icons.tag_faces, color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), Container( width: 220, padding: EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: Color.fromRGBO(52, 51, 69, 1), borderRadius: BorderRadius.circular(30), ), child: TextField( controller: _input, decoration: InputDecoration( isDense: true, border: InputBorder.none, hintText: '我来说几句', hintStyle: TextStyle( fontSize: 12, color: Color.fromRGBO(138, 138, 146, 1), ), ), style: TextStyle( fontSize: 12, color: Colors.white, ), textInputAction: TextInputAction.send, // 键盘右下角图标 ), ), GestureDetector( child: Icon(Icons.add, color: Color.fromRGBO(201, 201, 201, 1), size: 25.0), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( elevation: 0, // z轴阴影 titleSpacing: 0, // 标题与其他控件的间隔 backgroundColor: Color.fromRGBO(27, 28, 48, 1), title: Container( height: 60, decoration: BoxDecoration( color: Color.fromRGBO(27, 28, 48, 1), ), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( child: Padding( padding: EdgeInsets.only(right: 5), child: ClipOval( child: Image.asset( 'images/palRoom/photo.png', width: 46, height: 46, ), ), ), ), Expanded( flex: 1, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( children: [ Text( '以冬', style: TextStyle(color: Colors.white, fontSize: 12), ), SizedBox(width: 5), Text( 'ID309060', style: TextStyle( color: Color.fromRGBO(242, 174, 30, 1), fontSize: 10, ), ), ], ), Row( children: [ Text.rich( TextSpan( style: TextStyle( fontSize: 10, color: Colors.white, ), children: [ TextSpan(text: '交友'), TextSpan(text: ' | '), TextSpan(text: '房间名:成人避风港'), ]), ), SizedBox(width: 5), Container( padding: EdgeInsets.fromLTRB(6, 1, 6, 1), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(2)), color: Color.fromRGBO(47, 47, 59, 1), ), child: Row( children: [ Text( '在线:121', style: TextStyle( fontSize: 10, color: Colors.white, ), ), SizedBox(width: 5), Image.asset( 'images/palRoom/whiteRight.png', width: 5, height: 8, ), ], ), ) ], ), ], ), ) ], ), ), actions: [ IconButton( onPressed: () { print('more图标被点击'); setState(() { viewPanel = !viewPanel; }); }, icon: Image.asset( 'images/palRoom/more.png', fit: BoxFit.cover, width: 20.0, height: 5.0, ), ), ], ), backgroundColor: Color.fromRGBO(27, 28, 48, 1), body: Stack( children: [ _head(), _planel(), _footer(), ], ), ); } // 底部弹框方法 _modelBottomSheet(index) async { showModalBottomSheet( context: context, builder: (context) { return Container( height: 150.0, child: Column( children: [ list[index]['content'] == false ? ListTile( title: Text( "连线上麦", textAlign: TextAlign.center, ), onTap: () { setState(() { for (var i = 0; i < list.length; i++) { if (i == index) { list[index]['content'] = true; list[index]['name'] = '_users'; list[index]['image'] = 'images/palRoom/photo.png'; } else { list[i]['content'] = false; list[i]['name'] = ''; list[i]['image'] = 'images/palRoom/voice2.png'; } } }); initialize(); _engine.joinChannel( Token, widget.channelName, null, 0); // 加入频道 Navigator.pop(context); print("连线上麦:${list[index]['content']}"); }, ) : ListTile( title: Text( "离开下麦", textAlign: TextAlign.center, ), onTap: () { setState(() { list[index]['content'] = false; list[index]['name'] = ''; list[index]['image'] = 'images/palRoom/voice2.png'; }); _engine.leaveChannel(); print("离开下麦:${list[index]['content']}"); Navigator.pop(context); }, ), ListTile( title: Text( "取消", textAlign: TextAlign.center, ), onTap: () { Navigator.pop(context); }), ], ), ); }, ); } }