9

我有一个带有 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 上可用

4

0 回答 0