561 lines
19 KiB
Dart
561 lines
19 KiB
Dart
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<PalRoomPage> createState() => _PalRoomPageState();
|
||
}
|
||
|
||
class _PalRoomPageState extends State<PalRoomPage> {
|
||
final _users = <int>[]; // 用户id数组
|
||
final _infoStrings = <String>[]; // 状态文字信息列表
|
||
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<void> 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);
|
||
}),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|