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'; // 实现语音主要五个步骤逻辑: // 初始化引擎 // 开启音频权限,启用音频模块 // 创建房间 // 设置事件监听(成功加入房间,是否有用户加入,用户是否离开,用户是否掉线) // 布局实现 // 退出语音(根据需要销毁引擎,释放资源) 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 = [ {'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'}, ]; // 在线用户数据 List _userList = []; @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'); print(widget.agoraToken); print(widget.channelName); print(uid); await _engine.joinChannel( widget.agoraToken, widget.channelName, null, int.parse(uid)); // 加入频道 } // 获取房间详情接口 void _detail() async { var res = await MyHttpUtil().get("/chat/api/room/detail/${widget.channelName}"); print('_detail接口:${res}'); setState(() { this._userList = res.data.userList; }); } void _addAgoraEventHandlers() { _engine.setEventHandler(RtcEngineEventHandler(error: (code) { // 错误 setState(() { final info = '错误 $code'; _infoStrings.insert(0, {'type': 'system', 'content': info}); }); }, userJoined: (uid, elapsed) { // 用户加入 setState(() { final info = '用户加入: uid $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); _users.add(uid); print('用户加入:'); _detail(); }); }, joinChannelSuccess: (channel, uid, elapsed) { // 加入频道成功 setState(() { final info = '加入频道: $channel, uid: $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); print('加入频道成功:'); _detail(); }); }, leaveChannel: (stats) { // 离开频道 setState(() { _infoStrings.insert(0, {'type': 'system', 'content': '离开频道'}); _users.clear(); print('离开频道:'); _detail(); }); }, userOffline: (uid, reason) { // 用户离线 setState(() { final info = '用户离线: $uid , reason: $reason'; _infoStrings.insert(0, {'type': 'system', 'content': info}); _users.remove(uid); print('用户离线:'); _detail(); }); }, clientRoleChanged: (oldRole, newRole) { // 直播场景下用户角色切换成功回调 setState(() { final info = '用户旧身份$oldRole,用户新身份$newRole'; _infoStrings.insert(0, {'type': 'system', 'content': info}); print('直播场景下用户角色切换成功回调:'); _detail(); }); }, rejoinChannelSuccess: (channel, uid, elapsed) { // 成功重新加入频道回调 setState(() { final info = '用户成功重新加入: $uid'; _infoStrings.insert(0, {'type': 'system', 'content': info}); _users.add(uid); print('成功重新加入频道回调:'); _detail(); }); }, 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]['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'; } } }); // 申请连麦需要改变身份为主播! _engine.setClientRole(ClientRole.Broadcaster); 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.setClientRole(ClientRole.Audience); Navigator.pop(context); print("下麦:${_list[index]['content']}"); }, ), ListTile( title: Text( "取消", textAlign: TextAlign.center, ), onTap: () { Navigator.pop(context); }), ], ), ); }, ); } return Container( child: ListView( 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( // 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]['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() { _filtering(type, content) { switch (type) { case 'system': return Container( child: Text( '系统公告:$content', style: TextStyle(color: Color.fromRGBO(155, 154, 170, 1)), ), ); case 'speak': return Container( child: Text( '聊天消息:$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']), ), ), ], ), ); }, ), ), ), ), ); } // 底部输入框控制台 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, 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(), ], ), ); } }