Skip to main content

Player Integration in iOS WebView

Overview

In this document, we briefly go through the steps to get started with integrating the Bambuser Live Video Shopping player into your native iOS mobile app.


Requirements

  • Your app supports WebViews
    (or is web-based)
  • A webpage embedding Bambuser Live Video Shopping player, which can be loaded inside the WebView

    • Hosted and managed by you
    • Includes JavaScript code for connecting the player with your app

Below we explain how to integrate the Bambuser Live Video Shopping player in your iOS native app. Note that the examples we use are from the sample project on GitHub.

Getting started

Here you find a simple example project that can help you understand the implementation and get started with the technical integration quickly.

iOS project

iOS

  • Written in Swift
  • Uses WKWebview
  • Native event handlers
  • Some helper methods
HTML embed page

Embed HTML page

  • An static HTML page which we render inside the webview
  • Player configuration and event listeners
  • Sample show embedded

How it works

img

Because the Bambuser Live Video Shopping player is a web app, it works perfectly within a webview. Thanks to the ability mobile platforms to communicate between WebView and the native code, it is possible to utilize the Bambuser Player JavaScript API to configure and customize the behavior of the player inside the WebView.

Create an HTML page

Steps:

  1. Setup a webpage to render inside the webview
    • Hosted and managed on the customer side
    • Recommanded to be hosted remotely
  2. Embed the player on this webpage (Learn more)

Setup a WebView

Setup the player

Steps:

  • Create a new subclass of WKWebView
  • Initiate the view
  • Set allowsInlineMediaPlayback (Necessary to render Bambuser Live Video Shopping player)
  • Add a function to load the show
BambuserPlayer.swift
import WebKit

class BambuserPlayer: WKWebView {
init() {
let webConfiguration = WKWebViewConfiguration()

// Allow overlaying custom player components
// Necessary to render Bambuser Live Video Shopping player
webConfiguration.allowsInlineMediaPlayback = true

super.init(frame: .zero, configuration: webConfiguration)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

/// Load show url into the player
public func loadEmbeddedPlayer(_ embeddedPlayerUrl: URL) {
load(URLRequest(url: embeddedPlayerUrl))
}
}

Setup a View controller

Steps:

  • Create a new view controller
  • Initiate the BambuserPlayer
  • override loadView() to replace view with player
  • Set the url to point to your embed page.
BambuserViewController.swift
class BambuserViewController: UIViewController {

var player = BambuserPlayer()

override func loadView() {
view = player
}

// MARK: Embed page to be rendered inside the webview
// Note that player configuration, registering event listeners for the player,
// and also initiating the player are done within the embed HTML page.
// You need to create a similar page and render that in the WebView.
// For inspiration, look at the example webview embed HTML page on the GitHub repo.

// Here are a list of URLs to a sample embed page on different player states
// Uncomment one at a time to test different scenarios

// 1. Recorded show:
let url = URL(string: "https://bambuser.github.io/bambuser-lvs-webview-examples/index.html")

// 2. Live Show (fake live for testing the chat)
//let url = URL(string: "https://bambuser.github.io/bambuser-lvs-webview-examples/index.html?mockLiveBambuser=true")

// 3. Countdown - Scheduled show:
//let url = URL(string: "https://bambuser.github.io/bambuser-lvs-webview-examples/index.html?showId=2iduPdz2hn6UKd0eQmJq")

guard let url else {
return showAlert("Error", "Event has invalid URL")
}

do {
try player.loadEmbeddedPlayer(url, eventHandler: handleEvent)
} catch {
showAlert("Error", "Event has no playback URL")
}
}
}

Next, we will see how to communicate between the webview and the native functionalities.

Establish a bridge between native app and webview

There is a need to set up a data flow from the native app to the webview and vice versa.

Communicate from Native App to Webview

Some player configurations might need to be read from the app user preferences. For example locale (language) which can be different for each market

Evaluate Javascript inside the webview

Here we set up an interface to execute javascript code inside the webview. In BambuserPlayer.swift, add the following:

BambuserPlayer.swift
class BambuserPlayer: WKWebView {
init() {
/* ... previously implemented code ... */

configuration.userContentController = .init()
configuration.userContentController.add(
self,
name: "bambuserEventHandler"
)
}

/* ... */
}

extension BambuserPlayer: WKScriptMessageHandler {
/// This function is used to communicate message to the JS
private func evaluateJavascript(_ javascript: String, sourceURL: String? = nil, completion: ((_ result: Any? , _ error: String?) -> Void)? = nil) {

evaluateJavaScript(javascript) { (result, error) in
guard result != nil else {
print("error \(String(describing: error))")
completion?(nil, error?.localizedDescription)
return
}
completion?(result, nil)
print("Success: \(String(describing: result))")
}
}
}

In the example below, we create a JSON that contains some configuration data. Then we construct a javascript code as a string to attach the configuration data to the window object as window.iosAppConfig.

BambuserPlayer.swift
class BambuserPlayer: WKWebView {
init() {
/* ... previously implemented code ... */
navigationDelegate = self
}
}

extension BambuserPlayer: WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
debugPrint("didCommit")
let myJsonDict : [String: Any] = [
"locale": "\(Locale.current.languageCode ?? "en")-\(Locale.current.regionCode ?? "")"
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: myJsonDict, options: []),
let json = String(data: jsonData, encoding: .utf8) else {
print("Something wrong with JSON conversion!")
return
}
print("Json body: \(json)")

// To send the iOS config info
// check in console: "window.iosAppConfig"
let iOSConfig = "window.iosAppConfig = JSON.parse('\(json)');"
evaluateJavascript("\(iOSConfig)")
}
}

On the landing page, where you configure the player, you can use window.iosAppConfig to initialize configurations such as locale

HTML Embed
<script>
window.onBambuserLiveShoppingReady = function (player) {
player.configure({
buttons: {
dismiss: player.BUTTON.CLOSE,
},
locale: isIos ? iosAppConfig.locale : "en-US",
});
};
</script>

Communicate from Webview to Native App

Some player interactions should be communicated to the native app in order to run native functions. For example:

  • when a product is clicked
  • when a player is closed
  • when Share is clicked
  • when Add to Calendar is clicked

Invoke app methods from the WebView

Steps:

  1. Listen to messages coming from the webview
  2. Identify event
  3. Execute an intended native handler for that event
  • Add ScriptMessageHandler to establish a communication interface with the webview

    BambuserPlayer.swift
    class BambuserPlayer: WKWebView {
    init() {
    let webConfiguration = WKWebViewConfiguration()
    let contentController = WKUserContentController()

    webConfiguration.userContentController = contentController

    // Allow overlaying custom player components
    // Necessary to render Bambuser Live Video Shopping player
    webConfiguration.allowsInlineMediaPlayback = true

    // Make it fullscreen
    super.init(frame: .zero, configuration: webConfiguration)
    navigationDelegate = self
    uiDelegate = self

    // Add ScriptMessageHandler
    // Used to catch postMessage events from the webview
    contentController.add(
    self,
    name: "bambuserEventHandler"
    )
    }

    // ...
    }
  • Sending the event to the native message handler

    For communicating a player event from the WebView to the app, we need to create a postMessage. Then on the app side, we catch the postMessage and run the intended handler for each event.

    HTML Embed
    <script>
    window.onBambuserLiveShoppingReady = function (player) {
    player.on(player.EVENT.READY, function () {
    if (isIos) {
    window.webkit.messageHandlers.bambuserEventHandler.postMessage({
    eventName: "player.EVENT.READY",
    });
    }
    });

    player.on(player.EVENT.CLOSE, function () {
    if (isIos) {
    window.webkit.messageHandlers.bambuserEventHandler.postMessage({
    eventName: "player.EVENT.CLOSE",
    message: "The player has been closed!",
    });
    }
    });
    };
    </script>
  • Catching the messages on the native side

    BambuserPlayer.swift
    //MARK: WKScriptMessageHandler
    /// This function is called when there is a postMessage from the WebView
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {

    guard message.name == "bambuserEventHandler" else {
    print("No handler for this message: \(message)")
    return
    }

    guard let body = message.body as? [String: Any] else {
    print("could not convert message body to dictionary: \(message.body)")
    return
    }

    guard let eventName = body["eventName"] as? String else { return }

    // This will later be passed to handleEvent function
    eventHandler?(eventName, body["data"])
    }

    Later based on the event name, we run the desired native functionality.

    BambuserViewcontroller.swift
    func handleEvent(_ name: String, data: Any?) {
    // See all available events on our Player API Reference
    // https://bambuser.com/docs/one-to-many/player-api-reference/

    let dataDictionary = data as? [String: AnyObject]
    switch name {
    case "player.EVENT.CLOSE":
    // Add your handler methods if needed
    // As an example we invoke the close() method
    close()
    case "player.EVENT.READY":
    // Add your handler methods if needed
    // As an example we print a message when the 'player.EVENT.READY' is emitted
    print("Ready")
    case "player.EVENT.SHOW_ADD_TO_CALENDAR":
    let calendarEvent = dataDictionary?.decode(CalendarEvent.self)
    addToCalendar(calendarEvent)
    case "player.EVENT.SHOW_SHARE":
    guard
    let urlString = dataDictionary?["url"] as? String,
    let url = URL(string: urlString)
    else { return }
    shareShow(url)
    case "player.EVENT.SHOW_PRODUCT_VIEW":
    let product = dataDictionary?.decode(ProductModel.self)
    showProductView(product)
    default:
    showAlert("eventName", "This event does not have a handler for event \(name)!")
    }
    }

Handle Player Events

On the landing page, you can use the player API to handle different player events. Below we gathered the most common scenarios that are handled within the context of app integration.

Handle Closing the Player

Event name: player.EVENT.CLOSE

trigger
  • When a shopper closes the player.
  1. Make sure to configure the dismiss button as below
  2. HTML Embed
    // Inside onBambuserLiveShoppingReady

    player.configure({
    buttons: {
    dismiss: player.BUTTON.CLOSE,
    },
    });

  3. Handle player.EVENT.CLOSE to navigate shopper back to your native app
    // Inside onBambuserLiveShoppingReady

    player.on(player.EVENT.CLOSE, function () {
    window.webkit.messageHandlers.bambuserEventHandler.postMessage({
    eventName: "player.EVENT.CLOSE",
    message: "The player has been closed!",
    });
    });

Handle Product View

Event name: player.EVENT.SHOW_PRODUCT_VIEW

trigger
  • When a shopper clicks on a product (whether from the product list or highlighted product).
  1. Override default product click behavior
  2. HTML Embed
    // Inside onBambuserLiveShoppingReady

    player.configure({
    buttons: {
    product: player.BUTTON.NONE,
    },
    });

  3. Handle player.EVENT.SHOW_PRODUCT_VIEW event to display your native PDP
    // Inside onBambuserLiveShoppingReady

    player.on(player.EVENT.SHOW_PRODUCT_VIEW, (event) => {
    console.log("SHOW_PRODUCT_VIEW", event);
    window.webkit.messageHandlers.bambuserEventHandler.postMessage({
    eventName: "player.EVENT.SHOW_PRODUCT_VIEW",
    data: event,
    });
    });

Handle Share

Event name: player.EVENT.SHOW_SHARE

trigger
  • When a shopper clicks on Share button inside the player
  1. Hide player's default share view
  2. HTML Embed
    // Inside onBambuserLiveShoppingReady

    player.configure({
    ui: {
    hideShareView: true,
    },
    });

  3. Handle player.EVENT.SHOW_SHARE event to display native Share view
    // Inside onBambuserLiveShoppingReady

    player.on(player.EVENT.SHOW_SHARE, (event, callback) => {
    console.log("SHOW_SHARE", event);

    window.webkit.messageHandlers.bambuserEventHandler.postMessage({
    eventName: "player.EVENT.SHOW_SHARE",
    data: event,
    });
    });

Handle Add To Calendar

Event name: player.EVENT.SHOW_ADD_TO_CALENDAR

trigger
  • When a shopper clicks on Add to Calendar button inside the player
  1. Hide player's default calendar view
  2. HTML Embed
    // Inside onBambuserLiveShoppingReady

    player.configure({
    ui: {
    hideAddToCalendar: true,
    },
    });

  3. Handle player.EVENT.SHOW_ADD_TO_CALENDAR event to display native Add To Calendar view
    // Inside onBambuserLiveShoppingReady

    player.on(player.EVENT.SHOW_ADD_TO_CALENDAR, (event) => {
    console.log("SHOW_ADD_TO_CALENDAR", event);

    window.webkit.messageHandlers.bambuserEventHandler.postMessage({
    eventName: "player.EVENT.SHOW_ADD_TO_CALENDAR",
    data: event,
    });
    });

Handle URL requests from the webview

By default, WKWebView blocks all the URL navigation requests from the WebView. Therefore, if for example a link is clicked inside the WebView, no navigation performs unless you handle the navigation separately.

In the player, there are two cases where the users will have the possibility click links.

  1. Links in the chat term (E.g. Link to the Privacy Policy page)
  2. Links that are sent by moderators in the chat section

To handle this situation, we would need to intercept the URL navigation requests in the WKWebView.

Example:

  • Intercept Requests
    • Check if URL is valid
      • If URL contains /privacy, open the Privacy Policy page.
BambuserPlayer.swift
//MARK: Overriding URL requests coming from the app
/* No links from the player will be clickable unless handled here.
Common occasions where player contains links
- Chat terms contains link to Privacy policy/Terms & conditions
- Moderator may drop a link in the chat for viewers' usage
*/
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView?
{
guard let url = navigationAction.request.url else { return nil}
// Check if it's a valid URL
if let validUrl = URL(string: "\(url)") {
print("It is a valid URL: \(validUrl)")
print("\(validUrl.absoluteString)")
// Use regex to validate link type
// E.g.
let regex: String = "/privacy"
let regexResult = validUrl.absoluteString.range(
of: regex,
options: .regularExpression
)
let isPrivacyPolicyLink = (regexResult != nil)
// If it's a Privacy Policy link, open it in a browser
if isPrivacyPolicyLink {
print("Opening PP/TC link in a browser...")
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
// Detect and handle other types of link you expect in the player
print("An unhandled URL request from the player: \(validUrl)")
}
} else {
print("Not a valid URL: \(url)")
return nil
}
return nil
}