diff --git a/src/youtube-player/youtube-player.spec.ts b/src/youtube-player/youtube-player.spec.ts index e25a6873a66f..34b23bc9267e 100644 --- a/src/youtube-player/youtube-player.spec.ts +++ b/src/youtube-player/youtube-player.spec.ts @@ -132,6 +132,61 @@ describe('YoutubePlayer', () => { 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(); + + expect(playerSpy.cueVideoById).not.toHaveBeenCalled(); + + playerSpy.getPlayerState.and.returnValue(window.YT!.PlayerState.CUED); + events.onReady({target: playerSpy}); + + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 5, endSeconds: 6})); + + testComponent.endSeconds = 8; + fixture.detectChanges(); + + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 5, endSeconds: 8})); + + testComponent.startSeconds = 7; + fixture.detectChanges(); + + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 7, endSeconds: 8})); + + testComponent.startSeconds = 10; + testComponent.endSeconds = 11; + fixture.detectChanges(); + + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({startSeconds: 10, endSeconds: 11})); + }); + + it('sets the suggested quality', () => { + testComponent.suggestedQuality = 'small'; + fixture.detectChanges(); + + expect(playerSpy.setPlaybackQuality).not.toHaveBeenCalled(); + + events.onReady({target: playerSpy}); + + expect(playerSpy.setPlaybackQuality).toHaveBeenCalledWith('small'); + + testComponent.suggestedQuality = 'large'; + fixture.detectChanges(); + + expect(playerSpy.setPlaybackQuality).toHaveBeenCalledWith('large'); + + testComponent.videoId = 'other'; + fixture.detectChanges(); + + expect(playerSpy.cueVideoById).toHaveBeenCalledWith( + jasmine.objectContaining({suggestedQuality: 'large'})); + }); + it('proxies events as output', () => { events.onReady({target: playerSpy}); expect(testComponent.onReady).toHaveBeenCalledWith({target: playerSpy}); @@ -228,6 +283,7 @@ describe('YoutubePlayer', () => { selector: 'test-app', template: ` (); + /** The moment when the player is supposed to start playing */ + @Input() set startSeconds(startSeconds: number | undefined) { + this._startSeconds.emit(startSeconds); + } + private _startSeconds = new EventEmitter(); + + /** The moment when the player is supposed to stop playing */ + @Input() set endSeconds(endSeconds: number | undefined) { + this._endSeconds.emit(endSeconds); + } + private _endSeconds = new EventEmitter(); + + /** The suggested quality of the player */ + @Input() set suggestedQuality(suggestedQuality: YT.SuggestedVideoQuality | undefined) { + this._suggestedQuality.emit(suggestedQuality); + } + private _suggestedQuality = new EventEmitter(); + /** Outputs are direct proxies from the player itself. */ @Output() ready = new EventEmitter(); @Output() stateChange = new EventEmitter(); @@ -117,6 +135,10 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { const widthObs = this._widthObs.pipe(startWith(this._width)); const heightObs = this._heightObs.pipe(startWith(this._height)); + const startSecondsObs = this._startSeconds.pipe(startWith(undefined)); + const endSecondsObs = this._endSeconds.pipe(startWith(undefined)); + const suggestedQualityObs = this._suggestedQuality.pipe(startWith(undefined)); + /** An observable of the currently loaded player. */ const playerObs = createPlayerObservable( @@ -132,7 +154,15 @@ export class YouTubePlayer implements AfterViewInit, OnDestroy { bindSizeToPlayer(playerObs, widthObs, heightObs); - bindCueVideoCall(playerObs, this._videoId, this._destroyed); + bindSuggestedQualityToPlayer(playerObs, suggestedQualityObs); + + bindCueVideoCall( + playerObs, + this._videoId, + startSecondsObs, + endSecondsObs, + suggestedQualityObs, + this._destroyed); // After all of the subscriptions are set up, connect the observable. (playerObs as ConnectableObservable).connect(); @@ -345,6 +375,19 @@ function bindSizeToPlayer( .subscribe(([player, width, height]) => player && player.setSize(width, height)); } +/** Listens to changes from the suggested quality and sets it on the given player. */ +function bindSuggestedQualityToPlayer( + playerObs: Observable, + suggestedQualityObs: Observable +) { + return combineLatest( + playerObs, + suggestedQualityObs + ).subscribe( + ([player, suggestedQuality]) => + player && suggestedQuality && player.setPlaybackQuality(suggestedQuality)); +} + /** * Returns an observable that emits the loaded player once it's ready. Certain properties/methods * won't be available until the iframe finishes loading. @@ -433,8 +476,18 @@ function syncPlayerState( function bindCueVideoCall( playerObs: Observable, videoIdObs: Observable, + startSecondsObs: Observable, + endSecondsObs: Observable, + suggestedQualityObs: Observable, destroyed: Observable, ) { + const cueOptionsObs = combineLatest(startSecondsObs, endSecondsObs) + .pipe(map(([startSeconds, endSeconds]) => ({startSeconds, endSeconds}))); + + // Only respond to changes in cue options if the player is not running. + const filteredCueOptions = cueOptionsObs + .pipe(filterOnOther(playerObs, player => !!player && !hasPlayerStarted(player))); + // If the video id changed, there's no reason to run 'cue' unless the player // was initialized with a different video id. const changedVideoId = videoIdObs @@ -442,21 +495,33 @@ function bindCueVideoCall( // If the player changed, there's no reason to run 'cue' unless there are cue options. const changedPlayer = playerObs.pipe( - filterOnOther(videoIdObs, (videoId, player) => !!player && videoId != player.videoId)); - - merge(changedPlayer, changedVideoId) - .pipe( - withLatestFrom(combineLatest(playerObs, videoIdObs)), - map(([_, values]) => values), - takeUntil(destroyed), - ) - .subscribe(([player, videoId]) => { - if (!videoId || !player) { - return; - } - player.videoId = videoId; - player.cueVideoById({videoId}); + filterOnOther( + combineLatest(videoIdObs, cueOptionsObs), + ([videoId, cueOptions], player) => + !!player && + (videoId != player.videoId || !!cueOptions.startSeconds || !!cueOptions.endSeconds))); + + merge(changedPlayer, changedVideoId, filteredCueOptions) + .pipe( + withLatestFrom(combineLatest(playerObs, videoIdObs, cueOptionsObs, suggestedQualityObs)), + map(([_, values]) => values), + takeUntil(destroyed), + ) + .subscribe(([player, videoId, cueOptions, suggestedQuality]) => { + if (!videoId || !player) { + return; + } + player.videoId = videoId; + player.cueVideoById({ + videoId, + suggestedQuality, + ...cueOptions, }); + }); +} + +function hasPlayerStarted(player: YT.Player): boolean { + return [YT.PlayerState.UNSTARTED, YT.PlayerState.CUED].indexOf(player.getPlayerState()) === -1; } /** Combines the two observables temporarily for the filter function. */