@3dsource/angular-unreal-module 0.0.123 → 0.0.124
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.
|
@@ -1941,10 +1941,7 @@ class VideoService extends SubService {
|
|
|
1941
1941
|
interval(250).pipe(map(() => track.pcClient), switchMap$1((pcClient) => from(this.getStats(pcClient))), filter(Truthy))
|
|
1942
1942
|
: of(null)), filter(Truthy), share());
|
|
1943
1943
|
this.destroyAbort = new AbortController();
|
|
1944
|
-
this.destroyRef.onDestroy(() =>
|
|
1945
|
-
this.destroyAbort.abort();
|
|
1946
|
-
this.destroy();
|
|
1947
|
-
});
|
|
1944
|
+
this.destroyRef.onDestroy(() => this.destroy());
|
|
1948
1945
|
}
|
|
1949
1946
|
setContainer(container = null) {
|
|
1950
1947
|
this.container = container;
|
|
@@ -1956,6 +1953,9 @@ class VideoService extends SubService {
|
|
|
1956
1953
|
this.VideoEncoderQP = Number(new TextDecoder('utf-16').decode(data));
|
|
1957
1954
|
}
|
|
1958
1955
|
create() {
|
|
1956
|
+
if (!this.container) {
|
|
1957
|
+
Logger.warn('VideoService.create(): no container set; <video> will be built but never attached to the DOM, which suppresses autoplay.');
|
|
1958
|
+
}
|
|
1959
1959
|
this.disconnect$
|
|
1960
1960
|
.pipe(take(1), takeUntil$1(this.destroy$))
|
|
1961
1961
|
.subscribe(() => this.destroy());
|
|
@@ -1964,26 +1964,35 @@ class VideoService extends SubService {
|
|
|
1964
1964
|
this.createWebRtcAudio();
|
|
1965
1965
|
}
|
|
1966
1966
|
attachVideoStream(stream, pcClient) {
|
|
1967
|
-
if (this.video) {
|
|
1968
|
-
this.video.srcObject = stream;
|
|
1969
|
-
this.videoTrack$.next({ stream, pcClient });
|
|
1970
|
-
}
|
|
1971
|
-
else {
|
|
1967
|
+
if (!this.video) {
|
|
1972
1968
|
Logger.error('Video element is not defined.');
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
// Safari refuses muted-autoplay for MediaStreams that carry unmuted
|
|
1972
|
+
// audio tracks, even when the <video> is muted. Route video tracks to
|
|
1973
|
+
// the video element and audio tracks (if any) to the audio element.
|
|
1974
|
+
this.video.srcObject = new MediaStream(stream.getVideoTracks());
|
|
1975
|
+
const audioTracks = stream.getAudioTracks();
|
|
1976
|
+
if (audioTracks.length && this.audio) {
|
|
1977
|
+
this.audio.srcObject = new MediaStream(audioTracks);
|
|
1978
|
+
this.playAudio(this.audio);
|
|
1973
1979
|
}
|
|
1980
|
+
this.videoTrack$.next({ stream, pcClient });
|
|
1981
|
+
// Safari does not reliably honor the autoplay attribute when srcObject
|
|
1982
|
+
// is bound post-creation for a WebRTC MediaStream; trigger play()
|
|
1983
|
+
// explicitly and let safePlay() fall back if Safari refuses.
|
|
1984
|
+
void this.safePlay(this.video);
|
|
1974
1985
|
}
|
|
1975
1986
|
attachAudioStream(stream) {
|
|
1976
|
-
|
|
1977
|
-
if (this.video?.srcObject === stream) {
|
|
1987
|
+
if (!this.audio) {
|
|
1978
1988
|
return;
|
|
1979
1989
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
this.video?.srcObject !== stream) {
|
|
1984
|
-
this.audio.srcObject = stream;
|
|
1985
|
-
this.playAudio(this.audio);
|
|
1990
|
+
const audioTracks = stream.getAudioTracks();
|
|
1991
|
+
if (!audioTracks.length) {
|
|
1992
|
+
return;
|
|
1986
1993
|
}
|
|
1994
|
+
this.audio.srcObject = new MediaStream(audioTracks);
|
|
1995
|
+
this.playAudio(this.audio);
|
|
1987
1996
|
}
|
|
1988
1997
|
play() {
|
|
1989
1998
|
void this.safePlay(this.video);
|
|
@@ -2001,7 +2010,8 @@ class VideoService extends SubService {
|
|
|
2001
2010
|
typeof error === 'object' &&
|
|
2002
2011
|
'name' in error &&
|
|
2003
2012
|
error?.name !== 'NotAllowedError') {
|
|
2004
|
-
|
|
2013
|
+
Logger.warn('safePlay() rejected with non-autoplay error:', error);
|
|
2014
|
+
return;
|
|
2005
2015
|
}
|
|
2006
2016
|
// Try silent inline autoplay
|
|
2007
2017
|
video.muted = true;
|
|
@@ -2046,6 +2056,11 @@ class VideoService extends SubService {
|
|
|
2046
2056
|
}
|
|
2047
2057
|
try {
|
|
2048
2058
|
const stats = await pcClient.getStats(null);
|
|
2059
|
+
// The service may have been destroyed while pcClient.getStats() was
|
|
2060
|
+
// pending; bail before writing back to this.aggregatedStats.
|
|
2061
|
+
if (!this.video) {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2049
2064
|
return this.generateAggregatedStatsFunction(stats);
|
|
2050
2065
|
}
|
|
2051
2066
|
catch (err) {
|
|
@@ -2125,12 +2140,14 @@ class VideoService extends SubService {
|
|
|
2125
2140
|
if (report.type === 'transport' &&
|
|
2126
2141
|
Object.hasOwn(report, 'selectedCandidatePairId')) {
|
|
2127
2142
|
const selectedPair = stats.get(report.selectedCandidatePairId);
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2143
|
+
if (selectedPair) {
|
|
2144
|
+
const local = stats.get(selectedPair.localCandidateId);
|
|
2145
|
+
const remote = stats.get(selectedPair.remoteCandidateId);
|
|
2146
|
+
newStat.selectedPair = {
|
|
2147
|
+
local: { type: local?.candidateType, url: local?.url },
|
|
2148
|
+
remote: { type: remote?.candidateType, url: remote?.url },
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2134
2151
|
}
|
|
2135
2152
|
// Store mimetype of each codec
|
|
2136
2153
|
if (Object.hasOwn(newStat, 'codecs') &&
|
|
@@ -2170,6 +2187,19 @@ class VideoService extends SubService {
|
|
|
2170
2187
|
return;
|
|
2171
2188
|
}
|
|
2172
2189
|
this.changeStatusMainVideoOnScene(false);
|
|
2190
|
+
// Cancel any gesture listeners armed by safePlay/playAudio in this
|
|
2191
|
+
// session, then arm a fresh controller for the next one. Without this,
|
|
2192
|
+
// listeners survive mid-session disconnect$ → destroy() cycles.
|
|
2193
|
+
this.destroyAbort.abort();
|
|
2194
|
+
this.destroyAbort = new AbortController();
|
|
2195
|
+
// Pause and detach streams before nulling — Safari can otherwise hold
|
|
2196
|
+
// on to the RTCRtpReceiver until GC eventually clears srcObject.
|
|
2197
|
+
this.video.pause();
|
|
2198
|
+
this.video.srcObject = null;
|
|
2199
|
+
if (this.audio) {
|
|
2200
|
+
this.audio.pause();
|
|
2201
|
+
this.audio.srcObject = null;
|
|
2202
|
+
}
|
|
2173
2203
|
this.video = null;
|
|
2174
2204
|
this.audio = null;
|
|
2175
2205
|
this.videoTrack$.next(null);
|
|
@@ -2194,7 +2224,14 @@ class VideoService extends SubService {
|
|
|
2194
2224
|
video.id = STREAMING_VIDEO_ID;
|
|
2195
2225
|
video.playsInline = true;
|
|
2196
2226
|
video.muted = true;
|
|
2197
|
-
|
|
2227
|
+
// Safari occasionally resets `muted` when srcObject is rebound; the
|
|
2228
|
+
// `defaultMuted` reflection makes the muted state sticky and keeps the
|
|
2229
|
+
// muted-autoplay policy intact.
|
|
2230
|
+
video.defaultMuted = true;
|
|
2231
|
+
// No `autoplay` attribute on purpose: attachVideoStream() always calls
|
|
2232
|
+
// safePlay() explicitly, and the attribute would create a second
|
|
2233
|
+
// browser-internal play() Promise that we can't catch — which surfaces
|
|
2234
|
+
// as an unhandled rejection when destroy() pauses the element.
|
|
2198
2235
|
video.preload = 'auto';
|
|
2199
2236
|
video.disablePictureInPicture = true;
|
|
2200
2237
|
this.video = video;
|
|
@@ -2211,13 +2248,36 @@ class VideoService extends SubService {
|
|
|
2211
2248
|
audio.id = STREAMING_AUDIO_ID;
|
|
2212
2249
|
this.audio = audio;
|
|
2213
2250
|
}
|
|
2214
|
-
playAudio(audio) {
|
|
2215
|
-
|
|
2216
|
-
.
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2251
|
+
async playAudio(audio) {
|
|
2252
|
+
try {
|
|
2253
|
+
await audio.play();
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
catch {
|
|
2257
|
+
// Wait for the next real user gesture and retry.
|
|
2258
|
+
const resume = async () => {
|
|
2259
|
+
try {
|
|
2260
|
+
await audio.play();
|
|
2261
|
+
}
|
|
2262
|
+
catch {
|
|
2263
|
+
/* ignore — caller may need another gesture */
|
|
2264
|
+
}
|
|
2265
|
+
finally {
|
|
2266
|
+
window.removeEventListener('pointerdown', resume, true);
|
|
2267
|
+
window.removeEventListener('keydown', resume, true);
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
window.addEventListener('pointerdown', resume, {
|
|
2271
|
+
once: true,
|
|
2272
|
+
capture: true,
|
|
2273
|
+
signal: this.destroyAbort.signal,
|
|
2274
|
+
});
|
|
2275
|
+
window.addEventListener('keydown', resume, {
|
|
2276
|
+
once: true,
|
|
2277
|
+
capture: true,
|
|
2278
|
+
signal: this.destroyAbort.signal,
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2221
2281
|
}
|
|
2222
2282
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: VideoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2223
2283
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: VideoService }); }
|