IOS技术分享| ARCall视频通话重构

火星殖民
• 阅读 211

ARCall 简介

ARCall 是anyRTC开源的呼叫的示例项目,演示了如何通过 anyRTC云服务,并配合 anyRTC RTC SDK、anyRTC RTM SDK,快速实现呼叫邀请通话的场景。

  • 一对一视频呼叫。
  • 一对一音频呼叫。
  • 多人音视频通话,最大支持50人同时通话。
  • 可运用自采集模块,加载第三方美颜库,实现美颜贴图功能。
  • 可对接第三方推送实现推送功能。

重构内容

九月份我们对ARCall iOS端项目进行了重构,开发语言由 Objective-C 变为 Swift,通话界面和逻辑处理进行深度优化,希望能给有需要的开发者带来帮助。

下载体验 & 源码下载

ARCall 开源项目支持多个平台,包含Android、iOS、Web、uniapp

IOS技术分享| ARCall视频通话重构

开发环境

  • 开发工具:Xcode12 真机运行
  • 开发语言:Swift
  • 实现:音视频通话。

项目结构

ARCall 实现了点对点呼叫、多人呼叫邀请,功能包含视频通话、语音通话、多人通话、sip呼叫、手表呼叫、呼叫中邀请、大小屏切换、悬浮窗、AI降噪、断网重连等等功能。

IOS技术分享| ARCall视频通话重构

示例代码

效果展示

IOS技术分享| ARCall视频通话重构

代码实现
    func initializeUI() {
        self.navigationItem.leftBarButtonItem = createBarButtonItem(title: "", image: "icon_return_w")
        calleeIdLabel.text = "您的呼叫ID:\(localUid)"
        
        let setupButton = UIButton(type: .custom)
        setupButton.setImage(UIImage(named: "icon_set"), for: .normal)
        setupButton.frame = CGRect.init(x: 0, y: 0, width: 40, height: 40)
        setupButton.addTarget(self, action: #selector(setup), for: .touchUpInside)
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: setupButton)
        
        if callWay == .single {
            self.title = "点对点呼叫邀请"
            mainCollectionView.removeFromSuperview()
        } else {
            self.title = "多人呼叫邀请"
            titleLabel.text = "请输入对方的ID,可输入多个"
        }
        
        for objc in stackView.subviews {
            let button = objc as? UIButton
            button?.addTarget(self, action: #selector(didClickTextField), for: .touchUpInside)
        }
        
        self.view.addSubview(calleeIdTextField)
        mainCollectionView.collectionViewLayout = flowLayout
    }
    
    @objc func didClickTextField() {
        calleeIdTextField.becomeFirstResponder()
    }
    
    @objc func limitTextField() {
        
        if calleeIdTextField.text?.count ?? 0 > 4 {
            calleeIdTextField.text = String((calleeIdTextField.text?.prefix(4))!)
        }
        let calleeId = calleeIdTextField.text
        
        let arr = stringToArr(str: calleeId)
        for index in 0...3 {
            var text = ""
            (index < arr.count) ? (text = String(arr[index])) : nil
            let button: UIButton = stackView.subviews[index] as! UIButton
            button.setTitle(text, for: .normal)
        }
        
        if callWay == .single {
            // 点对点呼叫
            callButton.isEnabled = (calleeId?.count == 4)
            callButton.isEnabled ? (callButton.setTitleColor(UIColor(hexString: "#40A3FB"), for: .normal)) : (callButton.setTitleColor(UIColor.lightGray, for: .normal))
        } else {
            // 多人呼叫
            if calleeIdArr.count < 6 {
                if calleeId?.count == 4 {
                    if calleeIdArr.contains(calleeId!) == false {
                        calleeIdArr.append(calleeId!)
                        calleeIdTextField.text = ""
                        if mainCollectionView.isHidden {
                            mainCollectionView.isHidden = false
                            changeConstraint(exist: true)
                        }
                        mainCollectionView.reloadData()
                        callButton.isEnabled = true
                        callButton.setTitleColor(UIColor(hexString: "#40A3FB"), for: .normal)
                        
                        for index in 0...3 {
                            let button: UIButton = stackView.subviews[index] as! UIButton
                            button.setTitle("", for: .normal)
                        }
                    } else {
                        showToast(text: "当前ID已存在,请重新输入", image: "icon_warning")
                    }
                }
            }
        }
    }
效果展示

IOS技术分享| ARCall视频通话重构

代码实现
    private func acceptCallInvitation() {
        // 接受呼叫邀请
        if infoModel.callMode == .video {
            view.insertSubview(remoteVideo, belowSubview: calledView)
            view.insertSubview(localVideo, belowSubview: calledView)
            
            setVideoEncoderConfiguration()
            let videoCanvas = ARtcVideoCanvas()
            videoCanvas.view = localVideo.renderView
            view.insertSubview(localVideo, belowSubview: calledView)
            rtcKit.setupLocalVideo(videoCanvas)
        }
    }
    
    private func acceptedInvitation(callMode: CallMode) {
        // 对方已同意呼叫
        if !isCallSucess {
            callStateLabel.text = "接听中..."
            playCallBell(isOpen: false)
            isCallSucess.toggle()
            selectCallMode(mode: callMode)
            infoModel.callMode = callMode
            acceptCallInvitation()
            rtcKit.setClientRole(.broadcaster)
            
            // 兼容异常问题
            leaveReason = .drop
            dealWithException(time: 10)
        }
    }
    
    private func switchAudioMode() {
        // 切换音频呼叫
        windowStatus = .audio
        infoModel.callMode = .audio
        selectCallMode(mode: .audio)
        localVideo.removeFromSuperview()
        remoteVideo.removeFromSuperview()
        
        rtcKit.disableVideo()
    }
    
    private func selectCallMode(mode: CallMode) {
        // 选择呼叫模式
        calledView.isHidden = false
        callingView.isHidden = true
        hangupButton.isHidden = false
        
        let isHidden = (mode == .video)
        backView.isHidden = isHidden
        switchAudioButton.isHidden = !isHidden
        switchCameraButton.isHidden = !isHidden
        audioButton.isHidden = isHidden
        speakerButton.isHidden = isHidden
    }
效果展示

IOS技术分享| ARCall视频通话重构

代码实现
    private func initializeEngine() {
        // init ARtcEngineKit
        rtcKit = ARtcEngineKit.sharedEngine(withAppId: AppID, delegate: self)
        rtcKit.setChannelProfile(.liveBroadcasting)
        
        switchAINoise()
    }
    
    private func joinChannel() {
        rtcKit.joinChannel(byToken: nil, channelId: infoModel.channelId, uid: UserDefaults.string(forKey: .uid)) { (channel, uid, elapsed) in
            print("joinChannel sucess")
        }
    }
    
    @objc func switchAINoise() {
        // AI 降噪
        let dic = ["Cmd": "SetAudioAiNoise", "Enable": Default.bool(forKey: "noise") ? 1: 0] as [String : Any]
        rtcKit.setParameters(getJSONStringFromDictionary(dictionary: dic as NSDictionary))
    }
    
    private func switchVideoSize() {
        // 大小屏切换
        if isCallSucess {
            let frame = localVideo.frame
            localVideo.frame = remoteVideo.frame
            remoteVideo.frame = frame
            
            if localVideo.frame.width == ARScreenWidth {
                view.insertSubview(localVideo, belowSubview: calledView)
                view.insertSubview(remoteVideo, belowSubview: calledView)
            } else {
                view.insertSubview(remoteVideo, belowSubview: calledView)
                view.insertSubview(localVideo, belowSubview: calledView)
            }
        }
    }
效果展示

IOS技术分享| ARCall视频通话重构

代码实现
// MARK - videoLayout

extension ARGroupVideoController {
    func videoLayout() {
        videoArr.count <= 1 ? destroyGroupVc() : nil
        
        let count = videoArr.count
        let padding: CGFloat = 1.0
        var warpCount: CGFloat = 2.0
        let rate: CGFloat = 1.0
        if count > 4 {
            if count <= 9 {
                warpCount = 3.0
            } else if count <= 16 {
                warpCount = 4.0
            } else {
                warpCount = 5.0
            }
        }
        
        let itemWidth: CGFloat = (ARScreenWidth - padding * (warpCount - 1))/warpCount
        let itemHeight: CGFloat = itemWidth * rate
        let index = ceil(CGFloat(videoArr.count)/warpCount)
        containerConstraint.constant = itemHeight * index + (index - 1) * padding
        
        var lastView: UIView!
        for (index, object) in videoArr.enumerated() {
            let video = object as UIView
            containerView.insertSubview(video, at: 0)
            
            let rowCount: NSInteger = videoArr.count % NSInteger(warpCount) == 0 ? videoArr.count / NSInteger(warpCount) : videoArr.count / NSInteger(warpCount) + 1
            
            let currentRow: NSInteger = index / NSInteger(warpCount)
            let currentColumn: NSInteger = index % NSInteger(warpCount)
            
            video.snp.remakeConstraints({ (make) in
                make.width.equalTo(itemWidth)
                make.height.equalTo(itemHeight)

                if (currentRow == 0) {
                    make.top.equalTo(containerView)
                }
                
                if (currentRow == rowCount - 1) {
                    make.bottom.equalTo(containerView)
                }
                
                if currentRow != 0 && (currentRow != rowCount - 1) {
                    if currentColumn == 0 {
                        make.top.equalTo(lastView.snp_bottom).offset(padding)
                    } else {
                        make.bottom.equalTo(lastView.snp_bottom).offset(padding)
                    }
                }
                
                if (currentColumn == 0) {
                    make.left.equalTo(containerView);
                }
                
                if (currentColumn == Int(warpCount) - 1) {
                    make.right.equalTo(containerView)
                }
                
                if currentColumn != 0 && (currentColumn != Int(warpCount) - 1) {
                    make.left.equalTo(lastView.snp_right).offset(padding)
                }
            })
            lastView = video
        }
    }
}

结束语

针对本次 ARCall-iOS的重构,因时间有限项目中还存在一些bug和待完善的功能点。仅供参考,欢迎大家fork。有不足之处欢迎大家指出issues。
最后再贴一下 Github开源下载地址

如果觉得不错,希望点个star~

IOS技术分享| ARCall视频通话重构

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这