feat: Surface the provider's error cause in the media_unavailable notification (#2599)
- Closes: #2592
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { MediaUnavailableIssueReason } from '../../../../card-controller/issues/issues/media-unavailable';
|
||||
import type { LiveError } from '../../utils/dispatch-live-error';
|
||||
import type { LivenessDetector, LivenessVerdict } from '../stream-liveness-controller';
|
||||
|
||||
const LIVE_ERROR_EVENT = 'advanced-camera-card:live:error';
|
||||
@@ -39,9 +39,7 @@ export class ProviderErrorDetector implements LivenessDetector {
|
||||
return this._verdict;
|
||||
}
|
||||
|
||||
private _handler = (
|
||||
ev: CustomEvent<MediaUnavailableIssueReason | undefined>,
|
||||
): void => {
|
||||
private _handler = (ev: CustomEvent<LiveError>): void => {
|
||||
ev.stopPropagation();
|
||||
if (this._verdict.state !== 'not_live') {
|
||||
// Authoritative: an explicit provider error overrides even direct frame
|
||||
@@ -50,7 +48,8 @@ export class ProviderErrorDetector implements LivenessDetector {
|
||||
this._verdict = {
|
||||
state: 'not_live',
|
||||
authority: 'hard',
|
||||
reason: ev.detail ?? 'playback_error',
|
||||
reason: ev.detail.reason ?? 'playback_error',
|
||||
description: ev.detail.detail,
|
||||
};
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ export type LivenessVerdict =
|
||||
authority: LivenessAuthority;
|
||||
reason: MediaUnavailableIssueReason;
|
||||
|
||||
// Free text naming the specific failure, when the detector's source knew
|
||||
// it. Omitted when only the categorical reason is known.
|
||||
description?: string;
|
||||
|
||||
// Whether the wrapper should replace the provider with a reconnecting
|
||||
// placeholder (a silent freeze, e.g. an unavailable camera). Omitted when
|
||||
// the provider renders its own error and should stay mounted.
|
||||
@@ -50,6 +54,9 @@ export type LivenessVerdict =
|
||||
interface StreamFailure {
|
||||
reason: MediaUnavailableIssueReason;
|
||||
|
||||
// Free text naming the specific failure, when known.
|
||||
description?: string;
|
||||
|
||||
// Whether the wrapper should replace the provider with a reconnecting
|
||||
// placeholder (a silent freeze, e.g. an unavailable camera). False when the
|
||||
// provider renders its own error and should stay mounted.
|
||||
@@ -133,7 +140,11 @@ export class StreamLivenessController implements ReactiveController {
|
||||
public getFailure(): StreamFailure | null {
|
||||
const verdict = this._getVerdict();
|
||||
return verdict.state === 'not_live'
|
||||
? { reason: verdict.reason, renderPlaceholder: !!verdict.renderPlaceholder }
|
||||
? {
|
||||
reason: verdict.reason,
|
||||
description: verdict.description,
|
||||
renderPlaceholder: !!verdict.renderPlaceholder,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -170,14 +181,17 @@ export class StreamLivenessController implements ReactiveController {
|
||||
private _onDetectorChange(): void {
|
||||
const verdict = this._getVerdict();
|
||||
if (verdict.state === 'not_live') {
|
||||
this._triggerMediaUnavailableIssue(verdict.reason);
|
||||
this._triggerMediaUnavailableIssue(verdict.reason, verdict.description);
|
||||
}
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
// Tell the issue framework this target's media is not loaded, surfacing the
|
||||
// media_unavailable issue (status bar + retry) and its throttled reload.
|
||||
private _triggerMediaUnavailableIssue(reason: MediaUnavailableIssueReason): void {
|
||||
private _triggerMediaUnavailableIssue(
|
||||
reason: MediaUnavailableIssueReason,
|
||||
description?: string,
|
||||
): void {
|
||||
const targetID = this._config.getTargetID();
|
||||
if (!targetID) {
|
||||
return;
|
||||
@@ -186,6 +200,7 @@ export class StreamLivenessController implements ReactiveController {
|
||||
key: 'media_unavailable',
|
||||
targetID,
|
||||
reason,
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import type { MediaUnavailableIssueReason } from '../../../card-controller/issues/issues/media-unavailable';
|
||||
import { fireAdvancedCameraCardEvent } from '../../../utils/fire-advanced-camera-card-event';
|
||||
|
||||
// What a provider knows about its own failure.
|
||||
export interface LiveError {
|
||||
// The cause, drawn from the fixed set the card can describe and illustrate.
|
||||
// Absent when the provider cannot narrow it down, in which case the liveness
|
||||
// detector falls back to a generic playback error.
|
||||
reason?: MediaUnavailableIssueReason;
|
||||
|
||||
// Free text naming the specific failure (e.g. "Failed to start WebRTC stream:
|
||||
// ..."). Absent when the provider has none.
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
'advanced-camera-card:live:error': CustomEvent<
|
||||
MediaUnavailableIssueReason | undefined
|
||||
>;
|
||||
'advanced-camera-card:live:error': CustomEvent<LiveError>;
|
||||
}
|
||||
}
|
||||
|
||||
// The optional reason lets a provider that knows why it failed drive a specific
|
||||
// media-unavailable message; absent, the liveness detector falls back to a
|
||||
// generic playback error.
|
||||
export function dispatchLiveErrorEvent(
|
||||
element: EventTarget,
|
||||
reason?: MediaUnavailableIssueReason,
|
||||
): void {
|
||||
fireAdvancedCameraCardEvent(element, 'live:error', reason);
|
||||
export function dispatchLiveErrorEvent(element: EventTarget, error?: LiveError): void {
|
||||
fireAdvancedCameraCardEvent(element, 'live:error', error ?? {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user