Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agora-appbuilder-core",
"version": "4.1.15",
"version": "4.1.16",
"description": "React Native template for RTE app builder",
"main": "index.js",
"files": [
Expand Down
4 changes: 2 additions & 2 deletions template/defaultConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ const DefaultConfig = {
CHAT_ORG_NAME: '',
CHAT_APP_NAME: '',
CHAT_URL: '',
CLI_VERSION: '3.1.15',
CORE_VERSION: '4.1.15',
CLI_VERSION: '3.1.16',
CORE_VERSION: '4.1.16',
DISABLE_LANDSCAPE_MODE: false,
STT_AUTO_START: false,
CLOUD_RECORDING_AUTO_START: false,
Expand Down
20 changes: 15 additions & 5 deletions template/src/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const WhiteboardListener = () => {
);

return () => {
LocalEventEmitter.on(
LocalEventEmitter.off(
LocalEventsEnum.WHITEBOARD_ACTIVE_LOCAL,
WhiteboardCallBack,
);
Expand All @@ -227,8 +227,13 @@ export const WhiteboardListener = () => {
};

useEffect(() => {
whiteboardActive && currentLayout !== 'pinned' && setLayout('pinned');
}, []);
if (whiteboardActive) {
dispatch({type: 'UserPin', value: [getWhiteboardUid()]});
if (currentLayout !== 'pinned') {
setLayout('pinned');
}
}
}, [whiteboardActive]);

const toggleWhiteboard = (
whiteboardActive: boolean,
Expand Down Expand Up @@ -420,8 +425,13 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => {
};

useEffect(() => {
whiteboardActive && currentLayout !== 'pinned' && setLayout('pinned');
}, []);
if (whiteboardActive) {
dispatch({type: 'UserPin', value: [getWhiteboardUid()]});
if (currentLayout !== 'pinned') {
setLayout('pinned');
}
}
}, [whiteboardActive]);

const WhiteboardCallBack = ({status}) => {
if (status) {
Expand Down
33 changes: 30 additions & 3 deletions template/src/components/whiteboard/WhiteboardCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
whiteboardContext,
whiteboardPaper,
} from './WhiteboardConfigure';
import {StyleSheet, View, Text} from 'react-native';
import {StyleSheet, View, Text, ActivityIndicator} from 'react-native';
import {RoomPhase, ApplianceNames} from 'white-web-sdk';
import WhiteboardToolBox from './WhiteboardToolBox';
import WhiteboardWidget from './WhiteboardWidget';
Expand All @@ -28,7 +28,8 @@ const WhiteboardCanvas: React.FC<WhiteboardCanvasInterface> = ({
showToolbox,
}) => {
const wbSurfaceRef = useRef();
const {whiteboardRoom, boardColor} = useContext(whiteboardContext);
const {whiteboardRoom, boardColor, whiteboardRoomState} =
useContext(whiteboardContext);

useEffect(function () {
if (whiteboardPaper) {
Expand All @@ -37,12 +38,22 @@ const WhiteboardCanvas: React.FC<WhiteboardCanvasInterface> = ({
}

return () => {
// unBindRoom();
if (whiteboardPaper?.parentElement === wbSurfaceRef?.current) {
wbSurfaceRef?.current?.removeChild(whiteboardPaper);
}
};
}, []);

const isSyncing = whiteboardRoomState === RoomPhase.Connecting;

return (
<>
{isSyncing && (
<View style={style.syncingOverlay}>
<ActivityIndicator size="large" color="#fff" />
<Text style={style.syncingText}>Syncing whiteboard...</Text>
</View>
)}
<WhiteboardWidget whiteboardRoom={whiteboardRoom} />
{showToolbox &&
//@ts-ignore
Expand Down Expand Up @@ -94,6 +105,22 @@ const style = StyleSheet.create({
paddingTop: 50,
paddingLeft: 20,
},
syncingOverlay: {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 20,
borderRadius: 4,
},
syncingText: {
color: '#fff',
marginTop: 12,
fontSize: 14,
},
});

export default WhiteboardCanvas;
175 changes: 166 additions & 9 deletions template/src/components/whiteboard/WhiteboardConfigure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
useEffect(() => {
if (
whiteboardRoomState === RoomPhase.Connected &&
// In livestream, don't recenter the camera locally when whiteboard gets pinned.
// Followers must inherit the broadcaster's current viewport instead.
!$config.EVENT_MODE &&
pinnedUid &&
pinnedUid == whiteboardUidRef.current
) {
Expand All @@ -141,10 +144,20 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
boardColor: boardColorRemote,
whiteboardLastImageUploadPosition: whiteboardLastImageUploadPositionRemote,
} = useRoomInfo();
const shouldUseCursorAdapter = !($config.EVENT_MODE && !isHost);
const {currentLayout} = useLayout();

useEffect(() => {
try {
const setWritable =
typeof whiteboardRoom?.current?.setWritable === 'function'
? whiteboardRoom.current.setWritable.bind(whiteboardRoom.current)
: undefined;

if (!setWritable) {
return;
}

if (
whiteboardRoomState === RoomPhase.Connected &&
isHost &&
Expand All @@ -157,9 +170,9 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
(activeUids[0] === getWhiteboardUid() ||
pinnedUid === getWhiteboardUid())
) {
whiteboardRoom?.current?.setWritable(true);
setWritable(true);
} else {
whiteboardRoom?.current?.setWritable(false);
setWritable(false);
}
}
} catch (error) {
Expand All @@ -170,7 +183,20 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
error,
);
}
}, [currentLayout, isHost, whiteboardRoomState, activeUids, pinnedUid]);
// activeUids[0] (the max-slot uid) is the only element checked in the condition above —
// using the full activeUids array would re-run setWritable on every participant join/leave,
// briefly stalling the SDK draw queue and causing cumulative lag for attendees.
}, [currentLayout, isHost, whiteboardRoomState, activeUids?.[0], pinnedUid]);

useEffect(() => {
if (whiteboardRoomState === RoomPhase.Connected) {
// Netless reads the bound element size for viewport math. Refresh when layout or
// pin state changes (those affect the whiteboard container size). Participant
// count changes do not affect container size in pinned layout, so activeUids.length
// is intentionally excluded to avoid redundant refreshes on every join.
whiteboardRoom.current?.refreshViewSize?.();
}
}, [whiteboardRoomState, currentLayout, pinnedUid]);

const BoardColorChangedCallBack = ({boardColor}) => {
setBoardColor(boardColor);
Expand Down Expand Up @@ -330,11 +356,18 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
const InitState = whiteboardRoomState;
try {
const index = randomIntFromInterval(0, 9);
const joinStartTs = Date.now();
setWhiteboardRoomState(RoomPhase.Connecting);
console.log('[whiteboard-lag] join:start', {
ts: joinStartTs,
isHost,
eventMode: $config.EVENT_MODE,
whiteboardUid: `${whiteboardUidRef.current}`,
});
logger.log(LogSource.Internals, 'WHITEBOARD', 'Trying to join room');
whiteWebSdkClient.current
.joinRoom({
cursorAdapter: cursorAdapter,
cursorAdapter: shouldUseCursorAdapter ? cursorAdapter : undefined,
uid: `${whiteboardUidRef.current}`,
uuid: room_uuid,
roomToken: room_token,
Expand All @@ -347,17 +380,125 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
},
})
.then(room => {
logger.log(LogSource.Internals, 'WHITEBOARD', 'Join room successful');
const joinSuccessTs = Date.now();
logger.log(
LogSource.Internals,
'WHITEBOARD',
'Join room successful',
isHost,
$config.EVENT_MODE,
);
console.log('[whiteboard-lag] join:success', {
ts: joinSuccessTs,
latencyMs: joinSuccessTs - joinStartTs,
isHost,
eventMode: $config.EVENT_MODE,
whiteboardUid: `${whiteboardUidRef.current}`,
});
whiteboardRoom.current = room;
cursorAdapter.setRoom(room);
whiteboardRoom.current?.setViewMode(ViewMode.Freedom);
if (shouldUseCursorAdapter) {
cursorAdapter.setRoom(room);
}
// In livestream: host who starts the whiteboard is Broadcaster (attendees follow their viewport),
// co-hosts are Followers (follow Broadcaster, auto-switch to Freedom when they interact with the board),
// If no Broadcaster exists in the room (e.g. all hosts dropped and rejoined), first host to join claims it.
// In meeting: everyone gets Freedom (independent viewport).
const noBroadcasterInRoom =
room.state.broadcastState.broadcasterId === undefined;
const viewMode = $config.EVENT_MODE
? isHost
? noBroadcasterInRoom
? ViewMode.Broadcaster
: ViewMode.Follower
: ViewMode.Follower
: ViewMode.Freedom;
console.log('[whiteboard-view-mode] initial', {
isHost,
eventMode: $config.EVENT_MODE,
whiteboardUid: `${whiteboardUidRef.current}`,
broadcasterId: room.state.broadcastState.broadcasterId,
noBroadcasterInRoom,
viewMode,
});
room.setViewMode(viewMode);
// In livestream, lock camera gestures for followers so touchpad pan/zoom
// cannot kick them out of follower mode into freedom.
room.disableCameraTransform =
$config.EVENT_MODE && viewMode === ViewMode.Follower;
console.log('[whiteboard-lag] viewmode:applied', {
ts: Date.now(),
isHost,
viewMode,
disableCameraTransform: room.disableCameraTransform,
broadcasterId: room.state.broadcastState.broadcasterId,
});

// In livestream, if the Broadcaster drops, the next host to detect it claims Broadcaster.
// hasSeenBroadcaster ensures we only react to an actual drop (not the transient
// undefined state during initial room sync before the Broadcaster is propagated).
if ($config.EVENT_MODE && isHost) {
let hasSeenBroadcaster = false;
room.callbacks.on('onRoomStateChanged', modifyState => {
const currentBroadcastState = room.state?.broadcastState;
if (currentBroadcastState?.broadcasterId !== undefined) {
hasSeenBroadcaster = true;
}
// broadcasterId becomes undefined only after a clean disconnect (unmount cleanup
// guarantees this), so this is a reliable signal that the Broadcaster dropped.
if (
hasSeenBroadcaster &&
currentBroadcastState?.broadcasterId === undefined
) {
console.log('[whiteboard-view-mode] promote-to-broadcaster', {
isHost,
whiteboardUid: `${whiteboardUidRef.current}`,
});
room.setViewMode(ViewMode.Broadcaster);
room.disableCameraTransform = false;
}
});
}
whiteboardRoom.current?.bindHtmlElement(whiteboardPaper);
console.log('[whiteboard-lag] bindHtmlElement', {
ts: Date.now(),
isHost,
viewMode,
});
whiteboardRoom.current?.refreshViewSize?.();
console.log('[whiteboard-lag] refreshViewSize:after-bind', {
ts: Date.now(),
isHost,
viewMode,
});
if ($config.EVENT_MODE && viewMode === ViewMode.Follower) {
// Late followers can occasionally mount before the broadcaster viewport
// is fully applied. Re-applying follower mode after the first bind/size
// refresh nudges Netless to sync the current broadcaster view immediately.
requestAnimationFrame(() => {
console.log('[whiteboard-lag] follower-resync:start', {
ts: Date.now(),
isHost,
});
room.refreshViewSize?.();
room.setViewMode(ViewMode.Follower);
console.log('[whiteboard-lag] follower-resync:done', {
ts: Date.now(),
isHost,
});
});
}
if (isHost && !isMobileUA()) {
whiteboardRoom.current?.setMemberState({
strokeColor: [0, 0, 0],
});
}
setWhiteboardRoomState(RoomPhase.Connected);
console.log('[whiteboard-lag] roomPhase:connected', {
ts: Date.now(),
isHost,
viewMode,
totalJoinLatencyMs: Date.now() - joinStartTs,
});
})
.catch(err => {
setWhiteboardRoomState(InitState);
Expand All @@ -378,11 +519,13 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
const InitState = whiteboardRoomState;
try {
setWhiteboardRoomState(RoomPhase.Disconnecting);
whiteboardRoom.current
const room = whiteboardRoom.current;
room
?.disconnect()
.then(() => {
room?.bindHtmlElement(null);
whiteboardRoom.current = {} as Room;
whiteboardUidRef.current = Date.now();
whiteboardRoom.current?.bindHtmlElement(null);
setWhiteboardRoomState(RoomPhase.Disconnected);
})
.catch(err => {
Expand Down Expand Up @@ -429,6 +572,20 @@ const WhiteboardConfigure: React.FC<WhiteboardPropsInterface> = props => {
}
}, [whiteboardActive]);

// Disconnect from whiteboard room when component unmounts (e.g. user leaves the call abruptly)
useEffect(() => {
return () => {
if (
whiteboardRoom.current &&
Object.keys(whiteboardRoom.current)?.length
) {
whiteboardRoom.current?.bindHtmlElement(null);
whiteboardRoom.current?.disconnect();
whiteboardRoom.current = {} as Room;
}
};
}, []);

const getWhiteboardUid = () => {
return whiteboardUidRef?.current;
};
Expand Down
Loading