ep_webrtc 0.1.80 → 0.1.81

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.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  # ep_webrtc
4
4
 
5
- WebRTC-based audio/video chat with other users visiting the same pad.
5
+ WebRTC-based audio/video chat and screen sharing with other users visiting the
6
+ same pad.
6
7
 
7
8
  The audio and video streams are peer-to-peer: every user sends a copy of their
8
9
  audio/video streams directly to every other user visiting the same pad. Because
package/locales/en.json CHANGED
@@ -5,9 +5,11 @@
5
5
  "ep_webrtc_error_permission_cam": "Failed to get permission to access your camera.",
6
6
  "ep_webrtc_error_permission_mic": "Failed to get permission to access your microphone.",
7
7
  "ep_webrtc_error_permission_cammic": "Failed to get permission to access your camera and microphone.",
8
+ "ep_webrtc_error_permission_screen": "Failed to get permission to access your screen.",
8
9
  "ep_webrtc_error_notFound_cam": "Failed to access your camera.",
9
10
  "ep_webrtc_error_notFound_mic": "Failed to access your microphone.",
10
11
  "ep_webrtc_error_notFound_cammic": "Failed to access your camera and microphone.",
12
+ "ep_webrtc_error_notFound_screen": "Failed to access your screen.",
11
13
  "ep_webrtc_error_notReadable": "Sorry, a hardware error occurred that prevented access to your camera and/or microphone:",
12
14
  "ep_webrtc_error_otherCantAccess": "Sorry, the browser doesn't have access to your camera and/or microphone. Please check permissions in your browser's settings. This is most likely not a hardware error:",
13
15
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git@github.com:ether/ep_webrtc.git",
6
6
  "type": "git"
7
7
  },
8
- "version": "0.1.80",
8
+ "version": "0.1.81",
9
9
  "description": "WebRTC based audio/video chat to Etherpad",
10
10
  "author": "John McLear <john@mclear.co.uk>",
11
11
  "contributors": [],
@@ -102,6 +102,12 @@
102
102
  #rtcbox .interface-btn.video-btn:before { content: '\e83b'; }
103
103
  #rtcbox .interface-btn.video-btn.off:before { content: '\e83c'; }
104
104
 
105
+ #rtcbox .interface-btn.screenshare-btn:before {
106
+ content: "\f108";
107
+ font-family: "fontawesome-ep_webrtc";
108
+ }
109
+ #rtcbox .interface-btn.screenshare-btn.off:before { content: "\f109"; }
110
+
105
111
  #rtcbox .interface-btn.enlarge-btn:before { content: '\e840'; }
106
112
  #rtcbox .interface-btn.enlarge-btn.large:before { content: '\e83f'; }
107
113
 
@@ -84,6 +84,7 @@ class LocalTracks extends EventTargetPolyfill {
84
84
  constructor() {
85
85
  super();
86
86
  Object.defineProperty(this, 'stream', {value: new MediaStream(), writeable: false});
87
+ this.videoIsScreenshare = false;
87
88
  }
88
89
 
89
90
  _getTracks(kind) {
@@ -447,7 +448,10 @@ exports.rtc = new class {
447
448
  if (newTrack != null) return;
448
449
  switch (oldTrack.kind) {
449
450
  case 'audio': this._selfViewButtons.audio.enabled = false; break;
450
- case 'video': this._selfViewButtons.video.enabled = false; break;
451
+ case 'video':
452
+ this._selfViewButtons.video.enabled = false;
453
+ this._selfViewButtons.screenshare.enabled = false;
454
+ break;
451
455
  }
452
456
  });
453
457
  this._pad = null;
@@ -651,27 +655,52 @@ exports.rtc = new class {
651
655
  (t) => t !== this._disabledSilence && t.readyState === 'live');
652
656
  if (addAudioTrack) devices.push('mic');
653
657
  const addVideoTrack = updateVideo && this._selfViewButtons.video.enabled &&
654
- !this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live');
658
+ (!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
659
+ this._localTracks.videoIsScreenshare);
655
660
  if (addVideoTrack) devices.push('cam');
656
- if (addAudioTrack || addVideoTrack) {
661
+ const addScreenshareTrack = updateVideo && this._selfViewButtons.screenshare.enabled &&
662
+ !this._selfViewButtons.video.enabled && // Video button overrides screenshare button.
663
+ (!this._localTracks.stream.getVideoTracks().some((t) => t.readyState === 'live') ||
664
+ !this._localTracks.videoIsScreenshare);
665
+ const getUserMedia = async () => {
666
+ if (!addAudioTrack && !addVideoTrack) return new MediaStream();
657
667
  debug(`requesting permission to access ${devices.join(' and ')}`);
658
- let stream;
659
- try {
660
- stream = await window.navigator.mediaDevices.getUserMedia({
661
- audio: addAudioTrack,
662
- video: addVideoTrack && {width: {ideal: 320}, height: {ideal: 240}},
663
- });
664
- debug('successfully accessed device(s)');
665
- } catch (err) {
666
- // Display but otherwise ignore the error. The button(s) will be toggled back to
667
- // disabled below if we failed to access the microphone/camera. The user can re-click
668
- // the button to try again.
669
- err.devices = devices;
670
- (async () => this.showUserMediaError(err))();
671
- stream = new MediaStream();
672
- }
673
- for (const track of stream.getTracks()) this._localTracks.setTrack(track.kind, track);
674
- }
668
+ const stream = await window.navigator.mediaDevices.getUserMedia({
669
+ audio: addAudioTrack,
670
+ video: addVideoTrack && {width: {ideal: 320}, height: {ideal: 240}},
671
+ });
672
+ debug('successfully accessed device(s)');
673
+ return stream;
674
+ };
675
+ const getDisplayMedia = async () => {
676
+ if (!addScreenshareTrack) return new MediaStream();
677
+ debug('requesting permission to access screen');
678
+ const stream = await window.navigator.mediaDevices.getDisplayMedia({
679
+ video: {cursor: 'always'},
680
+ });
681
+ debug('successfully accessed screen');
682
+ return new MediaStream(stream.getVideoTracks());
683
+ };
684
+ await Promise.all([[getUserMedia, devices], [getDisplayMedia, ['screen']]].map(
685
+ async ([getMedia, devices]) => {
686
+ let stream;
687
+ try {
688
+ stream = await getMedia();
689
+ } catch (err) {
690
+ // Display but otherwise ignore the error. The button(s) will be toggled back to
691
+ // disabled below if we failed to access the microphone/camera. The user can re-click
692
+ // the button to try again.
693
+ err.devices = devices;
694
+ (async () => this.showUserMediaError(err))();
695
+ stream = new MediaStream();
696
+ }
697
+ for (const track of stream.getTracks()) {
698
+ if (track.kind === 'video') {
699
+ this._localTracks.videoIsScreenshare = devices.includes('screen');
700
+ }
701
+ this._localTracks.setTrack(track.kind, track);
702
+ }
703
+ }));
675
704
  if (updateAudio) {
676
705
  for (const track of this._localTracks.stream.getAudioTracks()) {
677
706
  // Re-check the state of the button because the user might have clicked it while
@@ -685,12 +714,15 @@ exports.rtc = new class {
685
714
  if (updateVideo) {
686
715
  for (const track of this._localTracks.stream.getVideoTracks()) {
687
716
  // Re-check the state of the button because the user might have clicked it while
688
- // getUserMedia() was running.
689
- track.enabled = this._selfViewButtons.video.enabled;
717
+ // getUserMedia() or getDisplayMedia() was running.
718
+ track.enabled = this._localTracks.videoIsScreenshare
719
+ ? this._selfViewButtons.screenshare.enabled : this._selfViewButtons.video.enabled;
690
720
  }
691
721
  const hasVideo = this._localTracks.stream.getVideoTracks().some(
692
722
  (t) => t.enabled && t.readyState === 'live');
693
- this._selfViewButtons.video.enabled = hasVideo;
723
+ this._selfViewButtons.video.enabled = hasVideo && !this._localTracks.videoIsScreenshare;
724
+ this._selfViewButtons.screenshare.enabled =
725
+ hasVideo && this._localTracks.videoIsScreenshare;
694
726
  }
695
727
  } finally {
696
728
  if (updateVideo) this._trackLocks.video.unlock();
@@ -744,8 +776,15 @@ exports.rtc = new class {
744
776
  const $rtcbox = $('#rtcbox');
745
777
  $rtcbox.empty(); // In case any peer videos didn't get cleaned up for some reason.
746
778
  $rtcbox.hide();
747
- for (const track of this._localTracks.stream.getTracks()) {
748
- this._localTracks.setTrack(track.kind, null);
779
+ await this._trackLocks.audio.lock();
780
+ await this._trackLocks.video.lock();
781
+ try {
782
+ for (const track of this._localTracks.stream.getTracks()) {
783
+ this._localTracks.setTrack(track.kind, null);
784
+ }
785
+ } finally {
786
+ this._trackLocks.video.unlock();
787
+ this._trackLocks.audio.unlock();
749
788
  }
750
789
  } finally {
751
790
  $checkbox.prop('disabled', false);
@@ -936,10 +975,11 @@ exports.rtc = new class {
936
975
  });
937
976
 
938
977
  // /////
939
- // Disable Video button
978
+ // Video and Screen Sharing Buttons
940
979
  // /////
941
980
 
942
981
  let $videoBtn;
982
+ let $screenshareBtn;
943
983
  if (isLocal) {
944
984
  $videoBtn = $('<span>').addClass('interface-btn video-btn buttonicon').appendTo($interface);
945
985
  this._selfViewButtons.video = {
@@ -956,11 +996,43 @@ exports.rtc = new class {
956
996
  if (videoHardDisabled) {
957
997
  $videoBtn.attr('title', 'Video disallowed by admin').addClass('disallowed');
958
998
  }
999
+ const {navigator: {mediaDevices: {getDisplayMedia} = {}} = {}} = window;
1000
+ $screenshareBtn = $('<span>')
1001
+ .addClass('interface-btn screenshare-btn buttonicon')
1002
+ .css('display', typeof getDisplayMedia === 'function' ? '' : 'none')
1003
+ .appendTo($interface);
1004
+ this._selfViewButtons.screenshare = {
1005
+ get enabled() { return !$screenshareBtn.hasClass('off'); },
1006
+ set enabled(val) {
1007
+ $screenshareBtn
1008
+ .toggleClass('off', !val)
1009
+ .attr('title', val ? 'Stop screen share' : 'Start screen share');
1010
+ },
1011
+ };
1012
+ this._selfViewButtons.screenshare.enabled = false;
959
1013
  addAsyncEventHandlers($videoBtn, videoHardDisabled ? {} : {
960
1014
  click: async () => {
961
1015
  const videoEnabled = !this._selfViewButtons.video.enabled;
962
1016
  _debug(`video button clicked to ${videoEnabled ? 'en' : 'dis'}able video`);
963
1017
  this._selfViewButtons.video.enabled = videoEnabled;
1018
+ // Unconditionally disable screen sharing. Either the camera was previously disabled in
1019
+ // which case the user now wants to share camera video, or the camera was previously
1020
+ // enabled in which case the user now wants to shut off all video.
1021
+ this._selfViewButtons.screenshare.enabled = false;
1022
+ await this.updateLocalTracks({updateVideo: true});
1023
+ // Don't use `await` here -- see the comment for the audio button click handler above.
1024
+ this.unmuteAndPlayAll();
1025
+ },
1026
+ });
1027
+ addAsyncEventHandlers($screenshareBtn, {
1028
+ click: async () => {
1029
+ const screenshareEnabled = !this._selfViewButtons.screenshare.enabled;
1030
+ _debug(`button clicked to ${screenshareEnabled ? 'en' : 'dis'}able screen sharing`);
1031
+ // Unconditionally disable the camera. Either screen sharing was previously disabled in
1032
+ // which case the user now wants to share the screen, or screen sharing was previously
1033
+ // enabled in which case the user wants to shut off all video.
1034
+ this._selfViewButtons.video.enabled = false;
1035
+ this._selfViewButtons.screenshare.enabled = screenshareEnabled;
964
1036
  await this.updateLocalTracks({updateVideo: true});
965
1037
  // Don't use `await` here -- see the comment for the audio button click handler above.
966
1038
  this.unmuteAndPlayAll();
@@ -1047,7 +1119,7 @@ exports.rtc = new class {
1047
1119
  {
1048
1120
  cls: 'videoended-error-btn',
1049
1121
  title: 'Video stopped unexpectedly. Click to retry.',
1050
- click: () => $videoBtn.click(),
1122
+ click: () => (this._localTracks.videoIsScreenshare ? $screenshareBtn : $videoBtn).click(),
1051
1123
  },
1052
1124
  ] : []),
1053
1125
  ];