feat: Background audio via Media Session and Minimal App Icon; Bump v4.0.4
|
|
@ -10,6 +10,7 @@ A modern, fast, and fully-featured YouTube-like video streaming platform. Built
|
||||||
- **Watch History & Suggestions**: Keep track of what you've watched seamlessly! Fully integrated library history tracking.
|
- **Watch History & Suggestions**: Keep track of what you've watched seamlessly! Fully integrated library history tracking.
|
||||||
- **Subscriptions Management**: Keep up to date with seamless subscription updates for YouTube channels.
|
- **Subscriptions Management**: Keep up to date with seamless subscription updates for YouTube channels.
|
||||||
- **Optimized for Safari**: Stutter-free playback algorithms and high-tolerance Hls.js configurations tailored for macOS users.
|
- **Optimized for Safari**: Stutter-free playback algorithms and high-tolerance Hls.js configurations tailored for macOS users.
|
||||||
|
- **Background Audio**: Allows videos to continue playing audio when the browser tab is hidden or device locked (perfect for music).
|
||||||
- **Progressive Web App**: Fully installable PWA out of the box with offline fallbacks and custom vector iconography.
|
- **Progressive Web App**: Fully installable PWA out of the box with offline fallbacks and custom vector iconography.
|
||||||
- **Region Selection**: Tailor your content to specific regions (e.g., Vietnam).
|
- **Region Selection**: Tailor your content to specific regions (e.g., Vietnam).
|
||||||
- **Responsive Design**: Beautiful, mobile-friendly interface with light and dark theme support.
|
- **Responsive Design**: Beautiful, mobile-friendly interface with light and dark theme support.
|
||||||
|
|
@ -37,7 +38,7 @@ version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
kv-tube-app:
|
kv-tube-app:
|
||||||
image: git.khoavo.myds.me/vndangkhoa/kv-tube-app:v4.0.3
|
image: git.khoavo.myds.me/vndangkhoa/kv-tube-app:v4.0.4
|
||||||
container_name: kv-tube-app
|
container_name: kv-tube-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
kv-tube-app:
|
kv-tube-app:
|
||||||
image: git.khoavo.myds.me/vndangkhoa/kv-tube-app:v4.0.3
|
image: git.khoavo.myds.me/vndangkhoa/kv-tube-app:v4.0.4
|
||||||
container_name: kv-tube-app
|
container_name: kv-tube-app
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
|
|
@ -97,13 +97,18 @@ export default function VideoPlayer({ videoId, title }: VideoPlayerProps) {
|
||||||
const audio = audioRef.current;
|
const audio = audioRef.current;
|
||||||
if (!video || !audio || !hasSeparateAudio) return;
|
if (!video || !audio || !hasSeparateAudio) return;
|
||||||
|
|
||||||
// Relax the tolerance to 0.4s to prevent choppy audio resetting on Safari
|
const isHidden = document.visibilityState === 'hidden';
|
||||||
|
|
||||||
if (Math.abs(video.currentTime - audio.currentTime) > 0.4) {
|
if (Math.abs(video.currentTime - audio.currentTime) > 0.4) {
|
||||||
audio.currentTime = video.currentTime;
|
if (!isHidden || !video.paused) {
|
||||||
|
audio.currentTime = video.currentTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.paused && !audio.paused) {
|
if (video.paused && !audio.paused) {
|
||||||
audio.pause();
|
if (!isHidden) {
|
||||||
|
audio.pause();
|
||||||
|
}
|
||||||
} else if (!video.paused && audio.paused) {
|
} else if (!video.paused && audio.paused) {
|
||||||
audio.play().catch(() => { });
|
audio.play().catch(() => { });
|
||||||
}
|
}
|
||||||
|
|
@ -189,10 +194,21 @@ export default function VideoPlayer({ videoId, title }: VideoPlayerProps) {
|
||||||
video.addEventListener(event, handler);
|
video.addEventListener(event, handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible' && video && audioRef.current && hasSeparateAudio) {
|
||||||
|
if (video.paused && !audioRef.current.paused) {
|
||||||
|
video.currentTime = audioRef.current.currentTime;
|
||||||
|
video.play().catch(() => { });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Object.entries(handlers).forEach(([event, handler]) => {
|
Object.entries(handlers).forEach(([event, handler]) => {
|
||||||
video.removeEventListener(event, handler);
|
video.removeEventListener(event, handler);
|
||||||
});
|
});
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
};
|
};
|
||||||
}, [hasSeparateAudio]);
|
}, [hasSeparateAudio]);
|
||||||
|
|
||||||
|
|
@ -210,6 +226,24 @@ export default function VideoPlayer({ videoId, title }: VideoPlayerProps) {
|
||||||
const handleWaiting = () => setIsBuffering(true);
|
const handleWaiting = () => setIsBuffering(true);
|
||||||
const handleLoadStart = () => setIsLoading(true);
|
const handleLoadStart = () => setIsLoading(true);
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: title || 'KV-Tube Video',
|
||||||
|
artist: 'KV-Tube',
|
||||||
|
artwork: [
|
||||||
|
{ src: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`, sizes: '480x360', type: 'image/jpeg' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('play', () => {
|
||||||
|
video.play().catch(() => { });
|
||||||
|
if (needsSeparateAudio && audioRef.current) audioRef.current.play().catch(() => { });
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => {
|
||||||
|
video.pause();
|
||||||
|
if (needsSeparateAudio && audioRef.current) audioRef.current.pause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
video.addEventListener('canplay', handleCanPlay);
|
video.addEventListener('canplay', handleCanPlay);
|
||||||
video.addEventListener('playing', handlePlaying);
|
video.addEventListener('playing', handlePlaying);
|
||||||
video.addEventListener('waiting', handleWaiting);
|
video.addEventListener('waiting', handleWaiting);
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 14 KiB |