talking_stick 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Changes.md +5 -0
- data/Guardfile +44 -0
- data/README.md +59 -24
- data/app/assets/images/talking_stick/line-spinner.svg +1 -0
- data/app/assets/images/talking_stick/loading.gif +0 -0
- data/app/assets/javascripts/talking_stick/talking_stick.js.erb +272 -0
- data/app/assets/javascripts/talking_stick/talking_stick/partner.js +89 -28
- data/app/assets/javascripts/talking_stick/talking_stick/rails_signaling.js +24 -6
- data/app/assets/stylesheets/talking_stick/application.css +55 -0
- data/app/controllers/talking_stick/application_controller.rb +6 -0
- data/app/controllers/talking_stick/participants_controller.rb +1 -1
- data/app/controllers/talking_stick/rooms_controller.rb +4 -1
- data/app/models/talking_stick/participant.rb +2 -1
- data/app/models/talking_stick/room.rb +17 -0
- data/app/models/talking_stick/signal.rb +2 -0
- data/app/views/talking_stick/rooms/_form.html.erb +12 -9
- data/app/views/talking_stick/rooms/edit.html.erb +5 -4
- data/app/views/talking_stick/rooms/index.html.erb +29 -22
- data/app/views/talking_stick/rooms/new.html.erb +5 -3
- data/app/views/talking_stick/rooms/show.html.erb +92 -19
- data/config/locales/en.yml +15 -0
- data/config/locales/it.yml +14 -0
- data/db/migrate/20150722200822_add_slug_to_talking_stick_rooms.rb +11 -0
- data/lib/generators/talking_stick/views/USAGE +18 -0
- data/lib/generators/talking_stick/views/views_generator.rb +18 -0
- data/lib/talking_stick/version.rb +1 -1
- data/spec/dummy/app/assets/javascripts/application.js +1 -0
- data/spec/dummy/app/assets/stylesheets/application.css +1 -0
- data/spec/models/talking_stick/participant_spec.rb +1 -1
- data/spec/models/talking_stick/room_spec.rb +32 -2
- data/talking_stick.gemspec +2 -0
- metadata +42 -5
- data/app/assets/javascripts/talking_stick/talking_stick.js +0 -141
- data/app/views/layouts/talking_stick/application.html.erb +0 -14
@@ -1,5 +1,6 @@
|
|
1
1
|
TalkingStick.Partner = function(participant, options) {
|
2
2
|
this.gatheringCandidates = false;
|
3
|
+
this.name = participant.name;
|
3
4
|
this.guid = participant.guid;
|
4
5
|
this.joinedAt = new Date(participant.joined_at);
|
5
6
|
this.localICECandidates = [];
|
@@ -8,7 +9,10 @@ TalkingStick.Partner = function(participant, options) {
|
|
8
9
|
};
|
9
10
|
$.extend(this._options, options);
|
10
11
|
this.signalingEngine = this._options.signalingEngine;
|
11
|
-
this.videoElement = this._options.videoElement;
|
12
|
+
this.videoElement = $(this._options.videoElement);
|
13
|
+
this.videoStream = undefined;
|
14
|
+
this.connected = false;
|
15
|
+
this.trigger('created');
|
12
16
|
}
|
13
17
|
|
14
18
|
TalkingStick.Partner.prototype.log = function() {
|
@@ -19,68 +23,91 @@ TalkingStick.Partner.prototype.log = function() {
|
|
19
23
|
TalkingStick.log.apply(this, args);
|
20
24
|
}
|
21
25
|
|
26
|
+
TalkingStick.Partner.prototype.trigger = function(name) {
|
27
|
+
name = 'talking_stick.partner.' + name;
|
28
|
+
args = Array.prototype.slice.call(arguments, 1);
|
29
|
+
// Syntactic sugar: make it easy to pass a list of args as the only argument
|
30
|
+
// This is the "right way" per
|
31
|
+
// http://stackoverflow.com/questions/4775722/check-if-object-is-array
|
32
|
+
if (args.length == 1 && Object.prototype.toString.call(args[0]) === '[object Array]') {
|
33
|
+
args = args[0];
|
34
|
+
}
|
35
|
+
args.unshift(this);
|
36
|
+
this.videoElement.trigger(name, args);
|
37
|
+
};
|
38
|
+
|
22
39
|
TalkingStick.Partner.prototype.errorCallback = function() {
|
23
40
|
// Convert arguments to a real array
|
24
41
|
var args = Array.prototype.slice.call(arguments);
|
42
|
+
this.trigger('error', args);
|
25
43
|
args.unshift('error');
|
26
44
|
this.log(args);
|
27
45
|
}
|
28
46
|
|
29
|
-
TalkingStick.Partner.prototype.setDescription = function(answer) {
|
30
|
-
this.log('trace', 'Setting remote description to', answer);
|
31
|
-
this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
32
|
-
};
|
33
|
-
|
34
47
|
TalkingStick.Partner.prototype.connect = function(stream) {
|
35
|
-
|
36
|
-
|
48
|
+
var configuration = {
|
49
|
+
iceServers: this._options.iceServers,
|
50
|
+
};
|
51
|
+
this.log('trace', 'Creating new peer connection with configuration', configuration);
|
52
|
+
this.peerConnection = new RTCPeerConnection(configuration);
|
53
|
+
|
54
|
+
var self = this;
|
55
|
+
this.peerConnection.oniceconnectionstatechange = function(ev) {
|
56
|
+
self.trigger('ice_connection_state_change', ev);
|
57
|
+
};
|
37
58
|
|
38
|
-
var partner = this;
|
39
59
|
this.peerConnection.onicecandidate = function() {
|
40
|
-
|
60
|
+
self.handleLocalICECandidate.apply(self, arguments);
|
41
61
|
}
|
42
62
|
|
43
63
|
this.peerConnection.addStream(stream);
|
44
64
|
};
|
45
65
|
|
46
|
-
TalkingStick.Partner.prototype.sendOffer = function() {
|
66
|
+
TalkingStick.Partner.prototype.sendOffer = function(options) {
|
47
67
|
// Fix scope for "this" inside createOffer()
|
48
|
-
var
|
68
|
+
var self = this;
|
49
69
|
this.peerConnection.createOffer(function(offer) {
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
70
|
+
self.log('trace', 'Created PeerConnection Offer; ICE candidate collection starting', offer);
|
71
|
+
self.gatheringCandidates = true;
|
72
|
+
self.peerConnection.setLocalDescription(offer);
|
73
|
+
self.signalingEngine.sendOffer(self.guid, offer);
|
74
|
+
setTimeout(self._checkForConnection, self._options.connectionTimeout);
|
75
|
+
}, function() { self.errorCallback.apply(self, arguments) } );
|
55
76
|
};
|
56
77
|
|
57
78
|
TalkingStick.Partner.prototype.handleOffer = function(offer) {
|
58
|
-
|
59
|
-
this.
|
60
|
-
var
|
79
|
+
var offer = new RTCSessionDescription(offer);
|
80
|
+
this.log('debug', 'Processing Offer received from', this.guid, offer);
|
81
|
+
var self = this;
|
61
82
|
|
62
83
|
this.peerConnection.onaddstream = function(event) {
|
63
|
-
|
84
|
+
self._attachMediaStream(event.stream);
|
85
|
+
self.connected = true;
|
64
86
|
};
|
87
|
+
|
88
|
+
this.peerConnection.setRemoteDescription(offer);
|
89
|
+
|
65
90
|
this.peerConnection.createAnswer(function(answer) {
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
});
|
91
|
+
self.peerConnection.setLocalDescription(new RTCSessionDescription(answer));
|
92
|
+
self.log('debug', 'Sending Answer to', self.guid);
|
93
|
+
self.signalingEngine.sendAnswer(self.guid, answer);
|
94
|
+
}, function() { self.errorCallback.apply(self, arguments) } );
|
70
95
|
};
|
71
96
|
|
72
97
|
TalkingStick.Partner.prototype.handleAnswer = function(answer) {
|
73
98
|
this.log('debug', 'Processing Answer received from', this.guid);
|
74
|
-
var
|
99
|
+
var self = this;
|
75
100
|
this.peerConnection.onaddstream = function(event) {
|
76
|
-
|
101
|
+
self._attachMediaStream(event.stream);
|
102
|
+
self.connected = true;
|
77
103
|
};
|
78
104
|
this.peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
79
105
|
};
|
80
106
|
|
81
107
|
TalkingStick.Partner.prototype.handleRemoteICECandidate = function(candidate) {
|
108
|
+
candidate = new RTCIceCandidate(candidate);
|
82
109
|
this.log('trace', 'Adding remote ICE candidate', candidate);
|
83
|
-
this.peerConnection.addIceCandidate(
|
110
|
+
this.peerConnection.addIceCandidate(candidate);
|
84
111
|
};
|
85
112
|
|
86
113
|
TalkingStick.Partner.prototype.handleLocalICECandidate = function(event) {
|
@@ -98,3 +125,37 @@ TalkingStick.Partner.prototype.handleLocalICECandidate = function(event) {
|
|
98
125
|
}
|
99
126
|
};
|
100
127
|
|
128
|
+
TalkingStick.Partner.prototype.cleanup = function() {
|
129
|
+
this.log('debug', 'Cleanup requested, shutting down.');
|
130
|
+
this.disconnect();
|
131
|
+
this.trigger('cleanup');
|
132
|
+
}
|
133
|
+
|
134
|
+
TalkingStick.Partner.prototype.disconnect = function() {
|
135
|
+
try {
|
136
|
+
this.peerConnection.close();
|
137
|
+
} catch(ex) {
|
138
|
+
// Ignore errors here in case the connection is already closed
|
139
|
+
}
|
140
|
+
};
|
141
|
+
|
142
|
+
TalkingStick.Partner.prototype._attachMediaStream = function(stream) {
|
143
|
+
this.log('trace', 'Attaching media stream');
|
144
|
+
var el = attachMediaStream(this.videoElement[0], stream);
|
145
|
+
if (el) {
|
146
|
+
// Compatibility with Temasys plugin
|
147
|
+
// See https://temasys.atlassian.net/wiki/display/TWPP/How+to+integrate+the+Temasys+WebRTC+Plugin+into+your+website - "Attach streams"
|
148
|
+
this.videoElement = $(el);
|
149
|
+
}
|
150
|
+
partner.videoStream = stream;
|
151
|
+
this.trigger('media');
|
152
|
+
};
|
153
|
+
|
154
|
+
TalkingStick.Partner._checkForConnection = function() {
|
155
|
+
this.log('trace', 'Checking for connection');
|
156
|
+
if (!this.connected) {
|
157
|
+
this.log('notice', 'Connection to partner timed out.');
|
158
|
+
this.trigger('connection_timeout');
|
159
|
+
}
|
160
|
+
};
|
161
|
+
|
@@ -1,15 +1,18 @@
|
|
1
1
|
TalkingStick.RailsSignaling = function(options) {
|
2
2
|
this.options = options;
|
3
3
|
this.roomUrl = options.url;
|
4
|
+
var self = this;
|
5
|
+
$('#localvideo').on('talking_stick.connected', function() { self.connected.apply(self)} );
|
4
6
|
}
|
5
7
|
|
6
8
|
TalkingStick.RailsSignaling.prototype.connected = function() {
|
7
9
|
// Check now, then schedule a timer to check for new participants
|
8
10
|
this._updateRoom();
|
9
11
|
var signaling = this;
|
10
|
-
setInterval(function() {
|
12
|
+
var pollingInterval = setInterval(function() {
|
11
13
|
signaling._updateRoom.apply(signaling);
|
12
14
|
}, 3000);
|
15
|
+
$('#localvideo').on('talking_stick.disconnected', function() { clearInterval(pollingInterval); });
|
13
16
|
}
|
14
17
|
|
15
18
|
TalkingStick.RailsSignaling.prototype.sendICECandidate = function(to, candidate) {
|
@@ -20,7 +23,9 @@ TalkingStick.RailsSignaling.prototype.sendICECandidate = function(to, candidate)
|
|
20
23
|
TalkingStick.RailsSignaling.prototype.iceCandidateGatheringComplete = function(to, candidates) {
|
21
24
|
var data = {
|
22
25
|
signal_type: 'candidates',
|
23
|
-
|
26
|
+
// IE doesn't like big objects, so filter down and
|
27
|
+
// only keep the actual candidate information
|
28
|
+
data: JSON.stringify(candidates, ['candidate', 'sdpMLineIndex', 'sdpMid']),
|
24
29
|
}
|
25
30
|
this._sendData('ICE Candidates', to, data);
|
26
31
|
}
|
@@ -62,21 +67,34 @@ TalkingStick.RailsSignaling.prototype._updateRoom = function() {
|
|
62
67
|
};
|
63
68
|
|
64
69
|
TalkingStick.RailsSignaling.prototype._updateParticipants = function(participants) {
|
70
|
+
$.each(TalkingStick.partners, function(i, partner) {
|
71
|
+
// Mark each partner for cleanup
|
72
|
+
partner.fresh = false;
|
73
|
+
});
|
74
|
+
|
65
75
|
$.each(participants, function(i, participant) {
|
66
76
|
if (participant.guid === TalkingStick.guid) {
|
67
77
|
// Don't try to set up a connection to ourself
|
68
|
-
//TalkingStick.log('trace', 'Skipping own GUID', participant.guid);
|
69
78
|
return;
|
70
79
|
}
|
71
80
|
|
72
|
-
|
81
|
+
var partner;
|
82
|
+
if (partner = TalkingStick.partners[participant.guid]) {
|
73
83
|
// We already have a connection to this participant
|
74
|
-
//
|
84
|
+
// Mark it active so it doesn't get removed
|
85
|
+
partner.fresh = true;
|
75
86
|
return;
|
76
87
|
}
|
88
|
+
|
77
89
|
TalkingStick.log('trace', 'Handling new partner', participant.guid);
|
90
|
+
partner = TalkingStick.addPartner(participant);
|
91
|
+
partner.fresh = true;
|
92
|
+
});
|
78
93
|
|
79
|
-
|
94
|
+
$.each(TalkingStick.partners, function(i, partner) {
|
95
|
+
if (partner.fresh) { return; }
|
96
|
+
partner.cleanup();
|
97
|
+
delete TalkingStick.partners[i];
|
80
98
|
});
|
81
99
|
};
|
82
100
|
|
@@ -13,3 +13,58 @@
|
|
13
13
|
*= require_tree .
|
14
14
|
*= require_self
|
15
15
|
*/
|
16
|
+
html,
|
17
|
+
body,
|
18
|
+
body > .container-fluid,
|
19
|
+
body > .container-fluid > .row
|
20
|
+
body > .container-fluid > .row > [class^="col-xs-"] {
|
21
|
+
height: 100%;
|
22
|
+
}
|
23
|
+
|
24
|
+
.container-fluid {
|
25
|
+
height: 100%;
|
26
|
+
}
|
27
|
+
|
28
|
+
#localvideo-container {
|
29
|
+
width: 100%;
|
30
|
+
left: 1em;
|
31
|
+
z-index: 99;
|
32
|
+
}
|
33
|
+
|
34
|
+
#localvideo {
|
35
|
+
width: 100%;
|
36
|
+
padding-top: 1em;
|
37
|
+
max-width: 200px;
|
38
|
+
}
|
39
|
+
|
40
|
+
.modal #localvideo {
|
41
|
+
max-height: 320px;
|
42
|
+
width: 100%;
|
43
|
+
max-width: none;
|
44
|
+
}
|
45
|
+
|
46
|
+
#partnervideos {
|
47
|
+
width: 100%;
|
48
|
+
height: 100%;
|
49
|
+
}
|
50
|
+
|
51
|
+
#partnervideos .video {
|
52
|
+
float: right;
|
53
|
+
padding: 1em;
|
54
|
+
max-height: 100%;
|
55
|
+
max-width: 100%;
|
56
|
+
}
|
57
|
+
|
58
|
+
#partnervideos .video video {
|
59
|
+
width: 100%;
|
60
|
+
}
|
61
|
+
|
62
|
+
#video-preview p {
|
63
|
+
font-size: 1.4em;
|
64
|
+
line-height: 1.3em;
|
65
|
+
padding: 1em 0;
|
66
|
+
}
|
67
|
+
|
68
|
+
#form-submit-buttons {
|
69
|
+
padding-top: 20px;
|
70
|
+
}
|
@@ -12,6 +12,9 @@ module TalkingStick
|
|
12
12
|
|
13
13
|
# GET /rooms/1
|
14
14
|
def show
|
15
|
+
@room.last_used = Time.now
|
16
|
+
@room.save
|
17
|
+
|
15
18
|
if params[:guid]
|
16
19
|
if @participant = Participant.where(guid: params[:guid]).first
|
17
20
|
@participant.last_seen = Time.now
|
@@ -99,7 +102,7 @@ module TalkingStick
|
|
99
102
|
private
|
100
103
|
# Use callbacks to share common setup or constraints between actions.
|
101
104
|
def set_room
|
102
|
-
@room = Room.
|
105
|
+
@room = Room.find_or_create(slug: (params[:id] || params[:room_id]))
|
103
106
|
end
|
104
107
|
|
105
108
|
def set_participant
|
@@ -7,11 +7,12 @@ module TalkingStick
|
|
7
7
|
|
8
8
|
def set_defaults
|
9
9
|
self.joined_at ||= Time.now
|
10
|
+
self.last_seen ||= self.joined_at
|
10
11
|
end
|
11
12
|
|
12
13
|
class << self
|
13
14
|
def remove_stale!(room)
|
14
|
-
self.where(room: room).where('last_seen < ?', Time.now -
|
15
|
+
self.where(room: room).where('last_seen < ?', Time.now - 15.seconds).destroy_all
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -2,5 +2,22 @@ module TalkingStick
|
|
2
2
|
class Room < ActiveRecord::Base
|
3
3
|
has_many :participants, dependent: :destroy
|
4
4
|
has_many :signals, dependent: :destroy
|
5
|
+
|
6
|
+
before_validation :sluggify_name
|
7
|
+
|
8
|
+
def self.find_or_create(slug:)
|
9
|
+
slug = slug.parameterize
|
10
|
+
find_by(slug: slug) || create(name: slug.titleize, slug: slug)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_param
|
14
|
+
slug
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def sluggify_name
|
20
|
+
self.slug = name.parameterize unless slug.present?
|
21
|
+
end
|
5
22
|
end
|
6
23
|
end
|
@@ -5,6 +5,8 @@ module TalkingStick
|
|
5
5
|
belongs_to :recipient, class_name: "TalkingStick::Participant"
|
6
6
|
validates :room, :sender, :recipient, presence: true
|
7
7
|
|
8
|
+
default_scope { order 'created_at ASC' }
|
9
|
+
|
8
10
|
# The normal delegate method seems to not be working for an unknown reason
|
9
11
|
def sender_guid
|
10
12
|
self.sender.guid
|
@@ -11,15 +11,18 @@
|
|
11
11
|
</div>
|
12
12
|
<% end %>
|
13
13
|
|
14
|
-
<div class="
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
<%= f.label :last_used %><br>
|
20
|
-
<%= f.datetime_select :last_used %>
|
14
|
+
<div class="form-group">
|
15
|
+
<div class="field">
|
16
|
+
<p><%= f.label :name %></p>
|
17
|
+
<p><%= f.text_field :name, class: 'form-control' %></p>
|
18
|
+
</div>
|
21
19
|
</div>
|
22
|
-
<div class="
|
23
|
-
|
20
|
+
<div class="form-group">
|
21
|
+
<div id="form-submit-buttons" class="actions">
|
22
|
+
<p><%= f.submit "Save", class: 'btn btn-success' %>
|
23
|
+
<span><%= link_to 'Back', rooms_path, class: 'btn btn-default pull-right' %></span>
|
24
|
+
</p>
|
25
|
+
</div>
|
26
|
+
|
24
27
|
</div>
|
25
28
|
<% end %>
|
@@ -1,29 +1,36 @@
|
|
1
|
-
<
|
1
|
+
<div class="container">
|
2
|
+
<p id="notice"><%= notice %></p>
|
2
3
|
|
3
|
-
|
4
|
+
<%= link_to t(:new_room), new_room_path, class: 'btn btn-success pull-right' %>
|
4
5
|
|
5
|
-
<
|
6
|
-
<thead>
|
7
|
-
<tr>
|
8
|
-
<th>Name</th>
|
9
|
-
<th>Last used</th>
|
10
|
-
<th colspan="3"></th>
|
11
|
-
</tr>
|
12
|
-
</thead>
|
6
|
+
<h1><%= t(:listing_rooms) %></h1>
|
13
7
|
|
14
|
-
<
|
15
|
-
|
8
|
+
<table class="table">
|
9
|
+
<thead>
|
16
10
|
<tr>
|
17
|
-
<
|
18
|
-
<
|
19
|
-
<
|
20
|
-
<
|
21
|
-
<
|
11
|
+
<th><%= t(:name) %></th>
|
12
|
+
<th><%= t(:last_used) %></th>
|
13
|
+
<th>Enter Room</th>
|
14
|
+
<th>Edit Room</th>
|
15
|
+
<th>Destroy Room</th>
|
16
|
+
<th colspan="3"></th>
|
22
17
|
</tr>
|
23
|
-
|
24
|
-
</tbody>
|
25
|
-
</table>
|
18
|
+
</thead>
|
26
19
|
|
27
|
-
<
|
20
|
+
<tbody>
|
21
|
+
<% @rooms.each do |room| %>
|
22
|
+
<tr>
|
23
|
+
<td><%= room.name %></td>
|
24
|
+
<td><%= room.last_used.try(:strftime, "%b %d, %l:%M %P") || t(:last_used_never) %></td>
|
25
|
+
<td><%= link_to 'Enter', room %></td>
|
26
|
+
<td><%= link_to t(:edit), edit_room_path(room) %></td>
|
27
|
+
<td><%= link_to t(:destroy), room, method: :delete, data: { confirm: 'Are you sure?' } %></td>
|
28
|
+
</tr>
|
29
|
+
<% end %>
|
30
|
+
</tbody>
|
31
|
+
</table>
|
28
32
|
|
29
|
-
|
33
|
+
<br>
|
34
|
+
|
35
|
+
|
36
|
+
</div>
|