[Swift] TencentCloudでビデオ通話アプリをつくる


:::note warn
iDoorPhoneの初期設定をお探しの方は、以下のトピック１〜２をご参照ください。
:::

# はじめに

iOS向けにTencentCloudを利用した通話アプリを作る機会があったのですが、いくつか参考になる記事があったものの、最初に動くまでいくつかハマりポイントもあったので、備忘録として記事化しようと思います。

1. TencentCloudの会員登録
2. アプリに必要な情報取得
3. Swiftでのアプリ実装



# 1. TencentCloudの会員登録

TencentCloudは、AWSやGCPのように様々なクラウドサービスを提供しており、通話アプリを創る場合、この中のTencent Real-Time Communication (TRTC)を利用します。TencentCloudは会員登録することで、サービスの無料利用枠が利用できます。（執筆時点で、無料会員でも毎月10000分の無料通話が可能）

<br>

まずはTencentCloudのページに行き、右上から無料の会員登録を行います。

https://www.tencentcloud.com/jp/campaign/freetier

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/c884f5a5-2ec9-9b00-d56e-77a3a47e1d2a.png)


<br>

必要情報を入力してサインアップしましょう。私は左下のGoogleアカウントから登録しました。
Googleアカウントを利用する場合でも、国、設定したいパスワード、誕生日の入力が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/cc18133b-c7c8-2765-0f01-be15fc4736ba.png)

上のページでサインアップを押すと、以下のように追加情報の入力を求められるので入力しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/13684f2b-9d75-3518-1dbe-7f484d3c5833.png)

このあとのページでクレカ登録を求められますが、一旦無視してページを閉じてしまっても大丈夫です。

<br>

# 2. アプリに必要な情報取得

通話アプリを作成するには、以下の事が必要になりますので、順を追って説明します。
* TencentCloud内でアプリケーションを作成する
* 作成したアプリケーションのSDKAppID / SDKSecretKeyを取得する

<br>

まずは、こちらのページに戻り、上のナビゲーションメニューの「製品」から、「Tencent Real-Time Communication (TRTC)」を選択しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/b193e3b4-9320-b492-6e9c-37cf28eba4ed.png)

<br>

以下のページ中段にある「無料ではじめる」を押す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/abbcab9d-674b-cb4a-fe59-50ce2cea3448.png)

その後、以下のアプリケーションの作成画面に行くので、以下を入力してGetStartedを押す。
* Application name : 好きなアプリケーション名を入力（デフォルトでも可）
* Select product   : 製品は「Call」を選択
* Region           : 適宜近いところを選ぶ（私はソウルを選びました）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/7c9f07fc-ae4c-5217-468d-07495417b116.png)

<br>

このような画面が表示されれば無事にアプリケーション作成は完了です。
この段階では無料体験版扱いなので、7日の期限付きとなります。

このページの中段「Basic Information」のところに、SDKAppID / SDKSecretKeyがありますので、こちらがアプリ作成に必要な情報になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/cbde70be-fcbd-20a7-3e85-2ced1e678c49.png)

<br>

# 3. Swiftでのアプリ実装
ここからは、Swiftで最小限の実装をしていくまでの解説になります。

環境：
* MacBook Air (M2, 2022)
* macOS Sonoma 14.5
* Xcode 15.0.1

<br>

## STEP1:プロジェクト作成
まずは新規プロジェクトを作成します。
インターフェースはStoryboardを選択してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/b7023a23-9d88-c719-7b16-ba54c9d555ad.png)

<br>

## STEP2:SDKの追加
次にSDKを入れていきます。
こちらのページの「iOS SDKのダウンロード」の箇所からZIPのダウンロードを選び、SDKをダウンロードしてください。ダウンロードしたZIPは解凍しておきます。
https://www.tencentcloud.com/jp/document/product/647/34615

:::note warn
公式ではCocoaPodsによる入れ方も書いてありますが、私の場合はうまくコンパイルが通らなかったので、手動でいれるのをおすすめしておきます。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/b411a7ea-2cc3-dfdb-b8ea-a03381759c72.png)

<br>

次に、Xcode画面左のNavigatorで「SDK」フォルダを作成し、ここに先ほど解凍したZIPの中にある４つのFrameworkを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/7a0f175b-4039-7d45-af6b-f948ccd28ea9.png)

Frameworkを選択するときは、念のため以下の画面で「Copy items if needed」にチェックしておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/d29dd1e6-6f8b-c366-34b4-0c9e00224754.png)

<br>

次に、必要なライブラリを追加します。プロジェクトの設定の「General->Frameworks,Libraries, and Embedded Content」を開き、以下の一覧にある必要なフレームワークとライブラリを追加してください。

また、さきほど追加した４つのライブラリが青で示されていますが、このうち「TXFFmpeg」「TXSoundTouch」の２つについては、Embedを「Embed&Sign」に変更しておきます。

:::note warn
ここが一番のハマりどころです。公式の解説や、他の記事などでは含まれていませんが、実際にビルドしようとすると、ReplayKit、CoreMotion、AVKitもないとエラーになりますので必ず入れておきましょう。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/7b2c9993-720a-bb11-7469-be35e99e1343.png)


<br>

## STEP3:プライバシー設定
ここまでできたら、プライバシーの設定をします。
Info.plistを開いて、以下のようにカメラとマイクのアクセスを追加してください。
* Privacy - Microphone Usage Description
* Privacy - Camera Usage Description

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/3c060182-17d0-1628-f6d6-bfd69e883799.png)

<br>

## STEP4:コード作成
ソースファイルに含まれているViewControllerに、最小限の通話サンプルとして以下のコードをコピペします。こちらはyuppejp(yuppe)さんの記事のものがすごく使いやすかったので、そのまま載せさせていただきます。

<details><summary>ViewController.swift</summary>

```swift:ViewController.swift
import Foundation
import UIKit
import TXLiteAVSDK_TRTC

class ViewController: UIViewController {
    @IBOutlet weak var remoteVideoView: UIView!
    @IBOutlet weak var remoteUserLabel: UILabel!
    @IBOutlet weak var joinButton: UIButton!
    private var trtcCloud: TRTCCloud = TRTCCloud.sharedInstance()
    private var joined = false

    override func viewDidLoad() {
        super.viewDidLoad()

        remoteUserLabel.text = ""
        remoteVideoView.layer.opacity = 0.3
        joinButton.tintColor = UIColor.systemGreen
    }

    @IBAction func joinButtonTouched(_ sender: Any) {
        if joined {
            exitRoom()
        } else {
            enterRoom()
        }
    }

    private func enterRoom() {
        let roomId: Int = 1
        let userId: String = "iOS demo1"
        let isFrontCamera = true
        
        trtcCloud.delegate = self
        trtcCloud.startLocalPreview(isFrontCamera, view: view)
        let params = TRTCParams()
        params.sdkAppId = UInt32(SDKAPPID)
        params.roomId = UInt32(roomId)
        params.userId = userId
        params.role = .anchor
        params.userSig = TrtcUserSig.genTestUserSig(identifier: userId) as String
        trtcCloud.enterRoom(params, appScene: .videoCall)
        
        let encParams = TRTCVideoEncParam()
        encParams.videoResolution = ._640_360
        encParams.videoBitrate = 550
        encParams.videoFps = 15
        trtcCloud.setVideoEncoderParam(encParams)

        trtcCloud.startLocalPreview(isFrontCamera, view: view)
        trtcCloud.startLocalAudio(.music)
    }

    private func exitRoom() {
        trtcCloud.exitRoom()
        trtcCloud.stopLocalPreview()
        trtcCloud.stopLocalAudio()
        trtcCloud.delegate = self
    }
}

extension ViewController: TRTCCloudDelegate {
    func onEnterRoom(_ result: Int) {
        print("*** onEnterRoom: result: \(result)")
        joined = true
        joinButton.setTitle("Leave", for: .normal)
        joinButton.tintColor = UIColor.red
    }
    
    func onExitRoom(_ reason: Int) {
        print("*** onExitRoom: reason: \(reason)")
        joined = false
        joinButton.setTitle("Join", for: .normal)
        joinButton.tintColor = UIColor.systemGreen
    }
    
    func onUserVideoAvailable(_ userId: String, available: Bool) {
        print("*** onUserAudioAvailable: userId: \(userId), available: \(available)")
        if available {
            trtcCloud.startRemoteView(userId, streamType:.small, view: remoteVideoView)
            remoteVideoView.layer.opacity = 1
            remoteUserLabel.text = userId
        } else {
            trtcCloud.stopRemoteView(userId, streamType: .small)
            remoteVideoView.layer.opacity = 0.3
            remoteUserLabel.text = ""
        }
    }
    
    func onError(_ errCode: TXLiteAVError, errMsg: String?, extInfo: [AnyHashable : Any]?) {
        if let errMsg = errMsg {
            print("*** onError: \(errCode): \(errMsg)")
        } else {
            print("*** onError: \(errCode): ?")
        }
    }
}
```
</details>

ただ、これだけだと、「Cannot find 'SDKAPPID' in scope」などのビルドエラーが出ると思いますので、新規Swiftファイル「TrtcUserSig.swift」をプロジェクトに追加し、以下のコードをコピペします。

<details><summary>TrtcUserSig.swift</summary>

```swift:TrtcUserSig.swift
import Foundation
import CommonCrypto
import zlib

let SDKAPPID: Int = MY_SDKAPPID
let SECRETKEY:String = MY_SECRETKEY
let EXPIRETIME: Int = 10 * 60 // seconds

class TrtcUserSig {
    
    class func genTestUserSig(identifier: String) -> String {
        let current = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970
        let TLSTime: CLong = CLong(floor(current))
        var obj: [String: Any] = [
            "TLS.ver": "2.0",
            "TLS.identifier": identifier,
            "TLS.sdkappid": SDKAPPID,
            "TLS.expire": EXPIRETIME,
            "TLS.time": TLSTime
        ]
        let keyOrder = [
            "TLS.identifier",
            "TLS.sdkappid",
            "TLS.time",
            "TLS.expire"
        ]
        var stringToSign = ""
        keyOrder.forEach { (key) in
            if let value = obj[key] {
                stringToSign += "\(key):\(value)\n"
            }
        }
        print("string to sign: \(stringToSign)")
        let sig = hmac(stringToSign)
        obj["TLS.sig"] = sig!
        print("sig: \(String(describing: sig))")
        guard let jsonData = try? JSONSerialization.data(withJSONObject: obj, options: .sortedKeys) else { return "" }
        
        let bytes = jsonData.withUnsafeBytes { (result) -> UnsafePointer<Bytef> in
            return result.bindMemory(to: Bytef.self).baseAddress!
        }
        let srcLen: uLongf = uLongf(jsonData.count)
        let upperBound: uLong = compressBound(srcLen)
        let capacity: Int = Int(upperBound)
        let dest: UnsafeMutablePointer<Bytef> = UnsafeMutablePointer<Bytef>.allocate(capacity: capacity)
        var destLen = upperBound
        let ret = compress2(dest, &destLen, bytes, srcLen, Z_BEST_SPEED)
        if ret != Z_OK {
            print("[Error] Compress Error \(ret), upper bound: \(upperBound)")
            dest.deallocate()
            return ""
        }
        let count = Int(destLen)
        let result = self.base64URL(data: Data.init(bytesNoCopy: dest, count: count, deallocator: .free))
        return result
    }
    
    class func hmac(_ plainText: String) -> String? {
        let cKey = SECRETKEY.cString(using: String.Encoding.ascii)
        let cData = plainText.cString(using: String.Encoding.ascii)
        
        let cKeyLen = SECRETKEY.lengthOfBytes(using: .ascii)
        let cDataLen = plainText.lengthOfBytes(using: .ascii)
        
        var cHMAC = [CUnsignedChar].init(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        let pointer = cHMAC.withUnsafeMutableBufferPointer { (unsafeBufferPointer) in
            return unsafeBufferPointer
        }
        CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), cKey!, cKeyLen, cData, cDataLen, pointer.baseAddress)
        let data = Data.init(bytes: pointer.baseAddress!, count: cHMAC.count)
        return data.base64EncodedString(options: [])
    }
    
    class func base64URL(data: Data) -> String {
        let result = data.base64EncodedString(options: Data.Base64EncodingOptions.init(rawValue: 0))
        var final = ""
        result.forEach { (char) in
            switch char {
            case "+":
                final += "*"
            case "/":
                final += "-"
            case "=":
                final += "_"
            default:
                final += "\(char)"
            }
        }
        return final
    }
}
```
</details>

コピペができたら、TrtcUserSig.swift内でSDKAPPID/SECRETKEYが見つからないエラーが出るので、ここに、最初にTencentCloudで取得したSDKAPPID/SECRETKEYを入力してください。（SECRETKEYは””で囲む）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/9c85e820-4ad6-6f80-0c99-8d4f16134ad5.png)

<br>

最後に、Storyboardに以下のような画面を作成して、ViewControllerのアウトレットと紐づけたら完成です。
* Remote video view : UIView
* Remote user label : UILabel

ビルドをして実行できるのを確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164437/fa5125a0-c1b5-9fe3-37af-a1d82c4d6ec8.png)





# 参考にさせて頂いた記事

* https://qiita.com/yuppejp/items/dd415aefc0f81df4c602
* https://qiita.com/tabu_ch/items/e2178856446b0c6e2d08