IOS技术分享| IOS快对讲调度场景实现
前言
“快对讲” 是基于 anyRTC 音视频技术 对讲业务的产品,为客户提供专业对讲、多媒体对讲和可视化调度功能。 主要功能包含:
- 频道与会话
- 多频道对讲、监听、锁定、强拆
- 音视频单人、多人呼叫、呼叫调度台
- 图片、视频上报
- 视频回传、监看
- 位置回传
- 即时消息:文字消息、语音消息、图片消息、视频消息、文件消息、位置消息
- 文字广播、媒体广播
- 监控、录像服务、调度台。
功能体验
场景功能实现
一、对讲
效果预览
部分代码实现
/**
anyRTC 云平台对讲 频道方法。
*/
__attribute__((visibility("default"))) @interface ARTalkChannel: NSObject
/// ARTalkChannelDelegate 接口类向 App 发送回调通知,上报运行时的频道相关事件。
@property (nonatomic, weak, nullable) id<ARTalkChannelDelegate> channelDelegate;
/// 加入频道。
/// - Parameter completionBlock 同一用户只能同时加入最多 20 个频道。加入频道超限时用户会收到错误码 ARTalkJoinChannelErrorExceedLimit
- (void)joinWithCompletion:(ARTalkJoinChannelBlock _Nullable)completionBlock;
/// 离开频道。
/// - Parameter completionBlock ARTalkLeaveChannelBlock 回调返回本方法的调用结果。
- (void)leaveWithCompletion:(ARTalkLeaveChannelBlock _Nullable)completionBlock;
/// 设置自己的对讲等级
/// - Parameter level 说话等级,0为最大,level 越大等级越低
- (int)setLevel:(int)level;
/// 获取自己的对讲等级
/// - returns 对讲等级
- (int)getLevel;
/// 设置推送音频质量
/// - Parameter nQuality: 1-5 低,中,高,超高,HD,默认为1
- (int)setPushAudioQuality:(int)nQuality;
/// 设置拉取音频质量,暂不可使用
/// - Parameter nQuality: 1-5 低,中,高,超高,HD,默认为1
- (int)setPullAudioQuality:(int)nQuality;
/// 开始对讲
/// - Parameter nTalkOnTime: 对讲时长,0为无限制
/// - returns 0方法调用成功,小于0方法调用失败
- (int)pushToTalk:(int)nTalkOnTime;
/// 结束对讲
/// - returns 0方法调用成功,小于0方法调用失败
- (int)stopPushToTalk;
/// 是否接收频道其它声音
/// - Parameter mute: mute true 静音,false 解除静音
/// - returns 0方法调用成功,小于0方法调用失败
- (int)muteAllRemoteAudio:(BOOL)mute;
/// 打断对讲
/// - returns 0方法调用成功,小于0方法调用失败
- (int)breakTalk;
/// 是否接收广播流
/// - Parameter enable: YES 接收,NO 不接收
- (int)enableAudioStream:(BOOL)enable;
/// 获取频道 ID
- (NSString *)getChannelId;
@end
二、频道与会话
效果预览
部分代码实现
class ARMainViewCellData: ARUICommonCellData {
/// 群id
var groupId: String = ""
/// 是否监听
var isMonitor: Bool = false
/// 监听状态
var monitorStatus: Bool = false
var groupName: String = ""
/// 群组类型(0:群组(频道),1:临时群组(会话))
var groupType: Int = 1
/// 在线人数
var onlineCount: NSInteger = 0
/// 群人数
var memberCount: NSInteger = 0
/// 用户在群组内权限(默认0,0:高,1:中,2:低,3:仅听)
var permission: NSInteger = 0
var isOwner: Bool = false
var item: ARCahnnelItem!
/// 对讲状态(优先级高)
var talkStatus: String?
/// 广播状态(优先级低)
var broadCastStatus: String?
/// 群组最大发言时长(单位:秒),默认:60s,0表示无限制
var groupMaxSpeak: NSInteger = 60
/// 最大排队人数(默认0:无排队,1:5人,2:10人)
var groupMaxQueue: NSInteger = 0
var groupDesc: String = ""
/// 群组级别(默认:0,0:低,1:中,2:高)
var groupGrade: NSInteger = 0
var groupImId: NSInteger = 0
/// 对讲状态
var userDataNofi: ARTalkUserDataNofi?
/// 未读数
var unReadNum: NSInteger = 0
weak var delegate: ARMainViewCellDataDelegate?
weak var dataSource: ARMainViewCellDataSource?
class func getCellData(item: ARCahnnelItem) -> ARMainViewCellData {
let cellData = ARMainViewCellData()
cellData.groupId = item.groupId
cellData.groupImId = item.groupImId
cellData.isMonitor = (item.isMonitor == 1)
cellData.groupName = item.groupName
cellData.groupType = item.groupType
cellData.onlineCount = item.onlineCount
cellData.memberCount = item.memberCount
cellData.permission = item.permission
cellData.isOwner = (item.ownerId == localUserData.uId)
cellData.groupMaxSpeak = item.groupMaxSpeak
cellData.groupMaxQueue = item.groupMaxQueue
cellData.groupDesc = item.groupDesc
cellData.groupGrade = item.groupGrade
cellData.item = item
cellData.unReadNum = ARIMMessage.queryGroupUnRead(recvId: UInt64(cellData.groupImId))
return cellData
}
func updateMonitorStatus(monitor: Bool, result: @escaping (_ code: NSInteger) -> Void) {
/// 修改监听状态
/// 频道: api 接口 + sdk 会话: sdk
debugPrint("ARUITalking - updateMonitorStatus monitor = \(monitor) monitorStatus = \(monitorStatus)")
guard monitor != monitorStatus else { return }
if groupType == 0 {
/// api 接口
ARNetWorkManager.shared.updateGroupMonitorStatus(groupId: groupId, isMonitor: monitor ? 1 : 0) {
print("ARUITalking - updateGroupMonitorStatus api sucess")
} failed: { _ in
print("ARUITalking - updateGroupMonitorStatus api failed")
}
}
if !monitor && groupId == ARTalkManager.shared.lockCellData?.groupId {
/// 解除频道锁定
ARTalkManager.shared.lockChannel(data: nil)
}
if ARTalkManager.shared.lockCellData != nil && ARTalkManager.shared.lockCellData?.groupId != groupId && monitor {
ARTalkManager.shared.getTalkChannel(channelId: groupId).muteAllRemoteAudio(true)
}
/// sdk 接口
ARTalkManager.shared.monitorChannel(channelId: groupId, grpImId: groupImId, isMonitor: monitor) { [weak self] code in
debugPrint("ARUITalking - updateMonitorStatus sdk result = \(code)")
guard let self = self else { return }
if code == 0 {
self.item.isMonitor = monitor ? 1 : 0
self.isMonitor = monitor
self.monitorStatus = monitor
if self.delegate != nil {
self.delegate?.onMonitorStatusChange()
}
}
result(code)
}
}
}
三、音视频通话
效果预览
部分代码实现
@interface ARUICalling : NSObject
/// 基础配置
@property(nonatomic, strong) ARUIConfiguration *config;
+ (instancetype)shareInstance;
/// 通话接口
/// @param users 用户信息
/// @param type 呼叫类型:视频/语音
- (void)call:(NSArray<ARCallUser *> *)users type:(ARUICallingType)type NS_SWIFT_NAME(call(users:type:));
/// 通话接口(用于自定义chanId或添加token)
/// @param users 用户信息
/// @param type 呼叫类型:视频/语音
/// @param chanId 频道id
/// @param token rtc token 动态密钥 【1】安全要求不高: 将值设为 nil 【2】安全要求高: 将值设置为 Token。如果你已经启用了 App Certificate,请务必使用 Token。【3】请务必确保用于生成 Token 的 App ID 和 ARUILogin initWithSdkAppID 时用的是同一个 App ID。
- (void)call:(NSArray<ARCallUser *> *)users type:(ARUICallingType)type chanId:(NSString *_Nullable)chanId token:(NSString *_Nullable)token NS_SWIFT_NAME(call(users:type:chanId:token:));
/// 通话回调
/// @param listener 回调
- (void)setCallingListener:(id<ARUICallingListerner>)listener NS_SWIFT_NAME(setCallingListener(listener:));
/// 设置铃声,建议在30s以内,只支持本地音频文件
/// @param filePath 音频文件路径
- (void)setCallingBell:(NSString *)filePath NS_SWIFT_NAME(setCallingBell(filePath:));
/// 开启静音模式(默认关)
- (void)enableMuteMode:(BOOL)enable NS_SWIFT_NAME(enableMuteMode(enable:));
/// 打开悬浮窗(默认关)
- (void)enableFloatWindow:(BOOL)enable NS_SWIFT_NAME(enableFloatWindow(enable:));
/// 开启自定义路由(默认关)
/// @param enable 打开后,在onStart回调中,会收到对应的ViewController对象,可以自行决定视图展示方式
- (void)enableCustomViewRoute:(BOOL)enable NS_SWIFT_NAME(enableCustomViewRoute(enable:));
/// rtc token 鉴权 (收到onCallInvitedByToken回调时调用)
/// @param token 安全机制
- (void)updateCallingToken:(NSString *_Nonnull)token NS_SWIFT_NAME(updateCallingToken(token:));
@end
四、视频回传、监看
效果预览
部分代码实现
func initializeEngine() {
// init ARtcEngineKit
rtcEngine = ARtcEngineKit.sharedEngine(withAppId: ARUILogin.getSdkAppID(), delegate: self)
let rtcConfig = ARCoreConfig.default().rtcConfig
if (rtcConfig?.addr.count != 0 && rtcConfig?.port != 0) {
/// 配置私有云
let dic: NSDictionary = ["Cmd": "ConfPriCloudAddr", "ServerAdd": rtcConfig?.addr as Any, "Port": rtcConfig?.port as Any] as NSDictionary
rtcEngine.setParameters(toJSONString(dict: dic))
}
rtcEngine.setChannelProfile(.liveBroadcasting)
rtcEngine.enableAudioVolumeIndication(2000, smooth: 3, report_vad: true)
rtcEngine.setClientRole(.broadcaster)
let configuration = ARCameraCapturerConfiguration()
configuration.cameraDirection = .rear
rtcEngine.setCameraCapturerConfiguration(configuration)
rtcEngine.enableVideo()
let fpsValue = ARUIVideoFrameRateFps(rawValue: localConfig.videoFps)?.fps()
rtcEngine.setVideoEncoderConfiguration(ARVideoEncoderConfiguration(size: (ARUIVideoDimension(rawValue: localConfig.videoSize)?.dimension())!, frameRate: ARVideoFrameRate(rawValue: fpsValue ?? 15)!, bitrate: 500, orientationMode: .adaptative))
setLocalVideoRender(render: view)
}
func setLocalVideoRender(render: UIView) {
let videoCanvas = ARtcVideoCanvas()
videoCanvas.view = render
videoCanvas.renderMode = .hidden
rtcEngine.setupLocalVideo(videoCanvas)
rtcEngine.startPreview()
}
func joinChannel(token: String?, expire: Bool) {
rtcEngine.joinChannel(byToken: token, channelId: channelId, uid: userInfo.uId) { [weak self] channel, uid, elapsed in
guard let self = self else { return }
if !expire {
self.startTimer()
self.startReport()
}
debugPrint("ARUIPassBack - joinChannel sucess")
}
}
五、图片、视频上报
部分代码实现
@objc protocol ARReportUploadManagerDelegate: NSObjectProtocol {
/// 上报进度
@objc optional func onReportDataProgress(progress: CGFloat, identification: String)
/// 上报成功
@objc optional func onReportDataSucess(identification: String)
/// 上报失败
@objc optional func onReportDataFailure(identification: String)
}
func fetchImage(for asset: PHAsset) {
let option = PHImageRequestOptions()
option.resizeMode = .fast
option.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: option) { [weak self] image, info in
var downloadFinished = false
if let info = info {
downloadFinished = !(info[PHImageCancelledKey] as? Bool ?? false) && (info[PHImageErrorKey] == nil)
}
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool ?? false)
if downloadFinished, !isDegraded {
guard let self = self else { return }
self.addNewData(images: [image!], assets: [asset])
}
}
}
func calculateFileSize() {
var fileSize = 0
var sandBoxSize = 0
for detail in reportInfo.detail {
if detail.type == 2 {
fileSize = detail.size
sandBoxSize = detail.compressSize
break
} else {
fileSize += detail.size
sandBoxSize += detail.compressSize
}
}
delegate?.onReportDataSourceChange?(fileSize: "\(formatLength(length: fileSize))", compressSize: "\(formatLength(length: sandBoxSize))", enable: assets.count != 0)
}
六、位置回传
部分代码实现
extension ARUIShareLocationController: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
/// 大头针、气泡
if annotation is MAPointAnnotation {
let customReuseIndetifier: String = "annotationReuseIndetifier"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: customReuseIndetifier) as? ARUICustomAnnotationView
if annotationView == nil {
annotationView = ARUICustomAnnotationView(annotation: annotation, reuseIdentifier: customReuseIndetifier)
annotationView?.image = UIImage(named: "icon_direction")
// 设置为false,用以调用自定义的calloutView
annotationView?.canShowCallout = false
// 设置中心点偏移,使得标注底部中间点成为经纬度对应点
annotationView?.centerOffset = CGPoint(x: 0, y: -18)
annotationView?.isDraggable = false
}
if annotation.isKind(of: ARUIMapAnnotation.self) {
let mapAnnotation = annotation as! ARUIMapAnnotation
annotationView?.setHeadUrl(url: mapAnnotation.faceUrl)
annotationView?.setNickName(text: mapAnnotation.nickName)
}
if annotation.isKind(of: MAUserLocation.self) {
localAnnotationView = annotationView
localAnnotationView?.setHeadUrl(url: localUserData.faceUrl)
localAnnotationView?.setNickName(text: localUserData.nickName)
localLocation = annotation as? MAUserLocation
}
return annotationView
}
return nil
}
func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) {
if (!updatingLocation && localAnnotationView != nil) {
UIView.animate(withDuration: 0.1) {
let degree = userLocation.heading.trueHeading - self.mapView.rotationDegree
self.localAnnotationView?.imageView.transform = CGAffineTransform(rotationAngle: degree * Double.pi / 180.0)
}
}
}
}
七、即时消息
部分代码实现
/// 消息内容类型
enum ARIMElemType: NSInteger, Codable, HandyJSONEnum {
/// 未知消息
case none = 100
/// 文本消息
case text = 101
/// 图片消息
case picture = 102
/// 语音消息
case voice = 103
/// 视频消息
case video = 104
/// ptt 语音
case pushtotalk = 105
/// 文件消息
case file = 106
case atText = 107
/// 合并消息
case merger = 108
case card = 109
/// 地理位置消息
case location = 110
/// 自定义消息
case custom = 111
case revoke = 112
case hasReadReceipt = 113
case typing = 114
case quote = 115
case common = 200
case groupMsg = 201
}
/// 消息状态
enum ARIMMessageStatus: NSInteger, Codable, HandyJSONEnum {
case sending = 1
case sendSuccess = 2
case sendFailed = 3
case hasDeleted = 4
case revoked = 5
case filtered = 6
}
/// IM消息
///"sendId":3578970541,"recvId":2723541307,"clientMsgId":"1653488317610","sessionType":1,"contentType":101,"content":"IM消息","curSeq":1011,"sendTime":1653488317,"status":2
struct ARIMMessage: HandyJSON, PersistableRecord {
/// 序列号
var curSeq: UInt64!
/// 消息 Id
var clientMsgId: String!
/// 发送者 Id
var sendId: UInt64!
/// 接受者 id
var recvId: UInt64!
/// 消息类型(点对点消息:1,群组消息:2)
var sessionType: ARIMSessionType = .c2c
/// 消息内容类型,附加信息中的<消息内容类型>
var contentType: ARIMElemType = .none
/// 消息内容
var content: String = ""
/// 消息状态
var status: ARIMMessageStatus?
/// 发送时间(单位:毫秒)
var sendTime: Int?
/// 发送者平台类型
var senderPlatformId: ARIMPlatform?
/// 附加消息
var ex: String?
var syncMsgId: String = ""
/// 消息保存时间(单位:秒)
var createTime: Int = 0
/// 消息归属Id
var msgImId: Int = 0
/// 已读状态,默认未读
var isRead: Bool = false
/// 是否播放(针对语音、ptt 已读未读),默认未播放
var isPlay: Bool = false
/////////////////////////////////////////////////////////////////////////////////
// 自定义消息体
/////////////////////////////////////////////////////////////////////////////////
/// 图片消息
var imageElem: ARUIImageElem?
/// 视频消息
var videoElem: ARUIVideoElem?
/// 语音消息
var soundElem: ARUISoundElem?
/// 文件消息
var fileElem: ARUIFileElem?
/// 位置消息
var locationElem: ARUILocationElem?
}
extension ARIMMessage {
func getImageElem() -> ARUIImageElem {
/// 获取图片消息单元
var elem = ARUIImageElem()
var jsonStr = content.base64Decoded()
/// 兼容之前未base64的消息
if content.base64Decoded() == "【不支持的消息类型】" {
jsonStr = content
}
elem.imageList = JSONDeserializer<ARUIImageItem>.deserializeModelArrayFrom(json: jsonStr) as? [ARUIImageItem]
return elem
}
func getVideoElem() -> ARUIVideoElem {
/// 获取视频消息单元
let elem = JSONDeserializer<ARUIVideoElem>.deserializeFrom(json: content.base64Decoded())
return elem!
}
func getSoundElem() -> ARUISoundElem {
/// 获取音频消息单元
let elem = JSONDeserializer<ARUISoundElem>.deserializeFrom(json: content.base64Decoded())
return elem!
}
func getFileElem() -> ARUIFileElem {
/// 获取文件消息单元
let elem = JSONDeserializer<ARUIFileElem>.deserializeFrom(json: content.base64Decoded())
return elem!
}
func getLocationElem() -> ARUILocationElem {
/// 获取位置消息单元
let elem = JSONDeserializer<ARUILocationElem>.deserializeFrom(json: content.base64Decoded())
return elem!
}
}
八、文字广播、媒体广播
部分代码实现
@protocol ARTalkChannelDelegate <NSObject>
@optional
/// 广播开启
/// @param channel 所在频道。详见 ARTalkChannel 。
/// @param uid 用户id
/// @param userData 自定义信息
- (void)channel:(ARTalkChannel * _Nonnull)channel userStreamOn:(NSString *)uid userData: (NSString * _Nullable)userData;
/// 广播关闭
/// @param channel 所在频道。详见 ARTalkChannel 。
/// @param uid 用户id
/// @param userData 自定义信息
- (void)channel:(ARTalkChannel * _Nonnull)channel userStreamOff:(NSString *)uid userData: (NSString * _Nullable)userData;
/// 开始对讲回调
/// @param channel 所在频道。详见 ARTalkChannel 。
/// @param code 错误码
- (void)channel:(ARTalkChannel * _Nonnull)channel pushToTalkResult:(ARTalkPushToTalkErrorCode)code;
/// 结束对讲回调
/// @param channel 所在频道。详见 ARTalkChannel 。
/// @param code 错误码
- (void)channel:(ARTalkChannel * _Nonnull)channel pushToTalkEnded:(ARTalkPushToTalkEndErrorCode)code;
/// 其他用户开始对讲回调
/// @param channel 所在频道。详见 ARTalkChannel 。
/// @param uid 用户id
/// @param userData 自定义信息
/// @param level 用户等级
- (void)channel:(ARTalkChannel * _Nonnull)channel userIsTalkOn:(NSString *)uid userData: (NSString * _Nullable)userData userLevel:(NSInteger)level;
/// 其他用户结束对讲回调
/// @param channel 所在频道。详见 ARTalkChannel 。
/// @param uid 用户id
/// @param userData 自定义信息
- (void)channel:(ARTalkChannel * _Nonnull)channel userIsTalkOff:(NSString *)uid userData: (NSString * _Nullable)userData;
@end
快对讲 iOS 端基础库
platform :ios, '11.0'
use_frameworks!
target 'ARUITalking' do
# anyRTC 音视频库
pod 'ARtcKit_iOS', '~> 4.3.0.3'
# anyRTC 实时消息库
pod 'ARtmKit_iOS', '~> 1.1.0.1'
# anyRTC 对讲库
pod 'ARTalkKit_iOS'
end
以上就是快对讲IOS端的基本功能实现。快对讲调度系统将语音、视频、图像、文本消息等信息高度融合一体,搭建综合指挥调度业务,不仅实现企业通讯数字信息化,进行高效协作提升企业整体形象,也能满足紧急救援、紧急决策等要求,达到统一指挥、联合行动的目的。开发者可以基于 ARUICalling 音视频通话开源组件,来开发自己的专属快对讲调度系统。