@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
- // do nothing the video has the same media stream as the audio track we have here (they are linked)
1977
- if (this.video?.srcObject === stream) {
1987
+ if (!this.audio) {
1978
1988
  return;
1979
1989
  }
1980
- // the video element has some other media stream not associated with this audio track
1981
- else if (this.audio &&
1982
- this.video?.srcObject &&
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
- throw error;
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
- const local = stats.get(selectedPair.localCandidateId);
2129
- const remote = stats.get(selectedPair.remoteCandidateId);
2130
- newStat.selectedPair = {
2131
- local: { type: local?.candidateType, url: local?.url },
2132
- remote: { type: remote?.candidateType, url: remote?.url },
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
- video.autoplay = true;
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
- from(audio.play())
2216
- .pipe(catchError(() => fromEvent(document, 'click')), take(1))
2217
- .subscribe({
2218
- next: () => audio.play(),
2219
- error: () => ({}),
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 }); }