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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/Changes.md +5 -0
  4. data/Guardfile +44 -0
  5. data/README.md +59 -24
  6. data/app/assets/images/talking_stick/line-spinner.svg +1 -0
  7. data/app/assets/images/talking_stick/loading.gif +0 -0
  8. data/app/assets/javascripts/talking_stick/talking_stick.js.erb +272 -0
  9. data/app/assets/javascripts/talking_stick/talking_stick/partner.js +89 -28
  10. data/app/assets/javascripts/talking_stick/talking_stick/rails_signaling.js +24 -6
  11. data/app/assets/stylesheets/talking_stick/application.css +55 -0
  12. data/app/controllers/talking_stick/application_controller.rb +6 -0
  13. data/app/controllers/talking_stick/participants_controller.rb +1 -1
  14. data/app/controllers/talking_stick/rooms_controller.rb +4 -1
  15. data/app/models/talking_stick/participant.rb +2 -1
  16. data/app/models/talking_stick/room.rb +17 -0
  17. data/app/models/talking_stick/signal.rb +2 -0
  18. data/app/views/talking_stick/rooms/_form.html.erb +12 -9
  19. data/app/views/talking_stick/rooms/edit.html.erb +5 -4
  20. data/app/views/talking_stick/rooms/index.html.erb +29 -22
  21. data/app/views/talking_stick/rooms/new.html.erb +5 -3
  22. data/app/views/talking_stick/rooms/show.html.erb +92 -19
  23. data/config/locales/en.yml +15 -0
  24. data/config/locales/it.yml +14 -0
  25. data/db/migrate/20150722200822_add_slug_to_talking_stick_rooms.rb +11 -0
  26. data/lib/generators/talking_stick/views/USAGE +18 -0
  27. data/lib/generators/talking_stick/views/views_generator.rb +18 -0
  28. data/lib/talking_stick/version.rb +1 -1
  29. data/spec/dummy/app/assets/javascripts/application.js +1 -0
  30. data/spec/dummy/app/assets/stylesheets/application.css +1 -0
  31. data/spec/models/talking_stick/participant_spec.rb +1 -1
  32. data/spec/models/talking_stick/room_spec.rb +32 -2
  33. data/talking_stick.gemspec +2 -0
  34. metadata +42 -5
  35. data/app/assets/javascripts/talking_stick/talking_stick.js +0 -141
  36. 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
- this.log('trace', 'Creating new peer connection');
36
- this.peerConnection = new RTCPeerConnection();
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
- partner.handleLocalICECandidate.apply(partner, arguments);
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 partner = this;
68
+ var self = this;
49
69
  this.peerConnection.createOffer(function(offer) {
50
- partner.log('trace', 'Created PeerConnection Offer; ICE candidate collection starting', offer);
51
- partner.gatheringCandidates = true;
52
- partner.peerConnection.setLocalDescription(offer);
53
- partner.signalingEngine.sendOffer(partner.guid, offer);
54
- }, this.errorCallback);
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
- this.log('debug', 'Processing Offer received from', this.guid);
59
- this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
60
- var partner = this;
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
- attachMediaStream($(partner.videoElement)[0], event.stream);
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
- partner.peerConnection.setLocalDescription(new RTCSessionDescription(answer));
67
- partner.log('debug', 'Sending Answer to', partner.guid);
68
- partner.signalingEngine.sendAnswer(partner.guid, answer);
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 partner = this;
99
+ var self = this;
75
100
  this.peerConnection.onaddstream = function(event) {
76
- attachMediaStream($(partner.videoElement)[0], event.stream);
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(new RTCIceCandidate(candidate));
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
- data: JSON.stringify(candidates),
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
- if (TalkingStick.partners[participant.guid]) {
81
+ var partner;
82
+ if (partner = TalkingStick.partners[participant.guid]) {
73
83
  // We already have a connection to this participant
74
- //TalkingStick.log('trace', 'Skipping participant since we already have a connection', participant.guid);
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
- TalkingStick.addPartner(participant);
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
+ }
@@ -1,4 +1,10 @@
1
1
  module TalkingStick
2
2
  class ApplicationController < ActionController::Base
3
+ layout 'application'
4
+ before_action :set_locale
5
+
6
+ def set_locale
7
+ I18n.locale = params[:locale] || I18n.default_locale
8
+ end
3
9
  end
4
10
  end
@@ -56,7 +56,7 @@ module TalkingStick
56
56
 
57
57
  private
58
58
  def set_room
59
- @room = Room.find params[:room_id]
59
+ @room = Room.find_or_create(slug: params[:room_id])
60
60
  end
61
61
 
62
62
  # Use callbacks to share common setup or constraints between actions.
@@ -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.find(params[:id] || params[:room_id])
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 - 5.minutes).delete_all
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="field">
15
- <%= f.label :name %><br>
16
- <%= f.text_field :name %>
17
- </div>
18
- <div class="field">
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="actions">
23
- <%= f.submit %>
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,6 +1,7 @@
1
- <h1>Editing Room</h1>
1
+ <div class="container">
2
2
 
3
- <%= render 'form' %>
3
+ <h1>Editing Room</h1>
4
4
 
5
- <%= link_to 'Show', @room %> |
6
- <%= link_to 'Back', rooms_path %>
5
+ <%= render 'form' %>
6
+
7
+ </div>
@@ -1,29 +1,36 @@
1
- <p id="notice"><%= notice %></p>
1
+ <div class="container">
2
+ <p id="notice"><%= notice %></p>
2
3
 
3
- <h1>Listing Rooms</h1>
4
+ <%= link_to t(:new_room), new_room_path, class: 'btn btn-success pull-right' %>
4
5
 
5
- <table>
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
- <tbody>
15
- <% @rooms.each do |room| %>
8
+ <table class="table">
9
+ <thead>
16
10
  <tr>
17
- <td><%= room.name %></td>
18
- <td><%= room.last_used %></td>
19
- <td><%= link_to 'Show', room %></td>
20
- <td><%= link_to 'Edit', edit_room_path(room) %></td>
21
- <td><%= link_to 'Destroy', room, method: :delete, data: { confirm: 'Are you sure?' } %></td>
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
- <% end %>
24
- </tbody>
25
- </table>
18
+ </thead>
26
19
 
27
- <br>
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
- <%= link_to 'New Room', new_room_path %>
33
+ <br>
34
+
35
+
36
+ </div>