Skip to content

Commit

Permalink
fix(rtc): play track on state changes
Browse files Browse the repository at this point in the history
  • Loading branch information
crimx committed Mar 27, 2023
1 parent 42e3141 commit 42afef0
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 137 deletions.
9 changes: 2 additions & 7 deletions packages/agora-rtc-react/src/components/LocalAudioTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MaybePromiseOrNull } from "../utils";

import { useEffect } from "react";
import { useAwaited } from "../hooks";
import { useAutoStopTrack } from "./TrackBoundary";
import { useAutoPlayAudioTrack } from "./TrackBoundary";

export interface LocalAudioTrackProps {
/**
Expand Down Expand Up @@ -48,13 +48,8 @@ export function LocalAudioTrack({
children,
}: LocalAudioTrackProps) {
const track = useAwaited(maybeTrack);
useAutoStopTrack(track);

useEffect(() => {
if (track && play !== track.isPlaying) {
play ? track.play() : track.stop();
}
}, [play, track]);
useAutoPlayAudioTrack(track, play);

useEffect(() => {
if (track && volume != null) {
Expand Down
11 changes: 2 additions & 9 deletions packages/agora-rtc-react/src/components/LocalVideoTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { MaybePromiseOrNull } from "../utils";

import { useEffect, useState } from "react";
import { useAwaited } from "../hooks";
import { useAutoStopTrack } from "./TrackBoundary";
import { useMergedStyle, VideoTrackStyle } from "./styles";
import { useAutoPlayVideoTrack } from "./TrackBoundary";

export interface LocalVideoTrackProps extends HTMLProps<HTMLDivElement> {
/**
Expand Down Expand Up @@ -46,15 +46,8 @@ export function LocalVideoTrack({
const [div, setDiv] = useState<HTMLDivElement | null>(null);

const track = useAwaited(maybeTrack);
useAutoStopTrack(track);

useEffect(() => {
if (div && track && play) {
track.play(div);
} else if (track && !play && track.isPlaying) {
track.stop();
}
}, [div, play, track]);
useAutoPlayVideoTrack(track, play, div);

useEffect(() => {
if (track && disabled != null) {
Expand Down
10 changes: 2 additions & 8 deletions packages/agora-rtc-react/src/components/RemoteAudioTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ReactNode } from "react";
import type { Nullable } from "../utils";

import { useEffect } from "react";
import { useAutoStopTrack } from "./TrackBoundary";
import { useAutoPlayAudioTrack } from "./TrackBoundary";

export interface RemoteAudioTrackProps {
/**
Expand Down Expand Up @@ -40,13 +40,7 @@ export function RemoteAudioTrack({
volume,
children,
}: RemoteAudioTrackProps) {
useAutoStopTrack(track);

useEffect(() => {
if (track && play !== track.isPlaying) {
play ? track.play() : track.stop();
}
}, [play, track]);
useAutoPlayAudioTrack(track, play);

useEffect(() => {
if (track && playbackDeviceId != null) {
Expand Down
14 changes: 3 additions & 11 deletions packages/agora-rtc-react/src/components/RemoteVideoTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { IRemoteVideoTrack } from "agora-rtc-sdk-ng";
import type { HTMLProps } from "react";
import type { Nullable } from "../utils";

import { useEffect, useState } from "react";
import { useAutoStopTrack } from "./TrackBoundary";
import { useState } from "react";
import { useAutoPlayVideoTrack } from "./TrackBoundary";
import { useMergedStyle, VideoTrackStyle } from "./styles";

export interface RemoteVideoTrackProps extends HTMLProps<HTMLDivElement> {
Expand All @@ -24,15 +24,7 @@ export function RemoteVideoTrack({ track, play, style, ...props }: RemoteVideoTr
const mergedStyle = useMergedStyle(VideoTrackStyle, style);
const [div, setDiv] = useState<HTMLDivElement | null>(null);

useAutoStopTrack(track);

useEffect(() => {
if (div && track && play) {
track.play(div);
} else if (track && !play && track.isPlaying) {
track.stop();
}
}, [div, play, track]);
useAutoPlayVideoTrack(track, play, div);

return <div {...props} ref={setDiv} style={mergedStyle} />;
}
56 changes: 52 additions & 4 deletions packages/agora-rtc-react/src/components/TrackBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { ITrack } from "agora-rtc-sdk-ng";
import type {
ILocalAudioTrack,
ILocalVideoTrack,
IRemoteAudioTrack,
IRemoteVideoTrack,
ITrack,
} from "agora-rtc-sdk-ng";
import type { PropsWithChildren } from "react";
import type { Nullable } from "../utils";

Expand Down Expand Up @@ -88,19 +94,61 @@ export function TrackBoundary({ children }: PropsWithChildren) {
}

/**
* Stops local or remote track when the component unmounts.
* Stops local or remote video track when the component unmounts.
* If `<TrackBoundary />` exists in ancestor it will not stop track on unmount but delegates to TrackBoundary.
*/
export function useAutoStopTrack(track: Nullable<ITrack>) {
export function useAutoPlayVideoTrack(
track: Nullable<IRemoteVideoTrack | ILocalVideoTrack>,
play?: boolean,
div?: HTMLElement | null,
) {
const controller = useContext(TrackBoundaryContext);

useEffect(() => {
if (track) {
if (div && play) {
track.play(div);
}

if (controller) {
controller.onMount(track);
return () => controller.onUnmount(track);
} else {
return () => {
if (track.isPlaying) {
track.stop();
}
};
}
}
}, [track, div, play, controller]);
}

/**
* Stops local or remote audio track when the component unmounts.
* If `<TrackBoundary />` exists in ancestor it will not stop track on unmount but delegates to TrackBoundary.
*/
export function useAutoPlayAudioTrack(
track: Nullable<IRemoteAudioTrack | ILocalAudioTrack>,
play?: boolean,
) {
const controller = useContext(TrackBoundaryContext);

useIsomorphicLayoutEffect(() => {
if (track) {
if (play) {
track.play();
}

if (controller) {
controller.onMount(track);
return () => controller.onUnmount(track);
} else {
return () => track.stop();
return () => {
if (track.isPlaying) {
track.stop();
}
};
}
}
}, [track, controller]);
Expand Down
76 changes: 6 additions & 70 deletions packages/agora-rtc-react/src/hooks/tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MutableRefObject, Ref, RefObject } from "react";
import type { MaybePromise } from "../utils";
import { createAsyncTaskRunner } from "../utils";
import type { MaybePromise, AsyncTaskRunner } from "../utils";

import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";

Expand Down Expand Up @@ -132,76 +133,11 @@ export function useAsyncEffect(
effect: () => MaybePromise<void | (() => MaybePromise<void>)>,
deps?: ReadonlyArray<unknown>,
): void {
const contextRef = useRef<
| {
isRunning?: boolean;
nextTask?: () => MaybePromise<void>;
disposer?: void | (() => MaybePromise<void>);
}
| undefined
>();
const runnerRef = useRef<AsyncTaskRunner | undefined>();
useEffect(() => {
const context = (contextRef.current ||= {});

if (context.isRunning) {
context.nextTask = () => runTask(effect);
} else {
runTask(effect);
}

function runNextTask() {
const nextTask = context.nextTask;
if (nextTask) {
context.nextTask = void 0;
nextTask();
}
}

async function disposeEffect() {
const disposer = context.disposer;
if (disposer) {
context.disposer = void 0;
try {
await disposer();
} catch (e) {
console.error(e);
}
}
}

async function runTask(effect: () => MaybePromise<void | (() => MaybePromise<void>)>) {
context.isRunning = true;

await disposeEffect();

try {
context.disposer = await effect();
} catch (e) {
console.error(e);
}

context.isRunning = false;

runNextTask();
}

async function stopTask() {
context.isRunning = true;

await disposeEffect();

context.isRunning = false;

runNextTask();
}

return () => {
if (context.isRunning) {
context.nextTask = stopTask;
} else {
stopTask();
}
};
const { run, dispose } = (runnerRef.current ||= createAsyncTaskRunner());
run(effect);
return dispose;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
55 changes: 33 additions & 22 deletions packages/agora-rtc-react/src/hooks/tracks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import type {
IRemoteAudioTrack,
IRemoteVideoTrack,
} from "agora-rtc-sdk-ng";
import type { AsyncTaskRunner } from "../utils";

import { useEffect, useState } from "react";
import { interval, joinDisposers } from "../utils";
import { useEffect, useRef, useState } from "react";
import { createAsyncTaskRunner, interval, joinDisposers } from "../utils";
import { listen } from "../listen";
import { useRTCClient } from "./context";
import { useIsConnected } from "./client";
Expand Down Expand Up @@ -39,6 +40,7 @@ export function useRemoteUserTrack(
const trackName = mediaType === "audio" ? "audioTrack" : "videoTrack";
const [track, setTrack] = useState(user && user[trackName]);
const isConnected = useIsConnected();
const runnerRef = useRef<AsyncTaskRunner | undefined>();

useEffect(() => {
if (!user || !isConnected) return;
Expand All @@ -48,47 +50,56 @@ export function useRemoteUserTrack(
const hasTrack = mediaType === "audio" ? "hasAudio" : "hasVideo";
const uid = user.uid;

setTrack(user[trackName]);
const unsubscribe = async (
user: IAgoraRTCRemoteUser,
mediaType: "audio" | "video",
): Promise<void> => {
if (user[trackName] && resolvedClient.remoteUsers.some(({ uid }) => user.uid === uid)) {
try {
await resolvedClient.unsubscribe(user, mediaType);
} catch (e) {
console.error(e);
}
}
if (!isUnmounted) {
setTrack(void 0);
}
};

const subscribe = async (user: IAgoraRTCRemoteUser, mediaType: "audio" | "video") => {
try {
await resolvedClient.subscribe(user, mediaType);
await new Promise(resolve => setTimeout(resolve, 1000));
if (isUnmounted) {
if (user[hasTrack]) {
resolvedClient.unsubscribe(user, mediaType);
}
return;
if (!user[trackName] && resolvedClient.remoteUsers.some(({ uid }) => user.uid === uid)) {
await resolvedClient.subscribe(user, mediaType);
}
if (!isUnmounted) {
setTrack(user[trackName]);
}
setTrack(user[trackName]);
} catch (error) {
console.error(error);
}
};

const unsubscribe = (user: IAgoraRTCRemoteUser, mediaType: "audio" | "video"): Promise<void> =>
resolvedClient.unsubscribe(user, mediaType).catch(console.error);
const runner = (runnerRef.current ||= createAsyncTaskRunner());

if (!user[trackName] && user[hasTrack] && resolvedClient.remoteUsers.includes(user)) {
subscribe(user, mediaType);
if (!user[trackName] && user[hasTrack]) {
runner.run(() => subscribe(user, mediaType));
} else {
setTrack(user[trackName]);
}

return joinDisposers([
() => {
isUnmounted = true;
if (user[trackName] && resolvedClient.remoteUsers.includes(user)) {
unsubscribe(user, mediaType);
}
runner.dispose();
},
listen(resolvedClient, "user-published", (pubUser, pubMediaType) => {
if (pubUser.uid === uid && pubMediaType === mediaType) {
subscribe(pubUser, pubMediaType);
runner.run(() => subscribe(pubUser, mediaType));
}
}),
listen(resolvedClient, "user-unpublished", (pubUser, pubMediaType) => {
if (pubUser.uid === uid && pubMediaType === mediaType && pubUser[trackName]) {
unsubscribe(pubUser, mediaType);
setTrack(undefined);
if (pubUser.uid === uid && pubMediaType === mediaType) {
runner.run(() => unsubscribe(pubUser, mediaType));
}
}),
]);
Expand Down
Loading

0 comments on commit 42afef0

Please sign in to comment.