@@ -14,7 +14,6 @@ import {
14
14
ChangeDetectionStrategy ,
15
15
Component ,
16
16
ElementRef ,
17
- EventEmitter ,
18
17
Input ,
19
18
NgZone ,
20
19
OnDestroy ,
@@ -40,6 +39,7 @@ import {
40
39
Subject ,
41
40
of ,
42
41
BehaviorSubject ,
42
+ fromEventPattern ,
43
43
} from 'rxjs' ;
44
44
45
45
import {
@@ -55,6 +55,7 @@ import {
55
55
take ,
56
56
takeUntil ,
57
57
withLatestFrom ,
58
+ switchMap ,
58
59
} from 'rxjs/operators' ;
59
60
60
61
declare global {
@@ -102,6 +103,15 @@ interface PendingPlayerState {
102
103
template : '<div #youtubeContainer></div>' ,
103
104
} )
104
105
export class YouTubePlayer implements AfterViewInit , OnDestroy , OnInit {
106
+ /** Whether we're currently rendering inside a browser. */
107
+ private _isBrowser : boolean ;
108
+ private _youtubeContainer = new Subject < HTMLElement > ( ) ;
109
+ private _destroyed = new Subject < void > ( ) ;
110
+ private _player : Player | undefined ;
111
+ private _existingApiReadyCallback : ( ( ) => void ) | undefined ;
112
+ private _pendingPlayerState : PendingPlayerState | undefined ;
113
+ private _playerChanges = new BehaviorSubject < Player | undefined > ( undefined ) ;
114
+
105
115
/** YouTube Video ID to view */
106
116
@Input ( )
107
117
get videoId ( ) : string | undefined { return this . _videoId . value ; }
@@ -155,25 +165,28 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
155
165
@Input ( ) showBeforeIframeApiLoads : boolean | undefined ;
156
166
157
167
/** Outputs are direct proxies from the player itself. */
158
- @Output ( ) ready = new EventEmitter < YT . PlayerEvent > ( ) ;
159
- @Output ( ) stateChange = new EventEmitter < YT . OnStateChangeEvent > ( ) ;
160
- @Output ( ) error = new EventEmitter < YT . OnErrorEvent > ( ) ;
161
- @Output ( ) apiChange = new EventEmitter < YT . PlayerEvent > ( ) ;
162
- @Output ( ) playbackQualityChange = new EventEmitter < YT . OnPlaybackQualityChangeEvent > ( ) ;
163
- @Output ( ) playbackRateChange = new EventEmitter < YT . OnPlaybackRateChangeEvent > ( ) ;
168
+ @Output ( ) ready : Observable < YT . PlayerEvent > =
169
+ this . _getLazyEmitter < YT . PlayerEvent > ( 'onReady' ) ;
170
+
171
+ @Output ( ) stateChange : Observable < YT . OnStateChangeEvent > =
172
+ this . _getLazyEmitter < YT . OnStateChangeEvent > ( 'onStateChange' ) ;
173
+
174
+ @Output ( ) error : Observable < YT . OnErrorEvent > =
175
+ this . _getLazyEmitter < YT . OnErrorEvent > ( 'onError' ) ;
176
+
177
+ @Output ( ) apiChange : Observable < YT . PlayerEvent > =
178
+ this . _getLazyEmitter < YT . PlayerEvent > ( 'onApiChange' ) ;
179
+
180
+ @Output ( ) playbackQualityChange : Observable < YT . OnPlaybackQualityChangeEvent > =
181
+ this . _getLazyEmitter < YT . OnPlaybackQualityChangeEvent > ( 'onPlaybackQualityChange' ) ;
182
+
183
+ @Output ( ) playbackRateChange : Observable < YT . OnPlaybackRateChangeEvent > =
184
+ this . _getLazyEmitter < YT . OnPlaybackRateChangeEvent > ( 'onPlaybackRateChange' ) ;
164
185
165
186
/** The element that will be replaced by the iframe. */
166
187
@ViewChild ( 'youtubeContainer' )
167
188
youtubeContainer : ElementRef < HTMLElement > ;
168
189
169
- /** Whether we're currently rendering inside a browser. */
170
- private _isBrowser : boolean ;
171
- private _youtubeContainer = new Subject < HTMLElement > ( ) ;
172
- private _destroyed = new Subject < void > ( ) ;
173
- private _player : Player | undefined ;
174
- private _existingApiReadyCallback : ( ( ) => void ) | undefined ;
175
- private _pendingPlayerState : PendingPlayerState | undefined ;
176
-
177
190
constructor (
178
191
private _ngZone : NgZone ,
179
192
/**
@@ -221,7 +234,6 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
221
234
iframeApiAvailableObs ,
222
235
this . _width ,
223
236
this . _height ,
224
- this . createEventsBoundInZone ( ) ,
225
237
this . _ngZone
226
238
) . pipe ( waitUntilReady ( player => {
227
239
// Destroy the player if loading was aborted so that we don't end up leaking memory.
@@ -233,6 +245,7 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
233
245
// Set up side effects to bind inputs to the player.
234
246
playerObs . subscribe ( player => {
235
247
this . _player = player ;
248
+ this . _playerChanges . next ( player ) ;
236
249
237
250
if ( player && this . _pendingPlayerState ) {
238
251
this . _initializePlayer ( player , this . _pendingPlayerState ) ;
@@ -257,25 +270,12 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
257
270
( playerObs as ConnectableObservable < Player > ) . connect ( ) ;
258
271
}
259
272
273
+ /**
274
+ * @deprecated No longer being used. To be removed.
275
+ * @breaking -change 11.0.0
276
+ */
260
277
createEventsBoundInZone ( ) : YT . Events {
261
- const output : YT . Events = { } ;
262
- const events = new Map < keyof YT . Events , EventEmitter < any > > ( [
263
- [ 'onReady' , this . ready ] ,
264
- [ 'onStateChange' , this . stateChange ] ,
265
- [ 'onPlaybackQualityChange' , this . playbackQualityChange ] ,
266
- [ 'onPlaybackRateChange' , this . playbackRateChange ] ,
267
- [ 'onError' , this . error ] ,
268
- [ 'onApiChange' , this . apiChange ]
269
- ] ) ;
270
-
271
- events . forEach ( ( emitter , name ) => {
272
- // Since these events all trigger change detection, only bind them if something is subscribed.
273
- if ( emitter . observers . length ) {
274
- output [ name ] = this . _runInZone ( event => emitter . emit ( event ) ) ;
275
- }
276
- } ) ;
277
-
278
- return output ;
278
+ return { } ;
279
279
}
280
280
281
281
ngAfterViewInit ( ) {
@@ -288,6 +288,7 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
288
288
window . onYouTubeIframeAPIReady = this . _existingApiReadyCallback ;
289
289
}
290
290
291
+ this . _playerChanges . complete ( ) ;
291
292
this . _videoId . complete ( ) ;
292
293
this . _height . complete ( ) ;
293
294
this . _width . complete ( ) ;
@@ -299,13 +300,6 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
299
300
this . _destroyed . complete ( ) ;
300
301
}
301
302
302
- private _runInZone < T extends ( ...args : any [ ] ) => void > ( callback : T ) :
303
- ( ...args : Parameters < T > ) => void {
304
- return ( ...args : Parameters < T > ) => this . _ngZone . run ( ( ) => callback ( ...args ) ) ;
305
- }
306
-
307
- /** Proxied methods. */
308
-
309
303
/** See https://developers.google.com/youtube/iframe_api_reference#playVideo */
310
304
playVideo ( ) {
311
305
if ( this . _player ) {
@@ -518,6 +512,37 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit {
518
512
player . seekTo ( seek . seconds , seek . allowSeekAhead ) ;
519
513
}
520
514
}
515
+
516
+ /** Gets an observable that adds an event listener to the player when a user subscribes to it. */
517
+ private _getLazyEmitter < T extends YT . PlayerEvent > ( name : keyof YT . Events ) : Observable < T > {
518
+ // Start with the stream of players. This way the events will be transferred
519
+ // over to the new player if it gets swapped out under-the-hood.
520
+ return this . _playerChanges . pipe (
521
+ // Switch to the bound event. `switchMap` ensures that the old event is removed when the
522
+ // player is changed. If there's no player, return an observable that never emits.
523
+ switchMap ( player => {
524
+ return player ? fromEventPattern < T > ( ( listener : ( event : T ) => void ) => {
525
+ player . addEventListener ( name , listener ) ;
526
+ } , ( listener : ( event : T ) => void ) => {
527
+ // The API seems to throw when we try to unbind from a destroyed player and it doesn't
528
+ // expose whether the player has been destroyed so we have to wrap it in a try/catch to
529
+ // prevent the entire stream from erroring out.
530
+ try {
531
+ player . removeEventListener ( name , listener ) ;
532
+ } catch { }
533
+ } ) : observableOf < T > ( ) ;
534
+ } ) ,
535
+ // By default we run all the API interactions outside the zone
536
+ // so we have to bring the events back in manually when they emit.
537
+ ( source : Observable < T > ) => new Observable < T > ( observer => source . subscribe ( {
538
+ next : value => this . _ngZone . run ( ( ) => observer . next ( value ) ) ,
539
+ error : error => observer . error ( error ) ,
540
+ complete : ( ) => observer . complete ( )
541
+ } ) ) ,
542
+ // Ensures that everything is cleared out on destroy.
543
+ takeUntil ( this . _destroyed )
544
+ ) ;
545
+ }
521
546
}
522
547
523
548
/** Listens to changes to the given width and height and sets it on the player. */
@@ -593,15 +618,14 @@ function createPlayerObservable(
593
618
iframeApiAvailableObs : Observable < boolean > ,
594
619
widthObs : Observable < number > ,
595
620
heightObs : Observable < number > ,
596
- events : YT . Events ,
597
621
ngZone : NgZone
598
622
) : Observable < UninitializedPlayer | undefined > {
599
623
600
624
const playerOptions =
601
625
videoIdObs
602
626
. pipe (
603
627
withLatestFrom ( combineLatest ( [ widthObs , heightObs ] ) ) ,
604
- map ( ( [ videoId , [ width , height ] ] ) => videoId ? ( { videoId, width, height, events } ) : undefined ) ,
628
+ map ( ( [ videoId , [ width , height ] ] ) => videoId ? ( { videoId, width, height} ) : undefined ) ,
605
629
) ;
606
630
607
631
return combineLatest ( [ youtubeContainer , playerOptions , of ( ngZone ) ] )
0 commit comments