์๋ ํ์ธ์ Foma ์ ๋๋ค!
์ค๋์ ์ ๋ฒ ๊ธ์์ Agora ํ๋ก์ ํธ ์ธํ ํ๋ ๋ฒ์ ์ด์ด์ ๊ตฌ์ฒด์ ์ผ๋ก ์์ฑ์ฑํ ์ ๊ตฌํํ๋ ๋ฒ์ ๋ํด์
๋ค๋ค๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
(ํน์๋ผ๋ ํ๋ก์ ํธ ์ค์ ๋ฒ์ ์๋ณด์ ๋ถ๋ค์ ์ฌ๊ธฐ ์์ ๋ณด๊ณ ์์ฃผ์ธ์!)
Preview
Pod
Podfile์ ์๋์ ๊ฐ์ด AgoraRtc๋ฅผ ์ถ๊ฐํด์ฃผ์ธ์! (๋ฒ์ ์ ๋ฐ๋์๋ ์์ผ๋ ํ์ธํด์ ์ถ๊ฐํด์ฃผ์ธ์.)
pod ‘AgoraRtcEngine_iOS’, ‘~> 3.1.0’
Info.plist
๋ง์ดํฌ ์ฌ์ฉ ๊ถํ์ ์ค์ ํด์ฃผ์ธ์!
<key>NSMicrophoneUsageDescription</key>
<string>๋ง์ดํฌ ์ข ์ธ๊ฒ?</string>
StoryBoard
AgoraViewController
๋จผ์ ๋ค๋น๊ฒ์ด์ ์ปจํธ๋กค๋ฌ๋ฅผ Embed ํด์ฃผ์๊ณ ์ด๋ฆ์ ์ ๋ ํ ์คํธํ๋์ ์ ์ฅํ ์ ์๋ ๋ฒํผ์ ๋ง๋ค์ด ๋์์ต๋๋ค. (์คํฐ์ง๋ฐฅ์ด ๋ฒํผ์ด์์.)
ChannelViewController
์ ์ฅํ ๋ฐฉ์ ํ๋ฉด์ ์ปฌ๋ ์ ๋ทฐ ํค๋๋ก ๋ ์ด๋ธ์ ๋ฌ์ ๋งํ๋ ์ฌ๋์ธ์ง ๋ฃ๋ ์ฌ๋์ธ์ง ๋์ฐ๊ณ
์ ๋ก ์ด๋ฏธ์ง๋ฅผ ๋์ฐ๋๋ก ํ๊ฒ ์ต๋๋ค.
์๋์ ๋ฐฉ์ ๋๊ฐ๋ ๋ฒํผ๋ ๋ง๋ค์ด์ฃผ์ธ์.
๊ทธ๋ฆฌ๊ณค ๋ ์ฌ์ด๋ฅผ ์ธ๊ทธ๋ก ์ฐ๊ฒฐํด์ฃผ๊ณ identifier๋ฅผ ์ค์ ํด์ฃผ์ธ์!
AgoraViewController
๋จผ์ ์ด๋ฆ์ ์ ๊ณ ์ ์ฅํ ๋ทฐ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์.
๊ทธ ๋ค์ ํ ์คํธํ๋์ ๋ ์ด๋ธ ๋ฒํผ์ ์ฐ๊ฒฐํด์ฃผ์ธ์.
class AgoraViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var fomaLabel: UILabel!
@IBOutlet weak var channelButton: UIButton!
...
์ด๋ฆ์ ์ ์ฅํ ๋ณ์๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์.
var name:String = ""
๋ฒํผ์ ์๋ชจ์์ผ๋ก ๋ง๋ค์ด ์ฃผ๊ฒ ์ต๋๋ค.
func setUI() {
channelButton.layer.cornerRadius = channelButton.frame.height/2
channelButton.layer.masksToBounds = true
}
viewDidLoad์ setUI๋ฉ์๋์ ํ ์คํธํ๋์ delegate๋ฅผ self๋ก ์ค์ ํด์ค๋๋ค.
override func viewDidLoad() {
super.viewDidLoad()
setUI()
nameTextField.delegate = self
}
ํ ์คํธ๊ฐ ๋ฐ๋๋ name์ ํ์ฌ ํ ์คํธํ๋์ ๊ฐ์ผ๋ก ๋ฐ๊ฟ์ค๋๋ค.
@IBAction func textChanged(_ sender: UITextField) {
name = sender.text ?? ""
}
ํ ์คํธํ๋๋๋ฆฌ๊ฒ์ดํธ๋ฅผ extensionํด์ฃผ์ธ์.
extension AgoraViewController:UITextFieldDelegate { }
ํ๋ฉด์ ์ด๋ํ๊ธฐ ์ ์ ChannelViewController์ ์ด๋ฆ์ ํ์ฌ ์ด๋ฆ์ผ๋ก ๋ฐ๊ฟ์ค๋๋ค.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showChannelViewController" {
let vc = segue.destination as! ChannelViewController
vc.name = name
}
}
์คํฐ์ง๋ฐฅ๋ชจ์ ๋ฒํผ์ ๋๋ ์๋ performSegue๋ก ChannelViewController๋ก ์ด๋ํ๊ฒ ํฉ๋๋ค.
@IBAction func joinChannel(_ sender: Any) {
performSegue(withIdentifier: "showChannelViewController", sender: nil)
}
ChannelViewController
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฑ๋์ ์ ์ฅํ์์ ๋๋ฅผ ๊ตฌํํ๋๋ก ํ๊ฒ ์ต๋๋ค.
AgoraRtcKit์ import ํด์ฃผ์ธ์!
import AgoraRtcKit
์คํ ๋ฆฌ๋ณด๋์์ leave๋ฒํผ๊ณผ ์ปฌ๋ ์ ๋ทฐ๋ฅผ ์ฐ๊ฒฐํด์ค๋๋ค.
@IBOutlet weak var leaveButton: UIButton!
@IBOutlet weak var collection: UICollectionView!
์๋์ ๊ฐ์ด ๋ณ์๋ค์ ์ค์ ํด์ฃผ์ธ์!
(์ ๋ Fomagran์ผ๋ก ์ ์ฅํ์ผ๋ฉด ๋งํ๋ ์ฌ๋์ผ๋ก ์๋๋ผ๋ฉด ๋ฃ๋ ์ฌ๋์ผ๋ก ์ค์ ํ์ต๋๋ค,)
//์
์ฅํ ๋ ์
๋ ฅํ ์ด๋ฆ์ ๋ฐ์ ๋ณ์
var name:String = ""
//agoraRtcKit
var agkit: AgoraRtcEngineKit?
//uid
var userID: UInt = 0
//๋งํ๊ณ ์๋ ์ฌ๋๋ค
var activeSpeakers: Set<UInt> = []
//๋งํ๊ณ ์๋ ์ฌ๋
var activeSpeaker: UInt?
//๋ฃ๊ณ ์๋ ์ฌ๋๋ค
var activeAudience: Set<UInt> = []
//๋งํ๋ ์ฌ๋์ธ์ง ๋ฃ๋ ์ฌ๋์ธ์ง ์ญํ
lazy var role:AgoraClientRole = name == "Fomagran" ? .broadcaster : .audience
๋ณธ๊ฒฉ์ ์ผ๋ก Agora๋ฅผ ์ฐ๊ฒฐํด์ค๋๋ค.
์ฑ์์ด๋๋ Agora ๋์ฌ๋ณด๋๋ก ๊ฐ์ ์ ํ์ธํ์๋ฉด ๋ฉ๋๋ค.
func connectAgora() {
//์ฑ์์ด๋๋ก ์คํ
agkit = AgoraRtcEngineKit.sharedEngine(withAppId: "์ฑ์์ด๋", delegate: self)
//์ค๋์ค ๊ฐ๋ฅํ๊ฒ ์ค์
agkit?.enableAudio()
//์ค๋์ค ๋ณผ๋ฅจ ์ค์
agkit?.enableAudioVolumeIndication(1000, smooth: 3, report_vad: true)
//์ฑ๋ ํ๋กํ ์ค์
agkit?.setChannelProfile(.liveBroadcasting)
//ํ์ฌ ์ญํ ์ค์
agkit?.setClientRole(role)
}
์ฑ๋์ ์ ์ฅํ๊ธฐ ์ํ ๋ฉ์๋๋ฅผ ์์ฑํด์ค๋๋ค.
agkit์์ joinChannel( byToken,channelId,info,uid,joinSuccess) ๊ฐ ํ๋ผ๋ฏธํฐ๋ก ์๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ฃผ์ธ์.
๊ทธ๋ฆฌ๊ณ ์ฐธ๊ฐํ ์ ์ ์ ์ญํ ์ ๋ฐ๋ผ ๋ฃ๋ ์ฌ๋๊ณผ ๋งํ๋ ์ฌ๋์ ๋ฃ์ด์ค๋ค ์ปฌ๋ ์ ๋ทฐ๋ฅผ ๋ฆฌ๋ก๋ ํด์ฃผ๊ฒ ์ต๋๋ค.
func joinChannel() {
agkit?.joinChannel(
byToken: "์์ ํ ํฐ ๊ฐ", channelId: "์ฑ๋ ์์ด๋",
info: nil, uid:userID,
joinSuccess: { (_, uid, elapsed) in
self.userID = uid
if self.role == .audience {
self.activeAudience.insert(uid)
} else {
self.activeSpeakers.insert(uid)
}
self.collection.reloadData()
}
)
}
์์ํ ํฐ๊ฐ์ ๋ฐ๋ ๋ฐฉ๋ฒ์ ๋ฐ์ ์๊ณ ๋ผ ์ฝ์์ฐฝ์ผ๋ก ์ด๋ํ ๋ค
ํ๋ก์ ํธ์ ์ค๋ฅธ์ชฝ ๋์ ์ฐํ๋ชจ์์ ๋๋ฌ์ Edit์ ํด์ค๋๋ค.
์๋๋ก ๋ด๋ ค์ Features์ Temp token for audio/video call์ Generate temp token์ ๋๋ฌ์ฃผ์ธ์!
์ฌ๊ธฐ์ ์ํ๋ ์ฑ๋์ด๋ฆ์ ์ ๊ณ ์๋ Generate Temp Token์ ๋๋ฌ์ค๋๋ค.
๊ทธ๋ฌ๋ฉด ์๋์ ๊ฐ์ด ์์ ํ ํฐ์ด ๋์ค๊ฒ ๋๋๋ฐ ์ด๊ฒ์ 24์๊ฐ๋์๋ง ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ ์ฅํ ๋ค ์ joinChannel์ byToken ํ๋ผ๋ฏธํฐ ๊ฐ์ผ๋ก ๋ฃ์ด์ฃผ์ธ์!
leaveChannel์ ํ ๋๋ ์๋์ ๊ฐ์ด ์ค์ ํด์ฃผ์ธ์.
@IBAction func leaveChannel(_ sender: Any) {
self.agkit?.createRtcChannel("MoA")?.leave()
self.agkit?.leaveChannel()
AgoraRtcEngineKit.destroy()
self.navigationController?.popViewController(animated: true)
}
AgoraRtcEngineDelegate
์๊ณ ๋ผ ๋๋ฆฌ๊ฒ์ดํธ๋ ์๋์ ๊ฐ์ด ์ค์ ํด์ค๋๋ค.
extension ChannelViewController: AgoraRtcEngineDelegate {
//Agora์์ ๋ฃ๋ ์ฌ๋์ด๋ ๋งํ๋ ์ฌ๋ ์ ๋ณด๊ฐ ๋ฐ๋์์๋
func rtcEngine(
_ engine: AgoraRtcEngineKit,
remoteAudioStateChangedOfUid uid: UInt, state: AgoraAudioRemoteState,
reason: AgoraAudioRemoteStateReason, elapsed: Int
) {
switch state {
//๋งํ๊ธฐ๋ฅผ ์์ํ ๋
case .decoding, .starting:
//๋ฃ๋ ์ฌ๋์์ ๋บด์ฃผ๊ณ
self.activeAudience.remove(uid)
//๋งํ๋ ์ฌ๋๋ค์ ๋ฃ์ด์ค๋ค.
self.activeSpeakers.insert(uid)
//๋ฉ์ท์๋
case .stopped, .failed:
//๋งํ๋ ์ฌ๋์์ ์ญ์ ํ๋ค.
self.activeSpeakers.remove(uid)
default:
return
}
self.collection.reloadData()
}
//ํ์ฌ ๋งํ๋ ์ฌ๋ ์ค์
func rtcEngine(_ engine: AgoraRtcEngineKit, activeSpeaker speakerUid: UInt) {
//ํ์ฌ ๋งํ๋ ์ฌ๋์ผ๋ก ์ค์ ํด์ค๋ค.
self.activeSpeaker = speakerUid
self.collection.reloadData()
}
}
ChannelSectionHeader
์ปฌ๋ ์ ๋ทฐ ํค๋๋ฅผ ๋ง๋ค๊ณ ์คํ ๋ฆฌ๋ณด๋์ ์ฐ๊ฒฐํด์ฃผ์ธ์.
class ChannelSectionHeader: UICollectionReusableView {
@IBOutlet weak var headerLabel:UILabel!
}
UserCollectionViewCell
์ปฌ๋ ์ ๋ทฐ์ ์ ์๋์ ๊ฐ์ด ์ค์ ํด์ฃผ์ธ์.
์ ๋ ๋งํ๋ ์ฌ๋์ด๋ฉด ์คํฐ์ง๋ฐฅ์ผ๋ก ์๋๋ฉด ๋ฑ์ด ์ด๋ฏธ์ง๋ก ํ์ต๋๋ค.
class UserCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var profile: UIImageView!
func setUI(isSpeaker:Bool) {
profile.layer.cornerRadius = profile.frame.height/2
profile.image = isSpeaker ? UIImage(named: "์คํฐ์ง๋ฐฅ.png") : UIImage(named: "๋ฑ์ด.jpeg")
}
}
UICollectionViewDataSource
์๋์ ๊ฐ์ด ์ปฌ๋ ์ ๋ทฐ ๋ฐ์ดํฐ์์ค๋ฅผ ์ค์ ํด์ฃผ์ธ์.
extension ChannelViewController:UICollectionViewDataSource {
//๋ฃ๋ ์ฌ๋๊ณผ ๋งํ๋ ์ฌ๋ ์น์
2๊ฐ๋ฅผ ๋ง๋ค์ด์ค.
func numberOfSections(in collectionView: UICollectionView) -> Int {
2
}
//section 0๋ฒ์จฐ๋ฅผ ๋งํ๋ ์ฌ๋, 1๋ฒ์จฐ๋ฅผ ๋ฃ๋ ์ฌ๋์ผ๋ก ์ค์ ํด์ค.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return section == 0 ? activeSpeakers.count : activeAudience.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collection.dequeueReusableCell(withReuseIdentifier: "UserCollectionViewCell", for: indexPath) as! UserCollectionViewCell
if indexPath.section == 0 {
cell.setUI(isMaster:true)
}else{
cell.setUI(isMaster:false)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let sectionHeader = collection.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "ChannelSectionHeader", for: indexPath) as! ChannelSectionHeader
sectionHeader.headerLabel.text = indexPath.section == 0 ? "Speakers" : "Audience"
return sectionHeader
}
}
์คํํ๋ฉด
์๋ฎฌ๋ ์ดํฐ์์๋ foma๋ก ์ ์ฅํ๊ณ ์ ํธ๋ํฐ์์๋ Fomagran์ผ๋ก ์ ์ฅํ์ต๋๋ค.
ํด๋ํฐ์์ ๋ง์ ํ๊ฒ ๋๋ฉด ์ค์ ๋ก ๋งฅ์์ ์๋ฆฌ๊ฐ ๋ค๋ฆฝ๋๋ค!!
(์๋ฆฌ๋ฅผ ๋ด์ ์ ์๊ฒ ๋ผ์ ์ํ๊น๋ค์....ใ )
์ค๋์ ์ด๋ ๊ฒ ์ค์๊ฐ ์์ฑ์ฑํ ํ๋ ๋ฒ์ ๋ํด์ ์์๋ณด์๋๋ฐ์.
๋ค์์ ๋ ๊ตฌ์ฒด์ ์ผ๋ก ์ฑ๋์ ์ด๋ป๊ฒ ๋ค์ด๊ฐ๊ณ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋์ง์ ๋ํด์ ๋ค๋ค๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
ํน์๋ผ๋ ๊ถ๊ธํ์ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ๋๊ธ๋ก ์๋ ค์ฃผ์ธ์!
Reference
๋๊ธ