diff --git a/Swift/AWSKinesisVideoWebRTCDemoApp.xcodeproj/project.pbxproj b/Swift/AWSKinesisVideoWebRTCDemoApp.xcodeproj/project.pbxproj
index a425a93..1136c29 100644
--- a/Swift/AWSKinesisVideoWebRTCDemoApp.xcodeproj/project.pbxproj
+++ b/Swift/AWSKinesisVideoWebRTCDemoApp.xcodeproj/project.pbxproj
@@ -387,6 +387,7 @@
17FC2F7B1E258FC500174015 = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1020;
+ ProvisioningStyle = Automatic;
};
703513E32399DA0400376B66 = {
CreatedOnToolsVersion = 11.2;
@@ -469,6 +470,7 @@
"${BUILT_PRODUCTS_DIR}/AWSCore/AWSCore.framework",
"${BUILT_PRODUCTS_DIR}/AWSKinesisVideo/AWSKinesisVideo.framework",
"${BUILT_PRODUCTS_DIR}/AWSKinesisVideoSignaling/AWSKinesisVideoSignaling.framework",
+ "${BUILT_PRODUCTS_DIR}/AWSKinesisVideoWebRTCStorage/AWSKinesisVideoWebRTCStorage.framework",
"${BUILT_PRODUCTS_DIR}/AWSMobileClient/AWSMobileClient.framework",
"${BUILT_PRODUCTS_DIR}/CommonCryptoModule/CommonCryptoModule.framework",
"${PODS_ROOT}/GoogleWebRTC/Frameworks/frameworks/WebRTC.framework",
@@ -482,6 +484,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSCore.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSKinesisVideo.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSKinesisVideoSignaling.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSKinesisVideoWebRTCStorage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSMobileClient.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CommonCryptoModule.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework",
@@ -527,6 +530,7 @@
"${BUILT_PRODUCTS_DIR}/AWSCore/AWSCore.framework",
"${BUILT_PRODUCTS_DIR}/AWSKinesisVideo/AWSKinesisVideo.framework",
"${BUILT_PRODUCTS_DIR}/AWSKinesisVideoSignaling/AWSKinesisVideoSignaling.framework",
+ "${BUILT_PRODUCTS_DIR}/AWSKinesisVideoWebRTCStorage/AWSKinesisVideoWebRTCStorage.framework",
"${BUILT_PRODUCTS_DIR}/AWSMobileClient/AWSMobileClient.framework",
"${BUILT_PRODUCTS_DIR}/CommonCryptoModule/CommonCryptoModule.framework",
"${PODS_ROOT}/GoogleWebRTC/Frameworks/frameworks/WebRTC.framework",
@@ -540,6 +544,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSCore.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSKinesisVideo.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSKinesisVideoSignaling.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSKinesisVideoWebRTCStorage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AWSMobileClient.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CommonCryptoModule.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework",
@@ -659,8 +664,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = YES;
- "ARCHS[sdk=iphonesimulator*]" = x86_64;
"ARCHS[sdk=iphoneos*]" = "$(ARCHS_STANDARD)";
+ "ARCHS[sdk=iphonesimulator*]" = x86_64;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
@@ -723,8 +728,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = YES;
- "ARCHS[sdk=iphonesimulator*]" = x86_64;
"ARCHS[sdk=iphoneos*]" = "$(ARCHS_STANDARD)";
+ "ARCHS[sdk=iphonesimulator*]" = x86_64;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
@@ -784,6 +789,8 @@
baseConfigurationReference = BDB1499CD0982C181C7DDE9E /* Pods-AWSKinesisVideoWebRTCDemoApp.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=*]" = "";
@@ -825,6 +832,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.kinesisvideo.KVSApp1;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 4.0;
};
name = Debug;
diff --git a/Swift/AWSKinesisVideoWebRTCDemoAppTests/VideoViewControllerTests.swift b/Swift/AWSKinesisVideoWebRTCDemoAppTests/VideoViewControllerTests.swift
index dbb7933..c0ce572 100755
--- a/Swift/AWSKinesisVideoWebRTCDemoAppTests/VideoViewControllerTests.swift
+++ b/Swift/AWSKinesisVideoWebRTCDemoAppTests/VideoViewControllerTests.swift
@@ -20,9 +20,9 @@ class VideoViewControllerTests: XCTestCase{
signalingClient!.connect()
RTCIceServersList.append(RTCIceServer.init(urlStrings: ["stun:stun.kinesisvideo." + "us-west-2" + ".amazonaws.com:443"]))
- webRTCClient = WebRTCClient(iceServers: RTCIceServersList, isAudioOn: true)
+ webRTCClient = WebRTCClient(iceServers: RTCIceServersList, isAudioOn: true, isVideoOn: true)
webRTCClient!.delegate = channelVC
- videoViewController = VideoViewController(webRTCClient: self.webRTCClient!, signalingClient: self.signalingClient!, localSenderClientID: "randomClientID", isMaster: true, mediaServerEndPoint: nil)
+ videoViewController = VideoViewController(webRTCClient: self.webRTCClient!, signalingClient: self.signalingClient!, localSenderClientID: "randomClientID", isMaster: true, signalingChannelArn: nil)
}
override func tearDown() {
diff --git a/Swift/KVSiOSApp/ChannelConfigurationViewController.swift b/Swift/KVSiOSApp/ChannelConfigurationViewController.swift
index d6a22af..9ba0853 100755
--- a/Swift/KVSiOSApp/ChannelConfigurationViewController.swift
+++ b/Swift/KVSiOSApp/ChannelConfigurationViewController.swift
@@ -2,6 +2,7 @@ import AWSCore
import AWSCognitoIdentityProvider
import AWSKinesisVideo
import AWSKinesisVideoSignaling
+import AWSKinesisVideoWebRTCStorage
import AWSMobileClient
import Foundation
import WebRTC
@@ -32,6 +33,7 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
// variables controlled by UI
var sendAudioEnabled: Bool = true
+ var sendVideoEnabled: Bool = true
var isMaster: Bool = false
var signalingConnected: Bool = false
var selectedResolution: VideoResolution = .resolution720p
@@ -52,6 +54,7 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
@IBOutlet var clientID: UITextField!
@IBOutlet var regionName: UITextField!
@IBOutlet var isAudioEnabled: UISwitch!
+ @IBOutlet var isVideoEnabled: UISwitch!
@IBOutlet var resolutionButton: UIButton!
// Connect Buttons
@@ -118,6 +121,14 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
}
}
+ @IBAction func videoStateChanged(sender: UISwitch!) {
+ if sender.isOn {
+ self.sendVideoEnabled = true
+ } else {
+ self.sendVideoEnabled = false
+ }
+ }
+
@IBAction func resolutionButtonTapped(_ sender: UIButton) {
let alert = UIAlertController(title: "Select Resolution", message: nil, preferredStyle: .actionSheet)
@@ -224,16 +235,22 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
}
}
// check whether signalling channel will save its recording to a stream
- // only applies for master
- var usingMediaServer : Bool = false
- if self.isMaster {
- usingMediaServer = isUsingMediaServer(channelARN: channelARN!, channelName: channelNameValue)
- // Make sure that audio is enabled if ingesting webrtc connection
- if(usingMediaServer && !self.sendAudioEnabled) {
- popUpError(title: "Invalid Configuration", message: "Audio must be enabled to use MediaServer")
+ var usingMediaServer: Bool = isUsingMediaServer(channelARN: channelARN!, channelName: channelNameValue)
+ // Make sure that audio is enabled if ingesting webrtc connection
+ if(usingMediaServer) {
+ if (self.isMaster && (!self.isAudioEnabled.isOn || !self.isVideoEnabled.isOn)) {
+ // Master mode: Both audio and video required
+ popUpError(title: "Invalid Configuration", message: "Video and audio must be enabled for WebRTC ingestion master")
return
+ } else if (!self.isMaster) {
+ // Viewer mode: Video not allowed, audio optional
+ if (self.isVideoEnabled.isOn) {
+ popUpError(title: "Invalid Configuration", message: "Video is not allowed for WebRTC ingestion viewer")
+ return
+ }
}
}
+
// get signalling channel endpoints
let endpoints = getSignallingEndpoints(channelARN: channelARN!, region: awsRegionValue, isMaster: self.isMaster, useMediaServer: usingMediaServer)
//// Ensure that the WebSocket (WSS) endpoint is available; WebRTC requires a valid signaling endpoint.
@@ -254,8 +271,20 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
service: .KinesisVideo,
url: URL(string: endpoints["HTTPS"]!!))
let RTCIceServersList = getIceCandidates(channelARN: channelARN!, endpoint: httpsEndpoint!, regionType: awsRegionType, clientId: localSenderId)
- webRTCClient = WebRTCClient(iceServers: RTCIceServersList, isAudioOn: sendAudioEnabled, resolution: selectedResolution)
+ webRTCClient = WebRTCClient(iceServers: RTCIceServersList, isAudioOn: sendAudioEnabled, isVideoOn: sendVideoEnabled, resolution: selectedResolution)
webRTCClient!.delegate = self
+
+ guard !usingMediaServer || endpoints["WEBRTC"] != nil else {
+ print("connectAsRole IllegalState! WEBRTC endpoint is required for WebRTC ingestion")
+ return
+ }
+ if usingMediaServer {
+ let webRTCEndpoint: String = endpoints["WEBRTC"]!!
+ let webRTCStorageConfiguration = AWSServiceConfiguration(region: awsRegionType,
+ endpoint: AWSEndpoint(urlString: webRTCEndpoint),
+ credentialsProvider: getCredentialsProvider())
+ AWSKinesisVideoWebRTCStorage.register(with: webRTCStorageConfiguration!, forKey: awsKinesisVideoKey)
+ }
// Connect to signalling channel with wss endpoint
print("Connecting to web socket from channel config")
@@ -267,7 +296,7 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
let seconds = 2.0
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
self.updateConnectionLabel()
- self.vc = VideoViewController(webRTCClient: self.webRTCClient!, signalingClient: self.signalingClient!, localSenderClientID: self.localSenderId, isMaster: self.isMaster, mediaServerEndPoint: endpoints["WEBRTC"] ?? nil)
+ self.vc = VideoViewController(webRTCClient: self.webRTCClient!, signalingClient: self.signalingClient!, localSenderClientID: self.localSenderId, isMaster: self.isMaster, signalingChannelArn: usingMediaServer ? channelARN : nil, isVideoEnabled: self.sendVideoEnabled)
self.present(self.vc!, animated: true, completion: nil)
}
}
@@ -348,6 +377,7 @@ class ChannelConfigurationViewController: UIViewController, UITextFieldDelegate
}
}
}).waitUntilFinished()
+ print("\(channelARN) configured for ingestion? \(usingMediaServer)")
return usingMediaServer
}
@@ -499,6 +529,12 @@ extension ChannelConfigurationViewController: SignalClientDelegate {
remoteSenderClientId = senderClientId
}
setRemoteSenderClientId()
+
+ // Mark that offer was received to stop storage session retries
+ if sdp.type == .offer {
+ vc?.markOfferReceived()
+ }
+
webRTCClient!.set(remoteSdp: sdp, clientId: senderClientId) { _ in
print("Setting remote sdp and sending answer.")
self.vc!.sendAnswer(recipientClientID: self.remoteSenderClientId!)
@@ -529,14 +565,23 @@ extension ChannelConfigurationViewController: WebRTCClientDelegate {
switch state {
case .connected, .completed:
print("WebRTC connected/completed state")
+ DispatchQueue.main.async {
+ self.vc?.showToast(message: "WebRTC Connected", length: "short")
+ }
case .disconnected:
print("WebRTC disconnected state")
+ DispatchQueue.main.async {
+ self.vc?.showToast(message: "WebRTC Disconnected", length: "short")
+ }
+ case .failed:
+ print("WebRTC failed state")
+ DispatchQueue.main.async {
+ self.vc?.showToast(message: "WebRTC Connection Failed", length: "long")
+ }
case .new:
print("WebRTC new state")
case .checking:
print("WebRTC checking state")
- case .failed:
- print("WebRTC failed state")
case .closed:
print("WebRTC closed state")
case .count:
diff --git a/Swift/KVSiOSApp/Main.storyboard b/Swift/KVSiOSApp/Main.storyboard
index e11ca83..b17a085 100755
--- a/Swift/KVSiOSApp/Main.storyboard
+++ b/Swift/KVSiOSApp/Main.storyboard
@@ -552,12 +552,12 @@
-
+
-
-
+
+
@@ -569,7 +569,7 @@
-
+
@@ -578,7 +578,7 @@
-
+
@@ -587,7 +587,7 @@
-
-
+
+
-
+
+
+
+
+
-
+
-
+
@@ -647,12 +665,9 @@
-
-
-
-
+
@@ -664,7 +679,7 @@
-
+
@@ -685,6 +700,7 @@
+
@@ -755,8 +771,14 @@
+
+
+
+
+
+
diff --git a/Swift/KVSiOSApp/SignalingClient.swift b/Swift/KVSiOSApp/SignalingClient.swift
index 2ac192f..0386273 100755
--- a/Swift/KVSiOSApp/SignalingClient.swift
+++ b/Swift/KVSiOSApp/SignalingClient.swift
@@ -104,6 +104,11 @@ extension SignalingClient: WebSocketDelegate {
}
func websocketDidReceiveMessage(socket _: WebSocketClient, text: String) {
+ guard !text.isEmpty else {
+ debugPrint("Ignoring empty WebSocket message")
+ return
+ }
+
debugPrint("Additional signaling messages \(text)")
var parsedMessage: Message?
diff --git a/Swift/KVSiOSApp/VideoViewController.swift b/Swift/KVSiOSApp/VideoViewController.swift
index c7e6432..be63b21 100755
--- a/Swift/KVSiOSApp/VideoViewController.swift
+++ b/Swift/KVSiOSApp/VideoViewController.swift
@@ -1,31 +1,45 @@
import UIKit
import AWSKinesisVideo
+import AWSKinesisVideoWebRTCStorage
import WebRTC
class VideoViewController: UIViewController {
@IBOutlet var localVideoView: UIView?
- @IBOutlet var joinStorageButton: UIButton?
private let webRTCClient: WebRTCClient
private let signalingClient: SignalingClient
private let localSenderClientID: String
private let isMaster: Bool
+ private let signalingChannelArn: String?
+ private let isVideoEnabled: Bool
+ private var hasReceivedOffer = false
+ private var storageSessionAttempts: [Date]
- init(webRTCClient: WebRTCClient, signalingClient: SignalingClient, localSenderClientID: String, isMaster: Bool, mediaServerEndPoint: String?) {
+ init(webRTCClient: WebRTCClient, signalingClient: SignalingClient, localSenderClientID: String, isMaster: Bool, signalingChannelArn: String?, isVideoEnabled: Bool = true) {
self.webRTCClient = webRTCClient
self.signalingClient = signalingClient
self.localSenderClientID = localSenderClientID
self.isMaster = isMaster
+ self.signalingChannelArn = signalingChannelArn
+ self.isVideoEnabled = isVideoEnabled
+ self.storageSessionAttempts = []
super.init(nibName: String(describing: VideoViewController.self), bundle: Bundle.main)
- if !isMaster {
+ let isIngestMedia: Bool = self.signalingChannelArn != nil
+ print("isIngestMedia? \(isIngestMedia)")
+ print("role: \(isMaster ? "master" : "viewer")")
+
+ if !isIngestMedia && !isMaster {
// In viewer mode send offer once connection is established
webRTCClient.offer { sdp in
self.signalingClient.sendOffer(rtcSdp: sdp, senderClientid: self.localSenderClientID)
}
}
- if mediaServerEndPoint == nil {
- self.joinStorageButton?.isHidden = true
+
+ if isIngestMedia {
+ DispatchQueue.global(qos: .background).async {
+ self.joinStorageSessionWithRetry()
+ }
}
}
@@ -40,26 +54,44 @@ class VideoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
+ let isIngestMode = signalingChannelArn != nil
+
#if arch(arm64)
- // Using metal (arm64 only)
let localRenderer = RTCMTLVideoView(frame: localVideoView?.frame ?? CGRect.zero)
let remoteRenderer = RTCMTLVideoView(frame: view.frame)
localRenderer.videoContentMode = .scaleAspectFill
remoteRenderer.videoContentMode = .scaleAspectFill
#else
- // Using OpenGLES for the rest
let localRenderer = RTCEAGLVideoView(frame: localVideoView?.frame ?? CGRect.zero)
let remoteRenderer = RTCEAGLVideoView(frame: view.frame)
#endif
- webRTCClient.startCaptureLocalVideo(renderer: localRenderer)
- webRTCClient.renderRemoteVideo(to: remoteRenderer)
-
- if let localVideoView = self.localVideoView {
- embedView(localRenderer, into: localVideoView)
+ if isIngestMode && isMaster {
+ // Ingestion master: local view only
+ webRTCClient.startCaptureLocalVideo(renderer: localRenderer)
+ embedView(localRenderer, into: view)
+ view.sendSubview(toBack: localRenderer)
+ localVideoView?.isHidden = true
+ } else if isIngestMode && !isMaster {
+ // Ingestion viewer: remote view only
+ webRTCClient.renderRemoteVideo(to: remoteRenderer)
+ embedView(remoteRenderer, into: view)
+ view.sendSubview(toBack: remoteRenderer)
+ localVideoView?.isHidden = true
+ } else {
+ // Non-ingestion: remote fullscreen + local in corner
+ if isVideoEnabled {
+ webRTCClient.startCaptureLocalVideo(renderer: localRenderer)
+ if let localVideoView = self.localVideoView {
+ embedView(localRenderer, into: localVideoView)
+ }
+ } else {
+ localVideoView?.isHidden = true
+ }
+ webRTCClient.renderRemoteVideo(to: remoteRenderer)
+ embedView(remoteRenderer, into: view)
+ view.sendSubview(toBack: remoteRenderer)
}
- embedView(remoteRenderer, into: view)
- view.sendSubview(toBack: remoteRenderer)
}
private func embedView(_ view: UIView, into containerView: UIView) {
@@ -83,10 +115,75 @@ class VideoViewController: UIViewController {
dismiss(animated: true)
}
- @IBAction func joinStorageSession(_: Any) {
- print("button pressed")
- joinStorageButton?.isHidden = true
+ func joinStorageSessionWithRetry() {
+ guard let signalingChannelArn = self.signalingChannelArn else {
+ print("joinStorageSessionWithRetry IllegalState! ARN cannot be nil")
+ return
+ }
+
+ // If we already received an offer, stop retrying
+ if hasReceivedOffer {
+ print("SDP offer received, stopping storage session retries")
+ return
+ }
+
+ // Clean up attempts older than 10 minutes
+ let tenMinutesAgo = Date().addingTimeInterval(-600)
+ storageSessionAttempts = storageSessionAttempts.filter { $0 > tenMinutesAgo }
+
+ // Check if we've exceeded 3 attempts in the last 10 minutes
+ if storageSessionAttempts.count >= 3 {
+ print("Too many storage session attempts (3) within 10 minutes. Stopping retries.")
+ return
+ }
+ // Record this attempt
+ storageSessionAttempts.append(Date())
+
+ let webrtcStorageClient = AWSKinesisVideoWebRTCStorage(forKey: awsKinesisVideoKey)
+
+ if self.isMaster {
+ let joinStorageSessionRequest = AWSKinesisVideoWebRTCStorageJoinStorageSessionInput()
+ joinStorageSessionRequest?.channelArn = signalingChannelArn
+
+ print("Calling JoinStorageSession with ARN: \(signalingChannelArn) (attempt \(storageSessionAttempts.count))")
+
+ webrtcStorageClient.joinSession(joinStorageSessionRequest!).continueWith(block: { (task) -> Void in
+ if let error = task.error {
+ print("Error joining storage session: \(error)")
+ } else {
+ print("Joined storage session!")
+ }
+
+ // Retry after 6 seconds if no offer received yet
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 6.0) {
+ self.joinStorageSessionWithRetry()
+ }
+ })
+ } else {
+ let joinStorageSessionAsViewerRequest = AWSKinesisVideoWebRTCStorageJoinStorageSessionAsViewerInput()
+ joinStorageSessionAsViewerRequest?.channelArn = signalingChannelArn
+ joinStorageSessionAsViewerRequest?.clientId = self.localSenderClientID
+
+ print("Calling JoinStorageSessionAsViewer with ARN: \(signalingChannelArn) and clientId: \(self.localSenderClientID) (attempt \(storageSessionAttempts.count))")
+
+ webrtcStorageClient.joinSession(asViewer: joinStorageSessionAsViewerRequest!).continueWith(block: { (task) -> Void in
+ if let error = task.error {
+ print("Error joining storage session: \(error)")
+ } else {
+ print("Joined storage session!")
+ }
+
+ // Retry after 6 seconds if no offer received yet
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 6.0) {
+ self.joinStorageSessionWithRetry()
+ }
+ })
+ }
+ }
+
+ func markOfferReceived() {
+ hasReceivedOffer = true
}
func sendAnswer(recipientClientID: String) {
@@ -96,4 +193,52 @@ class VideoViewController: UIViewController {
self.webRTCClient.updatePeerConnectionAndHandleIceCandidates(clientId: recipientClientID)
}
}
+
+ func showToast(message: String, length: String = "short") {
+ guard length == "short" || length == "long" else {
+ print("showToast: Invalid argument - length must either be short or long")
+ return
+ }
+
+ let durationSec = length == "short" ? 2.0 : 3.5
+ let padding: CGFloat = 12
+
+ let toastContainer = UIView()
+ toastContainer.backgroundColor = UIColor.black.withAlphaComponent(0.8)
+ toastContainer.layer.cornerRadius = 10
+ toastContainer.translatesAutoresizingMaskIntoConstraints = false
+ toastContainer.alpha = 0.0
+
+ let toastLabel = UILabel()
+ toastLabel.text = message
+ toastLabel.textAlignment = .center
+ toastLabel.textColor = UIColor.white
+ toastLabel.font = UIFont.systemFont(ofSize: 14)
+ toastLabel.numberOfLines = 0
+ toastLabel.translatesAutoresizingMaskIntoConstraints = false
+
+ toastContainer.addSubview(toastLabel)
+ self.view.addSubview(toastContainer)
+
+ NSLayoutConstraint.activate([
+ toastLabel.topAnchor.constraint(equalTo: toastContainer.topAnchor, constant: padding),
+ toastLabel.bottomAnchor.constraint(equalTo: toastContainer.bottomAnchor, constant: -padding),
+ toastLabel.leadingAnchor.constraint(equalTo: toastContainer.leadingAnchor, constant: padding),
+ toastLabel.trailingAnchor.constraint(equalTo: toastContainer.trailingAnchor, constant: -padding),
+ toastContainer.leadingAnchor.constraint(greaterThanOrEqualTo: self.view.leadingAnchor, constant: 20),
+ toastContainer.trailingAnchor.constraint(lessThanOrEqualTo: self.view.trailingAnchor, constant: -20),
+ toastContainer.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
+ toastContainer.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -50)
+ ])
+
+ UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
+ toastContainer.alpha = 1.0
+ }) { _ in
+ UIView.animate(withDuration: 0.5, delay: durationSec, options: .curveEaseIn, animations: {
+ toastContainer.alpha = 0.0
+ }) { _ in
+ toastContainer.removeFromSuperview()
+ }
+ }
+ }
}
diff --git a/Swift/KVSiOSApp/VideoViewController.xib b/Swift/KVSiOSApp/VideoViewController.xib
index a81c305..e690e19 100755
--- a/Swift/KVSiOSApp/VideoViewController.xib
+++ b/Swift/KVSiOSApp/VideoViewController.xib
@@ -1,23 +1,22 @@
-
-
+
+
-
+
-
-
+
-
+
-
diff --git a/Swift/KVSiOSApp/WebRTCClient.swift b/Swift/KVSiOSApp/WebRTCClient.swift
index 7b4d38f..5b93b17 100755
--- a/Swift/KVSiOSApp/WebRTCClient.swift
+++ b/Swift/KVSiOSApp/WebRTCClient.swift
@@ -30,11 +30,12 @@ final class WebRTCClient: NSObject {
private var remoteDataChannel: RTCDataChannel?
private var constructedIceServers: [RTCIceServer]?
private var selectedResolution: VideoResolution
+ private var remoteRenderer: RTCVideoRenderer?
private var peerConnectionFoundMap = [String: RTCPeerConnection]()
private var pendingIceCandidatesMap = [String: Set]()
- required init(iceServers: [RTCIceServer], isAudioOn: Bool, resolution: VideoResolution = .resolution720p) {
+ required init(iceServers: [RTCIceServer], isAudioOn: Bool, isVideoOn: Bool = true, resolution: VideoResolution = .resolution720p) {
self.selectedResolution = resolution
let config = RTCConfiguration()
@@ -55,7 +56,9 @@ final class WebRTCClient: NSObject {
if (isAudioOn) {
createLocalAudioStream()
}
+ if (isVideoOn) {
createLocalVideoStream()
+ }
peerConnection.delegate = self
}
@@ -146,8 +149,10 @@ final class WebRTCClient: NSObject {
peerConnection.setRemoteDescription(remoteSdp, completionHandler: completion)
if remoteSdp.type == RTCSdpType.answer {
print("Received answer for client ID: \(clientId)")
- updatePeerConnectionAndHandleIceCandidates(clientId: clientId)
+ } else {
+ print("Received offer from remote")
}
+ updatePeerConnectionAndHandleIceCandidates(clientId: clientId)
}
func checkAndAddIceCandidate(remoteCandidate: RTCIceCandidate, clientId: String) {
@@ -217,17 +222,25 @@ final class WebRTCClient: NSObject {
}
func renderRemoteVideo(to renderer: RTCVideoRenderer) {
+ debugPrint("renderRemoteVideo called, remoteVideoTrack: \(remoteVideoTrack != nil)")
+ remoteRenderer = renderer
remoteVideoTrack?.add(renderer)
}
+ func enableVideo(_ enabled: Bool) {
+ localVideoTrack?.isEnabled = enabled
+ }
+
+ func enableAudio(_ enabled: Bool) {
+ localAudioTrack?.isEnabled = enabled
+ }
+
private func createLocalVideoStream() {
localVideoTrack = createVideoTrack()
if let localVideoTrack = localVideoTrack {
peerConnection.add(localVideoTrack, streamIds: [streamId])
- remoteVideoTrack = peerConnection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack
}
-
}
private func createLocalAudioStream() {
@@ -266,6 +279,19 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
debugPrint("peerConnection didRemove stream:\(stream)")
}
+ func peerConnection(_ peerConnection: RTCPeerConnection, didAdd rtpReceiver: RTCRtpReceiver, streams mediaStreams: [RTCMediaStream]) {
+ debugPrint("peerConnection didAdd rtpReceiver: \(rtpReceiver.track?.kind ?? "unknown")")
+ if rtpReceiver.track?.kind == kRTCMediaStreamTrackKindVideo {
+ remoteVideoTrack = rtpReceiver.track as? RTCVideoTrack
+ debugPrint("Remote video track assigned: \(remoteVideoTrack != nil)")
+ // Add to renderer if we have one stored
+ if let renderer = remoteRenderer {
+ remoteVideoTrack?.add(renderer)
+ debugPrint("Added remote video track to renderer")
+ }
+ }
+ }
+
func peerConnectionShouldNegotiate(_: RTCPeerConnection) {
debugPrint("peerConnectionShouldNegotiate")
}
diff --git a/Swift/Podfile b/Swift/Podfile
index 8df3312..8ac0d35 100755
--- a/Swift/Podfile
+++ b/Swift/Podfile
@@ -9,6 +9,7 @@ target 'AWSKinesisVideoWebRTCDemoApp' do
pod 'CommonCryptoModule'
pod 'AWSKinesisVideo'
pod 'AWSKinesisVideoSignaling'
+ pod 'AWSKinesisVideoWebRTCStorage'
pod 'GoogleWebRTC', '~> 1.1'
pod 'Starscream', '~> 3.0'
target 'AWSKinesisVideoWebRTCDemoAppUITests' do