Created
August 18, 2025 13:12
-
-
Save TSB1999/08c589c15a1f147412df66672d40c99d to your computer and use it in GitHub Desktop.
The next Spotify won’t be a music platform — it’ll be a better player for YouTube.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { | |
| createContext, | |
| useState, | |
| useRef, | |
| useEffect, | |
| useContext, | |
| } from 'react'; | |
| import WebView from 'react-native-webview'; | |
| import {Trak} from '../types/Trak'; | |
| import {useSelector} from 'react-redux'; | |
| import {useEffectAsync} from '../hooks/useEffectAsync'; | |
| import {Modalize} from 'react-native-modalize'; | |
| import {useAsyncStorage} from '../hooks/useAsyncStorage'; | |
| import {asyncStorageKeys} from '../core/asyncStorageKeys'; | |
| import {handleRetrieveRadio} from '../app/utils/retreiveRadio'; | |
| import PushNotificationIOS from '@react-native-community/push-notification-ios'; | |
| import {nowPlaying} from '../app/utils/nowPlaying'; | |
| import {useTrakstar} from '../hooks/useTrakstar'; | |
| import {HeaderContext} from '../stores/context/HeaderProvider'; | |
| import {SPINBROKER, TRAKLIST} from '../app/utils/meta'; | |
| export interface Track { | |
| track: Trak; | |
| trace: Trace; | |
| buffer: Trak[]; | |
| protocolId?: string; | |
| uuid?: string; | |
| theme?: string; | |
| } | |
| export interface Trace { | |
| type: | |
| | 'track' | |
| | 'album' | |
| | 'artist' | |
| | 'playlist' | |
| | 'category' | |
| | 'user' | |
| | 'request' | |
| | 'edit' | |
| | 'web'; | |
| source: string; | |
| } | |
| interface Player { | |
| index: number; | |
| radio: Trak[] | null; | |
| radioIndex: number; | |
| bufferIndex: number; | |
| queue: Track[]; | |
| isLive: boolean; | |
| setIsLive: React.Dispatch<React.SetStateAction<boolean>>; | |
| isPaused: boolean; | |
| isShuffle: boolean; | |
| isPrimaryPlayer: boolean; | |
| isPrimaryPlayerInitialized: boolean; | |
| isSecondaryPlayerInitialized: boolean; | |
| isPrimaryWebViewLoaded: boolean; | |
| isSecondaryWebViewLoaded: boolean; | |
| currentTrack: Track | null; | |
| buffer1: React.RefObject<WebView>; | |
| buffer2: React.RefObject<WebView>; | |
| handleStop: () => void; | |
| handlePause: () => void; | |
| handleRepeatOptions: () => void; | |
| handlePlayPrevious: () => void; | |
| handleBuffer: (options: {isPrimary: boolean}) => void; | |
| handlePlayNext: () => void; | |
| handleQueue: (queue: Track[]) => void; | |
| handleSeek: (seekTo: number) => void; | |
| handlePlayNow: (track: Track) => void; | |
| handleQueueNext: (queue: Track[]) => void; | |
| handleNext: (options: {isPrimary: boolean}) => void; | |
| handleForcePiP: () => void; | |
| handleShuffleOptions: () => void; | |
| handleReleaseKeys: () => void; | |
| setRadio: React.Dispatch<React.SetStateAction<boolean>>; | |
| setIsPaused: React.Dispatch<React.SetStateAction<boolean>>; | |
| setPrimaryWebViewLoaded: React.Dispatch<React.SetStateAction<boolean>>; | |
| setSecondaryWebViewLoaded: React.Dispatch<React.SetStateAction<boolean>>; | |
| setSecondaryPlayerInitialized: React.Dispatch<React.SetStateAction<boolean>>; | |
| setPrimaryPlayerInitialized: React.Dispatch<React.SetStateAction<boolean>>; | |
| setBufferIndex: React.Dispatch<React.SetStateAction<number>>; | |
| setRadioIndex: React.Dispatch<React.SetStateAction<number>>; | |
| primaryRunnerKey: string | null; | |
| setPrimaryRunnerKey: React.Dispatch<React.SetStateAction<string | null>>; | |
| secondaryRunnerKey: string | null; | |
| setSecondaryRunnerKey: React.Dispatch<React.SetStateAction<string | null>>; | |
| primaryPreloadKey: string | null; | |
| setPrimaryPreloadKey: React.Dispatch<React.SetStateAction<string | null>>; | |
| secondaryPreloadKey: string | null; | |
| setSecondaryPreloadKey: React.Dispatch<React.SetStateAction<string | null>>; | |
| repeatOptions: 'repeat' | 'repeat-once' | 'off'; | |
| setRepeatOptions: React.Dispatch< | |
| React.SetStateAction<'repeat' | 'repeat-once' | 'off'> | |
| >; | |
| isPlayingFromQueue: boolean; | |
| isPlayingFromQueueLoop: boolean; | |
| setIsPlayingFromQueue: React.Dispatch<React.SetStateAction<boolean>>; | |
| setIsPlayingFromQueueLoop: React.Dispatch<React.SetStateAction<boolean>>; | |
| setIndex: React.Dispatch<React.SetStateAction<Number>>; | |
| setQueue: React.Dispatch<React.SetStateAction<Track[]>>; | |
| initializingPiP: boolean; | |
| setInitializingPiP: React.Dispatch<React.SetStateAction<boolean>>; | |
| } | |
| export const PlayerContext = createContext<Player>({ | |
| radioIndex: 0, | |
| radio: null, | |
| index: -1, | |
| bufferIndex: -1, | |
| queue: [], | |
| isLive: true, | |
| isPaused: false, | |
| isShuffle: false, | |
| isPrimaryWebViewLoaded: false, | |
| isSecondaryWebViewLoaded: false, | |
| isPrimaryPlayerInitialized: false, | |
| isSecondaryPlayerInitialized: false, | |
| isPrimaryPlayer: true, | |
| currentTrack: null, | |
| buffer1: {current: null}, | |
| buffer2: {current: null}, | |
| primaryRunnerKey: null, | |
| setPrimaryRunnerKey: () => {}, | |
| secondaryRunnerKey: null, | |
| setSecondaryRunnerKey: () => {}, | |
| primaryPreloadKey: null, | |
| setPrimaryPreloadKey: () => {}, | |
| secondaryPreloadKey: null, | |
| setSecondaryPreloadKey: () => {}, | |
| setIsLive: () => {}, | |
| setRadio: () => {}, | |
| setRepeatOptions: () => {}, | |
| setIsPlayingFromQueue: () => {}, | |
| setPrimaryWebViewLoaded: () => {}, | |
| setSecondaryWebViewLoaded: () => {}, | |
| setPrimaryPlayerInitialized: () => {}, | |
| setSecondaryPlayerInitialized: () => {}, | |
| setIsPlayingFromQueueLoop: () => {}, | |
| setBufferIndex: () => {}, | |
| setIsPaused: () => {}, | |
| setRadioIndex: () => {}, | |
| handleStop: () => {}, | |
| handlePause: () => {}, | |
| handlePlayPrevious: () => {}, | |
| handleBuffer: () => {}, | |
| handleQueue: () => {}, | |
| handlePlayNow: () => {}, | |
| handleQueueNext: () => {}, | |
| handleNext: () => {}, | |
| handlePlayNext: () => {}, | |
| handleShuffleOptions: () => {}, | |
| handleReleaseKeys: () => {}, | |
| handleSeek: (seekTo: number) => {}, | |
| repeatOptions: 'off', | |
| handleRepeatOptions: () => {}, | |
| isPlayingFromQueue: false, | |
| isPlayingFromQueueLoop: false, | |
| setIndex: () => {}, | |
| setQueue: () => {}, | |
| initializingPiP: false, | |
| setInitializingPiP: () => {}, | |
| handleForcePiP: () => {}, | |
| }); | |
| export const PlayerProvider = ({children}: {children: React.ReactChild}) => { | |
| const {setIsLoading} = useContext(HeaderContext); | |
| const initializedPlayer = useSelector( | |
| (state: any) => state.streaming.initializedPlayer, | |
| ); | |
| const [index, setIndex] = useState(initializedPlayer.index ?? -1); | |
| const [bufferIndex, setBufferIndex] = useState( | |
| initializedPlayer.bufferIndex ?? -1, | |
| ); | |
| const [isPaused, setIsPaused] = useState(true); | |
| const [queue, setQueue] = useState<Track[]>(initializedPlayer.queue ?? []); | |
| const [isPrimaryPlayer, setIsPrimaryPlayer] = useState(true); | |
| const [initializingPiP, setInitializingPiP] = useState(false); | |
| const [isLive, setIsLive] = useState(true); | |
| const [currentTrack, setCurrentTrack] = useState<Track | null>( | |
| initializedPlayer.currentTrack ?? null, | |
| ); | |
| const [isShuffle, setIsShuffle] = useState(false); | |
| const [radio, setRadio] = useState(initializedPlayer.radio); | |
| const [radioIndex, setRadioIndex] = useState(initializedPlayer.radioId); | |
| const [isPrimaryWebViewLoaded, setPrimaryWebViewLoaded] = useState(true); | |
| const [isSecondaryWebViewLoaded, setSecondaryWebViewLoaded] = useState(false); | |
| const [isPrimaryPlayerInitialized, setPrimaryPlayerInitialized] = | |
| useState(false); | |
| const [isSecondaryPlayerInitialized, setSecondaryPlayerInitialized] = | |
| useState(false); | |
| const [repeatOptions, setRepeatOptions] = useState< | |
| 'repeat-once' | 'repeat' | 'off' | |
| >(initializedPlayer.repeatOptions ?? 'repeat'); | |
| const [primaryRunnerKey, setPrimaryRunnerKey] = useState<string | null>(null); | |
| const [secondaryRunnerKey, setSecondaryRunnerKey] = useState<string | null>( | |
| null, | |
| ); | |
| const [primaryPreloadKey, setPrimaryPreloadKey] = useState<string | null>( | |
| null, | |
| ); | |
| const [secondaryPreloadKey, setSecondaryPreloadKey] = useState<string | null>( | |
| null, | |
| ); | |
| const [isPlayingFromQueue, setIsPlayingFromQueue] = useState<boolean>( | |
| initializedPlayer.isPlayingFromQueue, | |
| ); | |
| const [isPlayingFromQueueLoop, setIsPlayingFromQueueLoop] = useState<boolean>( | |
| initializedPlayer.isPlayingFromQueueLoop, | |
| ); | |
| const {handleStoreData} = useAsyncStorage(); | |
| const networkToken = useSelector( | |
| (state: any) => state.keys.jukerstone.networkToken, | |
| ); | |
| useEffectAsync(async () => { | |
| if (radioIndex > radio.length - 4) { | |
| const newRadio = await handleRetrieveRadio({token: networkToken}); | |
| if (newRadio.radio) { | |
| setRadio([...radio, ...newRadio.radio]); | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: '1', | |
| title: 'TrakStar™ Music: Radio', | |
| body: 'Generating new tracks for ya!', | |
| }); | |
| if (!queue.length && currentTrack) { | |
| currentTrack.buffer = [...currentTrack.buffer, ...newRadio.radio]; | |
| } else if (queue.length) { | |
| const oldBuffer = queue[index + 1].buffer; | |
| const newBuffer = [...oldBuffer, ...newRadio.radio]; | |
| queue[index + 1].buffer = newBuffer; | |
| } | |
| } | |
| } | |
| await handleStoreData(asyncStorageKeys.RADIO_ID, radioIndex); | |
| }, [radioIndex]); | |
| useEffectAsync(async () => { | |
| await handleStoreData(asyncStorageKeys.INDEX, index); | |
| await handleStoreData(asyncStorageKeys.BUFFER_ID, bufferIndex); | |
| await handleStoreData(asyncStorageKeys.QUEUE, queue); | |
| await handleStoreData(asyncStorageKeys.REPEAT_OPTIONS, repeatOptions); | |
| await handleStoreData( | |
| asyncStorageKeys.IS_PLAYING_FROM_QUEUE, | |
| isPlayingFromQueue, | |
| ); | |
| await handleStoreData( | |
| asyncStorageKeys.IS_PLAYING_FROM_QUEUE_LOOP, | |
| isPlayingFromQueueLoop, | |
| ); | |
| }, [index, bufferIndex, queue, currentTrack]); | |
| const buffer1 = useRef(null); | |
| const buffer2 = useRef(null); | |
| const handleStop = async () => { | |
| setQueue([]); | |
| setIndex(-1); | |
| setBufferIndex(-1); | |
| setCurrentTrack(null); | |
| setIsPaused(true); | |
| setRadio(null); | |
| setIsPlayingFromQueue(false); | |
| setInitializingPiP(false); | |
| setIsLoading(false); | |
| isPrimaryPlayer && setPrimaryWebViewLoaded(false); | |
| !isPrimaryPlayer && setSecondaryWebViewLoaded(false); | |
| isPrimaryPlayer && setPrimaryPlayerInitialized(false); | |
| !isPrimaryPlayer && setSecondaryPlayerInitialized(false); | |
| await handleStoreData(asyncStorageKeys.RADIO_ID, -1); | |
| await handleStoreData(asyncStorageKeys.RADIO, null); | |
| await handleStoreData(asyncStorageKeys.INDEX, -1); | |
| await handleStoreData(asyncStorageKeys.BUFFER_ID, -1); | |
| await handleStoreData(asyncStorageKeys.QUEUE, []); | |
| await handleStoreData(asyncStorageKeys.CURRENT_TRACK, null); | |
| }; | |
| const handleBuffer = ({isPrimary}: {isPrimary: boolean}) => { | |
| setIsPlayingFromQueue(false); | |
| setBufferIndex(prevIndex => { | |
| if (!queue.length && currentTrack) { | |
| if (prevIndex + 1 < currentTrack.buffer.length) { | |
| if (currentTrack.buffer[bufferIndex + 1]?.isRadio) { | |
| setRadioIndex(radioIndex + 1); | |
| } | |
| return prevIndex + 1; | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setIsPlayingFromQueueLoop(true); | |
| } else { | |
| handleStop(); | |
| } | |
| return -1; | |
| } | |
| } else if (queue.length) { | |
| if (prevIndex + 1 < queue[index].buffer.length) { | |
| if (queue[index].buffer[bufferIndex + 1]?.isRadio) { | |
| setRadioIndex(radioIndex + 1); | |
| } | |
| return prevIndex + 1; | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setIsPlayingFromQueueLoop(true); | |
| } else { | |
| handleStop(); | |
| } | |
| return -1; | |
| } | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setIsPlayingFromQueueLoop(true); | |
| } else { | |
| handleStop(); | |
| } | |
| return -1; | |
| } | |
| }); | |
| setIsPrimaryPlayer(isPrimary); | |
| }; | |
| const handleForcePiP = () => { | |
| const ref = isPrimaryPlayer ? buffer1 : buffer2; | |
| ref.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| } | |
| true; | |
| `); | |
| }; | |
| const handlePause = async () => { | |
| if (!currentTrack) { | |
| if (radio && radio.length) { | |
| setBufferIndex(-1); | |
| setCurrentTrack({ | |
| buffer: radio.slice(1), | |
| trace: { | |
| source: `juk:11:${radio[0].trak.youtube.url.split('=')[1]}`, | |
| type: 'track', | |
| }, | |
| track: radio[0], | |
| }); | |
| } | |
| // else { | |
| // const {radio, radioId} = await handleRetrieveRadio({ | |
| // token: networkToken, | |
| // }); | |
| // setBufferIndex(-1); | |
| // setRadioIndex(radioId); | |
| // setCurrentTrack({ | |
| // buffer: radio.slice(1), | |
| // trace: { | |
| // source: `juk:11:${radio[0].trak.youtube.url.split('=')[1]}`, | |
| // type: 'track', | |
| // }, | |
| // track: radio[0], | |
| // }); | |
| // } | |
| } | |
| if (isPrimaryPlayer && isPaused) { | |
| buffer1.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } else if (isPrimaryPlayer && !isPaused) { | |
| buffer1.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.pause(); | |
| } | |
| true; | |
| `); | |
| } else if (!isPrimaryPlayer && !isPaused) { | |
| buffer2.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.pause(); | |
| } | |
| true; | |
| `); | |
| } else if (!isPrimaryPlayer && isPaused) { | |
| buffer2.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } | |
| }; | |
| const handlePlayNow = (track: Track) => { | |
| if (repeatOptions == 'repeat-once') { | |
| setRepeatOptions('off'); | |
| } | |
| setPrimaryPreloadKey(null); | |
| setPrimaryRunnerKey(null); | |
| setSecondaryRunnerKey(null); | |
| setSecondaryPreloadKey(null); | |
| setCurrentTrack(track); | |
| setIsLoading(false); | |
| }; | |
| const handleQueue = (queue: Track[]) => { | |
| setQueue(prevQueue => [...prevQueue, ...queue]); | |
| }; | |
| const handleQueueNext = (queue: Track[]) => { | |
| setQueue(prevQueue => [...queue, ...prevQueue]); | |
| }; | |
| const handleNext = ({isPrimary}: {isPrimary: boolean}) => { | |
| setIsPlayingFromQueue(true); | |
| setIndex(prevIndex => { | |
| if (prevIndex < queue.length - 1) { | |
| return prevIndex + 1; | |
| } else { | |
| handleStop(); | |
| return prevIndex; | |
| } | |
| }); | |
| setIsPrimaryPlayer(isPrimary); | |
| }; | |
| const handleShuffleOptions = () => { | |
| setIsShuffle(!isShuffle); | |
| }; | |
| const handlePlayPrevious = () => { | |
| alert('coming soon'); | |
| }; | |
| const handlePlayNext = () => { | |
| setIsLoading(true); | |
| if (currentTrack && !queue.length) { | |
| setIsPlayingFromQueue(false); | |
| setIsPlayingFromQueueLoop(false); | |
| if (bufferIndex + 1 < currentTrack.buffer.length) { | |
| // not at end of buffer | |
| handlePlayNow({ | |
| track: currentTrack.buffer[bufferIndex + 1], | |
| buffer: currentTrack.buffer, | |
| trace: { | |
| source: `juk:11:${ | |
| currentTrack.buffer[bufferIndex + 1].trak.youtube.url.split( | |
| '=', | |
| )[1] | |
| }`, | |
| type: 'track', | |
| }, | |
| }); | |
| setBufferIndex(bufferIndex + 1); | |
| } else { | |
| // buffer end | |
| if (repeatOptions == 'repeat') { | |
| setBufferIndex(-1); | |
| handlePlayNow({ | |
| track: currentTrack.buffer[0], | |
| buffer: currentTrack.buffer, | |
| trace: { | |
| source: `juk:11:${ | |
| currentTrack.track.trak.youtube.url.split('=')[1] | |
| }`, | |
| type: 'track', | |
| }, | |
| }); | |
| } else { | |
| handleStop(); | |
| } | |
| } | |
| } else if (currentTrack && queue.length) { | |
| if (index + 1 < queue.length) { | |
| setIsPlayingFromQueue(true); | |
| setIsPlayingFromQueueLoop(false); | |
| // not at end of queue | |
| handlePlayNow({ | |
| track: queue[index + 1].track, | |
| buffer: currentTrack.buffer, | |
| trace: { | |
| source: `juk:11:${ | |
| currentTrack.track.trak.youtube.url.split('=')[1] | |
| }`, | |
| type: 'track', | |
| }, | |
| }); | |
| setIndex(index + 1); | |
| } else { | |
| // index end | |
| if (bufferIndex + 1 < currentTrack.buffer.length) { | |
| setIsPlayingFromQueue(false); | |
| setIsPlayingFromQueueLoop(false); | |
| // if not at buffer end | |
| handlePlayNow({ | |
| track: currentTrack.buffer[bufferIndex + 1], | |
| buffer: queue[index].buffer, | |
| trace: { | |
| source: `juk:11:${ | |
| currentTrack.track.trak.youtube.url.split('=')[1] | |
| }`, | |
| type: 'track', | |
| }, | |
| }); | |
| setBufferIndex(bufferIndex + 1); | |
| } else { | |
| // buffer end | |
| if (repeatOptions == 'repeat') { | |
| setIsPlayingFromQueue(true); | |
| setIsPlayingFromQueueLoop(true); | |
| setBufferIndex(-1); | |
| handlePlayNow({ | |
| track: currentTrack.buffer[0], | |
| buffer: currentTrack.buffer, | |
| trace: { | |
| source: `juk:11:${ | |
| currentTrack.track.trak.youtube.url.split('=')[1] | |
| }`, | |
| type: 'track', | |
| }, | |
| }); | |
| } else { | |
| handleStop(); | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| const handleReleaseKeys = () => { | |
| if (isPrimaryPlayer && isSecondaryWebViewLoaded && secondaryPreloadKey) { | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPlayerInitialized(false); | |
| setSecondaryPreloadKey(null); | |
| // setTRXUrl1(null); | |
| } else if ( | |
| !isPrimaryPlayer && | |
| isPrimaryWebViewLoaded && | |
| primaryPreloadKey | |
| ) { | |
| // setTRXUrl2(null); | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPlayerInitialized(false); | |
| setPrimaryPreloadKey(null); | |
| } | |
| }; | |
| const handleSeek = (seekTo: number) => { | |
| if (isPrimaryPlayer) { | |
| buffer1.current.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = ${seekTo}; | |
| } | |
| true; | |
| `); | |
| } else { | |
| buffer2.current.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = ${seekTo}; | |
| } | |
| true; | |
| `); | |
| } | |
| }; | |
| const handleRepeatOptions = () => { | |
| if (repeatOptions == 'off') { | |
| setRepeatOptions('repeat'); | |
| } else if (repeatOptions == 'repeat') { | |
| setRepeatOptions('repeat-once'); | |
| } else setRepeatOptions('off'); | |
| }; | |
| return ( | |
| <PlayerContext.Provider | |
| value={{ | |
| index, | |
| bufferIndex, | |
| setRadioIndex, | |
| queue, | |
| buffer1, | |
| buffer2, | |
| isPaused, | |
| handleStop, | |
| handleNext, | |
| handleQueue, | |
| handlePause, | |
| currentTrack, | |
| handlePlayNow, | |
| handleQueueNext, | |
| isPrimaryPlayer, | |
| handlePlayNext, | |
| handleSeek, | |
| handleBuffer, | |
| handleShuffleOptions, | |
| isShuffle, | |
| setIsPaused, | |
| radio, | |
| radioIndex, | |
| isPrimaryWebViewLoaded, | |
| setPrimaryWebViewLoaded, | |
| isSecondaryWebViewLoaded, | |
| setSecondaryWebViewLoaded, | |
| isPrimaryPlayerInitialized, | |
| setPrimaryPlayerInitialized, | |
| isSecondaryPlayerInitialized, | |
| setSecondaryPlayerInitialized, | |
| setRadio, | |
| handlePlayPrevious, | |
| setBufferIndex, | |
| primaryRunnerKey, | |
| setPrimaryRunnerKey, | |
| secondaryRunnerKey, | |
| setSecondaryRunnerKey, | |
| primaryPreloadKey, | |
| setPrimaryPreloadKey, | |
| secondaryPreloadKey, | |
| setSecondaryPreloadKey, | |
| repeatOptions, | |
| handleRepeatOptions, | |
| setRepeatOptions, | |
| isPlayingFromQueue, | |
| setIsPlayingFromQueue, | |
| setIndex, | |
| isPlayingFromQueueLoop, | |
| setIsPlayingFromQueueLoop, | |
| setQueue, | |
| initializingPiP, | |
| setInitializingPiP, | |
| handleForcePiP, | |
| handleReleaseKeys, | |
| isLive, | |
| setIsLive, | |
| }}> | |
| {children} | |
| </PlayerContext.Provider> | |
| ); | |
| }; | |
| export interface Progress { | |
| duration: number; | |
| currentTime: number; | |
| percent: number; | |
| } | |
| interface ProgressContextType { | |
| modalRef: React.RefObject<Modalize>; | |
| progress: Progress | null; | |
| setProgress: React.Dispatch<React.SetStateAction<Progress | null>>; | |
| progressSnapshot: () => Progress | null; | |
| } | |
| export const ProgressContext = createContext<ProgressContextType>({ | |
| progress: null, | |
| setProgress: () => {}, | |
| modalRef: {current: null}, // migrate away | |
| progressSnapshot: () => null, | |
| }); | |
| export const ProgressProvider = ({children}: {children: React.ReactChild}) => { | |
| const [progress, setProgress] = useState<Progress | null>(null); | |
| const modalRef = useRef(null); | |
| const progressSnapshot = () => { | |
| return progress; | |
| }; | |
| return ( | |
| <ProgressContext.Provider | |
| value={{ | |
| progress, | |
| setProgress, | |
| modalRef, | |
| progressSnapshot, | |
| }}> | |
| {children} | |
| </ProgressContext.Provider> | |
| ); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {useContext} from 'react'; | |
| import WebView from 'react-native-webview'; | |
| import {StyleSheet, View} from 'react-native'; | |
| import {useJukePod} from './useJukePod'; | |
| import {PlayerContext, Track} from './JukePodProvider'; | |
| import {Dimensions} from 'react-native'; | |
| export const JukePodSDK = ({ | |
| handleStream, | |
| }: { | |
| handleStream: ({ | |
| TRACK, | |
| IS_LIVE, | |
| PROTOCOL_ID, | |
| }: { | |
| TRACK: Track; | |
| IS_LIVE: boolean; | |
| PROTOCOL_ID: string; | |
| }) => void; | |
| }) => { | |
| const { | |
| picture1, | |
| picture2, | |
| handleMessage, | |
| isPrimaryPlayer, | |
| fetchVideoTimeJS, | |
| isPrimaryPlayerInitialized, | |
| isSecondaryPlayerInitialized, | |
| } = useJukePod({handleStream}); | |
| const context = useContext(PlayerContext); | |
| const {buffer1, buffer2} = context; | |
| return ( | |
| <View> | |
| <WebView | |
| ref={buffer1} | |
| style={styles.container} | |
| source={ | |
| { | |
| uri: isPrimaryPlayerInitialized ? picture1 : null, | |
| } as {uri: string} | |
| } | |
| onMessage={event => handleMessage(event, true)} | |
| injectedJavaScript={fetchVideoTimeJS(isPrimaryPlayer)} | |
| allowsInlineMediaPlayback={true} | |
| /> | |
| <WebView | |
| ref={buffer2} | |
| style={styles.container} | |
| source={ | |
| { | |
| uri: isSecondaryPlayerInitialized ? picture2 : null, | |
| } as {uri: string} | |
| } | |
| onMessage={event => handleMessage(event, false)} | |
| injectedJavaScript={fetchVideoTimeJS(!isPrimaryPlayer)} | |
| allowsInlineMediaPlayback={true} | |
| /> | |
| </View> | |
| ); | |
| }; | |
| const styles = StyleSheet.create({ | |
| container: { | |
| height: 240, | |
| width: Dimensions.get('screen').width, | |
| margin: 10, | |
| }, | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import {useContext, useEffect, useState, useRef, act} from 'react'; | |
| import { | |
| PlayerContext, | |
| Progress, | |
| ProgressContext, | |
| Track, | |
| } from './JukePodProvider'; | |
| import {useAppState} from '@react-native-community/hooks'; | |
| import {WebViewMessageEvent} from 'react-native-webview'; | |
| import PushNotificationIOS from '@react-native-community/push-notification-ios'; | |
| import {useEffectAsync} from '../hooks/useEffectAsync'; | |
| import {useAsyncStorage} from '../hooks/useAsyncStorage'; | |
| import {asyncStorageKeys} from '../core/asyncStorageKeys'; | |
| import uuid from 'react-native-uuid'; | |
| import {nowPlaying} from '../app/utils/nowPlaying'; | |
| import {TRAKLIST} from '../app/utils/meta'; | |
| import {API} from '../api'; | |
| export const useJukePod = ({ | |
| handleStream, | |
| }: { | |
| handleStream: ({ | |
| TRACK, | |
| IS_LIVE, | |
| PROTOCOL_ID, | |
| }: { | |
| TRACK: Track; | |
| IS_LIVE: boolean; | |
| PROTOCOL_ID: string; | |
| }) => void; | |
| }) => { | |
| const [hasStreamed, setHasStreamed] = useState(false); | |
| const {handleStoreData} = useAsyncStorage(); | |
| const intervalRef = useRef<any>(null); | |
| const { | |
| queue, | |
| index, | |
| buffer1, | |
| buffer2, | |
| isPaused, | |
| handleStop, | |
| handleNext, | |
| handleSeek, | |
| setIsPaused, | |
| bufferIndex, | |
| currentTrack, | |
| handleBuffer, | |
| repeatOptions, | |
| isPrimaryPlayer, | |
| setRepeatOptions, | |
| isPrimaryWebViewLoaded, | |
| setPrimaryWebViewLoaded, | |
| isSecondaryWebViewLoaded, | |
| setSecondaryWebViewLoaded, | |
| setPrimaryPlayerInitialized, | |
| setSecondaryPlayerInitialized, | |
| isPrimaryPlayerInitialized, | |
| isSecondaryPlayerInitialized, | |
| primaryRunnerKey, | |
| setPrimaryRunnerKey, | |
| secondaryRunnerKey, | |
| setSecondaryRunnerKey, | |
| primaryPreloadKey, | |
| setPrimaryPreloadKey, | |
| secondaryPreloadKey, | |
| setSecondaryPreloadKey, | |
| handlePlayNext, | |
| initializingPiP, | |
| setInitializingPiP, | |
| isLive, | |
| } = useContext(PlayerContext); | |
| const {setProgress} = useContext(ProgressContext); | |
| const appState = useAppState(); | |
| const [isPiPEnabled, setIsPiPEnabled] = useState(false); | |
| const [isProcessing, setIsProcessing] = useState(false); | |
| const [trxUrl1, setTRXUrl1] = useState<string | null>(null); | |
| const [trxUrl2, setTRXUrl2] = useState<string | null>(null); | |
| const [userActionPause, setUserActionPause] = useState<boolean>(false); | |
| const {track, trace, protocolId} = nowPlaying(); | |
| const startErrorCheckInterval = (isRunner: boolean) => { | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| } | |
| const isRunnerBuffer = isPrimaryPlayer ? buffer1.current : buffer2.current; | |
| const isPreloadBuffer = isPrimaryPlayer ? buffer2.current : buffer1.current; | |
| intervalRef.current = setInterval(() => { | |
| const ref = isRunner ? isRunnerBuffer : isPreloadBuffer; | |
| ref?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo && window.trakStarVideo.readyState < 3) { | |
| // Video is not playable, trigger error | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoError', | |
| data: '${isRunner}' | |
| })); | |
| } | |
| `); | |
| }, 4000); // Check every 2 seconds | |
| }; | |
| const inactiveTime = useRef(0); | |
| const backgroundTime = useRef(0); | |
| const previousAppState = useRef(appState); | |
| useEffect(() => { | |
| if (appState === 'inactive') { | |
| inactiveTime.current = new Date(); | |
| } | |
| if (appState === 'background') { | |
| backgroundTime.current = new Date(); | |
| const timeDiff = backgroundTime.current - inactiveTime.current; | |
| console.log( | |
| `Time difference between inactive and background: ${timeDiff}ms`, | |
| ); | |
| if (TRAKLIST) { | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: 'traklist-bk', | |
| title: 'Please return to the Stage Manager', | |
| body: 'Stage Manager allows you to multitask effectively. Please return to avoid issues.', | |
| }); | |
| } else if (timeDiff < 10 && !isPaused) { | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: '1', | |
| title: 'TrakStar™ Music: Screen Locked', | |
| body: 'Playback may pause when your screen is locked. To avoid this, minimise the app before locking the screen.', | |
| }); | |
| if (isPrimaryPlayer) { | |
| buffer1.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| } | |
| true; | |
| `); | |
| } else { | |
| buffer2.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| } | |
| true; | |
| `); | |
| } | |
| } else if (timeDiff > 100 && !isPaused) { | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: '0', | |
| title: 'TrakStar™ Music: Background Play', | |
| body: initializingPiP | |
| ? 'Please allow the Picture in Picture module to load before attempting background play.' | |
| : 'Playback may be interrupted by other media. Reopen the app to resume if needed.', | |
| }); | |
| } | |
| inactiveTime.current = 0; | |
| backgroundTime.current = 0; | |
| } | |
| if (appState === 'active') { | |
| inactiveTime.current = 0; | |
| backgroundTime.current = 0; | |
| } | |
| }, [appState]); | |
| useEffect(() => { | |
| if (TRAKLIST) return; | |
| if (appState === 'active' || appState === 'background') { | |
| if (isPrimaryPlayer) { | |
| if (appState === 'background' && !isPiPEnabled) { | |
| buffer1.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| }; | |
| true; | |
| `); | |
| } else if ( | |
| appState === 'active' && | |
| previousAppState.current == 'background' && | |
| !isPiPEnabled && | |
| currentTrack | |
| ) { | |
| setInitializingPiP(true); | |
| } | |
| setTRXUrl2(null); | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPlayerInitialized(false); | |
| setSecondaryPreloadKey(null); | |
| } else { | |
| if (appState === 'background' && !isPiPEnabled) { | |
| buffer2.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| }; | |
| true; | |
| `); | |
| } else if ( | |
| appState === 'active' && | |
| previousAppState.current == 'background' && | |
| !isPiPEnabled && | |
| currentTrack | |
| ) { | |
| setInitializingPiP(true); | |
| } | |
| setTRXUrl1(null); | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPlayerInitialized(false); | |
| setPrimaryPreloadKey(null); | |
| } | |
| if (isPrimaryPlayerInitialized || isSecondaryPlayerInitialized) { | |
| const ref = isPrimaryPlayer ? buffer1.current : buffer2.current; | |
| setTimeout(() => { | |
| ref?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| } else { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'No video element found.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| } | |
| true; | |
| `); | |
| }, 500); | |
| } | |
| } | |
| previousAppState.current = appState; | |
| }, [appState]); | |
| useEffectAsync(async () => { | |
| setHasStreamed(false); | |
| if (!currentTrack || !trace) { | |
| return; | |
| } | |
| setInitializingPiP(true); | |
| if (isPrimaryPlayer) { | |
| setTRXUrl2(null); | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPlayerInitialized(false); | |
| setPrimaryPlayerInitialized(true); | |
| } else { | |
| setTRXUrl1(null); | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPlayerInitialized(false); | |
| setSecondaryPlayerInitialized(true); | |
| } | |
| const key = uuid.v4(); | |
| if (isPrimaryPlayer) { | |
| setPrimaryRunnerKey(String(key)); | |
| const id = `${ | |
| currentTrack.track.trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl1(url); | |
| } else { | |
| setSecondaryRunnerKey(String(key)); | |
| const id = `${ | |
| currentTrack.track.trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl2(url); | |
| } | |
| await handleStoreData(asyncStorageKeys.CURRENT_TRACK, currentTrack); | |
| setTimeout(() => startErrorCheckInterval(true), 1500); | |
| }, [currentTrack]); | |
| useEffectAsync(async () => { | |
| if (isPrimaryPlayerInitialized) setSecondaryPlayerInitialized(true); | |
| if (isSecondaryPlayerInitialized) setPrimaryPlayerInitialized(true); | |
| const storedTrack: Track = { | |
| track: track!, | |
| buffer: currentTrack?.buffer ?? [], | |
| trace: currentTrack!.trace ?? { | |
| source: '', | |
| type: '', | |
| }, | |
| }; | |
| // alert(currentTrack.trace.source); | |
| await handleStoreData( | |
| asyncStorageKeys.CURRENT_TRACK, | |
| currentTrack ? storedTrack : null, | |
| ); | |
| if (!isPrimaryPlayer && appState === 'active') { | |
| if (TRAKLIST) { | |
| buffer2.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } else { | |
| buffer2.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } | |
| } else if (!isPrimaryPlayer && appState !== 'active') { | |
| setIsPiPEnabled(false); | |
| buffer2.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } else if (isPrimaryPlayer && appState === 'active') { | |
| if (TRAKLIST) { | |
| buffer1.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } else { | |
| buffer1.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } | |
| } else if (isPrimaryPlayer && appState !== 'active') { | |
| setIsPiPEnabled(false); | |
| buffer1.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } | |
| }, [isPrimaryPlayer]); | |
| useEffect(() => { | |
| if (isPrimaryPlayer) { | |
| setTRXUrl2(null); | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPlayerInitialized(false); | |
| setSecondaryPreloadKey(null); | |
| setSecondaryRunnerKey(null); | |
| } else { | |
| setTRXUrl1(null); | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPlayerInitialized(false); | |
| setPrimaryPreloadKey(null); | |
| setPrimaryRunnerKey(null); | |
| } | |
| }, [queue]); | |
| useEffect(() => { | |
| if (repeatOptions == 'repeat') { | |
| if (isPrimaryPlayer) { | |
| setSecondaryPreloadKey(null); | |
| setSecondaryRunnerKey(null); | |
| } else { | |
| setPrimaryPreloadKey(null); | |
| setPrimaryRunnerKey(null); | |
| } | |
| } | |
| }, [repeatOptions]); | |
| const fetchVideoTimeJS = (active: boolean) => ` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| var urlParams = new URLSearchParams(window.location.search); | |
| var requestId = urlParams.get('key'); | |
| window.trakStarVideo.addEventListener('canplay', () => { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoReady', | |
| data: { | |
| ready: true, | |
| requestId: requestId // Send the requestId back in the message | |
| } | |
| })); | |
| clearInterval(window.errorCheckInterval); // Stop the error checking | |
| }); | |
| window.trakStarVideo.addEventListener('enterpictureinpicture', function() { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'enteredPiP', | |
| data: 'Picture-in-Picture mode entered' | |
| })); | |
| }); | |
| window.trakStarVideo.addEventListener('leavepictureinpicture', function() { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'exitedPiP', | |
| data: 'Picture-in-Picture mode exited' | |
| })); | |
| }); | |
| window.trakStarVideo.addEventListener('ended', function() { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoEnded', | |
| data: 100 | |
| })); | |
| }); | |
| window.trakStarVideo.addEventListener('pause', function() { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoPaused', | |
| data: true | |
| })); | |
| }); | |
| window.trakStarVideo.addEventListener('play', function() { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoPlaying', | |
| data: true | |
| })); | |
| }); | |
| let lastUpdateTime = 0; | |
| window.trakStarVideo.addEventListener('timeupdate', () => { | |
| const now = Date.now(); | |
| // Throttle updates to once per second | |
| if (now - lastUpdateTime > 750) { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoCurrentTime', | |
| data: { | |
| currentTime: window.trakStarVideo.currentTime, | |
| duration: window.trakStarVideo.duration, | |
| percent: (window.trakStarVideo.currentTime / window.trakStarVideo.duration) | |
| } | |
| })); | |
| lastUpdateTime = now; | |
| } | |
| }); | |
| window.trakStarVideo.addEventListener('error', function() { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'videoError', | |
| data: 'An error occurred while trying to load the video.' | |
| })); | |
| }); | |
| window.trakStarVideo.preload = 'auto' | |
| window.trakStarVideo.muted = ${!active}; | |
| if (${active}) { | |
| window.trakStarVideo.play(); | |
| } else { | |
| window.trakStarVideo.pause(); | |
| }; | |
| true; | |
| } | |
| `; | |
| const handleMessage = async ( | |
| event: WebViewMessageEvent, | |
| isPrimaryMessage: boolean, | |
| ) => { | |
| const message = JSON.parse(event.nativeEvent.data); | |
| switch (message.eventType) { | |
| case 'videoReady': | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); // Stop error check interval if video is ready | |
| } | |
| const requestId = message.data.requestId; | |
| if (isPrimaryPlayer) { | |
| if (requestId === secondaryPreloadKey) | |
| setSecondaryWebViewLoaded(true); | |
| } else { | |
| if (requestId === primaryPreloadKey) setPrimaryWebViewLoaded(true); | |
| } | |
| if (appState === 'active') { | |
| if (isPrimaryPlayer && isPrimaryMessage) { | |
| buffer1.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| var urlParams = new URLSearchParams(window.location.search); | |
| var requestId = urlParams.get('key'); | |
| if (window.trakStarVideo) { | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| }; | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.', | |
| key: requestId | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| } | |
| true; | |
| `); | |
| } else if (!isPrimaryPlayer && !isPrimaryMessage) { | |
| buffer2.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| }; | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| } | |
| true; | |
| `); | |
| } | |
| } | |
| break; | |
| case 'enteredPiP': | |
| setIsPiPEnabled(true); | |
| break; | |
| case 'exitedPiP': | |
| setIsPiPEnabled(false); | |
| if (isPrimaryPlayer && appState === 'active') { | |
| buffer1.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| // Use requestAnimationFrame to trigger pause if userActionPause is true | |
| if (${userActionPause}) { | |
| requestAnimationFrame(() => window.trakStarVideo.pause()); | |
| } | |
| } | |
| true; | |
| `); | |
| } else if (!isPrimaryPlayer && appState === 'active') { | |
| buffer2.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.requestPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiated successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'enablePiP', | |
| data: 'PiP initiation failed: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| // Use requestAnimationFrame to trigger pause if userActionPause is true | |
| if (${userActionPause}) { | |
| requestAnimationFrame(() => window.trakStarVideo.pause()); | |
| } | |
| } | |
| true; | |
| `); | |
| } else if (isPrimaryPlayer && appState === 'background') { | |
| buffer1.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| // Use requestAnimationFrame to trigger pause if userActionPause is true | |
| if (${userActionPause}) { | |
| requestAnimationFrame(() => window.trakStarVideo.pause()); | |
| } | |
| } | |
| true; | |
| `); | |
| } else if (!isPrimaryPlayer && appState === 'background') { | |
| buffer2.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| window.trakStarVideo.muted = false; | |
| window.trakStarVideo.play(); | |
| // Use requestAnimationFrame to trigger pause if userActionPause is true | |
| if (${userActionPause}) { | |
| requestAnimationFrame(() => window.trakStarVideo.pause()); | |
| } | |
| } | |
| true; | |
| `); | |
| } | |
| break; | |
| case 'remotePause': | |
| setUserActionPause(true); | |
| if (isPrimaryPlayer) { | |
| buffer1.current?.injectJavaScript(` | |
| if (document.pictureInPictureElement) { | |
| document.exitPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'exitPiP', | |
| data: 'Exited PiP successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'exitPiP', | |
| data: 'Failed to exit PiP: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| } else { | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.pause(); | |
| } | |
| }; | |
| true; | |
| `); | |
| } else { | |
| buffer2.current?.injectJavaScript(` | |
| if (document.pictureInPictureElement) { | |
| document.exitPictureInPicture().then(() => { | |
| const message = { | |
| eventType: 'exitPiP', | |
| data: 'Exited PiP successfully.' | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }).catch(error => { | |
| const message = { | |
| eventType: 'exitPiP', | |
| data: 'Failed to exit PiP: ' + error.message | |
| }; | |
| window.ReactNativeWebView.postMessage(JSON.stringify(message)); | |
| }); | |
| } else { | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.pause(); | |
| } | |
| }; | |
| true; | |
| `); | |
| } | |
| setTimeout(() => { | |
| setUserActionPause(false); | |
| }, 3000); | |
| break; | |
| case 'videoPaused': | |
| if (isPrimaryPlayer && !isPrimaryMessage) return; | |
| if (!isPrimaryPlayer && isPrimaryMessage) return; | |
| if (TRAKLIST && appState == 'background') return; | |
| // Start the requestAnimationFrame loop | |
| if (!userActionPause && appState == 'background') { | |
| if (isPrimaryPlayer) { | |
| buffer1.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } else { | |
| buffer2.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| } | |
| true; | |
| `); | |
| } | |
| } | |
| setIsPaused(true); | |
| break; | |
| case 'remotePlay': | |
| if (isPrimaryPlayer) { | |
| buffer1.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| }; | |
| true; | |
| `); | |
| } else { | |
| buffer2.current?.injectJavaScript(` | |
| if (window.trakStarVideo) { | |
| window.trakStarVideo.play(); | |
| window.trakStarVideo.currentTime = window.trakStarVideo.currentTime; | |
| }; | |
| true; | |
| `); | |
| } | |
| setIsPaused(false); | |
| break; | |
| case 'videoPlaying': | |
| if (isPrimaryPlayer && !isPrimaryMessage) return; | |
| if (!isPrimaryPlayer && isPrimaryMessage) return; | |
| setIsPaused(false); | |
| break; | |
| case 'videoError': | |
| if (repeatOptions == 'repeat-once') return; | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current); | |
| } | |
| if (message.data == 'true') { | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: '0', | |
| title: 'TrakStar™ Music: Unavailable Video', | |
| body: 'Moving onto the next track.', | |
| }); | |
| handlePlayNext(); | |
| } else { | |
| if (!queue.length && currentTrack) { | |
| const bufferCopy = [...currentTrack.buffer]; // Create a copy of the buffer array | |
| bufferCopy.splice(bufferIndex == -1 ? 1 : bufferIndex + 1, 1); // Modify the copy | |
| currentTrack.buffer = bufferCopy; // | |
| } else if (index + 1 < queue.length) { | |
| queue.splice(index + 1, 1); | |
| } else if (index + 1 >= queue.length) { | |
| const bufferCopy = [...currentTrack.buffer]; // Create a copy of the buffer array | |
| bufferCopy.splice(bufferIndex == -1 ? 1 : bufferIndex + 1, 1); // Modify the copy | |
| currentTrack.buffer = bufferCopy; // | |
| } | |
| if (isPrimaryPlayer) { | |
| setTRXUrl2(null); | |
| setSecondaryPreloadKey(null); | |
| } else { | |
| setTRXUrl1(null); | |
| setPrimaryPreloadKey(null); | |
| } | |
| } | |
| break; | |
| case 'enablePiP': | |
| const data = message.data; | |
| if (data == 'PiP initiated successfully.') { | |
| setInitializingPiP(false); | |
| if (appState == 'active') { | |
| if (isPrimaryPlayer) { | |
| if (message.key === primaryRunnerKey) | |
| setPrimaryWebViewLoaded(true); | |
| } else { | |
| if (message.key === secondaryRunnerKey) | |
| setSecondaryWebViewLoaded(true); | |
| } | |
| } | |
| } | |
| break; | |
| case 'videoCurrentTime': | |
| const progress: Progress = message.data; | |
| const ref = isPrimaryPlayer ? buffer1 : buffer2; | |
| ref.current?.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| if (window.trakStarVideo && navigator.mediaSession) { | |
| navigator.mediaSession.metadata = new MediaMetadata({ | |
| title: "${track?.trak.title ?? 'TrakStar™ Music'}", | |
| artist: "${track?.trak.artist ?? 'JUKERSTONE LTD.'}", | |
| album: "${Date.now()}", // Customize or fetch this as needed | |
| artwork: [ | |
| { src: "${ | |
| track?.trak.thumbnail ?? | |
| 'https://firebasestorage.googleapis.com/v0/b/traklist-7b38a.appspot.com/o/sonar.png?alt=media&token=f81e029a-3f3c-481d-997c-715815bc7598' | |
| }", sizes: "512x512", type: "image/png" } | |
| ] | |
| }); | |
| // Remove the existing handlers if any | |
| navigator.mediaSession.setActionHandler('seekbackward', null); | |
| navigator.mediaSession.setActionHandler('seekforward', null); | |
| // Define the previous track action | |
| navigator.mediaSession.setActionHandler('previoustrack', () => { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'previousTrack', | |
| })); | |
| }); | |
| // Define the next track action | |
| navigator.mediaSession.setActionHandler('nexttrack', () => { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'nextTrack', | |
| })); | |
| }); | |
| // Handle the pause action | |
| navigator.mediaSession.setActionHandler('pause', () => { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'remotePause', | |
| })); | |
| }); | |
| // Handle the play action | |
| navigator.mediaSession.setActionHandler('play', () => { | |
| window.ReactNativeWebView.postMessage(JSON.stringify({ | |
| eventType: 'remotePlay', | |
| })); | |
| }); | |
| } | |
| `); | |
| setProgress(progress); // Update progress every 1 seconds | |
| setIsPaused(false); | |
| if (30 <= progress.currentTime && !hasStreamed && track && trace) { | |
| setHasStreamed(true); | |
| handleStream({ | |
| TRACK: {track, trace, buffer: []}, | |
| IS_LIVE: isLive, | |
| PROTOCOL_ID: protocolId ? protocolId : '', | |
| }); | |
| } | |
| if ( | |
| repeatOptions == 'repeat-once' && | |
| progress.currentTime > progress.duration - 2 | |
| ) { | |
| setHasStreamed(false); | |
| if (isPrimaryPlayer) { | |
| buffer1.current.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| window.trakStarVideo.currentTime = 0; | |
| true; | |
| `); | |
| } else { | |
| buffer2.current.injectJavaScript(` | |
| if (!window.trakStarVideo) { | |
| window.trakStarVideo = document.getElementsByTagName('video')[0]; | |
| } | |
| window.trakStarVideo.currentTime = 0; | |
| true; | |
| `); | |
| } | |
| } | |
| const key = uuid.v4(); | |
| if ( | |
| isPrimaryPlayer && | |
| !isSecondaryWebViewLoaded && | |
| !secondaryPreloadKey && | |
| queue.length && | |
| index + 1 < queue.length // Ensure index + 1 is within bounds | |
| ) { | |
| setSecondaryPlayerInitialized(true); | |
| setSecondaryPreloadKey(String(key)); | |
| const id = `${ | |
| queue[index + 1].track.trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl2(url); | |
| startErrorCheckInterval(false); | |
| } else if ( | |
| !isPrimaryPlayer && | |
| !isPrimaryWebViewLoaded && | |
| !primaryPreloadKey && | |
| queue.length && | |
| index + 1 < queue.length // Ensure index + 1 is within bounds | |
| ) { | |
| setPrimaryPlayerInitialized(true); | |
| setPrimaryPreloadKey(String(key)); | |
| const id = `${ | |
| queue[index + 1].track.trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl1(url); | |
| startErrorCheckInterval(false); | |
| } else if ( | |
| isPrimaryPlayer && | |
| !isSecondaryWebViewLoaded && | |
| !secondaryPreloadKey && | |
| queue.length && | |
| index + 1 == queue.length | |
| ) { | |
| if ( | |
| queue[index].buffer.length && | |
| bufferIndex + 1 < queue[index].buffer.length | |
| ) { | |
| setSecondaryPlayerInitialized(true); | |
| setSecondaryPreloadKey(String(key)); | |
| const id = `${ | |
| queue[index].buffer[ | |
| bufferIndex == -1 ? 1 : bufferIndex + 1 | |
| ].trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl2(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setSecondaryPlayerInitialized(true); | |
| setSecondaryPreloadKey(String(key)); | |
| const id = `${ | |
| queue[index].buffer[0].trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl2(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| setTRXUrl2(null); | |
| setSecondaryPlayerInitialized(false); | |
| } | |
| } | |
| } else if ( | |
| !isPrimaryPlayer && | |
| !isPrimaryWebViewLoaded && | |
| !primaryPreloadKey && | |
| queue.length && | |
| index + 1 == queue.length | |
| ) { | |
| if ( | |
| queue[index].buffer.length && | |
| bufferIndex + 1 < queue[index].buffer.length | |
| ) { | |
| setPrimaryPlayerInitialized(true); | |
| setPrimaryPreloadKey(String(key)); | |
| const id = `${ | |
| queue[index].buffer[ | |
| bufferIndex == -1 ? 1 : bufferIndex + 1 | |
| ].trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl1(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setPrimaryPlayerInitialized(true); | |
| setPrimaryPreloadKey(String(key)); | |
| const id = `${ | |
| queue[index].buffer[0].trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl1(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| setTRXUrl1(null); | |
| setPrimaryPlayerInitialized(false); | |
| } | |
| } | |
| } else if ( | |
| isPrimaryPlayer && | |
| !isSecondaryWebViewLoaded && | |
| !secondaryPreloadKey && | |
| !queue.length && | |
| currentTrack | |
| ) { | |
| if ( | |
| currentTrack.buffer.length && | |
| bufferIndex + 1 < currentTrack.buffer.length | |
| ) { | |
| setSecondaryPlayerInitialized(true); | |
| setSecondaryPreloadKey(String(key)); | |
| const id = `${ | |
| currentTrack.buffer[bufferIndex + 1].trak.youtube.url.split( | |
| '=', | |
| )[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl2(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setSecondaryPlayerInitialized(true); | |
| setSecondaryPreloadKey(String(key)); | |
| const id = `${ | |
| currentTrack.buffer[0].trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl2(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| setTRXUrl2(null); | |
| setSecondaryPlayerInitialized(false); | |
| } | |
| } | |
| } else if ( | |
| !isPrimaryPlayer && | |
| !isPrimaryWebViewLoaded && | |
| !primaryPreloadKey && | |
| !queue.length && | |
| currentTrack | |
| ) { | |
| if ( | |
| currentTrack.buffer.length && | |
| bufferIndex + 1 < currentTrack.buffer.length | |
| ) { | |
| setPrimaryPlayerInitialized(true); | |
| setPrimaryPreloadKey(String(key)); | |
| const id = `${ | |
| currentTrack.buffer[bufferIndex + 1].trak.youtube.url.split( | |
| '=', | |
| )[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl1(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| if (repeatOptions == 'repeat') { | |
| setPrimaryPlayerInitialized(true); | |
| setPrimaryPreloadKey(String(key)); | |
| const id = `${ | |
| currentTrack.buffer[0].trak.youtube.url.split('=')[1] | |
| }?playsinline=1&fs=0&key=${key}`; | |
| const url = API({id}).jukerstone.security.youtube; | |
| setTRXUrl1(url); | |
| startErrorCheckInterval(false); | |
| } else { | |
| setTRXUrl1(null); | |
| setPrimaryPlayerInitialized(false); | |
| } | |
| } | |
| } | |
| break; | |
| case 'videoEnded': | |
| setHasStreamed(false); | |
| if (isPrimaryPlayer) { | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPreloadKey(null); | |
| if (index + 1 < queue.length) { | |
| handleNext({isPrimary: false}); | |
| } else { | |
| handleBuffer({isPrimary: false}); | |
| } | |
| } else if (!isPrimaryPlayer) { | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPreloadKey(null); | |
| if (index + 1 < queue.length) { | |
| handleNext({isPrimary: true}); | |
| } else { | |
| handleBuffer({isPrimary: true}); | |
| } | |
| } | |
| break; | |
| case 'nextTrack': | |
| if (isProcessing) { | |
| return; // Ignore if already processing the next track | |
| } | |
| setIsProcessing(true); | |
| if ( | |
| isPrimaryPlayer && | |
| isSecondaryWebViewLoaded && | |
| secondaryPreloadKey | |
| ) { | |
| setTRXUrl1(null); | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPlayerInitialized(false); | |
| setPrimaryPreloadKey(null); | |
| if (index + 1 < queue.length) { | |
| handleNext({isPrimary: false}); | |
| } else { | |
| handleBuffer({isPrimary: false}); | |
| } | |
| if (repeatOptions == 'repeat-once') setRepeatOptions('off'); | |
| } else if ( | |
| !isPrimaryPlayer && | |
| isPrimaryWebViewLoaded && | |
| primaryPreloadKey | |
| ) { | |
| setTRXUrl2(null); | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPlayerInitialized(false); | |
| setSecondaryPreloadKey(null); | |
| if (index + 1 < queue.length) { | |
| handleNext({isPrimary: true}); | |
| } else { | |
| handleBuffer({isPrimary: true}); | |
| } | |
| if (repeatOptions == 'repeat-once') setRepeatOptions('off'); | |
| } else if ( | |
| (isPrimaryPlayer && !isSecondaryPlayerInitialized) || | |
| (!isPrimaryPlayer && !isPrimaryPlayerInitialized) | |
| ) { | |
| setTRXUrl1(null); | |
| setPrimaryWebViewLoaded(false); | |
| setPrimaryPlayerInitialized(false); | |
| setPrimaryPreloadKey(null); | |
| setTRXUrl2(null); | |
| setSecondaryWebViewLoaded(false); | |
| setSecondaryPlayerInitialized(false); | |
| setSecondaryPreloadKey(null); | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: '1', | |
| title: "That's all folks", | |
| body: 'Come back into the app and queue some new music', | |
| }); | |
| handleStop(); | |
| } else if ( | |
| (isPrimaryPlayer && !isSecondaryWebViewLoaded) || | |
| (!isPrimaryPlayer && !isPrimaryWebViewLoaded) | |
| ) { | |
| PushNotificationIOS.addNotificationRequest({ | |
| id: '1', | |
| title: 'TrakStar™ Music: Buffering', | |
| body: 'Your queue is buffering. Please try again in a few seconds.', | |
| }); | |
| } | |
| setIsProcessing(false); | |
| break; | |
| case 'previousTrack': | |
| handleSeek(0); | |
| break; | |
| default: | |
| console.warn(`Unhandled event type: ${message.eventType}`); | |
| break; | |
| } | |
| }; | |
| return { | |
| picture1: trxUrl1, | |
| picture2: trxUrl2, | |
| handleMessage, | |
| isPrimaryPlayer, | |
| isPrimaryPlayerInitialized, | |
| isSecondaryPlayerInitialized, | |
| fetchVideoTimeJS, | |
| }; | |
| }; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The next Spotify won’t be a music platform — it’ll be a better player for YouTube.
JukePod turns YouTube into a true music app with a dual-buffered RN WebView player, PiP, and background-safe playback.
Most people miss this: YouTube already has the world’s biggest music library.
Official tracks. Remixes. Live cuts. The weird, wonderful versions DSPs ignore. It’s all there — global, instant.
The gap: YouTube doesn’t feel like a music app.
So I built a player that does.
🎧 Introducing JukePod — a new kind of music playback experience
🧠 Why it’s different (architecture, not a hack)
JukePod uses a two-WebView “runner + preload” model keyed per track, with Media Session remote controls and background-safe behavior. There’s no SDK lock-in; everything is orchestrated at the playback surface with strict user-action gating, so it feels like a real music app — fast, smooth, and stable.
🛠️ What’s in this gist
If you’re curious about the React Native stack, the dual-buffer design, or PiP + background quirks, AMA.