IOS技术分享| IOS快对讲调度场景实现

lxf2023-05-08 00:49:51

前言

“快对讲” 是基于 音视频技术 对讲业务的产品,为客户提供专业对讲、多媒体对讲和可视化调度功能。 主要功能包含:

  • 频道与会话
  • 多频道对讲、监听、锁定、强拆
  • 音视频单人、多人呼叫、呼叫调度台
  • 图片、视频上报
  • 视频回传、监看
  • 位置回传
  • 即时消息:文字消息、语音消息、图片消息、视频消息、文件消息、位置消息
  • 文字广播、媒体广播
  • 监控、录像服务、调度台。

功能体验

  • 快对讲官网
  • 快对讲移动端下载
  • 快对讲调度管理平台

IOS技术分享| IOS快对讲调度场景实现

场景功能实现

一、对讲

效果预览

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
/**
 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

二、频道与会话

效果预览

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
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)
        }
    }
}

三、音视频通话

效果预览

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
@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

四、视频回传、监看

效果预览

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
    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")
        }
    }

五、图片、视频上报

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
@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)
    }

六、位置回传

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
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)
            }
        }
    }
}

七、即时消息

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
/// 消息内容类型
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!
    }
}

八、文字广播、媒体广播

IOS技术分享| IOS快对讲调度场景实现

部分代码实现
@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 音视频通话开源组件,来开发自己的专属快对讲调度系统。

IOS技术分享| IOS快对讲调度场景实现

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!