@tellescope/react-components 1.252.3 → 1.253.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.
- package/lib/cjs/Forms/inputs.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.js +28 -103
- package/lib/cjs/Forms/inputs.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioControls.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioControls.js +60 -43
- package/lib/cjs/TwilioVideo/TwilioControls.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioParticipant.js +33 -18
- package/lib/cjs/TwilioVideo/TwilioParticipant.js.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts +4 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/TwilioVideoContext.js +45 -9
- package/lib/cjs/TwilioVideo/TwilioVideoContext.js.map +1 -1
- package/lib/cjs/TwilioVideo/index.d.ts +1 -1
- package/lib/cjs/TwilioVideo/index.d.ts.map +1 -1
- package/lib/cjs/TwilioVideo/index.js +2 -1
- package/lib/cjs/TwilioVideo/index.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.js +28 -103
- package/lib/esm/Forms/inputs.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioControls.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioControls.js +62 -45
- package/lib/esm/TwilioVideo/TwilioControls.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioParticipant.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioParticipant.js +33 -18
- package/lib/esm/TwilioVideo/TwilioParticipant.js.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts +4 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/TwilioVideoContext.js +45 -9
- package/lib/esm/TwilioVideo/TwilioVideoContext.js.map +1 -1
- package/lib/esm/TwilioVideo/index.d.ts +1 -1
- package/lib/esm/TwilioVideo/index.d.ts.map +1 -1
- package/lib/esm/TwilioVideo/index.js +1 -1
- package/lib/esm/TwilioVideo/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/Forms/inputs.tsx +22 -101
- package/src/TwilioVideo/TwilioControls.tsx +53 -3
- package/src/TwilioVideo/TwilioParticipant.tsx +27 -16
- package/src/TwilioVideo/TwilioVideoContext.tsx +39 -3
- package/src/TwilioVideo/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tellescope/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.253.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./lib/cjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"@reduxjs/toolkit": "1.9.0",
|
|
52
52
|
"@stripe/react-stripe-js": "2.9.0",
|
|
53
53
|
"@stripe/stripe-js": "1.52.1",
|
|
54
|
-
"@tellescope/constants": "1.
|
|
55
|
-
"@tellescope/sdk": "1.
|
|
56
|
-
"@tellescope/types-client": "1.
|
|
57
|
-
"@tellescope/types-models": "1.
|
|
58
|
-
"@tellescope/types-utilities": "1.
|
|
59
|
-
"@tellescope/utilities": "1.
|
|
60
|
-
"@tellescope/validation": "1.
|
|
54
|
+
"@tellescope/constants": "1.253.0",
|
|
55
|
+
"@tellescope/sdk": "1.253.0",
|
|
56
|
+
"@tellescope/types-client": "1.253.0",
|
|
57
|
+
"@tellescope/types-models": "1.253.0",
|
|
58
|
+
"@tellescope/types-utilities": "1.253.0",
|
|
59
|
+
"@tellescope/utilities": "1.253.0",
|
|
60
|
+
"@tellescope/validation": "1.253.0",
|
|
61
61
|
"@twilio/video-processors": "3.2.0",
|
|
62
62
|
"css-to-react-native": "3.0.0",
|
|
63
63
|
"draft-js": "0.11.7",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
86
86
|
"react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
|
|
87
87
|
},
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "b99e2fae181b67bab736660f9e4b8f47acc276da",
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
}
|
package/src/Forms/inputs.tsx
CHANGED
|
@@ -1439,18 +1439,9 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
|
|
|
1439
1439
|
}) => {
|
|
1440
1440
|
const session = useResolvedSession()
|
|
1441
1441
|
const [loading, setLoading] = useState(false)
|
|
1442
|
-
const [polling, setPolling] = useState(false)
|
|
1443
1442
|
const [error, setError] = useState<string>()
|
|
1444
1443
|
|
|
1445
1444
|
const isEnduserSession = session.type === 'enduser'
|
|
1446
|
-
const pollTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
|
1447
|
-
|
|
1448
|
-
// Clean up polling timeout on unmount
|
|
1449
|
-
useEffect(() => {
|
|
1450
|
-
return () => {
|
|
1451
|
-
if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current)
|
|
1452
|
-
}
|
|
1453
|
-
}, [])
|
|
1454
1445
|
|
|
1455
1446
|
// Extract payerId from Insurance question response
|
|
1456
1447
|
const [payerId, memberId, payerName] = useMemo(() => {
|
|
@@ -1470,7 +1461,7 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
|
|
|
1470
1461
|
setError(undefined)
|
|
1471
1462
|
|
|
1472
1463
|
try {
|
|
1473
|
-
//
|
|
1464
|
+
// Single synchronous eligibility check
|
|
1474
1465
|
const { data } = await session.api.integrations.proxy_read({
|
|
1475
1466
|
id: enduserId,
|
|
1476
1467
|
integration: CANDID_TITLE,
|
|
@@ -1484,76 +1475,17 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
|
|
|
1484
1475
|
}),
|
|
1485
1476
|
})
|
|
1486
1477
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// If already completed, update answer immediately
|
|
1496
|
-
if (initialStatus === 'COMPLETED' || initialStatus === 'FAILED' || initialStatus === 'UNKNOWN') {
|
|
1497
|
-
onChange({
|
|
1498
|
-
payerId,
|
|
1499
|
-
status: initialStatus,
|
|
1500
|
-
coverageId,
|
|
1501
|
-
}, field.id)
|
|
1502
|
-
setLoading(false)
|
|
1503
|
-
return
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
// Step 2: Poll for results
|
|
1478
|
+
onChange({
|
|
1479
|
+
payerId,
|
|
1480
|
+
status: data?.status,
|
|
1481
|
+
benefits: data?.benefits,
|
|
1482
|
+
planMetadata: data?.planMetadata,
|
|
1483
|
+
}, field.id)
|
|
1507
1484
|
setLoading(false)
|
|
1508
|
-
setPolling(true)
|
|
1509
|
-
|
|
1510
|
-
const maxAttempts = 60 // 2 minutes at 2s intervals
|
|
1511
|
-
let attempts = 0
|
|
1512
|
-
|
|
1513
|
-
const pollForResult = async (): Promise<void> => {
|
|
1514
|
-
if (attempts >= maxAttempts) {
|
|
1515
|
-
setError('Eligibility check timed out. Please try again.')
|
|
1516
|
-
setPolling(false)
|
|
1517
|
-
return
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
attempts++
|
|
1521
|
-
|
|
1522
|
-
try {
|
|
1523
|
-
const { data: pollData } = await session.api.integrations.proxy_read({
|
|
1524
|
-
id: coverageId,
|
|
1525
|
-
integration: CANDID_TITLE,
|
|
1526
|
-
type: 'candid-eligibility-poll',
|
|
1527
|
-
query: JSON.stringify({ checkId }),
|
|
1528
|
-
})
|
|
1529
|
-
|
|
1530
|
-
const status = pollData?.status
|
|
1531
|
-
// Terminal statuses: COMPLETED, FAILED, or UNKNOWN (Candid returns UNKNOWN when eligibility cannot be determined)
|
|
1532
|
-
if (status === 'COMPLETED' || status === 'FAILED' || status === 'UNKNOWN') {
|
|
1533
|
-
onChange({
|
|
1534
|
-
payerId,
|
|
1535
|
-
status,
|
|
1536
|
-
coverageId,
|
|
1537
|
-
benefits: pollData?.benefits,
|
|
1538
|
-
}, field.id)
|
|
1539
|
-
setPolling(false)
|
|
1540
|
-
return
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// Still pending, poll again
|
|
1544
|
-
pollTimeoutRef.current = setTimeout(pollForResult, 2000)
|
|
1545
|
-
} catch (err: any) {
|
|
1546
|
-
setError(err?.message || 'Failed to check eligibility status')
|
|
1547
|
-
setPolling(false)
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
pollForResult()
|
|
1552
1485
|
} catch (err: any) {
|
|
1553
1486
|
setError(err?.message || 'Failed to check eligibility')
|
|
1554
1487
|
console.error('Candid eligibility check failed:', err)
|
|
1555
1488
|
setLoading(false)
|
|
1556
|
-
setPolling(false)
|
|
1557
1489
|
}
|
|
1558
1490
|
}, [session, field, payerId, memberId, payerName, onChange, enduserId])
|
|
1559
1491
|
|
|
@@ -1608,43 +1540,40 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
|
|
|
1608
1540
|
</Grid>
|
|
1609
1541
|
<Grid item>
|
|
1610
1542
|
<Typography variant="body1">
|
|
1611
|
-
|
|
1543
|
+
Checking eligibility...
|
|
1612
1544
|
</Typography>
|
|
1613
1545
|
</Grid>
|
|
1614
1546
|
<Grid item>
|
|
1615
1547
|
<Typography variant="body2" color="textSecondary">
|
|
1616
|
-
|
|
1548
|
+
This may take a few moments
|
|
1617
1549
|
</Typography>
|
|
1618
1550
|
</Grid>
|
|
1619
1551
|
</Grid>
|
|
1620
|
-
), [
|
|
1552
|
+
), [])
|
|
1621
1553
|
|
|
1622
1554
|
const resultsComponent = useMemo(() => {
|
|
1623
|
-
const
|
|
1624
|
-
const isFailed = value?.status === 'FAILED'
|
|
1555
|
+
const isActive = value?.status === 'ACTIVE'
|
|
1625
1556
|
return (
|
|
1626
1557
|
<Grid container spacing={2} direction="column">
|
|
1627
1558
|
<Grid item>
|
|
1628
1559
|
<Paper style={{
|
|
1629
1560
|
padding: 16,
|
|
1630
|
-
backgroundColor:
|
|
1631
|
-
border: `2px solid ${
|
|
1561
|
+
backgroundColor: isActive ? '#e8f5e9' : '#fff8e1',
|
|
1562
|
+
border: `2px solid ${isActive ? '#4caf50' : '#ffa000'}`
|
|
1632
1563
|
}}>
|
|
1633
1564
|
<Grid container spacing={2} direction="column" alignItems="center">
|
|
1634
1565
|
<Grid item>
|
|
1635
|
-
{
|
|
1566
|
+
{isActive ? (
|
|
1636
1567
|
<CheckCircleOutline style={{ fontSize: 48, color: '#4caf50' }} />
|
|
1637
1568
|
) : (
|
|
1638
|
-
<Typography variant="h2" style={{ color: '#
|
|
1569
|
+
<Typography variant="h2" style={{ color: '#ffa000' }}>!</Typography>
|
|
1639
1570
|
)}
|
|
1640
1571
|
</Grid>
|
|
1641
1572
|
<Grid item>
|
|
1642
1573
|
<Typography variant="h6" align="center">
|
|
1643
|
-
{
|
|
1574
|
+
{isActive
|
|
1644
1575
|
? `${payerName || 'Insurance'} eligibility verified`
|
|
1645
|
-
:
|
|
1646
|
-
? 'Eligibility check failed'
|
|
1647
|
-
: 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
|
|
1576
|
+
: 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
|
|
1648
1577
|
}
|
|
1649
1578
|
</Typography>
|
|
1650
1579
|
</Grid>
|
|
@@ -1655,9 +1584,9 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
|
|
|
1655
1584
|
)
|
|
1656
1585
|
}, [value, payerName])
|
|
1657
1586
|
|
|
1658
|
-
// Loading
|
|
1587
|
+
// Loading state for enduser sessions
|
|
1659
1588
|
if (isEnduserSession) {
|
|
1660
|
-
if (loading
|
|
1589
|
+
if (loading) { return checkingEligibilityComponent }
|
|
1661
1590
|
if (error) {
|
|
1662
1591
|
return errorComponent
|
|
1663
1592
|
}
|
|
@@ -1687,23 +1616,15 @@ export const CandidEligibilityInput = ({ field, value, onChange, responses, endu
|
|
|
1687
1616
|
</Grid>
|
|
1688
1617
|
)}
|
|
1689
1618
|
|
|
1690
|
-
{polling && (
|
|
1691
|
-
<Grid item>
|
|
1692
|
-
<Typography variant="body2" color="primary">
|
|
1693
|
-
{form_display_text_for_language(form, "Polling for results... (this may take 15-30 seconds)")}
|
|
1694
|
-
</Typography>
|
|
1695
|
-
</Grid>
|
|
1696
|
-
)}
|
|
1697
|
-
|
|
1698
1619
|
<Grid item container spacing={2}>
|
|
1699
1620
|
<Grid item>
|
|
1700
1621
|
<LoadingButton
|
|
1701
1622
|
variant="outlined"
|
|
1702
1623
|
onClick={checkEligibility}
|
|
1703
1624
|
submitText={form_display_text_for_language(form, "Check Eligibility")}
|
|
1704
|
-
submittingText={
|
|
1705
|
-
submitting={loading
|
|
1706
|
-
disabled={loading
|
|
1625
|
+
submittingText={form_display_text_for_language(form, "Checking...")}
|
|
1626
|
+
submitting={loading}
|
|
1627
|
+
disabled={loading}
|
|
1707
1628
|
/>
|
|
1708
1629
|
</Grid>
|
|
1709
1630
|
</Grid>
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
IconButton,
|
|
5
|
+
Button,
|
|
6
|
+
CircularProgress,
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogActions,
|
|
11
|
+
FormControlLabel,
|
|
12
|
+
Checkbox,
|
|
13
|
+
} from '@mui/material'
|
|
3
14
|
import {
|
|
4
15
|
Mic as MicIcon,
|
|
5
16
|
MicOff as MicOffIcon,
|
|
@@ -43,6 +54,24 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
43
54
|
room,
|
|
44
55
|
} = useTwilioVideo()
|
|
45
56
|
|
|
57
|
+
const [shareDialogOpen, setShareDialogOpen] = useState(false)
|
|
58
|
+
const [shareAudio, setShareAudio] = useState(true)
|
|
59
|
+
|
|
60
|
+
const handleScreenShareClick = () => {
|
|
61
|
+
if (isScreenSharing) {
|
|
62
|
+
// Already sharing — stop immediately, no dialog
|
|
63
|
+
toggleScreenShare()
|
|
64
|
+
} else {
|
|
65
|
+
setShareDialogOpen(true)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleConfirmShare = () => {
|
|
70
|
+
// This click is a user gesture, so getDisplayMedia is allowed
|
|
71
|
+
toggleScreenShare({ shareAudio })
|
|
72
|
+
setShareDialogOpen(false)
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
const handleLeave = () => {
|
|
47
76
|
disconnect()
|
|
48
77
|
onLeave?.()
|
|
@@ -58,6 +87,7 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
58
87
|
&& typeof navigator.mediaDevices.getDisplayMedia === 'function'
|
|
59
88
|
|
|
60
89
|
return (
|
|
90
|
+
<>
|
|
61
91
|
<Box
|
|
62
92
|
sx={{
|
|
63
93
|
display: 'flex',
|
|
@@ -115,7 +145,7 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
115
145
|
|
|
116
146
|
{showScreenShareProp && supportsScreenShare && (
|
|
117
147
|
<IconButton
|
|
118
|
-
onClick={
|
|
148
|
+
onClick={handleScreenShareClick}
|
|
119
149
|
disabled={!room}
|
|
120
150
|
sx={{
|
|
121
151
|
color: isScreenSharing ? '#4caf50' : 'white',
|
|
@@ -162,5 +192,25 @@ export const TwilioControlBar: React.FC<TwilioControlBarProps> = ({
|
|
|
162
192
|
</Button>
|
|
163
193
|
)}
|
|
164
194
|
</Box>
|
|
195
|
+
|
|
196
|
+
<Dialog open={shareDialogOpen} onClose={() => setShareDialogOpen(false)}>
|
|
197
|
+
<DialogTitle>Share your screen</DialogTitle>
|
|
198
|
+
<DialogContent>
|
|
199
|
+
<FormControlLabel
|
|
200
|
+
control={(
|
|
201
|
+
<Checkbox
|
|
202
|
+
checked={shareAudio}
|
|
203
|
+
onChange={e => setShareAudio(e.target.checked)}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
label="Also share audio"
|
|
207
|
+
/>
|
|
208
|
+
</DialogContent>
|
|
209
|
+
<DialogActions>
|
|
210
|
+
<Button onClick={() => setShareDialogOpen(false)}>Cancel</Button>
|
|
211
|
+
<Button variant="contained" onClick={handleConfirmShare}>Share</Button>
|
|
212
|
+
</DialogActions>
|
|
213
|
+
</Dialog>
|
|
214
|
+
</>
|
|
165
215
|
)
|
|
166
216
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
LocalParticipant,
|
|
5
5
|
VideoTrack,
|
|
6
6
|
AudioTrack,
|
|
7
|
+
RemoteAudioTrack,
|
|
7
8
|
RemoteTrackPublication,
|
|
8
9
|
LocalTrackPublication,
|
|
9
10
|
} from 'twilio-video'
|
|
@@ -20,6 +21,23 @@ export interface TwilioParticipantProps {
|
|
|
20
21
|
showScreenShare?: boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/** Renders a single remote audio track into its own <audio> element. */
|
|
25
|
+
const RemoteAudioTrackElement: React.FC<{ track: AudioTrack }> = ({ track }) => {
|
|
26
|
+
const audioRef = useRef<HTMLAudioElement>(null)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (audioRef.current) {
|
|
30
|
+
const el = audioRef.current
|
|
31
|
+
track.attach(el)
|
|
32
|
+
return () => {
|
|
33
|
+
track.detach(el)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, [track])
|
|
37
|
+
|
|
38
|
+
return <audio ref={audioRef} autoPlay />
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
24
42
|
participant,
|
|
25
43
|
isLocal = false,
|
|
@@ -28,10 +46,9 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
28
46
|
showScreenShare = false,
|
|
29
47
|
}) => {
|
|
30
48
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|
31
|
-
const audioRef = useRef<HTMLAudioElement>(null)
|
|
32
49
|
const [cameraTrack, setCameraTrack] = useState<VideoTrack | null>(null)
|
|
33
50
|
const [screenTrack, setScreenTrack] = useState<VideoTrack | null>(null)
|
|
34
|
-
const [
|
|
51
|
+
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([])
|
|
35
52
|
|
|
36
53
|
useEffect(() => {
|
|
37
54
|
const handleTrackSubscribed = (track: VideoTrack | AudioTrack) => {
|
|
@@ -42,7 +59,9 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
42
59
|
setCameraTrack(track as VideoTrack)
|
|
43
60
|
}
|
|
44
61
|
} else if (track.kind === 'audio') {
|
|
45
|
-
|
|
62
|
+
// Support multiple simultaneous audio tracks (e.g. microphone + screen-share audio).
|
|
63
|
+
// Add by identity, deduping so the same track isn't attached twice.
|
|
64
|
+
setAudioTracks(prev => (prev.includes(track as AudioTrack) ? prev : [...prev, track as AudioTrack]))
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
|
|
@@ -54,7 +73,7 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
54
73
|
setCameraTrack(null)
|
|
55
74
|
}
|
|
56
75
|
} else if (track.kind === 'audio') {
|
|
57
|
-
|
|
76
|
+
setAudioTracks(prev => prev.filter(t => t !== (track as AudioTrack)))
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
79
|
|
|
@@ -107,17 +126,6 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
107
126
|
}
|
|
108
127
|
}, [activeVideoTrack])
|
|
109
128
|
|
|
110
|
-
// Attach audio track (not for local participant to avoid echo)
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
if (audioTrack && audioRef.current && !isLocal) {
|
|
113
|
-
const el = audioRef.current
|
|
114
|
-
audioTrack.attach(el)
|
|
115
|
-
return () => {
|
|
116
|
-
audioTrack.detach(el)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}, [audioTrack, isLocal])
|
|
120
|
-
|
|
121
129
|
const shouldMirror = isLocal && !showScreenShare
|
|
122
130
|
|
|
123
131
|
return (
|
|
@@ -144,7 +152,10 @@ export const TwilioParticipant: React.FC<TwilioParticipantProps> = ({
|
|
|
144
152
|
transform: shouldMirror ? 'scaleX(-1)' : 'none',
|
|
145
153
|
}}
|
|
146
154
|
/>
|
|
147
|
-
{
|
|
155
|
+
{/* Render one audio element per track so simultaneous tracks (mic + screen-share audio) all play */}
|
|
156
|
+
{!isLocal && audioTracks.map(track => (
|
|
157
|
+
<RemoteAudioTrackElement key={(track as RemoteAudioTrack).sid} track={track} />
|
|
158
|
+
))}
|
|
148
159
|
{(() => {
|
|
149
160
|
const label = resolveIdentity(participant.identity)
|
|
150
161
|
if (!label && !isLocal) return null
|
|
@@ -11,6 +11,7 @@ import Video, {
|
|
|
11
11
|
import type { GaussianBlurBackgroundProcessor as GaussianBlurBackgroundProcessorType } from '@twilio/video-processors'
|
|
12
12
|
|
|
13
13
|
export const SCREEN_SHARE_TRACK_NAME = 'screen-share'
|
|
14
|
+
export const SCREEN_SHARE_AUDIO_TRACK_NAME = 'screen-share-audio'
|
|
14
15
|
|
|
15
16
|
export const BLUR_BACKGROUND_STORAGE_KEY = 'tellescope.twilio.blurBackground'
|
|
16
17
|
export const BLUR_BACKGROUND_ASSETS_PATH = '/twilio-video-processors'
|
|
@@ -64,7 +65,7 @@ export interface TwilioVideoActions {
|
|
|
64
65
|
disconnect: () => void
|
|
65
66
|
toggleVideo: () => Promise<void>
|
|
66
67
|
toggleAudio: () => void
|
|
67
|
-
toggleScreenShare: () => Promise<void>
|
|
68
|
+
toggleScreenShare: (options?: { shareAudio?: boolean }) => Promise<void>
|
|
68
69
|
toggleBlur: () => void
|
|
69
70
|
setIsHost: (isHost: boolean) => void
|
|
70
71
|
}
|
|
@@ -103,6 +104,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
103
104
|
const [isBlurLoading, setIsBlurLoading] = useState(false)
|
|
104
105
|
|
|
105
106
|
const localTracksRef = useRef<(LocalVideoTrack | LocalAudioTrack)[]>([])
|
|
107
|
+
const screenAudioTrackRef = useRef<LocalAudioTrack | null>(null)
|
|
106
108
|
const blurProcessorRef = useRef<GaussianBlurBackgroundProcessorType | null>(null)
|
|
107
109
|
const blurAttachedTrackRef = useRef<LocalVideoTrack | null>(null)
|
|
108
110
|
|
|
@@ -170,6 +172,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
170
172
|
track.stop()
|
|
171
173
|
})
|
|
172
174
|
localTracksRef.current = []
|
|
175
|
+
screenAudioTrackRef.current = null
|
|
173
176
|
setRoom(null)
|
|
174
177
|
setLocalVideoTrack(null)
|
|
175
178
|
setLocalAudioTrack(null)
|
|
@@ -197,6 +200,7 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
197
200
|
track.stop()
|
|
198
201
|
})
|
|
199
202
|
localTracksRef.current = []
|
|
203
|
+
screenAudioTrackRef.current = null
|
|
200
204
|
|
|
201
205
|
setRoom(null)
|
|
202
206
|
setLocalVideoTrack(null)
|
|
@@ -239,16 +243,27 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
239
243
|
setLocalScreenTrack(null)
|
|
240
244
|
setIsScreenSharing(false)
|
|
241
245
|
}
|
|
246
|
+
|
|
247
|
+
// Tear down the accompanying screen-share audio track, if any
|
|
248
|
+
if (screenAudioTrackRef.current) {
|
|
249
|
+
const audioTrack = screenAudioTrackRef.current
|
|
250
|
+
if (room) {
|
|
251
|
+
room.localParticipant.unpublishTrack(audioTrack)
|
|
252
|
+
}
|
|
253
|
+
audioTrack.stop()
|
|
254
|
+
localTracksRef.current = localTracksRef.current.filter(t => t !== audioTrack)
|
|
255
|
+
screenAudioTrackRef.current = null
|
|
256
|
+
}
|
|
242
257
|
}, [localScreenTrack, room])
|
|
243
258
|
|
|
244
|
-
const toggleScreenShare = useCallback(async () => {
|
|
259
|
+
const toggleScreenShare = useCallback(async (options?: { shareAudio?: boolean }) => {
|
|
245
260
|
if (isScreenSharing) {
|
|
246
261
|
stopScreenShare()
|
|
247
262
|
return
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
try {
|
|
251
|
-
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true })
|
|
266
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: !!options?.shareAudio })
|
|
252
267
|
const mediaStreamTrack = stream.getVideoTracks()[0]
|
|
253
268
|
const screenTrack = new LocalVideoTrack(mediaStreamTrack, { name: SCREEN_SHARE_TRACK_NAME })
|
|
254
269
|
|
|
@@ -260,6 +275,18 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
260
275
|
setLocalScreenTrack(screenTrack)
|
|
261
276
|
setIsScreenSharing(true)
|
|
262
277
|
|
|
278
|
+
// Capture & publish screen/tab audio if the browser provided an audio track
|
|
279
|
+
const audioMediaStreamTrack = stream.getAudioTracks()[0]
|
|
280
|
+
let screenAudioTrack: LocalAudioTrack | null = null
|
|
281
|
+
if (audioMediaStreamTrack) {
|
|
282
|
+
screenAudioTrack = new LocalAudioTrack(audioMediaStreamTrack, { name: SCREEN_SHARE_AUDIO_TRACK_NAME })
|
|
283
|
+
if (room) {
|
|
284
|
+
await room.localParticipant.publishTrack(screenAudioTrack)
|
|
285
|
+
}
|
|
286
|
+
localTracksRef.current.push(screenAudioTrack)
|
|
287
|
+
screenAudioTrackRef.current = screenAudioTrack
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
// Handle browser "Stop sharing" button
|
|
264
291
|
mediaStreamTrack.onended = () => {
|
|
265
292
|
if (room) {
|
|
@@ -269,6 +296,15 @@ export const TwilioVideoProvider: React.FC<TwilioVideoProviderProps> = ({ childr
|
|
|
269
296
|
localTracksRef.current = localTracksRef.current.filter(t => t !== screenTrack)
|
|
270
297
|
setLocalScreenTrack(null)
|
|
271
298
|
setIsScreenSharing(false)
|
|
299
|
+
|
|
300
|
+
if (screenAudioTrack) {
|
|
301
|
+
if (room) {
|
|
302
|
+
room.localParticipant.unpublishTrack(screenAudioTrack)
|
|
303
|
+
}
|
|
304
|
+
screenAudioTrack.stop()
|
|
305
|
+
localTracksRef.current = localTracksRef.current.filter(t => t !== screenAudioTrack)
|
|
306
|
+
screenAudioTrackRef.current = null
|
|
307
|
+
}
|
|
272
308
|
}
|
|
273
309
|
} catch (err) {
|
|
274
310
|
// User cancelled the screen share picker — not an error
|