How to embed CSS + JS for the Bitmovin UI with React Native?

I confirm this is a Feature Request and NOT a support/technical query. Please use your Bitmovin Dashboard for raising support tickets.

on

Which product is your request related to?

Player

Description

Hey!

First of all, I’m putting this as feature request as it could be part of the React Native example app :innocent:

As recommended here, we’d like to embed our Bitmovin Player UI with the app bundle directly for offline support.

However, the implementation with React Native + Expo is a bit tricky.
I’m trying to embed the assets using expo-assets, and reference them through local URIs. It’s a bit tricky especially because you can’t require the .js file. So, we rename the file to .bmjs extension, whitelist them for expo-assets, and at runtime we rename the file to .js. I do get a local URI with a file:// pointing to a local JS file, embedded in the phone, and yet it does not work with Bitmovin. No UI loads.

I wonder if I have done something wrong, and if there is a better strategy for that.

My post is a bit ambiguous between support / bug / feature request, but as I said above: I think this should be part of the documentation or part of the example.

Any help appreciated :raising_hands:

Thanks!

Dumb mistake on my end, what I’ve done actually works.

If you’re interested, here’s a piece of code that solves it.

import { Asset } from 'expo-asset'
import * as FileSystem from 'expo-file-system/legacy'

const asFileWithExt = async (moduleRef: number, desiredExt: 'js' | 'css') => {
  const asset = Asset.fromModule(moduleRef)
  await asset.downloadAsync()

  if (!FileSystem.cacheDirectory || !asset.hash) {
    throw new Error('asdflkjasdknj')
  }

  const dest = `${FileSystem.cacheDirectory}${asset.hash}.${desiredExt}`

  if (asset.localUri) {
    await FileSystem.copyAsync({ from: asset.localUri, to: dest })
  }

  return dest
}

export const getBitmovinUrl = async () => {
  const js = await asFileWithExt(require('./assets/player-ui.bmjs'), 'js')
  const css = await asFileWithExt(require('./assets/player-ui-css.bmcss'), 'css')

  return { js, css }
}

Hey!

Actually, the method above caused two major issues:

  • the js / css files were not working properly for some reason (strange encoding bugs) on Android
  • and it is not working at all on a Distribution app on iOS! (everything is sandboxed)

:white_check_mark:We ended up copying the files during the prebuild phase of expo with a custom expo plugin.

const { withXcodeProject, withDangerousMod } = require('@expo/config-plugins')
const path = require('path')
const fs = require('fs')

const BITMOVIN_ASSET_SOURCE_PATH = '../src/modules/player/uiConfig/assets'
const BITMOVIN_JS_ASSET_NAME = 'player-ui.bmjs'
const BITMOVIN_CSS_ASSET_NAME = 'player-ui-css.bmcss'

const BITMOVIN_JS_BUNDLED_FILENAME = 'bitmovinplayer-ui.js'
const BITMOVIN_CSS_BUNDLED_FILENAME = 'bitmovinplayer-ui.css'

const withBitmovinUIResources = (config) => {
  // iOS configuration
  config = withXcodeProject(config, (xcodeProjectConfig) => {
    const projectRoot = xcodeProjectConfig.modRequest.projectRoot
    const iosProjectRoot = path.join(projectRoot, 'ios')

    const sourcePath = path.join(projectRoot, BITMOVIN_ASSET_SOURCE_PATH)
    const jsSource = path.join(sourcePath, BITMOVIN_JS_ASSET_NAME)
    const cssSource = path.join(sourcePath, BITMOVIN_CSS_ASSET_NAME)

    // Destination files in iOS project
    const jsDestination = path.join(iosProjectRoot, BITMOVIN_JS_BUNDLED_FILENAME)
    const cssDestination = path.join(iosProjectRoot, BITMOVIN_CSS_BUNDLED_FILENAME)

    validateSourceFilesExist(jsSource, cssSource)

    // Copy files with correct extensions
    fs.copyFileSync(jsSource, jsDestination)
    fs.copyFileSync(cssSource, cssDestination)

    // Add to app bundle as resources
    const xcodeProject = xcodeProjectConfig.modResults
    xcodeProject.addResourceFile(BITMOVIN_JS_BUNDLED_FILENAME)
    xcodeProject.addResourceFile(BITMOVIN_CSS_BUNDLED_FILENAME)

    return xcodeProjectConfig
  })

  // Android configuration
  config = withDangerousMod(config, [
    'android',
    (androidConfig) => {
      const projectRoot = androidConfig.modRequest.projectRoot
      const androidAssetsDir = path.join(
        androidConfig.modRequest.platformProjectRoot,
        'app/src/main/assets',
      )

      const sourcePath = path.join(projectRoot, BITMOVIN_ASSET_SOURCE_PATH)
      const jsSource = path.join(sourcePath, BITMOVIN_JS_ASSET_NAME)
      const cssSource = path.join(sourcePath, BITMOVIN_CSS_ASSET_NAME)

      const jsDestination = path.join(androidAssetsDir, BITMOVIN_JS_BUNDLED_FILENAME)
      const cssDestination = path.join(androidAssetsDir, BITMOVIN_CSS_BUNDLED_FILENAME)

      validateSourceFilesExist(jsSource, cssSource)

      if (!fs.existsSync(androidAssetsDir)) {
        fs.mkdirSync(androidAssetsDir, { recursive: true })
      }

      fs.copyFileSync(jsSource, jsDestination)
      fs.copyFileSync(cssSource, cssDestination)

      return androidConfig
    },
  ])

  return config
}

const validateSourceFilesExist = (jsSource, cssSource) => {
  if (!fs.existsSync(jsSource)) {
    throw new Error(`Bitmovin UI JS source file ${jsSource} does not exist`)
  }

  if (!fs.existsSync(cssSource)) {
    throw new Error(`Bitmovin UI CSS source file ${cssSource} does not exist`)
  }
}

module.exports = withBitmovinUIResources

In the metro config

config.resolver.assetExts = [...config.resolver.assetExts, 'bmjs', 'bmcss']

And in your Expo app config

  // ...

  plugins: [
    './plugins/withBitmovinUIResources',
  ],

  // ...

This worked fine for us. Biggest problem is that we need a native deployment (instead of eas update) for every change :grin: