我有一个带有 Youtube IFrame API 的 Angular 应用程序。当在安卓设备上使用网站/pwa,并且正在播放 Youtube 视频时,应用程序会自动在 android 通知托盘中创建通知,让您查看当前正在播放的视频。
我创建了自己的播放列表控制器,因此我不使用 queueVideoById,因为我不喜欢默认的 youtube-iframe 播放列表控制器的工作方式(之后您无法删除排队的视频)。
从我的 AppComponent 我正在加载 YouTube IFrame API
app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
constructor(private youtubeHelper: YoutubeHelper) {
this.youtubeHelper.loadApi().then((isLoaded) => {
this.player.playSong(song.playerInfo.id);
});
}
@ViewChild('player') player: YoutubePlayerComponent;
}
app.component.html
<body>
<youtube-player #player [domId]="'player'" ... (previousPressed)="onPreviousPressed()" (nextPressed)="onNextPressed()"></youtube-player>
</body>
此帮助程序用于加载 IFrame API 脚本。LoadApi 插入一个脚本标签,其中 src 设置为 youtube-iframe-url:
import { Injectable } from '@angular/core';
import { Promise } from 'q';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class YoutubeHelper {
static scriptTag: HTMLScriptElement = null;
public loadApi() {
return Promise<boolean>((resolve, reject) => {
if (typeof window !== 'undefined') {
if (YoutubeHelper.scriptTag === null) {
window['onYouTubeIframeAPIReady'] = () => {
this.apiReady.next(true);
resolve(true);
};
YoutubeHelper.scriptTag = window.document.createElement('script');
YoutubeHelper.scriptTag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = window.document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(YoutubeHelper.scriptTag, firstScriptTag);
} else {
resolve(true);
}
} else {
resolve(false);
}
});
}
public unloadApi() {
return Promise((resolve) => {
if (typeof window !== 'undefined') {
if (YoutubeHelper.scriptTag !== null) {
YoutubeHelper.scriptTag.parentNode.removeChild(YoutubeHelper.scriptTag);
YoutubeHelper.scriptTag = null;
//console.log('Removed YouTube iframe api');
this.apiReady.next(false);
}
}
});
}
public apiReady = new BehaviorSubject<boolean>(
(typeof window === 'undefined')
? false
: window['YT'] !== undefined
);
}
当 iframe api 脚本插入 dom 时,YoutubePlayerComponent 通过创建 YT.Player 的实例来处理这个问题(这发生在this.youtubeHelper.apiReady
订阅中):
/// <reference path="../../../../node_modules/@types/youtube/index.d.ts" />
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { YoutubeHelper } from '../../helpers/youtube-api.helper';
import { SongProgress } from '../../entities/song-progress';
import { Song } from '../../entities/song';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'youtube-player',
templateUrl: './youtube-player.component.html',
styleUrls: ['./youtube-player.component.scss']
})
export class YoutubePlayerComponent implements OnInit {
constructor(private youtubeHelper: YoutubeHelper) {
}
@Input() domId: string;
// Properties omitted for breverity
@Output() previousPressed: EventEmitter<any> = new EventEmitter();
@Output() nextPressed: EventEmitter<any> = new EventEmitter();
private player: YT.Player;
private isPlayerReady = new BehaviorSubject<boolean>(false);
ngOnInit() {
}
ngAfterViewInit() {
this.youtubeHelper.apiReady.subscribe((ready) => {
if (ready && !this.player) {
this.player = new YT.Player(this.domId, {
width: this.width,
height: this.height,
playerVars: {
autoplay: this._autoplay
},
events: {
onReady: (event) => {
this.isPlayerReady.next(true);
},
onStateChange: (state: { data: YT.PlayerState }) => {
}
}
});
// Attempt to give the player methods my very own implementation
this.player.previousVideo = () => {
this.previousPressed.emit();
};
this.player.nextVideo = () => {
this.nextPressed.emit();
};
console.log('player', this.player);
}
});
}
static mediaInit = false;
public playSong(youtubeId: string) {
if (this.isPlayerReady.value) {
this.player.loadVideoById(youtubeId);
} else {
console.log('Player not yet ready');
this.isPlayerReady.subscribe((value) => {
if (value === true) {
this.player.loadVideoById(youtubeId);
}
});
}
// Here is the code that's supposed to interact with the media session in the iframe.
if (!YoutubePlayerComponent.mediaInit) {
YoutubePlayerComponent.mediaInit = true;
let frame = <HTMLIFrameElement>document.getElementById(this.domId);
frame.contentWindow.navigator.mediaSession.setActionHandler('previoustrack', () => {
this.previousPressed.emit();
});
frame.contentWindow.navigator.mediaSession.setActionHandler('nexttrack', () => {
this.nextPressed.emit();
});
}
}
public play() { this.player.playVideo(); }
// Pause, stop methods omitted for breverity
}
之后isPlayerReady
触发,允许播放视频:
调用时YoutubePlayerComponent.playSong
,它要么等待 YoutubePlayer 准备就绪(isPlayerReady),要么立即调用this.player.loadVideoById
播放请求的视频。
说了这么多,问题是:
默认情况下,当视频在我的网站/pwa 中播放时,它会显示媒体通知,如第一张图片所示。我希望能够导航到我自己的播放列表中的上一个/下一个曲目。所以我想拦截这些事件:
根据此链接,可以在播放视频后修改窗口的 mediaSession,将自定义处理程序附加到 android 托盘中的上一个/下一个按钮。但由于视频(当然还有 mediaSession)是从 iframe 播放的,所以无法修改元数据:
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Never Gonna Give You Up',
artist: 'Rick Astley',
album: 'Whenever You Need Somebody'
});
会成为:
document.getElementById('player').contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
title: 'Never Gonna Give You Up',
artist: 'Rick Astley',
album: 'Whenever You Need Somebody'
});
只是检查一下,在播放视频时 mainWindow 的 mediaSession 是空的:
navigator.mediaSession.metadata
> null
并且实际的 mediaSession 应该在:
document.getElementById("player").contentWindow.navigator.mediaSession
> Blocked a frame with origin "https://example.com" from accessing a cross-origin frame
那么有什么方法可以在使用 Youtube iframe API 时将上一个/下一个按钮添加到通知中,并处理这些事件?
如有必要,整个项目在 GitHub 上可用