talking_stick 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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>
|