(null)
+ const muteStateRef = useRef(muteMedia)
useEffect(() => {
if (autoLoadMedia) {
@@ -47,24 +51,48 @@ export default function YoutubeEmbeddedPlayer({
initPlayer()
}
+ let checkMutedInterval: NodeJS.Timeout | null = null
function initPlayer() {
try {
if (!videoId || !containerRef.current || !window.YT.Player) return
+
+ let currentMuteState = muteStateRef.current
playerRef.current = new window.YT.Player(containerRef.current, {
videoId: videoId,
playerVars: {
- mute: 1
+ mute: currentMuteState ? 1 : 0
},
events: {
onStateChange: (event: any) => {
if (event.data === window.YT.PlayerState.PLAYING) {
mediaManager.play(playerRef.current)
- } else if (event.data === window.YT.PlayerState.PAUSED) {
+ } else if (
+ event.data === window.YT.PlayerState.PAUSED ||
+ event.data === window.YT.PlayerState.ENDED
+ ) {
mediaManager.pause(playerRef.current)
}
},
onReady: () => {
setInitSuccess(true)
+ checkMutedInterval = setInterval(() => {
+ if (playerRef.current) {
+ const mute = playerRef.current.isMuted()
+ if (mute !== currentMuteState) {
+ currentMuteState = mute
+
+ if (mute !== muteStateRef.current) {
+ updateMuteMedia(currentMuteState)
+ }
+ } else if (muteStateRef.current !== mute) {
+ if (muteStateRef.current) {
+ playerRef.current.mute()
+ } else {
+ playerRef.current.unMute()
+ }
+ }
+ }
+ }, 200)
},
onError: () => setError(true)
}
@@ -80,9 +108,46 @@ export default function YoutubeEmbeddedPlayer({
if (playerRef.current) {
playerRef.current.destroy()
}
+ if (checkMutedInterval) {
+ clearInterval(checkMutedInterval)
+ checkMutedInterval = null
+ }
}
}, [videoId, display, mustLoad])
+ useEffect(() => {
+ muteStateRef.current = muteMedia
+ }, [muteMedia])
+
+ useEffect(() => {
+ const wrapper = wrapperRef.current
+
+ if (!wrapper || !initSuccess) return
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ const player = playerRef.current
+ if (!player) return
+
+ if (
+ !entry.isIntersecting &&
+ [window.YT.PlayerState.PLAYING, window.YT.PlayerState.BUFFERING].includes(
+ player.getPlayerState()
+ )
+ ) {
+ mediaManager.pause(player)
+ }
+ },
+ { threshold: 1 }
+ )
+
+ observer.observe(wrapper)
+
+ return () => {
+ observer.unobserve(wrapper)
+ }
+ }, [videoId, display, mustLoad, initSuccess])
+
if (error) {
return
}
@@ -104,8 +169,10 @@ export default function YoutubeEmbeddedPlayer({
if (!videoId && !initSuccess) {
return
}
+
return (
void
+
+ muteMedia: boolean
+ updateMuteMedia: (mute: boolean) => void
}
const UserPreferencesContext = createContext(undefined)
@@ -21,6 +24,7 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
const [notificationListStyle, setNotificationListStyle] = useState(
storage.getNotificationListStyle()
)
+ const [muteMedia, setMuteMedia] = useState(true)
const updateNotificationListStyle = (style: TNotificationStyle) => {
setNotificationListStyle(style)
@@ -31,7 +35,9 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
{children}
diff --git a/src/services/media-manager.service.ts b/src/services/media-manager.service.ts
index 7e0b5ec..714574e 100644
--- a/src/services/media-manager.service.ts
+++ b/src/services/media-manager.service.ts
@@ -1,15 +1,23 @@
import { YouTubePlayer } from '@/types/youtube'
+import { atom, getDefaultStore } from 'jotai'
+
+export const hasBackgroundAudioAtom = atom(false)
+const store = getDefaultStore()
type Media = HTMLMediaElement | YouTubePlayer
-class MediaManagerService {
+class MediaManagerService extends EventTarget {
static instance: MediaManagerService
private currentMedia: Media | null = null
constructor() {
+ super()
+ }
+
+ public static getInstance(): MediaManagerService {
if (!MediaManagerService.instance) {
- MediaManagerService.instance = this
+ MediaManagerService.instance = new MediaManagerService()
}
return MediaManagerService.instance
}
@@ -24,7 +32,7 @@ class MediaManagerService {
if (this.currentMedia === media) {
this.currentMedia = null
}
- pause(media)
+ _pause(media)
}
autoPlay(media: Media) {
@@ -34,6 +42,13 @@ class MediaManagerService {
) {
return
}
+ if (
+ store.get(hasBackgroundAudioAtom) &&
+ this.currentMedia &&
+ isMediaPlaying(this.currentMedia)
+ ) {
+ return
+ }
this.play(media)
}
@@ -45,21 +60,31 @@ class MediaManagerService {
;(document.pictureInPictureElement as HTMLMediaElement).pause()
}
if (this.currentMedia && this.currentMedia !== media) {
- pause(this.currentMedia)
+ _pause(this.currentMedia)
}
this.currentMedia = media
if (isMediaPlaying(media)) {
return
}
- play(this.currentMedia).catch((error) => {
+ _play(this.currentMedia).catch((error) => {
console.error('Error playing media:', error)
this.currentMedia = null
})
}
+
+ playAudioBackground(src: string, time: number = 0) {
+ this.dispatchEvent(new CustomEvent('playAudioBackground', { detail: { src, time } }))
+ store.set(hasBackgroundAudioAtom, true)
+ }
+
+ stopAudioBackground() {
+ this.dispatchEvent(new Event('stopAudioBackground'))
+ store.set(hasBackgroundAudioAtom, false)
+ }
}
-const instance = new MediaManagerService()
+const instance = MediaManagerService.getInstance()
export default instance
function isYouTubePlayer(media: Media): media is YouTubePlayer {
@@ -83,14 +108,14 @@ function isPipElement(media: Media) {
return (media as any).webkitPresentationMode === 'picture-in-picture'
}
-function pause(media: Media) {
+function _pause(media: Media) {
if (isYouTubePlayer(media)) {
return media.pauseVideo()
}
return media.pause()
}
-async function play(media: Media) {
+async function _play(media: Media) {
if (isYouTubePlayer(media)) {
return media.playVideo()
}
diff --git a/src/types/youtube.d.ts b/src/types/youtube.d.ts
index 520d5d0..50e5357 100644
--- a/src/types/youtube.d.ts
+++ b/src/types/youtube.d.ts
@@ -41,6 +41,9 @@ export interface YouTubePlayer {
getCurrentTime(): number
getDuration(): number
getPlayerState(): number
+ isMuted(): boolean
+ mute(): void
+ unMute(): void
}
export {}