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
- Written in Swift
- Uses WKWebview
- Native event handlers
- Some helper methods
Embed HTML page
- An static HTML page which we render inside the webview
- Player configuration and event listeners
- Sample show embedded
How it works
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:
- Setup a webpage to render inside the webview
- Hosted and managed on the customer side
- Recommanded to be hosted remotely
- 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
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.
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:
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
.
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
<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:
- Listen to messages coming from the webview
- Identify event
- Execute an intended native handler for that event
-
Add
ScriptMessageHandler
to establish a communication interface with the webviewBambuserPlayer.swiftclass 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 thepostMessage
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.swiftfunc handleEvent(_ name: String, data: Any?) {
// See all available events on our Player API Reference
// https://bambuser.com/docs/live/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
- When a shopper closes the player.
- Make sure to configure the dismiss button as below
- Handle
player.EVENT.CLOSE
to navigate shopper back to your native app - HTML Embed
- Swift
// Inside onBambuserLiveShoppingReady
player.configure({
buttons: {
dismiss: player.BUTTON.CLOSE,
},
});
// Inside onBambuserLiveShoppingReady
player.on(player.EVENT.CLOSE, function () {
window.webkit.messageHandlers.bambuserEventHandler.postMessage({
eventName: "player.EVENT.CLOSE",
message: "The player has been closed!",
});
});
// Inside WKScriptMessageHandler
case "player.EVENT.CLOSE":
// Close the webview and navigate to the previous view
close()
Handle Product View
Event name: player.EVENT.SHOW_PRODUCT_VIEW
- When a shopper clicks on a product (whether from the product list or highlighted product).
- Override default product click behavior
- Handle
player.EVENT.SHOW_PRODUCT_VIEW
event to display your native PDP - HTML Embed
- Swift
// Inside onBambuserLiveShoppingReady
player.configure({
buttons: {
product: player.BUTTON.NONE,
},
});
// 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,
});
});
// Inside WKScriptMessageHandler
case "player.EVENT.SHOW_PRODUCT_VIEW":
// Use above event payload to display
// native PDP
let product = dataDictionary?.decode(ProductModel.self)
showProductView(product)
Handle Share
Event name: player.EVENT.SHOW_SHARE
- When a shopper clicks on
Share
button inside the player
- Hide player's default share view
- Handle
player.EVENT.SHOW_SHARE
event to display native Share view - HTML Embed
- Swift
// Inside onBambuserLiveShoppingReady
player.configure({
ui: {
hideShareView: true,
},
});
// 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,
});
});
// Inside WKScriptMessageHandler
case "player.EVENT.SHOW_SHARE":
// Use above event payload to trigger
// native share drawer
guard
let urlString = dataDictionary?["url"] as? String,
let url = URL(string: urlString)
else { return }
shareShow(url)
Handle Add To Calendar
Event name: player.EVENT.SHOW_ADD_TO_CALENDAR
- When a shopper clicks on
Add to Calendar
button inside the player
- Hide player's default calendar view
- Handle
player.EVENT.SHOW_ADD_TO_CALENDAR
event to display native Add To Calendar view - HTML Embed
- Swift
// Inside onBambuserLiveShoppingReady
player.configure({
ui: {
hideAddToCalendar: true,
},
});
// 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,
});
});
// Inside WKScriptMessageHandler
case "player.EVENT.SHOW_ADD_TO_CALENDAR":
// Use above event payload to trigger
// native add to calendar drawer
let calendarEvent = dataDictionary?.decode(CalendarEvent.self)
addToCalendar(calendarEvent)
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.
- Links in the chat term (E.g. Link to the Privacy Policy page)
- 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.
- If URL contains
- Check if URL is valid
//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
}