import 'dart:async'; import 'dart:convert'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:agora_rtc_engine/rtc_engine.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:will_play/utils/auth.dart'; import '../../utils/settings.dart'; import '../../utils/http_util.dart'; import '../../utils/showToast.dart'; // 实现语音主要五个步骤逻辑: // 初始化引擎 // 开启音频权限,启用音频模块 // 创建房间 // 设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) // 布局实现 // 退出语音(根据需要销毁引擎,释放资源) class PalRoomPage extends StatefulWidget { final String agoraToken; final String userName; final String channelName; final ClientRole? role; PalRoomPage({ Key? key, this.agoraToken = '', 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 = [ {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, {'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png'}, ]; // 连麦用户列表数据 List newUserList = []; @override void initState() { super.initState(); _infoStrings.insert(0, { 'type': 'system', 'content': '|令牌:${widget.agoraToken}|用户名:${widget.userName}|房间:${widget.channelName}|身份:${widget.role}' }); initialize(); if (widget.role == ClientRole.Broadcaster) { _infoStrings.insert(0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); } else if (widget.role == ClientRole.Audience) { _infoStrings.insert(0, {'type': 'system', 'content': '您身份为观众,点击麦克风图标上麦'}); } } @override void dispose() { super.dispose(); // _users.clear(); _engine.leaveChannel(); _engine.destroy(); } Future initialize() async { // 判断appid是否存在 if (APP_ID.isEmpty) { setState(() { _infoStrings.insert(0, { 'type': 'system', 'content': 'APP ID缺失,请在settings.dart中提供您的APP ID' }); _infoStrings.insert(0, {'type': 'system', 'content': '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); _addAgoraEventHandlers(); // 调用状态文字信息列表方法 if (widget.role == ClientRole.Broadcaster) { await _engine.setClientRole(ClientRole.Broadcaster); } String uid = await Storage.get('id'); await _engine.joinChannel( widget.agoraToken, widget.channelName, null, int.parse(uid)); // 加入频道 _detail(); } // 获取房间详情接口 void _detail() async { var res = await MyHttpUtil().get("/chat/api/room/detail/${widget.channelName}"); // print('${res}'); var _userList = res.data['userList']; // 赋值image字段头像 _userList.forEach((item) { item['image'] = 'images/palRoom/photo.png'; item['flag'] = true; }); // 筛选出身份类型为主播的用户 newUserList = _userList.where((item) => item['type'] != 'audience').toList(); print('-----------------筛选出身份类型为主播的用户:${newUserList}-----------------'); for (var i = 0; i < _list.length; i++) { if (newUserList.length < (i + 1)) { setState(() { _list[i] = { 'nickname': '', 'flag': false, 'image': 'images/palRoom/voice2.png' }; }); } else { setState(() { _list[i] = newUserList[i]; }); } } print('-----------------${_list}-----------------'); } void _addAgoraEventHandlers() { _engine.setEventHandler(RtcEngineEventHandler( error: (code) { // 错误 setState(() { final info = '错误 $code'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, userJoined: (uid, elapsed) { _detail(); // 用户加入 setState(() { final info = '用户加入: uid $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); // _users.add(uid); }); }, joinChannelSuccess: (channel, uid, elapsed) { _detail(); // 加入频道成功 setState(() { final info = '加入频道成功: $channel, uid: $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, leaveChannel: (stats) { _detail(); // 离开频道 setState(() { _infoStrings.insert(0, {'type': 'system', 'content': '离开频道'}); // _users.clear(); }); }, userOffline: (uid, reason) { _detail(); // 用户离开当前频道 setState(() { final info = '用户离开当前频道: $uid , reason: $reason'; _infoStrings.insert(0, {'type': 'system', 'content': info}); // _users.remove(uid); }); }, clientRoleChanged: (oldRole, newRole) { _detail(); // 直播场景下用户角色切换成功回调 setState(() { final info = '用户旧身份$oldRole,用户新身份$newRole'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, rejoinChannelSuccess: (channel, uid, elapsed) { _detail(); // 成功重新加入频道回调 setState(() { final info = '用户成功重新加入: $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); // _users.add(uid); }); }, warning: (warn) { // 报告 RtcChannel 的警告码 print(warn); }, tokenPrivilegeWillExpire: (token) { // Token 服务将在30s内过期回调 // app 应重新获取 Token,然后调用 _engine.renewToken(token) 将新的 Token 传给 SDK print('即将服务失效的 Token:${token}'); }, requestToken: () { // Token 已过期回调 setState(() { final info = 'Token 已过期'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, )); } // 头部 Widget _head() { // 底部弹框组件 _modelBottomSheet(index) async { showModalBottomSheet( context: context, builder: (context) { return Container( height: 150.0, child: Column( children: [ _list[index]['flag'] == false ? ListTile( title: Text( "连线上麦", textAlign: TextAlign.center, ), onTap: () { // setState(() { // for (var i = 0; i < _list.length; i++) { // if (i == index) { // _list[index]['flag'] = true; // _list[index]['nickname'] = '_users'; // _list[index]['image'] = // 'images/palRoom/photo.png'; // } else { // _list[i]['flag'] = false; // _list[i]['name'] = ''; // _list[i]['image'] = 'images/palRoom/voice2.png'; // } // } // }); // 申请连麦需要改变身份为主播! _engine.setClientRole(ClientRole.Broadcaster); setState(() { _list[index]['flag'] = true; }); Navigator.pop(context); }, ) : ListTile( title: Text( "下麦", textAlign: TextAlign.center, ), onTap: () { setState(() { _list[index]['flag'] = false; _list[index]['nickname'] = ''; _list[index]['image'] = 'images/palRoom/voice2.png'; }); // 下麦再将身份改回为观众(下麦不代表离开频道) _engine.setClientRole(ClientRole.Audience); Navigator.pop(context); }, ), ListTile( title: Text( "取消", textAlign: TextAlign.center, ), onTap: () { Navigator.pop(context); }), ], ), ); }, ); } return Container( child: ListView( children: [ // --房主 start -- 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)), ], ), ), // --房主 end -- // -- 网格 start -- Container( // padding: EdgeInsets.only(left: 15, right: 15), child: GridView.builder( shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisSpacing: 40, mainAxisSpacing: 30, crossAxisCount: 4, childAspectRatio: 1 / 1.25, ), itemCount: _list.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( onTap: () { setState(() { if (widget.role == ClientRole.Audience) { // 观众身份的时候点击上麦克图标 _modelBottomSheet(index); // 调用底部弹框方法 } else { _infoStrings.insert( 0, {'type': 'system', 'content': '您身份为主播,已经上麦状态'}); } }); }, 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]['flag'] ? 55 : 14, height: _list[index]['flag'] ? 55 : 21, ), ), ), Text( _list[index]['nickname'], textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 12), ), ], ), ); }, ), ), // -- 网格 end -- ], ), ); } // 消息列表 Widget _planel() { _filtering(type, content, {name}) { switch (type) { case 'system': return Container( child: Text( '系统公告:$content', style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), ), ); case 'speak': return Container( child: Text( '${name}的聊天消息:$content', style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), ), ); case 'gift': return Container( child: Text( '礼物消息:$content', style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), ), ); } } return Visibility( visible: viewPanel, child: Container( padding: EdgeInsets.symmetric(vertical: 60), alignment: Alignment.bottomCenter, child: FractionallySizedBox( heightFactor: 0.5, child: Container( // decoration: BoxDecoration( // color: Colors.black, // ), 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: _filtering(_infoStrings[index]['type'], _infoStrings[index]['content'], name: _infoStrings[index]['name']), ), ), ], ), ); }, ), ), ), ), ); } // 底部输入框控制台 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( onSubmitted: (value) async { late String speak = value.trim(); if (speak.isEmpty) { showToast("请输入聊天内容~"); return; } String userName = await Storage.get('userName'); // 用户名 setState(() { _infoStrings.insert(0, { 'type': 'speak', 'content': '${speak}', 'name': '${userName}' }); }); }, 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( leading: IconButton( icon: Icon(Icons.arrow_back_ios), onPressed: () { print('离开'); _engine.setClientRole(ClientRole.Audience); _engine.leaveChannel(); Navigator.of(context).pop(); }), 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, 2, 6, 2), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(2)), color: Color.fromRGBO(47, 47, 59, 1), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, 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(), ], ), ); } }