Shoplive Developers
한국어KOEnglishEN日本語JA

Shoplive SDK 연동 가이드

라이브 커머스 방송의 시청(Player)과 송출(Streamer)을 앱에 연동하는 방법을, 구현하고 싶은 케이스(미션) 단위로 순서대로 따라갈 수 있게 안내합니다.
코드 예제는 상단의 iOS / Android 탭으로 전환해 볼 수 있습니다.

시작하기

개요 & 요구사항

Shoplive SDK는 라이브 커머스에 최적화된 방송 SDK입니다. 복잡한 스트리밍 처리는 SDK가 맡고, 앱에는 시청(Player)과 송출(Streamer)을 안정적으로 붙일 수 있는 최소한의 인터페이스만 노출합니다.
버튼 한 번으로 풀스크린 라이브 열기, 홈 피드에 작은 플레이어 임베드, PIP로 계속 보기, 상품·쿠폰 연동, 앱에서 바로 방송 송출까지 — 필요한 방식만 골라 손쉽게 적용할 수 있습니다.

바로 시작하기 →

모듈 구성

미션 지도 — 무엇을 구현하고 싶나요?

구현하고 싶은 것미션
버튼을 누르면 라이브가 풀스크린으로 열린다미션 1 · 플레이어 직접 띄우기
푸시/문자 링크를 탭하면 바로 라이브가 열린다미션 2 · 딥링크로 띄우기
로그인 회원으로 시청·채팅하게 한다 (토큰/해싱/Secret Key)미션 3 · 사용자 정보 연결
홈 피드·목록에 방송 미리보기(프리뷰)를 넣는다미션 4 · 화면에 임베드 (프리뷰)
다른 화면으로 이동해도 작은 창으로 계속 보이게 한다미션 5 · PIP 적용
상품 탭 → 우리 상세 페이지로 이동시킨다 / 쿠폰을 발급한다미션 6 · 이벤트 · 상품 · 쿠폰
색·폰트를 바꾸거나, 기본 UI를 끄고 우리 UI를 얹는다미션 7 · UI 커스터마이징 & 위치
앱에서 방송을 송출한다미션 8 · 방송 송출

제공 형태 (플랫폼 매핑)

형태iOSAndroid
Player · 풀스크린ShoplivePlayerViewController (present)ShoplivePlayer(context).start(…) (Activity)
Player · 임베드ShoplivePlayerView (UIView)ShoplivePlayerView (FrameLayout)
Streamer · 스튜디오ShopliveStreamerViewControllerShopliveStreamer(context).start(…)
참고 iOS의 ViewController 방식이 Android의 Activity 방식에 대응합니다. 개념·네이밍·이벤트 모델은 두 플랫폼이 동형이라, 한 플랫폼에서 익힌 코드는 다른 플랫폼에 그대로 번역됩니다.

요구사항

항목iOSAndroidFlutter
최소 OSiOS 15.0Android 6.0 (minSdk 23)iOS 15.0 · Android 6.0 (minSdk 23)
개발 도구Xcode 15 이상 권장 (Swift 5.9)Android Gradle Plugin 8.x 권장Flutter 3.22 이상 · Dart 3.4 이상 (Android: AGP 8.7 · JDK 17)
배포 방식Swift Package ManagerMaven 저장소 (sdk.shoplive.cloud)pub.dev (shoplive_player · shoplive_streamer)
현재 버전3.0.33.0.33.0.3
시작하기

설치

현재 배포 버전은 iOS · Android · Flutter 플러그인 모두 3.0.3입니다. 지원 환경은 요구사항을 참고하세요.
// Xcode ▸ File ▸ Add Package Dependencies…
// https://github.com/shoplive/shoplive-sdk-ios   (Dependency Rule: Up to Next Major — 3.0.3)

// 타깃에는 필요한 라이브러리만 추가하고, 쓰는 것만 import 합니다.
import ShoplivePlayerSDK     // 시청 — 미션 1~7
import ShopliveStreamerSDK   // 송출 — 미션 8 (송출을 붙일 때만)

// 공통(인증 · 설정 · 사용자)인 ShopliveCore 는 위 SDK 가 함께 가져옵니다.
// Shoplive.initialize · setUser 를 써도 따로 import 하지 않습니다.
// settings.gradle.kts — Shoplive Maven 저장소 추가
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")     // 시청
    implementation("cloud.shoplive:shoplive-streamer-sdk:3.0.3")   // 송출
    // core 는 위 SDK가 자동으로 끌어옵니다
}
# pubspec.yaml — Flutter 3.22 이상 · Dart 3.4 이상
dependencies:
  shoplive_player: 3.0.3     # 시청 (shoplive_core 포함)
  shoplive_streamer: 3.0.3   # 송출 — 필요할 때만

# ios/Podfile — iOS SDK 는 iOS 15 이상 (pod install 때 프레임워크를 내려받습니다)
platform :ios, "15.0"
# Android: Shoplive Maven 저장소는 플러그인이 자동으로 추가합니다
Flutter Flutter 플러그인은 같은 3.0.3 SDK 를 패키지 세 개로 감쌉니다 — shoplive_player(시청), shoplive_streamer(송출), shoplive_core(초기화 · 사용자 · 전환, 두 패키지에 포함). 세 패키지는 같은 버전을 씁니다. 모든 호출은 Future 를 돌려주고, 이벤트와 요청은 델리게이트 대신 스트림으로 옵니다(미션 6). 피드에 플레이어를 넣는 위젯은 없어서 미션 4 의 View 방식은 쓸 수 없습니다 — 하이브리드 연동이나 PIP 프리뷰를 쓰세요.

권한 (Streamer 사용 시)

  • iOS — Info.plist에 NSCameraUsageDescription / NSMicrophoneUsageDescription 추가.
  • Android — 런타임 권한(CAMERA / RECORD_AUDIO)은 SDK가 스스로 요청합니다.
시작하기

빠른 시작 (Quick Start)

설치를 마친 뒤, 아래 코드로 첫 라이브를 재생할 수 있습니다.

먼저 준비할 값 — 각 키의 역할

값무엇인가어디에 넣나언제 필요한가
accessKey고객사(서비스)를 식별하는 키. Shoplive가 고객사마다 발급합니다Shoplive.initialize — 앱 시작 시 1회항상 — 초기화에 필요한 유일한 값
campaignKey재생할 방송 하나를 가리키는 키. 방송(캠페인)마다 다릅니다play(campaignKey:) · start(campaignKey =)재생·송출할 때마다
사용자 JWT시청자가 누구인지 알리는 토큰. 고객사 백엔드가 Secret Key로 서명합니다Shoplive.setUser(.token(jwt:))로그인 사용자일 때 (미션 3) — 비로그인이면 생략
송출용 토큰방송자 권한을 증명하는 토큰Shoplive.setStreamToken(_:)앱에서 송출할 때만 (미션 8)
Secret KeyJWT 서명에 쓰는 비밀 값. Shoplive 담당자에게서 받습니다. 앱에 넣지 않습니다고객사 백엔드보안 인증을 쓸 때 (미션 3 · 4단계)

accessKey 가 없다면 Shoplive 담당자 또는 Shoplive 어드민에서 확인하세요. Secret Key 는 어드민에서 발급하지 않고 Shoplive 담당자에게서 받습니다.

시청 (Player) — 3줄

// 1) 앱 시작 시 1회
Shoplive.initialize(.init(accessKey: "발급받은_ACCESS_KEY"))
// 2) 사용자 식별 — 로그인 사용자만 (비로그인이면 생략, 미션 3)
Shoplive.setUser(.token(jwt: myJwt))
// 3) 재생
let player = ShoplivePlayerViewController(campaignKey: "재생할_CAMPAIGN_KEY")
present(player, animated: true)
// 1) 앱 시작 시 1회
Shoplive.initialize(context, ShopliveConfiguration("발급받은_ACCESS_KEY"))
// 2) 사용자 식별 — 로그인 사용자만 (비로그인이면 생략, 미션 3)
Shoplive.setUser(ShopliveUser.Token(myJwt))
// 3) 재생 (인스턴스 생성 즉시 시작)
ShoplivePlayer(context).start(campaignKey = "재생할_CAMPAIGN_KEY")
import "package:shoplive_player/shoplive_player.dart";

// 1) 앱 시작 시 1회
await Shoplive.initialize(const ShopliveConfiguration(accessKey: "YOUR_ACCESS_KEY"));
// 2) 사용자 식별 — 로그인한 사용자만 (게스트는 생략, 미션 3)
await Shoplive.setUser(ShopliveUser.token(myJwt));
// 3) 재생
await ShoplivePlayer.instance.start("YOUR_CAMPAIGN_KEY");

이 세 줄이면 캠페인을 불러와 재생하고, 채팅·쿠폰·상품 오버레이까지 동작합니다. 각 줄의 상세: ① 초기화 설정 · ② 사용자 정보 연결 · ③ 플레이어 띄우기.

순서 주의 재생 전에 initialize가 반드시 먼저 실행돼야 합니다. 초기화는 AppDelegate / Application.onCreate에서 처리하는 것을 권장합니다.

송출 (Streamer)

Shoplive.initialize(.init(accessKey: "발급받은_ACCESS_KEY"))
Shoplive.setStreamToken(streamerJwt)          // 송출 전용 토큰 (필수)

let studio = ShopliveStreamerViewController(campaignKey: "송출할_CAMPAIGN_KEY")
present(studio, animated: true)
Shoplive.initialize(context, ShopliveConfiguration("발급받은_ACCESS_KEY"))
Shoplive.setStreamToken(streamerJwt)          // 송출 전용 토큰 (필수)

ShopliveStreamer(context).start(campaignKey = "송출할_CAMPAIGN_KEY")
import "package:shoplive_streamer/shoplive_streamer.dart";

await Shoplive.initialize(const ShopliveConfiguration(accessKey: "YOUR_ACCESS_KEY"));
await Shoplive.setStreamToken(streamerJwt);   // 송출 전용 토큰 (필수)

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

다음 단계화면 용어 가이드 · 미션 1부터 시작 →

화면 & 데모

화면 용어 가이드

이 섹션은 화면의 각 요소를 부르는 이름을 정리한 것입니다. 이후 미션·옵션·이벤트 설명에서 같은 용어를 사용합니다. 아래 번호는 화면 이미지의 번호와 1:1로 대응합니다. LIVE 전용 표시는 방송 진행 중에만 노출되는 요소입니다.

플레이어 화면 (시청)

플레이어 화면 구성
번호요소노출 조건설명 · 관련 API
①영상 영역상시라이브/VOD가 렌더링되는 영역. resizeMode(fill/fit) — 미션 1
②브랜드 로고상시캠페인 설정(콘솔)에서 지정
③캠페인 제목상시campaignInfoReceived로도 수신 — 미션 6
④LIVE 배지 · 시청자 수 · 좋아요LIVE 전용상태는 campaignStatusChanged로 수신 — 미션 6
⑤채팅 텍스트 오버레이LIVE 전용영상 위에 흐르는 텍스트. 입력 폰트 appearance.chatInputFont — 미션 7
⑥상품 카드 (좌하단 · NOW)상시탭 시 ShoplivePlayerRequest.navigation(url:) — 미션 6
⑦우측 아이콘 열 (♡ · ? · 💬)상시기본 UI — 좋아요 · 문의 · 채팅 열기
⑧음소거 버튼상시isMuted 프로퍼티와 연동
⑨쿠폰 버블운영자가 발행했을 때탭 시 ShoplivePlayerRequest.coupon(id:respond:) — 미션 6
⑩공지(Notice) pill운영자가 발행했을 때운영자가 콘솔에서 발행하는 흐르는 공지
⑪이벤트 · 공지 팝업운영자가 발행했을 때운영자 발행. 커스텀 액션으로도 수신 — 미션 6
⑫닫기 · 최소화상시최소화 = in-App PIP 진입. enterPictureInPicture() — 미션 5
방송 전 / 방송 중 / 종료 후 캠페인 상태가 ready → live → ended(타입 사전)로 바뀌면 LIVE 전용 요소들이 SDK에 의해 자동 노출·회수됩니다. 앱을 따로 켜고 끌 필요가 없으며, 자체 UI를 만들 때만 campaignStatusChanged(미션 6)를 구독해 직접 토글합니다.
방송 상태 3컷 비교

스튜디오 화면 (송출)

스튜디오 화면 구성
번호요소노출 조건설명
①카메라 프리뷰상시방송 시작 전부터 표시 (전체 배경)
②뒤로 · 닫기상시좌상단. 스튜디오 종료
③연결 상태 (Normal)상시우상단. 네트워크 품질 표시 (ShopliveConnectionState 대응)
④줌 배율 (1x)상시탭하여 배율 전환
⑤줌 슬라이더상시드래그로 연속 줌
⑥사운드 · 카메라 전환 · 설정상시우측 하단 세로 3개 — SDK 제공 송출 도구
⑦오디오 레벨 미터상시좌하단. 마이크 입력 레벨 확인
⑧방송 시작 · 종료상시하단 중앙. 탭 → connecting → live. ShopliveStreamerEvent.stateChanged — 미션 8

이전 ← 빠른 시작다음 → 데모 앱으로 확인하기

화면 & 데모

데모 앱으로 확인하기

SDK와 함께 제공되는 데모 앱은 이 가이드의 미션과 동일한 케이스 메뉴(미션 지도)로 구성되어 있습니다. 코드를 작성하기 전에 데모 앱에서 동작을 먼저 확인하면 구현 이해가 빨라집니다.

  1. 기본 모드 — Shoplive 데모 설정: 설치 직후 별도 입력 없이 모든 기능(전 미션)을 데모 캠페인으로 확인할 수 있습니다.
  2. 내 정보 모드 — 고객사 설정 입력: 설정 화면에 발급받은 accessKey · campaignKey · (선택) 사용자 JWT(발급 방법)를 입력하면 내 캠페인과 내 사용자로 동일한 케이스를 체험할 수 있습니다.
  3. 각 케이스 화면에는 이 가이드의 해당 미션 번호가 표기되어, 데모에서 본 동작 → 가이드의 코드로 바로 이어집니다.
데모 앱 배포 채널(TestFlight / APK)과 데모 캠페인 키는 SDK 전달 시 함께 안내됩니다.

이전 ← 화면 용어 가이드다음 → 미션 1 · 플레이어 직접 띄우기

미션 · Player

미션 1 · 플레이어 직접 띄우기

목표버튼 탭 → 라이브가 풀스크린으로 열리고 채팅·상품까지 동작
사전 준비설치 완료 · accessKey · campaignKey (발급 안내)
  1. 앱 시작 시 Shoplive.initialize가 호출되어 있는지 확인합니다 (빠른 시작).
  2. 시청 진입 버튼의 액션에서 플레이어를 생성해 띄웁니다.
  3. (선택) 델리게이트를 연결해 이벤트를 받습니다 — 미션 6.
@IBAction func watchLive() {
    let player = ShoplivePlayerViewController(campaignKey: "재생할_CAMPAIGN_KEY")
    player.delegate = self               // 선택: 이벤트 수신
    present(player, animated: true)
}
fun watchLive() {
    ShoplivePlayer(context).apply {
        delegate = myDelegate            // 선택: 이벤트 수신
    }.start(campaignKey = "재생할_CAMPAIGN_KEY")
}
Future<void> watchLive() async {
  final player = ShoplivePlayer.instance;
  player.events.listen(onPlayerEvent);   // 선택: 이벤트 받기 (미션 6)
  await player.start("YOUR_CAMPAIGN_KEY");
}
화면 전환을 직접 제어하고 싶다면 (Android) ShoplivePlayer(context).intent(campaignKey = …)가 Intent를 돌려줍니다. 직접 startActivity로 띄우면 됩니다.

완료 확인

  • 버튼 탭 후 2~3초 내 영상이 재생된다
  • 방송 중 캠페인이면 채팅·라이브 배지가 보인다 (화면 용어 ②③)
  • 재생 실패 시 error 이벤트의 code/message가 로그에 찍힌다 (에러 처리)

연관: PIP · 이벤트다음 → 미션 2 · 딥링크로 띄우기

미션 · Player

미션 2 · 딥링크로 플레이어 띄우기

목표푸시/문자/웹의 myapp://live?campaign=XXXX 링크 탭 → 앱이 열리며 바로 재생
사전 준비미션 1 완료 · 앱 URL 스킴(또는 App Links/Universal Links) 설정

SDK에 별도 딥링크 API가 있는 것이 아니라, 딥링크에서 campaignKey를 파싱해 미션 1과 동일하게 재생합니다.

  1. OS/프레임워크의 딥링크 핸들러에서 URL을 받습니다.
  2. 쿼리에서 campaignKey(필요 시 referrer — ShoplivePlayOptions)를 꺼냅니다.
  3. 초기화·사용자 설정(미션 3) 완료를 확인한 뒤 플레이어를 띄웁니다.
// 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
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))
}
// 앱이 받은 딥링크 URI 로 부릅니다 (예: app_links · 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));
}
콜드 스타트 주의 딥링크는 앱이 콜드 스타트될 수 있습니다. Shoplive.initialize·setUser가 재생보다 먼저 끝나는지 확인하세요. 초기화를 AppDelegate/Application에 두면 안전합니다 (빠른 시작).

완료 확인

  • 앱이 꺼진 상태(콜드 스타트)에서 링크 탭 → 재생까지 이어진다
  • 앱이 켜진 상태(웜)에서도 동일하게 동작한다
  • referrer가 캠페인 유입 통계에 반영된다 (ShoplivePlayOptions)

이전 ← 미션 1다음 → 미션 3 · 사용자 정보 연결

미션 · Player

미션 3 · 사용자 정보 연결

목표우리 서비스 회원으로 시청·채팅·통계가 잡히게 한다
사전 준비미션 1 완료 · (보안 인증 시) 백엔드 JWT 발급 준비 (4단계)
setUser 는 사용자 정보를 넘기는 것이지, 사용자를 인증하는 것이 아닙니다 로그인(아이디·비밀번호, 소셜 로그인 등)은 전적으로 앱이 합니다. setUser 는 지금 시청자가 누구인지 Shoplive 에 알려 주기만 해서, 채팅·시청·통계가 그 회원 기준으로 남게 합니다. Shoplive 는 사용자의 자격 증명을 확인하지 않습니다. token 은 그 정보가 고객사 서버가 발급했고 바뀌지 않았는지(서명)를 Shoplive 가 확인할 수 있고, profile 은 그 확인 없이 정보를 그대로 넘깁니다.

1단계 · 로그인 상태에 따라 고르기 — 권장 방식

사용자권장이유
비로그인아무것도 호출하지 않음게스트 식별자는 서버가 발급합니다. guest는 아무 동작도 하지 않으므로, 로그인했던 사용자를 게스트로 되돌릴 때는 logout()을 씁니다
로그인 (권장)token — 서버가 서명한 JWT위변조할 수 없어 회원 등급별 입장 제한·구매 연동·정확한 시청자 통계에 쓸 수 있습니다
로그인 (백엔드 작업이 어려울 때)profile — id·이름 등을 직접 전달SDK가 서명 없는 JWT를 만듭니다(심플 인증). 식별·통계에는 충분하지만 권한 검증에는 쓰지 마세요
로그아웃 · 사용자 정보 초기화Shoplive.logout()설정한 사용자 정보(JWT·프로필)를 지우고 송출 토큰도 함께 지웁니다. accessKey·게스트 식별자·UTM 은 유지됩니다. 사용자 정보만 따로 지우는 호출은 없습니다(guest 는 아무 동작도 하지 않습니다)
권장 흐름 앱 시작(비로그인) → 아무것도 하지 않음 → 로그인 상태: 재생할 때마다 직전에 백엔드에서 새로 발급한 JWT 를 받아 setUser(.token(jwt:)) → 로그아웃 시 logout() 으로 사용자 정보 초기화. 바뀐 사용자 정보는 다음 재생부터 적용됩니다. 홈 미리보기(미션 4)는 비로그인으로 보여 주다가 본 시청에 들어갈 때 로그인을 유도하는 식으로, 화면마다 다르게 써도 됩니다.

2단계 · setUser 호출

// ① 로그인 · 보안 인증 (권장) — 백엔드가 서명한 JWT (4단계)
Shoplive.setUser(.token(jwt: myJwt))

// ② 로그인 · 심플 인증 — 프로필 직접 전달 (SDK 가 서명 없는 JWT 를 만든다)
Shoplive.setUser(.profile(
    id: hashedUserId,            // 원문 대신 해시 권장 (3단계)
    name: "라이브팬", age: 27, gender: .female,
    rank: 10))                    // 사용자 등급 (-100 ~ 100)

// ③ 비로그인 — 호출하지 않아도 됩니다

// 로그아웃 — 사용자 정보(JWT·프로필)를 지우고 송출 토큰도 함께 지운다 (accessKey·게스트 식별자·UTM 은 유지)
Shoplive.logout()
// ① 로그인 · 보안 인증 (권장) — 백엔드가 서명한 JWT (4단계)
Shoplive.setUser(ShopliveUser.Token(myJwt))

// ② 로그인 · 심플 인증 — 프로필 직접 전달 (SDK 가 서명 없는 JWT 를 만든다)
Shoplive.setUser(ShopliveUser.Profile(
    id = hashedUserId,           // 원문 대신 해시 권장 (3단계)
    name = "라이브팬", age = 27, gender = ShopliveGender.FEMALE,
    rank = 10))                    // 사용자 등급 (-100 ~ 100)

// ③ 비로그인 — 호출하지 않아도 됩니다

// 로그아웃 — 사용자 정보(JWT·프로필)를 지우고 송출 토큰도 함께 지운다 (accessKey·게스트 식별자·UTM 은 유지)
Shoplive.logout()
// ① 로그인 · 보안 인증 (권장) — 백엔드가 서명한 JWT (4단계)
await Shoplive.setUser(ShopliveUser.token(myJwt));

// ② 로그인 · 간편 인증 — 프로필을 직접 전달 (SDK 가 서명 없는 JWT 생성)
await Shoplive.setUser(ShopliveUser.profile(
    id: hashedUserId,            // 원본 ID 대신 해시 권장 (3단계)
    name: "LiveFan", age: 27, gender: ShopliveGender.female,
    rank: 10));                    // 사용자 등급 (-100~100)

// ③ 비로그인 — 호출하지 않음

// 로그아웃 — 사용자 정보·토큰 정리
await Shoplive.logout();

3단계 · ID 해싱 — 회원 ID 원문을 노출하지 않기

내부 회원 ID를 그대로 넘기고 싶지 않을 때는, 앱과 백엔드가 동일한 방식으로 해싱한 값을 id로 사용합니다. 같은 사용자는 항상 같은 값이 나오도록 일관된 salt·알고리즘을 쓰는 것이 핵심입니다(통계·재방문 식별).

해싱 코드 예시 보기 (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();
}
보안 참고 해싱은 원문 은닉일 뿐 인증이 아닙니다. 시청 권한 검증이 필요하면 반드시 서버 서명 JWT(token)를 사용하세요. salt 정책은 Shoplive 팀과 합의된 값을 권장합니다.

4단계 · 보안 인증용 JWT 만들기 (고객사 백엔드)

token 인증에 쓸 JWT는 Shoplive가 발급한 Secret Key로 고객사 백엔드가 서명합니다. 앱은 로그인 직후 백엔드에서 JWT를 받아 setUser(.token(jwt:))로 넘기기만 합니다.

  1. Shoplive 담당자에게서 Secret Key를 받습니다 — base64로 인코딩된 문자열입니다
  2. 로그인한 사용자 정보로 아래 payload를 만들고 HS256으로 서명합니다
  3. 앱이 요청할 때마다(재생 직전마다) JWT 를 새로 발급해 내려 줍니다. 앱에 JWT 를 저장해 두고 다시 쓰지 않습니다
JWT 는 매번 새로 만들어 씁니다 기기에 JWT 를 캐시하거나 예전 토큰을 다시 쓰지 마세요. 지난 발급 뒤로 사용자 정보(이름·등급·유형)가 바뀌었을 수 있고, 유출된 토큰은 exp 까지 계속 쓸 수 있습니다. 앱이 setUser 를 부를 때마다 백엔드에 새 JWT 를 요청하게 하세요.
claim필수설명예
exp필수만료 시각 (Unix 초)1516239022
userId필수로그인 사용자의 고유 값 (최대 255바이트). 해시 값을 써도 됩니다(3단계)sample_user_id
iat발급 시각 (Unix 초)1516195822
name채팅에 보일 이름. 금칙어는 별표(*)로 바뀝니다라이브팬
gender성별 — m · f · 빈 값(미지정)f
age나이. 통계에는 5세 단위로 묶여 표시됩니다27
userScore사용자 등급 (-100 ~ 100). 등급별 입장·이벤트 추첨 제한의 기준10
userType사용자 유형. 입장 제한(Entry Type)을 건 방송에서 특정 유형만 입장시킬 때VIP
profile채팅 프로필 이미지 URL (64×64px 권장)https://…/profile.png
custom임의 데이터 (최대 1KB){"grade":"vip"}

서명 예시입니다. 두 코드는 같은 토큰을 만듭니다 — Secret Key가 base64 문자열이라는 점만 주의하세요.

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

const secret = Buffer.from(process.env.SHOPLIVE_SECRET_KEY, "base64");  // base64 를 풀어 바이트로 쓴다
const token = jwt.sign(
  { userId: hashedUserId, name: "라이브팬", gender: "f", age: 27, userScore: 10 },
  secret,
  { algorithm: "HS256", expiresIn: "12h" }   // exp · iat 를 자동으로 채운다
);
// Java — io.jsonwebtoken (jjwt)
String jwt = Jwts.builder()
    .signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey)   // base64 문자열을 그대로 넘긴다
    .setExpiration(new Date(System.currentTimeMillis() + 12 * 60 * 60 * 1000L))
    .setIssuedAt(new Date())
    .claim("userId", hashedUserId)
    .claim("name", "라이브팬")
    .claim("gender", "f")
    .claim("age", 27)
    .claim("userScore", 10)
    .compact();
Secret Key는 백엔드에만 앱 코드·번들에 넣으면 누구나 꺼내 다른 사용자로 위장한 토큰을 만들 수 있습니다. 심플 인증(profile)의 JWT는 서명이 없어 권한 검증에 쓸 수 없습니다.

완료 확인

  • 로그인 후 시청 시 채팅 닉네임이 회원 이름으로 표시된다
  • Shoplive 어드민 통계에서 해당 사용자 ID(해시)가 조회된다
  • 로그아웃 → Shoplive.logout() 으로 사용자 정보를 초기화한 뒤 게스트로 시청된다

이전 ← 미션 2다음 → 미션 4 · 화면에 임베드 (프리뷰)

미션 · Player

미션 4 · 화면에 임베드 (프리뷰)

목표홈 피드·목록 안에 방송 미리보기(프리뷰)를 넣고, 탭하면 본 시청으로 연다
사전 준비미션 1 완료

피드·목록에 넣는 플레이어는 프리뷰로 띄웁니다. 서버가 주는 낮은 해상도 스트림을 받아 한 화면에 여러 개를 띄워도 트래픽이 적고, 무음으로 훑어보다가 탭하면 본 시청(미션 1)으로 넘어갑니다. 화면을 무엇으로 만들었는지에 따라 두 방식 중 하나를 고르세요.

방식언제프리뷰를 그리는 쪽탭했을 때
View 방식 (네이티브)피드·목록이 네이티브 화면SDK의 ShoplivePlayerView + type = .preview네이티브 풀스크린 플레이어로 연다
하이브리드 연동 (Web + Mobile SDK)피드·목록이 웹(WebView)웹 페이지가 Web SDK로 그린다웹이 앱으로 campaignKey를 넘기고, 앱이 네이티브 플레이어로 연다

View 방식 — 네이티브 프리뷰

// 프리뷰 프리셋 — 낮은 해상도(type = .preview) · 무음 · 오버레이 숨김 · in-App PIP 정책을 한 번에
let playerView = ShoplivePlayerView(configuration: .preview)
container.addSubview(playerView)       // 오토레이아웃은 호스트 앱 자유
playerView.delegate = self
playerView.play(campaignKey: "재생할_CAMPAIGN_KEY")
val playerView = ShoplivePlayerView(context)   // XML/Compose(AndroidView) 배치 자유
// 프리뷰 프리셋 — 낮은 해상도(PREVIEW) · 무음 · 오버레이 숨김 · in-App PIP 정책을 한 번에
playerView.configuration = ShoplivePlayerConfiguration.PREVIEW
container.addView(playerView)

playerView.delegate = myDelegate
playerView.bindLifecycle(viewLifecycleOwner)   // pause/resume/release 자동화
playerView.play(campaignKey = "재생할_CAMPAIGN_KEY")
// Flutter 3.0.3 에도 피드에 넣을 위젯이 없습니다 (ShoplivePlayerView 없음).
// 대안: 프리뷰 스트림을 작은 인앱 PIP 창으로 시작
await ShoplivePlayer.instance.start("YOUR_CAMPAIGN_KEY",
    playOptions: const ShoplivePlayOptions(presentationStyle: ShoplivePresentationStyle.pip),
    configuration: const ShoplivePlayerConfiguration(
        type: ShoplivePlayerType.preview,                            // 프리뷰 스트림 (저해상도)
        sound: ShopliveSoundOptions(muteOnStart: true),              // 음소거로 시작
        overlay: ShopliveOverlayOptions(ui: ShopliveOverlayUIMode.hidden),   // 오버레이 숨김
        pip: ShoplivePipOptions(isInAppPipEnabled: true)));          // presentationStyle: pip 에 필요

프리뷰 설정 — 무엇이 바뀌나

설정효과
type = .preview
(Android ShoplivePlayerType.PREVIEW)
서버가 주는 프리뷰 스트림(낮은 해상도)으로 받습니다. 프리뷰 스트림이 없는 캠페인은 본 스트림으로 재생합니다. 기본값은 .live(본 해상도)입니다.
sound.muteOnStart = true무음으로 시작합니다. 소리는 isMuted = false로 켭니다
overlay.ui = .hidden채팅·상품 등 오버레이를 숨깁니다. 재생 중에 overlayUI로 다시 켤 수 있습니다
Android sound.isVolumeKeyEnabled볼륨키로 소리를 조절할지 정합니다. 지정하지 않으면 PREVIEW는 끄고 LIVE는 켭니다
프리셋 하나로 iOS ShoplivePlayerConfiguration.preview · Android ShoplivePlayerConfiguration.PREVIEW가 위 표의 값(type = .preview · 무음 · 오버레이 숨김)에 in-App PIP 정책까지 한 번에 맞춥니다. 바꿀 값만 덧칠해도 됩니다 — iOS는 var config = ShoplivePlayerConfiguration.preview 뒤에 config.overlay.ui = .builtIn처럼, Android는 ShoplivePlayerConfiguration.PREVIEW.copy(…)로 씁니다. 피드에 끼우지 않고 떠 있는 작은 창으로 시작하려면 ShoplivePlayOptions.preview(Android ShoplivePlayOptions.PREVIEW)와 짝으로 씁니다(미션 5).

탭 → 본 시청으로

프리뷰는 낮은 해상도라 그대로 키우지 않고, 본 해상도(.live) 플레이어로 새로 엽니다. 프리뷰 뷰는 멈춰 둡니다.

playerView.stop()                      // 프리뷰는 멈추고
let player = ShoplivePlayerViewController(campaignKey: "재생할_CAMPAIGN_KEY")   // 본 해상도로 새로 연다
present(player, animated: true)
playerView.stop()                      // 프리뷰는 멈추고
ShoplivePlayer(context).start(campaignKey = "재생할_CAMPAIGN_KEY")   // 기본 type = LIVE
// 본 해상도로 본 시청 전환 — iOS 는 열린 세션에서 그대로 전환
await ShoplivePlayer.instance.start("YOUR_CAMPAIGN_KEY",
    configuration: const ShoplivePlayerConfiguration(type: ShoplivePlayerType.live));

하이브리드 연동 (Web + Mobile SDK)

하이브리드 앱을 연동할 때는 방송 리스트는 웹뷰에 그대로 두고, 방송을 탭했을 때만 네이티브 플레이어로 넘겨 재생하는 방식을 추천합니다.

방식직접 관리할 것
리스트도 네이티브로 구현리스트 UI 를 네이티브로 직접 만들고 항목마다 프리뷰를 띄웁니다(위 View 방식). 목록 구성·페이징·상태 갱신을 모두 직접 관리합니다.
웹뷰 리스트 재사용 (이 절)리스트는 웹뷰 안에서 플러그인의 setOverall 이 그대로 그립니다. 탭한 방송의 키만 네이티브로 넘기고, 네이티브는 재생만 합니다.
브리지로 넘기는 값은 campaignKey 하나 웹뷰와 네이티브는 별개 레이어입니다. accessKey 는 네이티브가 이미 갖고 있으므로(Shoplive.initialize), 웹뷰에서 네이티브로 넘기는 값은 campaignKey 하나입니다.
  1. 웹뷰 — 리스트를 그리고, 탭하면 네이티브로 campaignKey 를 넘긴다
  2. 네이티브 — 넘어온 값을 받는다
  3. 네이티브 — SDK 로 재생한다

1단계 · 리스트 렌더링 & 탭하면 네이티브로 넘기기

리스트는 setOverall 이 그립니다. ON_CLICK_CAMPAIGN_LIST_ITEM 콜백에서 campaignKey 를 네이티브 인터페이스로 보냅니다. 네이티브 인터페이스가 없으면(일반 웹 브라우저) 기본 웹 모달 재생으로 동작하므로, 같은 페이지를 웹과 앱 어디에 배포해도 안전합니다.

<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 — campaignKey 하나만 네이티브로
      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 {
        // 네이티브 인터페이스 없음(일반 웹) — 웹 모달 재생으로
        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 웹뷰 설정 인라인 재생을 하려면 WKWebViewConfiguration 의 allowsInlineMediaPlayback = true 가 필요합니다(위 코드와 별개인 웹뷰 자체 설정).

2단계 · 네이티브에서 받기

위 코드가 부르는 인터페이스 이름(ShopLiveAppInterface)을 네이티브에 그대로 등록합니다. 이 등록은 Shoplive SDK 기능이 아니라 웹뷰가 표준으로 제공하는 JS ↔ 네이티브 통신입니다. campaignKey 문자열 하나만 오가므로 파싱 없이 그대로 받습니다.

let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.userContentController.add(self, name: "ShopLiveAppInterface")   // WKWebView 생성 전(이 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)   // 3단계 · 네이티브 재생
}
webView.addJavascriptInterface(ShopLiveAppInterface(activity), "ShopLiveAppInterface")

class ShopLiveAppInterface(private val activity: Activity) {
    @JavascriptInterface                     // 백그라운드 스레드에서 불린다
    fun postMessage(campaignKey: String) {
        activity.runOnUiThread { playLive(activity, campaignKey) }   // 3단계 · 네이티브 재생
    }
}
// webview_flutter — 페이지에 window.ShopLiveAppInterface.postMessage 를 노출
controller.addJavaScriptChannel(
  "ShopLiveAppInterface",
  onMessageReceived: (JavaScriptMessage message) {
    playLive(message.message);   // 3단계 · 네이티브 재생
  },
);
브리지는 웹뷰 안의 어떤 페이지든 호출할 수 있습니다 웹뷰가 우리 페이지 밖의 URL(상품 링크 등)로 이동할 수 있다면 그 페이지에서도 ShopLiveAppInterface 를 부를 수 있습니다. 외부로 나가는 링크는 새 창이나 외부 브라우저로 열어, 웹뷰 안에는 우리 페이지만 남게 하세요.

3단계 · 네이티브 재생

브리지로 받은 campaignKey 하나면 전체 화면 플레이어를 띄울 수 있습니다 — 미션 1 과 같습니다. 게스트 재생 기준이며, 앱 시작 때 Shoplive.initialize 가 이미 불렸다는 전제입니다(빠른 시작).

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

로그인 사용자로 재생하기 (선택)

게스트 재생이면 건너뜁니다. 로그인한 시청자라면 재생 직전에 setUser 를 부릅니다(미션 3). JWT 는 웹뷰 플러그인에 쓴 JWT 와 같은 사용자 기준으로 발급합니다. 값을 어디서 가져올지는 앱의 로그인 구조에 따라 갈립니다.

로그인이 있는 곳할 일
네이티브 (앱이 로그인 관리)네이티브가 이미 값을 갖고 있습니다. 브리지 payload 는 그대로(campaignKey 하나) 두고, 3단계 직전에 setUser 를 부릅니다.
웹뷰 세션에만네이티브는 사용자를 모릅니다. 1단계 payload 에 인증 값을 객체로 함께 실어 보내고, 2단계는 문자열 대신 JSON 을 파싱하도록 바꿉니다.
// 1단계 확장 — 객체로 보내기 (JWT 예시)
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
}
// 로그인 사용자 — 재생 직전에 (미션 3)
Shoplive.setUser(.token(jwt: jwt))
// 이어서 3단계처럼 재생
// 로그인 사용자 — 재생 직전에 (미션 3)
Shoplive.setUser(ShopliveUser.Token(jwt))
// 이어서 3단계처럼 재생
// 로그인 사용자 — 재생 직전에 (미션 3)
await Shoplive.setUser(ShopliveUser.token(jwt));
// 이어서 3단계처럼 재생
플랫폼 차이 Android View 방식은 OS PIP(앱 전체 축소)를 제공하지 않습니다(임베드 뷰는 호스트 Activity 소유). OS PIP가 필요하면 Activity 방식(미션 1)을 쓰세요. in-App 플로팅 PIP(미션 5)는 두 플랫폼·두 형태 모두 지원합니다.

완료 확인

  • 피드에 프리뷰를 여러 개 띄워도 끊기지 않고 재생된다 (프리뷰 스트림)
  • 피드 스크롤 중에도 재생이 유지된다 (Android: bindLifecycle 연결)
  • 탭하면 본 시청이 본 해상도로 열린다
  • (하이브리드 연동) 웹의 미리보기 탭이 앱의 네이티브 플레이어로 이어진다

이전 ← 미션 3다음 → 미션 5 · PIP 적용

미션 · Player

미션 5 · PIP 적용

목표다른 화면으로 이동하거나 홈으로 나가도 작은 창으로 계속 시청
사전 준비미션 1 또는 미션 4 완료

PIP는 풀스크린/View, iOS/Android 모두 같은 메서드입니다. “어떤 뷰를 PIP로 보낼지”는 SDK가 압니다 — 호스트가 뷰를 넘길 필요가 없습니다.

종류설명제어
in-App PIP앱 안에서 떠 있는 작은 플로팅 창. 화면 전환을 가로질러 유지enterPictureInPicture() / exitPictureInPicture()
OS PIP홈으로 나가도 시스템 PIP로 계속 재생configuration.pip.isOSPipEnabled (자동, 필드 사전)
player.enterPictureInPicture()      // 작은 창으로
player.exitPictureInPicture()       // 원래대로
if player.isInPictureInPicture { /* … */ }

// 위치·배율 등은 설정으로
var cfg = ShoplivePlayerConfiguration()
cfg.pip.defaultPosition = .bottomRight
cfg.pip.scale = 0.4
cfg.pip.isOSPipEnabled = true
player.enterPictureInPicture()      // 작은 창으로
player.exitPictureInPicture()       // 원래대로
if (player.isInPictureInPicture) { /* … */ }

// 위치·배율 등은 설정으로
val cfg = ShoplivePlayerConfiguration(
    pip = ShoplivePlayerConfiguration.PipOptions(
        defaultPosition = ShoplivePipPosition.BOTTOM_RIGHT,
        scale = 0.4f,
        isOSPipEnabled = true))     // OS PIP 는 Activity 방식 전용
final player = ShoplivePlayer.instance;
await player.enterPictureInPicture();   // 작은 창으로
await player.exitPictureInPicture();    // 원래 화면으로
if (await player.isInPictureInPicture()) { /* … */ }

// 위치·크기 등은 설정으로 (현재 설정 위에 병합)
await player.setConfiguration(const ShoplivePlayerConfiguration(
    pip: ShoplivePipOptions(
        defaultPosition: ShoplivePipPosition.bottomRight,
        scale: 0.4,
        isOSPipEnabled: true)));   // Android: 호스트 Activity 에 android:supportsPictureInPicture="true" 도 추가
자동 PIP — 호출 없이 전환 상품 랜딩으로 화면 이동 시: navigation.actionOnNavigation = PIP (미션 6) · 앱이 백그라운드로 갈 때: pip.isOSPipEnabled — 설정만으로 자동 전환됩니다.

PIP로 바로 시작하기

위는 재생 중에 작은 창으로 보내는 방법입니다. 풀스크린을 아예 거치지 않고 처음부터 작은 창으로 시작할 수도 있습니다 — 상품 상세에서 미리보기처럼 띄우는 경우입니다.

var options = ShoplivePlayOptions()
options.presentationStyle = .pip            // 기본값 .fullScreen

player.play(campaignKey: key, options: options)
ShoplivePlayer(this).start(
    campaignKey = key,
    options = ShoplivePlayOptions(
        presentationStyle = ShoplivePlayerPresentationStyle.PIP))   // 기본값 FULLSCREEN
await ShoplivePlayer.instance.start(key,
    playOptions: const ShoplivePlayOptions(
        presentationStyle: ShoplivePresentationStyle.pip));   // 기본은 fullscreen — pip.isInAppPipEnabled 가 꺼져 있으면 INVALID_ARGUMENT
필드값의미
presentationStyleFULLSCREEN (기본) / PIP 이 재생을 어떤 표면으로 시작할지. 기본값이 현행 동작이라 기존 코드는 영향 없음
두 플랫폼 모두 in-App PIP로 시작합니다 PIP로 시작하면 SDK가 띄우는 플로팅 창(in-App PIP)에서 재생합니다. pip.isInAppPipEnabled가 꺼져 있으면 iOS는 조용히 풀스크린으로 재생합니다. Android에서 ShoplivePlayer.intent()로 만든 인텐트는 항상 풀스크린이라 PIP가 무시됩니다 — PIP로 시작하려면 start()를 쓰세요.

미리보기 프리셋

"작은 창 + 무음 + 오버레이 숨김" 조합은 iOS에 상수로 준비돼 있습니다. 설정과 시작 옵션이 한 벌씩이라 둘 다 넘겨야 미리보기가 완성됩니다. Android에는 프리셋이 없어 같은 값을 직접 지정합니다.

player.configuration = .preview                     // 무음 · 오버레이 숨김 · in-App PIP
player.play(campaignKey: key, options: .preview)    // presentationStyle = .pip

// 프리셋을 출발점으로 고쳐 쓰기 (값 타입이라 복사됨)
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),             // 무음
        overlay = ShoplivePlayerConfiguration.OverlayOptions(ui = ShopliveOverlayUIMode.HIDDEN)))  // 오버레이 숨김
// Flutter 에는 프리셋이 없습니다 — 같은 값을 직접 설정
await ShoplivePlayer.instance.start(key,
    playOptions: const ShoplivePlayOptions(presentationStyle: ShoplivePresentationStyle.pip),
    configuration: const ShoplivePlayerConfiguration(
        sound: ShopliveSoundOptions(muteOnStart: true),                       // 음소거
        overlay: ShopliveOverlayOptions(ui: ShopliveOverlayUIMode.hidden)));   // 오버레이 숨김

완료 확인

  • 축소 버튼/API 호출 시 작은 창으로 전환되고 드래그로 이동된다
  • 작은 창 탭 → 풀스크린 복귀, 재생이 끊기지 않는다
  • 홈 이동 시 OS PIP로 이어진다 (설정 시)

이전 ← 미션 4다음 → 미션 6 · 이벤트 · 상품 · 쿠폰

미션 · Player

미션 6 · 이벤트 · 상품 · 쿠폰 연동

목표상품 탭 → 우리 상세 페이지 이동, 쿠폰 발급 처리, 상태 이벤트 수신
사전 준비미션 1 완료 · 상품 URL 라우팅 규칙 (고객사 설정)

델리게이트 구조 — 딱 2개 메서드

채널성격예
event (통지)필요한 case만 골라 처리상태 변화 · 캠페인 정보/상태 · 재생 · 연결 상태 · 애널리틱스 · 에러 · 웹 커맨드
request (응답 요구)respond를 반드시 호출상품 랜딩(navigation) · 쿠폰 · 커스텀 액션
extension MyViewController: ShoplivePlayerDelegate {

  // 통지 — 전 케이스. 필요한 case만 남기고 지워도 됩니다
  func player(_ p: ShoplivePlayerControlling, didReceive event: ShoplivePlayerEvent) {
    switch event {
    case .stateChanged(let s):              // 재생 수명주기 (.inAppPIP 진입/복귀 포함)
        print("state:", s)
    case .campaignStatusChanged(let c):     // .ready / .live / .ended
        liveBadge.isHidden = (c != .live)
    case .campaignInfoReceived(let info):   // 캠페인 메타데이터 수신 (제목·브랜드 등)
        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):   // 공통 연결 상태
        networkBanner.isHidden = (cs != .reconnecting)
    case .userNameUpdateRequested(let payload):  // 닉네임 변경 요청 → 앱 프로필 화면
        openProfileEditor(payload)
    case .analytics(let info):              // 시청 통계 → 자체 애널리틱스로 전달
        tracker.log(info.campaignKey, info.isPlaying, info.durationMs)
    case .error(let e):                     // 복구 불가 오류만 전달됨
        show(e.message)
    case .webCommand(let name, let payload):  // 오버레이(웹)가 보낸 커스텀 커맨드 — 예: 로그인 요청
        handleWebCommand(name, payload)
    }
  }

  func player(_ p: ShoplivePlayerControlling, didRequest request: ShoplivePlayerRequest) {
    switch request {
    case .navigation(let url):  router.open(url)      // 상품/배너 랜딩
    case .coupon(let id, let respond):            // 발급 처리 후 결과 회신 — status: 팝업 처리(.show/.hide/.keep), alertType: .alert/.toast
        respond(ShopliveCouponResult(couponId: id, success: true, status: .hide, alertType: .toast))
    case .customWebAction(let id, _, _, let respond):  // 동작 수행 후 결과 회신
        respond(ShopliveCustomActionResult(id: id, success: true, status: .keep, alertType: .toast))
    }
  }
}
val myDelegate = object : ShoplivePlayerDelegate {

  // 통지 — 전 케이스. 필요한 case만 남기고 지워도 됩니다 (기본 구현 empty)
  override fun onEvent(p: ShoplivePlayerControlling, event: ShoplivePlayerEvent) {
    when (event) {
      is ShoplivePlayerEvent.StateChanged ->             // 재생 수명주기 (IN_APP_PIP 진입/복귀 포함)
          log(event.state)
      is ShoplivePlayerEvent.CampaignStatusChanged ->    // READY / LIVE / ENDED
          liveBadge.isVisible = event.status == ShopliveCampaignStatus.LIVE
      is ShoplivePlayerEvent.CampaignInfoReceived ->     // 캠페인 메타데이터 수신 (제목·브랜드 등)
          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 ->   // 공통 연결 상태
          networkBanner.isVisible = event.state == ShopliveConnectionState.RECONNECTING
      is ShoplivePlayerEvent.UserNameUpdateRequested ->  // 닉네임 변경 요청 → 앱 프로필 화면
          openProfileEditor(event.payload)
      is ShoplivePlayerEvent.Analytics ->                // 시청 통계 → 자체 애널리틱스로 전달
          tracker.log(event.info.campaignKey, event.info.isPlaying, event.info.durationMs)
      is ShoplivePlayerEvent.Error ->                    // 복구 불가 오류만 전달됨
          show(event.error.message)
      is ShoplivePlayerEvent.WebCommandReceived ->      // 오버레이(웹)가 보낸 커스텀 커맨드 — 예: 로그인 요청
          handleWebCommand(event.name, event.payload)
    }
  }

  override fun onRequest(p: ShoplivePlayerControlling, request: ShoplivePlayerRequest) {
    when (request) {
      is ShoplivePlayerRequest.Navigation -> router.open(request.url)
      is ShoplivePlayerRequest.Coupon ->             // 발급 처리 후 결과 회신 — status: 팝업 처리(SHOW/HIDE/KEEP), alertType: ALERT/TOAST
          request.respond(ShopliveCouponResult(couponId = request.id, success = true,
              status = ShopliveResultStatus.HIDE, alertType = ShopliveResultAlertType.TOAST))
      is ShoplivePlayerRequest.CustomWebAction ->    // 동작 수행 후 결과 회신
          request.respond(ShopliveCustomActionResult(id = request.id, success = true,
              status = ShopliveResultStatus.KEEP, alertType = ShopliveResultAlertType.TOAST))
    }
  }
}
final player = ShoplivePlayer.instance;

// 알림 — events 스트림. 필요한 case 만 남겨도 됩니다
player.events.listen((event) {
  switch (event) {
    case ShoplivePlayerStateChanged(:final state):          // 재생 생애주기 (inAppPip 진입·해제 포함)
      log(state);
    case ShopliveCampaignStatusChanged(:final status):      // ready / live / ended
      showLiveBadge(status == ShopliveCampaignStatus.live);
    case ShopliveCampaignInfoReceived(:final info):         // 방송 메타 수신 (제목·브랜드 등)
      setTitle(info.title);
    case ShoplivePlaybackEvent(:final kind):                // 재생 종류: started / rendering / rebufferingStarted / rebufferingEnded / ended …
      if (kind == ShoplivePlaybackEventKind.rebufferingStarted) showSpinner(true);
      if (kind == ShoplivePlaybackEventKind.rebufferingEnded) showSpinner(false);
    case ShopliveConnectionStateChanged(:final state):      // 공통 연결 상태
      showNetworkBanner(state == ShopliveConnectionState.reconnecting);
    case ShopliveUserNameUpdateRequested(:final payload):   // 닉네임 변경 요청 → 앱 프로필 화면
      openProfileEditor(payload);
    case ShopliveAnalyticsEvent e:                          // 시청 통계 → 자체 분석으로 전달
      tracker.log(e.campaignKey, e.isPlaying, e.durationMs);
    case ShoplivePlayerErrorEvent(:final error):            // 복구 불가 오류만 전달
      show(error.message);
    case ShopliveWebCommandReceived(:final name, :final payload):   // 오버레이(웹)가 보낸 커스텀 명령 — 예: 로그인 요청
      handleWebCommand(name, payload);
    default:
      break;
  }
});

// 요청 — 종류마다 스트림 하나. 세션 내내 구독을 유지합니다
player.navigationRequests.listen((request) => router.open(request.url));
player.couponRequests.listen((request) {                    // 발급 후 결과 보고 (respond 는 1회만 유효 · 기본 10초 후 시간 초과)
  request.respond(const ShopliveCouponResult(success: true,
      status: ShopliveResultStatus.hide, alertType: ShopliveResultAlertType.toast));
});
player.customWebActionRequests.listen((request) {           // 동작 수행 후 결과 보고
  request.respond(const ShopliveCustomActionResult(success: true,
      status: ShopliveResultStatus.keep, alertType: ShopliveResultAlertType.toast));
});

ShoplivePlayerEvent 종류 — didReceive event로 오는 통지

모두 선택 처리입니다 — 필요한 case만 골라 처리하고 나머지는 무시해도 됩니다. 표기는 iOS 기준(camelCase)이며, Android는 같은 이름의 sealed class(ShoplivePlayerEvent.StateChanged 등)이고, webCommand만 WebCommandReceived로 이름이 다릅니다.

ShoplivePlayerEvent 전체 선언 보기 (Swift · Kotlin · Dart)
public enum ShoplivePlayerEvent {
    case stateChanged(ShoplivePlayerState)                  // .inAppPIP 진입/복귀도 이 이벤트로 통지
    case campaignStatusChanged(ShopliveCampaignStatus)      // .ready / .live / .ended
    case campaignInfoReceived(ShopliveCampaignInfo)
    case playback(PlaybackEvent)                    // started/rendering/rebuffering/ended…
    case connectionStateChanged(ShopliveConnectionState)    // 공통 연결 상태
    case userNameUpdateRequested([String: Any])
    case analytics(info: ShopliveAnalyticsInfo)
    case error(ShopliveError)
    case webCommand(name: String, payload: [String: Any]?)  // 오버레이(웹) → 앱 커스텀 커맨드
}

// ShopliveCore — 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()               // IN_APP_PIP 진입/복귀도 이 이벤트로 통지
    data class CampaignStatusChanged(val status: ShopliveCampaignStatus) : ShoplivePlayerEvent()  // READY / LIVE / ENDED
    data class CampaignInfoReceived(val info: ShopliveCampaignInfo) : ShoplivePlayerEvent()
    data class Playback(val event: ShoplivePlaybackEvent) : ShoplivePlayerEvent()         // 하위 타입으로 종류를 구분
    data class ConnectionStateChanged(val state: ShopliveConnectionState) : ShoplivePlayerEvent() // 공통 연결 상태
    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()  // 오버레이(웹) → 앱 커스텀 커맨드
}

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,
)
// ShoplivePlayer.instance.events 로 아래 하위 클래스가 옵니다
sealed class ShoplivePlayerEvent {}
class ShoplivePlayerStateChanged { final ShoplivePlayerState state; }          // inAppPip 진입·해제도 이 이벤트
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 는 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; }   // 오버레이(웹) → 앱 커스텀 명령
class ShoplivePlayerUnknownEvent { final String rawType; }                     // 알 수 없거나 형식이 다른 값은 여기로 — 스트림이 끊기지 않습니다
casepayload언제 오나주 용도
stateChangedShoplivePlayerState플레이어 수명주기 변화 (idle → loading → playing…). in-App PIP 진입/복귀(inAppPIP)도 이 이벤트로 통지재생 상태에 따른 앱 UI 갱신 — 미션 5
campaignStatusChangedShopliveCampaignStatus방송 상태 변화: ready(대기) → live(방송 중) → ended(종료)라이브 배지 등 LIVE 전용 자체 UI 토글 — 화면 용어
campaignInfoReceivedShopliveCampaignInfo캠페인 메타데이터(제목·브랜드 등) 조회 완료 시 1회자체 UI에 캠페인 정보 표시
playbackPlaybackEvent (Android ShoplivePlaybackEvent)재생 파이프라인 이벤트: started / rendering / rebufferingStarted / rebufferingEnded / ended 등. iOS는 pb.event로, Android는 하위 타입으로 구분로딩 스피너·버퍼링 안내 표시
connectionStateChangedShopliveConnectionState연결 상태 변화 (connecting / connected / reconnecting / disconnected)네트워크 연결 상태 안내
userNameUpdateRequested[String: Any]시청자가 오버레이에서 닉네임 변경을 요청했을 때앱의 프로필 변경 화면 연결 — 미션 3
analyticsShopliveAnalyticsInfo시청 통계 이벤트 발생 시 (campaignKey · isPlaying · isMuted · durationMs · campaign/brand)자체 애널리틱스(GA/Amplitude 등)로 전달
errorShopliveError복구 불가 오류로 세션이 종결될 때만. 자동 복구 중인 문제는 오지 않음오류 안내 + 재시도 UI — 에러 처리
webCommandname + payload오버레이(웹)가 앱으로 커스텀 커맨드를 보냈을 때 (예: 로그인 요청). Android 3.0.3부터 웹 플레이어의 사용자 행동 로그도 name = "EVENT_LOG"로 옵니다앱 화면 전환·로그인 유도 등. 반대 방향은 send(command:payload:)

request 3종 — 앱이 해야 할 일

요청언제앱이 할 일
navigation(url:)상품/배너 탭 (화면 용어 ④)해당 URL로 화면 전환(딥링크/웹뷰/네이티브 상세). 유일한 필수 처리
coupon(id:respond:)쿠폰 버튼 (⑤ LIVE 전용)발급 처리 후 respond(ShopliveCouponResult(…))로 결과 회신 → 성공/실패 UI가 오버레이에 반영. status(show / hide / keep)로 쿠폰 팝업 처리, alertType(alert / toast)으로 안내 형태를 정합니다. 3.0.3 기준 생략하면 status는 hide, alertType은 alert입니다(두 플랫폼 공통 · Flutter 3.0.3 플러그인은 alertType 을 생략하면 toast). Android에서 웹 기본값을 따르려면 alertType = null
customWebAction(…)고객사 정의 커스텀 버튼동작 수행 후 respond(ShopliveCustomActionResult(…))

앱 → 오버레이 커맨드 보내기

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"});
네트워크 상태 확인 네트워크 상황은 playback(rebufferingStarted…)·connectionStateChanged(타입 사전) 이벤트로 확인합니다.

완료 확인

  • 상품 탭 → 우리 상세 페이지가 열린다 (플레이어는 actionOnNavigation 정책대로 PIP/유지/종료 — 미션 5)
  • 쿠폰 탭 → 발급 처리 후 오버레이에 성공 표시가 뜬다
  • 방송 종료 시 campaignStatusChanged(ended)가 수신된다

이전 ← 미션 5다음 → 미션 7 · UI 커스터마이징

미션 · Player

미션 7 · UI 커스터마이징 & 위치 변경

목표브랜드 색·폰트 적용, 또는 기본 UI를 끄고 자체 UI를 원하는 위치에 배치
사전 준비미션 1·미션 6 완료 (자체 UI는 이벤트 데이터 필요)

커스터마이징 두 가지 — SDK 업데이트 없이 가능

종류누가 바꾸나바꾸는 범위반영 방법
네이티브 커스터마이징고객사 — 앱 코드에서 직접표준 외형 옵션(아래 방법 ①) · 기본 오버레이를 숨기고 직접 만든 UI(방법 ②)다음 앱 배포에 함께 — 이미 쓰는 SDK 의 API 로 하므로 SDK 업데이트 불필요
웹을 통한 커스터마이징Shoplive플레이어 오버레이(채팅·상품·공지 등)는 Shoplive 가 제공하는 웹 레이어라, 디자인·배치를 Shoplive 쪽에서 바꿉니다Shoplive 쪽에서 반영 — SDK 업데이트·앱 배포 모두 불필요. 원하는 내용은 Shoplive 팀과 상의하세요

방법 ① 표준 외형 옵션 — appearance

로딩 색·상태바·채팅 폰트 등은 설정만으로 바꿉니다. 요소의 표시/스타일 수준 변경이면 이 방법으로 충분합니다.

옵션iOSAndroid
로딩 인디케이터 색 (화면 용어 ⑧)indicatorColor: UIColorindicatorColor: @ColorInt Int
로딩 애니메이션loadingAnimationImages: [UIImage]?loadingAnimation: @DrawableRes Int
상태바 표시—isStatusBarVisible
채팅 입력 폰트 (③)chatInputFont: UIFont?chatInputTypeface: Typeface
화면 캡처 허용allowScreenCaptureallowScreenCapture

방법 ② 요소 재배치 — 기본 UI 숨기고 자체 UI

버튼·요소 위치를 자유롭게 바꾸고 싶다면 이 방법입니다. 기본 오버레이 UI를 감추고(overlay.ui = hidden) 앱이 미션 6의 이벤트/요청 데이터로 자체 UI를 그립니다 — 위치·모양·애니메이션 전부 앱 마음대로입니다.

var cfg = ShoplivePlayerConfiguration()
cfg.overlay.ui = .hidden        // 기본 오버레이 UI 감춤 (커맨드 채널은 유지)
let player = ShoplivePlayerViewController(campaignKey: key, configuration: cfg)
// 이제 채팅/상품/쿠폰 UI를 앱이 delegate 데이터로 직접 그립니다
val cfg = ShoplivePlayerConfiguration(
    overlay = ShoplivePlayerConfiguration.OverlayOptions(ui = ShopliveOverlayUIMode.HIDDEN)   // 표시만 숨김
)
ShoplivePlayer(context).start(campaignKey = key, configuration = cfg)
// 이제 채팅/상품/쿠폰 UI를 앱이 delegate 데이터로 직접 그립니다
await ShoplivePlayer.instance.start(key,
    configuration: const ShoplivePlayerConfiguration(
        overlay: ShopliveOverlayOptions(ui: ShopliveOverlayUIMode.hidden)));   // 표시만 숨김
// 이제 events·요청 스트림 데이터로 채팅·상품·쿠폰 UI 를 앱이 직접 그립니다
기본 오버레이 vs 자체 UI
할 수 있는 것 / 없는 것 기본 오버레이 내부 요소를 픽셀 단위로 이동시키는 공개 API는 없습니다. 방법 ①(표준 옵션) 또는 방법 ②(자체 UI) 중에서 선택하세요. 숨김 상태에서도 커맨드 채널은 살아 있어 쿠폰·상품·커스텀 액션 데이터는 계속 전달됩니다 (미션 6).

완료 확인

  • 브랜드 색/폰트가 로딩·채팅에 반영된다 (방법 ①)
  • hidden 상태에서도 상품 탭·쿠폰 이벤트가 델리게이트로 계속 들어온다 (방법 ②)
  • 방송 상태(ready/live/ended)에 따라 자체 UI가 올바르게 토글된다 (화면 용어)

이전 ← 미션 6다음 → 미션 8 · 방송 송출

미션 · Streamer

미션 8 · 방송 송출

목표앱에서 스튜디오를 열어 방송을 시작·종료한다
사전 준비송출용 토큰(Secret Key로 서명) · 카메라/마이크 권한(설치)

카메라 프리뷰·방송 시작/종료·채팅·설정·카메라 전환/마이크/줌까지 스튜디오 UI 전체를 SDK가 제공합니다(화면 용어). 델리게이트 구현조차 선택 사항입니다.

1단계 · 송출 토큰 설정

시청용 setUser(미션 3)와 별개 경로입니다. 방송자 JWT도 Shoplive 담당자에게서 받은 Secret Key로 백엔드가 서명합니다.

2단계 · 스튜디오 실행 & 상태 수신

// 1) 송출 토큰 (필수)
Shoplive.setStreamToken(streamerJwt)

// 2) 스튜디오 실행
let studio = ShopliveStreamerViewController(campaignKey: "송출할_CAMPAIGN_KEY")
studio.streamerDelegate = self          // 선택
present(studio, animated: true)

// 3) 상태 수신 (선택)
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) 송출 토큰 (필수)
Shoplive.setStreamToken(streamerJwt)

// 2) 스튜디오 실행 — 런타임 권한도 SDK가 요청
ShopliveStreamer(context).apply {
    streamerDelegate = myStreamerDelegate   // 선택
}.start(campaignKey = "송출할_CAMPAIGN_KEY")

// 3) 상태 수신 (선택)
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) 송출 토큰 (필수)
await Shoplive.setStreamToken(streamerJwt);

final streamer = ShopliveStreamer.instance;
// 3) 상태 받기 (선택) — start 전에 구독
streamer.events.listen((event) {
  switch (event) {
    case ShopliveStreamerStateChanged(:final state):
      log(state);
    case ShopliveStreamerErrorEvent(:final error):
      show(error.message);
    default:
      break;
  }
});

// 2) 스튜디오 띄우기 — 런타임 권한 요청도 SDK 가 처리
await streamer.start("YOUR_CAMPAIGN_KEY");

송출 화면 옆에 웹 페이지 두기 (선택 · 3.0.3+) — 고객사 웹 페이지 URL을 넘기면 송출 화면 오른쪽에 붙고, 왼쪽으로 밀어 엽니다. 넘기지 않으면 지금과 같습니다. iOS ShopliveStreamerViewController(campaignKey:webPageURL:) · Android ShopliveStreamer(context).start(campaignKey, webPageUrl = "https://…") · Flutter streamer.start(campaignKey, webPageUrl: "https://…"). URL 은 http:// · https:// 로 시작하고 호스트가 있어야 합니다(Flutter 는 잘못된 URL 이면 송출 화면을 열기 전에 INVALID_ARGUMENT 로 실패). 이 페이지에는 SDK 네이티브 브리지가 없습니다.

ShopliveStreamerEvent 종류 — didReceive event로 오는 통지

Player와 같은 멘탈 모델이되, 스튜디오 UI를 SDK가 전부 제공하므로 이벤트가 2종으로 단순합니다. 모두 선택 처리입니다.

ShopliveStreamerEvent 전체 선언 보기 (Swift · Kotlin · Dart)
public enum ShopliveStreamerEvent {
    case stateChanged(ShopliveBroadcastState)   // .idle/.preview/.connecting/.live/.reconnecting/.ended
    case error(ShopliveError)           // 복구 불가 오류만
}
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()          // 복구 불가 오류만
}
sealed class ShopliveStreamerEvent {}
class ShopliveStreamerStateChanged { final ShopliveBroadcastState state; final bool? isRehearsal; }
// state: idle / preview / connecting / live / reconnecting / ended · isRehearsal: iOS 는 null (리허설 정보 없음)
class ShopliveStreamerErrorEvent { final ShopliveError error; }   // 복구 불가 오류만
class ShopliveStreamerUnknownEvent { final String rawType; }
casepayload언제 오나주 용도
stateChangedShopliveBroadcastState방송 수명주기 변화: idle → preview(카메라 프리뷰) → connecting → live(송출 중) → ended. 네트워크 순단 시 reconnecting → live 자동 복귀도 이 이벤트로 관찰앱 화면 전환·방송 중 표시 등 상태 연동
errorShopliveError복구 불가 오류로 방송이 종결될 때만 (예: 토큰 무효 — 1단계). 재연결 등 자동 복구 중에는 오지 않음오류 안내 + 재시도 UI — 에러 처리

ShopliveBroadcastState(타입 사전): idle → preview → connecting → live → (reconnecting) → ended. 방송 종료 정책(잔여 시간·종료 중 입장 허용 등)은 CampaignEndOption으로 유지됩니다.

향후 호환성 송출 방식이 향후 확장되어도 공개 API는 변경되지 않으므로, 지금 작성한 연동 코드는 그대로 유지됩니다.

완료 확인

  • 스튜디오 진입 시 카메라 프리뷰가 보인다 (preview 상태)
  • 방송 시작 → 시청 앱(미션 1)에서 해당 캠페인이 LIVE로 재생된다
  • 네트워크 순단 시 reconnecting → live로 자동 복귀한다

이전 ← 미션 7다음 → 데이터 필드 사전

레퍼런스

데이터 필드 사전

공개 API의 설정 객체를 필드 단위로 정리했습니다. 모든 필드는 기본값이 있어 필요한 것만 지정하면 됩니다.

ShopliveConfiguration — 초기화 (앱 1회)

사용처: 빠른 시작 · 미션 2 (콜드 스타트)

필드타입기본값설명
accessKeyString필수고객사를 식별하는 키 — Shoplive가 발급 (각 키의 역할)
allowedWebViewDomains[String]빈 배열 (전체 허용)오버레이 웹뷰 허용 도메인. 필수 도메인은 SDK가 자동 포함 (고객사 설정)
attributionShopliveAttribution?없음유입 트래킹 묶음 (아래)
appVersion (Android)String?없음3.0.3에서 제거 — SDK가 앱 버전을 직접 읽으므로 넘기지 않습니다 (Flutter 3.0.3 플러그인에는 남아 있지만 값을 무시하고 경고 로그만 남깁니다)

ShopliveAttribution — 유입 트래킹

필드타입설명
utmSource / utmCampaign / utmMedium / utmContentString?UTM 4종
advertisingId (iOS)String?광고 식별자 (IDFA) — 수집·전달은 앱 책임. 3.0.3 기준 iOS만 제공

ShopliveUser — 사용자 정보 (3형태)

사용처: 미션 3 · 사용자 정보 연결 (판단 기준·해싱·Secret Key 포함)

형태필드설명
tokenjwt: String서버 서명 JWT (권장) — 발급 방법
profileid(필수) · name / age / gender / rank(선택) · Android custom(선택, 추가 key-value)심플 인증 — SDK가 서명 없는 JWT를 만듭니다(권한 검증 불가). rank는 사용자 등급(-100 ~ 100). id는 해시 권장
guest—비로그인 — 아무 동작도 하지 않습니다. 게스트 식별자는 서버가 발급합니다
Shoplive.logout()—로그아웃 · 사용자 정보 초기화 — 사용자 정보(JWT·프로필)를 지우고 송출 토큰도 함께 지웁니다. accessKey·게스트 식별자·UTM 은 유지됩니다

ShoplivePlayerConfiguration — 재생 정책

사용처: 미션 1 · 미션 4 (프리뷰) · 미션 5 (PIP) · 미션 7 (UI)

그룹필드기본값설명
—typelive재생 형태 — preview면 서버가 주는 프리뷰 스트림(낮은 해상도)으로 받습니다 (미션 4)
pip
미션 5
isInAppPipEnabledtruein-App 플로팅 PIP 허용 (VC/Activity·View 공통)
isOSPipEnabledtrueOS PIP — 백그라운드 진입 시 자동 (Android는 Activity 방식 전용)
defaultPositionbottomRightin-App PIP 기본 모서리 위치 (ShoplivePipPosition)
scale0.4in-App PIP 크기 배율
padding0화면 가장자리 여백
fixedWidth (iOS)없음in-App PIP 창 너비 고정(pt) — 지정하면 scale 대신 사용
floatingOffset (iOS)0in-App PIP 창 위치에 더하는 추가 오프셋
windowProvider (iOS)없음in-App PIP 창을 띄울 UIWindow 지정 (미지정 시 자동 탐색)
enterOSPipOnBackPressed (Android)false뒤로가기 시 OS PIP로 전환 (Activity 방식 전용)
aspectRatio (Android)RATIO_9X16in-App PIP 창 비율 (ShoplivePipRatio)
isDragOutToCloseEnabledtruein-App PIP 창을 화면 밖으로 끌어내면 닫기. false면 제자리로 돌아오고 닫기 버튼 · close()로만 닫힙니다 (3.0.3+)
soundmuteOnStartfalse시작 시 음소거
mixWithOthersfalse다른 앱 오디오와 동시 재생
autoResumeOnCallEnded (iOS) / autoResumeOnFocusGained (Android)true통화 종료/포커스 회복 시 자동 재개
isVolumeKeyEnabled (Android)type 에 따라볼륨키 조절 허용 — 지정하지 않으면 PREVIEW는 끄고 LIVE는 켭니다
appearance
미션 7
indicatorColorwhite로딩 인디케이터 색
resizeMode (iOS)fill영상 맞춤 초기값 (fill / fit). 재생 중에는 플레이어의 resizeMode로 바꿉니다 (Android는 플레이어 속성만 제공)
loadingAnimationImages (iOS) / loadingAnimation (Android)없음커스텀 로딩 애니메이션
isStatusBarVisible (Android)true상태바 표시
chatInputFont (iOS) / chatInputTypeface (Android)시스템채팅 입력 폰트 (화면 용어 ③)
chatSendButtonFont (iOS)시스템채팅 전송 버튼 폰트
allowScreenCapturetrue화면 캡처/녹화 허용
navigation
미션 6
actionOnNavigationpip상품 랜딩 등 화면 전환 시 플레이어 동작: pip / keep / close
shareScheme없음공유 버튼이 사용할 앱 URL 스킴 (화면 용어 ⑥)
closeWhenAppDestroyed (Android)true앱이 종료될 때 플레이어도 함께 닫기
overlay
미션 7
uibuiltIn기본 오버레이 UI 표시(builtIn) / 숨김(hidden, 커맨드 채널 유지)
—customParameters빈 맵고객사 정의 key-value를 캠페인에 전달 (고객사 설정)

ShoplivePlayOptions — 재생 1회 옵션

사용처: 미션 1 · 미션 2 (딥링크 referrer)

필드타입기본값설명
referrerString?없음유입 경로 식별자 (딥링크의 ref 등) — 통계 반영
keepWindowStateOnPlayExecutedBoolfalse재생 시작 시 기존 창 상태(PIP 등) 유지
presentationStyleShoplivePlayerPresentationStylefullScreen시작 표현 — pip면 풀스크린을 거치지 않고 in-App PIP로 시작합니다(pip.isInAppPipEnabled = true 필요). 프리셋 ShoplivePlayOptions.preview(Android PREVIEW)가 이 값을 씁니다
레퍼런스

타입 & 상태

두 플랫폼 동형입니다(iOS는 소문자 case, Android는 대문자 enum).

타입값의미 · 사용처
ShoplivePlayerTypelive · preview재생 형태 — preview는 프리뷰 스트림(낮은 해상도) — 미션 4
ShoplivePlayerPresentationStylefullScreen · pip
(Android FULLSCREEN · PIP)
재생 시작 표현 — ShoplivePlayOptions · 미션 5
ShoplivePlayerStateidle · loading · playing · paused · inAppPIP(IN_APP_PIP) · closed플레이어 수명주기 — 미션 1·5
ShopliveBroadcastStateidle · preview · connecting · live · reconnecting · ended송출 수명주기 — 미션 8
ShopliveCampaignStatusready · live · ended캠페인(방송) 상태 — LIVE 전용 요소의 기준 (화면 용어)
ShopliveConnectionStateconnecting · connected · reconnecting · disconnected연결 상태 — 미션 6
ShopliveResizeModefill · fit화면 맞춤 — 미션 1
ShoplivePipPositiontop · middle · bottom × Left · Center · Right (9곳, 예: bottomRight)in-App PIP 기본 위치 — 미션 5
ShopliveOverlayUIModebuiltIn · hidden기본 오버레이 UI 표시/숨김 (커맨드 채널은 유지) — 미션 7
ShopliveNavigationActionpip · keep · close화면 전환 시 플레이어 동작 — 미션 6
레퍼런스

에러 처리

핵심 규칙: 자동 복구 가능한 것은 SDK가 처리하고 event로만 알립니다(예: 연결 실패 → 자동 폴백·재연결). 복구 불가한 것만 error로 종결 통지됩니다. 고객사가 에러 코드를 보고 복구 로직을 직접 구현할 일은 없습니다. 수신 방법은 미션 6을 참고하세요.

struct ShopliveError: Error {
    let code: ShopliveErrorCode        // 타입화된 enum
    let message: String
    let rawCode: Int           // 원본 숫자 코드 (3.0.3+, code 가 unknown 이어도 보존)
    let underlying: Error?
    let isRecoverable: Bool    // 코드별 고정값 — true 면 SDK가 재시도·재접속으로 스스로 회복하는 종류
}
// 3.0.3부터 시청 · 송출 공통 — import cloud.shoplive.core.publicsurface.ShopliveError (· ShopliveErrorCode)
class ShopliveError(
    val code: ShopliveErrorCode,       // 타입화된 enum
    val rawCode: Int,                  // 서버·엔진이 준 원본 숫자 코드
    override val message: String?,
    override val cause: Throwable? = null,
    val isRecoverable: Boolean = code.isRecoverable,   // 코드별 고정값 — true 면 SDK가 재시도·재접속으로 스스로 회복하는 종류
) : Exception(message, cause)
// 오류 이벤트는 ShopliveError 를 싣습니다
class ShopliveError {
  final String code;          // SDK 3.0.3 의 오류 상수 이름 — 값이 늘 수 있으니 불투명한 문자열로 다룹니다
  final int? rawCode;         // 원본 숫자 코드 (3.0.3+ 두 플랫폼 모두 값)
  final String? message;
  final bool recoverable;     // true 면 SDK 가 스스로 복구 (재시도·재연결)
}

// 모든 API 는 Future 를 돌려주고, 실패하면 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) { /* … */ }
}
상황동작앱이 할 일
일시적 네트워크 저하SDK 자동 복구, playback(rebufferingStarted) 통지없음 (필요 시 로딩 UI)
복구 가능 (isRecoverable = true)연결·네트워크·방송 대기 같은 일시 상태 — SDK가 재시도·재접속으로 스스로 회복대기 — 사용자 안내 정도
복구 불가 (isRecoverable = false)세션 종결code/message 안내 후 재시도 UI 제공
레퍼런스

고객사 직접 설정 항목

고객사 환경에 맞춰 코드에서 직접 지정하는 값들입니다.

항목어디서용도
상품/랜딩 URL 처리ShoplivePlayerRequest.navigation (미션 6)상품·배너 탭 시 이동할 화면을 앱이 결정 (딥링크/웹뷰/네이티브)
공유 스킴navigation.shareScheme (필드 사전)공유 버튼이 사용할 앱 URL 스킴
웹뷰 허용 도메인allowedWebViewDomains (필드 사전)오버레이가 열 수 있는 도메인 화이트리스트
유입 파라미터attribution · ShoplivePlayOptions.referrer (필드 사전)UTM / 광고 식별자 / 레퍼러
커스텀 파라미터customParameters (필드 사전)고객사 정의 key-value 전달
직접 지정할 수 없는 것 오버레이 웹뷰의 base URL은 재생 제어 채널이라 고객사가 지정하지 않습니다(내부 인프라). 온프레미스/전용 환경 URL이 필요하면 Shoplive 팀과 협의해 환경 프리셋으로 처리합니다.
레퍼런스

트러블슈팅 & FAQ

재생이 시작되지 않아요

① initialize가 재생보다 먼저 호출됐는지(빠른 시작), ② accessKey·campaignKey가 유효한지, ③ 보안 캠페인이라면 setUser(token)(미션 3)이 설정됐는지 확인하세요. 복구 불가 오류는 error 이벤트의 code/message(에러 처리)로 원인을 확인할 수 있습니다.

상품을 탭했는데 아무 일도 안 일어나요

ShoplivePlayerRequest.navigation(url:)은 앱이 반드시 처리해야 하는 요청입니다. 델리게이트의 request 메서드에서 URL 라우팅을 구현했는지 확인하세요 (미션 6).

기본 채팅 UI를 숨기고 자체 UI를 쓸 수 있나요?

네. overlay.ui = hidden으로 기본 UI 표시만 끄면 됩니다. 이때도 이벤트/요청 데이터는 계속 전달되므로 앱이 자체 UI를 그릴 수 있습니다 (미션 7).

채팅·쿠폰이 안 보여요

해당 요소들은 LIVE 전용입니다. 캠페인이 ready(방송 전)이거나 ended(종료)면 표시되지 않습니다 (화면 용어 가이드).

Android 임베드 View에서 OS PIP가 안 돼요

의도된 동작입니다. OS PIP는 호스트 Activity 전체를 축소하므로 Activity 방식 전용입니다. View 방식에서는 in-App 플로팅 PIP(enterPictureInPicture() — 미션 5)를 사용하세요.