The player also requires some Javascript to render the player into a certain element. This can be placed in a useEffect hook which would run after the elements have been added to the DOM.
import { useEffect } from 'react';
const KalturaPlayer = ({ targetId, partnerId, uiConfId, entryId }) => {
useEffect(() => {
const kalturaPlayer = KalturaPlayer.setup({
targetId,
provider: {
partnerId,
uiConfId
},
playback: {
autoplay: true
}
});
kalturaPlayer.loadMedia({ entryId });
}, [])
return (
<>
<div id={targetId} style={{ width: '640px', height: '360px' }} />
<script type="text/javascript" src={`https://cdnapisec.kaltura.com/p/${partnerId}/embedPlaykitJs/uiconf_id/${uiConfId}`} />
</>
);
}
This is all we need to render the player. If we want to expose more of the player's functionality to React, we can store the reference to the player instance in a ref and then "synchronize" React's state to the player using useEffect hooks. We should also destroy the player instance when the component dismounts to save resources.
import { useRef, useEffect } from 'react';
const KalturaPlayer = ({ targetId, partnerId, uiConfId, entryId }) => {
const playerRef = useRef(null);
useEffect(() => {
playerRef.current = KalturaPlayer.setup({
targetId,
provider: {
partnerId,
uiConfId
},
playback: {
autoplay: true
}
});
// Destroy this instance of the player when this component dismounts
return () => {
if (playerRef.current) {
playerRef.current.destroy();
}
}
}, [])
// This useEffect hook runs after the hook above
// Every time the entryId prop changes, that change will be synchronized with the player instance
useEffect(() => {
if (playerRef.current && entryId) {
playerRef.current.loadMedia({ entryId })
}
}, [entryId])
...
}
The same approach works in Next.js but instead of rendering the script
element directly, we need to use to the Script
component from next/script
. The Script component changes how script
elements are injected into the page so that they work alongside Next.js's page hydration.
import { useEffect } from 'react';
import Script from "next/script";
const KalturaPlayer = ({ targetId, partnerId, uiConfId, entryId }) => {
useEffect(() => {
const kalturaPlayer = KalturaPlayer.setup({
targetId,
provider: {
partnerId,
uiConfId
},
playback: {
autoplay: true
}
});
kalturaPlayer.loadMedia({ entryId });
}, [])
return (
<>
<div id={targetId} style={{ width: '640px', height: '360px' }} />
<Script src={`https://cdnapisec.kaltura.com/p/${partnerId}/embedPlaykitJs/uiconf_id/${uiConfId}`} />
</>
);
}
Now we need to tackle the analytics plugin. In plain React, the plugin script
can be placed after the player's script
, similar to the official instructions.
const KalturaPlayer = ({ targetId, partnerId, uiConfId }) => {
...
return (
<>
<div id={targetId} style={{ width: '640px', height: '360px' }} />
<script type="text/javascript" src={`https://cdnapisec.kaltura.com/p/${partnerId}/embedPlaykitJs/uiconf_id/${uiConfId}`} />
<script type="text/javascript" src="/js/playkit-kava.js" /> {/* Self hosted analytics plugin */}
</>
)
}
However, Next.js's Script
component injects elements out of order. So to achieve the same result, we need to manually control when the Script
component is rendered.
import { useState } from 'react';
import Script from "next/script";
const KalturaPlayer = ({ targetId, partnerId, uiConfId }) => {
...
const [loadKava, setLoadKava] = useState(false);
const handlePlayerReady = () => {
setLoadKava(true);
}
return (
<>
<div id={targetId} style={{ width: '640px', height: '360px' }} />
<Script src={`https://cdnapisec.kaltura.com/p/${partnerId}/embedPlaykitJs/uiconf_id/${uiConfId}`} onReady={handlePlayerReady} />
{loadKava && (
<Script src="/js/playkit-kava.js" /> {/* Self hosted analytics plugin */}
)}
</>
)
}
We adopted this solution during a larger rework of the video features of the site. The results were very positive.
By using the Script
component, the Kaltura player and its plugin was removed from the site's JS bundle. Now they only loaded when a video is played. This greatly improved the initial load metrics of the site. (Since the rework included some other changes, some of the improvements to the metrics such as cumulative layout shift were probably due to these changes. But I think the biggest gains came from this change.)
This solution also withstood the test of time. Since it reduced the number of moving pieces and constrained all the functionality around the video player to a single component, it was much easier to maintain as our requirements for the video player changed. We also upgraded several major dependencies (Next.js, React, Typescript) and tools (Node.js) and it survived these changes without breaking a sweat.
Third-party code exposes a fixed set of functionality and a way to utilize them. While you can access more functionality through some code acrobatics, it is not a great idea to do so. These unexposed internals will change over time and when they do, you will have a hard time.
A common misconception, especially amongst fresh developers, is the belief that everything needs to be done in the "React way" or in the "Modern way". They will then go to unreasonable lengths to stay within these paradigms.
Each paradigm has its pros and cons. While you should stick to a paradigm for the sake of consistency, there are a few cases where you can achieve big gains by opting out of it.
There are always many solutions to a certain problem. I think all of us are guilty of charging ahead with the first solution that pops into our heads. But a few extra minutes considering multiple solutions can save hours in the future.