From 0a30bec1510f15d8039b498dc56b00d86161683b Mon Sep 17 00:00:00 2001 From: SUZUKI Tetsuya Date: Fri, 15 Sep 2017 17:11:51 +0900 Subject: [PATCH] =?UTF-8?q?1.0.0=20=E3=81=AE=E3=82=A4=E3=83=B3=E3=83=9D?= =?UTF-8?q?=E3=83=BC=E3=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 71 ++ CHANGES.md | 118 ++ LICENSE | 2 +- README.md | 22 +- Sora/Cartfile | 3 + Sora/Cartfile.resolved | 3 + Sora/Sora.xcodeproj/project.pbxproj | 664 ++++++++++ .../xcshareddata/xcschemes/Sora.xcscheme | 99 ++ Sora/Sora/BuildInfo.swift | 72 ++ Sora/Sora/Connection.swift | 72 ++ .../AudioCodecViewController.swift | 74 ++ .../ConnectionController.storyboard | 1011 +++++++++++++++ .../ConnectionController.swift | 174 +++ .../ConnectionNavigationController.swift | 29 + .../ConnectionViewController.swift | 818 ++++++++++++ .../RoleViewController.swift | 99 ++ .../VideoCodecViewController.swift | 83 ++ Sora/Sora/Event.swift | 105 ++ Sora/Sora/EventHandlers.swift | 143 +++ Sora/Sora/Extensions.swift | 15 + Sora/Sora/Info.plist | 26 + Sora/Sora/JSON.swift | 28 + Sora/Sora/MediaConnection.swift | 413 ++++++ Sora/Sora/MediaOption.swift | 74 ++ Sora/Sora/MediaStream.swift | 156 +++ Sora/Sora/Message.swift | 460 +++++++ Sora/Sora/PeerConnection.swift | 1103 +++++++++++++++++ Sora/Sora/RTCExtensions.swift | 52 + Sora/Sora/Sora.h | 11 + Sora/Sora/VideoFrame.swift | 42 + Sora/Sora/VideoRenderer.swift | 44 + Sora/Sora/VideoView.swift | 132 ++ Sora/Sora/VideoView.xib | 31 + Sora/SoraTests/Info.plist | 24 + Sora/SoraTests/SoraTests.swift | 24 + Sora/jazzy.yaml | 15 + examples/SoraApp/Cartfile | 4 + .../SoraApp/SoraApp.xcodeproj/project.pbxproj | 426 +++++++ examples/SoraApp/SoraApp/AppDelegate.swift | 38 + .../AppIcon.appiconset/Contents.json | 48 + .../Base.lproj/LaunchScreen.storyboard | 27 + .../SoraApp/Base.lproj/Main.storyboard | 73 ++ examples/SoraApp/SoraApp/Info.plist | 42 + examples/SoraApp/SoraApp/ViewController.swift | 87 ++ 44 files changed, 7054 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 CHANGES.md create mode 100644 Sora/Cartfile create mode 100644 Sora/Cartfile.resolved create mode 100644 Sora/Sora.xcodeproj/project.pbxproj create mode 100644 Sora/Sora.xcodeproj/xcshareddata/xcschemes/Sora.xcscheme create mode 100644 Sora/Sora/BuildInfo.swift create mode 100644 Sora/Sora/Connection.swift create mode 100644 Sora/Sora/ConnectionController/AudioCodecViewController.swift create mode 100644 Sora/Sora/ConnectionController/ConnectionController.storyboard create mode 100644 Sora/Sora/ConnectionController/ConnectionController.swift create mode 100644 Sora/Sora/ConnectionController/ConnectionNavigationController.swift create mode 100644 Sora/Sora/ConnectionController/ConnectionViewController.swift create mode 100644 Sora/Sora/ConnectionController/RoleViewController.swift create mode 100644 Sora/Sora/ConnectionController/VideoCodecViewController.swift create mode 100644 Sora/Sora/Event.swift create mode 100644 Sora/Sora/EventHandlers.swift create mode 100644 Sora/Sora/Extensions.swift create mode 100644 Sora/Sora/Info.plist create mode 100644 Sora/Sora/JSON.swift create mode 100644 Sora/Sora/MediaConnection.swift create mode 100644 Sora/Sora/MediaOption.swift create mode 100644 Sora/Sora/MediaStream.swift create mode 100644 Sora/Sora/Message.swift create mode 100644 Sora/Sora/PeerConnection.swift create mode 100644 Sora/Sora/RTCExtensions.swift create mode 100644 Sora/Sora/Sora.h create mode 100644 Sora/Sora/VideoFrame.swift create mode 100644 Sora/Sora/VideoRenderer.swift create mode 100644 Sora/Sora/VideoView.swift create mode 100644 Sora/Sora/VideoView.xib create mode 100644 Sora/SoraTests/Info.plist create mode 100644 Sora/SoraTests/SoraTests.swift create mode 100644 Sora/jazzy.yaml create mode 100644 examples/SoraApp/Cartfile create mode 100644 examples/SoraApp/SoraApp.xcodeproj/project.pbxproj create mode 100644 examples/SoraApp/SoraApp/AppDelegate.swift create mode 100644 examples/SoraApp/SoraApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/SoraApp/SoraApp/Base.lproj/LaunchScreen.storyboard create mode 100644 examples/SoraApp/SoraApp/Base.lproj/Main.storyboard create mode 100644 examples/SoraApp/SoraApp/Info.plist create mode 100644 examples/SoraApp/SoraApp/ViewController.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7d82f6b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata + +## Other +*.xccheckout +*.moved-aside +*.xcuserstate +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build +Sora/Carthage/Build +Sora/Carthage/Checkouts +examples/SoraApp/Carthage/Build +examples/SoraApp/Carthage/Checkouts + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md + +fastlane/report.xml +fastlane/screenshots + +*.swp +doc +project.xcworkspace diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..0a163e43 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,118 @@ +# 変更履歴 + +- UPDATE + - 下位互換がある変更 +- ADD + - 下位互換がある追加 +- CHANGE + - 下位互換のない変更 +- FIX + - バグ修正 + +## 1.0.0 + +- [CHANGE] WebRTC M57 に対応した + +- [CHANGE] 対応アーキテクチャを arm64 のみにした + +- [CHANGE] マルチストリームに対応した + +- [CHANGE] シグナリング: "notify" に対応した + +- [CHANGE] シグナリング: Sora の仕様変更に伴い、 "stats" への対応を廃止した + +- [CHANGE] シグナリング: Sora の仕様変更に伴い、 "connect" の "access_token" パラメーターを "metadata" に変更した + +- [CHANGE] API: ArchiveFinished: 削除した + +- [CHANGE] API: ArchiveFailed: 削除した + +- [CHANGE] API: MediaConnection: MediaStream を複数保持するようにした + +- [CHANGE] API: MediaConnection: ``multistreamEnabled`` プロパティを追加した + +- [CHANGE] API: MediaConnection: 次の変数の型を変更した + + - ``webSocketEventHandlers``: ``WebSocketEventHandlers?`` --> ``WebSocketEventHandlers`` + - ``signalingEventHandlers``: ``SignalingEventHandlers?`` --> ``SignalingEventHandlers`` + - ``peerConnectionEventHandlers``: ``PeerConnectionEventHandlers?`` --> ``PeerConnectionEventHandlers`` + +- [CHANGE] API: MediaConnection: ``connect(accessToken:timeout:handler:)`` メソッドの型を ``connect(metadata:timeout:handler:)`` に変更した + +- [CHANGE] API: MediaConnection, MediaStream: 次の API を MediaStream に移行した + + - ``var videoRenderer`` + + - ``func startConnectionTimer(timeInterval:handler:)`` + +- [CHANGE] API: MediaConnection.State: 削除した + +- [CHANGE] API: MediaOption.AudioCodec: ``unspecified`` を ``default`` に変更した + +- [CHANGE] API: MediaOption.VideoCodec: ``unspecified`` を ``default`` に変更した + +- [CHANGE] API: MediaPublisher: ``autofocusEnabled`` プロパティを追加した + +- [CHANGE] API: MediaStream: RTCPeerConnection のラッパーではなく、 RTCMediaStream のラッパーに変更した + +- [CHANGE] API: MediaStream: ``startConnectionTimer(timeInterval:handler:)``: タイマーを起動した瞬間もハンドラーを実行するようにした + +- [CHANGE] API: MediaStream.State: 削除した + +- [CHANGE] API: PeerConnection: RTCPeerConnection のラッパーとして追加した + +- [CHANGE] API: SignalingConnected: 削除した + +- [CHANGE] API: SignalingCompleted: 削除した + +- [CHANGE] API: SignalingDisconnected: 削除した + +- [CHANGE] API: SignalingFailed: 削除した + +- [CHANGE] API: StatisticsReport: RTCStatsReport の変更 (名前が RTCLegacyStatsReport に変更された) に伴い削除した + +- [CHANGE] API: VideoView: 映像のアスペクト比を保持するようにした + +- [UPDATE] API: MediaCapturer: 同一の RTCPeerConnectionFactory で再利用するようにした + +- [UPDATE] API: MediaCapturer: 映像トラック名と音声トラック名を自動生成するようにした + +- [UPDATE] API: VideoRenderer: 描画処理をメインスレッドで実行するようにした + +- [UPDATE] API: VideoView: UI の設計に Nib ファイルを利用するようにした + +- [UPDATE] API: VideoView: バックグラウンド (ビューがキーウィンドウに表示されていない) では描画処理を中止するようにした + +- [ADD] API: BuildInfo: 追加した + +- [ADD] API: ConnectionController: 追加した + +- [ADD] API: Connection: 次の API を追加した + + - ``var numberOfConnections`` + + - ``func onChangeNumberOfConnections(handler:)`` + +- [ADD] API: Connection, MediaConnection, MediaStream, PeerConnection: 次のイベントで (NotificationCenter による) 通知を行うようにした + + - onConnect + - onDisconnect + - onFailure + +- [ADD] API: WebSocketEventHandlers, SignalingEventHandlers, PeerConnectionEventHandlers: イニシャライザーを追加した + +- [FIX] シグナリング: 音声コーデック Opus を指定するためのパラメーターの間違いを修正した + +- [FIX] 接続解除後にイベントログを記録しようとして落ちる現象を修正した + +- [FIX] 接続失敗時にデバイスを初期化しようとして落ちる現象を修正した (接続成功時のみ初期化するようにした) + +- [FIX] 接続試行中にエラーが発生して失敗したにも関わらず、成功と判断されてしまう場合がある現象を修正した + +- [FIX] API: MediaConnection: 接続解除後もタイマーが実行されてしまう場合がある現象を修正した (タイマーに関する API は MediaStream に移動した) + +- [FIX] API: PeerConnection: 接続失敗時でもタイムアウト時のイベントハンドラが呼ばれる現象を修正した + +## 0.1.0 + +**0.1.0 リリース** diff --git a/LICENSE b/LICENSE index 8dada3ed..8731b265 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright 2016 Shiguredo Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index fa695f4c..49caeb4f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# sora-ios-sdk-diet -WebRTC SFU Sora iOS SDK +# Sora iOS SDK + +Sora iOS SDK は [WebRTC SFU Sora](https://sora.shiguredo.jp) の iOS クライアントアプリケーションを開発するためのライブラリです。 + +使い方は [Sora iOS SDK ドキュメント](https://sora.shiguredo.jp/ios-sdk-doc/) を参照してください。 + +## システム条件 + +- iOS 10.1 以降 (シミュレーターは非対応) +- Mac OS X 10.11.6 以降 +- Xcode 8.1 以降 +- Swift 3.0.1 +- WebRTC M57 +- WebRTC SFU Sora 17.02 以降 + +## サポートについて + +Sora iOS SDK に関する質問・要望・バグなどの報告は Issues の利用をお願いします。 +ただし、 Sora のライセンス契約の有無に関わらず、 Issue への応答時間と問題の解決を保証しませんのでご了承ください。 +Sora iOS SDK に対する有償のサポートについては sora at shiguredo.jp までお問い合わせください。 diff --git a/Sora/Cartfile b/Sora/Cartfile new file mode 100644 index 00000000..242f4905 --- /dev/null +++ b/Sora/Cartfile @@ -0,0 +1,3 @@ +github "shiguredo/sora-webrtc-ios" "57.0" +github "facebook/SocketRocket" "0.4.2" +github "johnsundell/unbox" "2.2.1" diff --git a/Sora/Cartfile.resolved b/Sora/Cartfile.resolved new file mode 100644 index 00000000..63600e61 --- /dev/null +++ b/Sora/Cartfile.resolved @@ -0,0 +1,3 @@ +github "facebook/SocketRocket" "0.4.2" +github "shiguredo/sora-webrtc-ios" "57.0" +github "johnsundell/unbox" "2.2.1" diff --git a/Sora/Sora.xcodeproj/project.pbxproj b/Sora/Sora.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4deaa97b --- /dev/null +++ b/Sora/Sora.xcodeproj/project.pbxproj @@ -0,0 +1,664 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + 918201871D585E1A00178E2B /* Carthage */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 9182018A1D585E1B00178E2B /* Build configuration list for PBXAggregateTarget "Carthage" */; + buildPhases = ( + 9182018B1D585E2500178E2B /* ShellScript */, + ); + dependencies = ( + ); + name = Carthage; + productName = Carthage; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 9100904E1E58B4470099E00E /* VideoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9100904D1E58B4470099E00E /* VideoView.xib */; }; + 910090501E58B5450099E00E /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100904F1E58B5450099E00E /* VideoView.swift */; }; + 91192F741D598E4600F92D78 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91192F731D598E4600F92D78 /* Message.swift */; }; + 9138B4D01E655728006A76FB /* BuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9138B4CF1E655728006A76FB /* BuildInfo.swift */; }; + 9139343A1DD9D9A2002F3F6A /* EventHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913934391DD9D9A2002F3F6A /* EventHandlers.swift */; }; + 913C80651E8D00C200D83864 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913C80641E8D00C200D83864 /* Extensions.swift */; }; + 91577A031D85CB1700A5AF9F /* MediaConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91577A021D85CB1700A5AF9F /* MediaConnection.swift */; }; + 91705B801DED66D300D79306 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 918201901D58668E00178E2B /* WebRTC.framework */; }; + 91715A7B1D5DB3B50004C995 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 918201901D58668E00178E2B /* WebRTC.framework */; }; + 91715A851D5DF2570004C995 /* WebRTC.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 918201901D58668E00178E2B /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 91715A861D5DF25B0004C995 /* SocketRocket.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 918201911D58668E00178E2B /* SocketRocket.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 918201941D58668E00178E2B /* SocketRocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 918201911D58668E00178E2B /* SocketRocket.framework */; }; + 918A6DF71DA4DDC800028E3E /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 918A6DF61DA4DDC800028E3E /* Unbox.framework */; }; + 91A2FD551E25421B0081ADF9 /* PeerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91A2FD541E25421B0081ADF9 /* PeerConnection.swift */; }; + 91B1D6461D75E11F00112A4E /* VideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B1D6451D75E11F00112A4E /* VideoRenderer.swift */; }; + 91C109271E4A3199009F11F7 /* ConnectionController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 91C109201E4A3198009F11F7 /* ConnectionController.storyboard */; }; + 91C109281E4A3199009F11F7 /* AudioCodecViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C109211E4A3199009F11F7 /* AudioCodecViewController.swift */; }; + 91C109291E4A3199009F11F7 /* ConnectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C109221E4A3199009F11F7 /* ConnectionViewController.swift */; }; + 91C1092A1E4A3199009F11F7 /* ConnectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C109231E4A3199009F11F7 /* ConnectionController.swift */; }; + 91C1092B1E4A3199009F11F7 /* RoleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C109241E4A3199009F11F7 /* RoleViewController.swift */; }; + 91C1092C1E4A3199009F11F7 /* VideoCodecViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C109251E4A3199009F11F7 /* VideoCodecViewController.swift */; }; + 91C1092D1E4A3199009F11F7 /* ConnectionNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C109261E4A3199009F11F7 /* ConnectionNavigationController.swift */; }; + 91C4655E1D6376AC00AD9C28 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C4655D1D6376AC00AD9C28 /* JSON.swift */; }; + 91C7B08D1D54636A006F5FA2 /* Sora.h in Headers */ = {isa = PBXBuildFile; fileRef = 91C7B08C1D54636A006F5FA2 /* Sora.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 91C7B0941D54636A006F5FA2 /* Sora.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91C7B0891D54636A006F5FA2 /* Sora.framework */; }; + 91C7B0991D54636A006F5FA2 /* SoraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C7B0981D54636A006F5FA2 /* SoraTests.swift */; }; + 91DB5E9E1D6F43A5007744BF /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91DB5E9D1D6F43A5007744BF /* Connection.swift */; }; + 91DD141E1DC872F1005881C2 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91DD141D1DC872F1005881C2 /* Event.swift */; }; + 91E098841D799389004CF024 /* MediaStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91E098831D799389004CF024 /* MediaStream.swift */; }; + 91F82F751DF04BA600F8D923 /* MediaOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F82F741DF04BA600F8D923 /* MediaOption.swift */; }; + 91FA6F211D93CA9800D38DB4 /* VideoFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FA6F201D93CA9800D38DB4 /* VideoFrame.swift */; }; + 91FD95751DCA06F700047BA9 /* RTCExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FD95741DCA06F700047BA9 /* RTCExtensions.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 91C7B0951D54636A006F5FA2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91C7B0801D54636A006F5FA2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 91C7B0881D54636A006F5FA2; + remoteInfo = Sora; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 91715A831D5DF22F0004C995 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 91715A861D5DF25B0004C995 /* SocketRocket.framework in CopyFiles */, + 91715A851D5DF2570004C995 /* WebRTC.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 9100904D1E58B4470099E00E /* VideoView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VideoView.xib; sourceTree = ""; }; + 9100904F1E58B5450099E00E /* VideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; }; + 91192F731D598E4600F92D78 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 9138B4CF1E655728006A76FB /* BuildInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildInfo.swift; sourceTree = ""; }; + 913934391DD9D9A2002F3F6A /* EventHandlers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventHandlers.swift; sourceTree = ""; }; + 913C80641E8D00C200D83864 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 91577A021D85CB1700A5AF9F /* MediaConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaConnection.swift; sourceTree = ""; }; + 918201901D58668E00178E2B /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = Carthage/Build/iOS/WebRTC.framework; sourceTree = ""; }; + 918201911D58668E00178E2B /* SocketRocket.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SocketRocket.framework; path = Carthage/Build/iOS/SocketRocket.framework; sourceTree = ""; }; + 918A6DF61DA4DDC800028E3E /* Unbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Unbox.framework; path = Carthage/Build/iOS/Unbox.framework; sourceTree = ""; }; + 91A2FD541E25421B0081ADF9 /* PeerConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerConnection.swift; sourceTree = ""; }; + 91B1D6451D75E11F00112A4E /* VideoRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoRenderer.swift; sourceTree = ""; }; + 91C109201E4A3198009F11F7 /* ConnectionController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ConnectionController.storyboard; sourceTree = ""; }; + 91C109211E4A3199009F11F7 /* AudioCodecViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioCodecViewController.swift; sourceTree = ""; }; + 91C109221E4A3199009F11F7 /* ConnectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionViewController.swift; sourceTree = ""; }; + 91C109231E4A3199009F11F7 /* ConnectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionController.swift; sourceTree = ""; }; + 91C109241E4A3199009F11F7 /* RoleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoleViewController.swift; sourceTree = ""; }; + 91C109251E4A3199009F11F7 /* VideoCodecViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCodecViewController.swift; sourceTree = ""; }; + 91C109261E4A3199009F11F7 /* ConnectionNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionNavigationController.swift; sourceTree = ""; }; + 91C4655D1D6376AC00AD9C28 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; + 91C7B0891D54636A006F5FA2 /* Sora.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sora.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 91C7B08C1D54636A006F5FA2 /* Sora.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Sora.h; sourceTree = ""; }; + 91C7B08E1D54636A006F5FA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 91C7B0931D54636A006F5FA2 /* SoraTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SoraTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 91C7B0981D54636A006F5FA2 /* SoraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraTests.swift; sourceTree = ""; }; + 91C7B09A1D54636A006F5FA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 91C7B0A81D5463EA006F5FA2 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; + 91DB5E9D1D6F43A5007744BF /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; + 91DD141D1DC872F1005881C2 /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 91E098831D799389004CF024 /* MediaStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaStream.swift; sourceTree = ""; }; + 91F82F741DF04BA600F8D923 /* MediaOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaOption.swift; sourceTree = ""; }; + 91FA6F201D93CA9800D38DB4 /* VideoFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFrame.swift; sourceTree = ""; }; + 91FD95741DCA06F700047BA9 /* RTCExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTCExtensions.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 91C7B0851D54636A006F5FA2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 918201941D58668E00178E2B /* SocketRocket.framework in Frameworks */, + 918A6DF71DA4DDC800028E3E /* Unbox.framework in Frameworks */, + 91705B801DED66D300D79306 /* WebRTC.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 91C7B0901D54636A006F5FA2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 91C7B0941D54636A006F5FA2 /* Sora.framework in Frameworks */, + 91715A7B1D5DB3B50004C995 /* WebRTC.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9182018E1D58667600178E2B /* Frameworks */ = { + isa = PBXGroup; + children = ( + 918A6DF61DA4DDC800028E3E /* Unbox.framework */, + 918201901D58668E00178E2B /* WebRTC.framework */, + 918201911D58668E00178E2B /* SocketRocket.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 91C1091F1E4A3036009F11F7 /* ConnectionController */ = { + isa = PBXGroup; + children = ( + 91C109201E4A3198009F11F7 /* ConnectionController.storyboard */, + 91C109211E4A3199009F11F7 /* AudioCodecViewController.swift */, + 91C109231E4A3199009F11F7 /* ConnectionController.swift */, + 91C109261E4A3199009F11F7 /* ConnectionNavigationController.swift */, + 91C109221E4A3199009F11F7 /* ConnectionViewController.swift */, + 91C109241E4A3199009F11F7 /* RoleViewController.swift */, + 91C109251E4A3199009F11F7 /* VideoCodecViewController.swift */, + ); + path = ConnectionController; + sourceTree = ""; + }; + 91C7B07F1D54636A006F5FA2 = { + isa = PBXGroup; + children = ( + 91C7B08B1D54636A006F5FA2 /* Sora */, + 91C7B0A31D5463A4006F5FA2 /* Misc */, + 91C7B0971D54636A006F5FA2 /* SoraTests */, + 9182018E1D58667600178E2B /* Frameworks */, + 91C7B08A1D54636A006F5FA2 /* Products */, + ); + sourceTree = ""; + }; + 91C7B08A1D54636A006F5FA2 /* Products */ = { + isa = PBXGroup; + children = ( + 91C7B0891D54636A006F5FA2 /* Sora.framework */, + 91C7B0931D54636A006F5FA2 /* SoraTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 91C7B08B1D54636A006F5FA2 /* Sora */ = { + isa = PBXGroup; + children = ( + 91C7B08C1D54636A006F5FA2 /* Sora.h */, + 91C7B08E1D54636A006F5FA2 /* Info.plist */, + 9138B4CF1E655728006A76FB /* BuildInfo.swift */, + 91DB5E9D1D6F43A5007744BF /* Connection.swift */, + 91DD141D1DC872F1005881C2 /* Event.swift */, + 913934391DD9D9A2002F3F6A /* EventHandlers.swift */, + 913C80641E8D00C200D83864 /* Extensions.swift */, + 91C4655D1D6376AC00AD9C28 /* JSON.swift */, + 91577A021D85CB1700A5AF9F /* MediaConnection.swift */, + 91F82F741DF04BA600F8D923 /* MediaOption.swift */, + 91E098831D799389004CF024 /* MediaStream.swift */, + 91192F731D598E4600F92D78 /* Message.swift */, + 91A2FD541E25421B0081ADF9 /* PeerConnection.swift */, + 91FD95741DCA06F700047BA9 /* RTCExtensions.swift */, + 91FA6F201D93CA9800D38DB4 /* VideoFrame.swift */, + 91B1D6451D75E11F00112A4E /* VideoRenderer.swift */, + 9100904F1E58B5450099E00E /* VideoView.swift */, + 9100904D1E58B4470099E00E /* VideoView.xib */, + 91C1091F1E4A3036009F11F7 /* ConnectionController */, + ); + path = Sora; + sourceTree = ""; + }; + 91C7B0971D54636A006F5FA2 /* SoraTests */ = { + isa = PBXGroup; + children = ( + 91C7B0981D54636A006F5FA2 /* SoraTests.swift */, + 91C7B09A1D54636A006F5FA2 /* Info.plist */, + ); + path = SoraTests; + sourceTree = ""; + }; + 91C7B0A31D5463A4006F5FA2 /* Misc */ = { + isa = PBXGroup; + children = ( + 91C7B0A81D5463EA006F5FA2 /* Cartfile */, + ); + name = Misc; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 91C7B0861D54636A006F5FA2 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 91C7B08D1D54636A006F5FA2 /* Sora.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 91C7B0881D54636A006F5FA2 /* Sora */ = { + isa = PBXNativeTarget; + buildConfigurationList = 91C7B09D1D54636A006F5FA2 /* Build configuration list for PBXNativeTarget "Sora" */; + buildPhases = ( + 91C7B0841D54636A006F5FA2 /* Sources */, + 91C7B0851D54636A006F5FA2 /* Frameworks */, + 91C7B0861D54636A006F5FA2 /* Headers */, + 91C7B0871D54636A006F5FA2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sora; + productName = Sora; + productReference = 91C7B0891D54636A006F5FA2 /* Sora.framework */; + productType = "com.apple.product-type.framework"; + }; + 91C7B0921D54636A006F5FA2 /* SoraTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 91C7B0A01D54636A006F5FA2 /* Build configuration list for PBXNativeTarget "SoraTests" */; + buildPhases = ( + 91C7B08F1D54636A006F5FA2 /* Sources */, + 91C7B0901D54636A006F5FA2 /* Frameworks */, + 91C7B0911D54636A006F5FA2 /* Resources */, + 91715A831D5DF22F0004C995 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + 91C7B0961D54636A006F5FA2 /* PBXTargetDependency */, + ); + name = SoraTests; + productName = SoraTests; + productReference = 91C7B0931D54636A006F5FA2 /* SoraTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 91C7B0801D54636A006F5FA2 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0730; + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = "Shiguredo Inc."; + TargetAttributes = { + 918201871D585E1A00178E2B = { + CreatedOnToolsVersion = 7.3.1; + }; + 91C7B0881D54636A006F5FA2 = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = DQ232CBKHS; + LastSwiftMigration = 0800; + }; + 91C7B0921D54636A006F5FA2 = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = DQ232CBKHS; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = 91C7B0831D54636A006F5FA2 /* Build configuration list for PBXProject "Sora" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 91C7B07F1D54636A006F5FA2; + productRefGroup = 91C7B08A1D54636A006F5FA2 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 91C7B0881D54636A006F5FA2 /* Sora */, + 91C7B0921D54636A006F5FA2 /* SoraTests */, + 918201871D585E1A00178E2B /* Carthage */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 91C7B0871D54636A006F5FA2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9100904E1E58B4470099E00E /* VideoView.xib in Resources */, + 91C109271E4A3199009F11F7 /* ConnectionController.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 91C7B0911D54636A006F5FA2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9182018B1D585E2500178E2B /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Carthage/Build/iOS/WebRTC.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd $SRCROOT\ncarthage update --platform iOS"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 91C7B0841D54636A006F5FA2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 91577A031D85CB1700A5AF9F /* MediaConnection.swift in Sources */, + 91DD141E1DC872F1005881C2 /* Event.swift in Sources */, + 91E098841D799389004CF024 /* MediaStream.swift in Sources */, + 9139343A1DD9D9A2002F3F6A /* EventHandlers.swift in Sources */, + 9138B4D01E655728006A76FB /* BuildInfo.swift in Sources */, + 91B1D6461D75E11F00112A4E /* VideoRenderer.swift in Sources */, + 91C4655E1D6376AC00AD9C28 /* JSON.swift in Sources */, + 91C1092B1E4A3199009F11F7 /* RoleViewController.swift in Sources */, + 91C109291E4A3199009F11F7 /* ConnectionViewController.swift in Sources */, + 91FD95751DCA06F700047BA9 /* RTCExtensions.swift in Sources */, + 91192F741D598E4600F92D78 /* Message.swift in Sources */, + 91DB5E9E1D6F43A5007744BF /* Connection.swift in Sources */, + 91C1092A1E4A3199009F11F7 /* ConnectionController.swift in Sources */, + 913C80651E8D00C200D83864 /* Extensions.swift in Sources */, + 91F82F751DF04BA600F8D923 /* MediaOption.swift in Sources */, + 910090501E58B5450099E00E /* VideoView.swift in Sources */, + 91C1092D1E4A3199009F11F7 /* ConnectionNavigationController.swift in Sources */, + 91FA6F211D93CA9800D38DB4 /* VideoFrame.swift in Sources */, + 91C1092C1E4A3199009F11F7 /* VideoCodecViewController.swift in Sources */, + 91A2FD551E25421B0081ADF9 /* PeerConnection.swift in Sources */, + 91C109281E4A3199009F11F7 /* AudioCodecViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 91C7B08F1D54636A006F5FA2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 91C7B0991D54636A006F5FA2 /* SoraTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 91C7B0961D54636A006F5FA2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 91C7B0881D54636A006F5FA2 /* Sora */; + targetProxy = 91C7B0951D54636A006F5FA2 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 918201881D585E1B00178E2B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_BITCODE = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 918201891D585E1B00178E2B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_BITCODE = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 91C7B09B1D54636A006F5FA2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PROVISIONING_PROFILE = "49e345ee-1e24-4cef-9228-9df5fc1f7525"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = arm64; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 91C7B09C1D54636A006F5FA2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = NO; + PROVISIONING_PROFILE = "49e345ee-1e24-4cef-9228-9df5fc1f7525"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = arm64; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 91C7B09E1D54636A006F5FA2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = DQ232CBKHS; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + GCC_OPTIMIZATION_LEVEL = 0; + INFOPLIST_FILE = Sora/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = jp.shiguredo.Sora; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + VALID_ARCHS = arm64; + }; + name = Debug; + }; + 91C7B09F1D54636A006F5FA2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = DQ232CBKHS; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + GCC_OPTIMIZATION_LEVEL = 0; + INFOPLIST_FILE = Sora/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = jp.shiguredo.Sora; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + VALID_ARCHS = arm64; + }; + name = Release; + }; + 91C7B0A11D54636A006F5FA2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = DQ232CBKHS; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = SoraTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = jp.shiguredo.SoraTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 91C7B0A21D54636A006F5FA2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = DQ232CBKHS; + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = SoraTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = jp.shiguredo.SoraTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9182018A1D585E1B00178E2B /* Build configuration list for PBXAggregateTarget "Carthage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 918201881D585E1B00178E2B /* Debug */, + 918201891D585E1B00178E2B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 91C7B0831D54636A006F5FA2 /* Build configuration list for PBXProject "Sora" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 91C7B09B1D54636A006F5FA2 /* Debug */, + 91C7B09C1D54636A006F5FA2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 91C7B09D1D54636A006F5FA2 /* Build configuration list for PBXNativeTarget "Sora" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 91C7B09E1D54636A006F5FA2 /* Debug */, + 91C7B09F1D54636A006F5FA2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 91C7B0A01D54636A006F5FA2 /* Build configuration list for PBXNativeTarget "SoraTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 91C7B0A11D54636A006F5FA2 /* Debug */, + 91C7B0A21D54636A006F5FA2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 91C7B0801D54636A006F5FA2 /* Project object */; +} diff --git a/Sora/Sora.xcodeproj/xcshareddata/xcschemes/Sora.xcscheme b/Sora/Sora.xcodeproj/xcshareddata/xcschemes/Sora.xcscheme new file mode 100644 index 00000000..c72430bc --- /dev/null +++ b/Sora/Sora.xcodeproj/xcshareddata/xcschemes/Sora.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sora/Sora/BuildInfo.swift b/Sora/Sora/BuildInfo.swift new file mode 100644 index 00000000..a7ca2f39 --- /dev/null +++ b/Sora/Sora/BuildInfo.swift @@ -0,0 +1,72 @@ +import Foundation +import Unbox +import WebRTC + +public class BuildInfo { + + public static var WebRTCVersion: String? { + get { return shared?.WebRTCVersion } + } + + public static var WebRTCRevision: String? { + get { return shared?.WebRTCRevision } + } + + public static var WebRTCShortRevision: String? { + get { return shared?.WebRTCShortRevision } + } + + public static var VP9Enabled: Bool? { + get { return shared?.VP9Enabled } + } + + static var shared: BuildInfo? = BuildInfo() + + var WebRTCVersion: String + var WebRTCRevision: String + var WebRTCShortRevision: String + var VP9Enabled: Bool + + init?() { + let bundle = Bundle(for: RTCPeerConnection.self) + guard let url = bundle.url(forResource: "build_info", + withExtension: "json") else + { + print("Sora: failed to load 'build_info.json'") + return nil + } + + do { + let data = try Data(contentsOf: url) + let file: BuildInfoFile = try unbox(data: data) + WebRTCVersion = file.WebRTCVersion + WebRTCRevision = file.WebRTCRevision + WebRTCShortRevision = WebRTCRevision.substring(to: + WebRTCRevision.index(WebRTCRevision.startIndex, + offsetBy: 7)) + VP9Enabled = file.VP9Enabled + } catch { + print("Sora: failed to parse 'build_info.json'") + return nil + } + } + +} + +struct BuildInfoFile { + + var WebRTCVersion: String + var WebRTCRevision: String + var VP9Enabled: Bool + +} + +extension BuildInfoFile: Unboxable { + + init(unboxer: Unboxer) throws { + WebRTCVersion = try unboxer.unbox(key: "webrtc_version") + WebRTCRevision = try unboxer.unbox(key: "webrtc_revision") + VP9Enabled = try unboxer.unbox(key: "vp9") + } + +} diff --git a/Sora/Sora/Connection.swift b/Sora/Sora/Connection.swift new file mode 100644 index 00000000..8415336f --- /dev/null +++ b/Sora/Sora/Connection.swift @@ -0,0 +1,72 @@ +import Foundation +import WebRTC +import SocketRocket +import UIKit + +public indirect enum ConnectionError: Error { + case failureSetConfiguration(RTCConfiguration) + case connectionWaitTimeout + case connectionDisconnected + case connectionTerminated + case connectionBusy + case webSocketClose(Int, String?) + case webSocketError(Error) + case signalingFailure(reason: String) + case peerConnectionError(Error) + case iceConnectionFailed + case iceConnectionDisconnected + case mediaCapturerFailed + case mediaStreamNotFound + case aggregateError([ConnectionError]) + case updateError(ConnectionError) +} + +public class Connection { + + public struct NotificationKey { + + public enum UserInfo: String { + case connectionError = "Sora.Connection.UserInfo.connectionError" + case mediaConnection = "Sora.Connection.UserInfo.mediaConnection" + } + + public static var onConnect = + Notification.Name("Sora.Connection.Notification.onConnect") + public static var onDisconnect = + Notification.Name("Sora.Connection.Notification.onDisconnect") + public static var onFailure = + Notification.Name("Sora.Connection.Notification.onFailure") + + } + + public var URL: Foundation.URL + public var mediaChannelId: String + public var eventLog: EventLog + public var mediaPublisher: MediaPublisher! + public var mediaSubscriber: MediaSubscriber! + + public var numberOfConnections: (Int, Int) = (0, 0) { + willSet { + if numberOfConnections != newValue { + onChangeNumberOfConnectionsHandler?(newValue.0, newValue.1) + } + } + } + + public init(URL: Foundation.URL, mediaChannelId: String) { + self.URL = URL + self.mediaChannelId = mediaChannelId + eventLog = EventLog(URL: URL, mediaChannelId: mediaChannelId) + mediaPublisher = MediaPublisher(connection: self) + mediaSubscriber = MediaSubscriber(connection: self) + } + + // MARK: イベントハンドラ + + var onChangeNumberOfConnectionsHandler: ((Int, Int) -> ())? + + public func onChangeNumberOfConnections(handler: @escaping (Int, Int) -> ()) { + onChangeNumberOfConnectionsHandler = handler + } + +} diff --git a/Sora/Sora/ConnectionController/AudioCodecViewController.swift b/Sora/Sora/ConnectionController/AudioCodecViewController.swift new file mode 100644 index 00000000..1a64e646 --- /dev/null +++ b/Sora/Sora/ConnectionController/AudioCodecViewController.swift @@ -0,0 +1,74 @@ +import UIKit + +class AudioCodecViewController: UITableViewController { + + @IBOutlet weak var defaultLabel: UILabel! + @IBOutlet weak var OpusLabel: UILabel! + @IBOutlet weak var PCMULabel: UILabel! + + @IBOutlet weak var defaultCell: UITableViewCell! + @IBOutlet weak var OpusCell: UITableViewCell! + @IBOutlet weak var PCMUCell: UITableViewCell! + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + for label: UILabel in [defaultLabel, OpusLabel, PCMULabel] { + label.font = UIFont.preferredFont(forTextStyle: .body) + label.adjustsFontForContentSizeCategory = true + } + } + + override func viewWillAppear(_ animated: Bool) { + selectCodec(codec: ConnectionViewController.main?.audioCodec) + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + + func selectCodec(codec: AudioCodec? = nil) { + defaultCell.accessoryType = .none + OpusCell.accessoryType = .none + PCMUCell.accessoryType = .none + switch codec { + case .default?, nil: + ConnectionViewController.main?.audioCodec = .default + defaultCell?.accessoryType = .checkmark + case .Opus?: + ConnectionViewController.main?.audioCodec = .Opus + OpusCell?.accessoryType = .checkmark + case .PCMU?: + ConnectionViewController.main?.audioCodec = .PCMU + PCMUCell?.accessoryType = .checkmark + } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + switch indexPath.row { + case 0: + selectCodec(codec: .default) + case 1: + selectCodec(codec: .Opus) + case 2: + selectCodec(codec: .PCMU) + default: + break + } + } + +} diff --git a/Sora/Sora/ConnectionController/ConnectionController.storyboard b/Sora/Sora/ConnectionController/ConnectionController.storyboard new file mode 100644 index 00000000..b5cb583d --- /dev/null +++ b/Sora/Sora/ConnectionController/ConnectionController.storyboard @@ -0,0 +1,1011 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sora/Sora/ConnectionController/ConnectionController.swift b/Sora/Sora/ConnectionController/ConnectionController.swift new file mode 100644 index 00000000..e69348e2 --- /dev/null +++ b/Sora/Sora/ConnectionController/ConnectionController.swift @@ -0,0 +1,174 @@ +import UIKit + +public class ConnectionController: UIViewController { + + public enum Role { + case publisher + case subscriber + + static var allRoles: [Role] = [.publisher, .subscriber] + + static func containsAll(_ roles: [Role]) -> Bool { + let allRoles: [Role] = [.publisher, .subscriber] + for role in roles { + if !allRoles.contains(role) { + return false + } + } + return true + } + } + + public enum StreamType { + case single + case multiple + } + + public class Request { + + public var URL: URL + public var channelId: String + public var roles: [Role] + public var multistreamEnabled: Bool + public var videoEnabled: Bool + public var videoCodec: VideoCodec + public var audioEnabled: Bool + public var audioCodec: AudioCodec + + public init(URL: URL, + channelId: String, + roles: [Role], + multistreamEnabled: Bool, + videoEnabled: Bool, + videoCodec: VideoCodec, + audioEnabled: Bool, + audioCodec: AudioCodec) { + self.URL = URL + self.channelId = channelId + self.roles = roles + self.multistreamEnabled = multistreamEnabled + self.videoEnabled = videoEnabled + self.videoCodec = videoCodec + self.audioEnabled = audioEnabled + self.audioCodec = audioCodec + } + + } + + enum UserDefaultsKey: String { + case WebSocketSSLEnabled = "SoraConnectionControllerWebSocketSSLEnabled" + case host = "SoraConnectionControllerHost" + case port = "SoraConnectionControllerPort" + case signalingPath = "SoraConnectionControllerSignalingPath" + case channelId = "SoraConnectionControllerChannelId" + case roles = "SoraConnectionControllerRoles" + case multistreamEnabled = "SoraConnectionControllerMultistreamEnabled" + case videoEnabled = "SoraConnectionControllerVideoEnabled" + case videoCodec = "SoraConnectionControllerVideoCodec" + case audioEnabled = "SoraConnectionControllerAudioEnabled" + case audioCodec = "SoraConnectionControllerAudioCodec" + case autofocusEnabled = "SoraConnectionControllerAutofocusEnabled" + } + + public var connection: Connection? + + var connectionControllerStoryboard: UIStoryboard? + var connectionNavigationController: ConnectionNavigationController! + + public var WebSocketSSLEnabled: Bool = true + public var host: String? + public var port: UInt? + public var signalingPath: String? + public var channelId: String? + public var availableRoles: [Role] = [.publisher, .subscriber] + public var availableStreamTypes: [StreamType] = [.single, .multiple] + + public var userDefaults: UserDefaults? = + UserDefaults(suiteName: "jp.shiguredo.SoraConnectionController") + + var tupleOfAvailableStreamTypes: (Bool, Bool) { + get { + return (availableStreamTypes.contains(.single), + availableStreamTypes.contains(.multiple)) + } + } + + public init(WebSocketSSLEnabled: Bool = true, + host: String? = nil, + port: UInt? = nil, + signalingPath: String? = "signaling", + channelId: String? = nil, + availableRoles: [Role]? = nil, + availableStreamTypes: [StreamType]? = nil) { + super.init(nibName: nil, bundle: nil) + connectionControllerStoryboard = + UIStoryboard(name: "ConnectionController", + bundle: Bundle(for: ConnectionController.self)) + guard let navi = connectionControllerStoryboard? + .instantiateViewController(withIdentifier: "Navigation") + as! ConnectionNavigationController? else { + fatalError("failed loading ConnectionViewController") + } + connectionNavigationController = navi + connectionNavigationController.connectionController = self + + addChildViewController(connectionNavigationController) + view.addSubview(connectionNavigationController.view) + connectionNavigationController.didMove(toParentViewController: self) + + self.WebSocketSSLEnabled = WebSocketSSLEnabled + self.host = host + self.port = port + self.signalingPath = signalingPath + self.channelId = channelId + if let roles = availableRoles { + self.availableRoles = roles + } + if let streamTypes = availableStreamTypes { + self.availableStreamTypes = streamTypes + } + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override public func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + + var onRequestHandler: ((Connection, Request) -> Void)? + var onConnectHandler: ((Connection?, [Role]?, ConnectionError?) -> Void)? + var onCancelHandler: (() -> Void)? + + public func onRequest(handler: @escaping (Connection, Request) -> Void) { + onRequestHandler = handler + } + + public func onConnect(handler: + @escaping (Connection?, [Role]?, ConnectionError?) -> Void) { + onConnectHandler = handler + } + + public func onCancel(handler: @escaping () -> Void) { + onCancelHandler = handler + } + +} diff --git a/Sora/Sora/ConnectionController/ConnectionNavigationController.swift b/Sora/Sora/ConnectionController/ConnectionNavigationController.swift new file mode 100644 index 00000000..aa5982c7 --- /dev/null +++ b/Sora/Sora/ConnectionController/ConnectionNavigationController.swift @@ -0,0 +1,29 @@ +import UIKit + +class ConnectionNavigationController: UINavigationController { + + weak var connectionController: ConnectionController? + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/Sora/Sora/ConnectionController/ConnectionViewController.swift b/Sora/Sora/ConnectionController/ConnectionViewController.swift new file mode 100644 index 00000000..c425c015 --- /dev/null +++ b/Sora/Sora/ConnectionController/ConnectionViewController.swift @@ -0,0 +1,818 @@ +import UIKit + +class ConnectionViewController: UITableViewController { + + enum State { + case connected + case connecting + case disconnected + } + + @IBOutlet weak var cancelButtonItem: UIBarButtonItem! + + @IBOutlet weak var connectionStateCell: UITableViewCell! + @IBOutlet weak var connectionTimeLabel: UILabel! + @IBOutlet weak var enableMicrophoneLabel: UILabel! + @IBOutlet weak var enableWebSocketSSLLabel: UILabel! + @IBOutlet weak var hostLabel: UILabel! + @IBOutlet weak var portLabel: UILabel! + @IBOutlet weak var signalingPathLabel: UILabel! + @IBOutlet weak var channelIdLabel: UILabel! + @IBOutlet weak var roleLabel: UILabel! + @IBOutlet weak var roleCell: UITableViewCell! + @IBOutlet weak var enableMultistreamLabel: UILabel! + @IBOutlet weak var enableVideoLabel: UILabel! + @IBOutlet weak var videoCodecLabel: UILabel! + @IBOutlet weak var videoCodecCell: UITableViewCell! + @IBOutlet weak var enableAudioLabel: UILabel! + @IBOutlet weak var audioCodecLabel: UILabel! + @IBOutlet weak var audioCodecCell: UITableViewCell! + @IBOutlet weak var autofocusLabel: UILabel! + @IBOutlet weak var WebRTCVersionLabel: UILabel! + @IBOutlet weak var WebRTCRevisionLabel: UILabel! + @IBOutlet weak var VP9EnabledLabel: UILabel! + + @IBOutlet weak var connectionTimeValueLabel: UILabel! + @IBOutlet weak var enableMicrophoneSwitch: UISwitch! + @IBOutlet weak var enableWebSocketSSLSwitch: UISwitch! + @IBOutlet weak var hostTextField: UITextField! + @IBOutlet weak var portTextField: UITextField! + @IBOutlet weak var signalingPathTextField: UITextField! + @IBOutlet weak var channelIdTextField: UITextField! + @IBOutlet weak var rollValueLabel: UILabel! + @IBOutlet weak var enableMultistreamSwitch: UISwitch! + @IBOutlet weak var connectButton: UIButton! + @IBOutlet weak var enableVideoSwitch: UISwitch! + @IBOutlet weak var videoCodecValueLabel: UILabel! + @IBOutlet weak var enableAudioSwitch: UISwitch! + @IBOutlet weak var audioCodecValueLabel: UILabel! + @IBOutlet weak var autofocusSwitch: UISwitch! + @IBOutlet weak var WebRTCVersionValueLabel: UILabel! + @IBOutlet weak var WebRTCRevisionValueLabel: UILabel! + @IBOutlet weak var VP9EnabledValueLabel: UILabel! + + @IBOutlet weak var tapGestureRecognizer: UITapGestureRecognizer! + + weak var touchedField: UITextField? + + static var main: ConnectionViewController? + + var indicator: UIActivityIndicatorView? + + var state: State = .disconnected { + didSet { + DispatchQueue.main.async { + switch self.state { + case .connected: + self.cancelButtonItem.title = "Back" + self.connectButton.setTitle("Disconnect", for: .normal) + self.connectButton.isEnabled = true + self.connectionStateCell.accessoryView = nil + self.connectionStateCell.accessoryType = .checkmark + self.indicator?.stopAnimating() + self.enableControls(false) + self.connectionTimeLabel.textColor = nil + + case .disconnected: + self.cancelButtonItem.title = "Cancel" + self.connectButton.setTitle("Connect", for: .normal) + self.connectButton.isEnabled = true + self.connectionStateCell.accessoryView = nil + self.connectionStateCell.accessoryType = .none + self.indicator?.stopAnimating() + self.connectionTimeValueLabel.text = nil + self.enableControls(true) + self.connectionTimeLabel.textColor = UIColor.lightGray + + case .connecting: + self.connectButton.titleLabel!.text = "Connecting..." + self.connectButton.setTitle("Connecting...", for: .normal) + self.connectButton.isEnabled = false + self.indicator?.startAnimating() + self.connectionStateCell.accessoryView = self.indicator + self.connectionStateCell.accessoryType = .none + self.connectionTimeValueLabel.text = nil + self.enableControls(false) + self.connectionTimeLabel.textColor = UIColor.lightGray + } + } + } + } + + var roles: [ConnectionController.Role] = [] { + didSet { + var s: [String] = [] + if roles.contains(.publisher) { + s.append("Publisher") + } + if roles.contains(.subscriber) { + s.append("Subscriber") + } + rollValueLabel.text = s.joined(separator: ",") + } + } + + var multistreamEnabled: Bool { + get { return enableMultistreamSwitch.isOn } + } + + var videoEnabled: Bool { + get { return enableVideoSwitch.isOn } + } + + var videoCodec: VideoCodec? { + didSet { + switch videoCodec { + case .default?, nil: + videoCodecValueLabel.text = "Default" + case .VP8?: + videoCodecValueLabel.text = "VP8" + case .VP9?: + videoCodecValueLabel.text = "VP9" + case .H264?: + videoCodecValueLabel.text = "H.264" + } + } + } + + var audioEnabled: Bool { + get { return enableAudioSwitch.isOn } + } + + var audioCodec: AudioCodec? { + didSet { + switch audioCodec { + case .default?, nil: + audioCodecValueLabel.text = "Default" + case .Opus?: + audioCodecValueLabel.text = "Opus" + case .PCMU?: + audioCodecValueLabel.text = "PCMU" + } + } + } + + var connectionController: ConnectionController? { + get { + return (navigationController as! ConnectionNavigationController?)? + .connectionController + } + } + + var connection: Connection? + + // MARK: - View Controller + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + ConnectionViewController.main = self + indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + tapGestureRecognizer.cancelsTouchesInView = false + + for label: UILabel in [connectionTimeLabel, + connectionTimeValueLabel, + enableMicrophoneLabel, + enableWebSocketSSLLabel, + hostLabel, portLabel, signalingPathLabel, + channelIdLabel, + roleLabel, rollValueLabel, + enableMultistreamLabel, + connectButton.titleLabel!, + enableVideoLabel, videoCodecLabel, + videoCodecValueLabel, + enableAudioLabel, audioCodecLabel, + audioCodecValueLabel, autofocusLabel, + WebRTCVersionLabel, WebRTCVersionValueLabel, + WebRTCRevisionLabel, WebRTCRevisionValueLabel, + VP9EnabledLabel, VP9EnabledValueLabel] + { + label.font = UIFont.preferredFont(forTextStyle: .body) + label.adjustsFontForContentSizeCategory = true + } + for field: UITextField in [hostTextField, + portTextField, + signalingPathTextField, + channelIdTextField] { + field.font = UIFont.preferredFont(forTextStyle: .body) + field.adjustsFontForContentSizeCategory = true + } + + NotificationCenter.default + .addObserver(self, + selector: #selector(applicationDidEnterBackground(_:)), + name: NSNotification.Name.UIApplicationDidEnterBackground, + object: nil) + + state = .disconnected + roles = [.publisher, .subscriber] + videoCodec = .default + audioCodec = .default + enableLabel(enableMicrophoneLabel, isEnabled: false) + enableMicrophoneSwitch.setOn(false, animated: false) + autofocusSwitch.setOn(false, animated: false) + connectionTimeValueLabel.text = nil + hostTextField.text = connectionController?.host + hostTextField.placeholder = "ex) www.example.com" + portTextField.text = connectionController?.port?.description + portTextField.placeholder = "ex) 5000" + signalingPathTextField.text = connectionController?.signalingPath + signalingPathTextField.placeholder = "ex) signaling" + channelIdTextField.text = connectionController?.channelId + channelIdTextField.placeholder = "your channel ID" + + loadSettings() + + switch connectionController!.tupleOfAvailableStreamTypes { + case (true, true), (false, false): + break + case (true, false): + enableMultistreamLabel.textColor = UIColor.lightGray + enableMultistreamSwitch.isOn = false + enableMultistreamSwitch.isEnabled = false + case (false, true): + enableMultistreamLabel.textColor = UIColor.lightGray + enableMultistreamSwitch.isOn = true + enableMultistreamSwitch.isEnabled = false + } + + // build info + if let version = BuildInfo.WebRTCVersion { + WebRTCVersionValueLabel.text = version + } else { + WebRTCVersionValueLabel.text = "Unknown" + } + if let revision = BuildInfo.WebRTCShortRevision { + WebRTCRevisionValueLabel.text = revision + } else { + WebRTCRevisionValueLabel.text = "Unknown" + } + if let VP9 = BuildInfo.VP9Enabled { + VP9EnabledValueLabel.text = VP9 ? "Enabled" : "Disabled" + } else { + VP9EnabledValueLabel.text = "Unknown" + } + } + + func applicationDidEnterBackground(_ notification: Notification) { + saveSettings() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + saveSettings() + } + + override open func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func addRole(_ role: ConnectionController.Role) { + if !roles.contains(role) { + roles.append(role) + } + } + + func removeRole(_ role: ConnectionController.Role) { + roles = roles.filter { + each in return each != role + } + } + + // MARK: 設定の保存 + + func loadSettings() { + guard let defaults = connectionController!.userDefaults else { + return + } + + loadSwitchValue(userDefaults: defaults, + switch: enableWebSocketSSLSwitch, + key: .WebSocketSSLEnabled, + value: true) + loadTextFieldValue(userDefaults: defaults, + textField: hostTextField, + forKey: .host) + loadTextFieldValue(userDefaults: defaults, + textField: portTextField, + forKey: .port) + loadTextFieldValue(userDefaults: defaults, + textField: signalingPathTextField, + forKey: .signalingPath) + loadTextFieldValue(userDefaults: defaults, + textField: channelIdTextField, + forKey: .channelId) + + roles = [.publisher] + if let roleValue = defaults.string(forKey: + ConnectionController.UserDefaultsKey.roles.rawValue) { + roles = [] + if roleValue.contains("p") { + roles.append(.publisher) + } + if roleValue.contains("s") { + roles.append(.subscriber) + } + } + + loadSwitchValue(userDefaults: defaults, + switch: enableMultistreamSwitch, + key: .multistreamEnabled, + value: false) + loadSwitchValue(userDefaults: defaults, + switch: enableVideoSwitch, + key: .videoEnabled, + value: true) + loadSwitchValue(userDefaults: defaults, + switch: enableAudioSwitch, + key: .audioEnabled, + value: true) + loadSwitchValue(userDefaults: defaults, + switch: autofocusSwitch, + key: .autofocusEnabled, + value: false) + + switch defaults.string(forKey: + ConnectionController.UserDefaultsKey.videoCodec.rawValue) { + case "VP8"?: + videoCodec = .VP8 + case "VP9"?: + videoCodec = .VP9 + case "H.264"?: + videoCodec = .H264 + default: + videoCodec = nil + } + + switch defaults.string(forKey: + ConnectionController.UserDefaultsKey.audioCodec.rawValue) { + case "Opus"?: + audioCodec = .Opus + case "VP9"?: + audioCodec = .PCMU + default: + audioCodec = nil + } + } + + func loadSwitchValue(userDefaults: UserDefaults, + switch: UISwitch!, + key: ConnectionController.UserDefaultsKey, + value: Bool) { + let defaults = UserDefaults.standard + if let _ = defaults.object(forKey: key.rawValue) { + `switch`.setOn(defaults.bool(forKey: key.rawValue), animated: false) + } else { + `switch`.setOn(value, animated: false) + } + } + + func loadTextFieldValue(userDefaults: UserDefaults, + textField: UITextField, + forKey key: ConnectionController.UserDefaultsKey) { + if let text = userDefaults.string(forKey: key.rawValue) { + if !text.isEmpty { + textField.text = text + } + } + } + + func saveSettings() { + guard let defaults = connectionController!.userDefaults else { + return + } + + defaults.set(enableWebSocketSSLSwitch.isOn, + forKey: + ConnectionController.UserDefaultsKey.WebSocketSSLEnabled.rawValue) + saveTextField(userDefaults: defaults, + textField: hostTextField, + forKey: .host) + saveTextField(userDefaults: defaults, + textField: portTextField, + forKey: .port) + saveTextField(userDefaults: defaults, + textField: signalingPathTextField, + forKey: .signalingPath) + saveTextField(userDefaults: defaults, + textField: channelIdTextField, + forKey: .channelId) + + var roleValue = "" + if roles.contains(.publisher) { + roleValue.append("p") + } + if roles.contains(.subscriber) { + roleValue.append("s") + } + if roleValue.isEmpty { + roleValue = "ps" + } + + defaults.set(roleValue, + forKey: + ConnectionController.UserDefaultsKey.roles.rawValue) + defaults.set(multistreamEnabled, + forKey: + ConnectionController.UserDefaultsKey.multistreamEnabled.rawValue) + defaults.set(videoEnabled, + forKey: + ConnectionController.UserDefaultsKey.videoEnabled.rawValue) + + var videoCodecValue: String? + switch videoCodec { + case .VP8?: + videoCodecValue = "VP8" + case .VP9?: + videoCodecValue = "VP9" + case .H264?: + videoCodecValue = "H.264" + default: + videoCodecValue = nil + } + defaults.set(videoCodecValue, + forKey: + ConnectionController.UserDefaultsKey.videoCodec.rawValue) + + defaults.set(audioEnabled, + forKey: + ConnectionController.UserDefaultsKey.audioEnabled.rawValue) + + var audioCodecValue: String? + switch audioCodec { + case .Opus?: + audioCodecValue = "Opus" + case .PCMU?: + audioCodecValue = "PCMU" + default: + audioCodecValue = nil + } + defaults.set(audioCodecValue, + forKey: + ConnectionController.UserDefaultsKey.audioCodec.rawValue) + + defaults.set(autofocusSwitch.isOn, + forKey: + ConnectionController.UserDefaultsKey.autofocusEnabled.rawValue) + + defaults.synchronize() + } + + func saveTextField(userDefaults: UserDefaults, + textField: UITextField, + forKey key: ConnectionController.UserDefaultsKey) { + if let text = textField.text { + userDefaults.set(text, forKey: key.rawValue) + } + } + + // MARK: アクション + + func enableLabel(_ label: UILabel, isEnabled: Bool) { + label.textColor = isEnabled ? nil : UIColor.lightGray + } + + func enableControls(_ isEnabled: Bool) { + let labels: [UILabel] = [ + enableWebSocketSSLLabel, hostLabel, portLabel, + signalingPathLabel, channelIdLabel, roleLabel, + enableVideoLabel, videoCodecLabel, + enableAudioLabel, audioCodecLabel, + ] + for label in labels { + enableLabel(label, isEnabled: isEnabled) + } + + switch connectionController!.tupleOfAvailableStreamTypes { + case (true, false), (false, true): + enableMultistreamLabel.textColor = UIColor.lightGray + default: + enableMultistreamLabel.textColor = nil + } + + let fields: [UITextField] = [hostTextField, + portTextField, + signalingPathTextField, + channelIdTextField] + for field in fields { + if isEnabled { + field.textColor = nil + } else { + field.textColor = UIColor.lightGray + } + } + + let controls: [UIView] = [ + enableWebSocketSSLSwitch, hostTextField, portTextField, + signalingPathTextField, channelIdTextField, roleCell, + enableMultistreamSwitch, enableVideoSwitch, enableAudioSwitch, + videoCodecCell, audioCodecCell] + for control: UIView in controls { + control.isUserInteractionEnabled = isEnabled + } + } + + @IBAction func cancel(_ sender: AnyObject) { + back(isCancel: true) + } + + func back(isCancel: Bool) { + saveSettings() + dismiss(animated: true) { + if isCancel { + self.connectionController!.onCancelHandler?() + } + } + } + + var connectingAlertController: UIAlertController! + + @IBAction func connectOrDisconnect(_ sender: AnyObject) { + saveSettings() + + switch state { + case .connecting: + assertionFailure("invalid state") + + case .connected: + disconnect() + + case .disconnected: + guard let host = hostTextField.nonEmptyText() else { + presentSimpleAlert(title: "Error", + message: "Input host URL") + return + } + + var port: UInt? + if let text = portTextField.nonEmptyText() { + if let num = UInt(text) { + port = num + } else { + presentSimpleAlert(title: "Error", + message: "Invalid port number") + return + } + } + + let signalingPath = signalingPathTextField.nonEmptyText() ?? + signalingPathTextField.placeholder! + + guard let channelId = channelIdTextField.nonEmptyText() else { + presentSimpleAlert(title: "Error", + message: "Input channel ID") + return + } + + var portStr: String = "" + if let port = port { + portStr = String(format: ":%d", port) + } + let URLString = String(format: "%@://%@%@/%@", + enableWebSocketSSLSwitch.isOn ? "wss" : "ws", + host, portStr, signalingPath) + + guard let URL = URL(string: URLString) else { + presentSimpleAlert(title: "Error", + message: "Invalid server URL") + return + } + + if roles.isEmpty { + presentSimpleAlert(title: "Error", + message: "Select roles") + return + } + + connection = Connection(URL: URL, mediaChannelId: channelId) + let request = ConnectionController + .Request(URL: URL, + channelId: channelId, + roles: roles, + multistreamEnabled: multistreamEnabled, + videoEnabled: videoEnabled, + videoCodec: videoCodec ?? .default, + audioEnabled: audioEnabled, + audioCodec: audioCodec ?? .default) + connectionController?.onRequestHandler?(connection!, request) + + connectingAlertController = UIAlertController( + title: nil, + message: "Connecting to the server...", + preferredStyle: .alert) + let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + indicator.center = CGPoint(x: 25, y: 30) + connectingAlertController.view.addSubview(indicator) + connectingAlertController.addAction( + UIAlertAction(title: "Cancel", style: .cancel) + { + _ in + self.disconnect() + } + ) + DispatchQueue.main.async { + indicator.startAnimating() + self.present(self.connectingAlertController, animated: true) {} + } + + state = .connecting + if roles.contains(.publisher) { + self.connectPublisher() + } else if roles.contains(.subscriber) { + self.connectSubscriber() + } else { + assertionFailure("roles must not be empty") + } + } + } + + func connectPublisher() { + setMediaConnectionSettings(connection!.mediaPublisher) + connection!.mediaPublisher.connect { + error in + DispatchQueue.main.async { + if let error = error { + self.failConnection(error: error) + return + } + + self.enableLabel(self.enableMicrophoneLabel, isEnabled: true) + self.enableMicrophoneSwitch.isEnabled = true + self.enableMicrophoneSwitch.setOn(true, animated: true) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.connectionOnDisconnect(_:)), + name: MediaConnection.NotificationKey.onDisconnect, + object: self.connection!.mediaPublisher) + + if self.roles.contains(.subscriber) { + self.connectSubscriber() + } else { + self.finishConnection(self.connection!.mediaPublisher) + } + } + } + } + + func connectionOnDisconnect(_ notification: Notification) { + enableLabel(enableMicrophoneLabel, isEnabled: false) + enableMicrophoneSwitch.isEnabled = false + enableMicrophoneSwitch.isUserInteractionEnabled = true + enableMicrophoneSwitch.setOn(false, animated: true) + } + + func connectSubscriber() { + setMediaConnectionSettings(connection!.mediaSubscriber) + connection!.mediaSubscriber.connect { + error in + DispatchQueue.main.async { + if let error = error { + self.failConnection(error: error) + return + } + self.finishConnection(self.connection!.mediaSubscriber) + } + } + } + + + func disconnect() { + if let conn = connection { + conn.mediaPublisher.disconnect { _ in () } + conn.mediaSubscriber.disconnect { _ in () } + } + state = .disconnected + connectingAlertController = nil + } + + func setMediaConnectionSettings(_ mediaConn: MediaConnection) { + mediaConn.multistreamEnabled = multistreamEnabled + mediaConn.mediaOption.videoEnabled = videoEnabled + if let codec = videoCodec { + mediaConn.mediaOption.videoCodec = codec + } + mediaConn.mediaOption.audioEnabled = audioEnabled + if let codec = audioCodec { + mediaConn.mediaOption.audioCodec = codec + } + + } + + func failConnection(error: ConnectionError) { + var title = "Connection Error" + var message = error.localizedDescription + switch error { + case .connectionBusy: + message = "Connection is busy" + case .webSocketClose(let code, let reason): + let reason = reason ?? "?" + message = String(format: "WebSocket is closed (status code %d, reason %@)", + code, reason) + case .signalingFailure(reason: let reason): + title = "Signaling Failure" + message = reason + default: + break + } + + connectingAlertController.dismiss(animated: true) { + self.presentSimpleAlert(title: title, message: message) + self.state = .disconnected + self.connectionController?.onConnectHandler?(nil, nil, error) + } + } + + func finishConnection(_ mediaConnection: MediaConnection) { + if connectingAlertController != nil { + connectingAlertController.dismiss(animated: true) { + self.basicFinishConnection(mediaConnection) + } + connectingAlertController = nil + } else { + basicFinishConnection(mediaConnection) + } + } + + func basicFinishConnection(_ mediaConnection: MediaConnection) { + state = .connected + mediaConnection.mainMediaStream!.startConnectionTimer(timeInterval: 1) { + seconds in + if let seconds = seconds { + DispatchQueue.main.async { + let text = String(format: "%02d:%02d:%02d", + arguments: [seconds/(60*60), seconds/60, seconds%60]) + self.connectionTimeValueLabel.text = text + } + } + } + connectionController!.onConnectHandler?(connection, roles, nil) + back(isCancel: false) + } + + func presentSimpleAlert(title: String? = nil, message: String? = nil) { + let alert = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default) { + action in return + }) + present(alert, animated: true) {} + } + + @IBAction func switchMicrophoneEnabled(_ sender: AnyObject) { + guard let pub = connection?.mediaPublisher else { return } + guard pub.isAvailable else { return } + + pub.microphoneEnabled = enableMicrophoneSwitch.isOn + } + + // MARK: テキストフィールドの編集 + + @IBAction func hostTextFieldDidTouchDown(_ sender: AnyObject) { + touchedField = hostTextField + } + + @IBAction func hostTextFieldEditingDidEndOnExit(_ sender: AnyObject) { + touchedField = nil + } + + @IBAction func portTextFieldDidTouchDown(_ sender: AnyObject) { + touchedField = portTextField + } + + @IBAction func portTextFieldEditingDidEndOnExit(_ sender: AnyObject) { + touchedField = nil + } + + @IBAction func signalingPathTextFieldDidTouchDown(_ sender: AnyObject) { + touchedField = signalingPathTextField + } + + @IBAction func signalingPathTextFieldEditingDidEndOnExit(_ sender: AnyObject) { + touchedField = nil + } + + @IBAction func channelIdTextFieldDidTouchDown(_ sender: AnyObject) { + touchedField = channelIdTextField + } + + @IBAction func channelIdTextFieldEditingDidEndOnExit(_ sender: AnyObject) { + touchedField = nil + } + + @IBAction func handleTap(sender: UITapGestureRecognizer) { + if sender.state == .ended { + hostTextField.resignFirstResponder() + portTextField.resignFirstResponder() + signalingPathTextField.resignFirstResponder() + channelIdTextField.resignFirstResponder() + } + } + +} diff --git a/Sora/Sora/ConnectionController/RoleViewController.swift b/Sora/Sora/ConnectionController/RoleViewController.swift new file mode 100644 index 00000000..4b246b08 --- /dev/null +++ b/Sora/Sora/ConnectionController/RoleViewController.swift @@ -0,0 +1,99 @@ +import UIKit + +class RoleViewController: UITableViewController { + + struct Component { + var label: UILabel + var cell: UITableViewCell + } + + @IBOutlet weak var publisherLabel: UILabel! + @IBOutlet weak var publisherCell: UITableViewCell! + @IBOutlet weak var subscriberLabel: UILabel! + @IBOutlet weak var subscriberCell: UITableViewCell! + + var main: ConnectionViewController { + get { return ConnectionViewController.main! } + } + + lazy var components: [ConnectionController.Role: Component] = [ + .publisher: Component(label: self.publisherLabel, cell: self.publisherCell), + .subscriber: Component(label: self.subscriberLabel, cell: self.subscriberCell) + ] + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + for label: UILabel in [publisherLabel, subscriberLabel] { + label.font = UIFont.preferredFont(forTextStyle: .body) + label.adjustsFontForContentSizeCategory = true + } + } + + override func viewWillAppear(_ animated: Bool) { + clearCheckmarks() + for role in ConnectionController.Role.allRoles { + let comp = components[role]! + if main.connectionController!.availableRoles.contains(role) { + comp.label.textColor = UIColor.black + comp.cell.isUserInteractionEnabled = true + if main.roles.contains(role) { + comp.cell.accessoryType = .checkmark + } + } else { + comp.label.textColor = UIColor.lightGray + comp.cell.accessoryType = .none + comp.cell.isUserInteractionEnabled = false + } + } + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + + func clearCheckmarks() { + publisherCell?.accessoryType = .none + subscriberCell?.accessoryType = .none + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + switch indexPath.row { + case 0: + selectRole(.publisher) + case 1: + selectRole(.subscriber) + default: + break + } + } + + func selectRole(_ role: ConnectionController.Role) { + guard main.connectionController!.availableRoles.contains(role) else { + return + } + + if main.roles.count > 1 && main.roles.contains(role) { + main.removeRole(role) + components[role]?.cell.accessoryType = .none + } else { + main.addRole(role) + components[role]?.cell.accessoryType = .checkmark + } + } + +} diff --git a/Sora/Sora/ConnectionController/VideoCodecViewController.swift b/Sora/Sora/ConnectionController/VideoCodecViewController.swift new file mode 100644 index 00000000..6278175b --- /dev/null +++ b/Sora/Sora/ConnectionController/VideoCodecViewController.swift @@ -0,0 +1,83 @@ +import UIKit + +class VideoCodecViewController: UITableViewController { + + @IBOutlet weak var defaultLabel: UILabel! + @IBOutlet weak var VP8Label: UILabel! + @IBOutlet weak var VP9Label: UILabel! + @IBOutlet weak var H264Label: UILabel! + + @IBOutlet weak var defaultCell: UITableViewCell! + @IBOutlet weak var VP8Cell: UITableViewCell! + @IBOutlet weak var VP9Cell: UITableViewCell! + @IBOutlet weak var H264Cell: UITableViewCell! + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + for label: UILabel in [defaultLabel, VP8Label, VP9Label, H264Label] { + label.font = UIFont.preferredFont(forTextStyle: .body) + label.adjustsFontForContentSizeCategory = true + } + } + + override func viewWillAppear(_ animated: Bool) { + clearCheckmarks() + switch ConnectionViewController.main?.videoCodec { + case .default?, nil: + defaultCell.accessoryType = .checkmark + case .VP8?: + VP8Cell.accessoryType = .checkmark + case .VP9?: + VP9Cell.accessoryType = .checkmark + case .H264?: + H264Cell.accessoryType = .checkmark + } + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destinationViewController. + // Pass the selected object to the new view controller. + } + */ + + func clearCheckmarks() { + defaultCell?.accessoryType = .none + VP8Cell?.accessoryType = .none + VP9Cell?.accessoryType = .none + H264Cell?.accessoryType = .none + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + clearCheckmarks() + switch indexPath.row { + case 0: + ConnectionViewController.main?.videoCodec = .default + defaultCell?.accessoryType = .checkmark + case 1: + ConnectionViewController.main?.videoCodec = .VP8 + VP8Cell?.accessoryType = .checkmark + case 2: + ConnectionViewController.main?.videoCodec = .VP9 + VP9Cell?.accessoryType = .checkmark + case 3: + ConnectionViewController.main?.videoCodec = .H264 + H264Cell?.accessoryType = .checkmark + default: + break + } + } + +} diff --git a/Sora/Sora/Event.swift b/Sora/Sora/Event.swift new file mode 100644 index 00000000..494f2400 --- /dev/null +++ b/Sora/Sora/Event.swift @@ -0,0 +1,105 @@ +import Foundation + +public class Event { + + public enum EventType: String { + case WebSocket + case Signaling + case PeerConnection + case MediaPublisher + case MediaSubscriber + case MediaStream + case VideoRenderer + case VideoView + } + + public enum Marker { + case Atomic + case Start + case End + } + + public var URL: URL + public var mediaChannelId: String + public var type: EventType + public var comment: String + public var date: Date + + public init(URL: URL, + mediaChannelId: String, + type: EventType, + comment: String, + date: Date = Date()) { + self.URL = URL + self.mediaChannelId = mediaChannelId + self.type = type + self.comment = comment + self.date = date + } + + public var description: String { + get { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let desc = String(format: "[%@ %@ %@] %@: %@", + URL.absoluteString, + mediaChannelId, + formatter.string(from: date), + type.rawValue, comment) + return desc + } + } + +} + +public class EventLog { + + public var URL: URL + public var mediaChannelId: String + public var events: [Event] = [] + public var isEnabled: Bool = true + public var limit: Int? = nil + public var debugMode: Bool = false + + public static var globalDebugMode: Bool = false + + init(URL: URL, mediaChannelId: String) { + self.URL = URL + self.mediaChannelId = mediaChannelId + } + + public func clear() { + events = [] + } + + public func mark(event: Event) { + if isEnabled { + if EventLog.globalDebugMode || debugMode { + print(event.description) + } + if let limit = limit { + if limit < events.count { + events.removeFirst() + } + } + events.append(event) + onMarkHandler?(event) + } + } + + public func markFormat(type: Event.EventType, + format: String, + arguments: CVarArg...) { + let comment = String(format: format, arguments: arguments) + let event = Event(URL: URL, mediaChannelId: mediaChannelId, + type: type, comment: comment) + mark(event: event) + } + + var onMarkHandler: ((Event) -> Void)? + + public func onMark(handler: @escaping (Event) -> Void) { + onMarkHandler = handler + } + +} diff --git a/Sora/Sora/EventHandlers.swift b/Sora/Sora/EventHandlers.swift new file mode 100644 index 00000000..61d0939b --- /dev/null +++ b/Sora/Sora/EventHandlers.swift @@ -0,0 +1,143 @@ +import Foundation +import WebRTC +import SocketRocket + +open class WebSocketEventHandlers { + + var onOpenHandler: ((SRWebSocket) -> ())? + var onFailureHandler: ((SRWebSocket, Error) -> ())? + var onPongHandler: ((SRWebSocket, Data) -> ())? + var onMessageHandler: ((SRWebSocket, AnyObject) -> ())? + var onCloseHandler: ((SRWebSocket, Int, String?, Bool) -> ())? + + public init() {} + + public func onOpen(handler: @escaping (SRWebSocket) -> ()) { + onOpenHandler = handler + } + + public func onFailure(handler: @escaping (SRWebSocket, Error) -> ()) { + onFailureHandler = handler + } + + public func onPong(handler: @escaping (SRWebSocket, Data) -> ()) { + onPongHandler = handler + } + + public func onMessage(handler: @escaping (SRWebSocket, AnyObject) -> ()) { + onMessageHandler = handler + } + + public func onClose(handler: @escaping (SRWebSocket, Int, String?, Bool) -> ()) { + onCloseHandler = handler + } + +} + +open class SignalingEventHandlers { + + var onReceiveHandler: ((Message) -> Void)? + var onConnectHandler: ((Void) -> Void)? + var onDisconnectHandler: ((Void) -> Void)? + var onFailureHandler: ((ConnectionError) -> Void)? + var onPingHandler: ((Void) -> Void)? + + public init() {} + + public func onReceive(handler: @escaping ((Message) -> Void)) { + onReceiveHandler = handler + } + + public func onConnect(handler: @escaping ((Void) -> Void)) { + onConnectHandler = handler + } + + public func onDisconnect(handler: @escaping ((Void) -> Void)) { + onDisconnectHandler = handler + } + + public func onFailure(handler: @escaping ((ConnectionError) -> Void)) { + onFailureHandler = handler + } + + public func onPing(handler: @escaping ((Void) -> Void)) { + onPingHandler = handler + } + +} + +open class PeerConnectionEventHandlers { + + var onConnectHandler: ((RTCPeerConnection) -> Void)? + var onDisconnectHandler: ((RTCPeerConnection) -> Void)? + var onFailureHandler: ((RTCPeerConnection, Error) -> Void)? + var onChangeSignalingStateHandler: + ((RTCPeerConnection, RTCSignalingState) -> Void)? + var onAddStreamHandler: ((RTCPeerConnection, RTCMediaStream) -> Void)? + var onRemoveStreamHandler: ((RTCPeerConnection, RTCMediaStream) -> Void)? + var onNegotiateHandler: ((RTCPeerConnection) -> Void)? + var onChangeIceConnectionState: + ((RTCPeerConnection, RTCIceConnectionState) -> Void)? + var onChangeIceConnectionStateHandler: + ((RTCPeerConnection, RTCIceConnectionState) -> Void)? + var onChangeIceGatheringStateHandler: + ((RTCPeerConnection, RTCIceGatheringState) -> Void)? + var onGenerateIceCandidateHandler: + ((RTCPeerConnection, RTCIceCandidate) -> Void)? + var onRemoveCandidatesHandler: + ((RTCPeerConnection, [RTCIceCandidate]) -> Void)? + + public init() {} + + public func onConnect(handler: @escaping ((RTCPeerConnection) -> Void)) { + onConnectHandler = handler + } + + public func onDisconnect(handler: @escaping ((RTCPeerConnection) -> Void)) { + onDisconnectHandler = handler + } + + public func onFailure(handler: @escaping ((RTCPeerConnection, Error) -> Void)) { + onFailureHandler = handler + } + + public func onChangeSignalingState(handler: + @escaping (RTCPeerConnection, RTCSignalingState) -> Void) { + onChangeSignalingStateHandler = handler + } + + public func onAddStream(handler: + @escaping (RTCPeerConnection, RTCMediaStream) -> Void) { + onAddStreamHandler = handler + } + + public func onRemoveStream(handler: + @escaping (RTCPeerConnection, RTCMediaStream) -> Void) { + onRemoveStreamHandler = handler + } + + public func onNegotiate(handler: @escaping (RTCPeerConnection) -> Void) { + onNegotiateHandler = handler + } + + public func onChangeIceConnectionState(handler: + @escaping (RTCPeerConnection, RTCIceConnectionState) -> Void) { + onChangeIceConnectionStateHandler = handler + } + + public func onChangeIceGatheringState(handler: + @escaping (RTCPeerConnection, RTCIceGatheringState) -> Void) { + onChangeIceGatheringStateHandler = handler + } + + public func onGenerateIceCandidate(handler: + @escaping (RTCPeerConnection, RTCIceCandidate) -> Void) { + onGenerateIceCandidateHandler = handler + } + + public func onRemoveCandidates(handler: + @escaping (RTCPeerConnection, [RTCIceCandidate]) -> Void) { + onRemoveCandidatesHandler = handler + } + +} diff --git a/Sora/Sora/Extensions.swift b/Sora/Sora/Extensions.swift new file mode 100644 index 00000000..fc2fd023 --- /dev/null +++ b/Sora/Sora/Extensions.swift @@ -0,0 +1,15 @@ +import Foundation +import UIKit + +extension UITextField { + + public func nonEmptyText() -> String? { + if let text = text { + if !text.isEmpty { + return text + } + } + return nil + } + +} diff --git a/Sora/Sora/Info.plist b/Sora/Sora/Info.plist new file mode 100644 index 00000000..d3de8eef --- /dev/null +++ b/Sora/Sora/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Sora/Sora/JSON.swift b/Sora/Sora/JSON.swift new file mode 100644 index 00000000..ba82a7d4 --- /dev/null +++ b/Sora/Sora/JSON.swift @@ -0,0 +1,28 @@ +import Foundation + +struct JSONBuilder { + + var values: [String: AnyObject] + + func generate() -> String { + return try! String(data: JSONSerialization.data(withJSONObject: self.values, + options: JSONSerialization.WritingOptions(rawValue: 0)), + encoding: String.Encoding.utf8)! + } + +} + +/* +public protocol JSONEncodable { + + func JSONRepresentation() -> String + +} + +func CreateJSONRepresentation(obj: AnyObject) -> String { + return try! String(data: NSJSONSerialization.dataWithJSONObject(obj, options: NSJSONWritingOptions(rawValue: 0)), + encoding: NSUTF8StringEncoding)! +} + + +*/ diff --git a/Sora/Sora/MediaConnection.swift b/Sora/Sora/MediaConnection.swift new file mode 100644 index 00000000..7b6a7208 --- /dev/null +++ b/Sora/Sora/MediaConnection.swift @@ -0,0 +1,413 @@ +import Foundation +import WebRTC + +public class MediaConnection { + + public struct NotificationKey { + + public enum UserInfo: String { + case connectionError = "Sora.MediaConnection.UserInfo.connectionError" + } + + public static var onConnect = + Notification.Name("Sora.MediaConnection.Notification.onConnect") + public static var onDisconnect = + Notification.Name("Sora.MediaConnection.Notification.onDisconnect") + public static var onFailure = + Notification.Name("Sora.MediaConnection.Notification.onFailure") + + } + + public weak var connection: Connection! + public var peerConnection: PeerConnection? + public var mediaOption: MediaOption = MediaOption() + public var multistreamEnabled: Bool = false + public var mediaStreams: [MediaStream] = [] + + public var mainMediaStream: MediaStream? { + get { return mediaStreams.first } + } + + public var webSocketEventHandlers: WebSocketEventHandlers + = WebSocketEventHandlers() + public var signalingEventHandlers: SignalingEventHandlers + = SignalingEventHandlers() + public var peerConnectionEventHandlers: PeerConnectionEventHandlers + = PeerConnectionEventHandlers() + + public var isAvailable: Bool { + get { return peerConnection?.isAvailable ?? false } + } + + var eventLog: EventLog? { + get { return connection?.eventLog } + } + + var eventType: Event.EventType { + get { + assert(false, "must be override") + return .MediaPublisher + } + } + + var role: MediaStreamRole { + get { + assertionFailure("subclass must implement role()") + return .upstream + } + } + + init(connection: Connection) { + self.connection = connection + } + + // MARK: 接続 + + public func connect(metadata: String? = nil, + timeout: Int = 30, + handler: @escaping ((ConnectionError?) -> Void)) { + eventLog?.markFormat(type: eventType, format: "try connect") + peerConnection = PeerConnection(connection: connection, + mediaConnection: self, + role: role, + metadata: metadata, + mediaStreamId: nil, + mediaOption: mediaOption) + peerConnection!.connect(timeout: timeout) { + error in + if let error = error { + self.eventLog?.markFormat(type: self.eventType, + format: "connect error: %@", + arguments: error.localizedDescription) + self.peerConnection!.terminate() + self.peerConnection = nil + self.onFailureHandler?(error) + self.callOnConnectHandler(error) + handler(error) + } else { + self.eventLog?.markFormat(type: self.eventType, format: "connect ok") + self.internalOnConnect() + self.callOnConnectHandler() + handler(nil) + } + } + } + + // 内部用のコールバック + func internalOnConnect() {} + + public func disconnect(handler: @escaping (ConnectionError?) -> Void) { + eventLog?.markFormat(type: eventType, format: "try disconnect") + switch peerConnection?.state { + case nil, .disconnected?: + eventLog?.markFormat(type: eventType, + format: "error: already disconnected") + handler(ConnectionError.connectionDisconnected) + case .disconnecting?: + eventLog?.markFormat(type: eventType, + format: "error: connection is busy") + handler(ConnectionError.connectionBusy) + case .connected?, .connecting?: + eventLog?.markFormat(type: eventType, format: "disconnect ok") + for stream in mediaStreams { + stream.terminate() + } + mediaStreams = [] + peerConnection!.disconnect { + error in + handler(error) + } + } + } + + public func send(message: Messageable) -> ConnectionError? { + eventLog?.markFormat(type: eventType, format: "send message") + if isAvailable { + return peerConnection!.send(message: message) + } else { + return ConnectionError.connectionDisconnected + } + } + + // MARK: マルチストリーム + + func hasMediaStream(_ mediaStreamId: String) -> Bool { + guard let peerConn = peerConnection else { + assertionFailure("peer connection must not be nil") + return false + } + + if multistreamEnabled && !mediaStreams.isEmpty && + peerConn.clientId == mediaStreamId { + return true + } else { + return mediaStreams.contains { + stream in + return stream.mediaStreamId == mediaStreamId + } + } + } + + func addMediaStream(_ mediaStream: MediaStream) { + eventLog?.markFormat(type: eventType, + format: "add media stream '%@'", + arguments: mediaStream.mediaStreamId) + if hasMediaStream(mediaStream.mediaStreamId) { + assertionFailure("media stream already exists") + } + + mediaStreams.append(mediaStream) + onAddStreamHandler?(mediaStream) + } + + func removeMediaStream(_ mediaStreamId: String) { + eventLog?.markFormat(type: eventType, format: "remove media stream") + var removed: MediaStream? + mediaStreams = mediaStreams.filter { + e in + if e.mediaStreamId == mediaStreamId { + removed = e + return false + } else { + return true + } + } + if let stream = removed { + onRemoveStreamHandler?(stream) + } + } + + // MARK: イベントハンドラ + + private var onConnectHandler: ((ConnectionError?) -> Void)? + private var onDisconnectHandler: ((ConnectionError?) -> Void)? + private var onFailureHandler: ((ConnectionError) -> Void)? + private var onAddStreamHandler: ((MediaStream) -> Void)? + private var onRemoveStreamHandler: ((MediaStream) -> Void)? + + public func onConnect(handler: @escaping (ConnectionError?) -> Void) { + onConnectHandler = handler + } + + func callOnConnectHandler(_ error: ConnectionError? = nil) { + onConnectHandler?(error) + NotificationCenter + .default + .post(name: Connection.NotificationKey.onConnect, + object: connection, + userInfo: + [Connection.NotificationKey.UserInfo.connectionError: error as Any, + Connection.NotificationKey.UserInfo.mediaConnection: self]) + } + + public func onDisconnect(handler: @escaping (ConnectionError?) -> Void) { + onDisconnectHandler = handler + } + + func callOnDisconnectHandler(_ error: ConnectionError?) { + onDisconnectHandler?(error) + NotificationCenter + .default + .post(name: Connection.NotificationKey.onDisconnect, + object: connection, + userInfo: + [Connection.NotificationKey.UserInfo.connectionError: error as Any, + Connection.NotificationKey.UserInfo.mediaConnection: self]) + NotificationCenter + .default + .post(name: MediaConnection.NotificationKey.onDisconnect, + object: self, + userInfo: + [MediaConnection.NotificationKey.UserInfo + .connectionError: error as Any]) + } + + public func onFailure(handler: @escaping (ConnectionError) -> Void) { + onFailureHandler = handler + } + + func callOnFailureHandler(_ error: ConnectionError) { + onFailureHandler?(error) + NotificationCenter + .default + .post(name: Connection.NotificationKey.onFailure, + object: connection, + userInfo: + [Connection.NotificationKey.UserInfo.connectionError: error as Any, + Connection.NotificationKey.UserInfo.mediaConnection: self]) + NotificationCenter + .default + .post(name: MediaConnection.NotificationKey.onFailure, + object: self, + userInfo: + [MediaConnection.NotificationKey.UserInfo + .connectionError: error as Any]) + } + + public func onAddStream(handler: @escaping (MediaStream) -> Void) { + onAddStreamHandler = handler + } + + public func onRemoveStream(handler: @escaping (MediaStream) -> Void) { + onRemoveStreamHandler = handler + } + +} + +class MediaCapturer { + + public var videoCaptureTrack: RTCVideoTrack + public var videoCaptureSource: RTCAVFoundationVideoSource + public var audioCaptureTrack: RTCAudioTrack + + init(factory: RTCPeerConnectionFactory, mediaOption: MediaOption?) { + videoCaptureSource = factory + .avFoundationVideoSource(with: + mediaOption?.videoCaptureSourceMediaConstraints ?? + MediaOption.defaultMediaConstraints) + videoCaptureTrack = factory + .videoTrack(with: videoCaptureSource, + trackId: mediaOption?.videoCaptureTrackId ?? + MediaOption.createCaptureTrackId()) + audioCaptureTrack = factory + .audioTrack(withTrackId: mediaOption?.audioCaptureTrackId ?? + MediaOption.createCaptureTrackId()) + } + +} + +public enum CameraPosition: String { + case front + case back + + public func flip() -> CameraPosition { + switch self { + case .front: return .back + case .back: return .front + } + } + +} + +public class MediaPublisher: MediaConnection { + + public var canUseBackCamera: Bool? { + get { return mediaCapturer?.videoCaptureSource.canUseBackCamera } + } + + public var captureSession: AVCaptureSession? { + get { return mediaCapturer?.videoCaptureSource.captureSession } + } + + var _cameraPosition: CameraPosition? + + public var cameraPosition: CameraPosition? { + + get { + if mediaCapturer != nil { + if _cameraPosition == nil { + _cameraPosition = .front + } + } else { + _cameraPosition = nil + } + return _cameraPosition + } + + set { + if let capturer = mediaCapturer { + if let value = newValue { + eventLog?.markFormat(type: eventType, + format: "switch camera to %@", + arguments: value.rawValue) + switch value { + case .front: + capturer.videoCaptureSource.useBackCamera = false + case .back: + capturer.videoCaptureSource.useBackCamera = true + } + _cameraPosition = newValue + } + } + } + + } + + public var autofocusEnabled = false { + didSet { + if let session = captureSession { + for input in session.inputs { + if let device = input as? AVCaptureDevice { + if autofocusEnabled { + device.focusMode = .autoFocus + } else { + device.focusMode = .locked + } + } + } + } + } + } + + public var microphoneEnabled: Bool? { + + get { + guard let capturer = mediaCapturer else { return nil } + guard let stream = mainMediaStream else { return nil } + return stream.nativeMediaStream.audioTracks + .contains(capturer.audioCaptureTrack) + } + + set { + guard let capturer = mediaCapturer else { return } + guard let stream = mainMediaStream else { return } + + let hasTrack = stream.nativeMediaStream.audioTracks + .contains(capturer.audioCaptureTrack) + switch newValue { + case nil: + break + + case true?: + if !hasTrack { + stream.nativeMediaStream.addAudioTrack(capturer.audioCaptureTrack) + } + + case false?: + if hasTrack { + stream.nativeMediaStream.removeAudioTrack(capturer.audioCaptureTrack) + } + } + } + + } + + override var eventType: Event.EventType { + get { return .MediaPublisher } + } + + override var role: MediaStreamRole { + get { return .upstream } + } + + var mediaCapturer: MediaCapturer? { + get { return peerConnection?.mediaCapturer } + } + + override func internalOnConnect() { + autofocusEnabled = false + } + + public func flipCameraPosition() { + cameraPosition = cameraPosition?.flip() + } + +} + +public class MediaSubscriber: MediaConnection { + + override var role: MediaStreamRole { + get { return .downstream } + } + +} diff --git a/Sora/Sora/MediaOption.swift b/Sora/Sora/MediaOption.swift new file mode 100644 index 00000000..f76849bc --- /dev/null +++ b/Sora/Sora/MediaOption.swift @@ -0,0 +1,74 @@ +import Foundation +import WebRTC + +public enum MediaStreamRole { + + case upstream + case downstream + +} + +public enum VideoCodec { + + case `default` + case VP8 + case VP9 + case H264 + +} + +public enum AudioCodec { + + case `default` + case Opus + case PCMU + +} + +public class MediaOption { + + public var videoCodec: VideoCodec = .default + public var audioCodec: AudioCodec = .default + public var videoEnabled: Bool = true + public var audioEnabled: Bool = true + + public static var maxBitRate = 5000 + + public var bitRate: Int? { + didSet { + if let bitRate = bitRate { + self.bitRate = max(0, min(bitRate, MediaOption.maxBitRate)) + } + } + } + + public var configuration: RTCConfiguration = defaultConfiguration + public var signalingAnswerMediaConstraints: RTCMediaConstraints = defaultMediaConstraints + public var videoCaptureSourceMediaConstraints: RTCMediaConstraints = defaultMediaConstraints + public var peerConnectionMediaConstraints: RTCMediaConstraints = defaultMediaConstraints + public lazy var videoCaptureTrackId: String = MediaOption.createCaptureTrackId() + public lazy var audioCaptureTrackId: String = MediaOption.createCaptureTrackId() + + static var defaultConfiguration: RTCConfiguration = { + () -> RTCConfiguration in + let config = RTCConfiguration() + config.iceServers = [ + RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"], + username: nil, credential: nil)] + return config + }() + + static var defaultMediaConstraints: RTCMediaConstraints = + RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) + + // MARK: - トラック ID + + static var nextCaptureTrackId: Int = 0 + + public static func createCaptureTrackId() -> String { + let id = "capturer_" + MediaOption.nextCaptureTrackId.description + MediaOption.nextCaptureTrackId += 1 + return id + } + +} diff --git a/Sora/Sora/MediaStream.swift b/Sora/Sora/MediaStream.swift new file mode 100644 index 00000000..34ad1894 --- /dev/null +++ b/Sora/Sora/MediaStream.swift @@ -0,0 +1,156 @@ +import Foundation +import WebRTC + +public class MediaStream { + + public struct NotificationKey { + + public enum UserInfo: String { + case seconds = "Sora.MediaStream.UserInfo.seconds" + } + + public static var onCountUp = + Notification.Name("Sora.MediaStream.Notification.onCountUp") + + } + + static var defaultStreamId: String = "mainStream" + static var defaultVideoTrackId: String = "mainVideo" + static var defaultAudioTrackId: String = "mainAudio" + + public weak var peerConnection: PeerConnection? + public var nativeMediaStream: RTCMediaStream + public var creationTime: Date + + var eventLog: EventLog? { + get { return peerConnection?.eventLog } + } + + public var isAvailable: Bool { + get { return peerConnection?.isAvailable ?? false } + } + + public var mediaStreamId: String { + get { return nativeMediaStream.streamId } + } + + public var nativeVideoTrack: RTCVideoTrack? { + get { return nativeMediaStream.videoTracks.first } + } + + public var nativeAudioTrack: RTCAudioTrack? { + get { return nativeMediaStream.audioTracks.first } + } + + public var videoRenderer: VideoRenderer? { + + get { + return videoRendererAdapter?.videoRenderer + } + + set { + if let value = newValue { + videoRendererAdapter = VideoRendererAdapter(videoRenderer: value) + } else { + videoRendererAdapter = nil + } + } + + } + + var videoRendererAdapter: VideoRendererAdapter? { + + willSet { + guard let videoTrack = nativeVideoTrack else { return } + guard let adapter = videoRendererAdapter else { return } + eventLog?.markFormat(type: .VideoRenderer, + format: "remove old video renderer %@", + arguments: adapter.videoRenderer as! CVarArg) + videoTrack.remove(adapter) + } + + didSet { + guard let videoTrack = nativeVideoTrack else { return } + guard let adapter = videoRendererAdapter else { return } + eventLog?.markFormat(type: .VideoRenderer, + format: "set video renderer %@", + arguments: adapter.videoRenderer as! CVarArg) + videoTrack.add(adapter) + } + + } + + init(peerConnection: PeerConnection, nativeMediaStream: RTCMediaStream) { + self.peerConnection = peerConnection + self.nativeMediaStream = nativeMediaStream + creationTime = Date() + } + + func terminate() { + stopConnectionTimer() + } + + // MARK: タイマー + + var connectionTimer: Timer? + var connectionTimerHandler: ((Int?) -> Void)? + var connectionTimerForNotification: Timer? + + public func startConnectionTimer(timeInterval: TimeInterval, + handler: @escaping ((Int?) -> Void)) { + eventLog?.markFormat(type: .MediaStream, + format: "start timer (interval %f)", + arguments: timeInterval) + connectionTimerHandler = handler + + connectionTimer?.invalidate() + connectionTimer = Timer(timeInterval: timeInterval, repeats: true) { + timer in + self.updateConnectionTime(timer) + } + updateConnectionTime(connectionTimer!) + RunLoop.main.add(connectionTimer!, forMode: .commonModes) + + connectionTimerForNotification?.invalidate() + connectionTimerForNotification = Timer(timeInterval: 1.0, repeats: true) { + timer in + self.updateConnectionTimeForNotification( + self.connectionTimerForNotification!) + } + updateConnectionTime(connectionTimerForNotification!) + RunLoop.main.add(connectionTimerForNotification!, forMode: .commonModes) + } + + func updateConnectionTime(_ timer: Timer) { + if isAvailable { + let diff = Date(timeIntervalSinceNow: 0) + .timeIntervalSince(self.creationTime) + connectionTimerHandler?(Int(diff)) + } else { + connectionTimerHandler?(nil) + } + } + + func updateConnectionTimeForNotification(_ timer: Timer) { + var seconds: Int? + if isAvailable { + let diff = Date(timeIntervalSinceNow: 0) + .timeIntervalSince(self.creationTime) + seconds = Int(diff) + } + NotificationCenter + .default + .post(name: MediaStream.NotificationKey.onCountUp, + object: self, + userInfo: + [MediaStream.NotificationKey.UserInfo.seconds: seconds as Any]) + } + + public func stopConnectionTimer() { + eventLog?.markFormat(type: .MediaStream, format: "stop timer") + connectionTimer?.invalidate() + connectionTimer = nil + connectionTimerHandler = nil + } + +} diff --git a/Sora/Sora/Message.swift b/Sora/Sora/Message.swift new file mode 100644 index 00000000..d7f113cf --- /dev/null +++ b/Sora/Sora/Message.swift @@ -0,0 +1,460 @@ +import Foundation +import WebRTC +import Unbox + +public class Message { + + public enum MessageType: String { + case connect = "connect" + case offer = "offer" + case answer = "answer" + case candidate = "candidate" + case ping = "ping" + case pong = "pong" + case notify = "notify" + case update = "update" + } + + public var type: MessageType? + public var data: [String: Any] + + public init(type: MessageType, data: [String: Any] = [:]) { + self.type = type + self.data = data + } + + static func fromJSONData(_ data: Any) -> Message? { + let base: Data! + if data is Data { + base = data as? Data + } else if let data = data as? String { + if let data = data.data(using: String.Encoding.utf8) { + base = data + } else { + return nil + } + } else { + return nil + } + + do { + let j = try JSONSerialization.jsonObject(with: base, options: JSONSerialization.ReadingOptions(rawValue: 0)) + return fromJSONObject(j as Any) + } catch _ { + return nil + } + } + + static func fromJSONObject(_ j: Any) -> Message? { + if let j = j as? [String: Any] { + if let type = j["type"] as? String { + if let type = MessageType(rawValue: type) { + return Message(type: type, data: j) + } else { + return nil + } + } else { + return nil + } + } else { + return nil + } + } + + func JSON() -> [String: Any] { + var json: [String: Any] = self.data + json["type"] = type?.rawValue + return json + } + + func JSONRepresentation() -> String { + let j = JSON() + let data = try! JSONSerialization.data(withJSONObject: j, + options: + JSONSerialization.WritingOptions(rawValue: 0)) + return NSString(data: data, + encoding: String.Encoding.utf8.rawValue) as String! + } + + public var description: String { + get { return JSONRepresentation() } + } + +} + +extension Message : Messageable { + + public func message() -> Message { + return self + } + +} + +public protocol Messageable { + + func message() -> Message + +} + +protocol JSONEncodable { + + func encode() -> Any + +} + +enum Enable: JSONEncodable { + + case `default`(T) + case enable(T) + case disable + + func encode() -> Any { + switch self { + case .default(let value): + return value.encode() + case .enable(let value): + return value.encode() + case .disable: + return "false" as Any + } + } + +} + +enum SignalingRole: String, UnboxableEnum { + + case upstream + case downstream + + static func from(_ role: MediaStreamRole) -> SignalingRole { + switch role { + case .upstream: + return upstream + case .downstream: + return downstream + } + } + +} + +extension SignalingRole: JSONEncodable { + + func encode() -> Any { + switch self { + case .upstream: + return "upstream" as Any + case .downstream: + return "downstream" as Any + } + } + +} + +enum SignalingVideoCodec: String, UnboxableEnum { + + case VP8 = "VP8" + case VP9 = "VP9" + case H264 = "H264" + +} + +enum SignalingAudioCodec: String, UnboxableEnum { + + case Opus = "OPUS" + case PCMU = "PCMU" + +} + +struct SignalingVideo { + + var bit_rate: Int? + var codec_type: SignalingVideoCodec? + +} + +extension SignalingVideo: JSONEncodable { + + func encode() -> Any { + var data: [String: Any] = [:] + if let codec_type = codec_type { + data["codec_type"] = codec_type.rawValue + } + if let value = bit_rate { + data["bit_rate"] = value.description + } + return data as Any + } + +} + +struct SignalingAudio { + + var codec_type: SignalingAudioCodec? + +} + +extension SignalingAudio: JSONEncodable { + + func encode() -> Any { + var data: [String: Any] = [:] + if let codec_type = codec_type { + data["codec_type"] = codec_type.rawValue + } + return data as Any! + } + +} + +struct SignalingConnect { + + var role: SignalingRole + var channel_id: String + var metadata: String? + var mediaOption: MediaOption + var multistream: Bool + + init(role: SignalingRole, channel_id: String, metadata: String? = nil, + multistream: Bool = false, mediaOption: MediaOption) { + self.role = role + self.channel_id = channel_id + self.metadata = metadata + self.multistream = multistream + self.mediaOption = mediaOption + } + +} + +extension SignalingConnect: Messageable { + + func message() -> Message { + var data: [String : Any] = ["role": role.encode(), + "channel_id": channel_id] + if let value = metadata { + data["metadata"] = value + } + if multistream { + data["multistream"] = true + data["plan_b"] = true + } + + if !mediaOption.videoEnabled { + data["video"] = false + } else { + var video: [String: Any] = [:] + switch mediaOption.videoCodec { + case .default: + break + case .VP8: + video["codec_type"] = SignalingVideoCodec.VP8.rawValue + case .VP9: + video["codec_type"] = SignalingVideoCodec.VP9.rawValue + case .H264: + video["codec_type"] = SignalingVideoCodec.H264.rawValue + } + + if let bitRate = mediaOption.bitRate { + video["bit_rate"] = bitRate + } + + if !video.isEmpty { + data["video"] = video + } + } + + if !mediaOption.audioEnabled { + data["audio"] = false + } else { + var audio: [String: Any] = [:] + switch mediaOption.audioCodec { + case .default: + break + case .Opus: + audio["codec_type"] = SignalingAudioCodec.Opus.rawValue + case .PCMU: + audio["codec_type"] = SignalingAudioCodec.PCMU.rawValue + } + + if !audio.isEmpty { + data["audio"] = audio + } + } + + return Message(type: .connect, data: data as [String : Any]) + } + +} + +struct SignalingOffer { + + struct Configuration { + + struct IceServer { + var urls: [String] + var credential: String + var username: String + } + + var iceServers: [IceServer] + var iceTransportPolicy: String + + } + + var client_id: String + var sdp: String + var config: Configuration? + + func sessionDescription() -> RTCSessionDescription { + return RTCSessionDescription(type: RTCSdpType.offer, sdp: sdp) + } + +} + +extension SignalingOffer: Unboxable { + + init(unboxer: Unboxer) throws { + client_id = try unboxer.unbox(key: "client_id") + sdp = try unboxer.unbox(key: "sdp") + config = unboxer.unbox(key: "config") + } + +} + +extension SignalingOffer.Configuration: Unboxable { + + init(unboxer: Unboxer) throws { + iceServers = try unboxer.unbox(key: "iceServers") + iceTransportPolicy = try unboxer.unbox(key: "iceTransportPolicy") + } + +} + +extension SignalingOffer.Configuration.IceServer: Unboxable { + + init(unboxer: Unboxer) throws { + urls = try unboxer.unbox(key: "urls") + credential = try unboxer.unbox(key: "credential") + username = try unboxer.unbox(key: "username") + } + +} + +struct SignalingAnswer { + + var sdp: String + +} + +extension SignalingAnswer: Messageable { + + func message() -> Message { + return Message(type: .answer, data: ["sdp": sdp as Any]) + } + +} + +extension SignalingVideo: Unboxable { + + init(unboxer: Unboxer) throws { + bit_rate = unboxer.unbox(key: "bit_rate") + codec_type = unboxer.unbox(key: "codec_type") + } + +} + +extension SignalingAudio: Unboxable { + + init(unboxer: Unboxer) throws { + codec_type = unboxer.unbox(key: "codec_type") + } + +} + +struct SignalingICECandidate { + + var candidate: String + +} + +extension SignalingICECandidate: Messageable { + + func message() -> Message { + return Message(type: .candidate, + data: ["candidate": candidate as Any]) + } + +} + +struct SignalingPong { +} + +extension SignalingPong: Messageable { + + func message() -> Message { + return Message(type: .pong) + } + +} + +enum SignalingEventType: String, UnboxableEnum { + + case connectionCreated = "connection.created" + case connectionUpdated = "connection.updated" + case connectionDestroyed = "connection.destroyed" + +} + +struct SignalingNotify { + + var eventType: SignalingEventType + var role: SignalingRole + var connectionTime: Int + var numberOfChannels: Int + var numberOfUpstreamConnections: Int + var numberOfDownstreamConnections: Int + +} + +extension SignalingNotify: Unboxable { + + init(unboxer: Unboxer) throws { + eventType = try unboxer.unbox(key: "event_type") + role = try unboxer.unbox(key: "role") + connectionTime = try unboxer.unbox(key: "minutes") + numberOfChannels = try unboxer.unbox(key: "channel_connections") + numberOfUpstreamConnections = try unboxer.unbox(key: "channel_upstream_connections") + numberOfDownstreamConnections = try unboxer.unbox(key: "channel_downstream_connections") + } + +} + +struct SignalingUpdateOffer { + + var sdp: String + + func sessionDescription() -> RTCSessionDescription { + return RTCSessionDescription(type: RTCSdpType.offer, sdp: sdp) + } + +} + +extension SignalingUpdateOffer: Unboxable { + + public init(unboxer: Unboxer) throws { + sdp = try unboxer.unbox(key: "sdp") + } + +} + +struct SignalingUpdateAnswer { + + var sdp: String + +} + +extension SignalingUpdateAnswer: Messageable { + + func message() -> Message { + return Message(type: .update, data: ["sdp": sdp as Any]) + } + +} diff --git a/Sora/Sora/PeerConnection.swift b/Sora/Sora/PeerConnection.swift new file mode 100644 index 00000000..fe62cb07 --- /dev/null +++ b/Sora/Sora/PeerConnection.swift @@ -0,0 +1,1103 @@ +import Foundation +import WebRTC +import SocketRocket +import Unbox + +public enum StatusCode: Int { + + case signalingFailure = 4490 + +} + +public class PeerConnection { + + public enum State: String { + case connecting + case connected + case disconnecting + case disconnected + } + + public static var nativeFactory: RTCPeerConnectionFactory = { + RTCInitializeSSL() + RTCEnableMetrics() + return RTCPeerConnectionFactory() + }() + + public weak var connection: Connection? + public weak var mediaConnection: MediaConnection? + public var role: MediaStreamRole + public var metadata: String? + var mediaStreamId: String? + public var mediaOption: MediaOption + public var clientId: String? + + public var state: State { + willSet { onChangeStateHandler?(newValue) } + } + + public var isAvailable: Bool { + get { return state == .connected } + } + + var mediaCapturer: MediaCapturer? { + get { return context?.mediaCapturer } + } + + public var nativePeerConnection: RTCPeerConnection? { + get { return context?.nativePeerConnection } + } + + var context: PeerConnectionContext? + + var eventLog: EventLog? { + get { return connection?.eventLog } + } + + init(connection: Connection, + mediaConnection: MediaConnection, + role: MediaStreamRole, + metadata: String? = nil, + mediaStreamId: String? = nil, + mediaOption: MediaOption = MediaOption()) { + self.connection = connection + self.mediaConnection = mediaConnection + self.role = role + self.metadata = metadata + self.mediaStreamId = mediaStreamId + self.mediaOption = mediaOption + self.state = .disconnected + } + + // MARK: ピア接続 + + // 接続に成功すると nativePeerConnection プロパティがセットされる + func connect(timeout: Int, handler: @escaping ((ConnectionError?) -> Void)) { + eventLog?.markFormat(type: .PeerConnection, format: "connect") + switch state { + case .connected, .connecting, .disconnecting: + handler(ConnectionError.connectionBusy) + case .disconnected: + state = .connecting + context = PeerConnectionContext(peerConnection: self, role: role) + context!.connect(timeout: timeout, handler: handler) + } + } + + func disconnect(handler: @escaping (ConnectionError?) -> Void) { + eventLog?.markFormat(type: .PeerConnection, format: "disconnect") + switch state { + case .disconnecting: + handler(ConnectionError.connectionBusy) + case .disconnected: + handler(ConnectionError.connectionDisconnected) + case .connecting, .connected: + assert(nativePeerConnection == nil, "nativePeerConnection must not be nil") + state = .disconnecting + context?.disconnect(handler: handler) + } + } + + func terminate() { + eventLog?.markFormat(type: .PeerConnection, format: "terminate") + state = .disconnected + context = nil + } + + // MARK: WebSocket + + func send(message: Messageable) -> ConnectionError? { + let message = message.message() + switch state { + case .connected: + return context!.send(message) + case .disconnected: + return ConnectionError.connectionDisconnected + default: + return ConnectionError.connectionBusy + } + } + + // MARK: イベントハンドラ + + var onChangeStateHandler: ((State) -> Void)? + + public func onChangeState(handler: @escaping (State) -> Void) { + onChangeStateHandler = handler + } + +} + +// 接続状態を監視する +class ConnectionMonitor { + + enum State { + case stop + case monitoring + case terminating + case terminated + } + + weak var context: PeerConnectionContext! + var state: State = .stop + var error: ConnectionError? + var deadline: DispatchTime + var handler: (ConnectionError?) -> Void + var timeoutWorkItem: DispatchWorkItem! + var validationTimer: Timer! + + init(context: PeerConnectionContext, + timeout: Int, + handler: @escaping (ConnectionError?) -> Void) { + self.context = context + self.deadline = .now() + .seconds(timeout) + self.handler = handler + } + + func run() { + guard state == .stop else { return } + + state = .monitoring + + timeoutWorkItem = DispatchWorkItem { + if self.state == .monitoring { + self.terminate(error: ConnectionError.connectionWaitTimeout) + } + } + DispatchQueue.global().asyncAfter(deadline: self.deadline, + execute: timeoutWorkItem) + + validationTimer = Timer(timeInterval: 1.0, repeats: true) { timer in + self.validate() + } + RunLoop.main.add(validationTimer, forMode: .commonModes) + } + + func terminate(error: ConnectionError? = nil) { + guard state == .monitoring else { return } + + self.error = error + state = .terminating + validate() + } + + func validate() { + context.eventLog?.markFormat(type: .WebSocket, + format: "validate connection state") + + guard state == .terminating else { return } + + switch context.webSocketReadyState { + case nil, SRReadyState.CLOSED?: + break + default: + return + } + + switch context.nativeSignalingState { + case nil, RTCSignalingState.closed?: + break + default: + return + } + + switch context.nativeICEConnectionState { + case nil, RTCIceConnectionState.closed?: + break + default: + return + } + + timeoutWorkItem.cancel() + validationTimer.invalidate() + state = .terminated + handler(error) + } + + func completeConnection() { + timeoutWorkItem?.cancel() + } + +} + +class PeerConnectionContext: NSObject, SRWebSocketDelegate, RTCPeerConnectionDelegate { + + enum State: String { + case signalingConnecting + case signalingConnected + case peerConnectionReady + case peerConnectionOffered + case peerConnectionAnswering + case peerConnectionAnswered + case peerConnectionConnecting + case updateOffered + case connected + case disconnecting + case disconnected + case terminated + } + + weak var peerConnection: PeerConnection? + var role: MediaStreamRole + + private var _state: State = .disconnected + + var state: State { + get { + if peerConnection == nil { + return .terminated + } else { + return _state + } + } + set { + _state = newValue + switch newValue { + case .connected: + peerConnection?.state = .connected + case .disconnecting: + peerConnection?.state = .disconnecting + case .disconnected: + peerConnection?.state = .disconnected + default: + peerConnection?.state = .connecting + } + } + } + + var webSocket: SRWebSocket? + var nativePeerConnection: RTCPeerConnection? + + // 内容はそれぞれ SRWebSocket, RTCPeerConnection のプロパティと同じだが、 + // こちらはデリゲートの呼び出し時にセットする。 + // SRWebSocket, RTCPeerConnection の状態に関するプロパティは + // デリゲートの呼び出し前に変更されるので、 + // プロパティの監視で接続解除を判断すると終了処理を適切に行えない + var webSocketReadyState: SRReadyState? + var nativeSignalingState: RTCSignalingState? + var nativeICEConnectionState: RTCIceConnectionState? + + var upstream: RTCMediaStream? + var mediaCapturer: MediaCapturer? + var monitor: ConnectionMonitor? + + var connection: Connection! { + get { return peerConnection?.connection } + } + + var eventLog: EventLog? { + get { return connection?.eventLog } + } + + var mediaConnection: MediaConnection! { + get { return peerConnection?.mediaConnection } + } + + private var timeoutTimer: Timer? + + private var connectCompletionHandler: ((ConnectionError?) -> Void)? + private var disconnectCompletionHandler: ((ConnectionError?) -> Void)? + + init(peerConnection: PeerConnection, role: MediaStreamRole) { + self.peerConnection = peerConnection + self.role = role + super.init() + } + + // MARK: ピア接続 + + func connect(timeout: Int, handler: @escaping ((ConnectionError?) -> Void)) { + if state != .disconnected { + handler(ConnectionError.connectionBusy) + return + } + + eventLog?.markFormat(type: .WebSocket, + format: String(format: "open %@", + connection!.URL.description)) + state = .signalingConnecting + connectCompletionHandler = handler + + monitor = ConnectionMonitor(context: self, timeout: timeout) { error in + self.finishTermination(error: error) + } + monitor!.run() + webSocket = SRWebSocket(url: connection!.URL) + webSocket!.delegate = self + webSocket!.open() + webSocketReadyState = SRReadyState.CONNECTING + } + + func disconnect(handler: @escaping ((ConnectionError?) -> Void)) { + switch state { + case .disconnected: + handler(ConnectionError.connectionDisconnected) + case .signalingConnected, .connected: + disconnectCompletionHandler = handler + terminate() + default: + handler(ConnectionError.connectionBusy) + } + } + + // MARK: 終了処理 + + func terminate(error: ConnectionError? = nil) { + switch state { + case .disconnecting, .disconnected: + break + + default: + eventLog?.markFormat(type: .Signaling, + format: "begin terminate all connections") + state = .disconnecting + nativePeerConnection?.close() + webSocket?.close() + monitor!.terminate(error: error) + } + } + + func terminateByPeerConnection(error: Error) { + peerConnectionEventHandlers? + .onFailureHandler?(nativePeerConnection!, error) + terminate(error: ConnectionError.peerConnectionError(error)) + } + + func finishTermination(error: ConnectionError?) { + eventLog?.markFormat(type: .PeerConnection, + format: "finish termination") + + monitor = nil + if nativePeerConnection != nil { + peerConnectionEventHandlers?.onDisconnectHandler?(nativePeerConnection!) + } + + // この順にクリアしないと落ちる + mediaCapturer = nil + if nativePeerConnection != nil { + nativePeerConnection!.delegate = nil + } + nativePeerConnection = nil + webSocket?.delegate = nil + webSocket = nil + + state = .disconnected + if let error = error { + signalingEventHandlers?.onFailureHandler?(error) + mediaConnection?.callOnFailureHandler(error) + } + signalingEventHandlers?.onDisconnectHandler?() + connectCompletionHandler?(error) + connectCompletionHandler = nil + disconnectCompletionHandler?(error) + disconnectCompletionHandler = nil + mediaConnection?.callOnDisconnectHandler(error) + peerConnection?.terminate() + peerConnection = nil + + webSocketReadyState = nil + nativeSignalingState = nil + nativeICEConnectionState = nil + } + + func send(_ message: Messageable) -> ConnectionError? { + switch state { + case .disconnected, .terminated: + eventLog?.markFormat(type: .WebSocket, + format: "failed sending message (connection disconnected)") + return ConnectionError.connectionDisconnected + + case .signalingConnecting, .disconnecting: + eventLog?.markFormat(type: .WebSocket, + format: "failed sending message (connection busy)") + return ConnectionError.connectionBusy + + default: + let message = message.message() + eventLog?.markFormat(type: .WebSocket, + format: "send message (state %@): %@", + arguments: state.rawValue, message.description) + let s = message.JSONRepresentation() + eventLog?.markFormat(type: .WebSocket, + format: "send message as JSON: %@", + arguments: s) + webSocket!.send(message.JSONRepresentation()) + return nil + } + } + + // MARK: SRWebSocketDelegate + + var webSocketEventHandlers: WebSocketEventHandlers? { + get { return mediaConnection?.webSocketEventHandlers } + } + + var signalingEventHandlers: SignalingEventHandlers? { + get { return mediaConnection?.signalingEventHandlers } + } + + var peerConnectionEventHandlers: PeerConnectionEventHandlers? { + get { return mediaConnection?.peerConnectionEventHandlers } + } + + func webSocketDidOpen(_ webSocket: SRWebSocket!) { + eventLog?.markFormat(type: .WebSocket, format: "opened") + eventLog?.markFormat(type: .Signaling, format: "connected") + + webSocketReadyState = SRReadyState.OPEN + switch state { + case .disconnecting, .disconnected, .terminated: + break + + case .signalingConnecting: + webSocketEventHandlers?.onOpenHandler?(webSocket) + state = .signalingConnected + signalingEventHandlers?.onConnectHandler?() + + // ピア接続オブジェクトを生成する + eventLog?.markFormat(type: .PeerConnection, + format: "create peer connection") + nativePeerConnection = PeerConnection.nativeFactory + .peerConnection( + with: peerConnection!.mediaOption.configuration, + constraints: peerConnection!.mediaOption + .peerConnectionMediaConstraints, + delegate: self) + + // デバイスの初期化 (Upstream) + if role == MediaStreamRole.upstream { + if let error = createMediaCapturer() { + terminate(error: error) + return + } + } + + // シグナリング connect を送信する + let connect = SignalingConnect(role: SignalingRole.from(role), + channel_id: connection.mediaChannelId, + multistream: mediaConnection.multistreamEnabled, + mediaOption: peerConnection!.mediaOption) + eventLog?.markFormat(type: .Signaling, + format: "send connect message: %@", + arguments: connect.message().JSON().description) + if let error = send(connect) { + eventLog?.markFormat(type: .Signaling, + format: "send connect message failed: %@", + arguments: error.localizedDescription) + signalingEventHandlers?.onFailureHandler?(error) + terminate(error: ConnectionError.connectionTerminated) + return + } + state = .peerConnectionReady + + default: + eventLog?.markFormat(type: .Signaling, + format: "WebSocket opened in invalid state") + terminate(error: ConnectionError.connectionTerminated) + } + } + + // 同一の RTCPeerConnectionFactory に対して MediaCapturer を再利用する + // MediaCapturer を複数回生成すると落ちる可能性がある + static var sharedMediaCapturers: [RTCPeerConnectionFactory: MediaCapturer] = [:] + + func createMediaCapturer() -> ConnectionError? { + eventLog?.markFormat(type: .PeerConnection, format: "create media capturer") + if let shared = PeerConnectionContext + .sharedMediaCapturers[PeerConnection.nativeFactory] { + eventLog?.markFormat(type: .PeerConnection, + format: "use shared media capturer") + mediaCapturer = shared + } else { + mediaCapturer = MediaCapturer( + factory: PeerConnection.nativeFactory, + mediaOption: peerConnection!.mediaOption) + if mediaCapturer == nil { + eventLog?.markFormat(type: .PeerConnection, + format: "create media capturer failed") + return ConnectionError.mediaCapturerFailed + } + PeerConnectionContext + .sharedMediaCapturers[PeerConnection.nativeFactory] = mediaCapturer + } + + eventLog?.markFormat(type: .PeerConnection, + format: "video capturer track ID: %@", + arguments: mediaCapturer!.videoCaptureTrack.trackId) + eventLog?.markFormat(type: .PeerConnection, + format: "audio capturer track ID: %@", + arguments: mediaCapturer!.audioCaptureTrack.trackId) + + let upstream = PeerConnection.nativeFactory.mediaStream(withStreamId: + peerConnection!.mediaStreamId ?? MediaStream.defaultStreamId) + if peerConnection!.mediaOption.videoEnabled { + upstream.addVideoTrack(mediaCapturer!.videoCaptureTrack) + } + if peerConnection!.mediaOption.audioEnabled { + upstream.addAudioTrack(mediaCapturer!.audioCaptureTrack) + } + + nativePeerConnection!.add(upstream) + let wrap = MediaStream(peerConnection: peerConnection!, + nativeMediaStream: upstream) + mediaConnection?.addMediaStream(wrap) + return nil + } + + public func webSocket(_ webSocket: SRWebSocket!, + didCloseWithCode code: Int, + reason: String?, + wasClean: Bool) { + webSocketReadyState = SRReadyState.CLOSED + + if let reason = reason { + eventLog?.markFormat(type: .WebSocket, + format: "close: code \(code), reason %@, clean \(wasClean)", + arguments: reason) + } else { + eventLog?.markFormat(type: .WebSocket, + format: "close: code \(code), clean \(wasClean)") + } + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + var error: ConnectionError? = nil + if code != SRStatusCodeNormal.rawValue { + if code == StatusCode.signalingFailure.rawValue { + let reason = reason ?? "Unknown reason" + error = ConnectionError.signalingFailure(reason: reason) + } else { + error = ConnectionError.webSocketClose(code, reason) + } + } + + webSocketEventHandlers?.onCloseHandler?(webSocket, code, reason, wasClean) + terminate(error: error) + } + } + + func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) { + eventLog?.markFormat(type: .WebSocket, + format: "fail: %@", + arguments: error.localizedDescription) + + webSocketReadyState = SRReadyState.CLOSED + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + let error = ConnectionError.webSocketError(error) + webSocketEventHandlers?.onFailureHandler?(webSocket, error) + terminate(error: error) + } + } + + func webSocket(_ webSocket: SRWebSocket!, didReceivePong pongPayload: Data!) { + eventLog?.markFormat(type: .WebSocket, + format: "received pong: %@", + arguments: pongPayload.description) + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + webSocketEventHandlers?.onPongHandler?(webSocket, pongPayload) + } + } + + func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) { + eventLog?.markFormat(type: .WebSocket, + format: "received message: %@", + arguments: (message as AnyObject).description) + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + webSocketEventHandlers?.onMessageHandler?(webSocket, message as AnyObject) + if let message = Message.fromJSONData(message) { + signalingEventHandlers?.onReceiveHandler?(message) + eventLog?.markFormat(type: .Signaling, + format: "signaling message type: %@", + arguments: message.type?.rawValue ?? "") + + let json = message.JSON() + switch message.type { + case .ping?: + receiveSignalingPing() + + case .notify?: + receiveSignalingNotify(json: json) + + case .offer?: + receiveSignalingOffer(json: json) + + case .update?: + receiveSignalingUpdate(json) + + default: + return + } + } + } + } + + func receiveSignalingPing() { + eventLog?.markFormat(type: .Signaling, format: "received ping") + + switch state { + case .connected: + signalingEventHandlers?.onPingHandler?() + if let error = self.send(SignalingPong()) { + mediaConnection?.callOnFailureHandler(error) + } + + default: + break + } + } + + func receiveSignalingNotify(json: [String: Any]) { + switch state { + case .connected: + eventLog?.markFormat(type: .Signaling, format: "received notify") + + var notify: SignalingNotify! + do { + notify = Optional.some(try unbox(dictionary: json)) + } catch { + eventLog?.markFormat(type: .Signaling, + format: "failed parsing notify: %@", + arguments: json.description) + } + eventLog?.markFormat(type: .Signaling, + format: "notify: %@", + arguments: json.description) + + connection.numberOfConnections = + (notify.numberOfUpstreamConnections, + notify.numberOfDownstreamConnections) + + default: + break + } + } + + func receiveSignalingOffer(json: [String: Any]) { + switch state { + case .peerConnectionReady: + eventLog?.markFormat(type: .Signaling, format: "received offer") + let offer: SignalingOffer! + do { + offer = Optional.some(try unbox(dictionary: json)) + } catch { + eventLog?.markFormat(type: .Signaling, + format: "parsing offer failed") + return + } + + peerConnection!.clientId = offer.client_id + + if let config = offer.config { + eventLog?.markFormat(type: .Signaling, + format: "configure ICE transport policy") + let peerConfig = RTCConfiguration() + switch config.iceTransportPolicy { + case "relay": + peerConfig.iceTransportPolicy = .relay + default: + eventLog?.markFormat(type: .Signaling, + format: "unsupported iceTransportPolicy %@", + arguments: config.iceTransportPolicy) + return + } + + eventLog?.markFormat(type: .Signaling, format: "configure ICE servers") + for serverConfig in config.iceServers { + let server = RTCIceServer(urlStrings: serverConfig.urls, + username: serverConfig.username, + credential: serverConfig.credential) + peerConfig.iceServers = [server] + } + + if !nativePeerConnection!.setConfiguration(peerConfig) { + eventLog?.markFormat(type: .Signaling, + format: "cannot configure peer connection") + terminate(error: ConnectionError + .failureSetConfiguration(peerConfig)) + return + } + } + + createAndSendAnswer(sdp: offer.sessionDescription()) + + default: + eventLog?.markFormat(type: .Signaling, + format: "offer: invalid state %@", + arguments: state.rawValue) + terminate(error: ConnectionError.connectionTerminated) + } + } + + func createAndSendAnswer(sdp: RTCSessionDescription) { + state = .peerConnectionOffered + eventLog?.markFormat(type: .Signaling, + format: "set remote description") + nativePeerConnection!.setRemoteDescription(sdp) { + error in + if let error = error { + self.eventLog?.markFormat(type: .Signaling, + format: "set remote description failed") + self.terminateByPeerConnection(error: error) + return + } + + self.eventLog?.markFormat(type: .Signaling, + format: "create answer") + self.nativePeerConnection!.answer(for: self + .peerConnection!.mediaOption.signalingAnswerMediaConstraints) + { + (sdp, error) in + if let error = error { + self.eventLog?.markFormat(type: .Signaling, + format: "creating answer failed") + self.terminateByPeerConnection(error: error) + return + } + self.eventLog?.markFormat(type: .Signaling, + format: "generated answer: %@", + arguments: sdp!) + self.nativePeerConnection!.setLocalDescription(sdp!) { + error in + if let error = error { + self.eventLog?.markFormat(type: .Signaling, + format: "set local description failed") + self.peerConnectionEventHandlers? + .onFailureHandler?(self.nativePeerConnection!, error) + self.terminate(error: ConnectionError.peerConnectionError(error)) + return + } + + self.eventLog?.markFormat(type: .Signaling, + format: "send answer") + let answer = SignalingAnswer(sdp: sdp!.sdp).message() + if let error = self.send(answer) { + self.terminate(error: ConnectionError.peerConnectionError(error)) + return + } + + self.state = .peerConnectionAnswered + } + } + } + } + + func receiveSignalingUpdate(_ json: [String: Any]) { + switch state { + case .connected: + eventLog?.markFormat(type: .Signaling, format: "received 'update'", + arguments: json.description) + if !mediaConnection.multistreamEnabled { + eventLog?.markFormat(type: .Signaling, + format: "ignore 'update' in single stream mode") + return + } + + let update: SignalingUpdateOffer! + do { + update = Optional.some(try unbox(dictionary: json)) + } catch { + eventLog?.markFormat(type: .Signaling, + format: "parsing 'update' failed") + return + } + + createAndSendUpdateAnswer(sdp: update.sessionDescription()) + + default: + return + } + } + + + func createAndSendUpdateAnswer(sdp: RTCSessionDescription) { + state = .updateOffered + eventLog?.markFormat(type: .Signaling, + format: "set remote description to update-offer") + nativePeerConnection!.setRemoteDescription(sdp) { + error in + if let error = error { + self.eventLog?.markFormat(type: .Signaling, + format: "set remote description to update-offer failed") + self.terminateUpdate(error) + return + } + + self.eventLog?.markFormat(type: .Signaling, + format: "create update-answer") + self.nativePeerConnection!.answer(for: self + .peerConnection!.mediaOption.signalingAnswerMediaConstraints) + { + (sdp, error) in + if let error = error { + self.eventLog?.markFormat(type: .Signaling, + format: "creating update-answer failed") + self.terminateUpdate(error) + return + } + self.eventLog?.markFormat(type: .Signaling, + format: "generated update-answer: %@", + arguments: sdp!) + self.nativePeerConnection!.setLocalDescription(sdp!) { + error in + if let error = error { + self.eventLog?.markFormat(type: .Signaling, + format: "set local description to update-answer failed") + self.terminateUpdate(error) + return + } + + self.eventLog?.markFormat(type: .Signaling, + format: "send update-answer") + let answer: Message! + answer = SignalingUpdateAnswer(sdp: sdp!.sdp).message() + if let error = self.send(answer) { + self.terminateUpdate(error) + return + } + + // Answer 送信後に RTCPeerConnection の状態に変化はない + // (デリゲートのメソッドが呼ばれない) ため、 + // Answer を送信したら接続完了とみなす + self.state = .connected + } + } + } + } + + // マルチストリームのシグナリングのエラー + func terminateUpdate(_ error: Error) { + state = .connected + let connError = ConnectionError.peerConnectionError(error) + let updateError = ConnectionError.updateError(connError) + peerConnectionEventHandlers? + .onFailureHandler?(nativePeerConnection!, updateError) + mediaConnection?.callOnFailureHandler(updateError) + } + + // MARK: RTCPeerConnectionDelegate + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didChange stateChanged: RTCSignalingState) { + eventLog?.markFormat(type: .PeerConnection, + format: "signaling state changed: %@", + arguments: stateChanged.description) + + nativeSignalingState = stateChanged + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers?.onChangeSignalingStateHandler?( + nativePeerConnection, stateChanged) + switch stateChanged { + case .closed: + terminate(error: ConnectionError.connectionTerminated) + default: + break + } + } + } + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didAdd stream: RTCMediaStream) { + eventLog?.markFormat(type: .PeerConnection, + format: "added stream '%@'", + arguments: stream.streamId) + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + guard peerConnection != nil + && peerConnection!.mediaConnection != nil else + { + return + } + + if peerConnection!.mediaConnection!.hasMediaStream(stream.streamId) { + eventLog?.markFormat(type: .PeerConnection, + format: "stream '%@' already exists", + arguments: stream.streamId) + return + } + + peerConnectionEventHandlers?.onAddStreamHandler?(nativePeerConnection, stream) + nativePeerConnection.add(stream) + let wrap = MediaStream(peerConnection: peerConnection!, + nativeMediaStream: stream) + peerConnection!.mediaConnection!.addMediaStream(wrap) + + } + } + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didRemove stream: RTCMediaStream) { + eventLog?.markFormat(type: .PeerConnection, format: "removed stream") + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers? + .onRemoveStreamHandler?(nativePeerConnection, stream) + nativePeerConnection.remove(stream) + peerConnection?.mediaConnection?.removeMediaStream(stream.streamId) + } + } + + func peerConnectionShouldNegotiate(_ nativePeerConnection: RTCPeerConnection) { + eventLog?.markFormat(type: .PeerConnection, format: "should negatiate") + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers?.onNegotiateHandler?(nativePeerConnection) + } + } + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didChange newState: RTCIceConnectionState) { + eventLog?.markFormat(type: .PeerConnection, + format: "ICE connection state changed: %@", + arguments: newState.description) + + nativeICEConnectionState = newState + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers? + .onChangeIceConnectionState?(nativePeerConnection, newState) + switch newState { + case .connected: + switch state { + case .peerConnectionAnswered: + eventLog?.markFormat(type: .PeerConnection, + format: "remote peer connected", + arguments: newState.description) + finishConnection() + + default: + eventLog?.markFormat(type: .PeerConnection, + format: "ICE connection completed but invalid state %@", + arguments: newState.description) + terminate(error: ConnectionError.iceConnectionFailed) + } + + case .closed, .disconnected: + terminate(error: ConnectionError.iceConnectionDisconnected) + + case .failed: + let error = ConnectionError.iceConnectionFailed + mediaConnection?.callOnFailureHandler(error) + terminate(error: error) + + default: + break + } + } + } + + func finishConnection() { + eventLog?.markFormat(type: .PeerConnection, + format: "finish connection") + + if mediaConnection!.mediaStreams.isEmpty { + eventLog?.markFormat(type: .PeerConnection, + format: "media stream is not found") + terminate(error: .mediaStreamNotFound) + return + } + + monitor!.completeConnection() + state = .connected + if nativePeerConnection != nil { + peerConnectionEventHandlers?.onConnectHandler?(nativePeerConnection!) + } + connectCompletionHandler?(nil) + connectCompletionHandler = nil + } + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didChange newState: RTCIceGatheringState) { + eventLog?.markFormat(type: .PeerConnection, + format: "ICE gathering state changed: %@", + arguments: newState.description) + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers? + .onChangeIceGatheringStateHandler?(nativePeerConnection, newState) + } + } + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didGenerate candidate: RTCIceCandidate) { + eventLog?.markFormat(type: .PeerConnection, + format: "candidate generated: %@", + arguments: candidate.sdp) + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers? + .onGenerateIceCandidateHandler?(nativePeerConnection, candidate) + if let error = send(SignalingICECandidate(candidate: candidate.sdp)) { + eventLog?.markFormat(type: .PeerConnection, + format: "send candidate to server failed") + terminate(error: error) + } + } + } + + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didRemove candidates: [RTCIceCandidate]) { + eventLog?.markFormat(type: .PeerConnection, + format: "candidates %d removed", + arguments: candidates.count) + + switch state { + case .disconnecting, .disconnected, .terminated: + break + + default: + peerConnectionEventHandlers? + .onRemoveCandidatesHandler?(nativePeerConnection, candidates) + } + } + + // NOTE: Sora はデータチャネルに非対応 + func peerConnection(_ nativePeerConnection: RTCPeerConnection, + didOpen dataChannel: RTCDataChannel) { + eventLog?.markFormat(type: .PeerConnection, + format: + "data channel opened (Sora does not support data channels") + } + +} diff --git a/Sora/Sora/RTCExtensions.swift b/Sora/Sora/RTCExtensions.swift new file mode 100644 index 00000000..1a175548 --- /dev/null +++ b/Sora/Sora/RTCExtensions.swift @@ -0,0 +1,52 @@ +import Foundation +import WebRTC + +extension RTCSignalingState: CustomStringConvertible { + + public var description: String { + get { + switch self { + case .stable: return "stable" + case .haveLocalOffer: return "haveLocalOffer" + case .haveLocalPrAnswer: return "haveLocalPrAnswer" + case .haveRemoteOffer: return "haveRemoteOffer" + case .haveRemotePrAnswer: return "haveRemotePrAnswer" + case .closed: return "closed" + } + } + } + +} + +extension RTCIceConnectionState: CustomStringConvertible { + + public var description: String { + get { + switch self { + case .new: return "new" + case .checking: return "checking" + case .connected: return "connected" + case .completed: return "completed" + case .failed: return "failed" + case .disconnected: return "disconnected" + case .closed: return "closed" + case .count: return "count" + } + } + } + +} + +extension RTCIceGatheringState: CustomStringConvertible { + + public var description: String { + get { + switch self { + case .new: return "new" + case .gathering: return "gathering" + case .complete: return "complete" + } + } + } + +} diff --git a/Sora/Sora/Sora.h b/Sora/Sora/Sora.h new file mode 100644 index 00000000..be6d59c4 --- /dev/null +++ b/Sora/Sora/Sora.h @@ -0,0 +1,11 @@ +#import + +//! Project version number for Sora. +FOUNDATION_EXPORT double SoraVersionNumber; + +//! Project version string for Sora. +FOUNDATION_EXPORT const unsigned char SoraVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Sora/Sora/VideoFrame.swift b/Sora/Sora/VideoFrame.swift new file mode 100644 index 00000000..3680ac8c --- /dev/null +++ b/Sora/Sora/VideoFrame.swift @@ -0,0 +1,42 @@ +import Foundation +import CoreMedia +import WebRTC + +public enum VideoFrameHandle { + case webRTC(RTCVideoFrame) +} + +public protocol VideoFrame { + + var videoFrameHandle: VideoFrameHandle? { get } + var width: Int { get } + var height: Int { get } + var timestamp: CMTime { get } + +} + +struct RemoteVideoFrame: VideoFrame { + + var nativeVideoFrame: RTCVideoFrame + + var videoFrameHandle: VideoFrameHandle? { + get { return VideoFrameHandle.webRTC(nativeVideoFrame) } + } + + var width: Int { + get { return nativeVideoFrame.width } + } + + var height: Int { + get { return nativeVideoFrame.height } + } + + var timestamp: CMTime { + get { return CMTimeMake(nativeVideoFrame.timeStampNs, 1000000000) } + } + + init(nativeVideoFrame: RTCVideoFrame) { + self.nativeVideoFrame = nativeVideoFrame + } + +} diff --git a/Sora/Sora/VideoRenderer.swift b/Sora/Sora/VideoRenderer.swift new file mode 100644 index 00000000..cc828cce --- /dev/null +++ b/Sora/Sora/VideoRenderer.swift @@ -0,0 +1,44 @@ +import Foundation +import WebRTC + +public protocol VideoRenderer { + + func onChangedSize(_ size: CGSize) + func renderVideoFrame(_ videoFrame: VideoFrame?) + +} + +class VideoRendererAdapter: NSObject, RTCVideoRenderer { + + weak var connection: Connection? + var videoRenderer: VideoRenderer + + var eventLog: EventLog? { + get { return connection?.eventLog } + } + + init(videoRenderer: VideoRenderer) { + self.videoRenderer = videoRenderer + } + + func setSize(_ size: CGSize) { + eventLog?.markFormat(type: .VideoRenderer, + format: "set size %@ for %@", + arguments: size.debugDescription, self) + DispatchQueue.main.async { + self.videoRenderer.onChangedSize(size) + } + } + + func renderFrame(_ frame: RTCVideoFrame?) { + DispatchQueue.main.async { + if let frame = frame { + let frame = RemoteVideoFrame(nativeVideoFrame: frame) + self.videoRenderer.renderVideoFrame(frame) + } else { + self.videoRenderer.renderVideoFrame(nil) + } + } + } + +} diff --git a/Sora/Sora/VideoView.swift b/Sora/Sora/VideoView.swift new file mode 100644 index 00000000..ac5da76e --- /dev/null +++ b/Sora/Sora/VideoView.swift @@ -0,0 +1,132 @@ +import UIKit +import WebRTC + +public class VideoView: UIView, VideoRenderer { + + // キーウィンドウ外で RTCEAGLVideoView を生成すると次のエラーが発生するため、 + // contentView を Nib ファイルでセットせずに遅延プロパティで初期化する + // "Failed to bind EAGLDrawable: to GL_RENDERBUFFER 1" + // ただし、このエラーは無視しても以降の描画に問題はなく、クラッシュもしない + // また、遅延プロパティでもキーウィンドウ外で初期化すれば + // エラーが発生するため、根本的な解決策ではないので注意 + lazy var contentView: VideoViewContentView! = { + guard let topLevel = Bundle(for: VideoView.self) + .loadNibNamed("VideoView", owner: self, options: nil) else + { + assertionFailure("cannot load VideoView's nib file") + return nil + } + + let view: VideoViewContentView = topLevel[0] as! VideoViewContentView + view.frame = self.bounds + self.addSubview(view) + if view.allowsRender { + view.setNeedsDisplay() + } + return view + }() + + override public init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + } + + public func onChangedSize(_ size: CGSize) { + contentView.onChangedSize(size) + } + + public func renderVideoFrame(_ frame: VideoFrame?) { + contentView.renderVideoFrame(frame) + } + +} + +class VideoViewContentView: UIView, VideoRenderer { + + @IBOutlet weak var nativeVideoView: RTCEAGLVideoView! + + var allowsRender: Bool { + get { + // 前述のエラーはキーウィンドウ外での描画でも発生するので、 + // ビューがキーウィンドウに表示されている場合のみ描画を許可する + return !(isHidden || window == nil || !window!.isKeyWindow) + } + } + + var sizeToChange: CGSize? + + public func onChangedSize(_ size: CGSize) { + // ここも前述のエラーと同様の理由で処理を後回しにする + if allowsRender { + setRemoteVideoViewSize(size) + } else { + sizeToChange = size + } + } + + func setRemoteVideoViewSize(_ size: CGSize) { + nativeVideoView.setSize(size) + sizeToChange = nil + + // 映像の解像度のアスペクト比に合わせて + // RTCEAGLVideoView のサイズと位置を変更する + let adjustSize = fitSize(from: size, to: frame.size) + nativeVideoView.frame = + CGRect(x: (frame.size.width - adjustSize.width) / 2, + y: (frame.size.height - adjustSize.height) / 2, + width: adjustSize.width, + height: adjustSize.height) + setNeedsDisplay() + } + + public func renderVideoFrame(_ frame: VideoFrame?) { + guard allowsRender else { return } + updateSize() + + if let frame = frame { + if let handle = frame.videoFrameHandle { + switch handle { + case .webRTC(let frame): + nativeVideoView.renderFrame(frame) + } + } + } else { + nativeVideoView.renderFrame(nil) + } + } + + public override func draw(_ frame: CGRect) { + super.draw(frame) + nativeVideoView.draw(frame) + } + + public override func didMoveToWindow() { + // onChangedSize が呼ばれて RTCEAGLVideoView にサイズの変更がある場合、 + // このビューがウィンドウに表示されたタイミングでサイズの変更を行う + // これも前述のエラーを回避するため + updateSize() + } + + func updateSize() { + if let size = sizeToChange { + if allowsRender { + setRemoteVideoViewSize(size) + } + } + } + +} + +func fitSize(from: CGSize, to: CGSize) -> CGSize { + let baseW = CGSize(width: to.width, + height: to.width * (from.height / from.width)) + let baseH = CGSize(width: to.height * (from.width / from.height), + height: to.height) + return ([baseW, baseH].first { + size in + return size.width <= to.width && size.height <= to.height + })! +} diff --git a/Sora/Sora/VideoView.xib b/Sora/Sora/VideoView.xib new file mode 100644 index 00000000..6de7ca74 --- /dev/null +++ b/Sora/Sora/VideoView.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sora/SoraTests/Info.plist b/Sora/SoraTests/Info.plist new file mode 100644 index 00000000..ba72822e --- /dev/null +++ b/Sora/SoraTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Sora/SoraTests/SoraTests.swift b/Sora/SoraTests/SoraTests.swift new file mode 100644 index 00000000..061aa76e --- /dev/null +++ b/Sora/SoraTests/SoraTests.swift @@ -0,0 +1,24 @@ +import XCTest +import WebRTC +@testable import Sora + +class SoraTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Sora/jazzy.yaml b/Sora/jazzy.yaml new file mode 100644 index 00000000..5bf8b441 --- /dev/null +++ b/Sora/jazzy.yaml @@ -0,0 +1,15 @@ +module: Sora +module_version: 1.0 +author: Shiguredo Inc. +author_url: https://shiguredo.jp/ +github_url: https://github.com/shiguredo/sora-ios-sdk +swift_version: 2.2 +theme: fullwidth +clean: true +output: doc +framework_root: . +umbrella_header: Sora/Sora.h +documentation: guides/*.md +readme: guides/README.md +skip_undocumented: true +hide_documentation_coverage: true diff --git a/examples/SoraApp/Cartfile b/examples/SoraApp/Cartfile new file mode 100644 index 00000000..2c9b88b5 --- /dev/null +++ b/examples/SoraApp/Cartfile @@ -0,0 +1,4 @@ +github "shiguredo/sora-ios-sdk" "1.0.0" +github "shiguredo/sora-webrtc-ios" "57.0" +github "facebook/SocketRocket" "0.4.2" +github "johnsundell/unbox" "2.2.1" diff --git a/examples/SoraApp/SoraApp.xcodeproj/project.pbxproj b/examples/SoraApp/SoraApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ffd05a3f --- /dev/null +++ b/examples/SoraApp/SoraApp.xcodeproj/project.pbxproj @@ -0,0 +1,426 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 91389BBF1DF488E700E7DD73 /* Sora.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BBE1DF488E700E7DD73 /* Sora.framework */; }; + 91389BC41DF4899F00E7DD73 /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC11DF4899F00E7DD73 /* Unbox.framework */; }; + 91389BC51DF4899F00E7DD73 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC21DF4899F00E7DD73 /* WebRTC.framework */; }; + 91389BC61DF4899F00E7DD73 /* SocketRocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC31DF4899F00E7DD73 /* SocketRocket.framework */; }; + 91B89B891DFF033100BF435F /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B881DFF033100BF435F /* AudioToolbox.framework */; }; + 91B89B8B1DFF033700BF435F /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B8A1DFF033700BF435F /* AVFoundation.framework */; }; + 91B89B8D1DFF033D00BF435F /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B8C1DFF033D00BF435F /* AVKit.framework */; }; + 91B89B8F1DFF034400BF435F /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B8E1DFF034400BF435F /* CFNetwork.framework */; }; + 91B89B911DFF034900BF435F /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B901DFF034900BF435F /* CoreAudio.framework */; }; + 91B89B931DFF034D00BF435F /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B921DFF034D00BF435F /* CoreMedia.framework */; }; + 91B89B951DFF035100BF435F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B941DFF035100BF435F /* Foundation.framework */; }; + 91B89B971DFF035500BF435F /* MediaToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B961DFF035500BF435F /* MediaToolbox.framework */; }; + 91B89B991DFF035900BF435F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B981DFF035900BF435F /* Security.framework */; }; + 91B89B9B1DFF035F00BF435F /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B9A1DFF035F00BF435F /* VideoToolbox.framework */; }; + 91B89B9D1DFF036400BF435F /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B9C1DFF036400BF435F /* libc++.tbd */; }; + 91B89B9F1DFF036A00BF435F /* libstdc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89B9E1DFF036A00BF435F /* libstdc++.tbd */; }; + 91B89BA11DFF038C00BF435F /* libicucore.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B89BA01DFF038C00BF435F /* libicucore.tbd */; }; + 91B89BA21DFF03BB00BF435F /* Unbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC11DF4899F00E7DD73 /* Unbox.framework */; }; + 91B89BA31DFF03BB00BF435F /* Unbox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC11DF4899F00E7DD73 /* Unbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 91B89BA41DFF03BB00BF435F /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC21DF4899F00E7DD73 /* WebRTC.framework */; }; + 91B89BA51DFF03BB00BF435F /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC21DF4899F00E7DD73 /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 91B89BA61DFF03BB00BF435F /* SocketRocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC31DF4899F00E7DD73 /* SocketRocket.framework */; }; + 91B89BA71DFF03BB00BF435F /* SocketRocket.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BC31DF4899F00E7DD73 /* SocketRocket.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 91B89BA81DFF03BB00BF435F /* Sora.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BBE1DF488E700E7DD73 /* Sora.framework */; }; + 91B89BA91DFF03BB00BF435F /* Sora.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 91389BBE1DF488E700E7DD73 /* Sora.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 91C093521DF1E9FB005B9A48 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C093511DF1E9FB005B9A48 /* AppDelegate.swift */; }; + 91C093541DF1E9FB005B9A48 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C093531DF1E9FB005B9A48 /* ViewController.swift */; }; + 91C093571DF1E9FB005B9A48 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 91C093551DF1E9FB005B9A48 /* Main.storyboard */; }; + 91C093591DF1E9FB005B9A48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 91C093581DF1E9FB005B9A48 /* Assets.xcassets */; }; + 91C0935C1DF1E9FB005B9A48 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 91C0935A1DF1E9FB005B9A48 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 91B89BAA1DFF03BB00BF435F /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 91B89BA31DFF03BB00BF435F /* Unbox.framework in Embed Frameworks */, + 91B89BA71DFF03BB00BF435F /* SocketRocket.framework in Embed Frameworks */, + 91B89BA51DFF03BB00BF435F /* WebRTC.framework in Embed Frameworks */, + 91B89BA91DFF03BB00BF435F /* Sora.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 91389BBE1DF488E700E7DD73 /* Sora.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sora.framework; path = Carthage/Build/iOS/Sora.framework; sourceTree = ""; }; + 91389BC11DF4899F00E7DD73 /* Unbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Unbox.framework; path = Carthage/Build/iOS/Unbox.framework; sourceTree = ""; }; + 91389BC21DF4899F00E7DD73 /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = Carthage/Build/iOS/WebRTC.framework; sourceTree = ""; }; + 91389BC31DF4899F00E7DD73 /* SocketRocket.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SocketRocket.framework; path = Carthage/Build/iOS/SocketRocket.framework; sourceTree = ""; }; + 91B89B881DFF033100BF435F /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + 91B89B8A1DFF033700BF435F /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + 91B89B8C1DFF033D00BF435F /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; + 91B89B8E1DFF034400BF435F /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 91B89B901DFF034900BF435F /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; }; + 91B89B921DFF034D00BF435F /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + 91B89B941DFF035100BF435F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 91B89B961DFF035500BF435F /* MediaToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaToolbox.framework; path = System/Library/Frameworks/MediaToolbox.framework; sourceTree = SDKROOT; }; + 91B89B981DFF035900BF435F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 91B89B9A1DFF035F00BF435F /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; + 91B89B9C1DFF036400BF435F /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; + 91B89B9E1DFF036A00BF435F /* libstdc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.tbd"; path = "usr/lib/libstdc++.tbd"; sourceTree = SDKROOT; }; + 91B89BA01DFF038C00BF435F /* libicucore.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libicucore.tbd; path = usr/lib/libicucore.tbd; sourceTree = SDKROOT; }; + 91C0934E1DF1E9FB005B9A48 /* SoraApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SoraApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 91C093511DF1E9FB005B9A48 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 91C093531DF1E9FB005B9A48 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 91C093561DF1E9FB005B9A48 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 91C093581DF1E9FB005B9A48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 91C0935B1DF1E9FB005B9A48 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 91C0935D1DF1E9FB005B9A48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 91C0934B1DF1E9FB005B9A48 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 91B89BA11DFF038C00BF435F /* libicucore.tbd in Frameworks */, + 91B89B9F1DFF036A00BF435F /* libstdc++.tbd in Frameworks */, + 91B89B9D1DFF036400BF435F /* libc++.tbd in Frameworks */, + 91B89B9B1DFF035F00BF435F /* VideoToolbox.framework in Frameworks */, + 91B89B991DFF035900BF435F /* Security.framework in Frameworks */, + 91B89BA81DFF03BB00BF435F /* Sora.framework in Frameworks */, + 91B89B971DFF035500BF435F /* MediaToolbox.framework in Frameworks */, + 91B89B951DFF035100BF435F /* Foundation.framework in Frameworks */, + 91B89B931DFF034D00BF435F /* CoreMedia.framework in Frameworks */, + 91B89B911DFF034900BF435F /* CoreAudio.framework in Frameworks */, + 91B89BA21DFF03BB00BF435F /* Unbox.framework in Frameworks */, + 91B89B8F1DFF034400BF435F /* CFNetwork.framework in Frameworks */, + 91B89B8D1DFF033D00BF435F /* AVKit.framework in Frameworks */, + 91B89BA41DFF03BB00BF435F /* WebRTC.framework in Frameworks */, + 91B89B8B1DFF033700BF435F /* AVFoundation.framework in Frameworks */, + 91B89B891DFF033100BF435F /* AudioToolbox.framework in Frameworks */, + 91389BBF1DF488E700E7DD73 /* Sora.framework in Frameworks */, + 91389BC41DF4899F00E7DD73 /* Unbox.framework in Frameworks */, + 91389BC51DF4899F00E7DD73 /* WebRTC.framework in Frameworks */, + 91389BC61DF4899F00E7DD73 /* SocketRocket.framework in Frameworks */, + 91B89BA61DFF03BB00BF435F /* SocketRocket.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 91389BC01DF488F200E7DD73 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 91B89BA01DFF038C00BF435F /* libicucore.tbd */, + 91B89B9E1DFF036A00BF435F /* libstdc++.tbd */, + 91B89B9C1DFF036400BF435F /* libc++.tbd */, + 91B89B9A1DFF035F00BF435F /* VideoToolbox.framework */, + 91B89B981DFF035900BF435F /* Security.framework */, + 91B89B961DFF035500BF435F /* MediaToolbox.framework */, + 91B89B941DFF035100BF435F /* Foundation.framework */, + 91B89B921DFF034D00BF435F /* CoreMedia.framework */, + 91B89B901DFF034900BF435F /* CoreAudio.framework */, + 91B89B8E1DFF034400BF435F /* CFNetwork.framework */, + 91B89B8C1DFF033D00BF435F /* AVKit.framework */, + 91B89B8A1DFF033700BF435F /* AVFoundation.framework */, + 91B89B881DFF033100BF435F /* AudioToolbox.framework */, + 91389BC11DF4899F00E7DD73 /* Unbox.framework */, + 91389BC21DF4899F00E7DD73 /* WebRTC.framework */, + 91389BC31DF4899F00E7DD73 /* SocketRocket.framework */, + 91389BBE1DF488E700E7DD73 /* Sora.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 91C093451DF1E9FB005B9A48 = { + isa = PBXGroup; + children = ( + 91C093501DF1E9FB005B9A48 /* SoraApp */, + 91389BC01DF488F200E7DD73 /* Frameworks */, + 91C0934F1DF1E9FB005B9A48 /* Products */, + ); + sourceTree = ""; + }; + 91C0934F1DF1E9FB005B9A48 /* Products */ = { + isa = PBXGroup; + children = ( + 91C0934E1DF1E9FB005B9A48 /* SoraApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 91C093501DF1E9FB005B9A48 /* SoraApp */ = { + isa = PBXGroup; + children = ( + 91C093511DF1E9FB005B9A48 /* AppDelegate.swift */, + 91C093531DF1E9FB005B9A48 /* ViewController.swift */, + 91C093551DF1E9FB005B9A48 /* Main.storyboard */, + 91C093581DF1E9FB005B9A48 /* Assets.xcassets */, + 91C0935A1DF1E9FB005B9A48 /* LaunchScreen.storyboard */, + 91C0935D1DF1E9FB005B9A48 /* Info.plist */, + ); + path = SoraApp; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 91C0934D1DF1E9FB005B9A48 /* SoraApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 91C093601DF1E9FB005B9A48 /* Build configuration list for PBXNativeTarget "SoraApp" */; + buildPhases = ( + 91C0934A1DF1E9FB005B9A48 /* Sources */, + 91C0934B1DF1E9FB005B9A48 /* Frameworks */, + 91C0934C1DF1E9FB005B9A48 /* Resources */, + 91B89BAA1DFF03BB00BF435F /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SoraApp; + productName = SoraApp; + productReference = 91C0934E1DF1E9FB005B9A48 /* SoraApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 91C093461DF1E9FB005B9A48 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0810; + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = "Shiguredo Inc."; + TargetAttributes = { + 91C0934D1DF1E9FB005B9A48 = { + CreatedOnToolsVersion = 8.1; + DevelopmentTeam = DQ232CBKHS; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 91C093491DF1E9FB005B9A48 /* Build configuration list for PBXProject "SoraApp" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 91C093451DF1E9FB005B9A48; + productRefGroup = 91C0934F1DF1E9FB005B9A48 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 91C0934D1DF1E9FB005B9A48 /* SoraApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 91C0934C1DF1E9FB005B9A48 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 91C0935C1DF1E9FB005B9A48 /* LaunchScreen.storyboard in Resources */, + 91C093591DF1E9FB005B9A48 /* Assets.xcassets in Resources */, + 91C093571DF1E9FB005B9A48 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 91C0934A1DF1E9FB005B9A48 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 91C093541DF1E9FB005B9A48 /* ViewController.swift in Sources */, + 91C093521DF1E9FB005B9A48 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 91C093551DF1E9FB005B9A48 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 91C093561DF1E9FB005B9A48 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 91C0935A1DF1E9FB005B9A48 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 91C0935B1DF1E9FB005B9A48 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 91C0935E1DF1E9FB005B9A48 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 91C0935F1DF1E9FB005B9A48 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 91C093611DF1E9FB005B9A48 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = DQ232CBKHS; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/SoraApp", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = SoraApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = jp.shiguredo.SoraApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 91C093621DF1E9FB005B9A48 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = DQ232CBKHS; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/SoraApp", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = SoraApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = jp.shiguredo.SoraApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 91C093491DF1E9FB005B9A48 /* Build configuration list for PBXProject "SoraApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 91C0935E1DF1E9FB005B9A48 /* Debug */, + 91C0935F1DF1E9FB005B9A48 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 91C093601DF1E9FB005B9A48 /* Build configuration list for PBXNativeTarget "SoraApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 91C093611DF1E9FB005B9A48 /* Debug */, + 91C093621DF1E9FB005B9A48 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 91C093461DF1E9FB005B9A48 /* Project object */; +} diff --git a/examples/SoraApp/SoraApp/AppDelegate.swift b/examples/SoraApp/SoraApp/AppDelegate.swift new file mode 100644 index 00000000..ecede7fe --- /dev/null +++ b/examples/SoraApp/SoraApp/AppDelegate.swift @@ -0,0 +1,38 @@ +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/examples/SoraApp/SoraApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/SoraApp/SoraApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..b8236c65 --- /dev/null +++ b/examples/SoraApp/SoraApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,48 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/SoraApp/SoraApp/Base.lproj/LaunchScreen.storyboard b/examples/SoraApp/SoraApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..fdf3f97d --- /dev/null +++ b/examples/SoraApp/SoraApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/SoraApp/SoraApp/Base.lproj/Main.storyboard b/examples/SoraApp/SoraApp/Base.lproj/Main.storyboard new file mode 100644 index 00000000..a0687d05 --- /dev/null +++ b/examples/SoraApp/SoraApp/Base.lproj/Main.storyboard @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/SoraApp/SoraApp/Info.plist b/examples/SoraApp/SoraApp/Info.plist new file mode 100644 index 00000000..aa17dac1 --- /dev/null +++ b/examples/SoraApp/SoraApp/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSCameraUsageDescription + Real-Time Communication with WebRTC + NSMicrophoneUsageDescription + Real-Time Communication with WebRTC + + diff --git a/examples/SoraApp/SoraApp/ViewController.swift b/examples/SoraApp/SoraApp/ViewController.swift new file mode 100644 index 00000000..6dc9fc44 --- /dev/null +++ b/examples/SoraApp/SoraApp/ViewController.swift @@ -0,0 +1,87 @@ +import UIKit +import Sora + +class ViewController: UIViewController { + + @IBOutlet weak var publisherVideoView: Sora.VideoView! + @IBOutlet weak var subscriberVideoView: Sora.VideoView! + @IBOutlet weak var connectButton: UIButton! + @IBOutlet weak var disconnectButton: UIButton! + + var connection: Sora.Connection! + + override func viewDidLoad() { + super.viewDidLoad() + connectButton.isEnabled = true + disconnectButton.isEnabled = false + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + @IBAction func connect(_ sender: AnyObject) { + connectButton.isEnabled = false + disconnectButton.isEnabled = false + + connection = Sora.Connection(URL: + URL(string: "ws://192.168.0.2:5000/signaling")!, + mediaChannelId: "soraapp") + connection.eventLog.debugMode = true + + connection.mediaPublisher.connect { + error in + if let error = error { + print(error.localizedDescription) + self.connectButton.isEnabled = true + return + } + self.connection.mediaSubscriber.connect { + error in + if let error = error { + print(error.localizedDescription) + self.connectButton.isEnabled = true + self.connection.mediaPublisher.disconnect { + error in + if let error = error { + print(error.localizedDescription) + } + } + return + } + self.disconnectButton.isEnabled = true + self.connection.mediaPublisher.mainMediaStream! + .videoRenderer = self.publisherVideoView + self.connection.mediaSubscriber.mainMediaStream! + .videoRenderer = self.subscriberVideoView + } + } + } + + @IBAction func disconnect(_ sender: AnyObject) { + connection.mediaPublisher.disconnect { + error in + if let error = error { + print(error.localizedDescription) + } + } + connection.mediaSubscriber.disconnect { + error in + if let error = error { + print(error.localizedDescription) + } + } + self.connectButton.isEnabled = true + self.disconnectButton.isEnabled = false + } + + @IBAction func switchCameraPosition(_ sender: AnyObject) { + print("switch camera position") + if disconnectButton.isEnabled { + connection.mediaPublisher.flipCameraPosition() + } + } + +} +