实现音视频通话
更新时间: 2024/09/18 16:26:13
网易云信音视频通话产品的基本功能包括高质量的实时音视频通话。当您成功初始化 SDK 之后,您可以简单体验本产品的基本业务流程。本文档为您展示音视频通话提供的基本业务流程。
前提条件
请确认您已完成以下操作:
示例代码
网易云信为您提供完整的创建界面及实现基础音视频通话的示例代码作为参考,您可以直接拷贝用于运行测试。
xml 界面的完整示例代码
class CallPage extends StatefulWidget {
final String cid;
final int uid;
CallPage({Key? key, required this.cid, required this.uid});
@override
_CallPageState createState() {
return _CallPageState();
}
}
class _CallPageState extends State<CallPage>
with
NERtcVideoRendererEventListener,
NERtcChannelEventCallback,
NERtcAudioMixingEventCallback,
NERtcAudioEffectEventCallback,
NERtcStatsEventCallback,
NERtcDeviceEventCallback,
NERtcLiveTaskCallback {
late Settings _settings;
NERtcEngine _engine = NERtcEngine.instance;
List<_UserSession> _remoteSessions = [];
_UserSession? _localSession;
_UserSession? _localSubStreamSession;
bool isAudioEnabled = false;
bool isVideoEnabled = false;
bool isFrontCamera = true;
bool isFrontCameraMirror = true;
bool isSpeakerphoneOn = false;
bool forceSubAndUnsubVideo = false;
final foregroundServiceChannel = const MethodChannel(
'com.netease.nertc_core.nertc_core_example.foreground_service');
@override
void initState() {
super.initState();
print('start call: uid=${widget.uid}, cid=${widget.cid}');
_initSettings().then((value) => _initRtcEngine());
if (Platform.isAndroid) {
foregroundServiceChannel.invokeMethod('startForegroundService');
}
}
@override
Widget build(BuildContext context) {
screenSize = MediaQuery.of(context).size;
return WillPopScope(
child: GestureDetector(
onTap: () {
setState(() {
showControlPanel = !showControlPanel;
});
},
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
centerTitle: true,
title: Text(widget.cid),
),
body: buildCallingWidget(context),
),
),
// ignore: missing_return
onWillPop: () {
_requestPop();
return Future.value(true);
},
);
}
Widget buildCallingWidget(BuildContext context) {
return Stack(children: <Widget>[
buildVideoViews(context),
if (showControlPanel) buildControlPanel(context)
]);
}
Widget buildControlPanel(BuildContext context) {
return Container(
height: 40,
child: Row(
children: [
Expanded(
child: buildControlButton(
() {
bool audioEnabled = !isAudioEnabled;
_engine.enableLocalAudio(audioEnabled).then((value) {
if (value == 0) {
setState(() {
isAudioEnabled = audioEnabled;
});
}
});
},
Text(
isAudioEnabled ? '关闭语音' : '打开语音',
style: TextStyle(fontSize: 12),
),
)),
Expanded(
child: buildControlButton(
() {
bool videoEnabled = !isVideoEnabled;
_engine.enableLocalVideo(videoEnabled).then((value) {
if (value == 0) {
setState(() {
isVideoEnabled = videoEnabled;
});
}
});
},
Text(
isVideoEnabled ? '关闭视频' : '打开视频',
style: TextStyle(fontSize: 12),
),
)),
Expanded(
child: buildControlButton(
() {
_engine.deviceManager.switchCamera().then((value) {
if (value == 0) {
isFrontCamera = !isFrontCamera;
updateLocalMirror();
}
});
},
Text(
'切换摄像头',
style: TextStyle(fontSize: 12),
),
)),
Expanded(
child: buildControlButton(
() {
bool speakerphoneOn = !isSpeakerphoneOn;
_engine.deviceManager
.setSpeakerphoneOn(speakerphoneOn)
.then((value) {
if (value == 0) {
setState(() {
isSpeakerphoneOn = speakerphoneOn;
});
}
});
},
Text(
isSpeakerphoneOn ? '关闭扬声器' : '打开扬声器',
style: TextStyle(fontSize: 12),
),
)),
],
));
}
Widget buildVideoViews(BuildContext context) {
final sessions = [
if (_localSession != null) _localSession!,
if (_localSubStreamSession != null) _localSubStreamSession!,
..._remoteSessions,
];
return OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
final isLandscape = orientation == Orientation.landscape;
return GridView.builder(
scrollDirection: isLandscape ? Axis.horizontal : Axis.vertical,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isLandscape ? 1 : 3,
childAspectRatio: isLandscape ? 16 / 9 : 9 / 16,
crossAxisSpacing: 2.0,
mainAxisSpacing: 2.0,
),
itemCount: sessions.length,
itemBuilder: (BuildContext context, int index) {
return buildVideoView(context, sessions[index]);
},
);
},
);
}
Widget buildVideoView(BuildContext context, _UserSession session) {
return Container(
child: Stack(
children: [
NERtcVideoView(
uid: session.uid == widget.uid ? null : session.uid,
subStream: session.subStream,
mirrorListenable: session.mirror,
rendererEventLister: this,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
session.subStream ? '${session.uid} @ Sub' : '${session.uid}',
style: TextStyle(color: Colors.red, fontSize: 10),
)
],
)
],
),
);
}
实现音视频通话的完整示例代码
NERtcEngine _engine = NERtcEngine.instance;
//初始化SDK
await _engine
.create(
appKey: Config.APP_KEY,
channelEventCallback: this,
options: options)
·//添加callback回调
// _engine.setStatsEventCallback(this);
_engine.audioMixingManager.setEventCallback(this);
_engine.audioEffectManager.setEventCallback(this);
_engine.deviceManager.setEventCallback(this);
_engine.setLiveTaskEventCallback(this);
_engine.setEventCallback(this);
//设置本地摄像头的采集偏好等配置
final config = NERtcCameraCaptureConfig.manual(640, 360);
await _engine.setCameraCaptureConfig(config);
//设置本地视频参数
NERtcVideoConfig config = NERtcVideoConfig();
config.videoProfile = _settings.videoProfile;
config.frameRate = _settings.videoFrameRate;
config.degradationPrefer = _settings.degradationPreference;
config.frontCamera = _settings.frontFacingCamera;
config.videoCropMode = _settings.videoCropMode;
await _engine.setLocalVideoConfig(config);
//加入房间前提前设置好音视频开启状态
if (Platform.isIOS) {
await _engine.setAudioSessionOperationRestriction(
NERtcAudioSessionOperationRestriction
.values[_settings.audioSessionRestriction]);
}
await _engine.setAudioProfile(
NERtcAudioProfile.values[_settings.audioProfile],
NERtcAudioScenario.values[_settings.audioScenario]);
await _engine.enableLocalAudio(isAudioEnabled);
await _engine.enableLocalVideo(isVideoEnabled);
//加入房间
long yourUid = 1234567;//同一个房间不允许相同uid
_engine.joinChannel('', cid, yourUid)
//结束通话
_engine.leaveChannel();
//释放SDK
_engine.release();
API 时序图
实现音频通话的 API 调用时序如下图所示。
sequenceDiagram
autonumber
participant App
participant NERtcSDK
participant 云信服务端
App->>NERtcSDK: create 初始化 NERtcEngine
App->>NERtcSDK: joinChannel 加入房间
NERtcSDK->>云信服务端: 请求加入房间
NERtcSDK-->>App: onJoinChannel
App->>NERtcSDK: leaveChannel 离开房间
NERtcSDK->>云信服务端: 请求离开房间
App->>NERtcSDK: release 销毁实例
实现视频通话的 API 调用时序如下图所示。
sequenceDiagram
autonumber
participant App
participant NERtcSDK
participant 云信服务端
App->>NERtcSDK: create 初始化 NERtcEngine
Note over App, NERtcSDK: 设置本地视图
App->>NERtcSDK: setupLocalVideoCanvas
App->>NERtcSDK: enableLocalVideo
Note over App, NERtcSDK: 加入房间
App->>NERtcSDK: joinChannel
NERtcSDK->>云信服务端: 请求加入房间
NERtcSDK-->>App: onJoinChannel
Note over App, NERtcSDK: 设置远端视图
Note right of NERtcSDK: 远端用户加入房间
NERtcSDK-->>App: onUserJoined 远端用户加入房间的回调
NERtcSDK-->>App: onUserVideoStart 远端用户发布视频流的回调
App->>NERtcSDK: setupRemoteVideoCanvas 设置远端视频画布
App->>NERtcSDK: subscribeRemoteVideoStream 订阅远端视频流
云信服务端-->>App : onFirstVideoFrameDecoded 已接收到远端视频首帧并完成解码的回调
Note over App, NERtcSDK: 离开房间
App->>NERtcSDK: leaveChannel
NERtcSDK->>云信服务端: 请求离开房间
App->>NERtcSDK: release 销毁实例
实现音视频通话
步骤1: (可选)创建音视频通话界面
您可以参考此步骤根据业务场景创建相应的音视频通话界面,若您已实现相应界面,请忽略该步骤。
实现基础的音视频通话,建议您参考 xml 界面的示例代码在界面上添加以下控件。
- 房间 ID
- 用户昵称
- 本端视频窗口
- 远端视频窗口
- 麦克风按钮
- 摄像头按钮
- 结束通话按钮
效果图如下图所示。
步骤2: 导入类
在您的工程中对应的 Page 文件里添加如下代码先导入以下重要类:
dartimport 'package:nertc_core/nertc_core.dart';
步骤3: 初始化
默认情况下,请先执行 create
方法完成初始化。
java// 示例
void _initRtcEngine() async {
NERtcOptions options = NERtcOptions(
audioAutoSubscribe: _settings.autoSubscribeAudio,
serverRecordSpeaker: _settings.serverRecordSpeaker,
serverRecordAudio: _settings.serverRecordAudio,
serverRecordVideo: _settings.serverRecordVideo,
serverRecordMode:
NERtcServerRecordMode.values[_settings.serverRecordMode],
videoSendMode: NERtcVideoSendMode.values[_settings.videoSendMode],
videoEncodeMode:
NERtcMediaCodecMode.values[_settings.videoEncodeMediaCodecMode],
videoDecodeMode:
NERtcMediaCodecMode.values[_settings.videoDecodeMediaCodecMode]);
_engine
.create(
appKey: Config.Your_APP_KEY,
channelEventCallback: this,
options: options);
}
您需要将 App_Key
替换为您的应用对应的 App Key。
为了实现标准音视频通话业务,您还需要在初始化时注册相关必要回调,建议您在初始化方法中传入原型为 NERtcChannelEventCallback 的以下回调,并增加相应必要的处理。
//NERtcChannelEventCallback 重要回调
//本端用户加入房间结果回调
@override
void onJoinChannel(int result, int channelId, int elapsed, int uid) {
}
//本端用户离开房间回调
@override
void onLeaveChannel(int result) {
}
//远端用户加入房间
@override
void onUserJoined(int uid, NERtcUserJoinExtraInfo? joinExtraInfo) {
}
//远端用户离开房间
void onUserLeave(
int uid, int reason, NERtcUserLeaveExtraInfo? leaveExtraInfo) {
for (_UserSession session in _remoteSessions.toList()) {
if (session.uid == uid) {
_remoteSessions.remove(session);
}
}
setState(() {});
}
//远端用户打开音频
@override
void onUserAudioStart(int uid) {
}
//远端用户关闭音频
@override
void onUserAudioStop(int uid) {
}
//远端用户打开视频,建议在此按需设置画布及订阅视频
@override
void onUserVideoStart(int uid, int maxProfile) {
}
//远端用户关闭视频,可释放之前绑定的画布
void onUserVideoStop(int uid) {
}
//与服务器断连,退出页面
@override
void onDisconnect(int reason) {
}
步骤4: 设置本地视图
初始化成功后,可以设置本地视图,来预览本地图像。您可以根据业务需要实现加入房间之前预览或加入房间后预览。
- 若您想设置画布渲染参数,可以调用
setMirror
方法设置镜像模式。 - 在加入房间前,默认预览分辨率为 640*480,您可以通过
setLocalVideoConfig
接口的width
和height
参数调整采集分辨率。
-
实现加入房间前预览。
-
通过
startVideoPreview
与NERtcVideoView
,在加入房间前设置本地视图,预览本地图像。java
// 示例 _engine.startVideoPreview(); NERtcVideoView( uid: null, subStream: session.subStream, mirrorListenable: session.mirror, rendererEventLister: this, ),
-
若要结束预览,或者准备加入房间时,调用
stopVideoPreview
停止预览。
-
-
实现加入房间后预览。
调用
NERtcVideoView
设置本地视图,再调用enableLocalVideo
方法进行视频的采集发送与预览。成功加入房间后,即可预览本地图像。java
// 示例 // 开启本地视频采集并发送 _engine.enableLocalVideo(true); // 设置本地预览画布 NERtcVideoView( uid: null, subStream: session.subStream, mirrorListenable: session.mirror, rendererEventLister: this, ),
步骤5: 加入房间
加入房间前,请确保已完成初始化相关事项。
调用 joinChannel
方法加入房间。
调用 joinChannel
之后,NERTC SDK 会通过 Android 的 AudioManager.setMode()
方法调整音频模式(audio mode),此后请勿修改 SDK 调整过的音频模式,否则会导致音频路由错误等问题。
示例代码如下:
java_engine.joinChannel(token,channelName,uid);
参数说明:
参数 | 说明 |
---|---|
token | 安全认证签名(NERTC Token)。
|
channelName | 房间名称,长度为 1 ~ 64 字节。目前支持以下 89 个字符:a-z, A-Z, 0-9, space, !#$%&()+-:;≤.,>? @[]^_{|}~"。 设置相同房间名称的用户会进入同一个通话房间。 |
uid | 用户的唯一标识 id,为数字串,房间内每个用户的 uid 必须是唯一的。 |
channelOptions | 加入房间时可以设置携带一些特定信息,包括高级权限密钥。默认值为 NULL,具体请参考 NERtcJoinChannelOptions。 |
-
SDK 发起加入房间请求后,服务器会进行响应,您可以通过
NERtcChannelEventCallback
的onJoinChannel
回调监听加入房间的结果,同时该回调会抛出当前通话房间的 channelId 与加入房间总耗时(毫秒);其中 channelId 即音视频通话的 ID,建议您在业务层保存该数据,以便于后续问题排查。 -
成功加入房间之后,您可以通过监听
onConnectionStateChanged
回调实时监控自己在本房间内的连接状态。
步骤6: 设置远端视图
音视频通话过程中,除了要显示本地的视频画面,通常也要显示参与互动的其他连麦者/主播的远端视频画面。
-
监听远端用户进出房间。
当远端用户加入房间时,本端会触发
onUserJoined
回调,并抛出对方的 uid。当本端加入房间后,也会通过此回调抛出通话房间内已有的其他用户。
-
设置远端视频画布。
在监听到远端用户加入房间或发布视频流后,本端可以通过 NERtcVideoView 来设置远端用户视频画布,用于显示其视频画面。
-
监听远端视频流发布。
当房间中的其他用户发布视频流时,本端会触发
onUserVideoStart
回调。 -
订阅远端视频流。
在监听到远端用户发布视频流后,本端可以调用
subscribeRemoteVideoStream
方法对其发起视频流的订阅,来将对方的视频流渲染到视频画布上。 -
监听远端用户离开房间或关闭视频功能。
-
onUserLeave
:用户离开房间回调。 -
onUserVideoStop
:远端用户关闭视频功能回调。
-
示例代码:
flutterNERtcVideoView(
uid: session.uid == widget.uid ? null : session.uid,
subStream: session.subStream,
mirrorListenable: session.mirror,
rendererEventLister: this,
);
void onUserVideoStart(int uid, int maxProfile) {
setupVideoView(uid, maxProfile, false);
}
Future<void> setupVideoView(int uid, int maxProfile, bool subStream) async {
final session = _UserSession(uid, subStream);
_remoteSessions.add(session);
if (forceSubAndUnsubVideo || !_settings.autoSubscribeVideo) {
if (subStream) {
_engine.subscribeRemoteSubStreamVideo(uid, true);
} else {
_engine.subscribeRemoteVideoStream(
uid, NERtcRemoteVideoStreamType.high, true);
}
}
setState(() {});
}
Future<void> releaseVideoView(int uid, bool subStream) async {
for (_UserSession session in _remoteSessions.toList()) {
if (session.uid == uid && subStream == session.subStream) {
_remoteSessions.remove(session);
if (forceSubAndUnsubVideo || !_settings.autoSubscribeVideo) {
if (!subStream) {
_engine.subscribeRemoteVideoStream(
uid, NERtcRemoteVideoStreamType.high, false);
} else {
_engine.subscribeRemoteSubStreamVideo(uid, false);
}
}
setState(() {});
break;
}
}
}
@override
void onUserVideoStop(int uid) {
releaseVideoView(uid, false);
}
@override
void onUserLeave(
int uid, int reason, NERtcUserLeaveExtraInfo? leaveExtraInfo) {
for (_UserSession session in _remoteSessions.toList()) {
if (session.uid == uid) {
_remoteSessions.remove(session);
}
}
setState(() {});
}
步骤7: 音频流
在 NERTC SDK 中,本地音频的采集发布和远端音频订阅播放是默认启动的,正常情况下无需开发者主动干预。
步骤8: 退出通话房间
调用 leaveChannel
接口退出通话房间。
java// 示例
// 退出通话房间
_engine.leaveChannel();
NERtcChannelEventCallback 提供 onLeaveChannel
回调来监听当前用户退出房间的结果。
步骤9: 销毁实例
当确定 App 短期内不再使用音视频通话实例时,可以调用 release
接口释放对应的对象资源。
java// 示例
// 销毁实例
_engine.release();