shiyiya/oplayer

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"

context ui menu register({})

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.

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