React-Example: I'm using OPlayer on my site (10/10 would use it again)
OatsProgramming opened this issue ยท 6 comments
Hey broski, forgot to give it a star and let you know about using it (my b)
There were just a little nuances that I didn't catch the first time that would accidentally cause an infinite render, though that was completely on me (i was sleep deprived lmao) and I fixed the issue.
TL;DR
Pretty much i just changed the way i fetch the data and how i use useEffect: using the source index and then using that index as the dependency.
The reason as to why I'm providing this huge chunk of code is just in case if you were interested in adding it to your docs for people that might get a similar issue as I did. Though, now thinking about it, I've provided quite a lengthy a code chunk and sort of a specific scenario. But, just in case y'know? (I'll provide it at the end since it is kinda chunky)
I didn't get to fully play around with the options yet: there were things I haven't tested (though I'm about to in a bit lol), but so far, absolutely beautiful UI.
Here's my site if you'd like to add it to your docs: mirutv.vercel.app
This was the code before:
export default function HLSPlayer({ sources, poster }: {
sources: EnimeView['sources'],
poster?: string,
}) {
const divRef = useRef<HTMLDivElement>(null)
const [src, setSrc] = useState('')
const [hls, setHls] = useState<PlayerPlugin>()
const [player, setPlayer] = useState<Player>()
const uiOptions: UiConfig = {
menu: [
{
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id
})),
onChange: ({ value }) => {
enimeFetcher({ route: 'source', arg: value })
.then(res => setSrc(res?.url ?? ''))
.catch(err => console.error(err))
}
}
]
}
// Set initial src && lazy load hls
useEffect(() => {
enimeFetcher({ route: 'source', arg: sources[0].id })
.then(res => setSrc(res?.url ?? ''))
.catch(err => console.error(err))
import('@/lib/hlsPlayer/hls')
.then(mod => setHls(mod.default))
}, [])
// Create hlsPlayer
useEffect(() => {
const div = divRef.current
if (!div || !src || !hls) return
// Make sure that it doesnt create more than one player
if (!player) {
// Also avoid infinite loop
setPlayer(_ =>
Player.make(div, { source: { src, poster } })
.use([ui(uiOptions), hls])
.create()
)
}
else {
player.changeSource({ src })
.catch(err => console.error(err))
}
}, [divRef.current, src, hls, player])
return (
<div className={styles['temp']}>
<div ref={divRef} />
</div>
)
}
I wasn't thinking then due to the lack of active brain cells so forgive the bad code here.
Here's it now:
const divRef = useRef<HTMLDivElement>(null)
const [src, setSrc] = useState('')
const [hls, setHls] = useState<PlayerPlugin>()
const [player, setPlayer] = useState<Player>()
const [srcIdx, setSrcIdx] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const uiOptions: UiConfig = {
menu: [
{
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id
})),
onChange: ({ value }) => {
setSrcIdx(sources.findIndex(source => source.id === value))
}
}
]
}
// Set initial src && lazy load hls
useEffect(() => {
enimeFetcher({ route: 'source', arg: sources[0].id })
.then(res => setSrc(res?.url ?? ''))
.catch(err => console.error(err))
import('@/lib/hlsPlayer/hls')
.then(mod => setHls(mod.default))
}, [])
// Create hlsPlayer (only)
useEffect(() => {
const div = divRef.current
if (!div || !hls) return
// Make sure that it doesnt create more than one player
if (!player) {
// Also avoid infinite loop
setPlayer(_ =>
Player.make(div, { source: { src, poster } })
.use([ui(uiOptions), hls])
.create()
)
}
}, [divRef.current, hls, player])
// Fetch src onChange
useEffect(() => {
enimeFetcher({ route: 'source', arg: sources[srcIdx].id })
.then(res => res ? setSrc(res.url) : notFound())
.catch(err => console.log(err))
}, [srcIdx])
useEffect(() => {
// Only care after player has loaded (dont add it as a dependency)
if (!player) return
player.changeSource({ src })
.catch(err => console.log(err))
}, [src])
return (
<div className={`${isLoading && styles['isLoading']}`}>
{/* Transition softly to actual video content */}
{/* To prevent 'flashing' event when video has loaded */}
<div ref={divRef} onLoadedData={() => isLoading && setIsLoading(false)}/>
</div>
)
Out of curiosity, is it possible to change the color of the play button? My guess it's this (again, I haven't got to completely play around with the library since the default UI was already beautiful af) :
OUI({ theme: { primaryColor: '#6668ab' } })
pretty cool, thx.
๐ https://github.com/shiyiya/oplayer#who-use-oplayer
You can refer to the following code @OatsProgramming
function OPlayer({ sources, poster }) {
const playerRef = useRef < Player > null
// #oplayer element has rendered, just create player
useEffect(() => {
if (!playerRef.current) {
playerRef.current = Player.make('#app', { poster })
.use([
OUI(),
// Don't worry, the default is lazy loading
OHls(),
])
.create()
}
return () => playerRef.current?.destory()
}, [])
useEffect(() => {
playerRef.current?.context.ui.menu.unregister('Source')
playerRef.current?.context.ui.menu.register({
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id,
})),
onChange: ({ value }) => {
playerRef.current
.changeSource(
enimeFetcher({ route: 'source', arg: value }).then((res) => ({ src: res.url, poster }))
)
.catch((err) => console.log(err))
},
})
//play default id
playerRef.current
?.changeSource(
enimeFetcher({ route: 'source', arg: source[0].id }).then((res) => ({ src: res.url, poster }))
)
.catch((err) => console.log(err))
}, [sources])
return (
// No loading is required, it is also the default
<div id="oplayer" />
)
}
That is absolutely mad! Thanks for helping me make this better. I had to tweak some things ( there were tiny errors and had to make it look like its part of the code base lol ), but, nonetheless, I've never had anyone redo my code before and have it practically functional without knowing my code base on top of that. As expected from the creator tho lol
There are some parts that are a somewhat confusing to me.
I hope you don't mind about me asking quite a bit of questions: this library is beautiful and handy, but the docs are kinda hard to follow ๐ . I wanna use this to fullest potential y'know? Apologies if I sound like a nerd lol. As of this moment, I only have 2 main questions. You don't have to answer them right away tho! It's a bit lengthy and I apologize for that. So take your time!
Questions:
1) Is type safety yet to be integrated for the context object?
playerRef.current?.context.ui.menu.register({
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id,
})),
onChange: ({ value }) => {
playerRef.current
.changeSource(
enimeFetcher({ route: 'source', arg: value }).then((res) => ({ src: res.url, poster }))
)
.catch((err) => console.log(err))
},
})
Based on the code, playerRef.current?.context.ui.menu.register
, it looks like it takes an object similar to the UiConfig:
const uiOptions: UiConfig = {
menu: [
{
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id
})),
onChange: ({ value }) => {
setSrcIdx(sources.findIndex(source => source.id === value))
}
}
]
}
But when i hover over context.ui.menu.register
on any context property, its saying that its of type "any"
2) Is there a necessity for calling unregister()
?
playerRef.current?.context.ui.menu.unregister('Source')
playerRef.current?.context.ui.menu.register({
...
})
The only thing I can think of is garbage collection, but I'm not entirely sure. I've tried to find a way to do playerRef.current?.use(ui[uiOptions])
but it seems that it didnt register it. No errors tho. For now, I've set it up this way:
const uiOptions: UiConfig = {
pictureInPicture: true,
menu: [
{
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id
})),
onChange: ({ value }) => {
if (!playerRef.current) return
playerRef.current
.changeSource(
enimeFetcher({ route: 'source', arg: value })
.then((res) => res ? ({ src: res.url, poster }) : notFound())
)
.catch((err) => console.log(err))
},
}
]
}
const plugins = [
OUI(uiOptions),
// Don't worry, the default is lazy loading
OHls(),
]
// #oplayer element has rendered, just create player
useEffect(() => {
playerRef.current =
Player.make('#oplayer', { poster } as PlayerOptions)
.use(plugins)
.create()
return () => {
playerRef.current?.destroy()
}
}, [])
But again, I'm not sure if the unregister('Source')
method is something that needs to be integrated
Side Notes:
destroy()
Speaking of garbage collection: this is definitely something I've missed:
useEffect(() => {
playerRef.current =
Player.make('#oplayer', { poster } as PlayerOptions)
.use(plugins)
.create()
return () => {
playerRef.current?.destroy()
}
}, [])
I didn't know destroy() was necessary but then again I was super sleep deprived while making the app lol.
Loading
return (
<div className={`${isLoading && styles['isLoading']}`}>
{/* Transition softly to actual video content */}
{/* To prevent 'flashing' event when video has loaded */}
<div ref={divRef} onLoadedData={() => isLoading && setIsLoading(false)}/>
</div>
)
The reason why I did this is because the OPlayer is initially has no width and height; it would "slowly" expand as it loads. I'm not sure yet as to how to circumvent the issue with the library's tools. That's where I added a div to just hide it till its fully loaded.
THANK YOU :)
Besides that, seriously, thank you for helping me out with my code. Definitely cleaner and less verbose than what I had lol. It's rare for me to encounter someone to answer my questions all the while not knowing that I had a question. Usually it's spammers that give AI generated answers or left unanswered for months.
-
1
I tried to derive it automatically but it didn't work and now I have to declare it like this
https://github.com/shiyiya/oplayer/blob/main/examples/standalone/main.ts#L26 -
2
If sources has changed, we need to delete the old one before re-registering -
loading
set css#oplayer{aspect-ratio: 16/9;}
interface Ctx {
ui: ReturnType<typeof OUI>
hls: ReturnType<typeof OHls>
}
function OPlayer({ sources, poster }) {
const playerRef = useRef<Player<Ctx>>(null)
useEffect(() => {
if (!playerRef.current) {
playerRef.current = Player.make<Ctx>('#oplayer', { poster }).use([OUI(), OHls()]).create()
}
return () => playerRef.current?.destory()
}, [])
useEffect(() => {
playerRef.current?.context.ui.menu.unregister('Source')
playerRef.current?.context.ui.menu.register({
name: 'Source',
children: sources.map((source, idx) => ({
name: extractDomainName(source.url) ?? 'Unknown',
default: idx === 0,
value: source.id,
})),
onChange: ({ value }) => {
playerRef.current
.changeSource(
enimeFetcher({ route: 'source', arg: value }).then((res) => ({ src: res.url, poster }))
)
.catch((err) => console.log(err))
},
})
playerRef.current
?.changeSource(
enimeFetcher({ route: 'source', arg: source[0].id }).then((res) => ({ src: res.url, poster }))
)
.catch((err) => console.log(err))
}, [sources])
return <div id="oplayer" />
}
Ah I see: so register() and unregister() is a necessity and can't be replaced with .use() in its place
Wish I had the brains to help with your code to make the library just a tad easier to use with use()
method lol
I'll go ahead and implement the code and refactor it. Thanks again for all the help :)
register is to keep sources in sync. If sources do not change, they can be placed in options.
If you have further questions, you can use the discord
Oh I didn't even realize that was a thing ๐
I just simply thought it automatically did that or something since it didnt give me any errors
And for sure, I'll shoot a message there if anything lol