Following is the code that works for 95% of the scenarios for some cases specially in Android browsers we are facing issues like
PLAYBACK_VIDEO_DECODING_ERROR
SOURCE_STREAM_NOT_SUPPORTED
Test URL: https://tagmango.com/assets/1697361725233/master.m3u8
Also:
https://tagmango.com/assets/1697134410022/master.m3u8
gives DRM_FAILED_LICENSE_REQUEST
whereas it is a non-DRM video.
I am doubting it’s some sort of player instance error as we are using this a course module and feed where videos are played simultaneously.
const BitmovinPlayer: React.FC<Props> = ({
mediaId,
contentId,
isCompressDone,
mediaThumb = '',
hasError,
isDRMEnabled = false,
initalProgress,
onProgress,
onEnded,
errorDescription,
isCompressedUrl,
...urls
}) => {
const { mDeeplinkUrl } = useAppSelector((state) => state.app);
const { media } = useAppSelector((state) => state.media);
const dispatch = useAppDispatch();
const { mediaUrl, compressedUrl, hlsUrl, dashUrl } = useMemo(() => {
const modifiedUrl = (url?: string) => {
return getModifiedURL(mDeeplinkUrl, url);
};
return {
mediaUrl: modifiedUrl(urls.mediaUrl),
compressedUrl: modifiedUrl(urls.compressedUrl),
hlsUrl: modifiedUrl(urls.hlsUrl),
dashUrl: modifiedUrl(urls.dashUrl),
};
}, [mDeeplinkUrl, urls]);
const playerConfig = {
key: 'bf489d14-01f2-4327-b57b-02c23912ddf8',
ui: false,
style: {
aspectratio: '4/3',
},
analytics: {
key: 'cec00acb-0245-4e82-bfd9-30fc9eb6a66f',
title: mediaId,
videoId: mediaId,
},
network: {
preprocessHttpRequest: (type: any, request: any) => {
// Setting pallycon customData.
setCustomData(type, request);
return Promise.resolve(request);
},
},
};
const debounceRef = useRef<NodeJS.Timeout>();
const allowUpdateProgress = useRef(true);
const playerDiv = useRef<HTMLDivElement>(null);
const player = useRef<PlayerAPI | null>(null);
const [pallyconToken, setPallyconToken] = useState({
widevine: '',
fairplay: '',
playready: '',
});
const [played, setPlayed] = useState(false);
const handlePlay = () => {
setPlayed(true);
dispatch(
updateMedia({
mediaId,
mediaType: 'video',
playerState: 'playing',
}),
);
};
useEffect(() => {
if (media && media.mediaId !== mediaId && played) {
setPlayed(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [media, mediaId]);
const checkProgress = (position: number, duration: number) => {
if (onProgress) {
onProgress(position, duration);
}
};
/**
* update progress every 30 seconds
*/
const debounceUpdateProgress = (position: number, duration: number) => {
if (allowUpdateProgress.current) {
allowUpdateProgress.current = false;
checkProgress(position, duration);
debounceRef.current = setTimeout(() => {
allowUpdateProgress.current = true;
}, 30000);
}
};
// If You Use Token Reset During Playback Suck As CSL or KeyRotation or AirPlay,
// Continue to create new tokens and Set them.
function setCustomData(type: any, request: any) {
if (!pallyconToken) return;
switch (type) {
case HttpRequestType.DRM_LICENSE_WIDEVINE:
request.headers['pallycon-customdata-v2'] = pallyconToken.widevine;
break;
case HttpRequestType.DRM_LICENSE_FAIRPLAY:
request.headers['pallycon-customdata-v2'] = pallyconToken.fairplay;
break;
case HttpRequestType.DRM_LICENSE_PLAYREADY:
request.headers['pallycon-customdata-v2'] = pallyconToken.playready;
break;
default:
break;
}
}
function setupPlayer() {
if (!playerDiv.current) return;
const playerInstance = new Player(playerDiv.current, playerConfig);
UIFactory.buildDefaultUI(playerInstance);
let playerSource: SourceConfig = {
progressive: mediaUrl,
poster: mediaThumb,
options: {
// withCredentials: false, // This is required for CORS requests to include credentials
// manifestWithCredentials: true, // for credentials in DASH manifest
},
};
if (isDRMEnabled) {
playerSource = {
...playerSource,
hls: hlsUrl,
dash: dashUrl,
};
playerSource.drm = {
widevine: {
LA_URL: 'https://license.pallycon.com/ri/licenseManager.do',
mediaKeySystemConfig: {
persistentState: 'required',
},
},
playready: {
LA_URL: 'https://license.pallycon.com/ri/licenseManager.do',
},
fairplay: {
LA_URL: 'https://license.pallycon.com/ri/licenseManager.do',
// FairPlay certificate. Required for iOS.
certificateURL:
'https://license-global.pallycon.com/ri/fpsKeyManager.do?siteId=KL48',
prepareContentId: function (cId) {
return cId.substring(cId.indexOf('skd://') + 6);
},
prepareCertificate: function (rawResponse) {
var responseText = String.fromCharCode.apply(
null,
new Uint8Array(rawResponse) as any,
);
var raw = window.atob(responseText);
var rawLength = raw.length;
var certificate = new Uint8Array(new ArrayBuffer(rawLength));
for (var i = 0; i < rawLength; i++)
certificate[i] = raw.charCodeAt(i);
return certificate;
},
useUint16InitData: true,
},
};
} else if (isCompressedUrl) {
playerSource = {
...playerSource,
hls: compressedUrl,
};
}
// console.log('playerSource', playerSource);
playerInstance.load(playerSource).then(
() => {
player.current = playerInstance;
playerInstance.play().then(() => {
if (initalProgress && initalProgress < 95) {
playerInstance.seek(
(initalProgress * playerInstance.getDuration()) / 100,
);
}
});
console.log('Successfully loaded source');
},
() => {
console.log('Error while loading source');
},
);
playerInstance.on(PlayerEvent.Play, () => {
dispatch(
updateMedia({
mediaId,
mediaType: 'video',
playerState: 'playing',
}),
);
checkProgress(
playerInstance.getCurrentTime(),
playerInstance.getDuration(),
);
});
playerInstance.on(PlayerEvent.Paused, () => {
dispatch(
updateMedia({
mediaId,
mediaType: 'video',
playerState: 'paused',
}),
);
checkProgress(
playerInstance.getCurrentTime(),
playerInstance.getDuration(),
);
});
playerInstance.on(PlayerEvent.TimeChanged, () => {
if (playerInstance?.isLive()) return;
const position = playerInstance.getCurrentTime();
const duration = playerInstance.getDuration();
debounceUpdateProgress(position, duration);
});
playerInstance.on(PlayerEvent.PlaybackFinished, () => {
dispatch(
updateMedia({
mediaId,
mediaType: 'video',
playerState: 'stopped',
}),
);
checkProgress(playerInstance.getDuration(), playerInstance.getDuration());
if (onEnded) onEnded();
});
}
function destroyPlayer() {
if (player && player.current) {
// console.log('destroying player');
player.current.destroy();
player.current = null;
}
}
const getToken = async () => {
if (!contentId) return;
Promise.all([
API.fetchPallyconToken(contentId, 'Widevine'),
API.fetchPallyconToken(contentId, 'FairPlay'),
API.fetchPallyconToken(contentId, 'PlayReady'),
])
.then((values) => {
const widevine = values[0]?.data?.result;
const fairplay = values[1]?.data?.result;
const playready = values[2]?.data?.result;
setPallyconToken({
widevine,
fairplay,
playready,
});
})
.catch((err) => {
console.log(err);
message.error('Something went wrong');
});
};
useEffect(() => {
if (!mediaId || !played) {
return;
}
if (isDRMEnabled) getToken();
else setupPlayer();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mediaId, played]);
useEffect(() => {
if (pallyconToken) {
setupPlayer();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pallyconToken]);
useEffect(() => {
return () => {
destroyPlayer();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div
className={
!isCompressDone || !played || hasError
? 'player-wrapper-drm'
: 'player-wrapper-drm-custom'
}
style={{
backgroundImage:
isCompressDone && !hasError ? `url(${mediaThumb})` : 'none',
}}>
{!isCompressDone || hasError ? (
<CompressingVideo
hasError={hasError}
errorDescription={errorDescription}
/>
) : null}
{!played && isCompressDone && !hasError ? (
<PlayButton handlePlay={handlePlay} />
) : null}
{isCompressDone && played && mediaId && !hasError ? (
<div id={`player_${mediaId}`} ref={playerDiv} />
) : null}
</div>
);
};
export default React.memo(BitmovinPlayer);