Skip to content
180 changes: 166 additions & 14 deletions src/v1/bandwidthRtc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });

// Simulate the websocket "open" handler re-emitting "init" after a reconnect.
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(addSpy).toHaveBeenCalledWith(mediaStream, undefined);
expect(offerSpy).toHaveBeenCalledTimes(1);
Expand All @@ -333,7 +333,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set(id, { mediaStream: makeLiveStream(id) });
}

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(addSpy).toHaveBeenCalledTimes(3);
expect(offerSpy).toHaveBeenCalledTimes(1);
Expand All @@ -350,7 +350,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {

(brtc as any).publishedStreams.set("stream-1", { mediaStream: makeLiveStream("stream-1") });

const initPromise = brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
const initPromise = brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);
// Give the wait loop a couple of polls to prove it is actually waiting, not racing ahead.
await new Promise((resolve) => setTimeout(resolve, 250));
expect(addSpy).not.toHaveBeenCalled();
Expand All @@ -373,7 +373,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set("stream-1", { mediaStream: makeLiveStream("stream-1") });
jest.useFakeTimers({ doNotFake: ["nextTick"] });

const initPromise = brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
const initPromise = brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);
await jest.advanceTimersByTimeAsync(15_000);
await initPromise;

Expand All @@ -394,7 +394,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
const mediaStream = makeLiveStream("stream-1");
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream, codecPreferences });

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(addSpy).toHaveBeenCalledWith(mediaStream, codecPreferences);
});
Expand Down Expand Up @@ -434,7 +434,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
const freshTrack = makeTrack("audio");
mockGetUserMedia.mockResolvedValue({ getTracks: () => [freshTrack] });

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

// Re-acquired using the stored constraints' audio settings, but only for the kind
// that actually ended - video is left out entirely rather than requested as false.
Expand All @@ -455,7 +455,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
const mediaStream = makeLiveStream("stream-1");
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(mockGetUserMedia).not.toHaveBeenCalled();
});
Expand All @@ -472,7 +472,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });
mockGetUserMedia.mockResolvedValue({ getTracks: () => [makeTrack("audio")] });

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(mockGetUserMedia).toHaveBeenCalledWith({ audio: true });
});
Expand All @@ -487,7 +487,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });
(brtc as any).localDtmfSenders.set(mediaStream.id, { insertDTMF: jest.fn(), canInsertDTMF: true });

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect((brtc as any).localDtmfSenders.size).toBe(0);
});
Expand All @@ -504,7 +504,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
const mediaStream = makeLiveStream("stream-1");
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });

await expect(brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any)).resolves.toBeUndefined();
await expect(brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true)).resolves.toBeUndefined();

expect(errorHandler).toHaveBeenCalledTimes(1);
expect(errorHandler.mock.calls[0][0].message).toContain("gateway said no");
Expand All @@ -523,7 +523,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });
mockGetUserMedia.mockRejectedValue(new Error("NotAllowedError"));

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(errorHandler).toHaveBeenCalledTimes(1);
expect(offerSpy).not.toHaveBeenCalled();
Expand All @@ -543,7 +543,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
const freshTrack = makeTrack("audio");
mockGetUserMedia.mockResolvedValue({ getTracks: () => [freshTrack] });

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

expect(freshTrack.enabled).toBe(false);
});
Expand All @@ -564,7 +564,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
(brtc as any).publishedStreams.set(healthyStream.id, { mediaStream: healthyStream });
mockGetUserMedia.mockRejectedValue(new Error("NotAllowedError"));

await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any);
await brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true);

// The healthy stream still gets attached and renegotiated...
expect(addSpy).toHaveBeenCalledWith(healthyStream, undefined);
Expand All @@ -587,7 +587,7 @@ describe("bandwidthRtcV1 init reconnect replay", () => {
const mediaStream = makeLiveStream("stream-1");
(brtc as any).publishedStreams.set(mediaStream.id, { mediaStream });

await expect(brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any)).resolves.toBeUndefined();
await expect(brtc.init({ publishSdpOffer: {}, subscribeSdpOffer: {} } as any, true)).resolves.toBeUndefined();
});

test("concurrent init() calls are serialized rather than interleaved", async () => {
Expand Down Expand Up @@ -657,3 +657,155 @@ describe("bandwidthRtcV1 connect method", () => {
expect(errorHandler).toHaveBeenCalledWith(error);
});
});

describe("bandwidthRtcV1 retryIceOnFailed", () => {
beforeAll(() => {
setupNavigatorMocks();
setupMocks();
});

beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

function makePc(connectionState: string) {
return { connectionState } as any as RTCPeerConnection;
}

test("does nothing when shouldRetry is false", async () => {
const brtc = new BandwidthRtc();
const offerPublishSdp = jest.spyOn(brtc as any, "offerPublishSdp");

await (brtc as any).retryIceOnFailed(makePc("failed"), "publish", false);

expect(offerPublishSdp).not.toHaveBeenCalled();
});

test("does not restart the publish connection when the subscribe connection failed", async () => {
const brtc = new BandwidthRtc();
const offerPublishSdp = jest.spyOn(brtc as any, "offerPublishSdp");

await (brtc as any).retryIceOnFailed(makePc("failed"), "subscribe", true);

expect(offerPublishSdp).not.toHaveBeenCalled();
});

test("re-offers once and stops once the publish connection recovers", async () => {
const brtc = new BandwidthRtc();
const pc = makePc("failed");
jest.spyOn(brtc as any, "offerPublishSdp").mockImplementation(async () => {
(pc as any).connectionState = "connected";
return {} as any;
});

await (brtc as any).retryIceOnFailed(pc, "publish", true);

expect((brtc as any).offerPublishSdp).toHaveBeenCalledTimes(1);
});

test("retries every 5s until the timeout elapses if still failed", async () => {
const brtc = new BandwidthRtc();
const pc = makePc("failed");
jest.spyOn(brtc as any, "offerPublishSdp").mockResolvedValue({} as any);

const done = (brtc as any).retryIceOnFailed(pc, "publish", true);
// Initial offer, then retries at 5s/10s/... up to the 30s timeout.
for (let i = 0; i < 6; i++) {
await Promise.resolve();
await jest.advanceTimersByTimeAsync(5_000);
}
await done;

expect((brtc as any).offerPublishSdp).toHaveBeenCalledTimes(7);
});

test("does not throw when a retry's offerPublishSdp rejects", async () => {
const brtc = new BandwidthRtc();
const pc = makePc("failed");
jest.spyOn(brtc as any, "offerPublishSdp").mockRejectedValue(new Error("signaling down"));

const done = (brtc as any).retryIceOnFailed(pc, "publish", true);
for (let i = 0; i < 6; i++) {
await Promise.resolve();
await jest.advanceTimersByTimeAsync(5_000);
}

await expect(done).resolves.not.toThrow();
});
});

describe("bandwidthRtcV1 init on signaling reconnect", () => {
beforeAll(() => {
setupNavigatorMocks();
setupMocks();
});

function makeMockStream(id: string) {
return { id, getTracks: () => [] };
}

function makePreferencesResponse() {
return {
publishSdpOffer: { sdpOffer: "publish-offer" },
subscribeSdpOffer: { sdpOffer: "subscribe-offer" },
} as any;
}

test("first init does not close old peer connections or re-publish", async () => {
const brtc = new BandwidthRtc();
const setupPeerConnection = jest.spyOn(brtc as any, "setupPeerConnection").mockResolvedValue({ close: jest.fn() });
const addStream = jest.spyOn(brtc as any, "addStreamToPublishingPeerConnection");
const offerPublishSdp = jest.spyOn(brtc as any, "offerPublishSdp").mockResolvedValue({});
(brtc as any).publishedStreams.set("stream-1", { mediaStream: makeMockStream("stream-1") });

await brtc.init(makePreferencesResponse());

expect(setupPeerConnection).toHaveBeenCalledTimes(2);
expect(addStream).not.toHaveBeenCalled();
expect(offerPublishSdp).not.toHaveBeenCalled();
});

test("reconnect closes stale peer connections and re-publishes existing streams", async () => {
const brtc = new BandwidthRtc();
const oldPublishPc = { close: jest.fn() };
const oldSubscribePc = { close: jest.fn() };
(brtc as any).publishingPeerConnection = oldPublishPc;
(brtc as any).subscribingPeerConnection = oldSubscribePc;
(brtc as any).subscribingPeerConnectionSdpRevision = 5;
(brtc as any).subscribeTrackMetadata.set("track-1", { from: "someone" });
(brtc as any).localDtmfSenders.set("stream-1", { insertDTMF: jest.fn() });

const stream = makeMockStream("stream-1");
(brtc as any).publishedStreams.set("stream-1", { mediaStream: stream });

jest.spyOn(brtc as any, "setupPeerConnection").mockResolvedValue({ close: jest.fn(), connectionState: "connected" });
const addStream = jest.spyOn(brtc as any, "addStreamToPublishingPeerConnection").mockImplementation(() => {});
const offerPublishSdp = jest.spyOn(brtc as any, "offerPublishSdp").mockResolvedValue({});

await brtc.init(makePreferencesResponse(), true);

expect(oldPublishPc.close).toHaveBeenCalledTimes(1);
expect(oldSubscribePc.close).toHaveBeenCalledTimes(1);
expect(addStream).toHaveBeenCalledWith(stream, undefined);
expect(offerPublishSdp).toHaveBeenCalledTimes(1);
expect((brtc as any).subscribingPeerConnectionSdpRevision).toBe(0);
expect((brtc as any).subscribeTrackMetadata.size).toBe(0);
expect((brtc as any).localDtmfSenders.size).toBe(0);
});

test("reconnect with no published streams does not re-offer", async () => {
const brtc = new BandwidthRtc();
(brtc as any).publishingPeerConnection = { close: jest.fn() };
(brtc as any).subscribingPeerConnection = { close: jest.fn() };
jest.spyOn(brtc as any, "setupPeerConnection").mockResolvedValue({ close: jest.fn() });
const offerPublishSdp = jest.spyOn(brtc as any, "offerPublishSdp").mockResolvedValue({});

await brtc.init(makePreferencesResponse(), true);

expect(offerPublishSdp).not.toHaveBeenCalled();
});
});
Loading