canti primo tag
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import { Injectable, signal, effect, inject } from '@angular/core';
|
||||
import { CantiService } from './canti.service';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { MediaSessionService } from './media-session.service';
|
||||
import { MyCantiService } from './my-canti.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class YoutubePlayerService {
|
||||
private cantiService = inject(CantiService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private mediaSessionService = inject(MediaSessionService);
|
||||
private myCantiService = inject(MyCantiService);
|
||||
|
||||
public currentCantoId = signal<string | null>(null);
|
||||
public isPlaying = signal<boolean>(false);
|
||||
public videoProgress = signal<number>(0);
|
||||
public videoDuration = signal<number>(0);
|
||||
public isPlayerReady = signal<boolean>(false);
|
||||
|
||||
private player: any = null;
|
||||
private progressInterval: any = null;
|
||||
private onEndedCallback: (() => void) | null = null;
|
||||
private onErrorCallback: (() => void) | null = null;
|
||||
private silentAudio: HTMLAudioElement | null = null;
|
||||
private lastNextCallback: (() => void) | null = null;
|
||||
private lastPrevCallback: (() => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
this.loadYoutubeAPI();
|
||||
this.setupMediaSession();
|
||||
this.initSilentAudio();
|
||||
this.setupBackgroundPersistence();
|
||||
}
|
||||
|
||||
private initSilentAudio() {
|
||||
// 1-second silent MP3 base64
|
||||
const silentSrc = 'data:audio/mpeg;base64,SUQzBAAAAAABAFRYWFhYAAAADAAAY29udGVudAB0eXBlAGF1ZGlvL21wZWdB///+8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
this.silentAudio = new Audio(silentSrc);
|
||||
this.silentAudio.loop = true;
|
||||
}
|
||||
|
||||
private setupBackgroundPersistence() {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible' && this.isPlaying() && this.player) {
|
||||
// Ensure player is actually playing when returning to the app
|
||||
try {
|
||||
if (this.player.getPlayerState() !== (window as any).YT.PlayerState.PLAYING) {
|
||||
this.player.playVideo();
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setMediaSessionCallbacks(next?: () => void, prev?: () => void) {
|
||||
if (next) this.lastNextCallback = next;
|
||||
if (prev) this.lastPrevCallback = prev;
|
||||
|
||||
this.mediaSessionService.initActionHandlers({
|
||||
play: () => this.resume(),
|
||||
pause: () => this.pause(),
|
||||
seekto: (time) => this.seekTo(time),
|
||||
next: this.lastNextCallback || undefined,
|
||||
prev: this.lastPrevCallback || undefined
|
||||
});
|
||||
}
|
||||
|
||||
private setupMediaSession() {
|
||||
this.setMediaSessionCallbacks();
|
||||
}
|
||||
|
||||
private loadYoutubeAPI() {
|
||||
if ((window as any).YT) return;
|
||||
const tag = document.createElement('script');
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
const firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode?.insertBefore(tag, firstScriptTag);
|
||||
|
||||
(window as any).onYouTubeIframeAPIReady = () => {
|
||||
this.isPlayerReady.set(true);
|
||||
};
|
||||
}
|
||||
|
||||
public initPlayer(cantoId: string, startTime: number = 0, onEnded?: () => void, onError?: () => void) {
|
||||
if (this.currentCantoId() === cantoId && this.player) {
|
||||
this.onEndedCallback = onEnded || null;
|
||||
this.onErrorCallback = onError || null;
|
||||
|
||||
const currentTime = this.player.getCurrentTime();
|
||||
// Only seek if the difference is significant (more than 2 seconds)
|
||||
if (startTime > 0 && Math.abs(currentTime - startTime) > 2) {
|
||||
this.player.seekTo(startTime, true);
|
||||
}
|
||||
this.resume();
|
||||
return;
|
||||
}
|
||||
|
||||
this.onEndedCallback = onEnded || null;
|
||||
this.onErrorCallback = onError || null;
|
||||
|
||||
let canto = this.cantiService.getCantoById(cantoId);
|
||||
if (!canto) {
|
||||
canto = this.myCantiService.myCanti().find(c => c.id === cantoId);
|
||||
}
|
||||
if (!canto) return;
|
||||
|
||||
const videoId = this.cantiService.getYoutubeId(canto.link_youtube);
|
||||
if (!videoId) {
|
||||
if (onError) onError();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(window as any).YT || !(window as any).YT.Player) {
|
||||
setTimeout(() => this.initPlayer(cantoId, startTime, onEnded, onError), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse existing player if possible
|
||||
if (this.player && this.player.loadVideoById) {
|
||||
this.currentCantoId.set(cantoId);
|
||||
this.mediaSessionService.updateMetadata(cantoId);
|
||||
this.player.loadVideoById({
|
||||
videoId: videoId,
|
||||
startSeconds: startTime
|
||||
});
|
||||
if (this.silentAudio) this.silentAudio.play().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroyPlayer();
|
||||
this.currentCantoId.set(cantoId);
|
||||
this.mediaSessionService.updateMetadata(cantoId);
|
||||
|
||||
this.player = new (window as any).YT.Player('global-yt-player-container', {
|
||||
height: '1',
|
||||
width: '1',
|
||||
videoId: videoId,
|
||||
playerVars: {
|
||||
autoplay: 1,
|
||||
playsinline: 1,
|
||||
mute: 0,
|
||||
modestbranding: 1,
|
||||
rel: 0,
|
||||
controls: 0,
|
||||
disablekb: 1,
|
||||
start: startTime
|
||||
},
|
||||
events: {
|
||||
onReady: (event: any) => {
|
||||
event.target.unMute();
|
||||
event.target.setVolume(100);
|
||||
this.videoDuration.set(event.target.getDuration());
|
||||
this.startPolling();
|
||||
if (startTime > 0) {
|
||||
event.target.seekTo(startTime, true);
|
||||
}
|
||||
event.target.playVideo();
|
||||
this.isPlaying.set(true);
|
||||
if (this.silentAudio) this.silentAudio.play().catch(() => {});
|
||||
},
|
||||
onStateChange: (event: any) => {
|
||||
const state = event.data;
|
||||
if (state === (window as any).YT.PlayerState.PLAYING) {
|
||||
this.isPlaying.set(true);
|
||||
this.videoDuration.set(this.player.getDuration());
|
||||
this.mediaSessionService.setPlaybackState('playing');
|
||||
// Re-apply handlers to prevent YT from overriding them
|
||||
this.setMediaSessionCallbacks();
|
||||
} else if (state === (window as any).YT.PlayerState.PAUSED) {
|
||||
this.isPlaying.set(false);
|
||||
this.mediaSessionService.setPlaybackState('paused');
|
||||
// Re-apply handlers even when paused
|
||||
this.setMediaSessionCallbacks();
|
||||
} else if (state === (window as any).YT.PlayerState.ENDED) {
|
||||
this.isPlaying.set(false);
|
||||
this.mediaSessionService.setPlaybackState('none');
|
||||
if (this.onEndedCallback) this.onEndedCallback();
|
||||
}
|
||||
},
|
||||
onError: (event: any) => {
|
||||
console.error('Global YT Player Error:', event.data);
|
||||
if (this.onErrorCallback) this.onErrorCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public pause() {
|
||||
if (this.player) {
|
||||
this.player.pauseVideo();
|
||||
this.isPlaying.set(false);
|
||||
if (this.silentAudio) this.silentAudio.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public resume() {
|
||||
if (this.player) {
|
||||
this.player.playVideo();
|
||||
this.isPlaying.set(true);
|
||||
if (this.silentAudio) this.silentAudio.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
public togglePlayPause() {
|
||||
if (this.isPlaying()) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.resume();
|
||||
}
|
||||
}
|
||||
|
||||
public seekTo(seconds: number) {
|
||||
if (this.player) {
|
||||
this.player.seekTo(seconds, true);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.destroyPlayer();
|
||||
this.currentCantoId.set(null);
|
||||
this.isPlaying.set(false);
|
||||
}
|
||||
|
||||
private startPolling() {
|
||||
if (this.progressInterval) clearInterval(this.progressInterval);
|
||||
this.progressInterval = setInterval(() => {
|
||||
if (this.player && this.player.getCurrentTime) {
|
||||
this.videoProgress.set(this.player.getCurrentTime());
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private destroyPlayer() {
|
||||
if (this.progressInterval) {
|
||||
clearInterval(this.progressInterval);
|
||||
this.progressInterval = null;
|
||||
}
|
||||
if (this.player) {
|
||||
try {
|
||||
this.player.destroy();
|
||||
} catch (e) {}
|
||||
this.player = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user