Shoplive Developers
한국어KOEnglishEN日本語JA

Shoplive SDK Integration Guide

This guide walks you through adding live commerce viewing (Player) and broadcasting (Streamer) to your app, organized step by step around the case (mission) you want to build.
You can switch code samples with the iOS / Android tabs at the top.

Getting Started

Overview & Requirements

The Shoplive SDK is a broadcast SDK built for live commerce. It handles the complex streaming work for you and exposes only a small, stable interface for adding viewing (Player) and broadcasting (Streamer) to your app.
Open a full-screen live with a single button, embed a compact player in your home feed, keep watching in PIP, wire up products and coupons, or broadcast straight from your app — pick only the pieces you need and drop them in.

Start now →

Module Structure

Mission Map — What Do You Want to Build?

What you want to buildMission
Tapping a button opens the live stream in full screenMission 1 · Launch the Player Directly
Tapping a push/SMS link opens the live stream immediatelyMission 2 · Launch via Deep Link
Let signed-in members watch and chat (token/hashing/Secret Key)Mission 3 · Connect User Information
Put a broadcast preview in your home feed or listMission 4 · Embed in a Screen (Preview)
Keep the stream visible in a small window when the user navigates awayMission 5 · Enable PIP
Route a product tap to our own detail page / issue a couponMission 6 · Events · Products · Coupons
Change colors and fonts, or hide the default UI and render our ownMission 7 · UI Customization & Placement
Broadcast from the appMission 8 · Broadcasting

Delivery Forms (Platform Mapping)

FormiOSAndroid
Player · Full screenShoplivePlayerViewController (present)ShoplivePlayer(context).start(…) (Activity)
Player · EmbeddedShoplivePlayerView (UIView)ShoplivePlayerView (FrameLayout)
Streamer · StudioShopliveStreamerViewControllerShopliveStreamer(context).start(…)
Note The iOS ViewController approach corresponds to the Android Activity approach. Concepts, naming, and the event model are isomorphic across both platforms, so code you learn on one platform translates directly to the other.

Requirements

ItemiOSAndroidFlutter
Minimum OSiOS 15.0Android 6.0 (minSdk 23)iOS 15.0 · Android 6.0 (minSdk 23)
ToolingXcode 15 or later recommended (Swift 5.9)Android Gradle Plugin 8.x recommendedFlutter 3.22 or later · Dart 3.4 or later (Android: AGP 8.7 · JDK 17)
DistributionSwift Package ManagerMaven repository (sdk.shoplive.cloud)pub.dev (shoplive_player · shoplive_streamer)
Current version3.0.33.0.33.0.3
Getting Started

Installation

The current release is 3.0.3 for iOS, Android, and the Flutter plugin. For supported environments, see Requirements.
// Xcode ▸ File ▸ Add Package Dependencies…
// https://github.com/shoplive/shoplive-sdk-ios   (Dependency Rule: Up to Next Major — 3.0.3)

// Add only the libraries your target needs, and import only what you use.
import ShoplivePlayerSDK     // viewing — missions 1-7
import ShopliveStreamerSDK   // broadcasting — mission 8 (only if you ship it)

// ShopliveCore (auth · config · user) comes along with the SDKs above.
// You call Shoplive.initialize and setUser without importing it.
// settings.gradle.kts — add the Shoplive Maven repository
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://sdk.shoplive.cloud/maven-repo") }
    }
}

// app/build.gradle.kts
dependencies {
    implementation("cloud.shoplive:shoplive-player-sdk:3.0.3")     // viewing
    implementation("cloud.shoplive:shoplive-streamer-sdk:3.0.3")   // broadcasting
    // core is pulled in automatically by the SDKs above
}
# pubspec.yaml — Flutter 3.22 or later · Dart 3.4 or later
dependencies:
  shoplive_player: 3.0.3     # viewing (includes shoplive_core)
  shoplive_streamer: 3.0.3   # broadcasting — only if you need it

# ios/Podfile — the iOS SDK needs iOS 15 (the frameworks download during pod install)
platform :ios, "15.0"
# Android: the plugins add the Shoplive Maven repository for you
Flutter The Flutter plugin wraps the same 3.0.3 SDK in three packages — shoplive_player (viewing), shoplive_streamer (broadcasting) and shoplive_core (initialize · user · conversion, included in both). All three must use the same version. Every call returns a Future, and events and requests arrive as streams instead of a delegate (Mission 6). There is no widget for embedding the player in a feed, so the View approach of Mission 4 isn't available — use the Hybrid integration or a PIP preview.

Permissions (when using Streamer)

  • iOS — add NSCameraUsageDescription / NSMicrophoneUsageDescription to Info.plist.
  • Android — runtime permissions (CAMERA / RECORD_AUDIO) are requested by the SDK itself.
Getting Started

Quick Start

Once installation is complete, the code below is enough to play your first live stream.

What you need first — what each key does

ValueWhat it isWhere it goesWhen you need it
accessKeyIdentifies your company (service). Shoplive issues one per customerShoplive.initialize — once at app launchAlways — the only value initialization needs
campaignKeyPoints to one broadcast. Each broadcast (campaign) has its ownplay(campaignKey:) · start(campaignKey =)Every time you play or broadcast
User JWTTells Shoplive who the viewer is. Your backend signs it with the Secret KeyShoplive.setUser(.token(jwt:))For signed-in users (Mission 3) — skip it for guests
Broadcasting tokenProves the broadcaster's permissionShoplive.setStreamToken(_:)Only when the app broadcasts (Mission 8)
Secret KeyThe secret used to sign JWTs. You get it from your Shoplive contact. Never put it in the appYour backendWhen you use secure authentication (Mission 3 · Step 4)

If you don't have an accessKey, ask your Shoplive contact or check the Shoplive Admin. The Secret Key is not issued in the Admin — you get it from your Shoplive contact.

Viewing (Player) — 3 lines

// 1) Once at app startup
Shoplive.initialize(.init(accessKey: "YOUR_ACCESS_KEY"))
// 2) Identify the user — signed-in users only (skip for guests, Mission 3)
Shoplive.setUser(.token(jwt: myJwt))
// 3) Play
let player = ShoplivePlayerViewController(campaignKey: "YOUR_CAMPAIGN_KEY")
present(player, animated: true)
// 1) Once at app startup
Shoplive.initialize(context, ShopliveConfiguration("YOUR_ACCESS_KEY"))
// 2) Identify the user — signed-in users only (skip for guests, Mission 3)
Shoplive.setUser(ShopliveUser.Token(myJwt))
// 3) Play (starts as soon as the instance is created)
ShoplivePlayer(context).start(campaignKey = "YOUR_CAMPAIGN_KEY")
import "package:shoplive_player/shoplive_player.dart";

// 1) Once at app startup
await Shoplive.initialize(const ShopliveConfiguration(accessKey: "YOUR_ACCESS_KEY"));
// 2) Identify the user — signed-in users only (skip for guests, Mission 3)
await Shoplive.setUser(ShopliveUser.token(myJwt));
// 3) Play
await ShoplivePlayer.instance.start("YOUR_CAMPAIGN_KEY");

These three lines cover loading the campaign and playback, all the way to the chat, coupon, and product overlay. Details for each line: ① initialization settings · ② connecting user information · ③ launching the player.

Order matters initialize must run before playback. We recommend performing initialization in AppDelegate / Application.onCreate.

Broadcasting (Streamer)

Shoplive.initialize(.init(accessKey: "YOUR_ACCESS_KEY"))
Shoplive.setStreamToken(streamerJwt)          // broadcast-only token (required)

let studio = ShopliveStreamerViewController(campaignKey: "YOUR_CAMPAIGN_KEY")
present(studio, animated: true)
Shoplive.initialize(context, ShopliveConfiguration("YOUR_ACCESS_KEY"))
Shoplive.setStreamToken(streamerJwt)          // broadcast-only token (required)

ShopliveStreamer(context).start(campaignKey = "YOUR_CAMPAIGN_KEY")
import "package:shoplive_streamer/shoplive_streamer.dart";

await Shoplive.initialize(const ShopliveConfiguration(accessKey: "YOUR_ACCESS_KEY"));
await Shoplive.setStreamToken(streamerJwt);   // broadcast-only token (required)

await ShopliveStreamer.instance.start("YOUR_CAMPAIGN_KEY");

Next stepScreen Terminology Guide · Start with Mission 1 →

Screens & Demo

Screen Terminology Guide

This section defines what each element on screen is called. The missions, options, and event descriptions that follow use the same terms. The numbers below map 1:1 to the numbers in the screen image. Elements marked LIVE only are shown only while a broadcast is in progress.

Player Screen (Viewing)

Player screen layout
No.ElementVisibilityDescription
①Video areaAlwaysRenders live/VOD. resizeMode (fill/fit) — Mission 1
②Brand logoAlwaysSet in the campaign settings (console)
③Campaign titleAlwaysAlso received via campaignInfoReceived — Mission 6
④LIVE badge · viewers · likesLIVE onlyStatus via campaignStatusChanged — Mission 6
⑤Chat text overlayLIVE onlyText scrolling over the video. Input font appearance.chatInputFont — Mission 7
⑥Product card (bottom-left · NOW)AlwaysOn tap, ShoplivePlayerRequest.navigation(url:) — Mission 6
⑦Right icon column (♡ · ? · 💬)AlwaysBuilt-in UI — like · help · open chat
⑧Mute buttonAlwaysBound to the isMuted property
⑨Coupon bubbleWhen an operator publishesOn tap, ShoplivePlayerRequest.coupon(id:respond:) — Mission 6
⑩Notice pillWhen an operator publishesScrolling notice published by the operator (console)
⑪Event · notice popupWhen an operator publishesPublished by the operator. Also received as a custom action — Mission 6
⑫Close · minimizeAlwaysMinimize = enter in-App PIP. enterPictureInPicture() — Mission 5
Before / during / after the broadcast As the campaign status changes ready → live → ended (type dictionary), LIVE only elements are shown and withdrawn automatically by the SDK. Your app never has to toggle them; only when building your own UI do you subscribe to campaignStatusChanged (Mission 6) and toggle them yourself.
Broadcast states comparison

Studio Screen (Broadcasting)

Studio screen layout
No.ElementVisibilityDescription
①Camera previewAlwaysShown even before the broadcast starts (full background)
②Back · closeAlwaysTop-left. Exits the studio
③Connection state (Normal)AlwaysTop-right. Network quality (maps to ShopliveConnectionState)
④Zoom factor (1x)AlwaysTap to switch the zoom factor
⑤Zoom sliderAlwaysDrag for continuous zoom
⑥Sound · camera flip · settingsAlwaysThree at the bottom-right — SDK-provided broadcast tools
⑦Audio level meterAlwaysBottom-left. Check the mic input level
⑧Start · stop broadcastAlwaysBottom-center. Tap → connecting → live. ShopliveStreamerEvent.stateChanged — Mission 8

Previous ← Quick StartNext → Try It in the Demo App

Screens & Demo

Try It in the Demo App

The demo app shipped with the SDK is organized around the same case menu as this guide's missions (mission map). Seeing the behavior in the demo app before you write code makes the implementation much easier to grasp.

  1. Default mode — Shoplive demo settings: right after installation, you can try every feature (all missions) against a demo campaign with no input required.
  2. My-info mode — enter client settings: enter your issued accessKey · campaignKey · (optional) user JWT (how to issue one) on the settings screen to experience the same cases with your own campaign and your own users.
  3. Each case screen shows the corresponding mission number from this guide, so what you saw in the demo leads straight to the code in the guide.
The demo app distribution channel (TestFlight / APK) and the demo campaign key will be provided together with the SDK.

Previous ← Screen Terminology GuideNext → Mission 1 · Launch the Player Directly

Missions · Player

Mission 1 · Launch the Player Directly

GOALTap a button → the live stream opens in full screen with chat and products working
PREREQUISITESInstallation complete · accessKey · campaignKey (how to get them)
  1. Confirm that Shoplive.initialize is called at app startup (Quick Start).
  2. In the action of your entry button, create the player and present it.
  3. (Optional) Attach a delegate to receive events — Mission 6.
@IBAction func watchLive() {
    let player = ShoplivePlayerViewController(campaignKey: "YOUR_CAMPAIGN_KEY")
    player.delegate = self               // optional: receive events
    present(player, animated: true)
}
fun watchLive() {
    ShoplivePlayer(context).apply {
        delegate = myDelegate            // optional: receive events
    }.start(campaignKey = "YOUR_CAMPAIGN_KEY")
}
Future<void> watchLive() async {
  final player = ShoplivePlayer.instance;
  player.events.listen(onPlayerEvent);   // optional: receive events (Mission 6)
  await player.start("YOUR_CAMPAIGN_KEY");
}
If you want to control the screen transition yourself (Android) ShoplivePlayer(context).intent(campaignKey = …) returns an Intent. Just launch it yourself with startActivity.

Completion Checklist

  • The video plays within 2–3 seconds of tapping the button
  • For a live campaign, chat and the live badge are visible (screen terminology ②③)
  • On playback failure, the code/message of the error event is logged (error handling)

Related: PIP · eventsNext → Mission 2 · Launch via Deep Link

Missions · Player

Mission 2 · Launch the Player via Deep Link

GOALTapping a myapp://live?campaign=XXXX link from push/SMS/web → the app opens and playback starts immediately
PREREQUISITESMission 1 complete · app URL scheme (or App Links/Universal Links) configured

The SDK has no dedicated deep link API — you parse the campaignKey out of the deep link and play exactly as in Mission 1.

  1. Receive the URL in your OS/framework deep link handler.
  2. Pull campaignKey (and referrer if needed — ShoplivePlayOptions) out of the query.
  3. Confirm that initialization and user setup (Mission 3) are complete, then present the player.
// SceneDelegate
func scene(_ scene: UIScene, openURLContexts ctxs: Set<UIOpenURLContext>) {
    guard let url = ctxs.first?.url,
          let comps = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let key = comps.queryItems?.first(where: { $0.name == "campaign" })?.value
    else { return }

    let referrer = comps.queryItems?.first(where: { $0.name == "ref" })?.value
    let player = ShoplivePlayerViewController(
        campaignKey: key,
        options: .init(referrer: referrer))
    topViewController?.present(player, animated: true)
}
// Activity that receives the deep link
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val data: Uri? = intent?.data           // myapp://live?campaign=XXXX
    val key = data?.getQueryParameter("campaign") ?: return
    val ref = data.getQueryParameter("ref")

    ShoplivePlayer(this).start(
        campaignKey = key,
        options = ShoplivePlayOptions(referrer = ref))
}
// Call with the deep link URI your app receives (for example from app_links or go_router)
Future<void> openLive(Uri uri) async {
  final key = uri.queryParameters["campaign"];   // myapp://live?campaign=XXXX
  if (key == null) return;
  final ref = uri.queryParameters["ref"];

  await ShoplivePlayer.instance.start(key,
      playOptions: ShoplivePlayOptions(referrer: ref));
}
Watch out for cold starts A deep link may cold start your app. Make sure Shoplive.initialize and setUser finish before playback. Placing initialization in AppDelegate/Application keeps this safe (Quick Start).

Completion Checklist

  • Tapping the link with the app closed (cold start) leads all the way to playback
  • It behaves identically with the app already running (warm start)
  • referrer is reflected in campaign traffic statistics (ShoplivePlayOptions)

Previous ← Mission 1Next → Mission 3 · Connect User Information

Missions · Player

Mission 3 · Connect User Information

GOALHave viewing, chat, and statistics attributed to your own service members
PREREQUISITESMission 1 complete · (for secure auth) backend JWT issuance ready (step 4)
setUser passes user information — it does not authenticate the user Sign-in (ID · password, social login and so on) stays entirely in your app. setUser only tells Shoplive who the current viewer is so that chat, viewing and statistics are attributed to that member. Shoplive does not check the user's credentials. With token, Shoplive can verify that the information was issued by your server and not altered (signature check); with profile, the information is passed as is, without that check.

Step 1 · Choose by sign-in state — recommended setup

UserRecommendedWhy
Signed outCall nothingThe server issues the guest identifier. guest does nothing, so use logout() to turn a signed-in user back into a guest
Signed in (recommended)token — a JWT signed by your serverIt can't be forged, so you can use it for grade-based entry limits, purchase integration, and accurate viewer stats
Signed in (when backend work is hard)profile — pass id, name, and so onThe SDK builds an unsigned JWT (simple auth). It's enough for identification and stats, but don't use it for authorization
Log out · reset user infoShoplive.logout()Clears the user info you set (JWT and profile) and also clears the broadcasting token. Your accessKey, guest identifier and UTM stay. There is no call that clears only the user info (guest does nothing)
Recommended flow App launch (signed out) → do nothing → signed in: right before each playback, get a newly issued JWT from your backend and call setUser(.token(jwt:)) → on sign-out, reset the user info with logout(). User changes apply from the next playback. You can mix modes per screen — for example, show the home preview (Mission 4) signed out and prompt sign-in when the viewer opens full playback.

Step 2 · Call setUser

// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
Shoplive.setUser(.token(jwt: myJwt))

// ② Signed in · simple auth — pass the profile directly (the SDK builds an unsigned JWT)
Shoplive.setUser(.profile(
    id: hashedUserId,            // a hash is recommended over the raw ID (Step 3)
    name: "LiveFan", age: 27, gender: .female,
    rank: 10))                    // user grade (-100 to 100)

// ③ Signed out — no call needed

// Log out — clears the user info (JWT, profile) and also the broadcasting token (accessKey, guest identifier and UTM stay)
Shoplive.logout()
// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
Shoplive.setUser(ShopliveUser.Token(myJwt))

// ② Signed in · simple auth — pass the profile directly (the SDK builds an unsigned JWT)
Shoplive.setUser(ShopliveUser.Profile(
    id = hashedUserId,           // a hash is recommended over the raw ID (Step 3)
    name = "LiveFan", age = 27, gender = ShopliveGender.FEMALE,
    rank = 10))                    // user grade (-100 to 100)

// ③ Signed out — no call needed

// Log out — clears the user info (JWT, profile) and also the broadcasting token (accessKey, guest identifier and UTM stay)
Shoplive.logout()
// ① Signed in · secure auth (recommended) — a JWT signed by your backend (Step 4)
await Shoplive.setUser(ShopliveUser.token(myJwt));

// ② Signed in · simple auth — pass the profile directly (the SDK builds an unsigned JWT)
await Shoplive.setUser(ShopliveUser.profile(
    id: hashedUserId,            // a hash is recommended over the raw ID (Step 3)
    name: "LiveFan", age: 27, gender: ShopliveGender.female,
    rank: 10));                    // user grade (-100 to 100)

// ③ Signed out — no call needed

// Sign-out — clears user info and tokens
await Shoplive.logout();

Step 3 · ID hashing — don't expose raw member IDs

When you'd rather not pass your internal member ID as-is, use a value that your app and backend hash the same way as the id. The key is using a consistent salt and algorithm so the same user always produces the same value (for statistics and return-visitor identification).

View hashing code samples (Swift · Kotlin · Dart)
import CryptoKit

func hashUserId(_ raw: String) -> String {
    let salted = raw + "APP_SALT"
    let digest = SHA256.hash(data: Data(salted.utf8))
    return digest.map { String(format: "%02x", $0) }.joined()
}
import java.security.MessageDigest

fun hashUserId(raw: String): String {
    val salted = raw + "APP_SALT"
    return MessageDigest.getInstance("SHA-256")
        .digest(salted.toByteArray())
        .joinToString("") { "%02x".format(it) }
}
import "dart:convert";
import "package:crypto/crypto.dart";   // pub: crypto

String hashUserId(String raw) {
  final salted = raw + "APP_SALT";
  return sha256.convert(utf8.encode(salted)).toString();
}
Security note Hashing only obscures the raw value — it is not authentication. If you need to verify viewing permissions, always use a server-signed JWT (token). We recommend agreeing on the salt policy with the Shoplive team.

Step 4 · Create the JWT for secure auth (your backend)

The JWT for token auth is signed by your backend with the Secret Key that Shoplive issues. The app only fetches the JWT from your backend after sign-in and passes it to setUser(.token(jwt:)).

  1. Get the Secret Key from your Shoplive contact. It's a base64-encoded string
  2. Build the payload below from the signed-in user and sign it with HS256
  3. Issue a new JWT every time the app asks (right before each playback). Don't store a JWT in the app and reuse it
Generate the JWT fresh each time Don't cache a JWT on the device or reuse an old one. User details (name, grade, type) may have changed since the last issue, and a token that leaks stays usable until its exp. Have the app request a new JWT from your backend whenever it calls setUser.
claimRequiredDescriptionExample
expRequiredExpiration time (Unix seconds)1516239022
userIdRequiredUnique value for the signed-in user (up to 255 bytes). A hashed value works too (Step 3)sample_user_id
iatIssued-at time (Unix seconds)1516195822
nameName shown in chat. Banned words are masked with asterisks (*)LiveFan
genderGender — m · f · empty (unspecified)f
ageAge. Stats group it in 5-year bands27
userScoreUser grade (-100 to 100). The basis for grade-based entry and prize-draw limits10
userTypeUser type. Lets only certain types into broadcasts with an entry restriction (Entry Type)VIP
profileChat profile image URL (64×64px recommended)https://…/profile.png
customArbitrary data (up to 1 KB){"grade":"vip"}

Signing examples. Both produce the same token — just note that the Secret Key is a base64 string.

// Node.js — npm i jsonwebtoken
const jwt = require("jsonwebtoken");

const secret = Buffer.from(process.env.SHOPLIVE_SECRET_KEY, "base64");  // base64-decode it into bytes
const token = jwt.sign(
  { userId: hashedUserId, name: "LiveFan", gender: "f", age: 27, userScore: 10 },
  secret,
  { algorithm: "HS256", expiresIn: "12h" }   // fills in exp and iat automatically
);
// Java — io.jsonwebtoken (jjwt)
String jwt = Jwts.builder()
    .signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey)   // pass the base64 string as is
    .setExpiration(new Date(System.currentTimeMillis() + 12 * 60 * 60 * 1000L))
    .setIssuedAt(new Date())
    .claim("userId", hashedUserId)
    .claim("name", "LiveFan")
    .claim("gender", "f")
    .claim("age", 27)
    .claim("userScore", 10)
    .compact();
Keep the Secret Key on your backend If it ships in app code or the bundle, anyone can extract it and forge tokens for other users. The simple-auth (profile) JWT is unsigned, so you can't use it for authorization.

Completion Checklist

  • After signing in, the chat nickname shows the member's name
  • The user ID (hash) can be looked up in Shoplive Admin statistics
  • After you reset the user info with Shoplive.logout(), viewing continues as a guest

Previous ← Mission 2Next → Mission 4 · Embed in a Screen (Preview)

Missions · Player

Mission 4 · Embed in a Screen (Preview)

GOALPut a broadcast preview inside your home feed or list, and open full playback on tap
PREREQUISITESMission 1 complete

Players inside a feed or list run as a preview. They receive the low-resolution stream the server provides, so you can show several on one screen with little traffic, and a tap moves the viewer into full playback (Mission 1). Pick one of two approaches depending on what your screen is built with.

ApproachWhenWhat draws the previewOn tap
View approach (native)Your feed or list is a native screenThe SDK's ShoplivePlayerView + type = .previewOpen the native full-screen player
Hybrid integration (Web + Mobile SDK)Your feed or list is a web page (WebView)The web page draws it with the Web SDKThe web passes the campaignKey to the app, and the app opens the native player

View approach — native preview

// Preview preset — low resolution (type = .preview), mute, hidden overlay and in-App PIP policy in one go
let playerView = ShoplivePlayerView(configuration: .preview)
container.addSubview(playerView)       // Auto Layout is up to the host app
playerView.delegate = self
playerView.play(campaignKey: "YOUR_CAMPAIGN_KEY")
val playerView = ShoplivePlayerView(context)   // free to place in XML/Compose (AndroidView)
// Preview preset — low resolution (PREVIEW), mute, hidden overlay and in-App PIP policy in one go
playerView.configuration = ShoplivePlayerConfiguration.PREVIEW
container.addView(playerView)

playerView.delegate = myDelegate
playerView.bindLifecycle(viewLifecycleOwner)   // automates pause/resume/release
playerView.play(campaignKey = "YOUR_CAMPAIGN_KEY")
// Flutter 3.0.3 still has no widget to embed in a feed (no ShoplivePlayerView).
// Closest option: start the preview stream as a small in-app PIP window
await ShoplivePlayer.instance.start("YOUR_CAMPAIGN_KEY",
    playOptions: const ShoplivePlayOptions(presentationStyle: ShoplivePresentationStyle.pip),
    configuration: const ShoplivePlayerConfiguration(
        type: ShoplivePlayerType.preview,                            // preview stream (low resolution)
        sound: ShopliveSoundOptions(muteOnStart: true),              // start muted
        overlay: ShopliveOverlayOptions(ui: ShopliveOverlayUIMode.hidden),   // hide overlays
        pip: ShoplivePipOptions(isInAppPipEnabled: true)));          // required for presentationStyle: pip

Preview settings — what changes

SettingEffect
type = .preview
(Android ShoplivePlayerType.PREVIEW)
Receives the server's preview stream (low resolution). Campaigns without a preview stream play the main stream. The default is .live (full resolution).
sound.muteOnStart = trueStarts muted. Turn sound on with isMuted = false
overlay.ui = .hiddenHides overlays such as chat and products. You can show them again during playback with overlayUI
Android sound.isVolumeKeyEnabledWhether the volume keys adjust the sound. If unset, PREVIEW turns it off and LIVE turns it on
One preset — iOS ShoplivePlayerConfiguration.preview and Android ShoplivePlayerConfiguration.PREVIEW set the values in the table above (type = .preview, mute, hidden overlays) plus an in-App PIP policy in one go. You can override just what you need — on iOS start from var config = ShoplivePlayerConfiguration.preview and set, for example, config.overlay.ui = .builtIn; on Android use ShoplivePlayerConfiguration.PREVIEW.copy(…). To start as a small floating window instead of inside a feed, pair it with ShoplivePlayOptions.preview (Android ShoplivePlayOptions.PREVIEW) (Mission 5).

Tap → full playback

The preview is low resolution, so don't scale it up. Open a new full-resolution (.live) player and stop the preview view.

playerView.stop()                      // stop the preview
let player = ShoplivePlayerViewController(campaignKey: "YOUR_CAMPAIGN_KEY")   // opens at full resolution
present(player, animated: true)
playerView.stop()                      // stop the preview
ShoplivePlayer(context).start(campaignKey = "YOUR_CAMPAIGN_KEY")   // default type = LIVE
// Switch to full playback at full resolution — on iOS a running session switches in place
await ShoplivePlayer.instance.start("YOUR_CAMPAIGN_KEY",
    configuration: const ShoplivePlayerConfiguration(type: ShoplivePlayerType.live));

Hybrid integration (Web + Mobile SDK)

When integrating a hybrid app, we recommend keeping the broadcast list in the WebView and handing off to the native player only when a broadcast is tapped.

ApproachWhat you manage
Build the list natively tooYou implement the list UI natively and open a preview per item (View approach above). List layout, paging and state refresh are all yours to manage.
Reuse the WebView list (this section)The plugin's setOverall keeps rendering the list inside the WebView. Only the tapped broadcast's key is passed to native, and native just plays it.
Only campaignKey crosses the bridge The WebView and native are separate layers. Native already holds the accessKey (Shoplive.initialize), so the only value the WebView passes to native is campaignKey.
  1. WebView — render the list, and pass campaignKey to native on tap
  2. Native — receive the value
  3. Native — play it with the SDK

Step 1 · Render the list and pass the key to native on tap

setOverall renders the list. In the ON_CLICK_CAMPAIGN_LIST_ITEM callback, send the campaignKey to the native interface. When no native interface exists (a plain web browser), it falls back to the default web modal player, so the same page is safe to ship both on the web and inside the app.

<script src="https://static.shoplive.cloud/shoplive.js"></script>
<script>
  var APP_INTERFACE = "ShopLiveAppInterface";
  var messageCallback = {
    ON_CLICK_CAMPAIGN_LIST_ITEM: function (payload) {
      var campaignKey = payload.campaign.campaignKey;   // payload = { ak, campaign }
      // Android · iOS — send only campaignKey to the native interface
      if (window[APP_INTERFACE]) {
        window[APP_INTERFACE].postMessage(campaignKey);                        // Android
      } else if (window.webkit?.messageHandlers?.[APP_INTERFACE]) {
        window.webkit.messageHandlers[APP_INTERFACE].postMessage(campaignKey); // iOS
      } else {
        // No native interface (plain web) — fall back to the web modal player
        cloud.shoplive.showFeaturedPlayerModal({ campaignKey: campaignKey });
      }
    }
  };
  cloud.shoplive.initPlugin({ accessKey: "YOUR_ACCESS_KEY", messageCallback: messageCallback });
</script>
<div id="shoplive-overall"></div>
<script defer>
  cloud.shoplive.setOverall("shoplive-overall");
</script>
iOS WebView setting Inline playback needs allowsInlineMediaPlayback = true on WKWebViewConfiguration (a WebView setting, separate from the code above).

Step 2 · Receive the value on native

Register the interface name the code above calls (ShopLiveAppInterface) on the native side. This registration is not a Shoplive SDK feature — it is the standard JS ↔ native bridge each WebView provides. Only the campaignKey string is exchanged, so native receives it as is, with no parsing.

let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.userContentController.add(self, name: "ShopLiveAppInterface")   // register before creating the WKWebView (with this configuration)
let webView = WKWebView(frame: .zero, configuration: configuration)

// WKScriptMessageHandler
func userContentController(_ controller: WKUserContentController,
                           didReceive message: WKScriptMessage) {
    guard message.name == "ShopLiveAppInterface",
          let campaignKey = message.body as? String else { return }
    playLive(campaignKey)   // Step 3 · play natively
}
webView.addJavascriptInterface(ShopLiveAppInterface(activity), "ShopLiveAppInterface")

class ShopLiveAppInterface(private val activity: Activity) {
    @JavascriptInterface                     // called on a background thread
    fun postMessage(campaignKey: String) {
        activity.runOnUiThread { playLive(activity, campaignKey) }   // Step 3 · play natively
    }
}
// webview_flutter — exposes window.ShopLiveAppInterface.postMessage to the page
controller.addJavaScriptChannel(
  "ShopLiveAppInterface",
  onMessageReceived: (JavaScriptMessage message) {
    playLive(message.message);   // Step 3 · play natively
  },
);
The bridge is callable from any page in the WebView If the WebView can navigate to URLs other than your own page (product links, for example), those pages can call ShopLiveAppInterface too. Open external destinations in a new window or the external browser so that only your page stays inside the WebView.

Step 3 · Play on native

The single campaignKey received through the bridge is enough to open the full-screen player — the same as Mission 1. This is guest playback, assuming Shoplive.initialize already ran at app startup (Quick Start).

func playLive(_ campaignKey: String) {
    let player = ShoplivePlayerViewController(campaignKey: campaignKey)
    present(player, animated: true)
}
fun playLive(context: Context, campaignKey: String) {
    ShoplivePlayer(context).start(campaignKey = campaignKey)
}
Future<void> playLive(String campaignKey) =>
    ShoplivePlayer.instance.start(campaignKey);

Playing as a signed-in user (optional)

Skip this for guest playback. For a signed-in viewer, call setUser right before playback (Mission 3). Issue the JWT for the same user as the JWT used by the WebView plugin. Where the value comes from depends on your app's login structure.

Login lives inWhat to do
Native (the app manages login)Native already holds the value. Keep the bridge payload as is (campaignKey only) and call setUser right before Step 3.
Only the WebView sessionNative doesn't know the user. Send the auth value with the Step 1 payload as an object, and change Step 2 to parse JSON instead of a plain string.
// Extend Step 1 — send an object (JWT example)
var bridgePayload = { campaignKey: campaignKey, authToken: currentUserJWT };
if (window[APP_INTERFACE]) {
  window[APP_INTERFACE].postMessage(JSON.stringify(bridgePayload));       // Android — String only
} else if (window.webkit?.messageHandlers?.[APP_INTERFACE]) {
  window.webkit.messageHandlers[APP_INTERFACE].postMessage(bridgePayload); // iOS — object as is
}
// Signed in — right before playback (Mission 3)
Shoplive.setUser(.token(jwt: jwt))
// then play as in Step 3
// Signed in — right before playback (Mission 3)
Shoplive.setUser(ShopliveUser.Token(jwt))
// then play as in Step 3
// Signed in — right before playback (Mission 3)
await Shoplive.setUser(ShopliveUser.token(jwt));
// then play as in Step 3
Platform differences The Android View approach does not provide OS PIP (minimizing the whole app) — an embedded view is owned by the host Activity. If you need OS PIP, use the Activity approach (Mission 1). In-App floating PIP (Mission 5) is supported on both platforms and in both forms.

Completion Checklist

  • Several previews in a feed play without stuttering (preview stream)
  • Playback continues while the feed scrolls (Android: connect bindLifecycle)
  • A tap opens full playback at full resolution
  • (Hybrid integration) A preview tap on the web leads to the app's native player

Previous ← Mission 3Next → Mission 5 · Enable PIP

Missions · Player

Mission 5 · Enable PIP

GOALKeep watching in a small window even when the user navigates elsewhere or leaves for the home screen
PREREQUISITESMission 1 or Mission 4 complete

PIP uses the same methods for full screen/View and iOS/Android alike. “Which view goes into PIP” is something the SDK already knows — the host never has to hand over a view.

TypeDescriptionControl
in-App PIPA small floating window inside the app. Persists across screen transitionsenterPictureInPicture() / exitPictureInPicture()
OS PIPKeeps playing in the system PIP window even after leaving for the home screenconfiguration.pip.isOSPipEnabled (automatic, field dictionary)
player.enterPictureInPicture()      // shrink to a small window
player.exitPictureInPicture()       // back to normal
if player.isInPictureInPicture { /* … */ }

// Position, scale, and so on come from configuration
var cfg = ShoplivePlayerConfiguration()
cfg.pip.defaultPosition = .bottomRight
cfg.pip.scale = 0.4
cfg.pip.isOSPipEnabled = true
player.enterPictureInPicture()      // shrink to a small window
player.exitPictureInPicture()       // back to normal
if (player.isInPictureInPicture) { /* … */ }

// Position, scale, and so on come from configuration
val cfg = ShoplivePlayerConfiguration(
    pip = ShoplivePlayerConfiguration.PipOptions(
        defaultPosition = ShoplivePipPosition.BOTTOM_RIGHT,
        scale = 0.4f,
        isOSPipEnabled = true))     // OS PIP is available only with the Activity approach
final player = ShoplivePlayer.instance;
await player.enterPictureInPicture();   // shrink to a small window
await player.exitPictureInPicture();    // back to normal
if (await player.isInPictureInPicture()) { /* … */ }

// Position, scale, and so on come from configuration (merged on top of the current settings)
await player.setConfiguration(const ShoplivePlayerConfiguration(
    pip: ShoplivePipOptions(
        defaultPosition: ShoplivePipPosition.bottomRight,
        scale: 0.4,
        isOSPipEnabled: true)));   // Android: also add android:supportsPictureInPicture="true" to the host Activity
Automatic PIP — switching without a call When navigating to a product landing page: navigation.actionOnNavigation = PIP (Mission 6) · when the app goes to the background: pip.isOSPipEnabled — all switch automatically through configuration alone.

Starting directly in PIP

The code above moves a playing stream into a small window. You can also skip full screen entirely and start in the small window — for a preview on a product page, for example.

var options = ShoplivePlayOptions()
options.presentationStyle = .pip            // default is .fullScreen

player.play(campaignKey: key, options: options)
ShoplivePlayer(this).start(
    campaignKey = key,
    options = ShoplivePlayOptions(
        presentationStyle = ShoplivePlayerPresentationStyle.PIP))   // default FULLSCREEN
await ShoplivePlayer.instance.start(key,
    playOptions: const ShoplivePlayOptions(
        presentationStyle: ShoplivePresentationStyle.pip));   // default is fullscreen — fails with INVALID_ARGUMENT if pip.isInAppPipEnabled is off
FieldValueMeaning
presentationStyleFULLSCREEN (default) / PIP Which surface this playback starts on. The default is today's behaviour, so existing code is unaffected
Both platforms start in in-App PIP Starting with PIP plays in the floating window the SDK owns (in-App PIP). If pip.isInAppPipEnabled is off, iOS silently plays full screen instead. On Android, an intent built with ShoplivePlayer.intent() always opens full screen and ignores PIP — use start() to start in PIP.

Preview presets

"Small window + muted + overlay hidden" is common enough that iOS ships it as constants. Configuration and start options are separate, so pass both. Android has no presets, so set the same values directly.

player.configuration = .preview                     // muted · overlay hidden · in-App PIP
player.play(campaignKey: key, options: .preview)    // presentationStyle = .pip

// Start from the preset and adjust (value type, so it copies)
var cfg = ShoplivePlayerConfiguration.preview
cfg.pip.defaultPosition = .bottomLeft
ShoplivePlayer(this).start(
    campaignKey = key,
    options = ShoplivePlayOptions(presentationStyle = ShoplivePlayerPresentationStyle.PIP),
    configuration = ShoplivePlayerConfiguration(
        sound = ShoplivePlayerConfiguration.SoundOptions(muteOnStart = true),             // muted
        overlay = ShoplivePlayerConfiguration.OverlayOptions(ui = ShopliveOverlayUIMode.HIDDEN)))  // overlay hidden
// Flutter has no presets — set the same values directly
await ShoplivePlayer.instance.start(key,
    playOptions: const ShoplivePlayOptions(presentationStyle: ShoplivePresentationStyle.pip),
    configuration: const ShoplivePlayerConfiguration(
        sound: ShopliveSoundOptions(muteOnStart: true),                       // muted
        overlay: ShopliveOverlayOptions(ui: ShopliveOverlayUIMode.hidden)));   // overlay hidden

Completion Checklist

  • Tapping the minimize button or calling the API switches to a small window that can be dragged around
  • Tapping the small window returns to full screen without interrupting playback
  • Leaving for the home screen continues into OS PIP (when configured)

Previous ← Mission 4Next → Mission 6 · Events · Products · Coupons

Missions · Player

Mission 6 · Integrating Events, Products, and Coupons

GOALRoute a product tap to your own detail page, handle coupon issuance, and receive status events
PREREQUISITESMission 1 complete · product URL routing rules (client settings)

Delegate Structure — Exactly Two Methods

ChannelNatureExamples
event (notification)Handle only the cases you needState changes · campaign info/status · playback · connection state · analytics · errors · web commands
request (response required)You must call respondProduct landing (navigation) · coupons · custom actions
extension MyViewController: ShoplivePlayerDelegate {

  // Notifications — all cases. Feel free to keep only the cases you need
  func player(_ p: ShoplivePlayerControlling, didReceive event: ShoplivePlayerEvent) {
    switch event {
    case .stateChanged(let s):              // playback lifecycle (includes entering/leaving .inAppPIP)
        print("state:", s)
    case .campaignStatusChanged(let c):     // .ready / .live / .ended
        liveBadge.isHidden = (c != .live)
    case .campaignInfoReceived(let info):   // campaign metadata received (title, brand, etc.)
        titleLabel.text = info.title
    case .playback(let pb):                 // pb.event: .started / .rendering / .rebufferingStarted / .rebufferingEnded / .ended …
        if pb.event == .rebufferingStarted { spinner.isHidden = false }
        if pb.event == .rebufferingEnded { spinner.isHidden = true }
    case .connectionStateChanged(let cs):   // common connection state
        networkBanner.isHidden = (cs != .reconnecting)
    case .userNameUpdateRequested(let payload):  // nickname change requested → app profile screen
        openProfileEditor(payload)
    case .analytics(let info):              // viewing statistics → forward to your own analytics
        tracker.log(info.campaignKey, info.isPlaying, info.durationMs)
    case .error(let e):                     // only unrecoverable errors are delivered
        show(e.message)
    case .webCommand(let name, let payload):  // custom command sent by the overlay (web) — e.g. a login request
        handleWebCommand(name, payload)
    }
  }

  func player(_ p: ShoplivePlayerControlling, didRequest request: ShoplivePlayerRequest) {
    switch request {
    case .navigation(let url):  router.open(url)      // product/banner landing
    case .coupon(let id, let respond):            // report the result after issuing — status: popup handling (.show/.hide/.keep), alertType: .alert/.toast
        respond(ShopliveCouponResult(couponId: id, success: true, status: .hide, alertType: .toast))
    case .customWebAction(let id, _, _, let respond):  // report the result after performing the action
        respond(ShopliveCustomActionResult(id: id, success: true, status: .keep, alertType: .toast))
    }
  }
}
val myDelegate = object : ShoplivePlayerDelegate {

  // Notifications — all cases. Feel free to keep only the cases you need (default implementations are empty)
  override fun onEvent(p: ShoplivePlayerControlling, event: ShoplivePlayerEvent) {
    when (event) {
      is ShoplivePlayerEvent.StateChanged ->             // playback lifecycle (includes entering/leaving IN_APP_PIP)
          log(event.state)
      is ShoplivePlayerEvent.CampaignStatusChanged ->    // READY / LIVE / ENDED
          liveBadge.isVisible = event.status == ShopliveCampaignStatus.LIVE
      is ShoplivePlayerEvent.CampaignInfoReceived ->     // campaign metadata received (title, brand, etc.)
          titleView.text = event.info.title
      is ShoplivePlayerEvent.Playback -> when (event.event) {  // event.event: Started / Rendering / RebufferingStarted / RebufferingEnded / Ended …
          is ShoplivePlaybackEvent.RebufferingStarted -> spinner.isVisible = true
          is ShoplivePlaybackEvent.RebufferingEnded -> spinner.isVisible = false
          else -> Unit
      }
      is ShoplivePlayerEvent.ConnectionStateChanged ->   // common connection state
          networkBanner.isVisible = event.state == ShopliveConnectionState.RECONNECTING
      is ShoplivePlayerEvent.UserNameUpdateRequested ->  // nickname change requested → app profile screen
          openProfileEditor(event.payload)
      is ShoplivePlayerEvent.Analytics ->                // viewing statistics → forward to your own analytics
          tracker.log(event.info.campaignKey, event.info.isPlaying, event.info.durationMs)
      is ShoplivePlayerEvent.Error ->                    // only unrecoverable errors are delivered
          show(event.error.message)
      is ShoplivePlayerEvent.WebCommandReceived ->      // custom command sent by the overlay (web) — e.g. a login request
          handleWebCommand(event.name, event.payload)
    }
  }

  override fun onRequest(p: ShoplivePlayerControlling, request: ShoplivePlayerRequest) {
    when (request) {
      is ShoplivePlayerRequest.Navigation -> router.open(request.url)
      is ShoplivePlayerRequest.Coupon ->             // report the result after issuing — status: popup handling (SHOW/HIDE/KEEP), alertType: ALERT/TOAST
          request.respond(ShopliveCouponResult(couponId = request.id, success = true,
              status = ShopliveResultStatus.HIDE, alertType = ShopliveResultAlertType.TOAST))
      is ShoplivePlayerRequest.CustomWebAction ->    // report the result after performing the action
          request.respond(ShopliveCustomActionResult(id = request.id, success = true,
              status = ShopliveResultStatus.KEEP, alertType = ShopliveResultAlertType.TOAST))
    }
  }
}
final player = ShoplivePlayer.instance;

// Notifications — the events stream. Keep only the cases you need
player.events.listen((event) {
  switch (event) {
    case ShoplivePlayerStateChanged(:final state):          // playback lifecycle (includes inAppPip)
      log(state);
    case ShopliveCampaignStatusChanged(:final status):      // ready / live / ended
      showLiveBadge(status == ShopliveCampaignStatus.live);
    case ShopliveCampaignInfoReceived(:final info):         // campaign metadata received (title, brand, etc.)
      setTitle(info.title);
    case ShoplivePlaybackEvent(:final kind):                // playback kind: started / rendering / rebufferingStarted / rebufferingEnded / ended …
      if (kind == ShoplivePlaybackEventKind.rebufferingStarted) showSpinner(true);
      if (kind == ShoplivePlaybackEventKind.rebufferingEnded) showSpinner(false);
    case ShopliveConnectionStateChanged(:final state):      // common connection state
      showNetworkBanner(state == ShopliveConnectionState.reconnecting);
    case ShopliveUserNameUpdateRequested(:final payload):   // nickname change requested → app profile screen
      openProfileEditor(payload);
    case ShopliveAnalyticsEvent e:                          // viewing statistics → forward to your own analytics
      tracker.log(e.campaignKey, e.isPlaying, e.durationMs);
    case ShoplivePlayerErrorEvent(:final error):            // only unrecoverable errors are delivered
      show(error.message);
    case ShopliveWebCommandReceived(:final name, :final payload):   // custom command sent by the overlay (web) — e.g. a login request
      handleWebCommand(name, payload);
    default:
      break;
  }
});

// Requests — one stream per kind. Keep the listeners for the whole session
player.navigationRequests.listen((request) => router.open(request.url));
player.couponRequests.listen((request) {                    // report the result after issuing (respond is valid once; times out after 10 s by default)
  request.respond(const ShopliveCouponResult(success: true,
      status: ShopliveResultStatus.hide, alertType: ShopliveResultAlertType.toast));
});
player.customWebActionRequests.listen((request) {           // report the result after performing the action
  request.respond(const ShopliveCustomActionResult(success: true,
      status: ShopliveResultStatus.keep, alertType: ShopliveResultAlertType.toast));
});

ShoplivePlayerEvent Types — Notifications Delivered via didReceive event

All of them are optional to handle — pick the cases you need and ignore the rest. The notation follows iOS (camelCase); on Android these are sealed classes with the same names (ShoplivePlayerEvent.StateChanged, etc.), except that webCommand is named WebCommandReceived.

View the full ShoplivePlayerEvent declaration (Swift · Kotlin · Dart)
public enum ShoplivePlayerEvent {
    case stateChanged(ShoplivePlayerState)                  // entering/leaving .inAppPIP is also reported by this event
    case campaignStatusChanged(ShopliveCampaignStatus)      // .ready / .live / .ended
    case campaignInfoReceived(ShopliveCampaignInfo)
    case playback(PlaybackEvent)                    // started/rendering/rebuffering/ended…
    case connectionStateChanged(ShopliveConnectionState)    // common connection state
    case userNameUpdateRequested([String: Any])
    case analytics(info: ShopliveAnalyticsInfo)
    case error(ShopliveError)
    case webCommand(name: String, payload: [String: Any]?)  // overlay (web) → app custom command
}

// ShopliveCore — the kind is in event
public class PlaybackEvent: NSObject {
    public let event: PlaybackEventType
    public let datas: [String: Any]
}
public enum PlaybackEventType: Int {
    case request, started, audioLoaded, rendering, ended,
         rebufferingStarted, rebufferingEnded, error, buffering
}

public struct ShopliveAnalyticsInfo {
    public let campaignKey: String
    public let isPlaying: Bool
    public let isMuted: Bool
    public let durationMs: Int64
    public let campaign: ShopliveCampaign?
    public let brand: ShopliveBrand?
}
sealed class ShoplivePlayerEvent {
    data class StateChanged(val state: ShoplivePlayerState) : ShoplivePlayerEvent()               // entering/leaving IN_APP_PIP is also reported by this event
    data class CampaignStatusChanged(val status: ShopliveCampaignStatus) : ShoplivePlayerEvent()  // READY / LIVE / ENDED
    data class CampaignInfoReceived(val info: ShopliveCampaignInfo) : ShoplivePlayerEvent()
    data class Playback(val event: ShoplivePlaybackEvent) : ShoplivePlayerEvent()         // the kind is the subtype
    data class ConnectionStateChanged(val state: ShopliveConnectionState) : ShoplivePlayerEvent() // common connection state
    data class UserNameUpdateRequested(val payload: Map<String, Any?>) : ShoplivePlayerEvent()
    data class Analytics(val info: ShopliveAnalyticsInfo) : ShoplivePlayerEvent()
    data class Error(val error: ShopliveError) : ShoplivePlayerEvent()
    data class WebCommandReceived(val name: String, val payload: Map<String, Any?>) : ShoplivePlayerEvent()  // overlay (web) → app custom command
}

sealed class ShoplivePlaybackEvent {
    abstract val timestampMs: Long?
    // Requested · Started · Rendering · Buffering · AudioLoaded · RebufferingStarted · RebufferingEnded · Ended
    data class Failed(override val timestampMs: Long?, val code: Int? = null, val message: String? = null) : ShoplivePlaybackEvent()
}

data class ShopliveAnalyticsInfo(
    val campaignKey: String, val isPlaying: Boolean, val isMuted: Boolean, val durationMs: Long,
    val campaign: ShopliveCampaign? = null, val brand: ShopliveBrand? = null,
)
// Events arrive on ShoplivePlayer.instance.events as these subclasses
sealed class ShoplivePlayerEvent {}
class ShoplivePlayerStateChanged { final ShoplivePlayerState state; }          // entering/leaving inAppPip is also reported here
class ShopliveCampaignStatusChanged { final ShopliveCampaignStatus status; }    // ready / live / ended
class ShopliveCampaignInfoReceived { final ShopliveCampaignInfo info; }
class ShoplivePlaybackEvent { final ShoplivePlaybackEventKind kind; final int? timestampMs, code; final String? message; }
// kind: requested · started · audioLoaded · rendering · buffering · rebufferingStarted · rebufferingEnded · ended · failed (code/message only for failed)
class ShopliveConnectionStateChanged { final ShopliveConnectionState state; }
class ShopliveUserNameUpdateRequested { final Map<String, Object?> payload; }
class ShopliveAnalyticsEvent { final String campaignKey; final bool isPlaying, isMuted; final int durationMs;
                               final ShopliveCampaign? campaign; final ShopliveBrand? brand; }
class ShoplivePlayerErrorEvent { final ShopliveError error; }
class ShopliveWebCommandReceived { final String name; final Map<String, Object?> payload; }   // overlay (web) → app custom command
class ShoplivePlayerUnknownEvent { final String rawType; }                     // unknown or malformed payloads arrive here — the stream never breaks
casepayloadWhen it arrivesMain use
stateChangedShoplivePlayerStatePlayer lifecycle changes (idle → loading → playing…). Entering/leaving in-App PIP (inAppPIP) is reported by this event tooUpdating your app UI based on playback state — Mission 5
campaignStatusChangedShopliveCampaignStatusBroadcast status changes: ready (waiting) → live (on air) → ended (finished)Toggling your own LIVE only UI such as the live badge — screen terminology
campaignInfoReceivedShopliveCampaignInfoOnce, when campaign metadata (title, brand, etc.) has been fetchedShowing campaign information in your own UI
playbackPlaybackEvent (Android ShoplivePlaybackEvent)Playback pipeline events: started / rendering / rebufferingStarted / rebufferingEnded / ended, etc. Tell them apart with pb.event on iOS and by subtype on AndroidShowing loading spinners and buffering notices
connectionStateChangedShopliveConnectionStateConnection state changes (connecting / connected / reconnecting / disconnected)Network connection state notices
userNameUpdateRequested[String: Any]When a viewer requests a nickname change from the overlayOpening your app's profile edit screen — Mission 3
analyticsShopliveAnalyticsInfoWhen a viewing statistics event occurs (campaignKey · isPlaying · isMuted · durationMs · campaign/brand)Forwarding to your own analytics (GA/Amplitude, etc.)
errorShopliveErrorOnly when the session terminates due to an unrecoverable error. Issues being recovered automatically never arrive hereError messaging + retry UI — error handling
webCommandname + payloadWhen the overlay (web) sends a custom command to the app (e.g. a login request). From Android 3.0.3, the web player's user-action logs also arrive with name = "EVENT_LOG"Navigating in the app, prompting login, etc. The other direction is send(command:payload:)

The Three Requests — What Your App Must Do

RequestWhenWhat the app does
navigation(url:)Product/banner tap (screen terminology ④)Navigate to that URL (deep link/web view/native detail). The only mandatory handler
coupon(id:respond:)Coupon button (⑤ LIVE only)After issuing the coupon, report the result with respond(ShopliveCouponResult(…)) → success/failure UI is reflected in the overlay. status (show / hide / keep) sets what happens to the coupon popup and alertType (alert / toast) sets the notice style. As of 3.0.3, an omitted status is hide and an omitted alertType is alert (both platforms; the Flutter 3.0.3 plugin sends toast when you omit alertType). On Android, pass alertType = null to follow the web default
customWebAction(…)Client-defined custom buttonPerform the action, then call respond(ShopliveCustomActionResult(…))

Sending Commands from the App to the Overlay

player.send(command: "HIGHLIGHT_PRODUCT", payload: ["sku": "A-1024"])
player.send(command = "HIGHLIGHT_PRODUCT", payload = mapOf("sku" to "A-1024"))
await ShoplivePlayer.instance.send("HIGHLIGHT_PRODUCT", {"sku": "A-1024"});
Checking network state Network conditions surface through the playback(rebufferingStarted…) and connectionStateChanged events (type dictionary).

Completion Checklist

  • Tapping a product opens your own detail page (the player follows the actionOnNavigation policy — PIP/keep/close — Mission 5)
  • Tapping a coupon shows a success indicator in the overlay after issuance is handled
  • campaignStatusChanged(ended) is received when the broadcast ends

Previous ← Mission 5Next → Mission 7 · UI Customization

Missions · Player

Mission 7 · UI Customization & Repositioning

GOALApply brand colors and fonts, or hide the default UI and place your own UI wherever you want
PREREQUISITESMission 1 · Mission 6 complete (your own UI needs the event data)

Two ways to customize — no SDK update needed

KindWho changes itWhat it coversHow it ships
Native customizationYou, in your app codeStandard appearance options (option ① below) · your own UI on top of the hidden overlay (option ②)With your next app release — uses the APIs of the SDK you already have, so no SDK update
Web-based customizationShopliveThe player overlay (chat, products, notices and so on) is a web layer that Shoplive serves, so its design and layout are changed on the Shoplive sideApplied on the Shoplive side — no SDK update and no app release. Talk to the Shoplive team about what you need

Option ① Standard appearance options — appearance

Loading color, the status bar, the chat font, and similar are changed through configuration alone. If your change is at the level of visibility/styling, this approach is enough.

OptioniOSAndroid
Loading indicator color (screen terminology ⑧)indicatorColor: UIColorindicatorColor: @ColorInt Int
Loading animationloadingAnimationImages: [UIImage]?loadingAnimation: @DrawableRes Int
Status bar visibility—isStatusBarVisible
Chat input font (③)chatInputFont: UIFont?chatInputTypeface: Typeface
Allow screen captureallowScreenCaptureallowScreenCapture

Option ② Rearranging elements — hide the default UI and use your own

This is the approach if you want full freedom over where buttons and elements sit. Hide the default overlay UI (overlay.ui = hidden) and let your app render its own UI from the event/request data in Mission 6 — position, shape, and animation are entirely up to the app.

var cfg = ShoplivePlayerConfiguration()
cfg.overlay.ui = .hidden        // hide the default overlay UI (the command channel stays alive)
let player = ShoplivePlayerViewController(campaignKey: key, configuration: cfg)
// Now your app draws the chat/product/coupon UI itself from the delegate data
val cfg = ShoplivePlayerConfiguration(
    overlay = ShoplivePlayerConfiguration.OverlayOptions(ui = ShopliveOverlayUIMode.HIDDEN)   // hides display only
)
ShoplivePlayer(context).start(campaignKey = key, configuration = cfg)
// Now your app draws the chat/product/coupon UI itself from the delegate data
await ShoplivePlayer.instance.start(key,
    configuration: const ShoplivePlayerConfiguration(
        overlay: ShopliveOverlayOptions(ui: ShopliveOverlayUIMode.hidden)));   // hides display only
// Now your app draws the chat/product/coupon UI itself from the events and requests streams
Built-in overlay vs custom UI
What you can and cannot do There is no public API for moving elements inside the default overlay pixel by pixel. Choose either option ① (standard options) or option ② (your own UI). Even while hidden, the command channel stays alive, so coupon, product, and custom action data keep being delivered (Mission 6).

Completion Checklist

  • Brand colors/fonts are reflected in loading and chat (option ①)
  • Even in the hidden state, product taps and coupon events keep arriving at the delegate (option ②)
  • Your own UI toggles correctly according to the broadcast status (ready/live/ended) (screen terminology)

Previous ← Mission 6Next → Mission 8 · Broadcasting

Missions · Streamer

Mission 8 · Broadcasting

GOALOpen the studio from your app and start/stop a broadcast
PREREQUISITESA broadcasting token (signed with the Secret Key) · camera/mic permissions (installation)

Camera preview, start/stop broadcast, chat, settings, camera switching, mic, and zoom — the SDK provides the entire studio UI (screen terminology). Even implementing a delegate is optional.

Step 1 · Set the broadcasting token

This is a separate path from setUser for viewing (Mission 3). The broadcaster JWT is also signed by your backend using the Secret Key you got from your Shoplive contact.

Step 2 · Launch the studio & receive state

// 1) Broadcasting token (required)
Shoplive.setStreamToken(streamerJwt)

// 2) Launch the studio
let studio = ShopliveStreamerViewController(campaignKey: "YOUR_CAMPAIGN_KEY")
studio.streamerDelegate = self          // optional
present(studio, animated: true)

// 3) Receive state (optional)
extension MyViewController: ShopliveStreamerDelegate {
  func streamer(_ s: ShopliveStreamerViewController, didReceive event: ShopliveStreamerEvent) {
    switch event {
    case .stateChanged(let st): print("broadcast:", st)
    case .error(let e):         show(e.message)
    }
  }
}
// 1) Broadcasting token (required)
Shoplive.setStreamToken(streamerJwt)

// 2) Launch the studio — the SDK also requests runtime permissions
ShopliveStreamer(context).apply {
    streamerDelegate = myStreamerDelegate   // optional
}.start(campaignKey = "YOUR_CAMPAIGN_KEY")

// 3) Receive state (optional)
val myStreamerDelegate = object : ShopliveStreamerDelegate {
  override fun onEvent(s: ShopliveStreamer, event: ShopliveStreamerEvent) {
    when (event) {
      is ShopliveStreamerEvent.StateChanged -> log(event.state)
      is ShopliveStreamerEvent.Error -> show(event.error.message)
    }
  }
}
// 1) Broadcasting token (required)
await Shoplive.setStreamToken(streamerJwt);

final streamer = ShopliveStreamer.instance;
// 3) Receive state (optional) — subscribe before start
streamer.events.listen((event) {
  switch (event) {
    case ShopliveStreamerStateChanged(:final state):
      log(state);
    case ShopliveStreamerErrorEvent(:final error):
      show(error.message);
    default:
      break;
  }
});

// 2) Launch the studio — the SDK also requests runtime permissions
await streamer.start("YOUR_CAMPAIGN_KEY");

Put a web page next to the broadcast screen (optional · 3.0.3+) — pass your web page URL and it sits to the right of the broadcast screen; swipe left to open it. Without it, nothing changes. iOS ShopliveStreamerViewController(campaignKey:webPageURL:) · Android ShopliveStreamer(context).start(campaignKey, webPageUrl = "https://…") · Flutter streamer.start(campaignKey, webPageUrl: "https://…"). The URL must start with http:// or https:// and have a host (in Flutter, an invalid URL fails with INVALID_ARGUMENT before the broadcast screen opens). This page does not get the SDK's native bridge.

ShopliveStreamerEvent Types — Notifications Delivered via didReceive event

The mental model is the same as Player, but because the SDK provides the entire studio UI, there are only two event types. All of them are optional to handle.

View the full ShopliveStreamerEvent declaration (Swift · Kotlin · Dart)
public enum ShopliveStreamerEvent {
    case stateChanged(ShopliveBroadcastState)   // .idle/.preview/.connecting/.live/.reconnecting/.ended
    case error(ShopliveError)           // unrecoverable errors only
}
sealed class ShopliveStreamerEvent {
    data class StateChanged(val state: ShopliveBroadcastState, val isRehearsal: Boolean) : ShopliveStreamerEvent()  // IDLE/PREVIEW/CONNECTING/LIVE/RECONNECTING/ENDED
    data class Error(val error: ShopliveError) : ShopliveStreamerEvent()          // unrecoverable errors only
}
sealed class ShopliveStreamerEvent {}
class ShopliveStreamerStateChanged { final ShopliveBroadcastState state; final bool? isRehearsal; }
// state: idle / preview / connecting / live / reconnecting / ended · isRehearsal: null on iOS (no rehearsal flag)
class ShopliveStreamerErrorEvent { final ShopliveError error; }   // unrecoverable errors only
class ShopliveStreamerUnknownEvent { final String rawType; }
casepayloadWhen it arrivesMain use
stateChangedShopliveBroadcastStateBroadcast lifecycle changes: idle → preview (camera preview) → connecting → live (on air) → ended. Automatic recovery from a brief network drop (reconnecting → live) is also observed through this eventSyncing app state, such as screen transitions and on-air indicators
errorShopliveErrorOnly when the broadcast terminates due to an unrecoverable error (e.g. an invalid token — step 1). Never during automatic recovery such as reconnectionError messaging + retry UI — error handling

ShopliveBroadcastState (type dictionary): idle → preview → connecting → live → (reconnecting) → ended. Broadcast end policies (remaining time, allowing entry while ending, and so on) are still handled by CampaignEndOption.

Forward compatibility Even as broadcasting is extended in the future, the public API will not change, so the integration code you write today keeps working as-is.

Completion Checklist

  • The camera preview appears when you enter the studio (preview state)
  • Starting the broadcast → the campaign plays as LIVE in the viewing app (Mission 1)
  • A brief network drop recovers automatically via reconnecting → live

Previous ← Mission 7Next → Data Field Dictionary

Reference

Data Field Dictionary

The configuration objects of the public API, organized field by field. Every field has a default value, so you only need to specify the ones you need.

ShopliveConfiguration — initialization (once per app)

Used in: Quick Start · Mission 2 (cold start)

FieldTypeDefaultDescription
accessKeyStringRequiredIdentifies your company — issued by Shoplive (what each key does)
allowedWebViewDomains[String]Empty array (all allowed)Domains allowed in the overlay web view. Required domains are included automatically by the SDK (client settings)
attributionShopliveAttribution?NoneTraffic tracking bundle (below)
appVersion (Android)String?NoneRemoved in 3.0.3 — the SDK reads the app version itself, so don't pass it (the Flutter 3.0.3 plugin still has it, but ignores the value and only logs a warning)

ShopliveAttribution — traffic tracking

FieldTypeDescription
utmSource / utmCampaign / utmMedium / utmContentString?The four UTM parameters
advertisingId (iOS)String?Advertising identifier (IDFA) — collecting and passing it is the app's responsibility. iOS only as of 3.0.3

ShopliveUser — user information (3 forms)

Used in: Mission 3 · Connect User Information (including decision criteria, hashing, and the Secret Key)

FormFieldsDescription
tokenjwt: StringServer-signed JWT (recommended) — how to issue one
profileid (required) · name / age / gender / rank (optional) · Android custom (optional, extra key-value)Simple auth — the SDK generates an unsigned JWT (no permission checks). rank is the user grade (-100 to 100). A hash is recommended for id
guest—Signed out — does nothing. The server issues the guest identifier
Shoplive.logout()—Log out · reset user info — clears the user info (JWT and profile) and also the broadcasting token. Your accessKey, guest identifier and UTM stay

ShoplivePlayerConfiguration — playback policy

Used in: Mission 1 · Mission 4 (preview) · Mission 5 (PIP) · Mission 7 (UI)

GroupFieldDefaultDescription
—typelivePlayback form — preview receives the server's preview stream (low resolution) (Mission 4)
pip
Mission 5
isInAppPipEnabledtrueAllow in-App floating PIP (common to VC/Activity and View)
isOSPipEnabledtrueOS PIP — automatic when the app goes to the background (Android: Activity approach only)
defaultPositionbottomRightDefault corner position for in-App PIP (ShoplivePipPosition)
scale0.4Size scale for in-App PIP
padding0Margin from the screen edges
fixedWidth (iOS)NoneFixed in-App PIP width (pt) — used instead of scale when set
floatingOffset (iOS)0Extra offset added to the in-App PIP position
windowProvider (iOS)NoneThe UIWindow that hosts the in-App PIP (found automatically if unset)
enterOSPipOnBackPressed (Android)falseSwitch to OS PIP on Back (Activity approach only)
aspectRatio (Android)RATIO_9X16In-App PIP aspect ratio (ShoplivePipRatio)
isDragOutToCloseEnabledtrueDragging the in-App PIP window off screen closes it. With false it snaps back, and only the close button or close() closes it (3.0.3+)
soundmuteOnStartfalseStart muted
mixWithOthersfalsePlay alongside audio from other apps
autoResumeOnCallEnded (iOS) / autoResumeOnFocusGained (Android)trueResume automatically when a call ends / focus is regained
isVolumeKeyEnabled (Android)by typeAllow volume-key control — if unset, PREVIEW turns it off and LIVE turns it on
appearance
Mission 7
indicatorColorwhiteLoading indicator color
resizeMode (iOS)fillInitial video fit (fill / fit). Change it during playback with the player's resizeMode (Android offers the player property only)
loadingAnimationImages (iOS) / loadingAnimation (Android)NoneCustom loading animation
isStatusBarVisible (Android)trueStatus bar visibility
chatInputFont (iOS) / chatInputTypeface (Android)SystemChat input font (screen terminology ③)
chatSendButtonFont (iOS)SystemChat send button font
allowScreenCapturetrueAllow screen capture/recording
navigation
Mission 6
actionOnNavigationpipPlayer behavior on screen transitions such as a product landing: pip / keep / close
shareSchemeNoneThe app URL scheme used by the share button (screen terminology ⑥)
closeWhenAppDestroyed (Android)trueClose the player when the app is destroyed
overlay
Mission 7
uibuiltInShow the default overlay UI (builtIn) / hide it (hidden, command channel stays alive)
—customParametersEmpty mapPass client-defined key-values to the campaign (client settings)

ShoplivePlayOptions — per-playback options

Used in: Mission 1 · Mission 2 (deep link referrer)

FieldTypeDefaultDescription
referrerString?NoneTraffic source identifier (such as ref from a deep link) — reflected in statistics
keepWindowStateOnPlayExecutedBoolfalseKeep the existing window state (PIP, etc.) when playback starts
presentationStyleShoplivePlayerPresentationStylefullScreenHow playback starts — pip starts directly in in-App PIP without going full screen (requires pip.isInAppPipEnabled = true). The ShoplivePlayOptions.preview preset (Android PREVIEW) uses it
Reference

Types & States

These are isomorphic across both platforms (iOS uses lowercase cases, Android uppercase enums).

TypeValuesMeaning · where used
ShoplivePlayerTypelive · previewPlayback form — preview is the preview stream (low resolution) — Mission 4
ShoplivePlayerPresentationStylefullScreen · pip
(Android FULLSCREEN · PIP)
How playback starts — ShoplivePlayOptions · Mission 5
ShoplivePlayerStateidle · loading · playing · paused · inAppPIP(IN_APP_PIP) · closedPlayer lifecycle — Mission 1 · 5
ShopliveBroadcastStateidle · preview · connecting · live · reconnecting · endedBroadcasting lifecycle — Mission 8
ShopliveCampaignStatusready · live · endedCampaign (broadcast) status — the basis for LIVE only elements (screen terminology)
ShopliveConnectionStateconnecting · connected · reconnecting · disconnectedConnection state — Mission 6
ShopliveResizeModefill · fitScreen fitting — Mission 1
ShoplivePipPositiontop · middle · bottom × Left · Center · Right (9 positions, e.g. bottomRight)Default in-App PIP position — Mission 5
ShopliveOverlayUIModebuiltIn · hiddenShow/hide the default overlay UI (the command channel stays alive) — Mission 7
ShopliveNavigationActionpip · keep · closePlayer behavior on screen transitions — Mission 6
Reference

Error Handling

The core rule: anything that can be recovered automatically is handled by the SDK and only reported through event (for example, a failed connection → automatic fallback and reconnection). Only unrecoverable problems are reported as a terminating error. Clients never have to implement recovery logic themselves based on error codes. See Mission 6 for how to receive them.

struct ShopliveError: Error {
    let code: ShopliveErrorCode        // a typed enum
    let message: String
    let rawCode: Int           // original numeric code (3.0.3+, kept even when code is unknown)
    let underlying: Error?
    let isRecoverable: Bool    // fixed per code — true means a kind the SDK recovers from by itself (retry/reconnect)
}
// Shared by viewing and broadcasting from 3.0.3 — import cloud.shoplive.core.publicsurface.ShopliveError (· ShopliveErrorCode)
class ShopliveError(
    val code: ShopliveErrorCode,       // a typed enum
    val rawCode: Int,                  // raw numeric code from the server/engine
    override val message: String?,
    override val cause: Throwable? = null,
    val isRecoverable: Boolean = code.isRecoverable,   // fixed per code — true means a kind the SDK recovers from by itself (retry/reconnect)
) : Exception(message, cause)
// Error events carry a ShopliveError
class ShopliveError {
  final String code;          // SDK 3.0.3 error constant name — new values can appear, so treat it as an opaque string
  final int? rawCode;         // raw numeric code (3.0.3+, set on both platforms)
  final String? message;
  final bool recoverable;     // true means the SDK recovers by itself (retry/reconnect)
}

// Every API call returns a Future and fails with PlatformException
// code: INVALID_ARGUMENT · NO_HOST_SCREEN · NOT_RUNNING · SDK_ERROR
try {
  await ShoplivePlayer.instance.start(campaignKey);
} on PlatformException catch (e) {
  if (e.code == ShoplivePlatformErrorCode.noHostScreen) { /* … */ }
}
SituationBehaviorWhat the app does
Temporary network degradationThe SDK recovers automatically and reports playback(rebufferingStarted)Nothing (show loading UI if you like)
Recoverable (isRecoverable = true)Transient states such as connection, network, or waiting for the broadcast — the SDK recovers by itself (retry/reconnect)Wait — a user-facing notice at most
Unrecoverable (isRecoverable = false)The session terminatesShow code/message and provide retry UI
Reference

Client-Configured Items

These are values you set directly in code to match your own environment.

ItemWherePurpose
Product/landing URL handlingShoplivePlayerRequest.navigation (Mission 6)The app decides which screen to open when a product or banner is tapped (deep link/web view/native)
Share schemenavigation.shareScheme (field dictionary)The app URL scheme used by the share button
Allowed web view domainsallowedWebViewDomains (field dictionary)Whitelist of domains the overlay may open
Traffic parametersattribution · ShoplivePlayOptions.referrer (field dictionary)UTM / advertising identifier / referrer
Custom parameterscustomParameters (field dictionary)Passing client-defined key-values
What you cannot set yourself The base URL of the overlay web view is a playback control channel, so clients do not specify it (it is internal infrastructure). If you need an on-premise or dedicated environment URL, work with the Shoplive team to handle it as an environment preset.
Reference

Troubleshooting & FAQ

Playback doesn't start

Check that ① initialize was called before playback (Quick Start), ② the accessKey and campaignKey are valid, and ③ for a secured campaign, setUser(token) (Mission 3) is set. For unrecoverable errors, you can find the cause in the code/message of the error event (error handling).

I tapped a product and nothing happened

ShoplivePlayerRequest.navigation(url:) is a request the app must handle. Make sure you implemented URL routing in the delegate's request method (Mission 6).

Can I hide the default chat UI and use my own?

Yes. Just turn off the display of the default UI with overlay.ui = hidden. Event/request data keeps being delivered even then, so your app can render its own UI (Mission 7).

Chat and coupons aren't showing

Those elements are LIVE only. They are not displayed when the campaign is ready (before the broadcast) or ended (finished) (Screen Terminology Guide).

OS PIP doesn't work in the Android embedded View

That is intended. OS PIP minimizes the entire host Activity, so it is available only with the Activity approach. With the View approach, use in-App floating PIP (enterPictureInPicture() — Mission 5).

Shoplive SDK Integration Guide · v3 (Player / Streamer) · Docs