ep_webrtc 2.5.37 → 2.5.39
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/.github/workflows/automerge.yml +15 -0
- package/.github/workflows/backend-tests.yml +1 -1
- package/.github/workflows/frontend-tests.yml +1 -1
- package/.github/workflows/test-and-release.yml +7 -1
- package/package.json +1 -1
- package/static/js/index.js +16 -0
- package/static/tests/frontend-new/specs/screenshare.spec.ts +106 -0
|
@@ -2,6 +2,12 @@ name: Dependabot Automerge
|
|
|
2
2
|
permissions:
|
|
3
3
|
contents: write
|
|
4
4
|
pull-requests: write
|
|
5
|
+
# `actions: write` lets the post-merge step kick off Node.js Package on
|
|
6
|
+
# the default branch via `gh workflow run`. Without this, automerge'd
|
|
7
|
+
# PRs land on main but the on-push release job never fires (GitHub
|
|
8
|
+
# Actions intentionally suppresses on:push triggers when the push is
|
|
9
|
+
# authenticated with GITHUB_TOKEN).
|
|
10
|
+
actions: write
|
|
5
11
|
on:
|
|
6
12
|
workflow_run:
|
|
7
13
|
workflows:
|
|
@@ -21,6 +27,7 @@ jobs:
|
|
|
21
27
|
uses: actions/checkout@v6
|
|
22
28
|
|
|
23
29
|
- name: Automerge
|
|
30
|
+
id: automerge
|
|
24
31
|
uses: "pascalgn/automerge-action@v0.16.4"
|
|
25
32
|
env:
|
|
26
33
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -28,3 +35,11 @@ jobs:
|
|
|
28
35
|
MERGE_LABELS: ""
|
|
29
36
|
MERGE_RETRY_SLEEP: "100000"
|
|
30
37
|
|
|
38
|
+
- name: Trigger release on default branch
|
|
39
|
+
# `pascalgn/automerge-action` exits 0 whether or not it merged. Skip
|
|
40
|
+
# the dispatch when nothing was actually merged so we don't kick a
|
|
41
|
+
# phantom release run on every Dependabot Automerge invocation.
|
|
42
|
+
if: steps.automerge.outputs.mergeResult == 'merged'
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
45
|
+
run: gh workflow run test-and-release.yml --ref ${{ github.event.repository.default_branch }}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
name: Node.js Package
|
|
2
|
-
on:
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
# Invoked by automerge.yml after a Dependabot PR is merged. GitHub
|
|
5
|
+
# Actions doesn't fire on:push when the push is authored by GITHUB_TOKEN
|
|
6
|
+
# (the automerge action's only available identity), so without this
|
|
7
|
+
# dispatch trigger the release job never runs after auto-merges.
|
|
8
|
+
workflow_dispatch:
|
|
3
9
|
|
|
4
10
|
# id-token: write must be granted here so the reusable npmpublish workflow
|
|
5
11
|
# can request an OIDC token for npm trusted publishing.
|
package/package.json
CHANGED
package/static/js/index.js
CHANGED
|
@@ -1061,12 +1061,28 @@ exports.rtc = new class {
|
|
|
1061
1061
|
click: async () => {
|
|
1062
1062
|
const screenshareEnabled = !this._selfViewButtons.screenshare.enabled;
|
|
1063
1063
|
_debug(`button clicked to ${screenshareEnabled ? 'en' : 'dis'}able screen sharing`);
|
|
1064
|
+
// Capture the previous camera state so we can restore it if
|
|
1065
|
+
// the screen-picker is canceled (getDisplayMedia rejects).
|
|
1066
|
+
// Without this, clicking screenshare → cancel turns the
|
|
1067
|
+
// camera off permanently from the user's perspective and
|
|
1068
|
+
// leaves a stale disabled camera track in the local stream.
|
|
1069
|
+
const wasCameraOn = this._selfViewButtons.video.enabled;
|
|
1064
1070
|
// Unconditionally disable the camera. Either screen sharing was previously disabled in
|
|
1065
1071
|
// which case the user now wants to share the screen, or screen sharing was previously
|
|
1066
1072
|
// enabled in which case the user wants to shut off all video.
|
|
1067
1073
|
this._selfViewButtons.video.enabled = false;
|
|
1068
1074
|
this._selfViewButtons.screenshare.enabled = screenshareEnabled;
|
|
1069
1075
|
await this.updateLocalTracks({updateVideo: true});
|
|
1076
|
+
// If the user wanted to start screensharing but the picker
|
|
1077
|
+
// was canceled or denied, updateLocalTracks has flipped
|
|
1078
|
+
// screenshare.enabled back to false. Bring the camera back
|
|
1079
|
+
// to the state it was in before the click so the user isn't
|
|
1080
|
+
// left with no video at all.
|
|
1081
|
+
if (screenshareEnabled && wasCameraOn &&
|
|
1082
|
+
!this._selfViewButtons.screenshare.enabled) {
|
|
1083
|
+
this._selfViewButtons.video.enabled = true;
|
|
1084
|
+
await this.updateLocalTracks({updateVideo: true});
|
|
1085
|
+
}
|
|
1070
1086
|
// Don't use `await` here -- see the comment for the audio button click handler above.
|
|
1071
1087
|
this.unmuteAndPlayAll();
|
|
1072
1088
|
},
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {expect, test} from '@playwright/test';
|
|
2
|
+
import {goToNewPadWithParams, installFakeGetUserMedia, setPadPrefsCookie} from '../helper/utils';
|
|
3
|
+
|
|
4
|
+
test.describe('screen share button', () => {
|
|
5
|
+
test.beforeEach(async ({page, context}) => {
|
|
6
|
+
test.setTimeout(60_000);
|
|
7
|
+
await context.clearCookies();
|
|
8
|
+
await setPadPrefsCookie(page, {
|
|
9
|
+
rtcEnabled: false,
|
|
10
|
+
audioEnabledOnStart: false,
|
|
11
|
+
videoEnabledOnStart: true,
|
|
12
|
+
});
|
|
13
|
+
await goToNewPadWithParams(page, {});
|
|
14
|
+
await installFakeGetUserMedia(page);
|
|
15
|
+
// Stub getDisplayMedia onto navigator.mediaDevices so the
|
|
16
|
+
// screenshare-btn doesn't get hidden by addInterface (it checks
|
|
17
|
+
// typeof navigator.mediaDevices.getDisplayMedia === 'function').
|
|
18
|
+
// Tests below override this stub per-case to control behavior.
|
|
19
|
+
await page.evaluate(() => {
|
|
20
|
+
const w = window as any;
|
|
21
|
+
w.__getDisplayMediaCallCount = 0;
|
|
22
|
+
w.navigator.mediaDevices.getDisplayMedia = async () => {
|
|
23
|
+
w.__getDisplayMediaCallCount++;
|
|
24
|
+
const err: any = new Error('user cancelled the picker');
|
|
25
|
+
err.name = 'NotAllowedError';
|
|
26
|
+
throw err;
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
await page.evaluate(() => (window as any).ep_webrtc.activate());
|
|
30
|
+
await page.waitForFunction(
|
|
31
|
+
() => (window as any).$('#rtcbox').data('initialized'));
|
|
32
|
+
// Confirm the starting state: camera on, screenshare off.
|
|
33
|
+
await page.waitForFunction(() => {
|
|
34
|
+
const w = window as any;
|
|
35
|
+
const videoBtn = w.$('.video-btn');
|
|
36
|
+
const ssBtn = w.$('.screenshare-btn');
|
|
37
|
+
return videoBtn.length === 1 && !videoBtn.hasClass('off') &&
|
|
38
|
+
ssBtn.length === 1 && ssBtn.hasClass('off');
|
|
39
|
+
}, undefined, {timeout: 5000});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('canceling the screen-picker restores the camera', async ({page}) => {
|
|
43
|
+
// Click the screenshare button. Our stubbed getDisplayMedia
|
|
44
|
+
// rejects with NotAllowedError (the same error Chrome dispatches
|
|
45
|
+
// when the user clicks Cancel in the picker).
|
|
46
|
+
await page.evaluate(() => (window as any).$('.screenshare-btn').click());
|
|
47
|
+
await page.evaluate(
|
|
48
|
+
() => (window as any).$('.screenshare-btn').data('idle')('click'));
|
|
49
|
+
// The picker really fired.
|
|
50
|
+
const callCount = await page.evaluate(
|
|
51
|
+
() => (window as any).__getDisplayMediaCallCount);
|
|
52
|
+
expect(callCount).toBeGreaterThanOrEqual(1);
|
|
53
|
+
// Final state: camera is back on, screenshare is off.
|
|
54
|
+
const finalState = await page.evaluate(() => {
|
|
55
|
+
const w = window as any;
|
|
56
|
+
const videoBtn = w.$('.video-btn');
|
|
57
|
+
const ssBtn = w.$('.screenshare-btn');
|
|
58
|
+
return {
|
|
59
|
+
videoOff: videoBtn.hasClass('off'),
|
|
60
|
+
screenshareOff: ssBtn.hasClass('off'),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
expect(finalState).toEqual({videoOff: false, screenshareOff: true});
|
|
64
|
+
// And there's a live, enabled video track in the local stream so
|
|
65
|
+
// the user actually sees their camera again (not just a button
|
|
66
|
+
// toggled on with no underlying media).
|
|
67
|
+
const trackState = await page.evaluate(() => {
|
|
68
|
+
const w = window as any;
|
|
69
|
+
const v = document.querySelector('video') as HTMLVideoElement | null;
|
|
70
|
+
const stream = v && (v.srcObject as MediaStream | null);
|
|
71
|
+
const t = stream && stream.getVideoTracks()[0];
|
|
72
|
+
return t == null ? null : {enabled: t.enabled, readyState: t.readyState};
|
|
73
|
+
});
|
|
74
|
+
expect(trackState).toEqual({enabled: true, readyState: 'live'});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('canceling screen-picker when camera was off leaves both off', async ({page}) => {
|
|
78
|
+
// Turn camera off first.
|
|
79
|
+
await page.evaluate(() => (window as any).$('.video-btn').click());
|
|
80
|
+
await page.evaluate(
|
|
81
|
+
() => (window as any).$('.video-btn').data('idle')('click'));
|
|
82
|
+
// Sanity-check both buttons off, no live enabled video track.
|
|
83
|
+
const before = await page.evaluate(() => {
|
|
84
|
+
const w = window as any;
|
|
85
|
+
return {
|
|
86
|
+
videoOff: w.$('.video-btn').hasClass('off'),
|
|
87
|
+
screenshareOff: w.$('.screenshare-btn').hasClass('off'),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
expect(before).toEqual({videoOff: true, screenshareOff: true});
|
|
91
|
+
// Click screenshare → cancel.
|
|
92
|
+
await page.evaluate(() => (window as any).$('.screenshare-btn').click());
|
|
93
|
+
await page.evaluate(
|
|
94
|
+
() => (window as any).$('.screenshare-btn').data('idle')('click'));
|
|
95
|
+
// Both should remain off (don't auto-enable the camera the user
|
|
96
|
+
// had explicitly turned off).
|
|
97
|
+
const after = await page.evaluate(() => {
|
|
98
|
+
const w = window as any;
|
|
99
|
+
return {
|
|
100
|
+
videoOff: w.$('.video-btn').hasClass('off'),
|
|
101
|
+
screenshareOff: w.$('.screenshare-btn').hasClass('off'),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
expect(after).toEqual({videoOff: true, screenshareOff: true});
|
|
105
|
+
});
|
|
106
|
+
});
|