๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/Third Party

[iOS/API] Agora๋กœ ์‹ค์‹œ๊ฐ„ ์Œ์„ฑ์ฑ„ํŒ… ๊ตฌํ˜„ํ•˜๊ธฐ(feat.ํด๋Ÿฝํ•˜์šฐ์Šค) - 2

by Fomagran ๐Ÿ’ป 2021. 5. 11.
728x90
๋ฐ˜์‘ํ˜•

์•ˆ๋…•ํ•˜์„ธ์š” 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()
            }
        )
        
    }

 

 

์ž„์‹œํ† ํฐ๊ฐ’์„ ๋ฐ›๋Š” ๋ฐฉ๋ฒ•์€  ๋ฐ‘์— ์•„๊ณ ๋ผ ์ฝ˜์†”์ฐฝ์œผ๋กœ ์ด๋™ํ•œ ๋’ค

 

 

Dashboard

 

console.agora.io

 

ํ”„๋กœ์ ํŠธ์˜ ์˜ค๋ฅธ์ชฝ ๋์— ์—ฐํ•„๋ชจ์–‘์„ ๋ˆŒ๋Ÿฌ์„œ 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

 

 

How to Add Live Video Streaming in Your iOS App with Agora

Create a live streaming application to reach a global audience with Agora's Real-time Engagement technology. Perfect for university lectures or streaming a panel of experts.

www.agora.io

 

728x90
๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€