Skip to content

Commit

Permalink
Add utils for acquiring tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
third774 committed Jan 14, 2025
1 parent 270788f commit 610dd4d
Show file tree
Hide file tree
Showing 16 changed files with 237 additions and 1,108 deletions.
155 changes: 155 additions & 0 deletions fixtures/video-echo/app/components/Demo.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useMemo, useRef, useState } from "react";
import { devices$, ideallyGetTrack$, PartyTracks } from "partytracks/client";
import { useObservableAsValue, useOnEmit } from "partytracks/react";
import { map, of, shareReplay } from "rxjs";

import type { ComponentProps, ComponentRef } from "react";
import type { Observable } from "rxjs";

export function Demo() {
const [localFeedOn, setLocalFeedOn] = useState(true);
const [remoteFeedOn, setRemoteFeedOn] = useState(false);
const [preferredWebcamDeviceId, setPreferredWebcamDeviceId] = useState("");
const devices = useObservableAsValue(devices$);
const client = useMemo(
() =>
new PartyTracks({
apiBase: "/api/calls"
}),
[]
);

const peerConnectionState = useObservableAsValue(
client.peerConnectionState$,
"new"
);

const sessionId = useObservableAsValue(
useMemo(
() => client.session$.pipe(map((x) => x.sessionId)),
[client.session$]
),
null
);

const localVideoTrack$ = useWebcamTrack$(localFeedOn);
const localMicTrack$ = useMicTrack$(localFeedOn);
const remoteVideoTrack$ = useMemo(() => {
if (!localVideoTrack$ || !remoteFeedOn) return null;
return client.pull(client.push(localVideoTrack$));
}, [client, remoteFeedOn, localVideoTrack$]);
const remoteAudioTrack$ = useMemo(() => {
if (!localMicTrack$ || !remoteFeedOn) return null;
return client.pull(client.push(localMicTrack$));
}, [client, remoteFeedOn, localMicTrack$]);

return (
<div className="p-2 flex flex-col gap-3">
<div className="flex gap-2">
<Button onClick={() => setLocalFeedOn(!localFeedOn)}>
Turn Local {localFeedOn ? "Off" : "On"}
</Button>
<Button onClick={() => setRemoteFeedOn(!remoteFeedOn)}>
Turn Remote {remoteFeedOn ? "Off" : "On"}
</Button>
</div>
<div className="grid xl:grid-cols-2">
{localVideoTrack$ && localFeedOn && (
<Video videoTrack$={localVideoTrack$} />
)}
{localMicTrack$ && localFeedOn && (
<Audio audioTrack$={localMicTrack$} />
)}
{remoteVideoTrack$ && remoteFeedOn && (
<Video videoTrack$={remoteVideoTrack$} />
)}
{remoteAudioTrack$ && remoteFeedOn && (
<Audio audioTrack$={remoteAudioTrack$} />
)}
</div>
<select
value={preferredWebcamDeviceId}
onChange={(e) => setPreferredWebcamDeviceId(e.target.value)}
>
<option value="">Select webcam</option>
{devices
?.filter((d) => d.kind === "videoinput")
.map((d) => (
<option key={d.deviceId} value={d.deviceId}>
{d.label}
</option>
))}
</select>
<pre>
{JSON.stringify(
{ peerConnectionState, sessionId, preferredWebcamDeviceId },
null,
2
)}
</pre>
</div>
);
}

function Button(props: ComponentProps<"button">) {
return <button className="border px-1" {...props} />;
}

function Video(props: { videoTrack$: Observable<MediaStreamTrack | null> }) {
const ref = useRef<ComponentRef<"video">>(null);
useOnEmit(props.videoTrack$, (track) => {
if (!ref.current) return;
if (track) {
const mediaStream = new MediaStream();
mediaStream.addTrack(track);
ref.current.srcObject = mediaStream;
} else {
ref.current.srcObject = null;
}
});

return (
<video className="h-full w-full" ref={ref} autoPlay muted playsInline />
);
}

function Audio(props: { audioTrack$: Observable<MediaStreamTrack | null> }) {
const ref = useRef<ComponentRef<"audio">>(null);
useOnEmit(props.audioTrack$, (track) => {
if (!ref.current) return;
if (track) {
const mediaStream = new MediaStream();
mediaStream.addTrack(track);
ref.current.srcObject = mediaStream;
} else {
ref.current.srcObject = null;
}
});

// biome-ignore lint/a11y/useMediaCaption: Not able to generate captions for this currently.
return <audio className="h-full w-full" ref={ref} autoPlay playsInline />;
}

function useWebcamTrack$(enabled: boolean) {
return useMemo(() => {
if (!enabled) return null;
return ideallyGetTrack$({ kind: "videoinput" }).pipe(
shareReplay({
refCount: true,
bufferSize: 1
})
);
}, [enabled]);
}

function useMicTrack$(enabled: boolean) {
return useMemo(() => {
if (!enabled) return null;
return ideallyGetTrack$({ kind: "audioinput" }).pipe(
shareReplay({
refCount: true,
bufferSize: 1
})
);
}, [enabled]);
}
132 changes: 2 additions & 130 deletions fixtures/video-echo/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,136 +1,8 @@
import { useMemo, useRef, useState } from "react";
import { Demo } from "~/components/Demo.client";
import { useIsServer } from "~/hooks/useIsServer";
import { getUserMediaTrack$ } from "~/utils/rxjs/getUserMediaTrack$";
import { PartyTracks } from "partytracks/client";
import { useObservableAsValue, useOnEmit } from "partytracks/react";
import { map, shareReplay } from "rxjs";

import type { ComponentProps, ComponentRef } from "react";
import type { Observable } from "rxjs";

export default function Component() {
const isServer = useIsServer();
if (isServer) return null;
return <ClientOnlyDemo />;
}

function ClientOnlyDemo() {
const [localFeedOn, setLocalFeedOn] = useState(true);
const [remoteFeedOn, setRemoteFeedOn] = useState(false);
const partyTracks = useMemo(() => new PartyTracks(), []);

const peerConnectionState = useObservableAsValue(
partyTracks.peerConnectionState$,
"new"
);

const sessionId = useObservableAsValue(
useMemo(
() => partyTracks.session$.pipe(map((x) => x.sessionId)),
[partyTracks.session$]
),
null
);

const localVideoTrack$ = useWebcamTrack$(localFeedOn);
const localMicTrack$ = useMicTrack$(localFeedOn);
const remoteVideoTrack$ = useMemo(() => {
if (!localVideoTrack$ || !remoteFeedOn) return null;
return partyTracks.pull(partyTracks.push(localVideoTrack$));
}, [partyTracks, remoteFeedOn, localVideoTrack$]);
const remoteAudioTrack$ = useMemo(() => {
if (!localMicTrack$ || !remoteFeedOn) return null;
return partyTracks.pull(partyTracks.push(localMicTrack$));
}, [partyTracks, remoteFeedOn, localMicTrack$]);

return (
<div className="p-2 flex flex-col gap-3">
<div className="flex gap-2">
<Button onClick={() => setLocalFeedOn(!localFeedOn)}>
Turn Local {localFeedOn ? "Off" : "On"}
</Button>
<Button onClick={() => setRemoteFeedOn(!remoteFeedOn)}>
Turn Remote {remoteFeedOn ? "Off" : "On"}
</Button>
</div>
<div className="grid xl:grid-cols-2">
{localVideoTrack$ && localFeedOn && (
<Video videoTrack$={localVideoTrack$} />
)}
{localMicTrack$ && localFeedOn && (
<Audio audioTrack$={localMicTrack$} />
)}
{remoteVideoTrack$ && remoteFeedOn && (
<Video videoTrack$={remoteVideoTrack$} />
)}
{remoteAudioTrack$ && remoteFeedOn && (
<Audio audioTrack$={remoteAudioTrack$} />
)}
</div>
<pre>{JSON.stringify({ peerConnectionState, sessionId }, null, 2)}</pre>
</div>
);
}

function Button(props: ComponentProps<"button">) {
return <button className="border px-1" {...props} />;
}

function Video(props: { videoTrack$: Observable<MediaStreamTrack | null> }) {
const ref = useRef<ComponentRef<"video">>(null);
useOnEmit(props.videoTrack$, (track) => {
if (!ref.current) return;
if (track) {
const mediaStream = new MediaStream();
mediaStream.addTrack(track);
ref.current.srcObject = mediaStream;
} else {
ref.current.srcObject = null;
}
});

return (
<video className="h-full w-full" ref={ref} autoPlay muted playsInline />
);
}

function Audio(props: { audioTrack$: Observable<MediaStreamTrack | null> }) {
const ref = useRef<ComponentRef<"audio">>(null);
useOnEmit(props.audioTrack$, (track) => {
if (!ref.current) return;
if (track) {
const mediaStream = new MediaStream();
mediaStream.addTrack(track);
ref.current.srcObject = mediaStream;
} else {
ref.current.srcObject = null;
}
});

// biome-ignore lint/a11y/useMediaCaption: Not able to generate captions for this currently.
return <audio className="h-full w-full" ref={ref} autoPlay playsInline />;
}

function useWebcamTrack$(enabled: boolean) {
return useMemo(() => {
if (!enabled) return null;
return getUserMediaTrack$("videoinput").pipe(
shareReplay({
refCount: true,
bufferSize: 1
})
);
}, [enabled]);
}

function useMicTrack$(enabled: boolean) {
return useMemo(() => {
if (!enabled) return null;
return getUserMediaTrack$("audioinput").pipe(
shareReplay({
refCount: true,
bufferSize: 1
})
);
}, [enabled]);
return <Demo />;
}
Loading

0 comments on commit 610dd4d

Please sign in to comment.