feat: Automatically recover from frozen live streams (#2569)
- Closes: #2099
This commit is contained in:
@@ -2,6 +2,7 @@ import type {
|
||||
MediaLoadedInfo,
|
||||
MediaLoadedInfoEventDetail,
|
||||
MediaLoadedInfoOwner,
|
||||
UnsubscribeCallback,
|
||||
} from '../types';
|
||||
import { onAbort } from '../utils/abort-signal';
|
||||
import { log } from '../utils/debug';
|
||||
@@ -13,6 +14,21 @@ interface ActiveEntry {
|
||||
owner: MediaLoadedInfoOwner;
|
||||
}
|
||||
|
||||
// A change delivered to subscribers: a target's active media loading or
|
||||
// unloading, or the selected target changing.
|
||||
export type MediaLoadedInfoChange =
|
||||
// A target's media (re)loaded. `cached` marks a replay (a reconnect
|
||||
// re-dispatch, not an actual reload) rather than a genuine load.
|
||||
| { type: 'load'; targetID: string; info: MediaLoadedInfo; cached: boolean }
|
||||
|
||||
// A target's media was retired.
|
||||
| { type: 'unload'; targetID: string }
|
||||
|
||||
// The selected (current) target changed.
|
||||
| { type: 'select'; targetID: string | null };
|
||||
|
||||
type MediaLoadedInfoChangeCallback = (change: MediaLoadedInfoChange) => void;
|
||||
|
||||
export class MediaLoadedInfoManager {
|
||||
private _api: CardMediaLoadedAPI;
|
||||
|
||||
@@ -31,18 +47,32 @@ export class MediaLoadedInfoManager {
|
||||
// and card-level side effects. Driven by ViewManager on every view change.
|
||||
private _selected: string | null = null;
|
||||
|
||||
// Subscribers are notified of every change to the tracked media: a target's
|
||||
// load / unload, and selection changes.
|
||||
private _subscribers = new Set<MediaLoadedInfoChangeCallback>();
|
||||
|
||||
constructor(api: CardMediaLoadedAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
const cleared = [...this._active.keys()];
|
||||
const hadSelected = this._selected !== null;
|
||||
this._active.clear();
|
||||
this._lastKnown.clear();
|
||||
this._selected = null;
|
||||
this._api.getConditionStateManager().setState({ mediaLoadedInfo: null });
|
||||
cleared.forEach((targetID) => this._notify({ type: 'unload', targetID }));
|
||||
if (hadSelected) {
|
||||
this._notify({ type: 'select', targetID: null });
|
||||
}
|
||||
}
|
||||
|
||||
public set(mediaLoadedInfo: MediaLoadedInfo, owner: MediaLoadedInfoOwner): void {
|
||||
public set(
|
||||
mediaLoadedInfo: MediaLoadedInfo,
|
||||
owner: MediaLoadedInfoOwner,
|
||||
cached?: boolean,
|
||||
): void {
|
||||
if (!isValidMediaLoadedInfo(mediaLoadedInfo) || !mediaLoadedInfo.targetID) {
|
||||
return;
|
||||
}
|
||||
@@ -60,6 +90,15 @@ export class MediaLoadedInfoManager {
|
||||
if (targetID === this._selected) {
|
||||
this._emitChange(mediaLoadedInfo);
|
||||
}
|
||||
|
||||
// Notify for every target, not just the selected one (e.g. a background
|
||||
// grid camera).
|
||||
this._notify({
|
||||
type: 'load',
|
||||
targetID,
|
||||
info: mediaLoadedInfo,
|
||||
cached: cached ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
public setSelected(targetID: string | null): void {
|
||||
@@ -69,6 +108,7 @@ export class MediaLoadedInfoManager {
|
||||
|
||||
this._selected = targetID;
|
||||
this._emitChange(targetID ? this._active.get(targetID)?.info ?? null : null);
|
||||
this._notify({ type: 'select', targetID });
|
||||
}
|
||||
|
||||
public get(): MediaLoadedInfo | null {
|
||||
@@ -92,16 +132,18 @@ export class MediaLoadedInfoManager {
|
||||
if (!(owner instanceof HTMLElement) || !targetID) {
|
||||
return;
|
||||
}
|
||||
this.set(ev.detail.info, owner);
|
||||
this.set(ev.detail.info, owner, ev.detail.cached);
|
||||
onAbort(ev.detail.signal, () => this._clearTarget(targetID, owner));
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
const cleared = [...this._active.keys()];
|
||||
const selectedHadInfo = !!this._selected && this._active.has(this._selected);
|
||||
this._active.clear();
|
||||
if (selectedHadInfo) {
|
||||
this._api.getConditionStateManager().setState({ mediaLoadedInfo: null });
|
||||
}
|
||||
cleared.forEach((targetID) => this._notify({ type: 'unload', targetID }));
|
||||
}
|
||||
|
||||
private _clearTarget(targetID: string, owner: MediaLoadedInfoOwner): void {
|
||||
@@ -114,6 +156,8 @@ export class MediaLoadedInfoManager {
|
||||
if (targetID === this._selected) {
|
||||
this._api.getConditionStateManager().setState({ mediaLoadedInfo: null });
|
||||
}
|
||||
|
||||
this._notify({ type: 'unload', targetID });
|
||||
}
|
||||
|
||||
private _emitChange(mediaLoadedInfo: MediaLoadedInfo | null): void {
|
||||
@@ -121,4 +165,13 @@ export class MediaLoadedInfoManager {
|
||||
this._api.getStyleManager().setExpandedMode();
|
||||
this._api.getCardElementManager().update();
|
||||
}
|
||||
|
||||
public subscribe(listener: MediaLoadedInfoChangeCallback): UnsubscribeCallback {
|
||||
this._subscribers.add(listener);
|
||||
return () => this._subscribers.delete(listener);
|
||||
}
|
||||
|
||||
private _notify(change: MediaLoadedInfoChange): void {
|
||||
[...this._subscribers].forEach((listener) => listener(change));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user