@3dsource/angular-unreal-module 0.0.188 → 0.0.189
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -3,7 +3,7 @@ import { input, output, ChangeDetectionStrategy, Component, inject, DestroyRef,
|
|
|
3
3
|
import { NgTemplateOutlet, NgOptimizedImage, JsonPipe } from '@angular/common';
|
|
4
4
|
import { createAction, props, createReducer, on, createFeature, createSelector, Store, provideState } from '@ngrx/store';
|
|
5
5
|
import { Actions, ofType, createEffect, provideEffects } from '@ngrx/effects';
|
|
6
|
-
import { Subject, skip, share, timer, of, take, defer, switchMap, from, throwError, Observable, EMPTY, retry, merge, timeout as timeout$1, repeat, BehaviorSubject, interval,
|
|
6
|
+
import { Subject, skip, share, timer, of, take, defer, switchMap, from, throwError, Observable, EMPTY, retry, merge, timeout as timeout$1, repeat, BehaviorSubject, interval, takeUntil as takeUntil$1, fromEvent, tap as tap$1, throttleTime, map as map$1, delay, concat, debounceTime, mergeMap, startWith, takeWhile, NEVER, ignoreElements, scan, timestamp, pairwise, concatMap, animationFrameScheduler, shareReplay, firstValueFrom, asyncScheduler, filter as filter$1, withLatestFrom as withLatestFrom$1, Subscription, bufferTime, combineLatestWith } from 'rxjs';
|
|
7
7
|
import { UNREAL_CONFIG } from '@3dsource/angular-unreal-module/config';
|
|
8
8
|
export { UNREAL_CONFIG } from '@3dsource/angular-unreal-module/config';
|
|
9
9
|
import { tap, filter, takeUntil, catchError, map, timeout, finalize, first, switchMap as switchMap$1, distinctUntilChanged, withLatestFrom, exhaustMap, takeWhile as takeWhile$1, debounceTime as debounceTime$1, take as take$1, startWith as startWith$1 } from 'rxjs/operators';
|
|
@@ -2991,6 +2991,9 @@ class VideoService extends SubService {
|
|
|
2991
2991
|
// would drop that emission, so the stats interval (and `setViewportReady`,
|
|
2992
2992
|
// gated on `framesPerSecond`) would never start.
|
|
2993
2993
|
this.videoTrack$ = new BehaviorSubject(null);
|
|
2994
|
+
/** Bumped per attached peer connection: candidate-pair ids are only unique
|
|
2995
|
+
* inside one connection, so the epoch scopes the telemetry route ref. */
|
|
2996
|
+
this.pcEpoch = 0;
|
|
2994
2997
|
this.VideoEncoderQP = 0;
|
|
2995
2998
|
this.aggregatedStats = {};
|
|
2996
2999
|
/**
|
|
@@ -2999,7 +3002,7 @@ class VideoService extends SubService {
|
|
|
2999
3002
|
this.videoStats$ = this.videoTrack$.pipe(
|
|
3000
3003
|
// If videoTrack$ emits null, switch to EMPTY → the current interval is canceled.
|
|
3001
3004
|
switchMap$1((track) => track
|
|
3002
|
-
? interval(250).pipe(
|
|
3005
|
+
? interval(250).pipe(switchMap$1(() => from(this.getStats(track.pcClient, track.pcEpoch))), filter(Truthy))
|
|
3003
3006
|
: of(null)), filter(Truthy), share());
|
|
3004
3007
|
this.destroyAbort = new AbortController();
|
|
3005
3008
|
// Teardown is wired ONCE here instead of per `create()`. The per-create
|
|
@@ -3082,7 +3085,7 @@ class VideoService extends SubService {
|
|
|
3082
3085
|
this.audio.srcObject = new MediaStream(audioTracks);
|
|
3083
3086
|
this.playAudio(this.audio);
|
|
3084
3087
|
}
|
|
3085
|
-
this.videoTrack$.next({ stream, pcClient });
|
|
3088
|
+
this.videoTrack$.next({ stream, pcClient, pcEpoch: ++this.pcEpoch });
|
|
3086
3089
|
// Safari does not reliably honor the autoplay attribute when srcObject
|
|
3087
3090
|
// is bound post-creation for a WebRTC MediaStream; trigger play()
|
|
3088
3091
|
// explicitly and let safePlay() fall back if Safari refuses.
|
|
@@ -3165,7 +3168,7 @@ class VideoService extends SubService {
|
|
|
3165
3168
|
signal: this.destroyAbort.signal,
|
|
3166
3169
|
});
|
|
3167
3170
|
}
|
|
3168
|
-
async getStats(pcClient) {
|
|
3171
|
+
async getStats(pcClient, pcEpoch = 0) {
|
|
3169
3172
|
if (!pcClient || pcClient.connectionState === 'closed') {
|
|
3170
3173
|
return null;
|
|
3171
3174
|
}
|
|
@@ -3176,7 +3179,7 @@ class VideoService extends SubService {
|
|
|
3176
3179
|
if (!this.video) {
|
|
3177
3180
|
return null;
|
|
3178
3181
|
}
|
|
3179
|
-
return this.generateAggregatedStatsFunction(stats);
|
|
3182
|
+
return this.generateAggregatedStatsFunction(stats, pcEpoch);
|
|
3180
3183
|
}
|
|
3181
3184
|
catch (err) {
|
|
3182
3185
|
// Handle InvalidStateError or any transient WebRTC errors gracefully
|
|
@@ -3187,8 +3190,10 @@ class VideoService extends SubService {
|
|
|
3187
3190
|
throw err;
|
|
3188
3191
|
}
|
|
3189
3192
|
}
|
|
3190
|
-
generateAggregatedStatsFunction(stats) {
|
|
3193
|
+
generateAggregatedStatsFunction(stats, pcEpoch = 0) {
|
|
3191
3194
|
const newStat = {};
|
|
3195
|
+
let selectedPairId;
|
|
3196
|
+
let flaggedPairId;
|
|
3192
3197
|
// store each type of codec we can get stats on
|
|
3193
3198
|
newStat.codecs = {};
|
|
3194
3199
|
newStat.currentRoundTripTime = -1;
|
|
@@ -3197,7 +3202,9 @@ class VideoService extends SubService {
|
|
|
3197
3202
|
if (report.type === 'inbound-rtp' && report.kind === 'video') {
|
|
3198
3203
|
this.applyInboundVideoReport(newStat, report);
|
|
3199
3204
|
}
|
|
3200
|
-
//
|
|
3205
|
+
// Fallback RTT for when no selected pair resolves —
|
|
3206
|
+
// `applySelectedPair` overrides it with the selected pair's RTT.
|
|
3207
|
+
// Only read it from the *nominated* ICE candidate pair. The
|
|
3201
3208
|
// stats report contains multiple `candidate-pair` entries (one
|
|
3202
3209
|
// per host/srflx/relay combination tried during ICE); only the
|
|
3203
3210
|
// nominated pair carries a meaningful `currentRoundTripTime`,
|
|
@@ -3212,9 +3219,13 @@ class VideoService extends SubService {
|
|
|
3212
3219
|
Object.hasOwn(report, 'currentRoundTripTime')) {
|
|
3213
3220
|
newStat.currentRoundTripTime = report.currentRoundTripTime ?? 0;
|
|
3214
3221
|
}
|
|
3215
|
-
if (report.type === 'transport' &&
|
|
3216
|
-
|
|
3217
|
-
|
|
3222
|
+
if (report.type === 'transport' && report.selectedCandidatePairId) {
|
|
3223
|
+
selectedPairId = report.selectedCandidatePairId;
|
|
3224
|
+
}
|
|
3225
|
+
// Firefox reports no `transport` stats; its pre-standard `selected`
|
|
3226
|
+
// flag marks the same pair and is used only when the transport is absent.
|
|
3227
|
+
if (report.type === 'candidate-pair' && report.selected === true) {
|
|
3228
|
+
flaggedPairId = report.id;
|
|
3218
3229
|
}
|
|
3219
3230
|
// Store mimetype of each codec
|
|
3220
3231
|
if (Object.hasOwn(newStat, 'codecs') &&
|
|
@@ -3227,6 +3238,7 @@ class VideoService extends SubService {
|
|
|
3227
3238
|
.replace('audio/', '');
|
|
3228
3239
|
}
|
|
3229
3240
|
});
|
|
3241
|
+
this.applySelectedPair(newStat, stats, selectedPairId ?? flaggedPairId, pcEpoch);
|
|
3230
3242
|
this.applySessionTotals(newStat);
|
|
3231
3243
|
this.aggregatedStats = newStat;
|
|
3232
3244
|
return this.onAggregatedStats(newStat);
|
|
@@ -3268,24 +3280,37 @@ class VideoService extends SubService {
|
|
|
3268
3280
|
}
|
|
3269
3281
|
}
|
|
3270
3282
|
/**
|
|
3271
|
-
* Resolves the
|
|
3272
|
-
*
|
|
3273
|
-
*
|
|
3283
|
+
* Resolves the selected candidate pair into its route description, byte
|
|
3284
|
+
* counters and RTT. Anything missing from this report stays unknown, and a
|
|
3285
|
+
* malformed report never throws into the stats loop — the route is optional,
|
|
3286
|
+
* playback is not.
|
|
3274
3287
|
*/
|
|
3275
|
-
applySelectedPair(newStat, stats,
|
|
3276
|
-
|
|
3288
|
+
applySelectedPair(newStat, stats, pairId, pcEpoch) {
|
|
3289
|
+
newStat.selectedPair = null;
|
|
3290
|
+
if (!pairId) {
|
|
3277
3291
|
return;
|
|
3278
3292
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3293
|
+
try {
|
|
3294
|
+
const pair = stats.get(pairId);
|
|
3295
|
+
if (!pair) {
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
newStat.selectedPair = {
|
|
3299
|
+
ref: `${pcEpoch}:${pairId}`,
|
|
3300
|
+
timestamp: finite(pair.timestamp),
|
|
3301
|
+
bytesReceived: finite(pair.bytesReceived),
|
|
3302
|
+
bytesSent: finite(pair.bytesSent),
|
|
3303
|
+
local: candidateInfo(stats.get(pair.localCandidateId), true),
|
|
3304
|
+
remote: candidateInfo(stats.get(pair.remoteCandidateId), false),
|
|
3305
|
+
};
|
|
3306
|
+
const rtt = finite(pair.currentRoundTripTime);
|
|
3307
|
+
if (rtt !== undefined) {
|
|
3308
|
+
newStat.currentRoundTripTime = rtt;
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
catch {
|
|
3312
|
+
newStat.selectedPair = null;
|
|
3282
3313
|
}
|
|
3283
|
-
const local = stats.get(selectedPair.localCandidateId);
|
|
3284
|
-
const remote = stats.get(selectedPair.remoteCandidateId);
|
|
3285
|
-
newStat.selectedPair = {
|
|
3286
|
-
local: { type: local?.candidateType, url: local?.url },
|
|
3287
|
-
remote: { type: remote?.candidateType, url: remote?.url },
|
|
3288
|
-
};
|
|
3289
3314
|
}
|
|
3290
3315
|
/** Per-session values that do not come from a single report. */
|
|
3291
3316
|
applySessionTotals(newStat) {
|
|
@@ -3401,6 +3426,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
3401
3426
|
function pinBaseline(pinned, current) {
|
|
3402
3427
|
return (pinned || current);
|
|
3403
3428
|
}
|
|
3429
|
+
function finite(value) {
|
|
3430
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
3431
|
+
? value
|
|
3432
|
+
: undefined;
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* The route-relevant fields of one candidate. The address is uploaded for a
|
|
3436
|
+
* relay candidate only — host / srflx addresses identify the user, a relay
|
|
3437
|
+
* address identifies the TURN provider's region.
|
|
3438
|
+
*/
|
|
3439
|
+
function candidateInfo(candidate, isLocal) {
|
|
3440
|
+
if (!candidate) {
|
|
3441
|
+
return {};
|
|
3442
|
+
}
|
|
3443
|
+
const isRelay = candidate.candidateType === 'relay';
|
|
3444
|
+
return {
|
|
3445
|
+
type: candidate.candidateType,
|
|
3446
|
+
protocol: candidate.protocol,
|
|
3447
|
+
...(isRelay
|
|
3448
|
+
? {
|
|
3449
|
+
address: candidate.address ?? undefined,
|
|
3450
|
+
port: finite(candidate.port),
|
|
3451
|
+
}
|
|
3452
|
+
: {}),
|
|
3453
|
+
...(isLocal
|
|
3454
|
+
? { url: candidate.url, relayProtocol: candidate.relayProtocol }
|
|
3455
|
+
: {}),
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3404
3458
|
/**
|
|
3405
3459
|
* Whether a rejected `play()` was blocked by the browser autoplay policy.
|
|
3406
3460
|
* Narrows an `unknown` rejection value without assuming it is an Error — a
|
|
@@ -6799,6 +6853,56 @@ class CounterWindow {
|
|
|
6799
6853
|
}
|
|
6800
6854
|
}
|
|
6801
6855
|
|
|
6856
|
+
/**
|
|
6857
|
+
* Turns one `stream.sample` window of 250 ms stats reads into its route
|
|
6858
|
+
* fields: the runs of reads that shared a selected pair, and the description
|
|
6859
|
+
* of every pair this session has not described yet.
|
|
6860
|
+
*
|
|
6861
|
+
* One per session, because the described refs are that session's: the
|
|
6862
|
+
* collector files each session separately, so every file must describe the
|
|
6863
|
+
* routes it references.
|
|
6864
|
+
*
|
|
6865
|
+
* Only the reads that are there are reported. A read without a selected pair
|
|
6866
|
+
* closes the run, and nothing is interpolated across it — the server sees the
|
|
6867
|
+
* gap between two segments and keeps it unattributed.
|
|
6868
|
+
*/
|
|
6869
|
+
class RouteLog {
|
|
6870
|
+
constructor() {
|
|
6871
|
+
this.described = new Set();
|
|
6872
|
+
}
|
|
6873
|
+
take(samples) {
|
|
6874
|
+
const routeSegments = [];
|
|
6875
|
+
const routes = {};
|
|
6876
|
+
let open = null;
|
|
6877
|
+
for (const { aggregatedStats } of samples) {
|
|
6878
|
+
const pair = aggregatedStats.selectedPair;
|
|
6879
|
+
if (!pair) {
|
|
6880
|
+
open = null;
|
|
6881
|
+
continue;
|
|
6882
|
+
}
|
|
6883
|
+
const ts = pair.timestamp === undefined ? null : Math.round(pair.timestamp);
|
|
6884
|
+
const rx = pair.bytesReceived ?? null;
|
|
6885
|
+
const tx = pair.bytesSent ?? null;
|
|
6886
|
+
if (open?.ref === pair.ref) {
|
|
6887
|
+
open.ts[1] = ts;
|
|
6888
|
+
open.rx[1] = rx;
|
|
6889
|
+
open.tx[1] = tx;
|
|
6890
|
+
continue;
|
|
6891
|
+
}
|
|
6892
|
+
open = { ref: pair.ref, ts: [ts, ts], rx: [rx, rx], tx: [tx, tx] };
|
|
6893
|
+
routeSegments.push(open);
|
|
6894
|
+
if (!this.described.has(pair.ref)) {
|
|
6895
|
+
this.described.add(pair.ref);
|
|
6896
|
+
routes[pair.ref] = { local: pair.local, remote: pair.remote };
|
|
6897
|
+
}
|
|
6898
|
+
}
|
|
6899
|
+
return {
|
|
6900
|
+
...(routeSegments.length ? { routeSegments } : {}),
|
|
6901
|
+
...(Object.keys(routes).length ? { routes } : {}),
|
|
6902
|
+
};
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6802
6906
|
/**
|
|
6803
6907
|
* Telemetry for the WebRTC stream lifecycle.
|
|
6804
6908
|
*
|
|
@@ -7217,6 +7321,7 @@ class StreamStatusTelemetryService2 {
|
|
|
7217
7321
|
seq: 0,
|
|
7218
7322
|
sub,
|
|
7219
7323
|
bytes: new CounterWindow(),
|
|
7324
|
+
routes: new RouteLog(),
|
|
7220
7325
|
qpBad: false,
|
|
7221
7326
|
fpsLow: false,
|
|
7222
7327
|
maxFpsTarget: UNLIMITED_REFERENCE_FPS,
|
|
@@ -7661,6 +7766,7 @@ class StreamStatusTelemetryService2 {
|
|
|
7661
7766
|
width: latest?.frameWidth,
|
|
7662
7767
|
height: latest?.frameHeight,
|
|
7663
7768
|
},
|
|
7769
|
+
...s.routes.take(samples),
|
|
7664
7770
|
});
|
|
7665
7771
|
this.evaluateAnomalies(fps, qp);
|
|
7666
7772
|
}
|
|
@@ -7922,7 +8028,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
7922
8028
|
/** Compact one side of an ICE candidate pair to `"type (url)"` or just
|
|
7923
8029
|
* `type` (host candidates have no URL). */
|
|
7924
8030
|
function fmtCandidate(c) {
|
|
7925
|
-
if (!c) {
|
|
8031
|
+
if (!c?.type) {
|
|
7926
8032
|
return null;
|
|
7927
8033
|
}
|
|
7928
8034
|
return c.url ? `${c.type} (${c.url})` : c.type;
|
|
@@ -8695,17 +8801,42 @@ class VideoStatsComponent {
|
|
|
8695
8801
|
value: this.stats()?.aggregatedStats.VideoEncoderQP ?? 0,
|
|
8696
8802
|
}), /* @ts-ignore */
|
|
8697
8803
|
...(ngDevMode ? [{ debugName: "videoQP" }] : /* istanbul ignore next */ []));
|
|
8698
|
-
|
|
8804
|
+
/** What the window shows: the raw stats plus the few display-only values
|
|
8805
|
+
* derived here. Shared by the text rows and the graphs. */
|
|
8806
|
+
this.view = computed(() => {
|
|
8699
8807
|
const event = this.stats();
|
|
8700
|
-
if (!
|
|
8808
|
+
if (!event) {
|
|
8701
8809
|
return undefined;
|
|
8702
8810
|
}
|
|
8703
|
-
const
|
|
8704
|
-
|
|
8811
|
+
const stats = event.aggregatedStats;
|
|
8812
|
+
const pair = stats.selectedPair;
|
|
8813
|
+
const view = {
|
|
8814
|
+
...stats,
|
|
8705
8815
|
quality: event.quality,
|
|
8816
|
+
MBytesReceived: typeof stats.bytesReceived === 'number'
|
|
8817
|
+
? +(stats.bytesReceived / 1e6).toFixed(2)
|
|
8818
|
+
: undefined,
|
|
8819
|
+
// Only the stable part of the route: counters, stats timestamp and ref
|
|
8820
|
+
// change on every tick and would make the row jump.
|
|
8821
|
+
selectedPair: pair && {
|
|
8822
|
+
local: {
|
|
8823
|
+
type: pair.local.type,
|
|
8824
|
+
protocol: pair.local.protocol,
|
|
8825
|
+
url: pair.local.url,
|
|
8826
|
+
},
|
|
8827
|
+
remote: { type: pair.remote.type, protocol: pair.remote.protocol },
|
|
8828
|
+
},
|
|
8706
8829
|
};
|
|
8830
|
+
return view;
|
|
8831
|
+
}, /* @ts-ignore */
|
|
8832
|
+
...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
8833
|
+
this.videoStatus = computed(() => {
|
|
8834
|
+
const view = this.view();
|
|
8835
|
+
if (!this.isDevMode || !view) {
|
|
8836
|
+
return undefined;
|
|
8837
|
+
}
|
|
8707
8838
|
return this.fields
|
|
8708
|
-
.map((key) => ({ key, value:
|
|
8839
|
+
.map((key) => ({ key, value: view[key] }))
|
|
8709
8840
|
.filter((r) => !this.elementsToShow().includes(r.key));
|
|
8710
8841
|
}, /* @ts-ignore */
|
|
8711
8842
|
...(ngDevMode ? [{ debugName: "videoStatus" }] : /* istanbul ignore next */ []));
|
|
@@ -8740,9 +8871,7 @@ class VideoStatsComponent {
|
|
|
8740
8871
|
Math.floor(Math.random() * 16777216)
|
|
8741
8872
|
.toString(16)
|
|
8742
8873
|
.padStart(6, '0'),
|
|
8743
|
-
stat: computed(() => ({
|
|
8744
|
-
value: this.stats()?.aggregatedStats[key],
|
|
8745
|
-
})),
|
|
8874
|
+
stat: computed(() => ({ value: this.view()?.[key] })),
|
|
8746
8875
|
}));
|
|
8747
8876
|
this.graphList = computed(() => this.graphListItems.filter((r) => this.elementsToShow().includes(r.key)), /* @ts-ignore */
|
|
8748
8877
|
...(ngDevMode ? [{ debugName: "graphList" }] : /* istanbul ignore next */ []));
|