Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Screensharing #638

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/engine/resource/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ export enum InteractableType {
Player = 3,
Portal = 4,
UI = 5,
Screenshare = 6,
}

export const InteractableResource = defineResource("interactable", ResourceType.Interactable, {
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/screenshares/screenshares.common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ScreenshareProps {
peerId: string;
}
19 changes: 19 additions & 0 deletions src/plugins/screenshares/screenshares.game.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { addComponent } from "bitecs";

import { GameState } from "../../engine/GameTypes";
import { getModule } from "../../engine/module/module.common";
import { PhysicsModule } from "../../engine/physics/physics.game";
import { RemoteNode } from "../../engine/resource/RemoteResources";
import { InteractableType } from "../../engine/resource/schema";
import { addInteractableComponent } from "../interaction/interaction.game";
import { ScreenshareProps } from "./screenshares.common";

export const ScreenshareComponent = new Map<number, ScreenshareProps>();

export const addScreenshareComponent = (ctx: GameState, node: RemoteNode, data: ScreenshareProps) => {
const physics = getModule(ctx, PhysicsModule);
addInteractableComponent(ctx, physics, node, InteractableType.Screenshare);
addComponent(ctx.world, ScreenshareComponent, node.eid);
ScreenshareComponent.set(node.eid, data);
return node;
};
10 changes: 10 additions & 0 deletions src/ui/hooks/useWorldInteraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ export function useWorldInteraction(
});
return;
}

if (interactableType === InteractableType.Screenshare) {
interactionCallback({
interactableType,
action,
name: "Screenshare",
peerId: message.peerId,
held: false,
});
}
};

const onExitedWorld = (ctx: IMainThreadContext, message: ExitedWorldMessage) => {
Expand Down
6 changes: 5 additions & 1 deletion src/ui/views/gltf-viewer/GLTFViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ function GLTFViewerUI() {
console.log("Interacted with portal", interaction);
return;
}
if (interactableType === InteractableType.Screenshare) {
console.log("Interacted with screenshare", interaction);
return;
}
}

if (interactableType === InteractableType.Player) {
Expand All @@ -230,7 +234,7 @@ function GLTFViewerUI() {
<>
<Stats statsEnabled={statsEnabled} />
{editorEnabled && <EditorView />}
{activeEntity && <EntityTooltip activeEntity={activeEntity} portalProcess={{ joining: false }} />}
{activeEntity && <EntityTooltip activeEntity={activeEntity} interactionProcess={{ loading: false }} />}
<Reticle />
</>
);
Expand Down
45 changes: 33 additions & 12 deletions src/ui/views/session/entity-tooltip/EntityTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ import { Icon } from "../../../atoms/icon/Icon";
import { Dots } from "../../../atoms/loading/Dots";
import { Text } from "../../../atoms/text/Text";
import { InteractionState } from "../../../hooks/useWorldInteraction";
import { IInteractionProcess } from "../world/WorldInteraction";
import "./EntityTooltip.css";

export interface IPortalProcess {
joining?: boolean;
error?: Error;
}

interface EntityTooltipProps {
activeEntity: InteractionState;
portalProcess: IPortalProcess;
interactionProcess: IInteractionProcess;
}

export function EntityTooltip({ activeEntity, portalProcess }: EntityTooltipProps) {
export function EntityTooltip({ activeEntity, interactionProcess }: EntityTooltipProps) {
return (
<div className="EntityTooltip">
{activeEntity.interactableType === InteractableType.Player && (
Expand Down Expand Up @@ -101,20 +97,20 @@ export function EntityTooltip({ activeEntity, portalProcess }: EntityTooltipProp
)}
{activeEntity.interactableType === InteractableType.Portal && (
<>
{portalProcess.joining && <Dots color="world" size="sm" />}
{interactionProcess.loading && <Dots color="world" size="sm" />}
<Text weight="bold" color="world">
{portalProcess.joining ? "Joining portal" : "Portal"}
{interactionProcess.loading ? "Joining portal" : "Portal"}
</Text>
<div className="flex flex-column gap-xxs">
<Text variant="b3" color="world">
{activeEntity.name}
</Text>
{portalProcess.error && (
{interactionProcess.error && (
<Text variant="b3" color="world">
{portalProcess.error.message ?? "Unknown error joining portal."}
{interactionProcess.error.message ?? "Unknown error joining portal."}
</Text>
)}
{!portalProcess.joining && (
{!interactionProcess.loading && (
<Text variant="b3" color="world">
<span className="EntityTooltip__boxedKey">E</span> /
<Icon src={MouseIC} size="sm" className="EntityTooltip__mouseIcon" color="world" />
Expand All @@ -124,6 +120,31 @@ export function EntityTooltip({ activeEntity, portalProcess }: EntityTooltipProp
</div>
</>
)}
{activeEntity.interactableType === InteractableType.Screenshare && (
<>
{interactionProcess.loading && <Dots color="world" size="sm" />}
<Text weight="bold" color="world">
{interactionProcess.loading ? "Sharing screen" : "Share screen"}
</Text>
<div className="flex flex-column gap-xxs">
<Text variant="b3" color="world">
{activeEntity.name}
</Text>
{interactionProcess.error && (
<Text variant="b3" color="world">
{`Failed to share screen: ${interactionProcess.error.message}`}
</Text>
)}
{!interactionProcess.loading && (
<Text variant="b3" color="world">
<span className="EntityTooltip__boxedKey">E</span> /
<Icon src={MouseIC} size="sm" className="EntityTooltip__mouseIcon" color="world" />
<span> Share your screen</span>
</Text>
)}
</div>
</>
)}
</div>
);
}
5 changes: 4 additions & 1 deletion src/ui/views/session/reticle/Reticle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export function Reticle() {
(activeEntity.interactableType === InteractableType.Grabbable ||
activeEntity.interactableType === InteractableType.Interactable),
Reticle__player: activeEntity && activeEntity.interactableType === InteractableType.Player,
Reticle__portal: activeEntity && activeEntity.interactableType === InteractableType.Portal,
Reticle__portal:
activeEntity &&
(activeEntity.interactableType === InteractableType.Portal ||
activeEntity.interactableType === InteractableType.Screenshare),
})}
/>
);
Expand Down
66 changes: 56 additions & 10 deletions src/ui/views/session/world/WorldInteraction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import { CameraRigModule } from "../../../../plugins/camera/CameraRig.main";
import { Reticle } from "../reticle/Reticle";
import { useWorldNavigator } from "../../../hooks/useWorldNavigator";
import { useWorldLoader } from "../../../hooks/useWorldLoader";
import { useHydrogen } from "../../../hooks/useHydrogen";

export interface IPortalProcess {
joining?: boolean;
export interface IInteractionProcess {
loading?: boolean;
error?: Error;
}

Expand All @@ -35,20 +36,62 @@ export function WorldInteraction({ session, world, activeCall }: WorldInteractio
const camRigModule = getModule(mainThread, CameraRigModule);

const [activeEntity, setActiveEntity] = useMemoizedState<InteractionState | undefined>();
const [portalProcess, setPortalProcess] = useMemoizedState<IPortalProcess>({});
const [interactionProcess, setInteractionProcess] = useMemoizedState<IInteractionProcess>({});
const [members, setMembers] = useState(false);
const { platform } = useHydrogen(true);

const { navigateEnterWorld } = useWorldNavigator(session);
const { exitWorld } = useWorldLoader();
const selectWorld = useSetAtom(overlayWorldAtom);
const isMounted = useIsMounted();

const handleScreenshareGrab = useCallback(
async (interaction) => {
setInteractionProcess({});

if (!activeCall) {
setInteractionProcess({ error: new Error("no active call") });
return;
}
const { peerId } = interaction;
if (peerId) {
// Someone else is already using it.
if (peerId === session.userId) {
// "someone else" is you. Stop using it. (bonus: don't stop screen sharing if you have multiple going)
console.log("Stopping screensharing.");
activeCall.localMedia?.screenShare?.getTracks().forEach((track) => track.stop());
} else {
// Tell the user to try again later.
const currentUser = peerId
? activeCall?.members.get(peerId)?.member.displayName || getMxIdUsername(peerId)
: "Player";
setInteractionProcess({ error: new Error(`${currentUser} is currently using this.`) });
}
} else {
// Nobody is using it, go ahead and share your screen.
if (!activeCall.localMedia) {
setInteractionProcess({ error: new Error("activeCall.localMedia not initialized") });
} else {
try {
// TODO: use platform.mediaDevices.getDisplayMedia({audio: true, video: true}) to get screenshare with PC audio?
// See hydrogen-web/src/platform/web/dom/MediaDevices.ts
const screenStream = await platform.mediaDevices.getScreenShareTrack();
if (screenStream) await activeCall.setMedia(activeCall.localMedia.withScreenShare(screenStream));
} catch (err) {
setInteractionProcess({ error: err as Error });
}
}
}
},
[activeCall, platform.mediaDevices, session.userId, setInteractionProcess]
);

const handlePortalGrab = useCallback(
async (interaction) => {
let unSubStatusObserver: () => void | undefined;

try {
setPortalProcess({});
setInteractionProcess({});
const { uri } = interaction;
if (!uri) throw Error("Portal does not have valid matrix id/alias");

Expand All @@ -62,11 +105,11 @@ export function WorldInteraction({ session, world, activeCall }: WorldInteractio
const roomId = roomIdOrAlias.startsWith("#") ? aliasToRoomId(session.rooms, parsedUri.mxid1) : parsedUri.mxid1;

if (!roomId) {
setPortalProcess({ joining: true });
setInteractionProcess({ loading: true });
const rId = await session.joinRoom(roomIdOrAlias);
if (!isMounted()) return;

setPortalProcess({});
setInteractionProcess({});
const roomStatusObserver = await session.observeRoomStatus(rId);
unSubStatusObserver = roomStatusObserver.subscribe(async (roomStatus) => {
const newWorld = session.rooms.get(rId);
Expand Down Expand Up @@ -99,13 +142,13 @@ export function WorldInteraction({ session, world, activeCall }: WorldInteractio
}
} catch (err) {
if (!isMounted()) return;
setPortalProcess({ error: err as Error });
setInteractionProcess({ error: err as Error });
}
return () => {
unSubStatusObserver?.();
};
},
[session, selectWorld, exitWorld, navigateEnterWorld, isMounted, setPortalProcess]
[session, selectWorld, exitWorld, navigateEnterWorld, isMounted, setInteractionProcess]
);

const handleInteraction = useCallback(
Expand All @@ -123,6 +166,9 @@ export function WorldInteraction({ session, world, activeCall }: WorldInteractio
handlePortalGrab(interaction);
return;
}
if (interactableType === InteractableType.Screenshare) {
handleScreenshareGrab(interaction);
}
}

if (interactableType === InteractableType.Player) {
Expand All @@ -135,7 +181,7 @@ export function WorldInteraction({ session, world, activeCall }: WorldInteractio

setActiveEntity(interaction);
},
[handlePortalGrab, setActiveEntity, activeCall]
[handlePortalGrab, handleScreenshareGrab, setActiveEntity, activeCall]
);

useWorldInteraction(mainThread, handleInteraction);
Expand All @@ -149,7 +195,7 @@ export function WorldInteraction({ session, world, activeCall }: WorldInteractio
)}
{!camRigModule.orbiting && <Reticle />}
{activeEntity && !camRigModule.orbiting && (
<EntityTooltip activeEntity={activeEntity} portalProcess={portalProcess} />
<EntityTooltip activeEntity={activeEntity} interactionProcess={interactionProcess} />
)}
</div>
);
Expand Down