Facing inconsistent errors in the player, same video when played again does play

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);

Hi,

Thank you for reaching out to our community support.

If the PLAYBACK_VIDEO_DECODING_ERROR or SOURCE_STREAM_NOT_SUPPORTED errors happen only on Android browsers, there’s a possibility that it might be due to a decoding issue on the device as there are many fragmented Android devices in the market.

To clarify where the error happens, let me ask you some questions

  1. Would you please share which Android device (device vendor, device name, Android OS version) you mostly see the error?
  2. Would you also try to disable the hardware decoder on Chrome to see if the issue still happens? (there will be the Hardware-accelerated video decode option in chrome://flags)

Regarding the DRM_FAILED_LICENSE_REQUEST, I agree it’s an usual error code to see for non-DRM video. May I also ask the same such as

  • How frequently did you see the error code?
  • Are there specific steps we need to take to make the error happen (like seeking, pausing, resuming, reloading, etc.)?
  • Have you noticed if this issue is specific to certain devices? If yes, please kindly share the device details as well.

There are a few more troubleshooting options we suggest in each error code page. So it’d be also appreciated if you can have a look at it as well.

Best Regards,
Kazuhide