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.
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>