效果展示
實現(xiàn)細(xì)節(jié)包括:視頻的呼叫和接聽,來電時的響鈴,畫面切換等。先看效果圖,相關(guān)視頻界面截圖如下:

有人來電時的界面

呼叫其他人時的界面

視頻接通時的界面

點擊小窗口切換畫面時的界面
準(zhǔn)備工作
● 創(chuàng)建聲網(wǎng)賬戶,并獲取App ID,?官網(wǎng)
● 用到的聲網(wǎng)Flutter插件有兩個:
1> agora_rtc_engine(https://github.com/AgoraIO/Flutter-RTM)
主要實現(xiàn)視頻通話部分的插件,點擊官方demo查看代碼示例。
2>agora_rtm(https://github.com/AgoraIO/Flutter-RTM)
主要實現(xiàn)呼叫的信令系統(tǒng)插件,點擊官方demo可查看代碼示例。
功能細(xì)節(jié):登錄、收發(fā)消息
代碼部分
配置
- 在項目根目錄下的 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 文件中添加權(quán)限:
..
<uses-permission android:name="android.permission.READ_PHONE_STATE” />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- The Agora SDK requires Bluetooth permissions in case users are using Bluetooth devices.-->
<uses-permission android:name="android.permission.BLUETOOTH" />
..
- 在 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.
視頻相關(guān)
- 初始化
AgoraRtmClient _client;
/// 收到通話請求時的響鈴頁面
VideoAnswerPage answer;
/// 聲網(wǎng)RTM初始化、注冊接收
Future initAgoraRtm() async {
// 初始化
_client =await AgoraUtils.getAgoraRtmClient();
// 設(shè)置消息接收器
_client.onMessageReceived = (AgoraRtmMessage message, String peerId) {
if(!EmptyUtil.textIsEmpty(message.text)){
// 收到視頻請求,消息內(nèi)容定義為: “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 提交登錄
/// 聲網(wǎng)登錄
void _toggleLogin() async {
if (!_isLogin) {
// 獲取輸入框的user id(英文 || 數(shù)字)
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<AgoraCustomPage> {
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("聲網(wǎng)"),
),
body: buildStartPage(),
);
}
Widget buildStartPage(){
return SingleChildScrollView(
child: ConstrainedBox(// 添加額外為限制條件到child,如最小/大寬度、高度。。。
constraints: BoxConstraints(
minHeight: 120.0,
),
child: Column(children: <Widget>[
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();
},
// 文本內(nèi)容
child: Text("和好友視頻通話"),
// 按鈕顏色
color: ThemeColors.colorTheme,
))
]),),
);
}
void clickFriendVideo(){
if(EmptyUtil.textIsEmpty(_friendController.text)){
Fluttertoast.showToast(msg: "Friend id cannot be empty");
}else{
_callVideo(_friendController.text);
}
}
// 發(fā)送視頻通話請求
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;
// 發(fā)送消息 : “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<VideoCallPage> {
/// 和android本地交互的通道
static const _methodChannel1 = const MethodChannel(MethodChannelUtils.channelMedia);
static final _sessions = List<VideoSession>();
final _infoStrings = <String>[];
BuildContext mcontext;
AgoraRtmClient _client;
/// 聲網(wǎng)上獲取的App ID
var APP_ID = APPApiKey.Agora_app_id;
bool muted = false;
/// 視頻是否成功接通
bool videoSuccess = false;
/// 發(fā)出視頻請求但未接通時,自己取消通話
bool isClosedByOne = false;
/// 主窗口展示自己?
/// true 展示自己 false 展示好友
bool mainWindowShowOneself = true;
/// 計時的數(shù)值
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();// 信令系統(tǒng)
_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
// 加入視頻通話(或者可以考慮在對方接聽后發(fā)個消息通知請求方,請求方再加入視頻,可以節(jié)省點視頻分鐘數(shù))
AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
});
}
/// 獲取 AgoraRtmClient
Future<void> _initAgoraRTM() async{
_client =await AgoraUtils.getAgoraRtmClient();
AgoraUtils.videoCallState = this;
}
/// 發(fā)送消息通知對方取消通話
Future<void> _initSendMessage() async{
String msg = AgoraUtils.getAgoraMsgType(2);
await _client.sendMessageToPeer(widget.firendName, AgoraRtmMessage(msg));
}
/// Create third.agora sdk instance and initialze
Future<void> _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);
});
};
/// 成功加入某次視頻的回調(diào)
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');
});
};
/// 有其他用戶(好友)成功加入到視頻中的回調(diào)
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<Widget> _getRenderViews() {
return _sessions.map((session) => session.view).toList();
}
/// Video view wrapper
/// Expanded組件必須用在Row、Column、Flex內(nèi),并且從Expanded到封裝它的Row、Column、Flex的路徑必須只包括StatelessWidgets或StatefulWidgets組件(不能是其他類型的組件,像RenderObjectWidget,它是渲染對象,不再改變尺寸了,因此Expanded不能放進(jìn)RenderObjectWidget)。
Widget _videoView(view) {
return Expanded(child: Container(child: view));
}
/// Video layout wrapper
Widget _viewRows() {
//List<Widget> views = _getRenderViews();
List<Widget> views = new List();
views.addAll(_getRenderViews());
return _mainWindow(views);
}
/// 主窗口視圖
Widget _mainWindow(List<Widget> views){
return GestureDetector(
child: Container(
child: Column(
children: <Widget>[
mainWindowShowOneself ?
_videoView(views[0]) : _videoView(views[1])
],
)),
);
}
/// 右上角小窗口視圖
Widget _smallWindow() {
//List<Widget> views = _getRenderViews();
List<Widget> 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: <Widget>[
Column(
children: <Widget>[
_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: <Widget>[
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: <Widget>[
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++;// 秒數(shù)+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: <Widget>[_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;
/// 接聽視頻邀請
/// 參數(shù)(視頻通道id,好友名稱)
VideoAnswerPage(this.channelName, this.firendName,this._client);
@override
VideoAnswerState createState() {
videoAnswerState = new VideoAnswerState();
return videoAnswerState;
}
}
class VideoAnswerState extends State<VideoAnswerPage> {
static const _methodChannel1 = const MethodChannel(MethodChannelUtils.channelMedia);
static final _sessions = List<VideoSession>();
final _infoStrings = <String>[];
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<void> _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<void> _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);
});
};
/// 成功加入某次視頻的回調(diào)
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');
});
};
/// 有其他用戶加入到視頻中的回調(diào)
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<Widget> _getRenderViews() {
return _sessions.map((session) => session.view).toList();
}
/// Video view wrapper
/// Expanded組件必須用在Row、Column、Flex內(nèi),并且從Expanded到封裝它的Row、Column、Flex的路徑必須只包括StatelessWidgets或StatefulWidgets組件(不能是其他類型的組件,像RenderObjectWidget,它是渲染對象,不再改變尺寸了,因此Expanded不能放進(jìn)RenderObjectWidget)。
Widget _videoView(view) {
return Expanded(child: Container(child: view));
}
/// Video layout wrapper
Widget _viewRows() {
//List<Widget> views = _getRenderViews();
List<Widget> views = new List();
views.addAll(_getRenderViews());
return _mainWindow(views);
}
/// 主窗口視圖
Widget _mainWindow(List<Widget> views){
return GestureDetector(
child: Container(
child: Column(
children: <Widget>[
mainWindowShowOneself ?
_videoView(views[0]) : _videoView(views[1])
],
)),
);
}
/// 右上角小窗口視圖
Widget _smallWindow() {
//List<Widget> views = _getRenderViews();
List<Widget> 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: <Widget>[
Column(
children: <Widget>[
_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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
Text(
widget.firendName,
textAlign: TextAlign.left,
style: TextStyle(
color: ThemeColors.colorWhite,
fontSize: 25,
),
),
Text(
"邀請你進(jìn)行視頻通話",
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();
}
/// 當(dāng)點擊接受應(yīng)答
void _onAnswerVideo() {
try {
stopPlay();
AgoraRtcEngine.joinChannel(null, widget.channelName, null, 0);
} catch (e) {
print(e);
}
}
/// 當(dāng)點擊取消應(yīng)答
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++;// 秒數(shù)+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: <Widget>[_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<AgoraRtmClient> getAgoraRtmClient() async {
if(_client == null){
_client =
await AgoraRtmClient.createInstance(APPApiKey.Agora_app_id);
}
return _client;
}
/// 查詢用戶是否在線
/// true-在線 , false-離線
static Future<bool> queryPeerOnlineStatus(AgoraRtmClient _client, String peerUid) async {
if(EmptyUtil.textIsEmpty(peerUid)){
return false;
}else{
try {
Map<String, bool> result =
await _client.queryPeersOnlineStatus([peerUid]);
return result[peerUid];
} catch (errorCode) {
return false;
}
}
}
/// 獲取聲網(wǎng)的消息類型
/// 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 本地播放響鈴的相關(guān)代碼
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 是我找的音頻文件,類似微信視頻來電時的響鈴

圖片.png
最后
GitHub地址(https://github.com/Lightforest/FlutterVideo)
---end---