fix: Clear stale media unavailable errors when a camera recovers (#2627)
- Closes: #2576
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
} from '../../../test-utils';
|
||||
|
||||
const ISSUE_TRIGGER_EVENT = 'advanced-camera-card:issue:trigger';
|
||||
const ISSUE_RESOLVE_EVENT = 'advanced-camera-card:issue:resolve';
|
||||
|
||||
const setup = (options?: { targetID?: string | null }) => {
|
||||
const host = createLitElement();
|
||||
@@ -41,11 +42,16 @@ const setup = (options?: { targetID?: string | null }) => {
|
||||
issueTriggers.push((ev as CustomEvent).detail),
|
||||
);
|
||||
|
||||
const issueResolves: unknown[] = [];
|
||||
host.addEventListener(ISSUE_RESOLVE_EVENT, (ev) =>
|
||||
issueResolves.push((ev as CustomEvent).detail),
|
||||
);
|
||||
|
||||
const failViaProviderError = (error?: LiveError): void => {
|
||||
dispatchLiveErrorEvent(host, error);
|
||||
};
|
||||
|
||||
return { host, controller, issueTriggers, failViaProviderError };
|
||||
return { host, controller, issueTriggers, issueResolves, failViaProviderError };
|
||||
};
|
||||
|
||||
const createPlayer = (): {
|
||||
@@ -139,7 +145,9 @@ describe('StreamLivenessController', () => {
|
||||
const { controller, issueTriggers, failViaProviderError } = setup();
|
||||
controller.hostConnected();
|
||||
|
||||
failViaProviderError({ detail: 'Failed to start WebRTC stream: no candidates' });
|
||||
failViaProviderError({
|
||||
description: 'Failed to start WebRTC stream: no candidates',
|
||||
});
|
||||
|
||||
expect(controller.getFailure()).toEqual({
|
||||
reason: 'playback_error',
|
||||
@@ -338,6 +346,129 @@ describe('StreamLivenessController', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('resolving the issue', () => {
|
||||
const setupWithLiveStream = async () => {
|
||||
const result = setup();
|
||||
const { player, fireMediaPlayerLiveness } = createPlayer();
|
||||
|
||||
result.controller.hostConnected();
|
||||
result.host.dispatchEvent(
|
||||
createMediaLoadedInfoEvent({
|
||||
info: createMediaLoadedInfo({ mediaPlayerController: player }),
|
||||
}),
|
||||
);
|
||||
await callIntersectionHandler(true);
|
||||
|
||||
return { ...result, fireMediaPlayerLiveness };
|
||||
};
|
||||
|
||||
it('should resolve when frames confirm the stream is flowing', async () => {
|
||||
const { issueResolves, fireMediaPlayerLiveness } = await setupWithLiveStream();
|
||||
|
||||
fireMediaPlayerLiveness(true);
|
||||
|
||||
expect(issueResolves).toEqual([
|
||||
{ key: 'media_unavailable', targetID: 'camera.office' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not resolve while a hard failure exists', async () => {
|
||||
const { issueResolves, failViaProviderError, fireMediaPlayerLiveness } =
|
||||
await setupWithLiveStream();
|
||||
|
||||
// A provider has authoritatively condemned the stream. Frames continuing
|
||||
// to arrive must not talk the card out of it.
|
||||
failViaProviderError();
|
||||
fireMediaPlayerLiveness(true);
|
||||
|
||||
expect(issueResolves).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not resolve without a target', async () => {
|
||||
const result = setup({ targetID: null });
|
||||
const { player, fireMediaPlayerLiveness } = createPlayer();
|
||||
|
||||
result.controller.hostConnected();
|
||||
result.host.dispatchEvent(
|
||||
createMediaLoadedInfoEvent({
|
||||
info: createMediaLoadedInfo({ mediaPlayerController: player }),
|
||||
}),
|
||||
);
|
||||
await callIntersectionHandler(true);
|
||||
fireMediaPlayerLiveness(true);
|
||||
|
||||
expect(result.issueResolves).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not resolve on reconnect without fresh evidence', async () => {
|
||||
const { controller, issueResolves, fireMediaPlayerLiveness } =
|
||||
await setupWithLiveStream();
|
||||
fireMediaPlayerLiveness(true);
|
||||
issueResolves.length = 0;
|
||||
|
||||
// Away and back with nothing observed in between: the previous `live` is
|
||||
// a memory of the old watch, not evidence about the new one.
|
||||
controller.hostDisconnected();
|
||||
controller.hostConnected();
|
||||
|
||||
expect(issueResolves).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not announce recovery for a stream that was reset away', async () => {
|
||||
const { controller, issueResolves, fireMediaPlayerLiveness } =
|
||||
await setupWithLiveStream();
|
||||
fireMediaPlayerLiveness(true);
|
||||
issueResolves.length = 0;
|
||||
|
||||
// The stream this `live` describes is being torn down, so resetting must
|
||||
// not report it as recovered.
|
||||
controller.reset();
|
||||
|
||||
expect(issueResolves).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not mix a stale live verdict with a freshly reset one', async () => {
|
||||
const { controller, host, issueResolves, issueTriggers, failViaProviderError } =
|
||||
setup();
|
||||
const { player, fireMediaPlayerLiveness } = createPlayer();
|
||||
controller.hostConnected();
|
||||
host.dispatchEvent(
|
||||
createMediaLoadedInfoEvent({
|
||||
info: createMediaLoadedInfo({ mediaPlayerController: player }),
|
||||
}),
|
||||
);
|
||||
await callIntersectionHandler(true);
|
||||
|
||||
// Frames say live, then a provider error condemns the stream. Both
|
||||
// verdicts are held at once, by different detectors.
|
||||
fireMediaPlayerLiveness(true);
|
||||
failViaProviderError();
|
||||
issueResolves.length = 0;
|
||||
issueTriggers.length = 0;
|
||||
|
||||
// Resetting clears them in turn. If any detector announced part-way
|
||||
// through, the cleared provider error would leave the stale `live`
|
||||
// unopposed and the card would report a recovery that never happened.
|
||||
controller.reset();
|
||||
|
||||
expect(issueResolves).toEqual([]);
|
||||
expect(issueTriggers).toEqual([]);
|
||||
});
|
||||
|
||||
it('should re-read the detectors once they have all been reset', async () => {
|
||||
const { controller, host, fireMediaPlayerLiveness } = await setupWithLiveStream();
|
||||
fireMediaPlayerLiveness(true);
|
||||
vi.mocked(host.requestUpdate).mockClear();
|
||||
|
||||
controller.reset();
|
||||
|
||||
// Anything the detectors say while being reset is ignored, so this is the
|
||||
// single read the controller makes once they are all done.
|
||||
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(controller.isLive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report not live despite confirmed frames when always_error overrides', async () => {
|
||||
const host = createLitElement();
|
||||
document.body.append(host);
|
||||
|
||||
Reference in New Issue
Block a user