├── Chatchat ├── .DS_Store ├── ContactsTableViewController.h ├── AppDelegate.h ├── OutgoingViewController.h ├── VoiceCallViewController.h ├── VideoCallViewController.h ├── User.h ├── main.m ├── ChatViewController.h ├── User.m ├── RTCICECandidate+JSON.h ├── constants.h ├── IncomingCallViewController.h ├── RTCSessionDescription+JSON.h ├── CommonDefines.h ├── ChatSession.h ├── ChatSessionManager.h ├── UserManager.h ├── ARDSDPUtils.h ├── ChatSession.m ├── Message.h ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Message.m ├── CallViewController.h ├── VoiceCallViewController.m ├── RTCICECandidate+JSON.m ├── Info.plist ├── RTCSessionDescription+JSON.m ├── ChatSessionManager.m ├── AppDelegate.m ├── UserManager.m ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── OutgoingViewController.m ├── ARDSDPUtils.m ├── ChatViewController.m ├── VideoCallViewController.m ├── IncomingCallViewController.m ├── CallViewController.m └── ContactsTableViewController.m ├── Podfile ├── Server ├── package.json ├── public │ ├── css │ │ └── main.css │ └── main.js ├── server.js └── index.html ├── .gitignore ├── Chatchat.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── LICENSE └── README.md /Chatchat/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TaoistKing/chatchat/HEAD/Chatchat/.DS_Store -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '8.0' 2 | use_frameworks! 3 | target 'Chatchat' do 4 | pod 'Socket.IO-Client-Swift' , '~> 8.1.2' 5 | # pod 'libjingle_peerconnection', '~> 11177.2.0' 6 | pod 'WebRTC', '~> 54.6.13869' 7 | end 8 | -------------------------------------------------------------------------------- /Server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket-chat-example", 3 | "version": "0.0.1", 4 | "description": "my first socket.io app", 5 | "dependencies": { 6 | "express": "^4.10.2", 7 | "socket.io": "^1.4.6" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | Chatchat.xcodeproj/project.xcworkspace/xcuserdata/ 3 | Chatchat.xcodeproj/xcuserdata/ 4 | Server/node_modules/ 5 | Podfile.lock 6 | Chatchat.xcworkspace/ 7 | Pods/ 8 | */.DS_Store 9 | Chatchat/.DS_Store 10 | 11 | -------------------------------------------------------------------------------- /Chatchat.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Chatchat/ContactsTableViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsTableViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/5/31. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ContactsTableViewController : UITableViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Chatchat/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // Chatchat 4 | // 5 | // Created by wangruihit@gmail.com on 5/26/16. 6 | // Copyright © 2016 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /Chatchat/OutgoingViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // OutgoingViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/7/11. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "CallViewController.h" 10 | 11 | @interface OutgoingViewController : CallViewController 12 | 13 | - (RTCMediaConstraints *)defaultOfferConstraints; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Chatchat/VoiceCallViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // VoiceCallViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/7. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CommonDefines.h" 12 | #import "OutgoingViewController.h" 13 | 14 | @interface VoiceCallViewController : OutgoingViewController 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Chatchat/VideoCallViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCallViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/24. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CommonDefines.h" 12 | #import "OutgoingViewController.h" 13 | 14 | @interface VideoCallViewController : OutgoingViewController 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Chatchat/User.h: -------------------------------------------------------------------------------- 1 | // 2 | // User.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Message.h" 11 | 12 | @interface User : NSObject 13 | @property NSString *uniqueID; 14 | @property NSString *name; 15 | 16 | - (instancetype)initWithName: (NSString *)name UID: (NSString *)uid; 17 | @end -------------------------------------------------------------------------------- /Chatchat/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // Chatchat 4 | // 5 | // Created by wangruihit@gmail.com on 5/26/16. 6 | // Copyright © 2016 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Chatchat/ChatViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "CommonDefines.h" 11 | 12 | @interface ChatViewController : UIViewController 13 | 14 | @property (weak) id socketIODelegate; 15 | @property (weak) User *peer; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Chatchat/User.m: -------------------------------------------------------------------------------- 1 | // 2 | // User.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "User.h" 10 | 11 | @implementation User 12 | 13 | - (instancetype)initWithName: (NSString *)name UID: (NSString *)uid{ 14 | if (self = [super init]) { 15 | self.name = name; 16 | self.uniqueID = uid; 17 | } 18 | 19 | return self; 20 | } 21 | @end 22 | -------------------------------------------------------------------------------- /Chatchat/RTCICECandidate+JSON.h: -------------------------------------------------------------------------------- 1 | // 2 | // RTCICECandidate+JSON.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/8. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RTCIceCandidate (JSON) 12 | 13 | + (RTCIceCandidate *)candidateFromJSONDictionary:(NSDictionary *)dictionary; 14 | - (NSData *)JSONData; 15 | - (NSString *)JSONString; 16 | - (NSDictionary *)toDictionary; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Chatchat/constants.h: -------------------------------------------------------------------------------- 1 | // 2 | // constants.h 3 | // Chatchat 4 | // 5 | // Created by wangruihit@gmail.com on 5/26/16. 6 | // Copyright © 2016 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #ifndef constants_h 10 | #define constants_h 11 | 12 | #import 13 | 14 | #define kServerURL @"http://192.168.127.241:3000" 15 | #define kDefaultSTUNServerUrl @"stun:stun.ideasip.com" 16 | //@"stun:stun.qq.com:3478" 17 | //@"stun:stun.l.google.com:19302" 18 | 19 | #endif /* constants_h */ 20 | -------------------------------------------------------------------------------- /Chatchat/IncomingCallViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // IncomingCallViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/8. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "CommonDefines.h" 12 | #import "CallViewController.h" 13 | 14 | @interface IncomingCallViewController : CallViewController 15 | 16 | @property (strong) Message *offer; 17 | @property (strong) NSArray *pendingMessages; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Chatchat/RTCSessionDescription+JSON.h: -------------------------------------------------------------------------------- 1 | // 2 | // RTCSessionDescription+JSON.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/12/14. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RTCSessionDescription (JSON) 12 | 13 | + (RTCSessionDescription *)sdpFromJSONDictionary:(NSDictionary *)dictionary; 14 | + (RTCSessionDescription *)sdpFromJSONString:(NSString *)sdp; 15 | - (NSData *)JSONData; 16 | - (NSString *)JSONString; 17 | - (NSDictionary *)toDictionary; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Chatchat/CommonDefines.h: -------------------------------------------------------------------------------- 1 | // 2 | // CommonDefines.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #ifndef CommonDefines_h 10 | #define CommonDefines_h 11 | 12 | #import 13 | #import "User.h" 14 | #import "Message.h" 15 | 16 | @protocol SocketIODelegate 17 | - (void)sendMessage : (Message *)message; 18 | @end 19 | 20 | @protocol MessageReciver 21 | - (void)onMessage: (Message *)message; 22 | @end 23 | 24 | #endif /* CommonDefines_h */ 25 | -------------------------------------------------------------------------------- /Chatchat/ChatSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatSession.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/2. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "User.h" 12 | #import "Message.h" 13 | 14 | @interface ChatSession : NSObject 15 | 16 | @property (strong) User *peer; 17 | 18 | - (instancetype)initWithPeer: (User *)peer; 19 | 20 | - (NSArray *)listAllMessages; 21 | - (NSArray *)listUnread; 22 | - (NSUInteger)unreadCount; 23 | 24 | - (void)onMessage: (Message *)message; 25 | - (void)onUnreadMessage: (Message *)message; 26 | - (void)clearUnread; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Chatchat/ChatSessionManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // ChatSessionManager.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/2. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ChatSession.h" 11 | 12 | @interface ChatSessionManager : NSObject 13 | 14 | + (instancetype)sharedManager; 15 | 16 | - (ChatSession *)findSessionByPeer: (User *)user; 17 | - (ChatSession *)findSessionByUID: (NSString *)uid; 18 | 19 | - (ChatSession *)createSessionWithPeer: (User *)user; 20 | 21 | - (void)removeSession: (ChatSession *)session; 22 | - (void)removeSessionByPeer: (User *)user; 23 | - (void)removeSessionByUID: (NSString *)uid; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Chatchat/UserManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // UserManager.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/2. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "User.h" 11 | 12 | @interface UserManager : NSObject 13 | 14 | + (instancetype)sharedManager; 15 | 16 | - (BOOL)isUserExist : (NSString *)uid; 17 | 18 | - (void)addUser : (User *)user; 19 | - (void)addUserWithUID: (NSString *)uid name: (NSString *)name; 20 | - (void)removeUserByUID: (NSString *)uid; 21 | - (void)removeUser: (User *)user; 22 | - (void)removeAllUsers; 23 | 24 | - (NSArray *)listUsers; 25 | - (NSUInteger)numberUsers; 26 | 27 | - (User *)findUserByUID: (NSString *)uid; 28 | - (User *)localUser; 29 | - (void)setLocalUserWithName: (NSString *)name UID: (NSString *)uid; 30 | 31 | - (void)replaceAllUsersWithNewUsers : (NSArray *)users; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /Chatchat/ARDSDPUtils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | #import 12 | 13 | @class RTCSessionDescription; 14 | 15 | @interface ARDSDPUtils : NSObject 16 | 17 | // Updates the original SDP description to instead prefer the specified video 18 | // codec. We do this by placing the specified codec at the beginning of the 19 | // codec list if it exists in the sdp. 20 | + (RTCSessionDescription *) 21 | descriptionForDescription:(RTCSessionDescription *)description 22 | preferredVideoCodec:(NSString *)codec; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 TaoistKing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Chatchat/ChatSession.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatSession.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/2. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "ChatSession.h" 10 | 11 | @interface ChatSession () 12 | { 13 | NSMutableArray *_messages; 14 | NSMutableArray *_unreadMessages; 15 | } 16 | 17 | @end 18 | 19 | @implementation ChatSession 20 | 21 | - (instancetype)initWithPeer: (User *)peer{ 22 | if (self = [super init]) { 23 | self.peer = peer; 24 | } 25 | 26 | _messages = [NSMutableArray array]; 27 | _unreadMessages = [NSMutableArray array]; 28 | 29 | return self; 30 | } 31 | 32 | - (NSArray *)listAllMessages{ 33 | return _messages; 34 | } 35 | 36 | - (NSArray *)listUnread{ 37 | return _unreadMessages; 38 | } 39 | 40 | - (NSUInteger)unreadCount{ 41 | return _unreadMessages.count; 42 | } 43 | 44 | - (void)onMessage: (Message *)message{ 45 | [_messages addObject:message]; 46 | } 47 | 48 | - (void)onUnreadMessage: (Message *)message{ 49 | [_messages addObject:message]; 50 | [_unreadMessages addObject:message]; 51 | } 52 | 53 | - (void)clearUnread{ 54 | [_unreadMessages removeAllObjects]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /Chatchat/Message.h: -------------------------------------------------------------------------------- 1 | // 2 | // Message.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #define kMessageType_Signal @"signal" 12 | #define kMessageType_Text @"text" 13 | 14 | #define kMessageSubtype_Offer @"offer" 15 | #define kMessageSubtype_Answer @"answer" 16 | #define kMessageSubtype_Candidate @"candidate" 17 | #define kMessageSubtype_Close @"close" 18 | #define kMessageSubtype_Placeholder @"placehoder" 19 | 20 | @interface Message : NSObject 21 | @property NSString *from; 22 | @property NSString *to; 23 | @property id content;//Maybe NSString or NSDictionary 24 | @property NSString *time; 25 | @property NSString *type;//text, signal 26 | @property NSString *subtype;//offer, answer, candidate, close 27 | 28 | - (NSDictionary *)toDictionary; 29 | 30 | - (instancetype)initWithPeerUID : (NSString *)peerUID 31 | Type: (NSString *)type 32 | SubType: (NSString *)subtype 33 | Content: (id)content; 34 | 35 | + (instancetype)textMessageWithPeerUID : (NSString *)peerUID 36 | content : (NSString *)content; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Chatchat/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /Chatchat/Message.m: -------------------------------------------------------------------------------- 1 | // 2 | // Message.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "Message.h" 10 | #import "UserManager.h" 11 | 12 | @implementation Message 13 | 14 | 15 | 16 | - (NSDictionary *)toDictionary{ 17 | return @{@"from": self.from, 18 | @"to" : self.to, 19 | @"content" : self.content, 20 | @"time" : self.time, 21 | @"type" : self.type, 22 | @"subtype" : self.subtype 23 | }; 24 | } 25 | 26 | - (instancetype)initWithPeerUID : (NSString *)peerUID 27 | Type: (NSString *)type 28 | SubType: (NSString *)subtype 29 | Content: (id)content{ 30 | if (self = [super init]) { 31 | self.from = [[UserManager sharedManager] localUser].uniqueID; 32 | self.to = peerUID; 33 | self.type = type; 34 | self.subtype = subtype; 35 | self.content = content; 36 | self.time = @""; 37 | } 38 | 39 | return self; 40 | } 41 | 42 | + (instancetype)textMessageWithPeerUID : (NSString *)peerUID 43 | content : (NSString *)content{ 44 | Message *message = [[Message alloc] init]; 45 | message.type = kMessageType_Text; 46 | message.subtype = kMessageSubtype_Placeholder; 47 | message.from = [[UserManager sharedManager] localUser].uniqueID; 48 | message.to = peerUID; 49 | message.content = content; 50 | message.time = @""; 51 | 52 | return message; 53 | } 54 | 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Chatchat/CallViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // CallViewController.h 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/7/11. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | @import WebRTC; 11 | 12 | //#import "RTCFileLogger.h" 13 | //#import "RTCPeerConnectionFactory.h" 14 | //#import "RTCPeerConnection.h" 15 | //#import "RTCPeerConnectionDelegate.h" 16 | //#import "RTCMediaConstraints.h" 17 | //#import "RTCMediaStream.h" 18 | //#import "RTCAudioTrack.h" 19 | //#import "RTCVideoTrack.h" 20 | //#import "RTCVideoCapturer.h" 21 | //#import "RTCPair.h" 22 | //#import "RTCSessionDescription.h" 23 | //#import "RTCSessionDescriptionDelegate.h" 24 | //#import "RTCEAGLVideoView.h" 25 | //#import "RTCICEServer.h" 26 | //#import "RTCICECandidate.h" 27 | //#import "RTCAVFoundationVideoSource.h" 28 | //#import "RTCVideoCapturer.h" 29 | 30 | #import "constants.h" 31 | #import "CommonDefines.h" 32 | 33 | @interface CallViewController : UIViewController 34 | 35 | @property (strong, nonatomic) RTCPeerConnectionFactory *factory; 36 | @property (strong, nonatomic) RTCPeerConnection *peerConnection; 37 | 38 | @property (strong) id socketIODelegate; 39 | @property (strong) User *peer; 40 | 41 | - (RTCMediaConstraints *)defaultMediaConstraints; 42 | - (RTCMediaConstraints *)defaultVideoConstraints; 43 | - (NSArray *)defaultIceServers; 44 | - (RTCMediaConstraints *)defaultPeerConnectionConstraints; 45 | 46 | - (void)didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error; 47 | - (void)didSetSessionDescriptionWithError:(NSError *)error; 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /Chatchat/VoiceCallViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // VoiceCallViewController.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/7. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | #import "VoiceCallViewController.h" 12 | 13 | @interface VoiceCallViewController () 14 | { 15 | IBOutlet UILabel *_callingTitle; 16 | IBOutlet UIButton *_hangupButton; 17 | } 18 | @end 19 | 20 | @implementation VoiceCallViewController 21 | 22 | - (void)viewDidLoad{ 23 | [super viewDidLoad]; 24 | 25 | _callingTitle.text = [NSString stringWithFormat:@"Calling %@", self.peer.name]; 26 | 27 | RTCMediaStream *localStream = [self.factory mediaStreamWithStreamId:@"localStream"]; 28 | RTCAudioTrack *audioTrack = [self.factory audioTrackWithTrackId:@"audio0"]; 29 | [localStream addAudioTrack : audioTrack]; 30 | 31 | [self.peerConnection addStream:localStream]; 32 | 33 | [self.peerConnection offerForConstraints:[self defaultOfferConstraints] 34 | completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) { 35 | [self didCreateSessionDescription:sdp error:error]; 36 | }]; 37 | } 38 | 39 | 40 | - (IBAction)hangupButtonPressed:(id)sender{ 41 | [self sendCloseSignal]; 42 | 43 | if (self.peerConnection) { 44 | [self.peerConnection close]; 45 | } 46 | 47 | [self dismissViewControllerAnimated:YES completion:nil]; 48 | } 49 | 50 | - (void)sendCloseSignal{ 51 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID 52 | Type:@"signal" 53 | SubType:@"close" 54 | Content:@"call is denied"]; 55 | [self.socketIODelegate sendMessage:message]; 56 | } 57 | 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Chatchat/RTCICECandidate+JSON.m: -------------------------------------------------------------------------------- 1 | // 2 | // RTCICECandidate+JSON.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/8. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "RTCICECandidate+JSON.h" 10 | 11 | static NSString const *kRTCICECandidateTypeKey = @"type"; 12 | static NSString const *kRTCICECandidateTypeValue = @"candidate"; 13 | static NSString const *kRTCICECandidateMidKey = @"sdpMid"; 14 | static NSString const *kRTCICECandidateMLineIndexKey = @"sdpMLineIndex"; 15 | static NSString const *kRTCICECandidateSdpKey = @"candidate"; 16 | 17 | @implementation RTCIceCandidate (JSON) 18 | 19 | + (RTCIceCandidate *)candidateFromJSONDictionary:(NSDictionary *)dictionary { 20 | NSString *mid = dictionary[kRTCICECandidateMidKey]; 21 | NSString *sdp = dictionary[kRTCICECandidateSdpKey]; 22 | NSNumber *num = dictionary[kRTCICECandidateMLineIndexKey]; 23 | int mLineIndex = (int)[num integerValue]; 24 | return [[RTCIceCandidate alloc] initWithSdp:sdp sdpMLineIndex:mLineIndex sdpMid:mid]; 25 | } 26 | 27 | - (NSDictionary *)toDictionary{ 28 | NSDictionary *json = @{ 29 | kRTCICECandidateMLineIndexKey : @(self.sdpMLineIndex), 30 | kRTCICECandidateMidKey : self.sdpMid, 31 | kRTCICECandidateSdpKey : self.sdp 32 | }; 33 | 34 | return json; 35 | } 36 | 37 | 38 | - (NSData *)JSONData { 39 | NSDictionary *json = [self toDictionary]; 40 | NSError *error = nil; 41 | NSData *data = 42 | [NSJSONSerialization dataWithJSONObject:json 43 | options:NSJSONWritingPrettyPrinted 44 | error:&error]; 45 | if (error) { 46 | NSLog(@"Error serializing JSON: %@", error); 47 | return nil; 48 | } 49 | return data; 50 | } 51 | 52 | - (NSString *)JSONString{ 53 | return [[NSString alloc] initWithData:[self JSONData] encoding:NSUTF8StringEncoding]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Server/public/css/main.css: -------------------------------------------------------------------------------- 1 | .rounded { 2 | border-radius: 5px; 3 | } 4 | 5 | .centered { 6 | display: block; 7 | margin: auto; 8 | } 9 | 10 | .relative { 11 | position: relative; 12 | } 13 | 14 | .navbar-brand { 15 | margin-left: 0px !important; 16 | } 17 | 18 | .navbar-default { 19 | -webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 20 | -moz-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 21 | box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 22 | } 23 | 24 | .navbar-header { 25 | padding-left: 40px; 26 | } 27 | 28 | .margin-sm { 29 | margin: 5px !important; 30 | } 31 | .margin-md { 32 | margin: 10px !important; 33 | } 34 | .margin-xl { 35 | margin: 20px !important; 36 | } 37 | .margin-bottom-sm { 38 | margin-bottom: 5px !important; 39 | } 40 | .margin-bottom-md { 41 | margin-bottom: 10px !important; 42 | } 43 | .margin-bottom-xl { 44 | margin-bottom: 20px !important; 45 | } 46 | 47 | .divider { 48 | width: 100%; 49 | text-align: center; 50 | } 51 | 52 | .divider hr { 53 | margin-left: auto; 54 | margin-right: auto; 55 | width: 45%; 56 | } 57 | 58 | .fa-2 { 59 | font-size: 2em !important; 60 | } 61 | .fa-3 { 62 | font-size: 4em !important; 63 | } 64 | .fa-4 { 65 | font-size: 7em !important; 66 | } 67 | .fa-5 { 68 | font-size: 12em !important; 69 | } 70 | .fa-6 { 71 | font-size: 20em !important; 72 | } 73 | 74 | div.no-video-container { 75 | position: relative; 76 | } 77 | 78 | .no-video-icon { 79 | width: 100%; 80 | height: 240px; 81 | text-align: center; 82 | } 83 | 84 | .no-video-text { 85 | text-align: center; 86 | position: absolute; 87 | bottom: 0px; 88 | right: 0px; 89 | left: 0px; 90 | font-size: 24px; 91 | } 92 | 93 | pre { 94 | white-space: pre-wrap; 95 | white-space: -moz-pre-wrap; 96 | white-space: -pre-wrap; 97 | white-space: -o-pre-wrap; 98 | word-wrap: break-word; 99 | } 100 | 101 | @keyframes pulsating { 102 | 30% { 103 | color: #FFD700; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Chatchat/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIBackgroundModes 26 | 27 | audio 28 | voip 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | NSAppTransportSecurity 52 | 53 | NSAllowsArbitraryLoads 54 | 55 | 56 | NSCameraUsageDescription 57 | Press OK 58 | NSMicrophoneUsageDescription 59 | Press OK 60 | 61 | 62 | -------------------------------------------------------------------------------- /Chatchat/RTCSessionDescription+JSON.m: -------------------------------------------------------------------------------- 1 | // 2 | // RTCSessionDescription+JSON.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/12/14. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "RTCSessionDescription+JSON.h" 10 | 11 | NSString *const kSDPTypeString[] = { 12 | @"offer", 13 | @"pranswer", 14 | @"answer" 15 | }; 16 | 17 | @implementation RTCSessionDescription (JSON) 18 | 19 | 20 | + (RTCSessionDescription *)sdpFromJSONDictionary:(NSDictionary *)dictionary{ 21 | NSString *sdp = [dictionary objectForKey:@"sdp"]; 22 | NSString *type = [dictionary objectForKey:@"type"]; 23 | 24 | RTCSdpType sdpType; 25 | if ([type isEqualToString:@"offer"]) { 26 | sdpType = RTCSdpTypeOffer; 27 | }else if ([type isEqualToString:@"answer"]){ 28 | sdpType = RTCSdpTypeAnswer; 29 | }else if ([type isEqualToString:@"pranswer"]){ 30 | sdpType = RTCSdpTypePrAnswer; 31 | } 32 | 33 | return [[RTCSessionDescription alloc] initWithType:sdpType sdp:sdp]; 34 | } 35 | 36 | + (RTCSessionDescription *)sdpFromJSONString:(NSString *)sdp{ 37 | RTCSessionDescription *outcome = nil; 38 | 39 | NSData *data = [sdp dataUsingEncoding:NSUTF8StringEncoding]; 40 | NSError *error; 41 | NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&error]; 42 | if (!error) { 43 | outcome = [self sdpFromJSONDictionary:dic]; 44 | } 45 | 46 | return outcome; 47 | } 48 | 49 | - (NSData *)JSONData{ 50 | NSDictionary *json = [self toDictionary]; 51 | NSError *error = nil; 52 | NSData *data = 53 | [NSJSONSerialization dataWithJSONObject:json 54 | options:NSJSONWritingPrettyPrinted 55 | error:&error]; 56 | if (error) { 57 | NSLog(@"Error serializing JSON: %@", error); 58 | return nil; 59 | } 60 | return data; 61 | } 62 | 63 | - (NSString *)JSONString{ 64 | return [[NSString alloc] initWithData:[self JSONData] encoding:NSUTF8StringEncoding]; 65 | } 66 | 67 | - (NSDictionary *)toDictionary{ 68 | return @{@"type" : kSDPTypeString[self.type], 69 | @"sdp" : self.sdp}; 70 | } 71 | 72 | 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /Chatchat/ChatSessionManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatSessionManager.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/2. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "ChatSessionManager.h" 10 | 11 | 12 | @interface ChatSessionManager() 13 | { 14 | NSMutableArray *_sessions; 15 | } 16 | 17 | @end 18 | 19 | 20 | @implementation ChatSessionManager 21 | 22 | + (instancetype)sharedManager{ 23 | static ChatSessionManager *singleton = nil; 24 | static dispatch_once_t onceToken; 25 | dispatch_once(&onceToken, ^{ 26 | singleton = [[ChatSessionManager alloc] init]; 27 | }); 28 | 29 | return singleton; 30 | } 31 | 32 | - (instancetype)init{ 33 | if (self = [super init]) { 34 | _sessions = [NSMutableArray array]; 35 | } 36 | 37 | return self; 38 | } 39 | 40 | - (ChatSession *)findSessionByPeer: (User *)user{ 41 | ChatSession *session = nil; 42 | 43 | for (ChatSession *item in _sessions) { 44 | if ([item.peer.uniqueID isEqualToString:user.uniqueID]) { 45 | session = item; 46 | break; 47 | } 48 | } 49 | 50 | return session; 51 | } 52 | 53 | - (ChatSession *)findSessionByUID: (NSString *)uid{ 54 | ChatSession *session = nil; 55 | 56 | for (ChatSession *item in _sessions) { 57 | if ([item.peer.uniqueID isEqualToString:uid]) { 58 | session = item; 59 | break; 60 | } 61 | } 62 | 63 | return session; 64 | } 65 | 66 | 67 | - (ChatSession *)createSessionWithPeer: (User *)user{ 68 | ChatSession *session = [self findSessionByPeer:user]; 69 | if (session) { 70 | return session; 71 | } 72 | 73 | session = [[ChatSession alloc] initWithPeer:user]; 74 | [_sessions addObject:session]; 75 | 76 | return session; 77 | } 78 | 79 | - (void)removeSession: (ChatSession *)session{ 80 | [_sessions removeObject:session]; 81 | } 82 | 83 | - (void)removeSessionByPeer: (User *)user{ 84 | ChatSession *session = [self findSessionByPeer:user]; 85 | 86 | if (session) { 87 | [self removeSession:session]; 88 | } 89 | } 90 | - (void)removeSessionByUID: (NSString *)uid{ 91 | ChatSession *session = [self findSessionByUID:uid]; 92 | 93 | if (session) { 94 | [self removeSession:session]; 95 | } 96 | } 97 | 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /Server/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var http = require('http').Server(app); 4 | var users = {}; 5 | var fs = require('fs'); 6 | var privateKey = fs.readFileSync('public/key/private.pem','utf8'); 7 | var certificate = fs.readFileSync('public/key/file.crt', 'utf8'); 8 | var credentials = {key: privateKey, cert: certificate}; 9 | var https = require('https').Server(credentials, app); 10 | var io = require('socket.io')(https); 11 | 12 | app.use(express.static(__dirname + '/public')); 13 | 14 | function findUserByUID(uid){ 15 | return users[uid]; 16 | } 17 | 18 | function censor(key, value) { 19 | if (key == 'socketid') { 20 | return undefined; 21 | } 22 | return value; 23 | } 24 | 25 | app.get('/', function(req, res){ 26 | res.sendFile(__dirname + '/index.html'); 27 | }); 28 | 29 | app.get('/webrtc', function(req, res){ 30 | console.log(__dirname); 31 | res.sendFile(__dirname + '/webrtc.html'); 32 | }); 33 | 34 | app.get('/listUsers', function(req, res){ 35 | res.end(JSON.stringify(users, censor)); 36 | }); 37 | 38 | 39 | io.on('connection', function(socket){ 40 | console.log('a user connected'); 41 | 42 | socket.on('disconnect', function(){ 43 | console.log('user disconnected'); 44 | if(socket.uuid){ 45 | var usr = findUserByUID(socket.uuid); 46 | delete users[socket.uuid]; 47 | socket.broadcast.emit('user leave', {id: usr.id, name:usr.name}); 48 | } 49 | }); 50 | 51 | socket.on('chat message', function(msg){ 52 | if(msg.to == 'all'){ 53 | socket.broadcast.emit('chat message', msg); 54 | }else{ 55 | var target = findUserByUID(msg.to); 56 | if(target){ 57 | socket.broadcast.to(target.socketid).emit('chat message', msg); 58 | //socket_to.emit("chat message", msg); 59 | }else{ 60 | socket.broadcast.emit("chat message", msg); 61 | } 62 | } 63 | }); 64 | 65 | socket.on('register', function(info){ 66 | console.log("register request: " + info.name); 67 | if(findUserByUID(info.name) == null){ 68 | var usr = {id: info.name, name: info.name, socketid: socket.id}; 69 | users[info.name] = usr; 70 | socket.emit('register succeed', {id: info.name, name: info.name}); 71 | socket.broadcast.emit('new user', {id: info.name, name: info.name}); 72 | socket.uuid = info.name; 73 | }else{ 74 | socket.emit('register failed', {info: "name exist"}); 75 | } 76 | 77 | }); 78 | 79 | }); 80 | 81 | var server = http.listen(3000, function(){ 82 | var host = server.address().address 83 | var port = server.address().port 84 | console.log('listening on http://%s:%s', host, port); 85 | }); 86 | 87 | https.listen(3001, function(){ 88 | console.log('listening on https://:3001'); 89 | }); 90 | 91 | -------------------------------------------------------------------------------- /Chatchat/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // Chatchat 4 | // 5 | // Created by wangruihit@gmail.com on 5/26/16. 6 | // Copyright © 2016 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | 21 | if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) { 22 | UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge categories:nil]; 23 | [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; 24 | } 25 | 26 | [UIApplication sharedApplication].applicationIconBadgeNumber = 0; 27 | return YES; 28 | } 29 | 30 | - (void)applicationWillResignActive:(UIApplication *)application { 31 | // 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. 32 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 33 | } 34 | 35 | - (void)applicationDidEnterBackground:(UIApplication *)application { 36 | // 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. 37 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 38 | [UIApplication sharedApplication].applicationIconBadgeNumber = 0; 39 | 40 | } 41 | 42 | - (void)applicationWillEnterForeground:(UIApplication *)application { 43 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 44 | } 45 | 46 | - (void)applicationDidBecomeActive:(UIApplication *)application { 47 | // 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. 48 | } 49 | 50 | - (void)applicationWillTerminate:(UIApplication *)application { 51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /Chatchat/UserManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // UserManager.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/2. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "UserManager.h" 10 | 11 | @interface UserManager () 12 | { 13 | User *_localUser; 14 | NSMutableArray *_users; 15 | } 16 | @end 17 | 18 | @implementation UserManager 19 | 20 | + (instancetype)sharedManager{ 21 | static UserManager *singleton = nil; 22 | static dispatch_once_t onceToken; 23 | dispatch_once(&onceToken, ^{ 24 | singleton = [[UserManager alloc] init]; 25 | }); 26 | 27 | return singleton; 28 | } 29 | 30 | - (instancetype)init{ 31 | if (self = [super init]) { 32 | _users = [NSMutableArray array]; 33 | } 34 | 35 | return self; 36 | } 37 | 38 | - (BOOL)isUserExist : (NSString *)uid{ 39 | BOOL exist = NO; 40 | 41 | for (User *item in _users) { 42 | if ([item.uniqueID isEqualToString:uid]) { 43 | exist = YES; 44 | break; 45 | } 46 | } 47 | return exist; 48 | } 49 | 50 | - (void)addUser : (User *)user{ 51 | [_users addObject:user]; 52 | } 53 | 54 | - (void)addUserWithUID: (NSString *)uid name: (NSString *)name{ 55 | User *user = [[User alloc] initWithName:name UID:uid]; 56 | [_users addObject:user]; 57 | } 58 | 59 | - (void)removeUserByUID: (NSString *)uid{ 60 | User *user = [self findUserByUID:uid]; 61 | [self removeUser:user]; 62 | } 63 | 64 | - (void)removeUser: (User *)user{ 65 | [_users removeObject:user]; 66 | } 67 | 68 | - (void)removeAllUsers{ 69 | [_users removeAllObjects]; 70 | } 71 | 72 | - (NSArray *)listUsers{ 73 | return _users; 74 | } 75 | 76 | - (NSUInteger)numberUsers{ 77 | return _users.count; 78 | } 79 | 80 | - (User *)findUserByUID: (NSString *)uid{ 81 | User *found = nil; 82 | for (User *item in _users) { 83 | if ([item.uniqueID isEqualToString:uid]) { 84 | found = item; 85 | break; 86 | } 87 | } 88 | 89 | return found; 90 | } 91 | 92 | - (User *)localUser{ 93 | return _localUser; 94 | } 95 | 96 | - (void)setLocalUserWithName: (NSString *)name UID: (NSString *)uid{ 97 | if ([self findUserByUID:uid]) { 98 | return; 99 | } 100 | 101 | _localUser = [[User alloc] initWithName:name UID:uid]; 102 | [self addUser:_localUser]; 103 | } 104 | 105 | - (void)replaceAllUsersWithNewUsers : (NSArray *)users{ 106 | @synchronized(self) { 107 | [self removeAllUsers]; 108 | for (User *item in users) { 109 | if (![self isUserExist:item.uniqueID]) { 110 | [self addUser:item]; 111 | } 112 | } 113 | } 114 | } 115 | 116 | 117 | @end 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chatchat 2 | A simple chat system demostrating how to build chat applications based on WebRTC, socket.io & Node.js 3 | 4 | This system including a web server, a web client, and an iOS client, supports realtime text/voice/video chat. 5 | 6 | The iOS client supports [voip socket](https://developer.apple.com/library/ios/technotes/tn2277/_index.html#//apple_ref/doc/uid/DTS40010841-CH1-SUBSECTION15) mode which means it could receive message even in background 7 | >Note: Since voip mode is deprecated from iOS 10, so you can no longer receive messages from background anymore. Refer [here](https://forums.developer.apple.com/thread/50106). 8 | 9 | # About WebRTC 10 | WebRTC is an open framework for the web that enables Real Time Communications in the browser. It includes the fundamental building blocks for high-quality communications on the web, such as network, audio and video components used in voice and video chat applications. 11 | 12 | Home page : https://webrtc.org/ 13 | 14 | Source : https://chromium.googlesource.com/external/webrtc 15 | 16 | # About Socket.io 17 | Socket.IO is a JavaScript library for realtime web applications. It enables realtime, bi-directional communication between web clients and servers. It has two parts: a client-side library that runs in the browser, and a server-side library for node.js. Both components have a nearly identical API. Like node.js, it is event-driven. 18 | 19 | Socket.IO primarily uses the WebSocket protocol with polling as a fallback option, while providing the same interface. Although it can be used as simply a wrapper for WebSocket, it provides many more features, including broadcasting to multiple sockets, storing data associated with each client, and asynchronous I/O. 20 | 21 | Home page : http://socket.io/ 22 | 23 | Socket.io webserver : https://github.com/socketio/socket.io 24 | 25 | Socket.io swift client : https://github.com/socketio/socket.io-client-swift 26 | 27 | 28 | # Deploy steps 29 | ## Create SSL keys 30 | First, You need to create your own SSL keys for your https server. 31 | ``` 32 | cd Server/public/key 33 | openssl genrsa 1024 > private.pem 34 | openssl req -new -key private.pem -out csr.pem 35 | openssl x509 -req -days 365 -in csr.pem -signkey private.pem -out file.crt 36 | 37 | ``` 38 | ## Run your server 39 | Make sure [Node](https://nodejs.org/en/) is installed first, then open your terminal 40 | ``` 41 | cd Server 42 | npm install 43 | node server.js 44 | ``` 45 | ## Run your web client 46 | - open [localhost:3001](https://localhost:3001) on your browser(prefer Chrome or Firefox) 47 | - ignore the secure warning and proceed 48 | - type in your nickname and submit 49 | 50 | ## Run your iOS client 51 | - install dependency with `pod install` 52 | - open xcworkspace file 53 | - build and run 54 | - upon application launch, type in your host address(server address you are running on) 55 | - choose anyone online and start video chat 56 | 57 | Enjoy chating! :smile: 58 | -------------------------------------------------------------------------------- /Chatchat/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Chatchat/OutgoingViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // OutgoingViewController.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/7/11. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "OutgoingViewController.h" 10 | 11 | #import "RTCICECandidate+JSON.h" 12 | 13 | #import "RTCSessionDescription+JSON.h" 14 | 15 | @interface OutgoingViewController () 16 | 17 | @end 18 | 19 | @implementation OutgoingViewController 20 | 21 | - (void)viewDidLoad { 22 | [super viewDidLoad]; 23 | // Do any additional setup after loading the view. 24 | } 25 | 26 | - (void)didReceiveMemoryWarning { 27 | [super didReceiveMemoryWarning]; 28 | // Dispose of any resources that can be recreated. 29 | } 30 | 31 | - (RTCMediaConstraints *)defaultOfferConstraints { 32 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true", 33 | @"OfferToReceiveVideo": @"true"}; 34 | NSDictionary *optionalConstraints = @{@"DtlsSrtpKeyAgreement" : @"false"}; 35 | 36 | RTCMediaConstraints* constraints = 37 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints 38 | optionalConstraints:optionalConstraints]; 39 | return constraints; 40 | } 41 | 42 | 43 | - (void)onMessage: (Message *)message{ 44 | [super onMessage:message]; 45 | 46 | if ([message.type isEqualToString:@"signal"]) { 47 | if ([message.subtype isEqualToString:@"offer"]) { 48 | // 49 | //create peerconnection 50 | //set remote desc 51 | //I'm calling out, so I don't accept offer right now 52 | 53 | }else if([message.subtype isEqualToString:@"answer"]){ 54 | __weak OutgoingViewController *weakSelf = self; 55 | 56 | RTCSessionDescription *remoteDesc = [RTCSessionDescription sdpFromJSONDictionary:message.content]; 57 | [self.peerConnection setRemoteDescription:remoteDesc completionHandler:^(NSError * _Nullable error) { 58 | [weakSelf didSetSessionDescriptionWithError:error]; 59 | }]; 60 | 61 | }else if([message.subtype isEqualToString:@"candidate"]){ 62 | NSLog(@"got candidate from peer: %@", message.content); 63 | 64 | RTCIceCandidate *candidate = [RTCIceCandidate candidateFromJSONDictionary:message.content]; 65 | [self.peerConnection addIceCandidate:candidate]; 66 | }else if ([message.subtype isEqualToString:@"close"]){ 67 | [self handleRemoteHangup]; 68 | } 69 | } 70 | } 71 | 72 | - (void)handleRemoteHangup{ 73 | [self.peerConnection close]; 74 | 75 | //TODO play busy tone 76 | [self dismissViewControllerAnimated:YES completion:nil]; 77 | } 78 | 79 | /* 80 | #pragma mark - Navigation 81 | 82 | // In a storyboard-based application, you will often want to do a little preparation before navigation 83 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 84 | // Get the new view controller using [segue destinationViewController]. 85 | // Pass the selected object to the new view controller. 86 | } 87 | */ 88 | 89 | @end 90 | -------------------------------------------------------------------------------- /Chatchat/ARDSDPUtils.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | #import "ARDSDPUtils.h" 12 | 13 | #import "WebRTC/RTCLogging.h" 14 | #import "WebRTC/RTCSessionDescription.h" 15 | 16 | @implementation ARDSDPUtils 17 | 18 | + (RTCSessionDescription *) 19 | descriptionForDescription:(RTCSessionDescription *)description 20 | preferredVideoCodec:(NSString *)codec { 21 | NSString *sdpString = description.sdp; 22 | NSString *lineSeparator = @"\n"; 23 | NSString *mLineSeparator = @" "; 24 | // Copied from PeerConnectionClient.java. 25 | // TODO(tkchin): Move this to a shared C++ file. 26 | NSMutableArray *lines = 27 | [NSMutableArray arrayWithArray: 28 | [sdpString componentsSeparatedByString:lineSeparator]]; 29 | NSInteger mLineIndex = -1; 30 | NSString *codecRtpMap = nil; 31 | // a=rtpmap: / 32 | // [/] 33 | NSString *pattern = 34 | [NSString stringWithFormat:@"^a=rtpmap:(\\d+) %@(/\\d+)+[\r]?$", codec]; 35 | NSRegularExpression *regex = 36 | [NSRegularExpression regularExpressionWithPattern:pattern 37 | options:0 38 | error:nil]; 39 | for (NSInteger i = 0; (i < lines.count) && (mLineIndex == -1 || !codecRtpMap); 40 | ++i) { 41 | NSString *line = lines[i]; 42 | if ([line hasPrefix:@"m=video"]) { 43 | mLineIndex = i; 44 | continue; 45 | } 46 | NSTextCheckingResult *codecMatches = 47 | [regex firstMatchInString:line 48 | options:0 49 | range:NSMakeRange(0, line.length)]; 50 | if (codecMatches) { 51 | codecRtpMap = 52 | [line substringWithRange:[codecMatches rangeAtIndex:1]]; 53 | continue; 54 | } 55 | } 56 | if (mLineIndex == -1) { 57 | RTCLog(@"No m=video line, so can't prefer %@", codec); 58 | return description; 59 | } 60 | if (!codecRtpMap) { 61 | RTCLog(@"No rtpmap for %@", codec); 62 | return description; 63 | } 64 | NSArray *origMLineParts = 65 | [lines[mLineIndex] componentsSeparatedByString:mLineSeparator]; 66 | if (origMLineParts.count > 3) { 67 | NSMutableArray *newMLineParts = 68 | [NSMutableArray arrayWithCapacity:origMLineParts.count]; 69 | NSInteger origPartIndex = 0; 70 | // Format is: m= ... 71 | [newMLineParts addObject:origMLineParts[origPartIndex++]]; 72 | [newMLineParts addObject:origMLineParts[origPartIndex++]]; 73 | [newMLineParts addObject:origMLineParts[origPartIndex++]]; 74 | [newMLineParts addObject:codecRtpMap]; 75 | for (; origPartIndex < origMLineParts.count; ++origPartIndex) { 76 | if (![codecRtpMap isEqualToString:origMLineParts[origPartIndex]]) { 77 | [newMLineParts addObject:origMLineParts[origPartIndex]]; 78 | } 79 | } 80 | NSString *newMLine = 81 | [newMLineParts componentsJoinedByString:mLineSeparator]; 82 | [lines replaceObjectAtIndex:mLineIndex 83 | withObject:newMLine]; 84 | } else { 85 | RTCLogWarning(@"Wrong SDP media description format: %@", lines[mLineIndex]); 86 | } 87 | NSString *mangledSdpString = [lines componentsJoinedByString:lineSeparator]; 88 | return [[RTCSessionDescription alloc] initWithType:description.type 89 | sdp:mangledSdpString]; 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /Server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | chatchat: Video Call Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 31 |
32 |
33 |
34 |

Demo details

35 |

This Video Call demo is basically an example of how you can achieve a 36 | basic WebRTC video call.

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |

Local Stream 64 |

65 |
66 |
67 |
68 |
69 | 70 | 71 |
72 |
73 |
74 |
75 |
76 |

Remote Stream

77 |
78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 | 90 |
91 | 93 |
94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Chatchat/ChatViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ChatViewController.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/1. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "ChatViewController.h" 10 | #import "UserManager.h" 11 | #import "ChatSessionManager.h" 12 | 13 | @interface ChatViewController () 14 | { 15 | ChatSession *_session; 16 | UserManager *_userManager; 17 | ChatSessionManager *_sessionManager; 18 | } 19 | 20 | @property (strong) IBOutlet UITableView *tableView; 21 | @property (strong) IBOutlet UITextField *textField; 22 | @property (strong) IBOutlet UIScrollView *scrollView; 23 | 24 | @end 25 | 26 | @implementation ChatViewController 27 | 28 | - (void)viewDidLoad { 29 | [super viewDidLoad]; 30 | // Do any additional setup after loading the view. 31 | 32 | _userManager = [UserManager sharedManager]; 33 | _sessionManager = [ChatSessionManager sharedManager]; 34 | 35 | _session = [_sessionManager createSessionWithPeer:self.peer]; 36 | } 37 | 38 | - (void)didReceiveMemoryWarning { 39 | [super didReceiveMemoryWarning]; 40 | // Dispose of any resources that can be recreated. 41 | } 42 | 43 | - (void)viewDidAppear:(BOOL)animated{ 44 | [super viewDidAppear:animated]; 45 | 46 | UIEdgeInsets contentInsets = UIEdgeInsetsZero; 47 | self.scrollView.contentInset = contentInsets; 48 | self.scrollView.scrollIndicatorInsets = contentInsets; 49 | 50 | [[NSNotificationCenter defaultCenter] addObserver:self 51 | selector:@selector(keyboardWasShown:) 52 | name:UIKeyboardDidShowNotification 53 | object:nil]; 54 | 55 | [[NSNotificationCenter defaultCenter] addObserver:self 56 | selector:@selector(keyboardWillBeHidden:) 57 | name:UIKeyboardWillHideNotification 58 | object:nil]; 59 | } 60 | 61 | - (void)viewDidDisappear:(BOOL)animated{ 62 | [super viewDidDisappear:animated]; 63 | 64 | [_session clearUnread]; 65 | 66 | [[NSNotificationCenter defaultCenter] removeObserver:self 67 | name:UIKeyboardDidShowNotification 68 | object:nil]; 69 | [[NSNotificationCenter defaultCenter] removeObserver:self 70 | name:UIKeyboardWillHideNotification 71 | object:nil]; 72 | } 73 | 74 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ 75 | return [[_session listAllMessages] count]; 76 | } 77 | 78 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 79 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ChatTableViewCell" forIndexPath:indexPath]; 80 | Message *message = [[_session listAllMessages] objectAtIndex:indexPath.row]; 81 | if ([message.from isEqualToString: [_userManager localUser].uniqueID]) { 82 | cell.textLabel.text = @""; 83 | cell.detailTextLabel.text = message.content; 84 | }else{ 85 | cell.textLabel.text = message.content; 86 | cell.detailTextLabel.text = nil; 87 | } 88 | return cell; 89 | } 90 | 91 | - (void)onMessage: (Message *)message{ 92 | [_session onMessage:message]; 93 | dispatch_async(dispatch_get_main_queue(), ^{ 94 | [self.tableView reloadData]; 95 | }); 96 | } 97 | 98 | // Called when the UIKeyboardDidShowNotification is sent. 99 | - (void)keyboardWasShown:(NSNotification*)aNotification 100 | { 101 | NSDictionary* info = [aNotification userInfo]; 102 | CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; 103 | 104 | UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0); 105 | self.scrollView.contentInset = contentInsets; 106 | self.scrollView.scrollIndicatorInsets = contentInsets; 107 | 108 | // If active text field is hidden by keyboard, scroll it so it's visible 109 | // Your app might not need or want this behavior. 110 | CGRect aRect = self.view.frame; 111 | aRect.size.height -= kbSize.height; 112 | if (!CGRectContainsPoint(aRect, self.textField.frame.origin) ) { 113 | [self.scrollView scrollRectToVisible:self.textField.frame animated:YES]; 114 | } 115 | } 116 | 117 | // Called when the UIKeyboardWillHideNotification is sent 118 | - (void)keyboardWillBeHidden:(NSNotification*)aNotification 119 | { 120 | UIEdgeInsets contentInsets = UIEdgeInsetsZero; 121 | self.scrollView.contentInset = contentInsets; 122 | self.scrollView.scrollIndicatorInsets = contentInsets; 123 | } 124 | 125 | 126 | - (BOOL)textFieldShouldReturn:(UITextField *)textField{ 127 | [textField resignFirstResponder]; 128 | return YES; 129 | } 130 | 131 | - (void)textFieldDidEndEditing:(UITextField *)textField{ 132 | Message *message = [Message textMessageWithPeerUID:self.peer.uniqueID 133 | content:textField.text]; 134 | [self.socketIODelegate sendMessage:message]; 135 | 136 | //'received' a local message 137 | [self onMessage:message]; 138 | textField.text = nil; 139 | } 140 | 141 | 142 | /* 143 | #pragma mark - Navigation 144 | 145 | // In a storyboard-based application, you will often want to do a little preparation before navigation 146 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 147 | // Get the new view controller using [segue destinationViewController]. 148 | // Pass the selected object to the new view controller. 149 | } 150 | */ 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /Chatchat/VideoCallViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // VideoCallViewController.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/24. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "VideoCallViewController.h" 10 | 11 | #import 12 | 13 | @interface VideoCallViewController () 14 | { 15 | RTCVideoTrack *_localVideoTrack; 16 | RTCVideoTrack *_remoteVideoTrack; 17 | 18 | IBOutlet UILabel *_callingTitle; 19 | IBOutlet UIButton *_hangupButton; 20 | 21 | RTCEAGLVideoView *_cameraPreviewView; 22 | } 23 | @end 24 | 25 | 26 | @implementation VideoCallViewController 27 | 28 | - (RTCMediaConstraints *)defaultOfferConstraints { 29 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true", 30 | @"OfferToReceiveVideo": @"true"}; 31 | NSDictionary *optionalConstraints = @{@"DtlsSrtpKeyAgreement" : @"false"}; 32 | 33 | RTCMediaConstraints* constraints = 34 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints 35 | optionalConstraints:optionalConstraints]; 36 | return constraints; 37 | } 38 | 39 | - (void)viewDidLoad{ 40 | [super viewDidLoad]; 41 | 42 | _callingTitle.text = [NSString stringWithFormat:@"Calling %@", self.peer.name]; 43 | _cameraPreviewView = nil; 44 | 45 | [self startLocalStream]; 46 | } 47 | 48 | - (void)startLocalStream{ 49 | RTCMediaStream *localStream = [self.factory mediaStreamWithStreamId:@"localStream"]; 50 | RTCAudioTrack *audioTrack = [self.factory audioTrackWithTrackId:@"audio0"]; 51 | [localStream addAudioTrack : audioTrack]; 52 | 53 | //How to initialize souce?? 54 | RTCVideoSource *source = [self.factory avFoundationVideoSourceWithConstraints:[self defaultVideoConstraints]]; 55 | RTCVideoTrack *localVideoTrack = [self.factory videoTrackWithSource:source trackId:@"video0"]; 56 | [localStream addVideoTrack:localVideoTrack]; 57 | _localVideoTrack = localVideoTrack; 58 | 59 | [self.peerConnection addStream:localStream]; 60 | 61 | [self.peerConnection offerForConstraints:[self defaultOfferConstraints] 62 | completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) { 63 | [self didCreateSessionDescription:sdp error:error]; 64 | }]; 65 | 66 | [self startPreview]; 67 | } 68 | 69 | - (void)startPreview{ 70 | if (_cameraPreviewView.superview == self.view) { 71 | return; 72 | } 73 | 74 | dispatch_async(dispatch_get_main_queue(), ^{ 75 | _cameraPreviewView = [[RTCEAGLVideoView alloc] initWithFrame: self.view.bounds]; 76 | _cameraPreviewView.delegate = self; 77 | 78 | [self.view addSubview:_cameraPreviewView]; 79 | [_localVideoTrack addRenderer:_cameraPreviewView]; 80 | 81 | [self.view bringSubviewToFront:_callingTitle]; 82 | [self.view bringSubviewToFront:_hangupButton]; 83 | 84 | }); 85 | } 86 | 87 | 88 | #pragma mark -- RTCEAGLVideoViewDelegate -- 89 | - (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size{ 90 | NSLog(@"Video size changed to: %d, %d", (int)size.width, (int)size.height); 91 | } 92 | 93 | 94 | #pragma mark -- peerConnection delegate override -- 95 | 96 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 97 | didAddStream:(RTCMediaStream *)stream{ 98 | [super peerConnection:peerConnection didAddStream:stream]; 99 | 100 | NSLog(@"%s, video tracks: %lu", __FILE__, (unsigned long)stream.videoTracks.count); 101 | 102 | if (stream.videoTracks.count) { 103 | _remoteVideoTrack = [stream.videoTracks lastObject]; 104 | 105 | dispatch_async(dispatch_get_main_queue(), ^{ 106 | // Scale local view to upright corner 107 | if (_cameraPreviewView) { 108 | [UIView animateWithDuration:0.5 animations:^{ 109 | NSUInteger width = 100; 110 | float screenRatio = [[UIScreen mainScreen] bounds].size.height / [[UIScreen mainScreen] bounds].size.width; 111 | NSUInteger height = width * screenRatio; 112 | _cameraPreviewView.frame = CGRectMake(self.view.bounds.size.width - 100, 0, width, height); 113 | } completion:^(BOOL finished) { 114 | // Create a new render view with a size of your choice 115 | RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:self.view.bounds]; 116 | renderView.delegate = self; 117 | [_remoteVideoTrack addRenderer:renderView]; 118 | [self.view addSubview:renderView]; 119 | 120 | if (_cameraPreviewView) { 121 | [self.view bringSubviewToFront:_cameraPreviewView]; 122 | } 123 | [self.view bringSubviewToFront:_callingTitle]; 124 | [self.view bringSubviewToFront:_hangupButton]; 125 | 126 | }]; 127 | } 128 | }); 129 | } 130 | } 131 | 132 | 133 | - (IBAction)hangupButtonPressed:(id)sender{ 134 | [self sendCloseSignal]; 135 | 136 | if (self.peerConnection) { 137 | [self.peerConnection close]; 138 | } 139 | 140 | [self dismissViewControllerAnimated:YES completion:nil]; 141 | } 142 | 143 | - (void)sendCloseSignal{ 144 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID 145 | Type:@"signal" 146 | SubType:@"close" 147 | Content:@"call is denied"]; 148 | [self.socketIODelegate sendMessage:message]; 149 | } 150 | 151 | @end 152 | -------------------------------------------------------------------------------- /Chatchat/IncomingCallViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // IncomingCallViewController.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/6/8. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | #import 9 | 10 | #import "IncomingCallViewController.h" 11 | #import "RTCICECandidate+JSON.h" 12 | #import "RTCSessionDescription+JSON.h" 13 | 14 | @interface IncomingCallViewController () 15 | { 16 | RTCVideoTrack *_localVideoTrack; 17 | RTCVideoTrack *_remoteVideoTrack; 18 | 19 | BOOL _accepted; 20 | 21 | BOOL _videoEnabled; 22 | RTCEAGLVideoView *_cameraPreviewView; 23 | 24 | } 25 | 26 | @property (strong) IBOutlet UIButton *acceptButton; 27 | @property (strong) IBOutlet UIButton *denyButton; 28 | @property (strong) IBOutlet UILabel *callTitle; 29 | @end 30 | 31 | @implementation IncomingCallViewController 32 | 33 | - (RTCMediaConstraints *)defaultAnswerConstraints{ 34 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true", 35 | @"OfferToReceiveVideo": @"true"}; 36 | RTCMediaConstraints* constraints = 37 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints 38 | optionalConstraints:nil]; 39 | return constraints; 40 | } 41 | 42 | - (RTCMediaConstraints *)defaultVideoAnswerConstraints{ 43 | NSDictionary *mandatoryConstraints = @{@"OfferToReceiveAudio": @"true", 44 | @"OfferToReceiveVideo": @"true"}; 45 | RTCMediaConstraints* constraints = 46 | [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints 47 | optionalConstraints:nil]; 48 | return constraints; 49 | } 50 | 51 | - (void)viewDidLoad{ 52 | [super viewDidLoad]; 53 | 54 | _videoEnabled = NO; 55 | _cameraPreviewView = nil; 56 | _accepted = NO; 57 | 58 | _localVideoTrack = nil; 59 | _remoteVideoTrack = nil; 60 | 61 | NSString *title = [NSString stringWithFormat:@"Call From %@", self.peer.name]; 62 | NSString *sdp = [self.offer.content objectForKey:@"sdp"]; 63 | if ([sdp containsString:@"video"]) { 64 | _videoEnabled = YES; 65 | title = [NSString stringWithFormat:@"Video Call From %@", self.peer.name]; 66 | } 67 | 68 | self.callTitle.text = title; 69 | 70 | __weak IncomingCallViewController *weakSelf = self; 71 | RTCSessionDescription *offer = [[RTCSessionDescription alloc] initWithType:RTCSdpTypeOffer sdp:sdp]; 72 | [self.peerConnection setRemoteDescription:offer 73 | completionHandler:^(NSError * _Nullable error) { 74 | [weakSelf didSetSessionDescriptionWithError:error]; 75 | }]; 76 | 77 | RTCMediaStream *localStream = [self.factory mediaStreamWithStreamId:@"localStream"]; 78 | RTCAudioTrack *audioTrack = [self.factory audioTrackWithTrackId:@"audio0"]; 79 | [localStream addAudioTrack : audioTrack]; 80 | 81 | if (_videoEnabled) { 82 | RTCVideoSource *source = [self.factory avFoundationVideoSourceWithConstraints:[self defaultVideoConstraints]]; 83 | RTCVideoTrack *localVideoTrack = [self.factory videoTrackWithSource:source trackId:@"video0"]; 84 | [localStream addVideoTrack:localVideoTrack]; 85 | 86 | _localVideoTrack = localVideoTrack; 87 | 88 | [self startPreview]; 89 | } 90 | 91 | [self.peerConnection addStream:localStream]; 92 | 93 | NSLog(@"%s, presenting view with offer: %@", __FILE__, self.offer.content); 94 | } 95 | 96 | - (void)viewDidAppear:(BOOL)animated{ 97 | [super viewDidAppear:animated]; 98 | 99 | //deal with any pending signal message after view loaded 100 | for (Message *item in self.pendingMessages) { 101 | [self onMessage:item]; 102 | } 103 | } 104 | 105 | - (void)startPreview{ 106 | if (_cameraPreviewView.superview == self.view) { 107 | return; 108 | } 109 | dispatch_async(dispatch_get_main_queue(), ^{ 110 | NSUInteger width = 100; 111 | float screenRatio = [[UIScreen mainScreen] bounds].size.height / [[UIScreen mainScreen] bounds].size.width; 112 | NSUInteger height = width * screenRatio; 113 | _cameraPreviewView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width - width, 0, width, height)]; 114 | _cameraPreviewView.delegate = self; 115 | [_localVideoTrack addRenderer:_cameraPreviewView]; 116 | 117 | [self.view addSubview:_cameraPreviewView]; 118 | [self.view bringSubviewToFront:self.callTitle]; 119 | [self.view bringSubviewToFront:self.acceptButton]; 120 | 121 | }); 122 | } 123 | 124 | - (void)startRemoteVideo{ 125 | dispatch_async(dispatch_get_main_queue(), ^{ 126 | RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:self.view.bounds]; 127 | renderView.delegate = self; 128 | [_remoteVideoTrack addRenderer:renderView]; 129 | [self.view addSubview:renderView]; 130 | 131 | if (_cameraPreviewView) { 132 | [self.view bringSubviewToFront:_cameraPreviewView]; 133 | } 134 | [self.view bringSubviewToFront:self.acceptButton]; 135 | [self.view bringSubviewToFront:self.callTitle]; 136 | 137 | }); 138 | } 139 | 140 | #pragma mark -- RTCEAGLVideoViewDelegate -- 141 | - (void)videoView:(RTCEAGLVideoView*)videoView didChangeVideoSize:(CGSize)size{ 142 | NSLog(@"Video size changed to: %d, %d", (int)size.width, (int)size.height); 143 | } 144 | 145 | #pragma mark -- peerConnection delegate override -- 146 | 147 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 148 | didAddStream:(RTCMediaStream *)stream{ 149 | // Create a new render view with a size of your choice 150 | [super peerConnection:peerConnection didAddStream:stream]; 151 | 152 | if (stream.videoTracks.count) { 153 | _remoteVideoTrack = [stream.videoTracks lastObject]; 154 | } 155 | } 156 | 157 | - (void)onMessage: (Message *)message{ 158 | if ([message.type isEqualToString:@"signal"]) { 159 | if ([message.subtype isEqualToString:@"offer"]) { 160 | // 161 | //create peerconnection 162 | //set remote desc 163 | //I'm calling in, so I don't accept offer right now 164 | 165 | }else if([message.subtype isEqualToString:@"answer"]){ 166 | 167 | }else if([message.subtype isEqualToString:@"candidate"]){ 168 | NSLog(@"%s, got candidate from peer: %@", __FILE__, message.content); 169 | RTCIceCandidate *candidate = [RTCIceCandidate candidateFromJSONDictionary:message.content]; 170 | [self.peerConnection addIceCandidate:candidate]; 171 | 172 | }else if([message.subtype isEqualToString:@"close"]){ 173 | [self handleRemoteHangup]; 174 | } 175 | } 176 | } 177 | 178 | - (void)handleRemoteHangup{ 179 | [self.peerConnection close]; 180 | 181 | //TODO play busy tone 182 | [self dismissViewControllerAnimated:YES completion:nil]; 183 | } 184 | 185 | 186 | - (IBAction)acceptButtonPressed:(id)sender{ 187 | if (_accepted) { 188 | //close pc 189 | //dismiss this vc 190 | [self sendCloseSignal]; 191 | [self.peerConnection close]; 192 | 193 | [self dismissViewControllerAnimated:YES completion:nil]; 194 | 195 | }else{ 196 | NSLog(@"call accepted"); 197 | 198 | RTCMediaConstraints *constraints = [self defaultAnswerConstraints]; 199 | if (_videoEnabled) { 200 | constraints = [self defaultVideoAnswerConstraints]; 201 | } 202 | [self.peerConnection answerForConstraints:constraints 203 | completionHandler:^(RTCSessionDescription * _Nullable sdp, NSError * _Nullable error) { 204 | [self didCreateSessionDescription:sdp error:error]; 205 | }]; 206 | 207 | _accepted = YES; 208 | 209 | self.denyButton.hidden = YES; 210 | 211 | [self.acceptButton setTitle:@"Hangup" forState:UIControlStateNormal]; 212 | 213 | if (_videoEnabled) { 214 | dispatch_async(dispatch_get_main_queue(), ^{ 215 | [self startPreview]; 216 | [self startRemoteVideo]; 217 | }); 218 | } 219 | } 220 | } 221 | 222 | - (IBAction)denyButtonPressed:(id)sender{ 223 | //signal denied 224 | //close pc 225 | //dismiss this vc 226 | NSLog(@"call denied"); 227 | 228 | [self sendCloseSignal]; 229 | [self.peerConnection close]; 230 | [self dismissViewControllerAnimated:YES completion:nil]; 231 | } 232 | 233 | - (void)sendCloseSignal{ 234 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID 235 | Type:@"signal" 236 | SubType:@"close" 237 | Content:@"call is denied"]; 238 | [self.socketIODelegate sendMessage:message]; 239 | } 240 | 241 | @end 242 | 243 | -------------------------------------------------------------------------------- /Chatchat/CallViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // CallViewController.m 3 | // Chatchat 4 | // 5 | // Created by WangRui on 16/7/11. 6 | // Copyright © 2016年 Beta.Inc. All rights reserved. 7 | // 8 | 9 | #import "CallViewController.h" 10 | 11 | #import "RTCICECandidate+JSON.h" 12 | 13 | #import "ARDSDPUtils.h" 14 | 15 | #import "RTCSessionDescription+JSON.h" 16 | 17 | //enum to string. 18 | //Modify these names when defination changed 19 | NSString *const RTCIceStateNames[] = { 20 | @"RTCICEConnectionNew", 21 | @"RTCICEConnectionChecking", 22 | @"RTCICEConnectionConnected", 23 | @"RTCICEConnectionCompleted", 24 | @"RTCICEConnectionFailed", 25 | @"RTCICEConnectionDisconnected", 26 | @"RTCICEConnectionClosed", 27 | @"RTCICEConnectionMax" 28 | }; 29 | 30 | @interface CallViewController () 31 | { 32 | RTCFileLogger *_logger; 33 | NSTimer *_statTimer; 34 | } 35 | 36 | @end 37 | 38 | @implementation CallViewController 39 | 40 | - (RTCMediaConstraints *)defaultMediaConstraints{ 41 | return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil]; 42 | } 43 | 44 | - (RTCMediaConstraints *)defaultVideoConstraints{ 45 | float screenRatio = [[UIScreen mainScreen] bounds].size.height / [[UIScreen mainScreen] bounds].size.width; 46 | NSDictionary *mandatoryConstraints = @{@"minAspectRatio" : [NSString stringWithFormat:@"%.1f", screenRatio - 0.1], 47 | @"maxAspectRatio" : [NSString stringWithFormat:@"%.1f", screenRatio + 0.1]}; 48 | return [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:nil]; 49 | } 50 | 51 | - (NSArray *)defaultIceServers{ 52 | return @[[[RTCIceServer alloc] initWithURLStrings:@[kDefaultSTUNServerUrl] username:@"" credential:@""]]; 53 | } 54 | 55 | - (RTCMediaConstraints *)defaultPeerConnectionConstraints { 56 | NSDictionary *optionalConstraints = @{@"DtlsSrtpKeyAgreement" : @"true"}; 57 | RTCMediaConstraints* constraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil 58 | optionalConstraints:optionalConstraints]; 59 | return constraints; 60 | } 61 | 62 | 63 | - (void)viewDidLoad { 64 | [super viewDidLoad]; 65 | // Do any additional setup after loading the view. 66 | 67 | _logger = [[RTCFileLogger alloc] init]; 68 | [_logger start]; 69 | 70 | RTCInitializeSSL(); 71 | 72 | _factory = [[RTCPeerConnectionFactory alloc] init]; 73 | 74 | RTCConfiguration *configure = [[RTCConfiguration alloc] init]; 75 | configure.iceServers = [self defaultIceServers]; 76 | 77 | _peerConnection = [_factory peerConnectionWithConfiguration:configure 78 | constraints:[self defaultMediaConstraints] 79 | delegate:self]; 80 | 81 | } 82 | 83 | - (void)didReceiveMemoryWarning { 84 | [super didReceiveMemoryWarning]; 85 | // Dispose of any resources that can be recreated. 86 | } 87 | 88 | - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion{ 89 | [super dismissViewControllerAnimated:flag completion:completion]; 90 | 91 | if (_statTimer) { 92 | [_statTimer invalidate]; 93 | } 94 | 95 | RTCCleanupSSL(); 96 | } 97 | 98 | #pragma mark -- RTCSessionDescriptionDelegate -- 99 | - (void)didCreateSessionDescription:(RTCSessionDescription *)sdp error:(NSError *)error 100 | { 101 | NSLog(@"didCreateSessionDescription : %ld:%@", (long)sdp.type, sdp.description); 102 | RTCSessionDescription *descriptionPreferH264 = [ARDSDPUtils descriptionForDescription:sdp preferredVideoCodec:@"H264"]; 103 | 104 | __weak CallViewController *weakSelf = self; 105 | [self.peerConnection setLocalDescription:descriptionPreferH264 106 | completionHandler:^(NSError * _Nullable error) { 107 | [weakSelf didSetSessionDescriptionWithError:error]; 108 | }]; 109 | 110 | NSString *subtype = (sdp.type == RTCSdpTypeOffer) ? @"offer" : @"answer"; 111 | // Send offer through the signaling channel of our application 112 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID 113 | Type:@"signal" 114 | SubType:subtype 115 | Content:[descriptionPreferH264 toDictionary]]; 116 | 117 | [self.socketIODelegate sendMessage:message]; 118 | } 119 | 120 | - (void)didSetSessionDescriptionWithError:(NSError *)error 121 | { 122 | if (_peerConnection.signalingState == RTCSignalingStateHaveLocalOffer) { 123 | NSLog(@"have local offer"); 124 | }else if (_peerConnection.signalingState == RTCSignalingStateHaveRemoteOffer){ 125 | NSLog(@"have remote offer"); 126 | }else if(_peerConnection.signalingState == RTCSignalingStateHaveRemotePrAnswer){ 127 | NSLog(@"have remote answer"); 128 | } 129 | } 130 | 131 | 132 | #pragma mark -- peerConnection delegate -- 133 | 134 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 135 | didAddStream:(nonnull RTCMediaStream *)stream{ 136 | NSLog(@"%s is called", __FUNCTION__); 137 | NSLog(@"audio tracks: %lu", (unsigned long)stream.audioTracks.count); 138 | // Create a new render view with a size of your choice 139 | // RTCEAGLVideoView *renderView = [[RTCEAGLVideoView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; 140 | // [stream.videoTracks.lastObject addRenderer:renderView]; 141 | } 142 | 143 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 144 | didChangeSignalingState:(RTCSignalingState)stateChanged{ 145 | NSLog(@"signaling state changed: %ld", (long)stateChanged); 146 | } 147 | 148 | // Triggered when a remote peer close a stream. 149 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 150 | didRemoveStream:(nonnull RTCMediaStream *)stream{ 151 | NSLog(@"removed stream"); 152 | } 153 | 154 | // Triggered when renegotiation is needed, for example the ICE has restarted. 155 | - (void)peerConnectionShouldNegotiate:(RTCPeerConnection *)peerConnection{ 156 | 157 | } 158 | 159 | // Called any time the ICEConnectionState changes. 160 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 161 | didChangeIceConnectionState:(RTCIceConnectionState)newState{ 162 | NSLog(@"Ice changed: %@", RTCIceStateNames[newState]); 163 | 164 | if (newState == RTCIceConnectionStateConnected) { 165 | dispatch_async(dispatch_get_main_queue(), ^{ 166 | if (!_statTimer) { 167 | _statTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(statTimerFired:) userInfo:nil repeats:YES]; 168 | [_statTimer fire]; 169 | } 170 | }); 171 | } 172 | } 173 | 174 | // Called any time the ICEGatheringState changes. 175 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 176 | didChangeIceGatheringState:(RTCIceGatheringState)newState{ 177 | 178 | } 179 | 180 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 181 | didGenerateIceCandidate:(RTCIceCandidate *)candidate{ 182 | NSLog(@"got candidate: %@", candidate.sdp); 183 | 184 | dispatch_async(dispatch_get_main_queue(), ^{ 185 | Message *message = [[Message alloc] initWithPeerUID:self.peer.uniqueID 186 | Type:@"signal" 187 | SubType:@"candidate" 188 | Content:[candidate toDictionary]]; 189 | [self.socketIODelegate sendMessage:message]; 190 | }); 191 | } 192 | 193 | - (void)peerConnection:(RTCPeerConnection *)peerConnection 194 | didRemoveIceCandidates:(NSArray *)candidates{ 195 | 196 | } 197 | 198 | // New data channel has been opened. 199 | - (void)peerConnection:(RTCPeerConnection*)peerConnection 200 | didOpenDataChannel:(RTCDataChannel*)dataChannel{ 201 | 202 | } 203 | 204 | 205 | - (void)onMessage: (Message *)message{ 206 | NSLog(@"onMessage:"); 207 | NSLog(@"%@", message); 208 | } 209 | 210 | #pragma mark -- Stat Report -- 211 | - (void)statTimerFired: (id)sender{ 212 | for (RTCMediaStream *stream in _peerConnection.localStreams) { 213 | for (RTCVideoTrack *track in stream.audioTracks) { 214 | [_peerConnection statsForTrack:track statsOutputLevel:RTCStatsOutputLevelStandard completionHandler:^(NSArray * _Nonnull stats) { 215 | NSLog(@"%@", stats); 216 | }]; 217 | } 218 | for (RTCVideoTrack *track in stream.videoTracks) { 219 | [_peerConnection statsForTrack:track statsOutputLevel:RTCStatsOutputLevelStandard completionHandler:^(NSArray * _Nonnull stats) { 220 | NSLog(@"%@", stats); 221 | }]; 222 | } 223 | } 224 | } 225 | 226 | 227 | /* 228 | #pragma mark - Navigation 229 | 230 | // In a storyboard-based application, you will often want to do a little preparation before navigation 231 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 232 | // Get the new view controller using [segue destinationViewController]. 233 | // Pass the selected object to the new view controller. 234 | } 235 | */ 236 | 237 | @end 238 | -------------------------------------------------------------------------------- /Server/public/main.js: -------------------------------------------------------------------------------- 1 | 2 | var myusername; 3 | var peerusername; 4 | var socket = io(); 5 | var pc; 6 | var localStream; 7 | var remoteCandidates = []; 8 | var sendChannel; 9 | var receiveChannel; 10 | 11 | $(document).ready(function(){ 12 | console.log("document ready."); 13 | console.log(adapter.browserDetails.browser); 14 | 15 | $(this).attr('disabled', true).unbind('click'); 16 | // Make sure the browser supports WebRTC 17 | if(!isWebrtcSupported()) { 18 | bootbox.alert("No WebRTC support... "); 19 | return; 20 | } 21 | 22 | $('#videocall').removeClass('hide').show(); 23 | $('#login').removeClass('hide').show(); 24 | $('#registernow').removeClass('hide').show(); 25 | $('#register').click(registerUsername); 26 | $('#username').focus(); 27 | }); 28 | 29 | 30 | socket.on('register succeed', function(msg){ 31 | myusername = msg["name"]; 32 | console.log("Successfully registered as " + myusername + "!"); 33 | $('#youok').removeClass('hide').show().html("Registered as '" + myusername + "'"); 34 | // Get a list of available peers, just for fun 35 | //videocall.send({"message": { "request": "list" }}); 36 | // TODO Enable buttons to call now 37 | $('#phone').removeClass('hide').show(); 38 | $('#call').unbind('click').click(doCall); 39 | $('#peer').focus(); 40 | 41 | openDevices(); 42 | }); 43 | 44 | socket.on('register failed', function(msg){ 45 | console.log("register failed: " + msg.info); 46 | }) 47 | 48 | socket.on('new user', function(data){ 49 | console.log("new user " + data.name); 50 | }); 51 | 52 | socket.on('user leave', function(data){ 53 | console.log(data.name + " left"); 54 | }); 55 | 56 | socket.on('chat message', function(data){ 57 | //dispatch signal messages to corresponding functions, ie , 58 | //onRemoteOffer/ onRemoteAnswer/onRemoteIceCandidate 59 | //IS this message to me ? 60 | if(data.to != myusername){ 61 | return; 62 | } 63 | // 64 | if(data.type == 'signal'){ 65 | onSignalMessage(data); 66 | }else if(data.type == 'text'){ 67 | console.log('received text message from ' + data.from + ', content: ' + data.content); 68 | }else{ 69 | console.log('received unknown message type ' + data.type + ' from ' + data.from); 70 | } 71 | }); 72 | 73 | socket.on('connect', function(){ 74 | console.log("server connected"); 75 | }); 76 | 77 | function isWebrtcSupported() { 78 | return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null; 79 | }; 80 | 81 | function checkEnter(field, event) { 82 | var theCode = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode; 83 | if(theCode == 13) { 84 | if(field.id == 'username') 85 | registerUsername(); 86 | else if(field.id == 'peer') 87 | doCall(); 88 | else if(field.id == 'datasend') 89 | sendData(); 90 | return false; 91 | } else { 92 | return true; 93 | } 94 | } 95 | 96 | function sendData() { 97 | const data = $('#datasend').val(); 98 | sendChannel.send(data); 99 | console.log('Sent Data: ' + data); 100 | $('#datasend').val(''); 101 | } 102 | 103 | function registerUsername() { 104 | // Try a registration 105 | $('#username').attr('disabled', true); 106 | $('#register').attr('disabled', true).unbind('click'); 107 | var username = $('#username').val(); 108 | if(username === "") { 109 | bootbox.alert("Insert a username to register (e.g., pippo)"); 110 | $('#username').removeAttr('disabled'); 111 | $('#register').removeAttr('disabled').click(registerUsername); 112 | return; 113 | } 114 | if(/[^a-zA-Z0-9]/.test(username)) { 115 | bootbox.alert('Input is not alphanumeric'); 116 | $('#username').removeAttr('disabled').val(""); 117 | $('#register').removeAttr('disabled').click(registerUsername); 118 | return; 119 | } 120 | var info = { "name": username }; 121 | socket.emit("register", info); 122 | console.log("trying to register as " + username); 123 | } 124 | 125 | function openDevices(){ 126 | var options = {audio:false, video:true}; 127 | navigator.mediaDevices 128 | .getUserMedia(options) 129 | .then(onLocalStream) 130 | .catch(function(e) { 131 | alert('getUserMedia() failed'); 132 | console.log('getUserMedia() error: ', e); 133 | }); 134 | } 135 | 136 | function onLocalStream(stream) { 137 | console.log('Received local stream'); 138 | $('#videos').removeClass('hide').show(); 139 | if($('#myvideo').length === 0) 140 | $('#videoleft').append('