From 9b4d4b4af8ea1e671ca222b000e48a863d13d1cf Mon Sep 17 00:00:00 2001 From: Nathan Tate Date: Fri, 6 Sep 2019 11:45:00 -0700 Subject: [PATCH] feat(youtube-player): Add a feature for waiting until the API has finished loading --- .../youtube-player/youtube-player-demo.html | 3 +- .../youtube-player/youtube-player-demo.ts | 21 +- src/youtube-player/youtube-player.spec.ts | 380 ++++++++++-------- src/youtube-player/youtube-player.ts | 75 +++- 4 files changed, 270 insertions(+), 209 deletions(-) diff --git a/src/dev-app/youtube-player/youtube-player-demo.html b/src/dev-app/youtube-player/youtube-player-demo.html index 7562ccc62978..5b30c7ae0f4b 100644 --- a/src/dev-app/youtube-player/youtube-player-demo.html +++ b/src/dev-app/youtube-player/youtube-player-demo.html @@ -10,7 +10,6 @@

Basic Example

Unset -

Loading youtube api...

- + diff --git a/src/dev-app/youtube-player/youtube-player-demo.ts b/src/dev-app/youtube-player/youtube-player-demo.ts index 92c8b00a5d5b..a603919c4e02 100644 --- a/src/dev-app/youtube-player/youtube-player-demo.ts +++ b/src/dev-app/youtube-player/youtube-player-demo.ts @@ -6,14 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ - import {ChangeDetectorRef, Component} from '@angular/core'; - -declare global { - interface Window { - YT: typeof YT | undefined; - onYouTubeIframeAPIReady: () => void; - } -} + import {Component} from '@angular/core'; interface Video { id: string; @@ -45,16 +38,4 @@ export class YouTubePlayerDemo { video: Video | undefined = VIDEOS[0]; videos = VIDEOS; apiLoaded = false; - - constructor(private _ref: ChangeDetectorRef) { - if (window.YT) { - this.apiLoaded = true; - return; - } - - window.onYouTubeIframeAPIReady = () => { - this.apiLoaded = true; - this._ref.detectChanges(); - }; - } } diff --git a/src/youtube-player/youtube-player.spec.ts b/src/youtube-player/youtube-player.spec.ts index 34b23bc9267e..e02786502298 100644 --- a/src/youtube-player/youtube-player.spec.ts +++ b/src/youtube-player/youtube-player.spec.ts @@ -6,10 +6,6 @@ import {createFakeYtNamespace} from './fake-youtube-player'; const VIDEO_ID = 'a12345'; -declare global { - interface Window { YT: typeof YT | undefined; } -} - describe('YoutubePlayer', () => { let playerCtorSpy: jasmine.Spy; let playerSpy: jasmine.SpyObj; @@ -32,250 +28,286 @@ describe('YoutubePlayer', () => { TestBed.compileComponents(); })); - beforeEach(() => { - fixture = TestBed.createComponent(TestApp); - testComponent = fixture.debugElement.componentInstance; - fixture.detectChanges(); - }); + describe('API ready', () => { + beforeEach(() => { + fixture = TestBed.createComponent(TestApp); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + }); - afterEach(() => { - window.YT = undefined; - }); + afterEach(() => { + window.YT = undefined; + }); - it('initializes a youtube player', () => { - let containerElement = fixture.nativeElement.querySelector('div'); + it('initializes a youtube player', () => { + let containerElement = fixture.nativeElement.querySelector('div'); - expect(playerCtorSpy).toHaveBeenCalledWith( - containerElement, jasmine.objectContaining({ - videoId: VIDEO_ID, - width: DEFAULT_PLAYER_WIDTH, - height: DEFAULT_PLAYER_HEIGHT, - })); - }); + expect(playerCtorSpy).toHaveBeenCalledWith( + containerElement, jasmine.objectContaining({ + videoId: VIDEO_ID, + width: DEFAULT_PLAYER_WIDTH, + height: DEFAULT_PLAYER_HEIGHT, + })); + }); - it('destroys the iframe when the component is destroyed', () => { - events.onReady({target: playerSpy}); + it('destroys the iframe when the component is destroyed', () => { + events.onReady({target: playerSpy}); - testComponent.visible = false; - fixture.detectChanges(); + testComponent.visible = false; + fixture.detectChanges(); - expect(playerSpy.destroy).toHaveBeenCalled(); - }); + expect(playerSpy.destroy).toHaveBeenCalled(); + }); - it('responds to changes in video id', () => { - let containerElement = fixture.nativeElement.querySelector('div'); + it('responds to changes in video id', () => { + let containerElement = fixture.nativeElement.querySelector('div'); - testComponent.videoId = 'otherId'; - fixture.detectChanges(); + testComponent.videoId = 'otherId'; + fixture.detectChanges(); - expect(playerSpy.cueVideoById).not.toHaveBeenCalled(); + expect(playerSpy.cueVideoById).not.toHaveBeenCalled(); - events.onReady({target: playerSpy}); + events.onReady({target: playerSpy}); - expect(playerSpy.cueVideoById).toHaveBeenCalledWith( - jasmine.objectContaining({videoId: 'otherId'})); + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({videoId: 'otherId'})); - testComponent.videoId = undefined; - fixture.detectChanges(); + testComponent.videoId = undefined; + fixture.detectChanges(); - expect(playerSpy.destroy).toHaveBeenCalled(); + expect(playerSpy.destroy).toHaveBeenCalled(); - testComponent.videoId = 'otherId2'; - fixture.detectChanges(); + testComponent.videoId = 'otherId2'; + fixture.detectChanges(); - expect(playerCtorSpy).toHaveBeenCalledWith( - containerElement, jasmine.objectContaining({videoId: 'otherId2'})); - }); + expect(playerCtorSpy).toHaveBeenCalledWith( + containerElement, jasmine.objectContaining({videoId: 'otherId2'})); + }); - it('responds to changes in size', () => { - testComponent.width = 5; - fixture.detectChanges(); + it('responds to changes in size', () => { + testComponent.width = 5; + fixture.detectChanges(); - expect(playerSpy.setSize).not.toHaveBeenCalled(); + expect(playerSpy.setSize).not.toHaveBeenCalled(); - events.onReady({target: playerSpy}); + events.onReady({target: playerSpy}); - expect(playerSpy.setSize).toHaveBeenCalledWith(5, DEFAULT_PLAYER_HEIGHT); - expect(testComponent.youtubePlayer.width).toBe(5); - expect(testComponent.youtubePlayer.height).toBe(DEFAULT_PLAYER_HEIGHT); + expect(playerSpy.setSize).toHaveBeenCalledWith(5, DEFAULT_PLAYER_HEIGHT); + expect(testComponent.youtubePlayer.width).toBe(5); + expect(testComponent.youtubePlayer.height).toBe(DEFAULT_PLAYER_HEIGHT); - testComponent.height = 6; - fixture.detectChanges(); + testComponent.height = 6; + fixture.detectChanges(); - expect(playerSpy.setSize).toHaveBeenCalledWith(5, 6); - expect(testComponent.youtubePlayer.width).toBe(5); - expect(testComponent.youtubePlayer.height).toBe(6); + expect(playerSpy.setSize).toHaveBeenCalledWith(5, 6); + expect(testComponent.youtubePlayer.width).toBe(5); + expect(testComponent.youtubePlayer.height).toBe(6); - testComponent.videoId = undefined; - fixture.detectChanges(); - testComponent.videoId = VIDEO_ID; - fixture.detectChanges(); + testComponent.videoId = undefined; + fixture.detectChanges(); + testComponent.videoId = VIDEO_ID; + fixture.detectChanges(); - expect(playerCtorSpy).toHaveBeenCalledWith( - jasmine.any(Element), jasmine.objectContaining({width: 5, height: 6})); - expect(testComponent.youtubePlayer.width).toBe(5); - expect(testComponent.youtubePlayer.height).toBe(6); + expect(playerCtorSpy).toHaveBeenCalledWith( + jasmine.any(Element), jasmine.objectContaining({width: 5, height: 6})); + expect(testComponent.youtubePlayer.width).toBe(5); + expect(testComponent.youtubePlayer.height).toBe(6); - events.onReady({target: playerSpy}); - testComponent.width = undefined; - fixture.detectChanges(); + events.onReady({target: playerSpy}); + testComponent.width = undefined; + fixture.detectChanges(); - expect(playerSpy.setSize).toHaveBeenCalledWith(DEFAULT_PLAYER_WIDTH, 6); - expect(testComponent.youtubePlayer.width).toBe(DEFAULT_PLAYER_WIDTH); - expect(testComponent.youtubePlayer.height).toBe(6); + expect(playerSpy.setSize).toHaveBeenCalledWith(DEFAULT_PLAYER_WIDTH, 6); + expect(testComponent.youtubePlayer.width).toBe(DEFAULT_PLAYER_WIDTH); + expect(testComponent.youtubePlayer.height).toBe(6); - testComponent.height = undefined; - fixture.detectChanges(); + testComponent.height = undefined; + fixture.detectChanges(); - expect(playerSpy.setSize).toHaveBeenCalledWith(DEFAULT_PLAYER_WIDTH, DEFAULT_PLAYER_HEIGHT); - expect(testComponent.youtubePlayer.width).toBe(DEFAULT_PLAYER_WIDTH); - expect(testComponent.youtubePlayer.height).toBe(DEFAULT_PLAYER_HEIGHT); - }); + expect(playerSpy.setSize).toHaveBeenCalledWith(DEFAULT_PLAYER_WIDTH, DEFAULT_PLAYER_HEIGHT); + expect(testComponent.youtubePlayer.width).toBe(DEFAULT_PLAYER_WIDTH); + expect(testComponent.youtubePlayer.height).toBe(DEFAULT_PLAYER_HEIGHT); + }); - it('initializes the player with start and end seconds', () => { - testComponent.startSeconds = 5; - testComponent.endSeconds = 6; - fixture.detectChanges(); + it('initializes the player with start and end seconds', () => { + testComponent.startSeconds = 5; + testComponent.endSeconds = 6; + fixture.detectChanges(); - expect(playerSpy.cueVideoById).not.toHaveBeenCalled(); + expect(playerSpy.cueVideoById).not.toHaveBeenCalled(); - playerSpy.getPlayerState.and.returnValue(window.YT!.PlayerState.CUED); - events.onReady({target: playerSpy}); + playerSpy.getPlayerState.and.returnValue(window.YT!.PlayerState.CUED); + events.onReady({target: playerSpy}); - expect(playerSpy.cueVideoById).toHaveBeenCalledWith( - jasmine.objectContaining({startSeconds: 5, endSeconds: 6})); + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 5, endSeconds: 6})); - testComponent.endSeconds = 8; - fixture.detectChanges(); + testComponent.endSeconds = 8; + fixture.detectChanges(); - expect(playerSpy.cueVideoById).toHaveBeenCalledWith( - jasmine.objectContaining({startSeconds: 5, endSeconds: 8})); + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 5, endSeconds: 8})); - testComponent.startSeconds = 7; - fixture.detectChanges(); + testComponent.startSeconds = 7; + fixture.detectChanges(); - expect(playerSpy.cueVideoById).toHaveBeenCalledWith( - jasmine.objectContaining({startSeconds: 7, endSeconds: 8})); + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 7, endSeconds: 8})); - testComponent.startSeconds = 10; - testComponent.endSeconds = 11; - fixture.detectChanges(); + testComponent.startSeconds = 10; + testComponent.endSeconds = 11; + fixture.detectChanges(); - expect(playerSpy.cueVideoById).toHaveBeenCalledWith( - jasmine.objectContaining({startSeconds: 10, endSeconds: 11})); - }); + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 10, endSeconds: 11})); + }); - it('sets the suggested quality', () => { - testComponent.suggestedQuality = 'small'; - fixture.detectChanges(); + it('sets the suggested quality', () => { + testComponent.suggestedQuality = 'small'; + fixture.detectChanges(); - expect(playerSpy.setPlaybackQuality).not.toHaveBeenCalled(); + expect(playerSpy.setPlaybackQuality).not.toHaveBeenCalled(); - events.onReady({target: playerSpy}); + events.onReady({target: playerSpy}); - expect(playerSpy.setPlaybackQuality).toHaveBeenCalledWith('small'); + expect(playerSpy.setPlaybackQuality).toHaveBeenCalledWith('small'); - testComponent.suggestedQuality = 'large'; - fixture.detectChanges(); + testComponent.suggestedQuality = 'large'; + fixture.detectChanges(); - expect(playerSpy.setPlaybackQuality).toHaveBeenCalledWith('large'); + expect(playerSpy.setPlaybackQuality).toHaveBeenCalledWith('large'); - testComponent.videoId = 'other'; - fixture.detectChanges(); + testComponent.videoId = 'other'; + fixture.detectChanges(); - expect(playerSpy.cueVideoById).toHaveBeenCalledWith( - jasmine.objectContaining({suggestedQuality: 'large'})); - }); + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({suggestedQuality: 'large'})); + }); - it('proxies events as output', () => { - events.onReady({target: playerSpy}); - expect(testComponent.onReady).toHaveBeenCalledWith({target: playerSpy}); + it('proxies events as output', () => { + events.onReady({target: playerSpy}); + expect(testComponent.onReady).toHaveBeenCalledWith({target: playerSpy}); - events.onStateChange({target: playerSpy, data: 5}); - expect(testComponent.onStateChange).toHaveBeenCalledWith({target: playerSpy, data: 5}); + events.onStateChange({target: playerSpy, data: 5}); + expect(testComponent.onStateChange).toHaveBeenCalledWith({target: playerSpy, data: 5}); - events.onPlaybackQualityChange({target: playerSpy, data: 'large'}); - expect(testComponent.onPlaybackQualityChange) - .toHaveBeenCalledWith({target: playerSpy, data: 'large'}); + events.onPlaybackQualityChange({target: playerSpy, data: 'large'}); + expect(testComponent.onPlaybackQualityChange) + .toHaveBeenCalledWith({target: playerSpy, data: 'large'}); - events.onPlaybackRateChange({target: playerSpy, data: 2}); - expect(testComponent.onPlaybackRateChange) - .toHaveBeenCalledWith({target: playerSpy, data: 2}); + events.onPlaybackRateChange({target: playerSpy, data: 2}); + expect(testComponent.onPlaybackRateChange) + .toHaveBeenCalledWith({target: playerSpy, data: 2}); - events.onError({target: playerSpy, data: 5}); - expect(testComponent.onError) - .toHaveBeenCalledWith({target: playerSpy, data: 5}); + events.onError({target: playerSpy, data: 5}); + expect(testComponent.onError) + .toHaveBeenCalledWith({target: playerSpy, data: 5}); - events.onApiChange({target: playerSpy}); - expect(testComponent.onApiChange).toHaveBeenCalledWith({target: playerSpy}); - }); + events.onApiChange({target: playerSpy}); + expect(testComponent.onApiChange).toHaveBeenCalledWith({target: playerSpy}); + }); - it('proxies methods to the player', () => { - events.onReady({target: playerSpy}); + it('proxies methods to the player', () => { + events.onReady({target: playerSpy}); - testComponent.youtubePlayer.playVideo(); - expect(playerSpy.playVideo).toHaveBeenCalled(); + testComponent.youtubePlayer.playVideo(); + expect(playerSpy.playVideo).toHaveBeenCalled(); - testComponent.youtubePlayer.pauseVideo(); - expect(playerSpy.pauseVideo).toHaveBeenCalled(); + testComponent.youtubePlayer.pauseVideo(); + expect(playerSpy.pauseVideo).toHaveBeenCalled(); - testComponent.youtubePlayer.stopVideo(); - expect(playerSpy.stopVideo).toHaveBeenCalled(); + testComponent.youtubePlayer.stopVideo(); + expect(playerSpy.stopVideo).toHaveBeenCalled(); - testComponent.youtubePlayer.mute(); - expect(playerSpy.mute).toHaveBeenCalled(); + testComponent.youtubePlayer.mute(); + expect(playerSpy.mute).toHaveBeenCalled(); - testComponent.youtubePlayer.unMute(); - expect(playerSpy.unMute).toHaveBeenCalled(); + testComponent.youtubePlayer.unMute(); + expect(playerSpy.unMute).toHaveBeenCalled(); - testComponent.youtubePlayer.isMuted(); - expect(playerSpy.isMuted).toHaveBeenCalled(); + testComponent.youtubePlayer.isMuted(); + expect(playerSpy.isMuted).toHaveBeenCalled(); - testComponent.youtubePlayer.seekTo(5, true); - expect(playerSpy.seekTo).toHaveBeenCalledWith(5, true); + testComponent.youtubePlayer.seekTo(5, true); + expect(playerSpy.seekTo).toHaveBeenCalledWith(5, true); - testComponent.youtubePlayer.isMuted(); - expect(playerSpy.isMuted).toHaveBeenCalled(); + testComponent.youtubePlayer.isMuted(); + expect(playerSpy.isMuted).toHaveBeenCalled(); - testComponent.youtubePlayer.setVolume(54); - expect(playerSpy.setVolume).toHaveBeenCalledWith(54); + testComponent.youtubePlayer.setVolume(54); + expect(playerSpy.setVolume).toHaveBeenCalledWith(54); - testComponent.youtubePlayer.getVolume(); - expect(playerSpy.getVolume).toHaveBeenCalled(); + testComponent.youtubePlayer.getVolume(); + expect(playerSpy.getVolume).toHaveBeenCalled(); - testComponent.youtubePlayer.setPlaybackRate(1.5); - expect(playerSpy.setPlaybackRate).toHaveBeenCalledWith(1.5); + testComponent.youtubePlayer.setPlaybackRate(1.5); + expect(playerSpy.setPlaybackRate).toHaveBeenCalledWith(1.5); - testComponent.youtubePlayer.getPlaybackRate(); - expect(playerSpy.getPlaybackRate).toHaveBeenCalled(); + testComponent.youtubePlayer.getPlaybackRate(); + expect(playerSpy.getPlaybackRate).toHaveBeenCalled(); - testComponent.youtubePlayer.getAvailablePlaybackRates(); - expect(playerSpy.getAvailablePlaybackRates).toHaveBeenCalled(); + testComponent.youtubePlayer.getAvailablePlaybackRates(); + expect(playerSpy.getAvailablePlaybackRates).toHaveBeenCalled(); - testComponent.youtubePlayer.getVideoLoadedFraction(); - expect(playerSpy.getVideoLoadedFraction).toHaveBeenCalled(); + testComponent.youtubePlayer.getVideoLoadedFraction(); + expect(playerSpy.getVideoLoadedFraction).toHaveBeenCalled(); - testComponent.youtubePlayer.getPlayerState(); - expect(playerSpy.getPlayerState).toHaveBeenCalled(); + testComponent.youtubePlayer.getPlayerState(); + expect(playerSpy.getPlayerState).toHaveBeenCalled(); - testComponent.youtubePlayer.getCurrentTime(); - expect(playerSpy.getCurrentTime).toHaveBeenCalled(); + testComponent.youtubePlayer.getCurrentTime(); + expect(playerSpy.getCurrentTime).toHaveBeenCalled(); - testComponent.youtubePlayer.getPlaybackQuality(); - expect(playerSpy.getPlaybackQuality).toHaveBeenCalled(); + testComponent.youtubePlayer.getPlaybackQuality(); + expect(playerSpy.getPlaybackQuality).toHaveBeenCalled(); - testComponent.youtubePlayer.getAvailableQualityLevels(); - expect(playerSpy.getAvailableQualityLevels).toHaveBeenCalled(); + testComponent.youtubePlayer.getAvailableQualityLevels(); + expect(playerSpy.getAvailableQualityLevels).toHaveBeenCalled(); - testComponent.youtubePlayer.getDuration(); - expect(playerSpy.getDuration).toHaveBeenCalled(); + testComponent.youtubePlayer.getDuration(); + expect(playerSpy.getDuration).toHaveBeenCalled(); - testComponent.youtubePlayer.getVideoUrl(); - expect(playerSpy.getVideoUrl).toHaveBeenCalled(); + testComponent.youtubePlayer.getVideoUrl(); + expect(playerSpy.getVideoUrl).toHaveBeenCalled(); - testComponent.youtubePlayer.getVideoEmbedCode(); - expect(playerSpy.getVideoEmbedCode).toHaveBeenCalled(); + testComponent.youtubePlayer.getVideoEmbedCode(); + expect(playerSpy.getVideoEmbedCode).toHaveBeenCalled(); + }); }); + + describe('API loaded asynchronously', () => { + let api: typeof YT | undefined; + + beforeEach(() => { + api = window.YT; + window.YT = undefined; + + fixture = TestBed.createComponent(TestApp); + testComponent = fixture.debugElement.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + window.YT = undefined; + }); + + it('waits until the api is ready before initializing', () => { + expect(playerCtorSpy).not.toHaveBeenCalled(); + + window.YT = api; + window.onYouTubeIframeAPIReady!(); + + let containerElement = fixture.nativeElement.querySelector('div'); + + expect(playerCtorSpy).toHaveBeenCalledWith( + containerElement, jasmine.objectContaining({ + videoId: VIDEO_ID, + width: DEFAULT_PLAYER_WIDTH, + height: DEFAULT_PLAYER_HEIGHT, + })); + }); + }); + }); /** Test component that contains a YouTubePlayer. */ diff --git a/src/youtube-player/youtube-player.ts b/src/youtube-player/youtube-player.ts index 13bb7cbae452..08ba716290a7 100644 --- a/src/youtube-player/youtube-player.ts +++ b/src/youtube-player/youtube-player.ts @@ -9,6 +9,7 @@ import { OnDestroy, Output, ViewChild, + OnInit, } from '@angular/core'; import { @@ -20,9 +21,11 @@ import { MonoTypeOperatorFunction, merge, OperatorFunction, + Subject, } from 'rxjs'; import { + combineLatest as combineLatestOp, map, scan, withLatestFrom, @@ -33,10 +36,15 @@ import { first, distinctUntilChanged, takeUntil, + take, + skipWhile, } from 'rxjs/operators'; declare global { - interface Window { YT: typeof YT | undefined; } + interface Window { + YT: typeof YT | undefined; + onYouTubeIframeAPIReady: (() => void) | undefined; + } } export const DEFAULT_PLAYER_WIDTH = 640; @@ -63,14 +71,16 @@ type UninitializedPlayer = Pick', }) -export class YouTubePlayer implements AfterViewInit, OnDestroy { +export class YouTubePlayer implements AfterViewInit, OnDestroy, OnInit { /** YouTube Video ID to view */ @Input() - get videoId(): string | undefined { return this._player && this._player.videoId; } + get videoId(): string | undefined { return this._videoId; } set videoId(videoId: string | undefined) { - this._videoId.emit(videoId); + this._videoId = videoId; + this._videoIdObs.emit(videoId); } - private _videoId = new EventEmitter(); + private _videoId: string | undefined; + private _videoIdObs = new EventEmitter(); /** Height of video player */ @Input() @@ -110,6 +120,13 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { } private _suggestedQuality = new EventEmitter(); + /** + * Whether the iframe will attempt to load regardless of the status of the api on the + * page. Set this to true if you don't want the `onYouTubeIframeAPIReady` field to be + * set on the global window. + */ + @Input() showBeforeIframeApiLoads: boolean | undefined; + /** Outputs are direct proxies from the player itself. */ @Output() ready = new EventEmitter(); @Output() stateChange = new EventEmitter(); @@ -125,13 +142,26 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { private _player: Player | undefined; - constructor(private _ngZone: NgZone) { + constructor(private _ngZone: NgZone) {} + + ngOnInit() { + let iframeApiAvailableObs: Observable = observableOf(true); if (!window.YT) { - throw new Error('Namespace YT not found, cannot construct embedded youtube player. ' + - 'Please install the YouTube Player API Reference for iframe Embeds: ' + - 'https://developers.google.com/youtube/iframe_api_reference'); + if (this.showBeforeIframeApiLoads) { + throw new Error('Namespace YT not found, cannot construct embedded youtube player. ' + + 'Please install the YouTube Player API Reference for iframe Embeds: ' + + 'https://developers.google.com/youtube/iframe_api_reference'); + } + + const iframeApiAvailableSubject = new Subject(); + window.onYouTubeIframeAPIReady = () => { + this._ngZone.run(() => iframeApiAvailableSubject.next(true)); + }; + iframeApiAvailableObs = iframeApiAvailableSubject.pipe(take(1), startWith(false)); } + // Add initial values to all of the inputs. + const videoIdObs = this._videoIdObs.pipe(startWith(this._videoId)); const widthObs = this._widthObs.pipe(startWith(this._width)); const heightObs = this._heightObs.pipe(startWith(this._height)); @@ -143,7 +173,8 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { const playerObs = createPlayerObservable( this._youtubeContainer, - this._videoId, + videoIdObs, + iframeApiAvailableObs, widthObs, heightObs, this.createEventsBoundInZone(), @@ -158,7 +189,7 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { bindCueVideoCall( playerObs, - this._videoId, + videoIdObs, startSecondsObs, endSecondsObs, suggestedQualityObs, @@ -192,6 +223,7 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { return; } this._player.destroy(); + window.onYouTubeIframeAPIReady = undefined; this._destroyed.emit(); } @@ -309,7 +341,11 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { } /** See https://developers.google.com/youtube/iframe_api_reference#getPlayerState */ - getPlayerState(): YT.PlayerState { + getPlayerState(): YT.PlayerState | undefined { + if (!window.YT) { + return undefined; + } + if (!this._player) { return YT.PlayerState.UNSTARTED; } @@ -432,6 +468,7 @@ function fromPlayerOnReady(player: UninitializedPlayer): Observable { function createPlayerObservable( youtubeContainer: Observable, videoIdObs: Observable, + iframeApiAvailableObs: Observable, widthObs: Observable, heightObs: Observable, events: YT.Events, @@ -445,7 +482,18 @@ function createPlayerObservable( ); return combineLatest(youtubeContainer, playerOptions) - .pipe(scan(syncPlayerState, undefined), distinctUntilChanged()); + .pipe( + skipUntilRememberLatest(iframeApiAvailableObs), + scan(syncPlayerState, undefined), + distinctUntilChanged()); +} + +/** Skips the given observable until the other observable emits true, then emit the latest. */ +function skipUntilRememberLatest(notifier: Observable): MonoTypeOperatorFunction { + return pipe( + combineLatestOp(notifier), + skipWhile(([_, doneSkipping]) => !doneSkipping), + map(([value]) => value)); } /** Destroy the player if there are no options, or create the player if there are options. */ @@ -462,6 +510,7 @@ function syncPlayerState( if (player) { return player; } + const newPlayer: UninitializedPlayer = new YT.Player(container, videoOptions); // Bind videoId for future use. newPlayer.videoId = videoOptions.videoId;