0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

はじめに

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

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

1. TencentCloudの会員登録

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


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

image.png


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

image.png

上のページでサインアップを押すと、以下のように追加情報の入力を求められるので入力しておきます。
image.png

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


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

通話アプリを作成するには、以下の事が必要になりますので、順を追って説明します。

  • TencentCloud内でアプリケーションを作成する
  • 作成したアプリケーションのSDKAppID / SDKSecretKeyを取得する

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

image.png


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

image.png

その後、以下のアプリケーションの作成画面に行くので、以下を入力してGetStartedを押す。

  • Application name : 好きなアプリケーション名を入力(デフォルトでも可)
  • Select product : 製品は「Call」を選択
  • Region : 適宜近いところを選ぶ(私はソウルを選びました)

image.png


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

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

image.png


3. Swiftでのアプリ実装

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

環境:

  • MacBook Air (M2, 2022)
  • macOS Sonoma 14.5
  • Xcode 15.0.1

STEP1:プロジェクト作成

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

image.png


STEP2:SDKの追加

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

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

image.png


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

image.png

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

image.png


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

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

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

image.png


STEP3:プライバシー設定

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

  • Privacy - Microphone Usage Description
  • Privacy - Camera Usage Description

image.png


STEP4:コード作成

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

ViewController.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): ?")
        }
    }
}

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

TrtcUserSig.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
    }
}

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

image.png


最後に、Storyboardに以下のような画面を作成して、ViewControllerのアウトレットと紐づけたら完成です。

  • Remote video view : UIView
  • Remote user label : UILabel

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

image.png

参考にさせて頂いた記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?