效果展示
实现细节包括:视频的呼叫和接听,来电时的响铃,画面切换等。先看效果图,相关视频界面截图如下:
准备工作
● 创建声网账户,并获取App ID,☞官网
● 用到的声网Flutter插件有两个:
1> agora_rtc_engine(https://github.com/AgoraIO/Flutter-RTM)
主要实现视频通话部分的插件,点击官方demo查看代码示例。
2>agora_rtm(https://github.com/AgoraIO/Flutter-RTM)
主要实现呼叫的信令系统插件,点击官方demo可查看代码示例。
功能细节:登录、收发消息
代码部分
配置
- 在项目根目录下的 pubspec.yaml 文件中添加插件:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
agora_rtc_engine: 0.9.4
agora_rtm: 0.9.3
- 在 android-app-build.gradle 中添加
android {
..
defaultConfig {
..
ndk {
abiFilters 'armeabi-v7a'
}
..
}
..
}
- 在android 的 AndroidManifest.xml 文件中添加权限:
..
..
- 在 ios 的 info.plist 文件中添加
Privacy - Microphone Usage Description, and add a note in the Value column.
Privacy - Camera Usage Description, and add a note in the Value column.
视频相关
- 初始化
AgoraRtmClient _client;
/// 收到通话请求时的响铃页面
VideoAnswerPage answer;
/// 声网RTM初始化、注册接收
Future initAgoraRtm() async {
// 初始化
_client =await AgoraUtils.getAgoraRtmClient();
// 设置消息接收器
_client.onMessageReceived = (AgoraRtmMessage message, String peerId) {
if(!EmptyUtil.textIsEmpty(message.text)){
// 收到视频请求,消息内容定义为: “CALLVIDEO,视频通道id”(请求者id接收者id)
if(message.text.contains(AgoraUtils.getAgoraMsgType(1)) && message.text.contains(",")){
try{
String _channelName =message.text.split(",")[1];
// 收到通话请求时的响铃页面
answer = new VideoAnswerPage(_channelName,peerId,_client);
// 跳到响铃页面
Navigator.push(c, new MaterialPageRoute(
builder: (BuildContext context) {
return answer;
}));
}catch(e){
print(e.toString());
}
// 收到消息:视频请求者取消了通话
}else if(message.text.contains(AgoraUtils.getAgoraMsgType(2)) ){
if(answer != null){
answer.videoAnswerState.isClosedByOne = true;
answer.videoAnswerState.onCallEnd(c);
}
// 对方拒绝了通话请求
}else if(message.text.contains(AgoraUtils.getAgoraMsgType(3)) ){
if(AgoraUtils.videoCallState != null){
AgoraUtils.videoCallState.isClosedByOne = true;
AgoraUtils.videoCallState.onCallEnd(c);
}
}
}
};
_client.onConnectionStateChanged = (int state, int reason) {
// _log('Connection state changed: ' + state.toString() + ', reason: ' + reason.toString());
if (state == 5) {
_client.logout();
}
};
}
- 登录
自定义user id 提交登录
/// 声网登录
void _toggleLogin() async {
if (!_isLogin) {
// 获取输入框的user id(英文 || 数字)
String userId = _userNameController.text;
if (userId.isEmpty) {
Fluttertoast.showToast(msg: "Please input your user id to login");
return;
}
if(_client == null){
return;
}
try {
await _client.login(null, userId);
setState(() {
_isLogin = true;
});
//
User user = new User();
user.agoraId = userId;
// 保存用户信息
ConstantObject.mUser = user;
} catch (errorCode) {
print(errorCode);
}
}
}
- 请求视频通话
class AgoraCustomPage extends StatefulWidget {
@override
createState() => new AgoraCustomState();
}
class AgoraCustomState extends State {
TextEditingController _friendController = new TextEditingController();
TextEditingController _groupController = new TextEditingController();
String _channelName = "zhijie";
AgoraRtmClient _client;
@override
void initState() {
super.initState();
initAgoraRtm();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("声网"),
),
body: buildStartPage(),
);
}
Widget buildStartPage(){
return SingleChildScrollView(
child: ConstrainedBox(// 添加额外为限制条件到child,如最小/大宽度、高度。。。
constraints: BoxConstraints(
minHeight: 120.0,
),
child: Column(children: [
Container(
width: double.infinity,
margin: const EdgeInsets.fromLTRB(20,30,20,0),
child: TextField(
controller: _friendController,
autofocus: false,
decoration: InputDecoration(
icon: Icon(Icons.person),
labelText: '请输入好友id',
helperText: '请正确输入好友的id',
),
)
),
Container(
width: double.infinity,
height: 50,
margin: const EdgeInsets.fromLTRB(20,30,20,0),
child: RaisedButton(
onPressed: (){
clickFriendVideo();
},
// 文本内容
child: Text("和好友视频通话"),
// 按钮颜色
color: ThemeColors.colorTheme,
))
]),),
);
}
void clickFriendVideo(){
if(EmptyUtil.textIsEmpty(_friendController.text)){
Fluttertoast.showToast(msg: "Friend id cannot be empty");
}else{
_callVideo(_friendController.text);
}
}
// 发送视频通话请求
Future _callVideo(String peerId) async {
if(_client != null){
// 查看对方是否在线
bool online =await AgoraUtils.queryPeerOnlineStatus(_client, peerId);
if(online){
try{
// 自定义视频通道(这里采取 自己的agoraid+对方的id),
_channelName = ConstantObject.getUser().agoraId+peerId;
// 发送消息 : “CALLVIDEO,_channelName”
String msg = AgoraUtils.getAgoraMsgType(1)+","+_channelName;
await _client.sendMessageToPeer(peerId, AgoraRtmMessage(msg));
// 跳到拨打视频的等待接听页面
Navigator.push(context, new MaterialPageRoute(
builder: (BuildContext context) {
return new VideoCallPage(_channelName,peerId);
}));
}catch(e){
print(e.toString());
}
}else{
Fluttertoast.showToast(msg: "The friend is offline");
}
}
}
/// 获取AgoraRtmClient
Future initAgoraRtm() async {
_client =await AgoraUtils.getAgoraRtmClient();
}
}
- 拨打视频的等待接听页面(及通话页面)
class VideoCallPage extends StatefulWidget {
/// 视频通道
final String channelName;
/// 好友的 agora Id
final String firendName;
VideoCallPage(this.channelName, this.firendName);
/*/// Creates a call page with given channel name.
const VideoCallPage({Key key, this.channelName, this.firendName}) : super(key: key);*/
@override
createState() => new VideoCallState();
}
class VideoCallState extends State {
/// 和android本地交互的通道
static const _methodChannel1 = const MethodChannel(MethodChannelUtils.channelMedia);
static final _sessions = List();
final _infoStrings = [];
BuildContext mcontext;
AgoraRtmClient _client;
/// 声网上获取的App ID
var APP_ID = APPApiKey.Agora_app_id;
bool muted = false;
/// 视频是否成功接通
bool videoSuccess = false;
/// 发出视频请求但未接通时,自己取消通话
bool isClosedByOne = false;
/// 主窗口展示自己?
/// true 展示自己 false 展示好友
bool mainWindowShowOneself = true;
/// 计时的数值
int _count = 0;
Timer _timer;
@override
void dispose() {
// clean up native views & destroy sdk
_sessions.forEach((session) {
AgoraRtcEngine.removeNativeView(session.viewId);
});
_sessions.clear();
AgoraRtcEngine.leaveChannel();
// 停止播放响铃
stopPlay();
if(!isClosedByOne && !videoSuccess){
/// 请求视频对方还未接听时,自己先取消,则需要通知对方,我已取消
_initSendMessage();
}
stopTimer();
AgoraUtils.clearVideoCallState();
super.dispose();
}
@override
void initState() {
super.initState();
// initialize third.agora sdk
initialize();// 初始化视频SDK
startPlay();// 开始播放响铃
}
void initialize() {
if (APP_ID.isEmpty) {
setState(() {
_infoStrings
.add("APP_ID missing, please provide your APP_ID in settings.dart");
_infoStrings.add("Agora Engine is not starting");
});
return;
}
_initAgoraRTM();// 信令系统
_initAgoraRtcEngine();// 视频通话
_addAgoraEventHandlers();
// use _addRenderView everytime a native video view is needed
_addRenderView(0, (viewId) {
AgoraRtcEngine.setupLocalVideo(viewId, VideoRenderMode.Hidden);
AgoraRtcEngine.startPreview();
// state can access widget directly
// 加入视频通话(或者可以考虑在对方接听后发个消息通知请求方,请求方再加入视频,可以节省点视频分钟数)
AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
});
}
/// 获取 AgoraRtmClient
Future _initAgoraRTM() async{
_client =await AgoraUtils.getAgoraRtmClient();
AgoraUtils.videoCallState = this;
}
/// 发送消息通知对方取消通话
Future _initSendMessage() async{
String msg = AgoraUtils.getAgoraMsgType(2);
await _client.sendMessageToPeer(widget.firendName, AgoraRtmMessage(msg));
}
/// Create third.agora sdk instance and initialze
Future _initAgoraRtcEngine() async {
AgoraRtcEngine.create(APP_ID);
AgoraRtcEngine.enableVideo();
}
/// Add third.agora event handlers
void _addAgoraEventHandlers() {
AgoraRtcEngine.onError = (int code) {
setState(() {
String info = 'onError: ' + code.toString();
_infoStrings.add(info);
});
};
/// 成功加入某次视频的回调
AgoraRtcEngine.onJoinChannelSuccess =
(String channel, int uid, int elapsed) {
setState(() {
String info = 'onJoinChannel: ' + channel + ', uid: ' + uid.toString();
_infoStrings.add(info);
});
};
AgoraRtcEngine.onLeaveChannel = () {
setState(() {
_infoStrings.add('onLeaveChannel');
});
};
/// 有其他用户(好友)成功加入到视频中的回调
AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {
setState(() {
String info = 'userJoined: ' + uid.toString();
//setState(() { videoSuccess = true; });
_infoStrings.add(info);
videoSuccess = true;// 成功开始视频通话
stopPlay(); // 停止播放响铃
startTimer(); // 开始通话计时
_addRenderView(uid, (viewId) {
AgoraRtcEngine.setupRemoteVideo(viewId, VideoRenderMode.Hidden, uid);
});
});
};
/// 好友退出通话
AgoraRtcEngine.onUserOffline = (int uid, int reason) {
setState(() {
String info = 'userOffline: ' + uid.toString();
_infoStrings.add(info);
onCallEnd(mcontext);// 自己也退出
_removeRenderView(uid);
});
};
AgoraRtcEngine.onFirstRemoteVideoFrame =
(int uid, int width, int height, int elapsed) {
setState(() {
String info = 'firstRemoteVideo: ' +
uid.toString() +
' ' +
width.toString() +
'x' +
height.toString();
_infoStrings.add(info);
});
};
}
/// Create a native view and add a new video session object
/// The native viewId can be used to set up local/remote view
void _addRenderView(int uid, Function(int viewId) finished) {
Widget view = AgoraRtcEngine.createNativeView(uid, (viewId) {
setState(() {
_getVideoSession(uid).viewId = viewId;
if (finished != null) {
finished(viewId);
}
});
});
VideoSession session = VideoSession(uid, view);
_sessions.add(session);
}
/// Remove a native view and remove an existing video session object
void _removeRenderView(int uid) {
VideoSession session = _getVideoSession(uid);
if (session != null) {
_sessions.remove(session);
}
AgoraRtcEngine.removeNativeView(session.viewId);
}
/// Helper function to filter video session with uid
VideoSession _getVideoSession(int uid) {
return _sessions.firstWhere((session) {
return session.uid == uid;
});
}
/// Helper function to get list of native views
List _getRenderViews() {
return _sessions.map((session) => session.view).toList();
}
/// Video view wrapper
/// Expanded组件必须用在Row、Column、Flex内,并且从Expanded到封装它的Row、Column、Flex的路径必须只包括StatelessWidgets或StatefulWidgets组件(不能是其他类型的组件,像RenderObjectWidget,它是渲染对象,不再改变尺寸了,因此Expanded不能放进RenderObjectWidget)。
Widget _videoView(view) {
return Expanded(child: Container(child: view));
}
/// Video layout wrapper
Widget _viewRows() {
//List views = _getRenderViews();
List views = new List();
views.addAll(_getRenderViews());
return _mainWindow(views);
}
/// 主窗口视图
Widget _mainWindow(List views){
return GestureDetector(
child: Container(
child: Column(
children: [
mainWindowShowOneself ?
_videoView(views[0]) : _videoView(views[1])
],
)),
);
}
/// 右上角小窗口视图
Widget _smallWindow() {
//List views = _getRenderViews();
List views = new List();
if(!videoSuccess ){
return _emptyView();
}else {
views.addAll(_getRenderViews());
if( mainWindowShowOneself ){
if(!EmptyUtil.listIsEmpty(views) && views.length > 1){
return _smallVideoView(views[1]);
}else{
return _emptyView();
}
}else {
if(!EmptyUtil.listIsEmpty(views)){
return _smallVideoView(views[0]);
}else{
return _emptyView();
}
}
}
}
/// 右上角小窗口视图
Widget _smallVideoView(Widget view){
return GestureDetector(
onTap: updateDoubleWindow,
onDoubleTap: updateDoubleWindow,
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 80.0,
height: 130.0,
margin: EdgeInsets.all(20),
color: ThemeColors.colorWhite,
child: Stack(children: [
Column(
children: [
_videoView(view)
],
),
Container(
width: double.infinity,
height: double.infinity,
color: ThemeColors.transparent,
child: Text(" "),
)
],)
),
),
);
}
/// 未接通视频前的一层透明遮罩
Widget _mask(){
return Container(
child:Offstage(
offstage: videoSuccess,
child: Container(
width: double.infinity,
height: double.infinity,
color: ThemeColors.transparent1,
),
) ,
);
}
/// 响铃时的dialog
Widget _ProgressDialog() {
return Offstage(
offstage: videoSuccess,
child:Container(
height: 25.0,
color: ThemeColors.transparent,
margin: EdgeInsets.only(top:110),
alignment: Alignment.topCenter,
child: SpinKitWave(color: ThemeColors.colorTheme),
) ,
);
}
/// 视频界面底部的工具栏(静音、挂断、摄像头切换)
Widget _toolbar() {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 48),
child:Container(
height: 100.0,
child: Column(children: [
Offstage(
offstage: !videoSuccess,//true -显示
child: Container(
height: 20.0,
margin: EdgeInsets.only(bottom:10.0),
child: Text(DateTimeUtil.getHMmmss_Seconds(_count),
style: TextStyle(
color: ThemeColors.colorWhite,
fontSize: 16,
)),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RawMaterialButton(
onPressed: () => _onToggleMute(),
child: new Icon(
muted ? Icons.mic : Icons.mic_off,
color: muted ? Colors.white : ThemeColors.colorTheme,
size: 20.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: muted ? ThemeColors.colorTheme : Colors.white,
padding: const EdgeInsets.all(12.0),
),
RawMaterialButton(
onPressed: () => onCallEnd(context),
child: new Icon(
Icons.call_end,
color: Colors.white,
size: 35.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: Colors.redAccent,
padding: const EdgeInsets.all(15.0),
),
RawMaterialButton(
onPressed: () => _onSwitchCamera(),
child: new Icon(
Icons.switch_camera,
color: ThemeColors.colorTheme,
size: 20.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: Colors.white,
padding: const EdgeInsets.all(12.0),
)
],
),
],),
) ,
);
}
/// 好友的信息视图(名称)
Widget _friendInfo() {
return Container(
height: 50.0,
alignment: Alignment.topCenter,
margin: EdgeInsets.only(top:60),
child: Offstage(
offstage: videoSuccess,
child: Text(
widget.firendName,
style: TextStyle(
color: ThemeColors.colorWhite,
fontSize: 26,
),
),
)
);
}
/// 退出通话
void onCallEnd(BuildContext context) {
Navigator.pop(context);
}
void _onToggleMute() {
setState(() {
muted = !muted;
});
AgoraRtcEngine.muteLocalAudioStream(muted);
}
/// 切换摄像头
void _onSwitchCamera() {
AgoraRtcEngine.switchCamera();
}
/// 开始播放自定义的响铃文件
void startPlay(){
_methodChannel1.invokeListMethod(MethodChannelUtils.methodStartMedia);
}
/// 停止播放响铃
void stopPlay(){
_methodChannel1.invokeListMethod(MethodChannelUtils.methodStopMedia);
}
/// 更换主窗口和小窗口的画面
void updateDoubleWindow(){
setState(() {
mainWindowShowOneself = !mainWindowShowOneself;
});
}
/// 开始计时
void startTimer() {
const oneSec = const Duration(seconds: 1);
var callback = (timer) => {
setState(() {
_count++;// 秒数+1
})
};
_timer = Timer.periodic(oneSec, callback);
}
/// 停止计时
void stopTimer(){
if(_timer != null){
_timer.cancel();
}
}
@override
Widget build(BuildContext context) {
mcontext = context;
return Scaffold(
appBar: AppBar(
title: Text('Agora Flutter'),
),
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [_viewRows(),_smallWindow(),_mask(),_ProgressDialog(), _toolbar(),_friendInfo()],//_panel(),
)));
}
Widget _emptyView(){
return Container(
width: 1.0,
height: 1.0,
);
}
}
- 等待接听视频的页面(及通话页面)
class VideoAnswerPage extends StatefulWidget {
/// non-modifiable channel name of the page
final String channelName;
final String firendName;
VideoAnswerState videoAnswerState;
AgoraRtmClient _client;
/// 接听视频邀请
/// 参数(视频通道id,好友名称)
VideoAnswerPage(this.channelName, this.firendName,this._client);
@override
VideoAnswerState createState() {
videoAnswerState = new VideoAnswerState();
return videoAnswerState;
}
}
class VideoAnswerState extends State {
static const _methodChannel1 = const MethodChannel(MethodChannelUtils.channelMedia);
static final _sessions = List();
final _infoStrings = [];
BuildContext mcontext;
var APP_ID = APPApiKey.Agora_app_id;
bool muted = false;
/// 视频是否成功接通
bool videoSuccess = false;
/// 拒绝通话
bool isClosedByOne = false;
/// 主窗口展示自己?
/// true 自己 false 好友
bool mainWindowShowOneself = true;
int _count = 0;
Timer _timer;
@override
void dispose() {
// clean up native views & destroy sdk
_sessions.forEach((session) {
AgoraRtcEngine.removeNativeView(session.viewId);
});
_sessions.clear();
AgoraRtcEngine.leaveChannel();
stopPlay();
if(!isClosedByOne && !videoSuccess){
/// 视频没有接通前自己挂断,则需要通知对方,我已拒绝
_initSendMessage();
}
stopTimer();
super.dispose();
}
@override
void initState() {
super.initState();
// initialize third.agora sdk
initialize();
startPlay();
}
void startPlay(){
_methodChannel1.invokeListMethod(MethodChannelUtils.methodStartMedia);
}
void stopPlay(){
_methodChannel1.invokeListMethod(MethodChannelUtils.methodStopMedia);
}
void initialize() {
if (APP_ID.isEmpty) {
setState(() {
_infoStrings
.add("APP_ID missing, please provide your APP_ID in settings.dart");
_infoStrings.add("Agora Engine is not starting");
});
return;
}
_initAgoraRtcEngine();
_addAgoraEventHandlers();
// use _addRenderView everytime a native video view is needed
_addRenderView(0, (viewId) {
AgoraRtcEngine.setupLocalVideo(viewId, VideoRenderMode.Hidden);
AgoraRtcEngine.startPreview();
// state can access widget directly
// AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);// 修改为点击接听按钮后再接通
});
}
Future _initSendMessage() async{
/*-------收到消息--------*/
try {
String msg = AgoraUtils.getAgoraMsgType(3);
await widget._client.sendMessageToPeer(widget.firendName, AgoraRtmMessage(msg));
} catch (e) {
print(e);
}
}
/// Create third.agora sdk instance and initialze
Future _initAgoraRtcEngine() async {
AgoraRtcEngine.create(APP_ID);
AgoraRtcEngine.enableVideo();
}
/// Add third.agora event handlers
void _addAgoraEventHandlers() {
AgoraRtcEngine.onError = (int code) {
setState(() {
String info = 'onError: ' + code.toString();
_infoStrings.add(info);
});
};
/// 成功加入某次视频的回调
AgoraRtcEngine.onJoinChannelSuccess =
(String channel, int uid, int elapsed) {
setState(() {
String info = 'onJoinChannel: ' + channel + ', uid: ' + uid.toString();
_infoStrings.add(info);
});
};
AgoraRtcEngine.onLeaveChannel = () {
setState(() {
_infoStrings.add('onLeaveChannel');
});
};
/// 有其他用户加入到视频中的回调
AgoraRtcEngine.onUserJoined = (int uid, int elapsed) {
setState(() {
String info = 'userJoined: ' + uid.toString();
//setState(() { videoSuccess = true; });
_infoStrings.add(info);
videoSuccess = true;
startTimer();
_addRenderView(uid, (viewId) {
AgoraRtcEngine.setupRemoteVideo(viewId, VideoRenderMode.Hidden, uid);
});
});
};
AgoraRtcEngine.onUserOffline = (int uid, int reason) {
setState(() {
String info = 'userOffline: ' + uid.toString();
_infoStrings.add(info);
onCallEnd(mcontext);
_removeRenderView(uid);
});
};
AgoraRtcEngine.onFirstRemoteVideoFrame =
(int uid, int width, int height, int elapsed) {
setState(() {
String info = 'firstRemoteVideo: ' +
uid.toString() +
' ' +
width.toString() +
'x' +
height.toString();
_infoStrings.add(info);
});
};
}
/// Create a native view and add a new video session object
/// The native viewId can be used to set up local/remote view
void _addRenderView(int uid, Function(int viewId) finished) {
Widget view = AgoraRtcEngine.createNativeView(uid, (viewId) {
setState(() {
_getVideoSession(uid).viewId = viewId;
if (finished != null) {
finished(viewId);
}
});
});
VideoSession session = VideoSession(uid, view);
_sessions.add(session);
}
/// Remove a native view and remove an existing video session object
void _removeRenderView(int uid) {
VideoSession session = _getVideoSession(uid);
if (session != null) {
_sessions.remove(session);
}
AgoraRtcEngine.removeNativeView(session.viewId);
}
/// Helper function to filter video session with uid
VideoSession _getVideoSession(int uid) {
return _sessions.firstWhere((session) {
return session.uid == uid;
});
}
/// Helper function to get list of native views
List _getRenderViews() {
return _sessions.map((session) => session.view).toList();
}
/// Video view wrapper
/// Expanded组件必须用在Row、Column、Flex内,并且从Expanded到封装它的Row、Column、Flex的路径必须只包括StatelessWidgets或StatefulWidgets组件(不能是其他类型的组件,像RenderObjectWidget,它是渲染对象,不再改变尺寸了,因此Expanded不能放进RenderObjectWidget)。
Widget _videoView(view) {
return Expanded(child: Container(child: view));
}
/// Video layout wrapper
Widget _viewRows() {
//List views = _getRenderViews();
List views = new List();
views.addAll(_getRenderViews());
return _mainWindow(views);
}
/// 主窗口视图
Widget _mainWindow(List views){
return GestureDetector(
child: Container(
child: Column(
children: [
mainWindowShowOneself ?
_videoView(views[0]) : _videoView(views[1])
],
)),
);
}
/// 右上角小窗口视图
Widget _smallWindow() {
//List views = _getRenderViews();
List views = new List();
if(!videoSuccess ){
return _emptyView();
}else {
views.addAll(_getRenderViews());
if( mainWindowShowOneself ){
if(!EmptyUtil.listIsEmpty(views) && views.length > 1){
return _smallVideoView(views[1]);
}else{
return _emptyView();
}
}else {
if(!EmptyUtil.listIsEmpty(views)){
return _smallVideoView(views[0]);
}else{
return _emptyView();
}
}
}
}
Widget _smallVideoView(Widget view){
return GestureDetector(
onTap: updateDoubleWindow,
onDoubleTap: updateDoubleWindow,
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 80.0,
height: 130.0,
margin: EdgeInsets.all(20),
color: ThemeColors.colorWhite,
child: Stack(children: [
Column(
children: [
_videoView(view)
],
),
Container(
width: double.infinity,
height: double.infinity,
color: ThemeColors.transparent,
child: Text(" 视图 ",
style: TextStyle(
color: ThemeColors.transparent,
fontSize: 20.0,
),
),
)
],)
),
),
);
}
/// 未接通视频前的一层遮罩
Widget _mask(){
return Container(
child:Offstage(
offstage: videoSuccess,
child: Container(
width: double.infinity,
height: double.infinity,
color: ThemeColors.transparent1,
),
) ,
);
}
/// Toolbar layout
Widget _toolbar() {
if(videoSuccess){
return _answerSuccessToolbar();
}else{
return _waitAnswerToolbar();
}
}
/// 通话时的工具栏
Widget _answerSuccessToolbar(){
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 48),
child:Container(
height: 100.0,
child: Column(children: [
Container(
height: 20.0,
margin: EdgeInsets.only(bottom:10.0),
child: Text(DateTimeUtil.getHMmmss_Seconds(_count),
style: TextStyle(
color: ThemeColors.colorWhite,
fontSize: 16,
)),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RawMaterialButton(
onPressed: () => _onToggleMute(),
child: new Icon(
muted ? Icons.mic : Icons.mic_off,
color: muted ? Colors.white : ThemeColors.colorTheme,
size: 20.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: muted ? ThemeColors.colorTheme : Colors.white,
padding: const EdgeInsets.all(12.0),
),
RawMaterialButton(
onPressed: () => onCallEnd(context),
child: new Icon(
Icons.call_end,
color: Colors.white,
size: 35.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: Colors.redAccent,
padding: const EdgeInsets.all(15.0),
),
RawMaterialButton(
onPressed: () => _onSwitchCamera(),
child: new Icon(
Icons.switch_camera,
color: ThemeColors.colorTheme,
size: 20.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: Colors.white,
padding: const EdgeInsets.all(12.0),
)
],
),
],),
) ,
);
}
/// 响铃时的工具栏
Widget _waitAnswerToolbar(){
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 48),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
RawMaterialButton(
onPressed: () => _onCancelAnswer(context),
child: new Icon(
Icons.call_end,
color: Colors.white,
size: 35.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: Colors.redAccent,
padding: const EdgeInsets.all(15.0),
),
RawMaterialButton(
onPressed: () => _onAnswerVideo(),
child: new Icon(
Icons.call_received,
color: Colors.white,
size: 35.0,
),
shape: new CircleBorder(),
elevation: 2.0,
fillColor: Colors.green,
padding: const EdgeInsets.all(12.0),
)
],
),
);
}
/// 好友的信息视图
Widget _friendInfo() {
return Container(
alignment: Alignment.topLeft,
margin: EdgeInsets.all(15),
child: Offstage(
offstage: videoSuccess,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.firendName,
textAlign: TextAlign.left,
style: TextStyle(
color: ThemeColors.colorWhite,
fontSize: 25,
),
),
Text(
"邀请你进行视频通话",
textAlign: TextAlign.start,
style: TextStyle(
color: ThemeColors.colorWhite,
fontSize: 12,
height: 1.5,
),
),
],)
)
);
}
void _onToggleMute() {
setState(() {
muted = !muted;
});
AgoraRtcEngine.muteLocalAudioStream(muted);
}
/// 切换摄像头
void _onSwitchCamera() {
AgoraRtcEngine.switchCamera();
}
/// 当点击接受应答
void _onAnswerVideo() {
try {
stopPlay();
AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
} catch (e) {
print(e);
}
}
/// 当点击取消应答
void _onCancelAnswer(BuildContext context) {
Navigator.pop(context);
}
/// 退出视频页面,停止视频
void onCallEnd(BuildContext context) {
Navigator.pop(context);
}
void updateDoubleWindow(){
setState(() {
mainWindowShowOneself = !mainWindowShowOneself;
});
}
/// 开始计时
void startTimer() {
const oneSec = const Duration(seconds: 1);
var callback = (timer) => {
setState(() {
_count++;// 秒数+1
})
};
_timer = Timer.periodic(oneSec, callback);
}
/// 停止计时
void stopTimer(){
if(_timer != null){
_timer.cancel();
}
}
@override
Widget build(BuildContext context) {
mcontext = context;
return Scaffold(
appBar: AppBar(
title: Text('Agora Flutter QuickStart'),
),
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [_viewRows(),_smallWindow(),_mask(), _toolbar(),_friendInfo()],//_panel(),
)));
}
Widget _emptyView(){
return Container(
width: 1.0,
height: 1.0,
);
}
}
- Agora的工具类
class AgoraUtils{
static AgoraRtmClient _client;
static VideoCallState _videoCallState;
/// Agora 初始化
static Future getAgoraRtmClient() async {
if(_client == null){
_client =
await AgoraRtmClient.createInstance(APPApiKey.Agora_app_id);
}
return _client;
}
/// 查询用户是否在线
/// true-在线 , false-离线
static Future queryPeerOnlineStatus(AgoraRtmClient _client, String peerUid) async {
if(EmptyUtil.textIsEmpty(peerUid)){
return false;
}else{
try {
Map result =
await _client.queryPeersOnlineStatus([peerUid]);
return result[peerUid];
} catch (errorCode) {
return false;
}
}
}
/// 获取声网的消息类型
/// 1-请求视频通话
/// 2-取消请求通话
/// 3-拒绝通话请求
static String getAgoraMsgType(int type){
switch(type){
case 1:
return "CALLVIDEO";
case 2:
return "CANCEL_VIDEO";
case 3:
return "REFUSE_VIDEO";
default:
return "";
}
}
/// 视频请求
static set videoCallState(VideoCallState value) {
_videoCallState = value;
}
/// 视频请求
static VideoCallState get videoCallState => _videoCallState;
static clearVideoCallState(){
_videoCallState= null;
}
}
- Android 本地播放响铃的相关代码
MediaPlayer mediaPlayer;
private float BEEP_VOLUME = 9.10f;
MediaPlayer.OnCompletionListener beepListener;
private void startPlayBell(){
if(beepListener == null){
beepListener = new MediaPlayer.OnCompletionListener() {
// 声音
public void onCompletion(MediaPlayer mediaPlayer) {
mediaPlayer.seekTo(0);
}
};
}
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnCompletionListener(beepListener);
mediaPlayer.setLooping(true);
AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.wechat_video);
try {
mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
file.close();
mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
mediaPlayer.prepare();
} catch (IOException e) {
mediaPlayer = null;
}
// }
if(mediaPlayer != null){
mediaPlayer.start();
}
}
private void stopPlayBell(){
if(mediaPlayer != null){
mediaPlayer.stop();
mediaPlayer.release();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mediaPlayer != null){
mediaPlayer.release();
}
}
其中 R.raw.wechat_video 是我找的音频文件,类似微信视频来电时的响铃
最后
GitHub地址(https://github.com/Lightforest/FlutterVideo)
---end---