@ -493,8 +493,6 @@ const formatMediaImg = (path: string | undefined, size: "w500" | "original" = "w
// Detect Android TV / Smart TV user agent or previous user preference
const detectTvEnvironment = ( ) : boolean = > {
if ( typeof window === "undefined" ) return false ;
const saved = localStorage . getItem ( "has_tv_mode" ) ;
if ( saved !== null ) return saved === "true" ;
const ua = ( navigator . userAgent || "" ) . toLowerCase ( ) ;
const isTvUa =
ua . includes ( "android tv" ) ||
@ -521,6 +519,17 @@ const detectTvEnvironment = (): boolean => {
const isAndroid = ua . includes ( "android" ) ;
const noTouch = ! ( "ontouchstart" in window ) && navigator . maxTouchPoints === 0 ;
if ( isAndroid && noTouch ) return true ;
// On pure desktop (macOS / Windows / Desktop Linux), default to desktop PC mode
const isDesktop =
( ua . includes ( "windows nt" ) || ua . includes ( "macintosh" ) || ( ua . includes ( "linux" ) && ! ua . includes ( "android" ) ) ) &&
! isTvUa ;
if ( isDesktop ) {
return localStorage . getItem ( "has_tv_mode_manual" ) === "true" ;
}
const saved = localStorage . getItem ( "has_tv_mode" ) ;
if ( saved !== null ) return saved === "true" ;
return false ;
} ;
@ -540,10 +549,10 @@ export default function Cinema({
const lastFocusedCardRef = useRef < HTMLElement | null > ( null ) ;
useEffect ( ( ) = > {
if ( isTvEnvironment && ! isTvMode ) {
setIsTvMode ( true ) ;
if ( isTvEnvironment !== isTvMode ) {
setIsTvMode ( isTvEnvironment ) ;
}
} , [ isTvEnvironment , isTvMode ]) ;
} , [ isTvEnvironment ]) ;
const [ tvToast , setTvToast ] = useState < { text : string ; icon? : string } | null > ( null ) ;
const tvToastTimerRef = useRef < any > ( null ) ;
@ -563,6 +572,7 @@ export default function Cinema({
setIsTvMode ( ( prev ) = > {
const next = ! prev ;
localStorage . setItem ( "has_tv_mode" , String ( next ) ) ;
localStorage . setItem ( "has_tv_mode_manual" , String ( next ) ) ;
showTvToast ( next ? "Т В -Режим включен (10-Foot UI)" : "ПК-Режим включен" , "📺" , 2500 ) ;
playTvBeep ( next ? 750 : 400 , 0.05 ) ;
return next ;
@ -654,55 +664,83 @@ export default function Cinema({
const [ isPlayingState , setIsPlayingState ] = useState ( true ) ;
// Dedicated multi-protocol command dispatcher to balancer iframes
const sendIframePlaybackCommand = useCallback ( ( command : "play" | "pause" | "toggle" ) = > {
// Robust multi-level recursive window broadcast to reach nested balancer iframes
const broadcastToWindowTree = useCallback ( ( payload : any ) = > {
const iframe = document . querySelector < HTMLIFrameElement > ( ".has-cinema-iframe-elem" ) ;
if ( ! iframe || ! iframe . contentWindow ) return ;
const win = iframe . contentWindow ;
// Never focus iframe directly — focus must strictly remain on our app controls for TV remote
const post = ( payload : any ) = > {
const send = ( target : Window ) = > {
try {
win . postMessage ( payload , "*" ) ;
target . postMessage ( payload , "*" ) ;
} catch { }
try {
win . postMessage ( JSON . stringify ( payload ) , "*" ) ;
if ( typeof payload === "object" ) {
target . postMessage ( JSON . stringify ( payload ) , "*" ) ;
}
} catch { }
} ;
const rootWin = iframe . contentWindow ;
send ( rootWin ) ;
// Broadcast down into subframes (VidLink, Alloha, Playerjs, AutoEmbed nested wrappers)
try {
if ( rootWin . frames && rootWin . frames . length > 0 ) {
for ( let i = 0 ; i < rootWin . frames . length ; i ++ ) {
try {
send ( rootWin . frames [ i ] ) ;
if ( rootWin . frames [ i ] . frames && rootWin . frames [ i ] . frames . length > 0 ) {
for ( let j = 0 ; j < rootWin . frames [ i ] . frames . length ; j ++ ) {
try {
send ( rootWin . frames [ i ] . frames [ j ] ) ;
} catch { }
}
}
} catch { }
}
}
} catch { }
} , [ ] ) ;
// Dedicated multi-protocol command dispatcher to balancer iframes
const sendIframePlaybackCommand = useCallback ( ( command : "play" | "pause" | "toggle" ) = > {
const isPlay = command === "play" ;
const isToggle = command === "toggle" ;
// Отправляем команды многократно для надежности (разные плееры реагируют в разное время)
for ( let i = 0 ; i < 3 ; i ++ ) {
setTimeout ( ( ) = > {
// 1. Kodik Player API (Supports both object and stringified commands)
post ( { key : "kodik_player_api" , value : { method : isToggle ? "toggle" : ( isPlay ? "play" : "pause" ) } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : isToggle ? "toggle" : ( isPlay ? "play" : "pause" ) } } ) ;
if ( ! isToggle ) {
post ( { key : "kodik_player_api" , value : { method : isPlay ? "play" : "pause" } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : isPlay ? "play" : "pause" } } ) ;
}
// 2. Playerjs API (Used by Alloha, Kinoplay, Collaps, VideoCDN, Ashdi, HDRezka)
post ( { api : isToggle ? "toggle" : ( isPlay ? "play" : "pause" ) } ) ;
broadcastToWindowTree ( { api : isToggle ? "toggle" : ( isPlay ? "play" : "pause" ) } ) ;
if ( ! isToggle ) {
post ( { api : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { api : isPlay ? "play" : "pause" } ) ;
}
// 3. Alloha API
post ( { action : isPlay ? "play" : "pause" } ) ;
post ( { method : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { action : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { method : isPlay ? "play" : "pause" } ) ;
// 4. VidLink Pro / Video.js API
post ( { type : isPlay ? "PLAYER_PLAY" : "PLAYER_PAUSE" } ) ;
post ( { type : isPlay ? "play" : "pause" } ) ;
post ( { action : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { type : isPlay ? "PLAYER_PLAY" : "PLAYER_PAUSE" } ) ;
broadcastToWindowTree ( { type : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { action : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { event : isPlay ? "play" : "pause" } ) ;
// 5. YouTube / VidSrc / AutoEmbed HTML5 API
post ( { event : "command" , func : isPlay ? "playVideo" : "pauseVideo" , args : "" } ) ;
broadcastToWindowTree ( { event : "command" , func : isPlay ? "playVideo" : "pauseVideo" , args : "" } ) ;
// 6. Generic HTML5 postMessage
post ( { method : isPlay ? "play" : "pause" } ) ;
post ( { command : isPlay ? "play" : "pause" } ) ;
} , [ ] ) ;
broadcastToWindowTree ( { method : isPlay ? "play" : "pause" } ) ;
broadcastToWindowTree ( { command : isPlay ? "play" : "pause" } ) ;
} , i * 100 ) ;
}
} , [ broadcastToWindowTree ] ) ;
const setPlayerVolumeMax = useCallback ( ( showToast : boolean = false ) = > {
// 1. Native HTML5 Video Element
@ -713,58 +751,60 @@ export default function Cinema({
} catch { }
}
// 2. Iframe Players (Kodik, Alloha, Playerjs, VidLink, YouTube/AutoEmbed, etc.)
const iframe = document . querySelector < HTMLIFrameElement > ( ".has-cinema-iframe-elem" ) ;
if ( iframe && iframe . contentWindow ) {
const win = iframe . contentWindow ;
const post = ( payload : any ) = > {
try { win . postMessage ( payload , "*" ) ; } catch { }
try { win . postMessage ( JSON . stringify ( payload ) , "*" ) ; } catch { }
} ;
// 2. Kodik API
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : "volume" , volume : 1 } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : "volume" , volume : 100 } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : "unmute" } } ) ;
// 1. Kodik API
post ( { key : "kodik_player_api" , value : { method : "volume" , volume : 1 } } ) ;
post ( { key : "kodik_player_api" , value : { method : "volume" , volume : 100 } } ) ;
post ( { key : "kodik_player_api" , value : { method : "unmute" } } ) ;
// 3. Playerjs API (Alloha, Kinoplay, Collaps, VideoCDN, Ashdi, HDRezka)
broadcastToWindowTree ( { api : "volume" , set : 100 } ) ;
broadcastToWindowTree ( { api : "volume" , set : 1 } ) ;
broadcastToWindowTree ( { api : "volume" , volume : 100 } ) ;
broadcastToWindowTree ( { api : "volume" , volume : 1 } ) ;
broadcastToWindowTree ( { api : "unmute" } ) ;
// 2. Playerjs API (Alloha, Kinoplay, Collaps, VideoCDN, Ashdi, HDRezka)
post ( { api : "volume" , volume : 1 } ) ;
post ( { api : "volume" , volume : 100 } ) ;
post ( { api : "volume" , value : 100 } ) ;
post ( { api : "unmute" } ) ;
// 4. Alloha API
broadcastToWindowTree ( { action : "volume" , volume : 1 } ) ;
broadcastToWindowTree ( { action : "volume" , value : 100 } ) ;
broadcastToWindowTree ( { action : "volume" , set : 100 } ) ;
broadcastToWindowTree ( { action : "unmute" } ) ;
broadcastToWindowTree ( { method : "volume" , value : 1 } ) ;
broadcastToWindowTree ( { method : "setVolume" , value : 100 } ) ;
broadcastToWindowTree ( { method : "setVolume" , volume : 100 } ) ;
broadcastToWindowTree ( { method : "unmute" } ) ;
broadcastToWindowTree ( { method : "unMute" } ) ;
// 3. Alloha API
post ( { action : "volume" , volume : 1 } ) ;
post ( { action : "volume" , value : 100 } ) ;
post ( { action : "unmute" } ) ;
post ( { method : "volume" , value : 1 } ) ;
post ( { method : "setVolume" , value : 100 } ) ;
post ( { method : "unmute" } ) ;
// 5. JW Player / VidLink Pro / Video.js API
broadcastToWindowTree ( { method : "setMute" , value : false } ) ;
broadcastToWindowTree ( { method : "setMute" , mute : false } ) ;
broadcastToWindowTree ( { method : "setVolume" , value : 100 } ) ;
broadcastToWindowTree ( { method : "setVolume" , volume : 100 } ) ;
broadcastToWindowTree ( { action : "setMute" , value : false } ) ;
broadcastToWindowTree ( { action : "setVolume" , value : 100 } ) ;
broadcastToWindowTree ( { command : "setMute" , value : false } ) ;
broadcastToWindowTree ( { type : "SET_VOLUME" , volume : 100 } ) ;
broadcastToWindowTree ( { type : "SET_VOLUME" , data : { volume : 100 } } ) ;
broadcastToWindowTree ( { type : "UNMUTE" } ) ;
broadcastToWindowTree ( { type : "unmute" } ) ;
broadcastToWindowTree ( { action : "volume" , value : 1 } ) ;
broadcastToWindowTree ( { action : "unmute" } ) ;
// 4. VidLink Pro / Video.js API
post ( { type : "VOLUME" , volume : 1 } ) ;
post ( { type : "SET_VOLUME" , data : { volume : 1 } } ) ;
post ( { type : "SET_VOLUME" , data : { volume : 100 } } ) ;
post ( { type : "unmute" } ) ;
post ( { action : "volume" , value : 1 } ) ;
post ( { action : "unmute" } ) ;
// 6. YouTube / VidSrc / AutoEmbed HTML5 API
broadcastToWindowTree ( { event : "command" , func : "setVolume" , args : [ 100 ] } ) ;
broadcastToWindowTree ( { event : "command" , func : "unMute" , args : "" } ) ;
// 5. YouTube / VidSrc / AutoEmbed HTML5 API
post ( { event : "command" , func : "setVolume" , args : [ 100 ] } ) ;
post ( { event : "command" , func : "unMute" , args : "" } ) ;
// 6. Generic HTML5 postMessage
post ( { method : "setVolume" , volume : 100 } ) ;
post ( { method : "setVolume" , value : 100 } ) ;
post ( { method : "unmute" } ) ;
post ( { command : "setVolume" , volume : 100 } ) ;
post ( { command : "unmute" } ) ;
}
// 7. Generic HTML5 postMessage
broadcastToWindowTree ( { method : "setVolume" , volume : 100 } ) ;
broadcastToWindowTree ( { method : "setVolume" , value : 100 } ) ;
broadcastToWindowTree ( { method : "unmute" } ) ;
broadcastToWindowTree ( { method : "unMute" } ) ;
broadcastToWindowTree ( { command : "setVolume" , volume : 100 } ) ;
broadcastToWindowTree ( { command : "unmute" } ) ;
if ( showToast ) {
showTvToast ( "Громкость плеера: 100%" , "🔊" , 1500 ) ;
}
} , [ showTvToast] ) ;
} , [ broadcastToWindowTree, showTvToast] ) ;
const triggerPlayerPlay = useCallback ( ( ) = > {
if ( videoRef . current ) {
@ -775,8 +815,17 @@ export default function Cinema({
videoRef . current . play ( ) . catch ( ( ) = > { } ) ;
} else {
sendIframePlaybackCommand ( "play" ) ;
const iframe = document . querySelector < HTMLIFrameElement > ( ".has-cinema-iframe-elem" ) ;
if ( iframe ) {
try {
iframe . focus ( ) ;
} catch { }
}
}
// Установка громкости 100% каждый раз при воспроизведении
setPlayerVolumeMax ( false ) ;
setTimeout ( ( ) = > setPlayerVolumeMax ( false ) , 400 ) ;
setTimeout ( ( ) = > setPlayerVolumeMax ( false ) , 1200 ) ;
setIsPlayingState ( true ) ;
showTvToast ( "Воспроизведение" , "▶" , 1200 ) ;
} , [ sendIframePlaybackCommand , setPlayerVolumeMax , showTvToast ] ) ;
@ -804,8 +853,9 @@ export default function Cinema({
} else {
triggerPlayerPlay ( ) ;
}
sendIframePlaybackCommand ( "toggle" ) ;
}
} , [ isPlayingState , triggerPlayerPlay , triggerPlayerPause ]) ;
} , [ isPlayingState , triggerPlayerPlay , triggerPlayerPause , sendIframePlaybackCommand ]) ;
const triggerPlayerSeek = useCallback ( ( secondsDelta : number ) = > {
if ( videoRef . current ) {
@ -815,53 +865,59 @@ export default function Cinema({
) ;
currentPlaybackRef . current . currentTime = videoRef . current . currentTime ;
} else {
const iframe = document . querySelector < HTMLIFrameElement > ( ".has-cinema-iframe-elem" ) ;
if ( iframe && iframe . contentWindow ) {
const win = iframe . contentWindow ;
const post = ( payload : any ) = > {
try { win . postMessage ( payload , "*" ) ; } catch { }
try { win . postMessage ( JSON . stringify ( payload ) , "*" ) ; } catch { }
} ;
const curTime = currentPlaybackRef . current . currentTime || 0 ;
const targetTime = Math . max ( 0 , curTime + secondsDelta ) ;
currentPlaybackRef . current . currentTime = targetTime ;
// Отправляем команды перемотки несколько раз для надежности
for ( let i = 0 ; i < 3 ; i ++ ) {
setTimeout ( ( ) = > {
// 1. Kodik API (Seek to absolute seconds & relative seek delta)
post ( { key : "kodik_player_api" , value : { method : "seek" , time : targetTime } } ) ;
post ( { key : "kodik_player_api" , value : { method : "seek_delta" , time : secondsDelta } } ) ;
post ( { key : "kodik_player_api" , value : { method : secondsDelta > 0 ? "forward" : "rewind" , time : Math.abs ( secondsDelta ) } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : "seek" , time : targetTime } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : "seek_delta" , time : secondsDelta } } ) ;
broadcastToWindowTree ( { key : "kodik_player_api" , value : { method : secondsDelta > 0 ? "forward" : "rewind" , time : Math.abs ( secondsDelta ) } } ) ;
// 2. Playerjs API (Used by Alloha, Kinoplay, Collaps, VideoCDN, Ashdi, HDRezka)
post ( { api : "seek" , time : targetTime } ) ;
post ( { api : "seek" , delta : secondsDelta } ) ;
post ( { api : "seek_delta" , time : secondsDelta } ) ;
post ( { api : "seek" , time : secondsDelta > 0 ? ` + ${ secondsDelta } ` : ` ${ secondsDelta } ` } ) ;
broadcastToWindowTree ( { api : "seek" , set : targetTime } ) ;
broadcastToWindowTree ( { api : "seek" , set : secondsDelta > 0 ? ` + ${ secondsDelta } ` : ` ${ secondsDelta } ` } ) ;
broadcastToWindowTree ( { api : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { api : "seek" , delta : secondsDelta } ) ;
broadcastToWindowTree ( { api : "seek_delta" , time : secondsDelta } ) ;
// 3. Alloha API
post ( { action : "seek" , time : targetTime } ) ;
post ( { action : "seek" , delta : secondsDelta } ) ;
post ( { method : "seek" , time : targetTime } ) ;
post ( { method : "seekTo" , value : targetTime } ) ;
broadcastToWindowTree ( { action : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { action : "seek" , delta : secondsDelta } ) ;
broadcastToWindowTree ( { method : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { method : "seekTo" , value : targetTime } ) ;
// 4. VidLink Pro / Video.js API
post ( { type : "SEEK" , data : { time : targetTime } } ) ;
post ( { type : "PLAYER_SEEK" , time : targetTime } ) ;
post ( { type : "seek" , time : targetTime } ) ;
post ( { action : "seek" , time : targetTime } ) ;
post ( { action : "seek" , delta : secondsDelta } ) ;
broadcastToWindowTree ( { type : "SEEK" , data : { time : targetTime } } ) ;
broadcastToWindowTree ( { type : "PLAYER_SEEK" , time : targetTime } ) ;
broadcastToWindowTree ( { type : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { action : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { action : "seek" , delta : secondsDelta } ) ;
// 5. YouTube / VidSrc / AutoEmbed HTML5 API
post ( { event : "command" , func : "seekTo" , args : [ targetTime , true ] } ) ;
post ( { event : "command" , func : "seek" , args : [ targetTime ] } ) ;
post ( { action : "seekTo" , value : targetTime } ) ;
broadcastToWindowTree ( { event : "command" , func : "seekTo" , args : [ targetTime , true ] } ) ;
broadcastToWindowTree ( { event : "command" , func : "seek" , args : [ targetTime ] } ) ;
broadcastToWindowTree ( { action : "seekTo" , value : targetTime } ) ;
// 6. Generic HTML5 postMessage
post ( { method : "seek" , time : targetTime } ) ;
post ( { command : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { method : "seek" , time : targetTime } ) ;
broadcastToWindowTree ( { command : "seek" , time : targetTime } ) ;
} , i * 100 ) ;
}
}
showTvToast ( secondsDelta > 0 ? ` + ${ secondsDelta } сек ` : ` ${ secondsDelta } сек ` , secondsDelta > 0 ? "⏩" : "⏪" , 1000 ) ;
} , [ broadcastToWindowTree , showTvToast ] ) ;
// Give TV remote control directly to iframe if a user needs to interact with in-player controls
const triggerIframeDirectFocus = useCallback ( ( ) = > {
const iframe = document . querySelector < HTMLIFrameElement > ( ".has-cinema-iframe-elem" ) ;
if ( iframe ) {
iframe . focus ( ) ;
showTvToast ( "Управление передано плееру. Нажмите [Back] на пульте для возврата в меню HAS" , "🎮" , 3000 ) ;
}
} , [ showTvToast ] ) ;
// Universal helper to convert a TMDB item into CinemaMediaItem
@ -1085,17 +1141,22 @@ export default function Cinema({
}
}
// Also query Russian media info for Kinopoisk ID / rating
// Also query Russian media info for Kinopoisk ID / rating & pre-resolve stream
if ( isTauri && item . tmdb_id ) {
invoke < RussianMediaInfo > ( "get_russian_media_info" , {
tmdbId : item.tmdb_id ,
isTv : item.media_type === "tv" ,
title : item.title || item . original_title ,
} ) . then ( ( info ) = > {
if ( info ? . kp_id ) {
if ( info ) {
if ( info . kp_id ) setCurrentKpId ( Number ( info . kp_id ) ) ;
if ( info . imdb_id ) setCurrentImdbId ( info . imdb_id ) ;
if ( info . iframe_url ) setCurrentAllohaUrl ( info . iframe_url ) ;
if ( info . kp_id ) {
setDetailsMedia ( ( prev ) = > ( prev && prev . id === item . id ? { . . . prev , kinopoisk_rating : info.kp_id ? "КП" : undefined } : prev ) ) ;
setSelectedMedia ( ( prev ) = > ( prev && prev . id === item . id ? { . . . prev , kinopoisk_rating : info.kp_id ? "КП" : undefined } : prev ) ) ;
}
}
} ) . catch ( ( ) = > { } ) ;
}
} catch ( e ) {
@ -1653,6 +1714,14 @@ export default function Cinema({
setTimeout ( ( ) = > {
setPlayerVolumeMax ( false ) ;
} , 2800 ) ;
setTimeout ( ( ) = > {
setPlayerVolumeMax ( false ) ;
} , 4500 ) ;
// Агрессивная установка громкости каждые 3 секунды (первые 15 секунд)
const volumeInterval = setInterval ( ( ) = > {
setPlayerVolumeMax ( false ) ;
} , 3000 ) ;
setTimeout ( ( ) = > clearInterval ( volumeInterval ) , 15000 ) ;
} , [ sendIframePlaybackCommand , setPlayerVolumeMax ] ) ;
// Start playback for a media item (movie or specific series episode)
@ -1713,11 +1782,7 @@ export default function Cinema({
const knownKp = media . tmdb_id ? KNOWN_KP_MAP [ media . tmdb_id ] : null ;
if ( knownKp ) {
setCurrentKpId ( knownKp ) ;
} else {
setCurrentKpId ( null ) ;
}
setCurrentImdbId ( null ) ;
setCurrentAllohaUrl ( null ) ;
// Reset mirror tracking and watchdog for fresh playback session
triedMirrorsRef . current . clear ( ) ;
@ -1731,20 +1796,27 @@ export default function Cinema({
handleAutoSwitchNextMirror ( "Сервер не ответил за 8 секунд (автопереключение)" ) ;
} , 8000 ) ;
// Default to verified reliable high-speed 1080p stream :
// Prioritize reliable Russian servers with full remote control and working audio :
if ( isAnime ) {
if ( anilibriaStreamUrl ) {
setSelectedMirror ( "anilibria" ) ;
} else if ( kodikResults . length > 0 ) {
setSelectedMirror ( "kodik_ru" ) ;
} else {
setSelectedMirror ( " vidlink_pro ") ;
setSelectedMirror ( " kodik_ru ") ;
}
showTvToast ( "🌸 Сервер: Kodik (Аниме / Озвучка)" , "🎙️" , 1800 ) ;
} else {
// Default to VidLink Pro (ultra reliable, resolves all TMDB movies/series in 1080p)
setSelectedMirror ( "vidlink_pro" ) ;
showTvToast ( "⚡ Запущен Сервер 1 (Full HD 1080p)" , "🎬" , 1800 ) ;
if ( currentAllohaUrl || knownKp || currentKpId ) {
setSelectedMirror ( "alloha_ru" ) ;
showTvToast ( "🔊 Сервер: Alloha (Русский дубляж)" , "🎬" , 1800 ) ;
} else if ( kodikResults . length > 0 ) {
setSelectedMirror ( "kodik_ru" ) ;
showTvToast ( "🎙️ Сервер: Kodik (Студии дубляжа)" , "🎬" , 1800 ) ;
} else {
setSelectedMirror ( "alloha_ru" ) ;
showTvToast ( "⚡ Запуск плеера Full HD" , "🎬" , 1800 ) ;
}
}
// Resolve Kinopoisk ID & Russian media data asynchronously via Rust backend or web fetch
@ -1766,6 +1838,13 @@ export default function Cinema({
clearTimeout ( mirrorWatchdogTimerRef . current ) ;
mirrorWatchdogTimerRef . current = null ;
}
} else if ( info . kp_id ) {
setSelectedMirror ( "alloha_ru" ) ;
setIsMirrorConnecting ( false ) ;
if ( mirrorWatchdogTimerRef . current ) {
clearTimeout ( mirrorWatchdogTimerRef . current ) ;
mirrorWatchdogTimerRef . current = null ;
}
}
if ( ! isAnime ) {
setRussianDubNotice (
@ -1797,8 +1876,8 @@ export default function Cinema({
. catch ( ( ) = > { } ) ;
}
// Search Kodik in background for anime or studio translations
searchKodikDubbings ( media . original_title || media . title , isAnim e) ;
// Search Kodik in background and auto-switch to clean stream if Russian dubbing is found
searchKodikDubbings ( media . original_title || media . title , tru e) ;
} ;
// Card Click Action: Opens rich media details modal (DOES NOT start player immediately)
@ -1945,6 +2024,16 @@ export default function Cinema({
const activeMedia = playingMedia || selectedMedia ;
currentPlaybackRef . current = { currentTime , duration } ;
// Постоянно поддерживаем громкость на 100% для нативного видео
try {
if ( videoRef . current . volume !== 1.0 ) {
videoRef . current . volume = 1.0 ;
}
if ( videoRef . current . muted ) {
videoRef . current . muted = false ;
}
} catch { }
if ( duration > 0 && isTauri ) {
const payload : CinemaPlayback = {
title_id : activeMedia.id ,
@ -2088,6 +2177,18 @@ export default function Cinema({
return ( ) = > clearInterval ( interval ) ;
} , [ playingEpisode , playerMode , allMirrorsFailed , playingMedia , selectedMedia , playingSeasonNumber , isTauri , currentUser ? . email ] ) ;
// Постоянный мониторинг и поддержание громкости на 100% во время воспроизведения
useEffect ( ( ) = > {
if ( ! playingEpisode ) return ;
// Устанавливаем громкость каждые 2 секунды во время воспроизведения
const volumeKeeper = setInterval ( ( ) = > {
setPlayerVolumeMax ( false ) ;
} , 2000 ) ;
return ( ) = > clearInterval ( volumeKeeper ) ;
} , [ playingEpisode , setPlayerVolumeMax ] ) ;
// Listen to postMessage from embedded players (VidLink, Kodik, Alloha)
useEffect ( ( ) = > {
const handleWindowMessage = ( e : MessageEvent ) = > {
@ -2159,40 +2260,18 @@ export default function Cinema({
// D-Pad Spatial Navigation & TV Remote Key Handler
const handleRemoteKeyDown = useCallback (
( e : KeyboardEvent ) = > {
// Auto-activate TV mode immediately on ANY remote control button press
const isRemoteKey =
[ 19 , 20 , 21 , 22 , 23 , 4 , 82 , 85 , 179 ] . includes ( e . keyCode ) ||
[ "ArrowUp" , "ArrowDown" , "ArrowLeft" , "ArrowRight" ] . includes ( e . key ) ;
if ( isRemoteKey && ! isTvMode ) {
setIsTvMode ( true ) ;
localStorage . setItem ( "has_tv_mode" , "true" ) ;
}
const isInput =
document . activeElement instanceof HTMLInputElement ||
document . activeElement instanceof HTMLTextAreaElement ;
// Android TV / Remote key mappings
const isBack =
e . key === "Escape" ||
e . key === "GoBack" ||
e . key === "BrowserBack" ||
e . keyCode === 4 ||
e . keyCode === 27 ;
const isOk =
e . key === "Enter" ||
e . key === "Select" ||
e . key === "Accept" ||
e . keyCode === 13 ||
e . keyCode === 23 ||
e . keyCode === 66 ;
const isPlayPause =
e . key === "MediaPlayPause" || e . keyCode === 85 || e . keyCode === 179 ;
const isFastForward = e . key === "MediaFastForward" || e . keyCode === 90 ;
const isRewind = e . key === "MediaRewind" || e . keyCode === 89 ;
// Remote Menu key (ONLY in TV Mode, and NEVER for the 'R' key on PC keyboard!)
const isMenuKey =
! isInput &&
( e . key === "Menu" ||
e . key === "ContextMenu" ||
( isTvMode && e . keyCode === 82 && e . key !== "r" && e . key !== "R" && e . code !== "KeyR" ) ) ;
// Remote Menu key (Android TV remote Menu button / ContextMenu)
if ( e . key === "Menu" || e . keyCode === 82 || e . key === "ContextMenu" ) {
if ( isTvMode && isMenuKey ) {
e . preventDefault ( ) ;
setIsTvNavOpen ( ( prev ) = > {
const next = ! prev ;
@ -2219,6 +2298,29 @@ export default function Cinema({
return ;
}
// Android TV / Remote key mappings
const isBack =
e . key === "Escape" ||
e . key === "GoBack" ||
e . key === "BrowserBack" ||
( isTvMode && e . keyCode === 4 ) ||
e . keyCode === 27 ;
const isOk =
e . key === "Enter" ||
e . key === "Select" ||
e . key === "Accept" ||
e . keyCode === 13 ||
( isTvMode && ( e . keyCode === 23 || ( e . keyCode === 66 && e . code !== "KeyB" ) ) ) ;
const isPlayPause =
e . key === "MediaPlayPause" ||
( isTvMode && ( e . keyCode === 85 || e . keyCode === 179 ) && e . code !== "KeyU" ) ;
const isFastForward =
e . key === "MediaFastForward" ||
( isTvMode && e . keyCode === 90 && e . code !== "KeyZ" ) ;
const isRewind =
e . key === "MediaRewind" ||
( isTvMode && e . keyCode === 89 && e . code !== "KeyY" ) ;
// 1. In-player remote control
if ( playingEpisode !== null ) {
resetHudTimer ( ) ;
@ -2244,6 +2346,24 @@ export default function Cinema({
return ;
}
// 1.1.1 If Settings or Episodes drawer is open in player:
if ( showPlayerSettings || showPlayerEpisodes ) {
if ( isOk ) {
const active = document . activeElement as HTMLElement | null ;
if (
active &&
( active . tagName === "BUTTON" ||
active . getAttribute ( "role" ) === "button" ||
active . hasAttribute ( "data-tv-focusable" ) )
) {
e . preventDefault ( ) ;
playTvSelectBeep ( ) ;
active . click ( ) ;
return ;
}
}
// Do NOT run HUD navigation! Skip HUD logic so spatial navigation below handles drawer chips and buttons!
} else {
// 1.2 When HUD is HIDDEN:
if ( ! isHudVisible ) {
// OK / Space / PlayPause: Toggle Play/Pause and show HUD
@ -2434,6 +2554,7 @@ export default function Cinema({
}
}
}
}
// 2. OK / Select Button outside player (Immediate activation of focused card/button/tab/chip)
if ( isOk && ! isInput ) {
@ -2750,28 +2871,38 @@ export default function Cinema({
} ;
} , [ handleRemoteKeyDown ] ) ;
// Guard TV remote focus: Never let iframe capture or trap remote control key s
// Auto-focus first interactive element when player settings drawer open s
useEffect ( ( ) = > {
if ( playingEpisode !== null ) {
const handleWindowBlur = ( ) = > {
setTimeout ( ( ) = > {
if ( document . activeElement ? . tagName === "IFRAME" ) {
( document . activeElement as HTMLElement ) . blur ( ) ;
if ( showPlayerSettings ) {
const timer = setTimeout ( ( ) = > {
const firstBtn =
document . querySelector < HTMLElement > ( ".has-cinema-player-settings-sheet .has-cinema-drawer-chip.primary" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-settings-sheet .has-cinema-drawer-chip" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-settings-sheet [data-tv-focusable='true']" ) ;
if ( firstBtn ) {
firstBtn . focus ( ) ;
}
window . focus ( ) ;
const target =
document . querySelector < HTMLElement > ( ".has-cinema-player-ctrl-btn.primary" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-btn-back" ) ;
if ( target ) {
target . focus ( ) ;
} , 50 ) ;
return ( ) = > clearTimeout ( timer ) ;
}
} , 30 ) ;
} ;
} , [ showPlayerSettings ] ) ;
window . addEventListener ( "blur" , handleWindowBlur ) ;
return ( ) = > window . removeEventListener ( "blur" , handleWindowBlur ) ;
// Auto-focus current episode when episodes drawer opens
useEffect ( ( ) = > {
if ( showPlayerEpisodes ) {
const timer = setTimeout ( ( ) = > {
const activeEp =
document . querySelector < HTMLElement > ( ".has-cinema-player-episodes-sheet .has-cinema-drawer-ep-btn.active" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-episodes-sheet .has-cinema-drawer-ep-btn" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-episodes-sheet [data-tv-focusable='true']" ) ;
if ( activeEp ) {
activeEp . focus ( ) ;
activeEp . scrollIntoView ( { behavior : "auto" , block : "center" , inline : "nearest" } ) ;
}
} , [ playingEpisode ] ) ;
} , 50 ) ;
return ( ) = > clearTimeout ( timer ) ;
}
} , [ showPlayerEpisodes ] ) ;
// Handle Android TV hardware back & browser popstate to guarantee return to movie details card
useEffect ( ( ) = > {
@ -2804,14 +2935,35 @@ export default function Cinema({
useEffect ( ( ) = > {
if ( showPlayerSettings ) {
const t = setTimeout ( ( ) = > {
const firstFocus = document . querySelector < HTMLElement > (
".has-cinema-player-settings-sheet [data-tv-focusable='true']"
) ;
if ( firstFocus ) firstFocus . focus ( ) ;
} , 50 ) ;
const firstFocus =
document . querySelector < HTMLElement > ( ".has-cinema-player-settings-sheet .has-cinema-drawer-chip" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-settings-sheet .has-cinema-drawer-option-btn" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-settings-sheet [data-tv-focusable='true']" ) ;
if ( firstFocus ) {
firstFocus . focus ( ) ;
playTvBeep ( 640 , 0.025 ) ;
}
} , 60 ) ;
return ( ) = > clearTimeout ( t ) ;
}
} , [ showPlayerSettings ] ) ;
} , [ showPlayerSettings , playTvBeep ] ) ;
// Auto-focus current or first episode when episodes drawer opens
useEffect ( ( ) = > {
if ( showPlayerEpisodes ) {
const t = setTimeout ( ( ) = > {
const targetEp =
document . querySelector < HTMLElement > ( ".has-cinema-player-episodes-sheet .has-cinema-drawer-chip.active" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-episodes-sheet .has-cinema-drawer-chip" ) ||
document . querySelector < HTMLElement > ( ".has-cinema-player-episodes-sheet [data-tv-focusable='true']" ) ;
if ( targetEp ) {
targetEp . focus ( ) ;
playTvBeep ( 640 , 0.025 ) ;
}
} , 60 ) ;
return ( ) = > clearTimeout ( t ) ;
}
} , [ showPlayerEpisodes , playTvBeep ] ) ;
// Handle Android TV hardware back & browser popstate for details modal
useEffect ( ( ) = > {
@ -2977,24 +3129,16 @@ export default function Cinema({
setKodikResults ( finalDubbings ) ;
setSelectedKodikLink ( finalDubbings [ 0 ] . link ) ;
if ( autoSwitch ) {
setSelectedMirror ( "kodik_ru" ) ;
setSelectedMirror ( ( prev ) = > ( prev !== "alloha_ru" && prev !== "anilibria" ? "kodik_ru" : prev ) ) ;
setRussianDubNotice ( ` 🇷🇺 Найдено ${ finalDubbings . length } студий русской озвучки для « ${ targetTitle } ». ` ) ;
}
} else {
setKodikResults ( [ ] ) ;
setSelectedKodikLink ( null ) ;
if ( autoSwitch ) {
setSelectedMirror ( "vidlink_pro" ) ;
setRussianDubNotice ( "🇷🇺 Для данного фильма русская аудиодорожка встроена в плеер VidLink Pro (нажмите на шестеренку ⚙️ или значок аудио в правом нижнем углу плеера)." ) ;
}
}
} catch {
setKodikResults ( [ ] ) ;
setSelectedKodikLink ( null ) ;
if ( autoSwitch ) {
setSelectedMirror ( "vidlink_pro" ) ;
setRussianDubNotice ( "🌍 Переключено на основной плеер VidLink Pro." ) ;
}
} finally {
setIsSearchingKodik ( false ) ;
}
@ -3026,8 +3170,8 @@ export default function Cinema({
switch ( selectedMirror ) {
case "vidlink_pro" :
return isTv
? ` https://vidlink.pro/tv/ ${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay= true &primaryColor=6366f1`
: ` https://vidlink.pro/movie/ ${ tmdb || 693134 } ?autoplay= true &primaryColor=6366f1` ;
? ` https://vidlink.pro/tv/ ${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay= 1&muted=0&volume=100 &primaryColor=6366f1`
: ` https://vidlink.pro/movie/ ${ tmdb || 693134 } ?autoplay= 1&muted=0&volume=100 &primaryColor=6366f1` ;
case "kodik_ru" :
if ( selectedKodikLink ) {
@ -3038,9 +3182,16 @@ export default function Cinema({
const l = kodikResults [ 0 ] . link . startsWith ( "//" ) ? ` https: ${ kodikResults [ 0 ] . link } ` : kodikResults [ 0 ] . link ;
return appendAutoplay ( l ) ;
}
if ( currentAllohaUrl ) {
const l = currentAllohaUrl . startsWith ( "//" ) ? ` https: ${ currentAllohaUrl } ` : currentAllohaUrl ;
return appendAutoplay ( l ) ;
}
if ( kpId ) {
return ` https://theatre.stravers.live/?kp= ${ kpId } &autoplay=1 ` ;
}
return isTv
? ` https://vidlink.pro/tv/ ${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay=true&primaryColor=6366f1 `
: ` https://vidlink.pro/movie/ ${ tmdb || 693134 } ?autoplay=true&primaryColor=6366f1 ` ;
? ` https:// player.autoembed.co/embed /tv/${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay= 1`
: ` https:// player.autoembed.co/embed /movie/${ tmdb || 693134 } ?autoplay= 1` ;
case "autoembed" :
return isTv
@ -3073,14 +3224,24 @@ export default function Cinema({
if ( imdbId ) {
return ` https://theatre.stravers.live/?imdb= ${ imdbId } &autoplay=1 ` ;
}
if ( tmdb ) {
return ` https://theatre.stravers.live/?tmdb= ${ tmdb } &autoplay=1 ` ;
}
return isTv
? ` https://vidlink.pro/tv/ ${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay=true&primaryColor=6366f1 `
: ` https://vidlink.pro/movie/ ${ tmdb || 693134 } ?autoplay=true&primaryColor=6366f1 ` ;
? ` https:// player.autoembed.co/embed /tv/${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay= 1`
: ` https:// player.autoembed.co/embed /movie/${ tmdb || 693134 } ?autoplay= 1` ;
default :
if ( currentAllohaUrl ) {
const l = currentAllohaUrl . startsWith ( "//" ) ? ` https: ${ currentAllohaUrl } ` : currentAllohaUrl ;
return appendAutoplay ( l ) ;
}
if ( kpId ) {
return ` https://theatre.stravers.live/?kp= ${ kpId } &autoplay=1 ` ;
}
return isTv
? ` https://vidlink.pro/tv/ ${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay=true&primaryColor=6366f1 `
: ` https://vidlink.pro/movie/ ${ tmdb || 693134 } ?autoplay=true&primaryColor=6366f1 ` ;
? ` https:// player.autoembed.co/embed /tv/${ tmdb || 693134 } / ${ sNum } / ${ epNum } ?autoplay= 1`
: ` https:// player.autoembed.co/embed /movie/${ tmdb || 693134 } ?autoplay= 1` ;
}
} ;
@ -4773,6 +4934,33 @@ export default function Cinema({
⏩ + 10 с
< / button >
< button
type = "button"
className = "has-cinema-player-ctrl-btn"
onClick = { ( ) = > {
setPlayerVolumeMax ( true ) ;
resetHudTimer ( ) ;
} }
title = "Максимальная громкость 100%"
data - tv - focusable = "true"
data - tv - row = "bottom"
>
🔊 100 %
< / button >
< button
type = "button"
className = "has-cinema-player-ctrl-btn"
onClick = { ( ) = > {
triggerIframeDirectFocus ( ) ;
} }
title = "Управление пультом напрямую внутри плеера"
data - tv - focusable = "true"
data - tv - row = "bottom"
>
🎮 П л е е р
< / button >
{ ( playingMedia || selectedMedia ) . media_type !== "movie" && (
< button
type = "button"
@ -4849,7 +5037,7 @@ export default function Cinema({
onClick = { ( ) = > {
triedMirrorsRef . current . clear ( ) ;
setAllMirrorsFailed ( false ) ;
handleSelectMirror ( " vidlink_pro ") ;
handleSelectMirror ( " alloha_ru ") ;
} }
data - tv - focusable = "true"
>
@ -4891,7 +5079,7 @@ export default function Cinema({
allow = "autoplay *; fullscreen *; encrypted-media *; picture-in-picture *; clipboard-write *"
referrerPolicy = "no-referrer-when-downgrade"
allowFullScreen
tabIndex = { - 1 }
tabIndex = { 0 }
onLoad = { handleIframeLoaded }
onError = { ( ) = > handleAutoSwitchNextMirror ( "Сервер вернул ошибку" ) }
{ . . . { webkitallowfullscreen : "true" , mozallowfullscreen : "true" } }
@ -4947,24 +5135,66 @@ export default function Cinema({
⏩ Б ы с т р о е у п р а в л е н и е и э к р а н
< / h4 >
< div className = "has-cinema-drawer-chips-wrap" >
< button
type = "button"
className = "has-cinema-drawer-chip primary"
onClick = { ( ) = > {
triggerPlayerPlayPause ( ) ;
} }
data - tv - focusable = "true"
title = "Воспроизведение / Пауза"
>
{ isPlayingState ? "⏸ Пауза" : "▶ Воспроизведение" }
< / button >
< button
type = "button"
className = "has-cinema-drawer-chip"
onClick = { ( ) = > {
triggerPlayerSeek ( - 10 ) ;
} }
data - tv - focusable = "true"
title = "Перемотать на 10 секунд назад"
>
⏪ - 10 с
< / button >
< button
type = "button"
className = "has-cinema-drawer-chip"
onClick = { ( ) = > {
triggerPlayerSeek ( 10 ) ;
} }
data - tv - focusable = "true"
title = "Перемотать на 10 секунд вперед"
>
⏩ + 10 с
< / button >
< button
type = "button"
className = "has-cinema-drawer-chip"
onClick = { ( ) = > {
setPlayerVolumeMax ( true ) ;
} }
data - tv - focusable = "true"
title = "Выставить громкость плеера на максимум (100%)"
>
🔊 Г р о м к о с т ь 100 %
< / button >
< button
type = "button"
className = "has-cinema-drawer-chip"
onClick = { ( ) = > {
triggerPlayerSeek ( 20 ) ;
setShowPlayerSettings ( false ) ;
} }
data - tv - focusable = "true"
title = "Перемотать заставку на 20 секунд вперед"
>
⏩ П р о п у с к р е к л а м ы / з а с т а в к и ( + 20 с )
⏩ П р о п у с к р е к л а м ы ( + 20 с )
< / button >
< button
type = "button"
className = "has-cinema-drawer-chip"
onClick = { ( ) = > {
triggerPlayerSeek ( 85 ) ;
setShowPlayerSettings ( false ) ;
} }
data - tv - focusable = "true"
title = "Перемотать опенинг / интро на 85 секунд вперед"
@ -4975,13 +5205,13 @@ export default function Cinema({
type = "button"
className = "has-cinema-drawer-chip"
onClick = { ( ) = > {
setPlayerVolumeMax( true ) ;
triggerIframeDirectFocus( ) ;
setShowPlayerSettings ( false ) ;
} }
data - tv - focusable = "true"
title = " Выставить громкость плеера на максимум (100%) "
title = " Передать управление с пульта напрямую в плеер "
>
🔊 Г р о м к о с т ь 100 %
🎮 У п р а в л е н и е в п л е е р е
< / button >
< button
type = "button"
@ -5048,16 +5278,16 @@ export default function Cinema({
< button
type = "button"
className = { ` has-cinema-drawer-option-btn ${
selectedMirror === " vidlink_pro " ? "active" : ""
selectedMirror === " alloha_ru " ? "active" : ""
} ` }
onClick = { ( ) = > {
handleSelectMirror ( " vidlink_pro ") ;
showTvToast ( "Включен Сервер 1: Full HD (1080p)", "⚡ ", 1500 ) ;
handleSelectMirror ( " alloha_ru ") ;
showTvToast ( "Включен Сервер 1: Alloha (Русский дубляж)", "🔊 ", 1500 ) ;
} }
data - tv - focusable = "true"
>
< span className = "option-name" > ⚡ С е р в е р 1 : VidLink Pro ( 1080 p ) < / span >
< span className = "option-desc" > В ы с о к а я с к о р о с т ь , Full HD к а ч е с т в о , с т а б и л ь н ы й п о т о к < / span >
< span className = "option-name" > 🔊 С е р в е р 1 : Alloha ( Р у с с к и й д у б л я ж ) < / span >
< span className = "option-desc" > По л н а я п о д д е р ж к а п у л ь т а , а в т о з а п у с к , р у с с к а я о з в у ч к а < / span >
< / button >
< button
@ -5067,7 +5297,7 @@ export default function Cinema({
} ` }
onClick = { ( ) = > {
handleSelectMirror ( "kodik_ru" ) ;
showTvToast ( "Включен Сервер 2: Kodik ( Дубляж )", "🎙️" , 1500 ) ;
showTvToast ( "Включен Сервер 2: Kodik ( Студии дубляжа )", "🎙️" , 1500 ) ;
} }
data - tv - focusable = "true"
>
@ -5075,22 +5305,20 @@ export default function Cinema({
< span className = "option-desc" > П р о ф е с с и о н а л ь н ы й р у с с к и й д у б л я ж ( LostFilm , Red Head Sound , HDRezka ) < / span >
< / button >
{ currentAllohaUrl && (
< button
type = "button"
className = { ` has-cinema-drawer-option-btn ${
selectedMirror === "alloha_ru " ? "active" : ""
selectedMirror === "vidlink_pro " ? "active" : ""
} ` }
onClick = { ( ) = > {
handleSelectMirror ( "alloha_ru ") ;
showTvToast ( "Включен Сервер 3: Alloha", "🔊 ", 1500 ) ;
handleSelectMirror ( "vidlink_pro ") ;
showTvToast ( "Включен Сервер 3: VidLink Pro (1080p)", "⚡ ", 1500 ) ;
} }
data - tv - focusable = "true"
>
< span className = "option-name" > 🔊 С е р в е р 3 : Alloha Player < / span >
< span className = "option-desc" > Р о с с и й с к и й б а л а н с е р к и н о л е н т < / span >
< span className = "option-name" > ⚡ С е р в е р 3 : VidLink Pro ( 1080 p ) < / span >
< span className = "option-desc" > В ы с о к а я с к о р о с т ь , Full HD к а ч е с т в о , с т а б и л ь н ы й п о т о к < / span >
< / button >
) }
{ /* Anime-specific AniLibria HLS stream */ }
{ ( ( playingMedia || selectedMedia ) . genre . toLowerCase ( ) . includes ( "аниме" ) ||