Shoplive V1 모바일 SDK 연동 가이드
개요 & 요구사항
Shoplive V1 모바일 SDK 는 앱에 라이브 커머스 시청(Player) 을 연동합니다.
스트리밍·채팅·오버레이는 SDK 가 맡고, 앱은 방송을 띄운 뒤 사용자·상품·쿠폰을 이어 붙이기만 하면 됩니다.
버튼 한 번으로 풀스크린 라이브 열기, 딥링크 진입, 다른 화면으로 가도 작은 창으로 계속 보기(PIP),
상품·쿠폰 연동, 구매 전환 측정까지 지원합니다. 전부 다 붙일 필요는 없고, 우리 앱에
필요한 미션만 골라 적용하면 됩니다.
| 플랫폼 | 최소 | 권장 | 최신 버전 |
|---|---|---|---|
| iOS | iOS 15 · Swift 5 · CocoaPods 1.9 이상 | — | 1.9.0 |
| Android | Android 4.4(API 19). shoplive-media3 를 쓰면 Android 6.0(API 23) | 재생 엔진 내장(ExoPlayer 별도 설치 불필요 · 설치의 재생 엔진 모듈 하나만 선언) | 1.9.0 |
| Flutter | Dart SDK >=2.17.1 <4.0.0 · Flutter >=2.5.0 · iOS 15.0 · Android minSdk 19 · JDK 17 | — | 1.9.01 |
- Flutter
shoplive_player1.9.0 에는 iOS · Android SDK 1.9.0(ExoPlayer 2.19.1.20)이 들어 있습니다. 플러그인은compileSdk 36·jvmToolchain 17로 빌드하므로 Android 빌드에 JDK 17 이 필요합니다.
Android SDK 는 compileSdk · targetSdk 36 으로 빌드합니다. 앱 쪽에 필요한 하한은
요확인입니다.
- iOS 를 SPM 으로 설치한다면 버전을 1.9.0 으로 못 박으세요. 배포 저장소를 V1 과 V3 가 같이 쓰고 있어서, 버전을 지정하지 않으면 V3(3.x)가 설치됩니다. 지정하는 방법은 설치 에 있습니다.
- Flutter 를 쓴다면 위 표의 iOS·Android 요구사항도 그대로 지켜야 합니다. Flutter 플러그인은 네이티브 SDK 를 감싼 껍데기라서, 안쪽 요구사항이 그대로 따라옵니다.
- Flutter 1.8.x 에서 올린다면 숏폼 코드를 확인하세요.
shoplive_player1.9.0 부터 숏폼 API 와 숏폼 SDK 의존성이 빠졌습니다. 1.8.x 에서 숏폼 API 를 쓰던 코드는 올리기 전에 지워야 합니다. - Xcode 가 26 보다 낮다면 정식 배포본 설치나 빌드가 실패할 수 있습니다. 이때 쓸 수 있는 하위 호환 배포본을 플랫폼 부록 에 정리해 두었습니다.
모듈 구성
SDK 는 하는 일에 따라 나뉘어 있습니다. 어떤 역할이 필요한지 먼저 보고, 설치할 때 쓰는 실제 이름은 카드 아래쪽에서 확인하세요. Flutter 는 이 모두를 패키지 하나가 감싸고 있어 따로 고를 것이 없습니다.
ShopliveSDKCommon · Android shoplive-common · shoplive-network
Player시청
방송을 재생하고 PIP·오버레이를 띄웁니다.
iOS ShopLiveSDK · Android shoplive-sdk-core
재생 엔진Android 전용
영상을 실제로 디코딩합니다. 둘 중 하나만 넣으세요.
Android shoplive-exoplayer 또는 shoplive-media3
PIP 권한Android 전용
다른 앱 위에 작은 창을 띄울 권한을 요청합니다. 미션 8 에서 씁니다.
Android shoplive-permission
Flutter 패키지Flutter
위 네이티브 모듈을 전부 감싼 패키지 하나입니다.
Flutter shoplive_player
미션 지도
구현하고 싶은 것을 아래에서 찾아 그 미션으로 바로 가세요. 순서대로 읽지 않아도 됩니다. 다만 모든 미션은 설치와 미션 1을 마친 상태를 전제로 합니다.
| 구현하고 싶은 것 | 미션 |
|---|---|
| 버튼을 누르면 라이브가 풀스크린으로 열린다 | 미션 1 · 플레이어 띄우기 |
| 푸시·문자 링크를 탭하면 바로 라이브가 열린다 | 미션 2 · 딥링크로 띄우기 |
| 로그인 회원으로 시청하게 한다 (게스트 · 간편 · JWT) | 미션 3 · 사용자 정보 연결 |
| 시청자가 채팅하게 한다 | 미션 4 · 채팅 |
| 방송 중에 쿠폰을 발급한다 | 미션 5 · 쿠폰 |
| 상품을 누르면 우리 상세 페이지로 보낸다 | 미션 6 · 상품 클릭 |
| 시청자가 방송 링크를 공유하게 한다 | 미션 7 · 방송 링크 공유 |
| 다른 화면으로 가도 작은 창으로 계속 보이게 한다 | 미션 8 · PIP |
| 방송 전·중에 미리보기를 노출한다 | 미션 9 · 방송 미리보기 |
| 구매 전환을 측정한다 | 미션 10 · 구매 전환 추적 |
| 색·동작을 우리 앱에 맞게 바꾼다 | 미션 11 · UI·동작 커스터마이징 |
다음 → 설치
설치
쓰는 플랫폼의 항목만 보면 됩니다. iOS 는 CocoaPods 와 Swift Package Manager 중
하나만 골라 아래 탭에서 해당하는 쪽을 따라가세요.
iOS 설치 (최신 1.9.0)
Shoplive iOS SDK 는 공식 CocoaPods CDN 이 아니라 사설 spec 저장소로 배포됩니다.
Podfile 에 source 를 두 줄 모두 써야 합니다.
# Podfile
source 'https://github.com/shoplive/pod-specs.git' # Shoplive spec repo
source 'https://cdn.cocoapods.org/' # 공식 CocoaPods CDN
platform :ios, '15.0'
use_frameworks!
target 'YourApp' do
pod 'ShopLive', '1.9.0'
pod 'ShopliveSDKCommon', '1.9.0'
end
pod install --repo-update
source 를 하나라도 쓰는 순간 기본 CDN 이 자동 적용되지 않습니다.
공식 CDN 줄을 빼먹으면 Shoplive SDK 가 아닌 다른 모든 pod 의 설치가 실패합니다.pod 'ShopliveSDK' 라고 안내하지만, 실제 spec 에는 그 이름이
없습니다. ShopLive·ShopliveSDKCommon·ShopliveShortformSDK 세 가지만 존재하므로
위 예제처럼 ShopLive 로 씁니다.1.9.0 부터 SPM 배포 저장소가 shoplive-sdk-ios 하나로 통합됐습니다.
1.8.x 까지는 ios-sdk 와 common-ios 두 저장소를 따로 받아야 했지만,
새로 연동한다면 통합 저장소만 쓰면 됩니다.
- Xcode 에서 File → Add Package Dependencies… 를 열고
https://github.com/shoplive/shoplive-sdk-ios를 입력합니다. - 의존성 규칙을 Up to Next Major Version, 기준 버전 1.9.0 으로 지정합니다.
ShopLiveSDK·ShopliveSDKCommon두 product 를 앱 타깃에 추가합니다.
dependencies: [
.package(url: "https://github.com/shoplive/shoplive-sdk-ios", .upToNextMajor(from: "1.9.0"))
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "ShopLiveSDK", package: "shoplive-sdk-ios"),
.product(name: "ShopliveSDKCommon", package: "shoplive-sdk-ios")
]
)
]
main 브랜치는 3.x(V3) 매니페스트를 담고 있고, 1.x 는 release/1.9.0 브랜치·태그에만 있습니다.
.upToNextMajor(from: "1.9.0") 처럼 메이저 버전을 고정해야 3.x 로 잘못 올라가지 않습니다.
Xcode UI 에서 추가할 때도 규칙을 "Up to Next Major Version" + 1.9.0 으로 지정하세요.Android 설치
Shoplive 라이브러리는 Maven Central 이 아니라 Shoplive 가 직접 운영하는 저장소에 올라가 있습니다. 그래서 의존성을 적기 전에 저장소부터 등록해야 합니다. 이 단계를 건너뛰면 라이브러리를 찾지 못해 Gradle sync 가 실패합니다.
// settings.gradle (dependencyResolutionManagement) 또는 프로젝트 build.gradle
repositories {
maven { url 'https://sdk.shoplive.cloud/maven-repo' }
google()
mavenCentral()
}
그다음 앱 모듈에 라이브러리를 추가합니다. 재생 엔진은 shoplive-exoplayer 와
shoplive-media3 중 하나만 넣으세요. 둘 다 넣으면 클래스가 중복돼 빌드가 깨집니다.
shoplive-sdk-core 는 shoplive-common · shoplive-network · 재생 엔진을
전이 의존성으로 싣지 않습니다. 그래서 모두 선언해야 합니다. 하나라도 빠지면 빌드는 되어도 실행 중에 클래스를 찾지 못합니다.// app/build.gradle
dependencies {
def shoplive_sdk_version = "1.9.0"
implementation "cloud.shoplive:shoplive-sdk-core:$shoplive_sdk_version"
implementation "cloud.shoplive:shoplive-common:$shoplive_sdk_version"
implementation "cloud.shoplive:shoplive-network:$shoplive_sdk_version"
implementation "cloud.shoplive:shoplive-exoplayer:2.19.1.20"
// ExoPlayer 대신 Media3 를 쓰려면:
// implementation "cloud.shoplive:shoplive-media3:1.10.1.3"
}
cloud.shoplive:shoplive-sdk 를 쓰고 있다면 바꿔야 합니다
예전 문서에 나오는 shoplive-sdk 라는 이름은 지금 저장소에 없습니다(404).
그대로 두면 Gradle 이 의존성을 찾지 못해 빌드가 실패합니다. 위 예제처럼
shoplive-sdk-core · shoplive-common · shoplive-network 를 넣고,
재생 엔진으로 shoplive-exoplayer 나 shoplive-media3 중 하나를 더하세요.마지막으로 이벤트 핸들러를 등록합니다. 플레이어 안에서 시청자가 누른 것(상품·쿠폰·공유 버튼 등)은 전부 이 핸들러를 통해 앱으로 넘어옵니다.
ShopLive.setHandler(object : ShopLiveHandler() {
override fun handleNavigation(context: Context, url: String) {
// 링크 이동 처리
}
})
Flutter 설치
flutter pub add 'shoplive_player:^1.9.0'
# pubspec.yaml
dependencies:
shoplive_player: ^1.9.0
ShopLivePlayer · ShopLiveCommon 이
없어 이 문서의 예제가 컴파일되지 않습니다.iOS — 플러그인은 ShopLive 1.9.0 pod 에 의존합니다. 이 pod 는 Shoplive spec 저장소
(shoplive/pod-specs)에만 있고 공식 CocoaPods trunk 의 최신은 1.8.13 입니다.
그래서 ios/Podfile 맨 위에 아래 세 줄을 넣고 pod install --repo-update 를 실행하세요.
# ios/Podfile (맨 위)
source 'https://github.com/shoplive/pod-specs.git'
source 'https://cdn.cocoapods.org/'
platform :ios, '15.0'
cd ios && pod install --repo-update
Android — 플러그인이 Shoplive maven 저장소를 넣어 주지만, settings.gradle 이
FAIL_ON_PROJECT_REPOS 모드면 그 설정이 무시됩니다. 앱 쪽 저장소 목록에도 직접 넣는 것을 권장합니다.
// android/build.gradle(.kts)
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://sdk.shoplive.cloud/maven-repo") }
}
}
설치 후 사용할 Dart 파일에 import 문을 추가합니다.
import 'package:shoplive_player/shoplive_player.dart';
import 'package:shoplive_player/shoplive_common.dart';
완료 확인
- iOS CocoaPods 는
.xcworkspace로, SPM 은 패키지 해결까지 정상적으로 빌드된다 - iOS SPM Xcode 의 Package Dependencies 목록에서 해결된 버전이 1.9.0(3.x 아님)인지 확인한다
- Android 프로젝트에 Shoplive maven 저장소가 추가되고
ShopLiveHandler가 등록되어 있다 - Flutter
pubspec.yaml의dependencies에shoplive_player: ^1.9.0(3.x 아님)이 추가되어 있다
빠른 시작
import ShopliveSDKCommon
import ShopLiveSDK
// 1) 앱 시작 시 1회
ShopLiveCommon.setAccessKey(accessKey: "발급받은_ACCESS_KEY")
// 2) 시청 진입
ShopLive.play(data: .init(campaignKey: "재생할_CAMPAIGN_KEY"))
// 1) 앱 시작 시 1회
ShopLive.setAccessKey("발급받은_ACCESS_KEY")
ShopLive.setHandler(myHandler)
// 2) 시청 진입
ShopLive.play(this, ShopLivePlayerData("재생할_CAMPAIGN_KEY"))
final common = ShopLiveCommon();
final player = ShopLivePlayer();
// 1) 앱 시작 시 1회
common.setAccessKey(accessKey: '발급받은_ACCESS_KEY');
// 2) 시청 진입
player.play(data: ShopLivePlayerData(campaignKey: '재생할_CAMPAIGN_KEY'));
화면 용어 가이드
플레이어는 화면 전체를 영상으로 채우고, 그 위에 배지·버튼·채팅 같은 요소를 겹쳐 올립니다. 좌상단의 Shoplive 로고는 항상 보입니다. 이 가이드는 각 요소를 계속 같은 이름으로 부르기 때문에, 여기서 이름을 한 번 맞춰 두면 뒤에 나오는 미션을 훨씬 수월하게 읽을 수 있습니다.
아래는 같은 캠페인의 방송 전과 방송 중 화면입니다. 요소가 모두 보이는 오른쪽(방송 중) 화면에 번호를 붙였고, 각 번호를 어느 미션에서 다루는지 바로 아래 표로 이어집니다. 플레이어를 띄우는 방법 자체는 미션 1 에 있습니다.
1
2
3
4
5
6
7
8
| 번호 | 요소 | 어디서 다루나 |
|---|---|---|
| 1 | LIVE 배지 | SDK가 방송 상태에 따라 자동으로 표시합니다(별도 연동 불필요) |
| 2 | 시청자 수 · 좋아요 수 | SDK가 자동으로 갱신합니다(별도 연동 불필요). 미션 11의 브랜드 좋아요(팔로우) 버튼과는 다른 요소입니다. 이 숫자는 보여주기만 하고, 그쪽은 캠페인 설정이 따로 필요합니다. |
| 3 | 음소거 버튼 | 미션 11 |
| 4 | 더보기 · 공유 · 닫기(우상단 아이콘 열) | 공유는 미션 7, 화면을 벗어났을 때 작은 창으로 내려가는 동작은 미션 8 에서 다룹니다 |
| 5 | 쿠폰 버블 | 미션 5 |
| 6 | 채팅 텍스트 오버레이 | 미션 4 |
| 7 | 채팅 열기 · 좋아요 버튼(우하단 아이콘 열) | 채팅 열기는 미션 4 에서 다룹니다. 좋아요 버튼이 미션 11의 브랜드 좋아요(팔로우)와 같은 기능인지는 아직 확인되지 않았습니다(요확인) |
| 8 | 상품 카드 | 미션 6 |
이전 ← 빠른 시작다음 → 데모 앱으로 확인하기
데모 앱으로 확인하기
Shoplive 조직에는 데모 저장소가 여러 개 있는데 대부분 V3 용입니다. V1 로 연동하면서 V3 데모를 열면 API 가 전혀 달라 혼란스럽습니다. 아래 표로 먼저 구분하세요.
| 저장소 | 플랫폼 | SDK | 바로 쓸 수 있나 |
|---|---|---|---|
shoplive-sdk-flutter |
Flutter | V1(1.9.0 태그) | ✅ 태그로 바꾼 뒤 실행 (아래) |
ios-sdk-sample |
iOS | V1 | ⚠️ Podfile 손봐야 빌드됨 (아래) |
mobile-sdk-demo |
iOS · Android | V3 | ❌ V1 과 API 가 달라 참고용으로도 쓸 수 없음 |
android-sdk-sample | Android | — | ❌ 데모가 아니라 AAR/POM 배포 저장소 |
Flutter 클론 후 바로 실행
shoplive_player 패키지(pub.dev 배포)의 예제 앱이
shoplive_flutter_sample/example/ 에 들어 있습니다.
git clone https://github.com/shoplive/shoplive-sdk-flutter.git
cd shoplive-sdk-flutter && git checkout 1.9.0
cd shoplive_flutter_sample/example && flutter pub get && flutter run
iOS Podfile 을 먼저 고쳐야 합니다
Xcode 프로젝트(swift/ShopLiveSwiftSample)에 PIP 설정 · 옵션 설정 · 사용자 정보 등
여러 메뉴가 들어 있습니다. 다만 저장소에 커밋된 Podfile 은 그대로 쓸 수 없습니다.
platform :ios, '11.0' 에
Shoplive spec 저장소 source 도 빠져 있습니다. 설치의 Podfile 예제(source 두 줄 포함)로
바꾸고 버전을 1.9.0 으로 지정한 뒤 pod install --repo-update 를 실행하세요.이전 ← 화면 용어 가이드다음 → 미션 1 · 플레이어 띄우기
미션 1 · 플레이어 띄우기
- 앱 시작 시(1회) Access Key 로 SDK 를 초기화합니다.
- 시청 진입 시점에 Campaign Key 로 재생을 호출합니다.
- (선택) 저화질·무음 미리보기로 시청을 유도할 수 있습니다.
import ShopliveSDKCommon
import ShopLiveSDK
ShopLiveCommon.setAccessKey(accessKey: "{ACCESS_KEY}")
ShopLive.play(data: .init(campaignKey: "{CAMPAIGN_KEY}"))
ShopLive.setAccessKey(accessKey)
ShopLive.play(this, ShopLivePlayerData(campaignKey).apply {
keepWindowStateOnPlayExecuted = false
referrer = "referrer"
})
final common = ShopLiveCommon();
final player = ShopLivePlayer();
common.setAccessKey(accessKey: "{ACCESS_KEY}");
player.play(data: ShopLivePlayerData(campaignKey: "{CAMPAIGN_KEY}"));
ShopLivePlayerData 대신 ShopLivePlayerDataObjc 를 쓰세요. 생성자는
initWithCampaignKey:keepWindowStateonPlayExecuted:referrer: 입니다. 레이블의
on 이 소문자이니 그대로 쓰세요.미리보기(Preview)로 시청 유도
진행 중인 방송을 저화질·무음 미리보기로 먼저 보여주고, 탭하면 본 재생으로 전환하도록 유도할 수 있습니다. 세 플랫폼 모두 제공하지만 호출 방식이 다릅니다. iOS 는 데이터 객체를 넘기고, Flutter 는 옵션 설정과 노출이 두 호출로 나뉘어 있습니다.
let previewData = ShopLivePreviewData(
campaignKey: "{CAMPAIGN_KEY}",
isMuted: true,
resolution: .PREVIEW
)
ShopLive.preview(data: previewData) {
// 미리보기를 탭했을 때 본 재생으로 전환하고 싶다면 여기서 play 를 호출합니다
ShopLive.play(data: .init(campaignKey: "{CAMPAIGN_KEY}"))
}
ShopLive.showPreviewPopup(this@YourActivity, ShopLivePreviewData(campaignKey))
// 1) 옵션을 먼저 설정하고
player.setPreviewOption(
data: ShopLivePlayerPreviewData(
campaignKey: "{CAMPAIGN_KEY}",
useCloseButton: true,
enableSwipeOut: true,
position: "BOTTOM_RIGHT",
),
);
// 2) 노출한다 (숨길 때는 hidePreview)
player.showPreview(campaignKey: "{CAMPAIGN_KEY}");
ShopLive.preview(data:completion:) 의 completion 은 미리보기를 탭했을 때 불립니다.
본 재생으로 전환할지는 앱이 정합니다. 구버전 preview(with campaignKey:) 는 deprecated 입니다.
Flutter 는 setPreviewOption(옵션) 과 showPreview(노출) 가 나뉘어 있어
옵션을 먼저 설정해야 하고, 닫을 때는 hidePreview() 를 씁니다.
Android 는 API · 옵션 의 setPreviewOptions 로 창 모양을 따로 지정합니다.완료 확인
- 앱 실행 직후 Access Key 초기화가 1회 끝나 있다
- 버튼 탭 → 2~3초 내 방송이 재생된다
- 재생 실패 시 에러 콜백(iOS
handleError/ AndroidonError)에 코드·메시지가 찍힌다
이전 ← 데모 앱으로 확인하기다음 → 미션 2 · 딥링크로 띄우기
미션 2 · 딥링크로 띄우기
광고·푸시·문자에 붙는 링크에는 보통 어느 방송을 열지와 어디서 들어왔는지가 쿼리로 함께 실려 옵니다. 앱이 그 링크로 열리면 쿼리를 하나씩 꺼내 SDK 에 넘긴 다음 플레이어를 실행하면 됩니다.
이 미션은 https://xxxx/yyyy?ak=…&ck=…&utm_source=… 형태의 링크를 받았다고 보고 진행합니다.
쿼리는 종류마다 넘기는 방법이 다르니, 먼저 아래 표로 어떤 쿼리를 어떻게 처리할지 확인하세요.
| 쿼리 | 값 | SDK 전달 방법 |
|---|---|---|
ak | Access Key | setAccessKey |
ck | Campaign Key | 플레이어 실행 시 campaignKey 로 사용 |
utm_source · utm_mediumutm_campaign · utm_content | 광고 유입 경로 | setUtmSource 등 전용 setter |
| 그 밖의 쿼리 | 앱이 정의한 값 | addParameter(key, value) |
Android Manifest 설정
먼저 딥링크를 받을 Activity 를 Manifest 에 선언합니다. intent-filter 의
data 에 적은 주소로 링크가 들어올 때 이 Activity 가 열립니다.
<!-- AndroidManifest.xml -->
<activity
android:name=".SchemeActivity"
android:exported="true"
android:noHistory="true"
android:screenOrientation="portrait">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="xxxx"
android:path="/yyyy" />
</intent-filter>
</activity>
app_links·uni_links 같은 플러그인이나 네이티브 설정으로 처리하세요.
SDK 가 관여하는 구간은 쿼리 파라미터 전달부터이고, 거기서부터는 세 플랫폼이 같습니다.https:// 링크 대신 shoplive://live?ak=…&ck=… 같은 우리만의 스킴으로 받고 싶다면,
<intent-filter> 를 아래처럼 하나 더 두면 됩니다. 이때 android:autoVerify 는 빼세요.
도메인 소유를 검증하는 https 링크 전용 속성이라 커스텀 스킴에서는 아무 일도 하지 않습니다.
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="shoplive"
android:host="live" />
</intent-filter>
쿼리 파라미터 전달
여기서부터는 세 플랫폼이 같습니다. URL 에서 쿼리를 하나씩 꺼내 종류에 맞게 넘기면 됩니다.
ak 는 Access Key 로, utm 으로 시작하는 넷은 각각의 전용 setter 로,
그 밖의 쿼리는 addParameter 로 보냅니다. 다 넘긴 뒤 ck 값으로 플레이어를 실행합니다.
// SceneDelegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else { return }
var campaignKey: String? = nil
for item in queryItems {
let value = item.value ?? ""
switch item.name {
case "ak": ShopLiveCommon.setAccessKey(accessKey: value)
case "ck": campaignKey = value
case "utm_source": ShopLiveCommon.setUtmSource(utmSource: value)
case "utm_medium": ShopLiveCommon.setUtmMedium(utmMedium: value)
case "utm_campaign": ShopLiveCommon.setUtmCampaign(utmCampaign: value)
case "utm_content": ShopLiveCommon.setUtmContent(utmContent: value)
default: ShopLive.addParameter(key: item.name, value: value)
}
}
guard let campaignKey = campaignKey else { return }
ShopLive.play(data: .init(campaignKey: campaignKey))
}
class SchemeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handle(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handle(intent)
}
private fun handle(intent: Intent?) {
val uri = intent?.data
if (uri == null) {
finish()
return
}
var campaignKey: String? = null
for (name in uri.queryParameterNames) {
val value = uri.getQueryParameter(name) ?: continue
when (name) {
"ak" -> ShopLiveCommon.setAccessKey(value)
"ck" -> campaignKey = value
"utm_source" -> ShopLive.setUtmSource(value)
"utm_medium" -> ShopLive.setUtmMedium(value)
"utm_campaign" -> ShopLive.setUtmCampaign(value)
"utm_content" -> ShopLive.setUtmContent(value)
else -> ShopLive.addParameter(name, value)
}
}
// 플레이어 실행은 NEW_TASK 로 띄운 Activity 에 맡긴다 (아래 설명 참고)
startActivity(Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("campaignKey", campaignKey)
})
finish()
}
}
final common = ShopLiveCommon();
final player = ShopLivePlayer();
void handleDeepLink(Uri uri) {
uri.queryParameters.forEach((key, value) {
switch (key) {
case 'ak':
common.setAccessKey(accessKey: value);
break;
case 'ck':
break; // 아래에서 바로 사용
case 'utm_source':
common.setUtmSource(utmSource: value);
break;
case 'utm_medium':
common.setUtmMedium(utmMedium: value);
break;
case 'utm_campaign':
common.setUtmCampaign(utmCampaign: value);
break;
case 'utm_content':
common.setUtmContent(utmContent: value);
break;
default:
player.addParameter(key: key, value: value);
}
});
final campaignKey = uri.queryParameters['ck'];
if (campaignKey != null) {
player.play(data: ShopLivePlayerData(campaignKey: campaignKey));
}
}
ShopLive.play() 는 없습니다
예전 문서에는 파라미터를 세팅한 뒤 ShopLive.play() 로 끝내라고 나오지만, 그렇게 쓰면
컴파일되지 않습니다. 지금 쓸 수 있는 것은 play(data:) 하나입니다.
링크에서 꺼낸 campaignKey 를 위 예제처럼 play(data:) 에 실어 넘기세요.addParameter 로 넘기면 통계에 잡히지 않습니다
setUtmSource 같은 전용 setter 로 넣은 값만 Shoplive 어드민 통계에서 유입 경로로 집계됩니다.
addParameter 로 넣으면 그냥 커스텀 쿼리 하나로 붙을 뿐입니다.
utm 네 가지는 반드시 전용 setter 를 쓰세요.SchemeActivity 로 따로 두고 FLAG_ACTIVITY_NEW_TASK 로
다음 화면을 띄우는 이유입니다. 왜 그래야 하는지와 증상은
FAQ · 트러블슈팅 에 정리해 두었습니다.완료 확인
- 딥링크로 앱을 열면 링크의
ck방송이 곧바로 재생된다 - utm 파라미터가 붙은 링크로 들어왔을 때 Shoplive 어드민 통계에 유입 경로가 남는다
- (Android) 딥링크로 진입한 상태에서 상품을 눌러 PIP 로 내려가도 앱 화면이 정상적으로 앞으로 나온다
이전 ← 미션 1다음 → 미션 3 · 사용자 정보 연결
미션 3 · 사용자 정보 연결
플레이어에 시청자가 누구인지 알려주면, 채팅에 이름이 뜨고 상품 클릭·구매 로그가 그 사람 기준으로 쌓입니다. 알려주지 않아도 방송은 보이지만, 시청자를 구분할 수 없습니다.
setUser·setAuthToken 은 지금 시청자가 누구인지 Shoplive 에 알려 주기만 해서, 채팅·시청·통계가 그 회원 기준으로 남게 합니다. Shoplive 는 사용자의 자격 증명을 확인하지 않습니다. 보안 인증(setAuthToken 으로 넘기는 JWT)은 그 정보가 고객사 서버가 발급했고 바뀌지 않았는지(서명)를 Shoplive 가 확인할 수 있고, 간편 인증(setUser)은 그 확인 없이 정보를 그대로 넘깁니다.로그인 상태에 따라 고르기 — 권장 방식
| 사용자 | 권장 | 이유 |
|---|---|---|
| 비로그인 | 게스트 | 회원 정보를 넘길 필요가 없습니다. 대화명만 원하면 따로 지정할 수 있습니다 |
| 로그인 (권장) | 보안 인증 — 백엔드가 서명한 JWT | 위변조할 수 없어 회원 등급별 입장 제한·구매 연동·정확한 시청자 통계에 쓸 수 있습니다 |
| 로그인 (백엔드 작업이 어려울 때) | 간편 인증 — ID·이름 등을 앱에서 직접 전달 | 서명이 없어 앱에서 값을 바꿔치기할 수 있습니다. 식별·통계에는 충분하지만 권한 검증에는 쓰지 마세요 |
| 로그아웃 · 사용자 정보 초기화 | 사용자 정보 초기화 후 플레이어 재실행 | 로그아웃한 회원의 사용자 정보(JWT·프로필)가 남지 않게 지웁니다. 바뀐 값은 다음 재생부터 적용되므로 플레이어를 재실행하세요 |
setAuthToken → 로그아웃 시 사용자 정보 초기화.
바뀐 사용자 정보는 다음 재생부터 적용됩니다. 이미 재생 중이면 게스트 → 로그인 계정 전환처럼 플레이어를 재실행하세요.isLoggedIn() 으로 로그인 여부를 판단하지 마세요
iOS ShopLiveCommon.isLoggedIn() 은 1.9.0 에서 로그인 여부와 관계없이 항상 true 를 돌려줍니다.
로그인 판단은 앱의 회원 상태로 하세요.게스트 계정 사용
import ShopLiveSDK
import ShopliveSDKCommon
let user = ShopLiveCommonUser(userId: "{USER_ID}", userName: "{USER_NAME}")
ShopLiveCommon.setUser(user: user)
ShopLive.play(data: .init(campaignKey: "{CAMPAIGN_KEY}"))
val user = ShopLiveCommonUser(userId = "{USER_ID}").apply {
userName = "{USER_NAME}"
}
ShopLive.setUser(user)
ShopLive.play(this, ShopLivePlayerData(campaignKey))
// common·player 는 미션 1에서 만든 인스턴스를 재사용합니다
final user = ShopLiveCommonUser(userId: "{USER_ID}", userName: "{USER_NAME}");
common.setUser(accessKey: "{ACCESS_KEY}", user: user);
player.play(data: ShopLivePlayerData(campaignKey: "{CAMPAIGN_KEY}"));
간편 인증 사용자 계정 연동
ID·이름 같은 값을 앱에서 그대로 넘깁니다. 서버 작업은 없지만 위조를 막지는 못합니다. userId 에는 회원 ID 원문 대신 해시 값을 써도 됩니다 — ID 해싱 참고.
let user = ShopLiveCommonUser(
userId: "{USER_ID}",
userName: "{USER_NAME}",
age: 25,
gender: .male,
userScore: 80,
custom: ["grade": "vip"])
ShopLiveCommon.setUser(user: user)
ShopLive.play(data: .init(campaignKey: "{CAMPAIGN_KEY}"))
val user = ShopLiveCommonUser(userId = "{USER_ID}").apply {
userName = "{USER_NAME}"
gender = ShopLiveCommonUserGender.MALE
age = 25
userScore = 20
}
ShopLive.setUser(user)
final user = ShopLiveCommonUser(
userId: "{USER_ID}",
userName: "{USER_NAME}",
gender: ShopLiveCommonUserGender.MALE,
userScore: 100,
age: 25,
custom: {"key": "value"},
);
common.setUser(accessKey: "{ACCESS_KEY}", user: user);
ShopLiveCommonUser 대신 ShopLiveCommonUserObjc 를 쓰세요. 생성자는
initWithUserId:userName:age:gender:userScore:custom: 이고, age · userScore 는
NSNumber, gender 는 문자열(m · f · n)입니다.ID 해싱 — 회원 ID 원문을 노출하지 않기
내부 회원 ID를 그대로 넘기고 싶지 않을 때는, 앱과 백엔드가 동일한 방식으로 해싱한 값을 userId로 사용합니다.
같은 사용자는 항상 같은 값이 나오도록 일관된 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) }
}
// pubspec.yaml — dependencies: crypto
import 'dart:convert';
import 'package:crypto/crypto.dart';
String hashUserId(String raw) {
final salted = raw + "APP_SALT";
return sha256.convert(utf8.encode(salted)).toString();
}
보안 인증용 JWT 만들기 (고객사 백엔드)
보안 인증에 쓸 JWT는 Shoplive 담당자로부터 발급받은 Secret Key로 고객사 백엔드가 서명합니다. 앱은 로그인 직후 백엔드에서 JWT를 받아 setAuthToken으로 넘기기만 합니다. 앱 쪽 코드는 보안 인증 사용자 계정 연동에 있습니다.
- Shoplive 담당자에게서 Secret Key를 받습니다 — base64로 인코딩된 문자열입니다
- 로그인한 사용자 정보로 아래 payload를 만들고 HS256으로 서명합니다
- 앱이 요청할 때마다(재생 직전마다) JWT 를 새로 발급해 내려 줍니다. 앱에 JWT 를 저장해 두고 다시 쓰지 않습니다
exp 까지 계속 쓸 수 있습니다. 앱이 setAuthToken 을 부를 때마다 백엔드에 새 JWT 를 요청하게 하세요.| claim | 필수 | 설명 | 예 |
|---|---|---|---|
exp | 필수 | 만료 시각 (Unix 초) | 1516239022 |
userId | 필수 | 로그인 사용자의 고유 값 (최대 255바이트). 해시 값을 써도 됩니다(ID 해싱) | sample_user_id |
iat | 발급 시각 (Unix 초) | 1516195822 | |
name | 채팅에 보일 이름. 금칙어는 별표(*)로 바뀝니다 | {USER_NAME} | |
gender | 성별 — m · f · 빈 값(미지정) | f | |
age | 나이. 통계에는 5세 단위로 묶여 표시됩니다 | 27 | |
userScore | 사용자 등급 (-100 ~ 100, 서버 규칙 · SDK 는 검사하지 않음). 등급별 입장·이벤트 추첨 제한의 기준 | 10 | |
userType | 사용자 유형. 입장 제한(Entry Type)을 건 방송에서 특정 유형만 입장시킬 때 | VIP | |
profile | 채팅 프로필 이미지 URL (64×64px 권장) | https://…/profile.png | |
custom | 임의 데이터 (최대 1KB, 서버 규칙 · SDK 는 검사하지 않음) | {"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: "{USER_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", "{USER_NAME}")
.claim("gender", "f")
.claim("age", 27)
.claim("userScore", 10)
.compact();
보안 인증 사용자 계정 연동
위에서 백엔드가 서명한 JWT 를 받아, 재생 직전에 SDK 에 넘깁니다.
ShopLiveCommon.setAuthToken(authToken: "{USER_JWT}")
ShopLive.play(data: .init(campaignKey: "{CAMPAIGN_KEY}"))
ShopLiveCommon.setAuthToken("{USER_JWT}")
ShopLive.play(this, ShopLivePlayerData(campaignKey))
common.setAuthToken(userJWT: "{USER_JWT}");
player.play(data: ShopLivePlayerData(campaignKey: "{CAMPAIGN_KEY}"));
ShopLiveCommon.setUserJWT를 가리키는 경우가 있지만 실제로 그런 API 는 없습니다. 반드시 ShopLiveCommon.setAuthToken(authToken:)를 사용하세요.ShopLive.getAuthToken()은 setAuthToken()으로 넣은 값만 돌려줍니다. setUser()로 발급된 JWT 를 읽으려면 ShopLiveCommon.getAuthToken()을 사용하세요.ShopLiveCommon().setUser(accessKey:user:)는 두 인자 모두 필수입니다. accessKey 를 생략할 수 없습니다.사용자 이름 변경 감지
사용자가 Shoplive Player 안에서 직접 대화명을 바꾸면 SDK 가 변경 사항을 앱에 알려줍니다.
func onSetUserName(_ payload: [String: Any]) {
payload.forEach { key, value in
print("onSetUserName key: \(key) value: \(value)")
}
}
override fun onSetUserName(jsonObject: JSONObject) {
super.onSetUserName(jsonObject)
Log.d(TAG, "userId=${jsonObject.get("userId")}, userName=${jsonObject.get("userName")}")
}
player.userInfo.listen((data) {
print("userInfo: ${data.userInfo}");
});
userInfo 스트림이 iOS·Android 의
onSetUserName과 같은 네이티브 이벤트를 전달합니다.게스트 → 로그인 계정 전환
어드민에서 게스트 채팅을 OFF로 막아둔 상태에서 사용자가 채팅을 시도하면 LOGIN_REQUIRED 명령이 전달됩니다.
로그인 화면으로 보낸 뒤, 로그인 성공 시 위 간편·보안 인증으로 사용자 정보를 다시 설정하고 플레이어를 재실행하세요.
func handleReceivedCommand(_ command: String, data: [String: Any]?) {
switch command {
case "LOGIN_REQUIRED":
// 로그인 화면 이동 → 로그인 성공 시 사용자 정보 재설정 후 플레이어 재실행
break
default:
break
}
}
override fun onReceivedCommand(context: Context, command: String, data: JSONObject) {
when (command) {
"LOGIN_REQUIRED" -> {
// 로그인 화면 이동 → 로그인 성공 시 사용자 정보 재설정 후 플레이어 재실행
}
}
}
player.receivedCommand.listen((data) {
switch (data.command) {
case "LOGIN_REQUIRED":
// 로그인 화면 이동 → 로그인 성공 시 사용자 정보 재설정 후 플레이어 재실행
break;
default:
break;
}
});
로그아웃 — 사용자 정보 초기화
로그아웃하면 설정한 사용자 정보(JWT·프로필)를 지웁니다. 이름은 setAuthToken 이지만 넘기는 값은 사용자 정보(JWT)입니다. 바뀐 값은 다음 재생부터 적용되므로, 재생 중이었다면 플레이어를 재실행하세요.
// 로그아웃 — 사용자 정보 초기화 (JWT·프로필을 지운다)
ShopLiveCommon.setAuthToken(authToken: nil)
ShopLiveCommon.setUser(user: nil)
// 한 번에 지우기 — JWT · 사용자 · accessKey 를 모두 지운다
ShopLiveCommon.clearAuth()
ShopLiveCommon.setAccessKey(accessKey: "{ACCESS_KEY}") // 다음 재생 전에 다시 설정
// 로그아웃 — 사용자 정보 초기화 (JWT·프로필을 지운다)
ShopLiveCommon.setAuthToken(null)
ShopLiveCommon.setUser(null)
// ShopLive.resetUser() 는 setUser(null) 과 같다
// 한 번에 지우기 — 사용자 · JWT 만 지운다 (accessKey 는 유지)
ShopLiveCommon.clearAuth()
// 로그아웃 — 사용자 정보 초기화
common.setAuthToken(userJWT: null);
common.clearAuth(); // Flutter 의 setUser 는 null 을 받지 않는다
clearAuth() 는 플랫폼마다 지우는 범위가 다릅니다
- iOS
ShopLiveCommon.clearAuth()는 JWT · 사용자 · accessKey 를 모두 지웁니다. 다음 재생 전에setAccessKey를 다시 호출하세요. - Android
ShopLiveCommon.clearAuth()는 사용자 · JWT 만 지우고 accessKey 는 유지합니다(iOS 와 다름).ShopLiveCommon.isLoggedIn()은 JWT 가 있으면true입니다.
clearAuth() 는 네이티브 ShopLiveCommon.clearAuth() 를 그대로 부릅니다. iOS 에서는 accessKey 까지 초기화되므로, 다음 로그인 때 setUser(accessKey:user:) 로 accessKey 를 다시 넘기세요. Flutter 의 setUser 는 어차피 accessKey 가 필수 인자입니다.완료 확인
- 로그인 후 시청 시 채팅 닉네임이 회원 이름으로 표시된다
- Shoplive 어드민 통계에서 해당 사용자 ID(해시)가 조회된다
- 게스트로 시청 시 채팅 대화명이 정상 표시되고, 로그아웃 후 사용자 정보를 초기화하고 플레이어를 재실행하면 게스트로 시청된다
- 재생할 때마다 백엔드에서 새로 발급한 JWT 를 넘기고, Secret Key 는 앱 코드·번들에 없다
- Android 에서
ShopLiveCommon.getAuthToken()과ShopLive.getAuthToken()을 혼동하지 않았다
미션 4 · 채팅
- Shoplive 어드민에서 방송 상세 정보 → 채팅 권한으로 들어가 게스트 채팅 허용 여부를 확인합니다.
- 게스트 채팅을 막아둔 경우 미션 3의 로그인 유도 흐름이 그대로 적용됩니다.
- (선택) 채팅·전송 버튼 폰트를 앱 폰트에 맞게 변경합니다.
let inputFont = UIFont.systemFont(ofSize: 14, weight: .regular)
let sendButtonFont = UIFont.systemFont(ofSize: 14, weight: .medium)
ShopLive.setChatViewFont(inputBoxFont: inputFont, sendButtonFont: sendButtonFont)
val nanumGothic = ResourcesCompat.getFont(requireContext(), R.font.nanumgothic)
ShopLiveCommon.setTextTypeface(nanumGothic)
완료 확인
- 어드민의 채팅 권한 설정과 앱의 실제 동작(게스트 허용 여부)이 일치한다
- (폰트 변경 시) 채팅 입력창·전송 버튼에 지정한 폰트가 적용된다
미션 5 · 쿠폰
LINK / LINK(새 창)
쿠폰 클릭 이벤트를 LINK 또는 LINK (New window)로 설정한 경우입니다.
func handleNavigation(with url: URL) {
let safari = SFSafariViewController(url: url)
present(safari, animated: true)
}
override fun handleNavigation(context: Context, url: String) {
val intent = Intent(this@YourActivity, WebViewActivity::class.java)
intent.putExtra("url", url)
startActivity(intent)
}
player.handleNavigation.listen((data) {
launchUrl(Uri.parse(data.url), mode: LaunchMode.externalApplication);
});
Download coupon
쿠폰 클릭 이벤트를 Download coupon으로 선택한 경우입니다. 쿠폰 ID 를 전달받아 발급 처리 후 결과를 SDK 에 돌려줍니다.
func handleDownloadCoupon(with couponId: String, result: @escaping (ShopLiveCouponResult) -> Void) {
let couponResult = ShopLiveCouponResult(
couponId: couponId, success: true,
message: "Coupon download success!",
status: .SHOW, alertType: .ALERT)
result(couponResult)
}
override fun handleDownloadCoupon(
context: Context,
couponId: String,
callback: ShopLiveHandlerCallback
) {
callback.couponResult(
isDownloadSuccess = true,
message = "Coupon download success!",
couponStatus = ShopLive.CouponPopupStatus.HIDE,
alertType = ShopLive.CouponPopupResultAlertType.ALERT
)
}
player.handleDownloadCoupon.listen((data) async {
await player.sendDownloadCouponResult(
couponId: data.couponId,
success: true,
message: "Coupon download success!",
popupStatus: "KEEP",
alertType: "ALERT",
);
});
callback은 SDK 가 이미 만들어 전달하는 객체입니다. callback.couponResult(...)만 호출하면 되고, 직접 ShopLiveHandlerCallback을 새로 만들 필요는 없습니다.User define
쿠폰 클릭 이벤트를 User define으로 선택한 경우입니다. 고객사가 정의한 payload 를 그대로 전달받습니다.
func handleCustomAction(with id: String, type: String, payload: Any?, result: @escaping (ShopLiveCustomActionResult) -> Void) {
let actionResult = ShopLiveCustomActionResult(
id: id, success: true, message: "success!",
status: .SHOW, alertType: .ALERT)
result(actionResult)
}
override fun handleCustomAction(
context: Context,
id: String,
type: String,
payload: JSONObject,
callback: ShopLiveHandlerCallback
) {
callback.customActionResult(
isSuccess = true,
message = "success!",
couponStatus = ShopLive.CouponPopupStatus.HIDE,
alertType = ShopLive.CouponPopupResultAlertType.ALERT
)
}
player.handleCustomAction.listen((data) async {
await player.sendCustomActionResult(
id: data.id,
success: true,
message: "success!",
popupStatus: "SHOW",
alertType: "ALERT",
);
});
customActionResult의 3번째 인자명은 couponStatus입니다. 커스텀 액션인데도 이름이 쿠폰 계열과 같습니다. SDK 소스가 그렇게 정의되어 있어 그대로 사용합니다.
Flutter 의 동등한 인자명은 popupStatus로 서로 다르니, 플랫폼을 바꿔가며 코드를 복사할 때 주의하세요.
완료 확인
- LINK 쿠폰 클릭 시 지정한 방식(사파리·웹뷰·외부 브라우저)으로 이동한다
- Download coupon 클릭 시 발급 결과 팝업(SHOW/HIDE/KEEP)이 의도대로 나타난다
- User define 클릭 시 payload 의 id·type 이 로그에 정상 출력된다
이전 ← 미션 4다음 → 미션 6 · 상품 클릭
미션 6 · 상품 클릭
- 상품 목록에서 상품이나 장바구니 버튼을 누르면 SDK 가
CLICK_PRODUCT_DETAIL·CLICK_PRODUCT_CART커맨드를 앱에 전달합니다. - 상품·배너에 상세 URL 이 설정돼 있으면 같은 클릭이
handleNavigation으로도 전달됩니다. - iOS 는 링크 이동만 클로저 하나로 가로채는
hookNavigation도 제공합니다.
상품·장바구니 클릭 커맨드
커맨드 이름은 세 플랫폼이 같습니다. 커맨드 수신 콜백은 쿠폰·로그인 요청 등 다른 커맨드도 함께 들어오므로 커맨드 이름으로 분기해서 처리하세요.
// ShopLiveSDKDelegate
func handleReceivedCommand(_ command: String, data: [String: Any]?) {
switch command {
case "CLICK_PRODUCT_DETAIL":
// data: 선택한 상품 정보
break
case "CLICK_PRODUCT_CART":
// data: 장바구니에 담은 상품 정보
break
default:
break
}
}
// ShopLiveHandler
override fun onReceivedCommand(context: Context, command: String, data: JSONObject) {
when (command) {
"CLICK_PRODUCT_DETAIL" -> {
// data: 선택한 상품 정보
}
"CLICK_PRODUCT_CART" -> {
// data: 장바구니에 담은 상품 정보
}
}
}
final player = ShopLivePlayer();
player.receivedCommand.listen((event) {
switch (event.command) {
case 'CLICK_PRODUCT_DETAIL':
// event.data: 선택한 상품 정보
break;
case 'CLICK_PRODUCT_CART':
// event.data: 장바구니에 담은 상품 정보
break;
}
});
handleReceivedCommand(_:with:) 는 deprecated 입니다
인자가 with payload: Any? 인 쪽은 구버전이고, 현행은 위 예제의 data: [String: Any]? 입니다.
둘 다 @objc optional 이라 잘못된 쪽을 구현해도 빌드는 통과하므로, 값이 안 들어온다면 이 시그니처부터 확인하세요.링크 이동 처리 (handleNavigation)
상품이나 배너에 상세 URL 을 등록해 두었다면, 그 클릭은 handleNavigation 으로도 전달됩니다.
미션 5의 LINK 쿠폰과 같은 콜백이라서 쿠폰·상품·배너 클릭이 전부 여기로 모입니다.
URL 만 보고 어디로 보낼지 정해야 하므로, 링크 형식을 미리 정해 두면 분기하기 편합니다.
func handleNavigation(with url: URL) {
// url: 선택한 상품 또는 배너에 설정된 상세 정보 URL
print("handleNavigation \(url)")
}
override fun handleNavigation(context: Context, url: String) {
Log.d(TAG, "handleNavigation url=$url")
}
final player = ShopLivePlayer();
player.handleNavigation.listen((event) {
debugPrint("handleNavigation : ${event.url}");
});
상세 URL 이 등록된 상품을 누르면 CLICK_PRODUCT_DETAIL 커맨드와
handleNavigation 이 둘 다 호출됩니다. 웹 플레이어가 클릭 한 번을 별개의 메시지
두 개로 보내기 때문입니다. (상세 URL 이 없는 상품은 handleNavigation 없이 커맨드만 옵니다.)
그래서 양쪽에서 화면 전환을 구현하면 상세 화면이 두 번 열립니다.
둘 중 한쪽에서만 이동을 처리하세요. 상세 URL 을 쓰는 상품만 있다면 handleNavigation 한쪽,
URL 없는 상품도 섞여 있다면 커맨드 한쪽으로 몰아 두는 편이 단순합니다.
iOS 전용 hookNavigation
델리게이트를 거치지 않고 링크 이동만 클로저로 받고 싶을 때 씁니다.
ShopLive.hookNavigation { url in
// url: 선택한 상품 또는 배너에 설정된 상세 정보 URL
print("hookNavigation \(url)")
}
handleNavigation 델리게이트는
물론이고, 링크를 누른 뒤 플레이어를 PIP 로 내리거나 닫는 동작(setNextActionOnHandleNavigation)도
실행되지 않습니다. 둘 중 하나만 쓰세요.완료 확인
- 상품 목록에서 상품을 누르면
CLICK_PRODUCT_DETAIL이 로그에 찍힌다 - 장바구니 버튼을 누르면
CLICK_PRODUCT_CART가 찍힌다 - 상세 URL 이 설정된 상품을 누르면
handleNavigation으로 URL 이 전달된다 - (iOS, hookNavigation 사용 시)
handleNavigation대신 hook 클로저만 호출된다
이전 ← 미션 5다음 → 미션 7 · 방송 링크 공유
미션 7 · 방송 링크 공유
- 공유 버튼이 내보낼 링크 형식(URI Scheme 또는 웹 URL)을
setShareScheme로 지정합니다. - 따로 처리하지 않으면 SDK 가 OS 기본 공유 시트를 띄웁니다.
- 앱 디자인에 맞춘 공유 UI 를 쓰려면 공유 데이터를 받아 직접 화면을 띄웁니다.
공유 링크 형식 지정
공유 링크에 ak(Access Key)·ck(Campaign Key)를 실어두면, 링크를 받은 쪽에서
그 값으로 같은 방송에 진입시킬 수 있습니다.
// URI Scheme
let scheme = "{your_scheme}://{your_host}?ak=xxx&ck=yyy"
ShopLive.setShareScheme(scheme, shareDelegate: nil)
// 또는 웹 URL
ShopLive.setShareScheme("https://www.share_link.com/?ak=xxx&ck=yyy", shareDelegate: nil)
// URI Scheme
val scheme = "{your_scheme}://{your_host}?ak=xxx&ck=yyy"
ShopLive.setShareScheme(scheme)
// 또는 웹 URL
ShopLive.setShareScheme("https://www.share_link.com/?ak=xxx&ck=yyy")
final player = ShopLivePlayer();
player.setShareScheme(
shareSchemeUrl: "{your_scheme}://{your_host}?ak=xxx&ck=yyy",
);
custom: 파라미터는 존재하지 않습니다
실제 시그니처는 setShareScheme(_ scheme: String?, shareDelegate: ShopLivePlayerShareDelegate?) 입니다.
클로저를 넘기는 custom: 레이블은 SDK 에 없어 그대로 쓰면 컴파일되지 않습니다.
기본 공유 시트를 쓸 때는 shareDelegate: nil 을 넘기세요.기본 시스템 공유 UI
iOS 는 shareDelegate: nil 로 넘기고, Android 는 handleShare 를 재정의하지 않으면
SDK 가 OS 기본 공유 시트를 띄웁니다. 별도 코드가 필요 없습니다.
(iOS 의 shareDelegate 는 기본값이 없어 생략할 수 없고 nil 을 명시해야 합니다.)
커스텀 공유 UI
앱 디자인에 맞춘 공유 화면을 쓰려면 공유 데이터(url · 캠페인 정보)를 받아 직접 띄웁니다.
// 1) 공유 데이터를 받을 델리게이트를 지정
ShopLive.setShareScheme(scheme, shareDelegate: self)
// 2) 델리게이트에서 앱의 공유 UI 를 띄운다
extension MainViewController: ShopLivePlayerShareDelegate {
func handleShare(data: ShopLivePlayerShareData) {
guard let urlString = data.url, let url = URL(string: urlString) else { return }
let activityVC = UIActivityViewController(activityItems: [url],
applicationActivities: nil)
ShopLive.viewController?.present(activityVC, animated: true, completion: nil)
}
}
// ShopLiveHandler. 재정의하지 않으면 SDK 가 기본 공유 시트를 띄웁니다
override fun handleShare(context: Context, data: ShopLivePlayerShareData) {
val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.custom_share_dialog, null)
AlertDialog.Builder(context)
.setView(view)
.create()
.show()
}
handleShare 의 두 번째 인자는 String 이 아닙니다
원문은 handleShare(context: Context, shareUrl: String) 로 적혀 있으나 그런 시그니처는 없습니다.
실제는 handleShare(context: Context, data: ShopLivePlayerShareData) 이고,
data.url 외에 data.campaign(campaignKey·title·description·thumbnail)도 함께 옵니다.setShareScheme 로 링크 형식만 지정하고
공유 UI 는 각 네이티브의 기본 공유 시트를 그대로 씁니다.완료 확인
- 플레이어의 공유 버튼을 누르면 지정한 scheme·URL 이 담긴 공유 시트가 뜬다
- 공유된 링크에
ak·ck쿼리가 그대로 들어 있다 - (커스텀 UI 사용 시) 기본 공유 시트 대신 앱의 공유 화면이 뜬다
이전 ← 미션 6다음 → 미션 8 · PIP
미션 8 · PIP
PIP 는 두 종류입니다. 앱 내 PIP 는 우리 앱 안에서만 떠 있는 작은 창이고, OS PIP 는 홈 화면이나 다른 앱 위까지 올라가는 시스템 창입니다. 둘은 만드는 주체가 달라서 설정도 따로 합니다. 플랫폼마다 쓸 수 있는 범위도 다릅니다.
| 플랫폼 | 앱 내 PIP | OS PIP |
|---|---|---|
| iOS | 지원 | 지원 (프로젝트에서 Background Modes 활성화 필요) |
| Android | 폐기. 호출해도 동작하지 않음 | 지원 (Android 8.0 이상) |
| Flutter | 전용 옵션 없음. 전환 API 만 있고 실제 동작은 각 네이티브를 그대로 따름 | |
showPreviewPopup 하나로 처리합니다.앱 내 PIP
플레이어의 PIP 아이콘을 누르거나 화면을 아래로 쓸어내리면 방송이 앱 안의 작은 창으로 내려갑니다. 미리보기와 달리 소리는 계속 납니다. 이 기능을 실제로 쓸 수 있는 것은 iOS 와 Flutter(iOS 빌드) 뿐입니다. Android 는 아래에 적은 대로 폐기됐습니다.
// PIP 전체 허용 여부 (앱 내 PIP · OS PIP 공통 전제)
ShopLive.setEnabledPictureInPictureMode(isEnabled: true)
// 크기·위치·모서리·닫기 버튼을 한 번에 지정
let config = ShopLiveInAppPipConfiguration(
useCloseButton: true,
pipPosition: .bottomRight,
enableSwipeOut: true,
pipSize: ShopLiveInAppPipSize(pipMaxSize: 200),
pipRadius: 10,
pipPinPositions: [.topLeft, .topRight, .bottomLeft, .bottomRight],
closeButtonConfig: ShopLiveCloseButtonConfig(position: .topRight, width: 30, height: 30, offsetX: 3, offsetY: 3, color: .white)
)
ShopLive.setInAppPipConfiguration(config: config)
final player = ShopLivePlayer();
// 앱 내 PIP 와 미리보기 창은 같은 창이라 설정도 같은 호출 하나로 합니다
player.setPreviewOption(
data: ShopLivePlayerPreviewData(
campaignKey: '재생할_CAMPAIGN_KEY',
useCloseButton: true,
enableSwipeOut: true,
pipRadius: 10,
pipMaxSize: 400,
position: 'BOTTOM_RIGHT',
),
);
창 크기는 pipSize 로 정합니다. pipMaxSize(긴 변 기준, 나머지 변은 영상 비율에 맞춰 자동)·
pipFixedWidth·pipFixedHeight 중 하나만 받는 생성자로 나뉘어 있으니,
원하는 기준 하나를 골라 넣으세요.
iOS 닫기 버튼 모양은 closeButtonConfig(ShopLiveCloseButtonConfig)로 정합니다.
모든 필드는 선택이고, 그림자 관련 필드는 기본값이 없습니다.
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
position | .topLeft · .topRight | .topLeft | 닫기 버튼 위치 |
width · height | CGFloat | 30 | 버튼 크기 |
offsetX · offsetY | CGFloat | 3 | 모서리에서 떨어진 거리 |
color | UIColor | .white | 버튼 색 |
shadowOffsetX · shadowOffsetY | CGFloat | 없음 | 그림자 오프셋 |
shadowBlur | CGFloat | 없음 | 그림자 블러 반경 |
shadowBlurStyle | .normal · .solid · .inner · .outer | 없음 | 그림자 블러 스타일 |
shadowColor | UIColor | 없음 | 그림자 색 |
imageStr | String | 없음 | 버튼 이미지 URL |
setEnabledPictureInPictureMode 는 기본값이 true 입니다. 이걸 false 로 두면
PIP 전환이 요청될 때 작은 창으로 내려가는 대신 플레이어가 종료됩니다.
"PIP 기능만 끄고 전체화면은 유지" 가 아니니 주의하세요.showInAppPip()·hideInAppPip() 를 호출해도 아무 일도 일어나지 않고,
isInAppPipMode() 는 언제나 false 를 돌려줍니다.
setEnabledPictureInPictureMode(isEnabled, useInAppPip) 의 두 번째 인자도 무시됩니다.
Android 에서 작은 창이 필요하면 아래 OS PIP 를 쓰세요.setPreviewOption(ShopLivePlayerPreviewData) 하나가
iOS 에서는 setInAppPipConfiguration 과 setPictureInPicturePadding 으로,
Android 에서는 ShopLivePreviewData 로 이어집니다.
미션 9 의 미리보기 설정과 같은 호출이니 한 번만 지정하면 됩니다.
대신 Flutter 에 아예 없는 것도 있습니다. OS PIP 활성화 토글(setEnabledPictureInPictureMode 계열)과
Android 오버레이 권한 API(ShopLivePermissionManager)는 Flutter SDK 에 없어,
필요하면 네이티브 코드를 직접 불러야 합니다.
예전 문서를 보고 연동했다면: 없어진 API 세 가지
pipScale·setEnabledPipSwipeOut·ShopLive.useCloseButton(_:) 은 deprecated 입니다.
셋 다 위 예제의 setInAppPipConfiguration(config:) 하나로 합쳐졌습니다.
크기는 pipSize, 화면 밖으로 밀어 닫기는 enableSwipeOut, 닫기 버튼은
useCloseButton 프로퍼티로 옮겨 적으면 됩니다.
설정 항목 자체가 없어진 것은 아니고, 전역 함수로 하나씩 부르던 방식만 사라졌습니다.
OS PIP
홈 화면으로 나가거나 다른 앱으로 전환해도 시스템 창으로 재생을 이어갑니다.
iOS Xcode 프로젝트에서 Signing & Capabilities → Background Modes 의 Audio, AirPlay, and Picture in Picture 를 켜야 동작합니다. 이 설정을 빠뜨리면 코드가 맞아도 OS PIP 로 내려가지 않습니다.
ShopLive.setEnabledOSPictureInPictureMode(isEnabled: true)
setEnabledPictureInPictureMode 가 켜져 있어야 동작합니다
두 플래그는 AND 조건입니다. setEnabledPictureInPictureMode(isEnabled: false) 로 두면
setEnabledOSPictureInPictureMode(isEnabled: true) 를 호출해도 OS PIP 컨트롤러가
만들어지지 않습니다. "앱 내 PIP 만 끄고 OS PIP 는 쓰겠다" 는 설정은 불가능합니다.Android OS PIP 는 기본으로 켜져 있습니다. 비율만 서비스에 맞게 지정하면 됩니다.
// OS PIP 진입 허용 (기본값 true)
ShopLive.setEnabledPictureInPictureMode(true)
// PIP 창 비율. 지정하지 않으면 9:16
// RATIO_1X1 / RATIO_1X2 / RATIO_2X3 / RATIO_3X4 / RATIO_9X16
ShopLive.setPIPRatio(ShopLivePIPRatio.RATIO_9X16)
android:supportsPictureInPicture="true" 와
SYSTEM_ALERT_WINDOW 권한이 SDK 모듈 Manifest 에 이미 선언돼 있어 Manifest 병합으로 앱에 반영됩니다.ShopLivePermissionManager.request(this, ShopLivePermissionType.DRAW_OVERLAY) { hasPermission ->
// hasPermission == true 이면 오버레이 권한이 허용된 상태입니다
}
이 헬퍼는 별도 모듈이라 의존성을 추가해야 씁니다.
// app/build.gradle
implementation "cloud.shoplive:shoplive-permission:$shoplive_sdk_version"
UNSUPPORTED_OS_PIP_MODE(9500) 에러가 onError 로 전달됩니다.
8.0 이상이어도 시스템 설정에서 이 앱의 PIP 가 꺼져 있으면 SDK 가 설정 화면으로 이동할지 묻는 다이얼로그를 띄웁니다.
사용자에게 보이는 흐름이므로 미리 확인해 두세요.PIP 전환 API
사용자 조작(PIP 버튼·스와이프) 말고 앱 코드에서 직접 PIP 로 내리거나 전체화면으로 되돌릴 수 있습니다. 로그인 화면으로 이동시키기 직전처럼, 플레이어를 잠시 비켜둘 때 씁니다.
// 앱 내 PIP 로 전환
ShopLive.startPictureInPicture()
// 전체화면으로 복귀
ShopLive.stopPictureInPicture()
// OS PIP 로 전환. 오버레이 권한 안내 화면이 닫히기를 기다려야 하면 true
ShopLive.startPictureInPicture(shouldDelayForOverlayPermissionFlow = false)
// 전체화면으로 복귀
ShopLive.stopPictureInPicture()
final player = ShopLivePlayer();
player.startPictureInPicture();
player.stopPictureInPicture();
startPictureInPicture() 는 deprecated 입니다
startPictureInPicture(shouldDelayForOverlayPermissionFlow: Boolean) 를 쓰세요.
true 를 주면 권한 안내 화면이 정리될 시간을 잠시 두고 PIP 로 내려갑니다.iOS 전용 PIP 이동 영역과 여백
앱 내 PIP 가 하단 탭바나 필수 버튼을 가리지 않도록, 이동할 수 있는 영역과 PIP 자체의 여백을 제한할 수 있습니다.
offset 으로,
PIP 창 자체의 여백(회색 점선)은 padding 으로 정합니다// 화면 가장자리에서 PIP 가 들어오지 못하게 할 영역
let offsetApplied = ShopLive.setPictureInPictureFloatingOffset(
offset: UIEdgeInsets(top: 60, left: 0, bottom: 90, right: 0))
// PIP 자체에 줄 여백
let paddingApplied = ShopLive.setPictureInPicturePadding(
padding: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8))
Bool 을 돌려줍니다. 반환값을 꼭 확인하세요
오프셋과 패딩을 합친 영역이 화면 가로·세로를 넘어서면 값을 적용하지 않고 false 를 돌려줍니다.
무시하면 설정이 조용히 빠진 채로 동작합니다.완료 확인
- (iOS) 플레이어에서 아래로 쓸어내리면 앱 안의 작은 창으로 내려가고 오디오가 끊기지 않는다
- (iOS) Background Modes 를 켠 뒤 홈으로 나가면 시스템 PIP 창으로 재생이 이어진다
- (Android) 홈으로 나가면 지정한 비율의 OS PIP 창으로 전환된다
startPictureInPicture·stopPictureInPicture호출로 전환·복귀가 동작한다- (iOS, 오프셋 설정 시) 두 함수의 반환값이
true이고 PIP 가 지정한 영역 밖으로 나가지 않는다
이전 ← 미션 7다음 → 미션 9 · 방송 미리보기
미션 9 · 방송 미리보기
미리보기는 기본이 음소거이고, 창을 탭하면 전체화면 플레이어로 전환할 수 있습니다.
Android 에서는 Activity 위에 띄우는 Popup Preview 가 권장 방식입니다.
showPreviewPopup 하나로 처리합니다.미리보기 시작
let previewData = ShopLivePreviewData(
campaignKey: "재생할_CAMPAIGN_KEY",
keepWindowStateOnPlayExecuted: true,
referrer: nil,
isMuted: true,
isEnabledVolumeKey: false,
resolution: .PREVIEW
)
ShopLive.preview(data: previewData) {
// 미리보기 창을 탭했을 때 전체화면으로 넘기려면
ShopLive.play(data: .init(campaignKey: "재생할_CAMPAIGN_KEY"))
}
ShopLive.showPreviewPopup(this, ShopLivePreviewData("재생할_CAMPAIGN_KEY").apply {
useCloseButton = true
enabledSwipeOut = true
resolutionType = ShopLivePreviewResolutionType.LIVE
onClickListener = View.OnClickListener { view ->
// 미리보기 → 플레이어 전환 애니메이션
ShopLive.setPreviewTransitionAnimation(this@MainActivity, view)
ShopLive.play(this@MainActivity, ShopLivePlayerData("재생할_CAMPAIGN_KEY"))
}
})
final player = ShopLivePlayer();
// 1) 옵션을 먼저 지정
player.setPreviewOption(
data: ShopLivePlayerPreviewData(
campaignKey: '재생할_CAMPAIGN_KEY',
useCloseButton: true,
enableSwipeOut: true,
pipRadius: 10,
pipMaxSize: 400,
position: 'BOTTOM_RIGHT',
),
);
// 2) 미리보기 시작
player.showPreview(campaignKey: '재생할_CAMPAIGN_KEY');
미리보기를 코드로 닫아야 할 때는 아래를 호출합니다. 다른 화면으로 이동하는 순간처럼 닫아야 하는 시점에 부르는 것이지, 띄운 직후에 부르는 것이 아닙니다.
player.hidePreview();
iOS preview(data:) 뒤에 붙는 completion 은 재생이 시작될 때가
아니라 사용자가 미리보기 창을 탭했을 때 불립니다. 그리고 completion 을 넘기는 순간
"탭하면 전체화면" 이라는 SDK 기본 동작이 사라집니다. 전체화면으로 넘기고 싶다면 위 예제처럼
completion 안에서 직접 play(data:) 를 부르고, 기본 동작을 그대로 쓰고 싶다면 nil 을 넘기세요.
Flutter 옵션을 먼저, 노출을 나중에 호출해야 합니다.
showPreview 는 campaignKey 만 받고(showPreview({required String campaignKey})),
옵션은 setPreviewOption 으로 따로 넣습니다. 순서가 바뀌면 옵션이 반영되지 않습니다.
예전 문서에 나오는 showPreview(data:) 는 없는 API 입니다.
Android ShopLivePreviewData(campaignKey) 한 인자짜리를 쓰세요.
ShopLivePreviewData(activity, accessKey, campaignKey) 는 인자 세 개가 전부 deprecated 입니다.
accessKey 는 미션 1 처럼 ShopLiveCommon.setAccessKey 로 미리 넣어 두면 됩니다.
showPreviewPopup 이 null 을 돌려줄 수 있습니다
이미 플레이어가 떠 있거나 대상 Activity 가 종료 중이면 미리보기를 띄우지 않고 null 을 반환합니다.
반환값을 받아 쓰는 코드라면 null 처리를 빠뜨리지 마세요.옵션 전체와 기본값은 Android ShopLivePreviewData 속성 ·
Flutter ShopLivePlayerPreviewData 표에 있습니다.
Android 저장해 둔 옵션으로 띄우기
미리보기 옵션을 앱 시작 시 한 번 저장해 두면, 이후에는 campaignKey 만 넘겨 띄울 수 있습니다.
// 옵션을 한 번 저장. campaignKey 는 showPreviewPopup 호출 값으로 덮어써집니다
ShopLive.setPreviewOptions(ShopLivePreviewData("재생할_CAMPAIGN_KEY").apply {
useCloseButton = true
enabledSwipeOut = true
})
// 띄울 때는 campaignKey 만
ShopLive.showPreviewPopup(this, "재생할_CAMPAIGN_KEY")
ShopLivePreviewData("") 처럼 빈 campaignKey 를 넣지 마세요
생성자가 campaignKey 를 검증해, 비어 있으면 NOT_INITIALIZED_CAMPAIGN_KEY 에러를
onError 로 흘립니다. 옵션만 미리 저장하는 경우에도 실제 campaignKey 를 넣으세요.배너형 미리보기 뷰 — 레이아웃에 끼워 넣기
지금까지의 Popup Preview 는 화면 위에 떠 있는 창입니다. 그 대신 레이아웃 안에
직접 끼워 넣고 싶을 때, 예를 들어 목록의 셀마다 미리보기를 붙이고 싶을 때는 미리보기 뷰를 씁니다.
iOS 는 ShopLivePlayerPreview, Android 는 cloud.shoplive.sdk.ShopLivePreview 입니다.
Flutter 에는 이렇게 끼워 넣는 뷰가 없습니다.
iOS 뷰를 만들어 addSubview 하고 제약을 건 뒤,
.initialize → .start(accessKey:campaignKey:referrer:) 순서로 부릅니다.
이벤트는 resultHandler 클로저로 받습니다. 받을 수 있는 케이스 전체는
미리보기 컴포넌트 에 있습니다. 셀을 재사용할 때는 .stop 또는 .close 로 정리하세요.
Android 레이아웃 XML 에 뷰를 두고 코드에서 시작·해제합니다.
<cloud.shoplive.sdk.ShopLivePreview
android:id="@+id/preview"
android:layout_width="120dp"
android:layout_height="180dp" />
import ShopLiveSDK
import ShopliveSDKCommon
final class PreviewCell: UITableViewCell {
private var preview: ShopLivePlayerPreview?
func configure(campaignKey: String) {
let preview = ShopLivePlayerPreview()
preview.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(preview)
NSLayoutConstraint.activate([
preview.topAnchor.constraint(equalTo: contentView.topAnchor),
preview.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
preview.widthAnchor.constraint(equalToConstant: 120),
preview.heightAnchor.constraint(equalToConstant: 180)
])
self.preview = preview
preview.action(.initialize)
preview.resultHandler = { result in
switch result {
case .didChangeCampaignStatus(let status):
print(status)
case .onError(let code, let message):
print(code, message)
case .handleReceivedCommand(let command, let payload):
print(command, payload ?? [:])
default:
break // 그 밖의 케이스는 미리보기 컴포넌트 레퍼런스 참고
}
}
// 전역 accessKey 를 이 값으로 바꾼다
preview.action(.start(accessKey: "{ACCESS_KEY}", campaignKey: campaignKey, referrer: nil))
}
override func prepareForReuse() {
super.prepareForReuse()
preview?.action(.close) // 셀 재사용 시 정리 — 부모 뷰에서 제거한다
preview = nil
}
}
// owner 는 어댑터 생성자로 받아 둔 LifecycleOwner(호스트 Activity/Fragment)입니다
class PreviewAdapter(
private val owner: LifecycleOwner,
private val campaignKeys: List<String>,
) : RecyclerView.Adapter<PreviewAdapter.ViewHolder>() {
override fun getItemCount() = campaignKeys.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_preview, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.onBind(campaignKeys[position])
}
// RecyclerView 가 셀을 붙이고 뗄 때 재생을 시작·해제합니다
override fun onViewAttachedToWindow(holder: ViewHolder) {
super.onViewAttachedToWindow(holder)
holder.onViewAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: ViewHolder) {
holder.onViewDetachedFromWindow()
super.onViewDetachedFromWindow(holder)
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val preview: ShopLivePreview = view.findViewById(R.id.preview)
fun onBind(campaignKey: String) {
preview.initializePlayer()
preview.setLifecycleObserver(owner)
preview.campaignKey = campaignKey
preview.setMuted(true)
}
fun onViewAttachedToWindow() {
preview.start() // 미션 1에서 ShopLiveCommon.setAccessKey 로 이미 설정한 accessKey 를 씁니다
}
fun onViewDetachedFromWindow() {
preview.release()
}
}
}
ShopLivePreview 뷰의 start() 와 prepare() 는 예외를 던질 수 있습니다
accessKey 와 campaignKey 가 설정되지 않은 상태에서
무인자 start() 또는 prepare() 를 호출하면
ShopLiveCommonError 가 발생합니다.RecyclerView 안에서 쓸 때
ViewHolder 가 attach 될 때 preview.start() 를, detach 될 때는 반드시
preview.release() 를 호출하세요. 셀이 재활용되는데 이전 인스턴스를 release() 하지 않으면
화면 밖으로 스크롤된 미리보기가 계속 재생 상태로 남습니다.ShopLivePreview 클래스입니다.
팝업도 이 뷰를 인플레이트해 띄우는 wrapper 일 뿐입니다. 화면비를 수신하면(내부 콜백, 배너·팝업
구분 없이 동일하게 실행) 그 크기를 ShopLive.setPreviewOptions 로 저장해 둔 값의
width/height 에 그대로 되저장합니다. 저장된 값이 있을 때만 일어나는 한쪽 방향 부작용입니다.
팝업 자신도 재생 중 같은 경로를 타지만 그건 자기 값을 다시 쓰는 것이라 눈에 띄지 않고,
배너처럼 팝업 UI 없이 계속 떠 있는 인스턴스가 있으면 사용자가 인지하지 못한 채
저장값이 바뀌어 있을 수 있습니다.완료 확인
- 미리보기 창이 음소거 상태로 뜨고 방송 영상이 재생된다
- 창을 탭하면 전체화면 플레이어로 전환된다
- 닫기 버튼 또는 화면 밖으로 밀기로 미리보기가 종료된다
- (Android) 플레이어 실행 중에 미리보기를 요청하면 중복으로 뜨지 않는다
- 배너형 미리보기 뷰(iOS
ShopLivePlayerPreview· AndroidShopLivePreview)가 목록 안에서도 스크롤에 따라 정상적으로 시작·해제된다
이전 ← 미션 8다음 → 미션 10 · 구매 전환 추적
미션 10 · 구매 전환 추적
Shoplive 는 사용자가 라이브(또는 다시보기)·숏폼 영상을 3초 이상 시청하고, 그 영상에 등록된 상품을 시청일로부터 28일 이내에 구매했을 때 이를 구매 전환으로 기록합니다. 전환은 마지막으로 시청한 영상을 기준으로 잡히고 라이브(다시보기)와 숏폼을 각각 따로 세므로, 한 번의 구매가 양쪽에 각각 전환으로 남을 수 있습니다.
구매 완료 시점에 전환 이벤트 보내기
구매가 확정되는 화면(주문 완료 페이지 등)에서 sendConversionEvent 를 한 번 호출합니다.
전달한 상품 식별값이 시청 기록과 매핑되면서 시청과 구매의 연관이 기록됩니다.
import ShopliveSDKCommon
let products = [
ShopLiveConversionProductData(
productId: "productId",
customerProductId: "customerProductId",
sku: "sku",
url: "url",
purchaseQuantity: 1,
purchaseUnitPrice: 1000.0)
]
ShopLiveEvent.sendConversionEvent(data: .init(
type: "purchase",
products: products,
orderId: "orderId",
referrer: "referrer",
custom: ["campaignKey": "campaignKey"]))
ShopLiveEvent.sendConversionEvent(
ShopLiveConversionData(
"purchase",
products = listOf(
ShopLiveConversionProductData(
productId = "productId",
customerProductId = "customerProductId",
sku = "sku",
url = "url",
purchaseQuantity = 1,
purchaseUnitPrice = 1000.0,
)
),
orderId = "orderId",
referrer = "referrer",
custom = mapOf("campaignKey" to "campaignKey"),
)
)
| ShopLiveConversionData | 타입 | 설명 |
|---|---|---|
type | String? | 구매 전환은 "purchase" · Android 는 생략 불가 |
products | List<ShopLiveConversionProductData>? | 구매한 상품 목록 · Android 는 생략 불가 |
orderId | String? | 주문번호 |
referrer | String? | 유입 경로 |
custom | Map<String, Any?>? | 함께 남길 값 (예: campaignKey) |
| ShopLiveConversionProductData | 타입 | 설명 |
|---|---|---|
productId | String? | Shoplive 고유 상품 ID |
customerProductId | String? | 고객사 고유 상품 ID |
sku | String? | 상품 SKU |
url | String? | 상품 URL |
purchaseQuantity | Int? | 구매 수량 |
purchaseUnitPrice | Double? | 구매 단가 |
type 과 products 는 생략할 수 없습니다
이 두 인자에만 기본값이 없어, 넘길 값이 없더라도 null 을 명시해야 컴파일됩니다.
나머지(orderId·referrer·custom)와 ShopLiveConversionProductData 의
모든 인자는 기본값이 있어 생략할 수 있습니다. iOS 는 전부 기본값이 있어 필요한 것만 넘기면 됩니다.orderId 는 숫자가 아니라 문자열입니다
원문 표에는 Int? 로 적혀 있지만 실제 정의는 iOS·Android 모두 String? 입니다.
주문번호가 숫자로만 이루어져 있어도 문자열로 넣으세요.purchaseUnitPrice 에 정수 리터럴을 쓰면 컴파일되지 않습니다
타입이 Double? 인데 Kotlin 은 1000 을 Double 로 자동 변환하지 않습니다.
1000.0 으로 쓰세요.shoplive_player 1.9.0 에는 sendConversionEvent · setAnonId 가 없습니다.
예전 문서의 Dart 예제(ShopLiveEvent.sendConversionEvent)는 컴파일되지 않습니다.
필요하면 iOS · Android 네이티브 코드에서 위 예제를 직접 호출하세요(플랫폼 채널).onEvent) 안에서
sendConversionEvent 를 호출합니다. 실제 서비스에서는 구매가 확정된 뒤 한 번만 호출하세요.
클릭할 때마다 보내면 사지 않은 상품까지 전환으로 기록됩니다.시청자와 구매자를 같은 사람으로 묶기
전환은 "이 사람이 보고, 이 사람이 샀다"가 이어져야 기록됩니다. 전환 이벤트는 호출하는 순간의 사용자 식별값을 읽어 함께 보내므로, 시청할 때 쓴 것과 같은 값이 구매 시점에도 설정돼 있어야 합니다.
| 상황 | 설정할 것 |
|---|---|
| 로그인 사용자 | 미션 3과 같은 방식으로 setUser 나 setAuthToken 을 씁니다 |
| 비로그인이지만 앱이 자체 기기 식별값을 관리함 | setAnonId 로 그 값을 넘김 |
| 비로그인이고 자체 식별값도 없음 | 따로 설정하지 않아도 됩니다. SDK 가 저장해 둔 식별값으로 연결합니다 |
// 로그인 사용자: 시청할 때와 같은 방식
ShopLiveCommon.setUser(user: ShopLiveCommonUser(userId: "{USER_ID}"))
// 또는 백엔드가 서명한 JWT
ShopLiveCommon.setAuthToken(authToken: "{USER_JWT}")
// 비로그인: 앱이 관리하는 기기 식별값이 있을 때만
ShopLiveCommon.setAnonId(anonId: "{DEVICE_ID}")
// 로그인 사용자: 시청할 때와 같은 방식
ShopLive.setUser(ShopLiveCommonUser(userId = "{USER_ID}"))
// 또는 백엔드가 서명한 JWT
ShopLiveCommon.setAuthToken("{USER_JWT}")
// 비로그인: 앱이 관리하는 기기 식별값이 있을 때만
ShopLiveCommon.setAnonId("{DEVICE_ID}")
setAnonId 가 없습니다
Flutter shoplive_player 1.9.0 에는 sendConversionEvent · setAnonId 가 없습니다.
예전 문서의 Dart 예제(ShopLiveEvent.sendConversionEvent)는 컴파일되지 않습니다.
필요하면 iOS · Android 네이티브 코드에서 위 예제를 직접 호출하세요(플랫폼 채널).완료 확인
- 구매 완료 화면에서
sendConversionEvent가 한 번만 호출된다 - 호출 시점에 시청할 때와 같은 사용자 식별값(
userId·JWT·anonId)이 설정돼 있다 orderId를 문자열로,purchaseUnitPrice를 실수로 넣어 빌드가 통과한다
이전 ← 미션 9다음 → 미션 11 · UI·동작 커스터마이징
미션 11 · UI·동작 커스터마이징
음소거 상태 유지
사용자가 음소거를 켠 채 플레이어를 나갔다가 다시 들어오면 기본값(소리 켜짐)으로 돌아옵니다. 이벤트 콜백으로 음소거 상태를 받아 두었다가 다음 재생 때 되돌려 주세요.
// ShopLiveSDKDelegate
private var isMuted = false
func onEvent(name: String, feature: ShopLiveLog.Feature, campaign: String, payload: [String: Any]) {
switch name {
case "video_muted":
isMuted = true
case "video_unmuted":
isMuted = false
default:
break
}
}
// ShopLiveHandler
private var isMuted = false
override fun onEvent(context: Context, data: ShopLiveLog.Data) {
when (data.name) {
"video_muted" -> isMuted = true
"video_unmuted" -> isMuted = false
}
}
final player = ShopLivePlayer();
bool isMuted = false;
player.log.listen((data) {
switch (data.name) {
case "video_muted":
isMuted = true;
break;
case "video_unmuted":
isMuted = false;
break;
}
});
저장해 둔 값을 다음 재생 전에 넘깁니다.
ShopLive.setMuteWhenPlayStart(isMuted)
ShopLive.play(data: .init(campaignKey: "{CAMPAIGN_KEY}"))
ShopLive.setMuteWhenPlayStart(isMuted)
ShopLive.play(this, ShopLivePlayerData("{CAMPAIGN_KEY}"))
final player = ShopLivePlayer();
player.setMuteWhenPlayStart(isMute: isMuted);
player.play(data: ShopLivePlayerData(campaignKey: "{CAMPAIGN_KEY}"));
onEvent 로 받으세요
예전 가이드가 안내하는 log 콜백(iOS log(name:feature:campaign:payload:) ·
Android log(data:))은 두 SDK 모두 deprecated 입니다. 새로 붙인다면 onEvent 를 쓰세요.
Flutter 만 예외입니다. Flutter SDK 에는 onEvent 스트림이 없어 log 스트림이 맞습니다.
링크 클릭 후 플레이어 동작
상품·공지·배너의 링크를 눌러 앱이 다른 화면으로 넘어갈 때, 뒤에 남는 플레이어를 어떻게 할지 고릅니다.
| 값 | 동작 |
|---|---|
PIP (기본값) | 작은 창으로 내려가 방송이 계속 재생됨 |
KEEP | 전체화면 그대로 유지 |
CLOSE | 플레이어 종료 |
// 위 표의 값 중 하나를 넣습니다 (.PIP · .KEEP · .CLOSE)
ShopLive.setNextActionOnHandleNavigation(actionType: .PIP)
// 위 표의 값 중 하나를 넣습니다 (PIP · KEEP · CLOSE)
ShopLive.setNextActionOnHandleNavigation(ActionType.PIP)
actionType: 을 빠뜨리지 마세요
원문 예제의 ShopLive.setNextActionOnHandleNavigation(.PIP) 는 컴파일되지 않습니다.
실제 시그니처는 setNextActionOnHandleNavigation(actionType: ActionType) 입니다.ShopLivePlayer 에 setNextActionOnHandleNavigation 이 있어 자동완성에도 뜨지만,
넘긴 값이 네이티브까지 전달되지 않습니다. Dart 는 'show'·'hide'·'keep'
문자열을 보내는데(ShopLiveActionType.parseValue() 의 반환 타입이 String 입니다)
네이티브 쪽은 이 값을 정수로 읽습니다. 그래서 실제로는
Android 는 무엇을 넘기든 항상 PIP 로 동작하고 iOS 는 아무 일도 일어나지 않습니다(기본값 PIP 유지).
Dart 열거형이 show·hide·keep 으로 위 표의
PIP·KEEP·CLOSE 와 이름부터 다른 것도 같은 문제의 표시입니다.
Flutter 에서 이 동작이 필요하면 Shoplive 담당자에게 문의하세요.hookNavigation 을 쓰면 이 설정은 무시됩니다
미션 6의 hookNavigation 을 등록하면 SDK 는 그 클로저만 호출하고
링크 이동 후 동작을 건너뜁니다. 두 방식을 동시에 기대하지 마세요.브랜드 좋아요(팔로우)
플레이어 안에 브랜드 이미지와 좋아요(팔로우) 버튼을 띄우고, 그 상태를 앱이 가진 사용자 데이터와 맞춥니다.
1) 처음 진입할 때 팔로우 상태 맞추기. 앱이 가진 팔로우 상태를
SET_BRAND_FAVORITE 커맨드로 보내면 플레이어의 버튼 표시가 그 값으로 갱신됩니다.
아래 2) 에서도 같은 커맨드를 씁니다.
ShopLive.sendCommandMessage(
command: "SET_BRAND_FAVORITE",
payload: ["identifier": "brand-identifier", "favorite": true])
ShopLive.sendCommandMessage(
"SET_BRAND_FAVORITE",
mapOf("identifier" to "brand-identifier", "favorite" to true)
)
custom 으로는 팔로우 상태가 바뀌지 않습니다
원문 Android 가이드는 custom 에 brand 키를 넣어 초기 상태를 넘기는 예제를 싣고 있지만,
현행 웹 플레이어는 custom 안의 키를 읽지 않습니다.
custom 은 사용자 정보에 실려 백엔드로 전달되는 값이지 플레이어 UI 를 바꾸지 않습니다
(미션 3에서 쓴 그 필드입니다). 초기 상태도 위 SET_BRAND_FAVORITE 로 보내세요.
덧붙여 그 원문 예제가 쓰는 ShopLiveUser 와 addCustomParameter 는 deprecated 입니다.
사용자 정보는 미션 3처럼 ShopLiveCommonUser 로 넘기세요.2) 버튼을 눌렀을 때 받아서 처리하기. 미션 6과 같은 커맨드 콜백으로 들어옵니다.
팔로우와 해제는 같은 이벤트입니다. 커맨드 이름이 따로 있지 않고
favorite boolean 하나가 실려 옵니다.
앱이 할 일은 자기 쪽 팔로우 처리를 한 뒤, 그 결과로 확정된 상태를
SET_BRAND_FAVORITE 로 보내는 것입니다.
ON_CLICK_BRAND_FAVORITE_BUTTON payload | 타입 | 언제 오는가 |
|---|---|---|
identifier | String | 항상 |
favorite | Bool | 항상 옵니다. 다만 클릭 직전 상태인지 클릭 후 상태인지가 캠페인 테마에 따라 다릅니다(아래 주의) |
brand | String (브랜드명) | 일부 캠페인 테마에서만. 없을 수 있으니 의존하지 마세요 |
name | String | brand 와 같은 조건 · 같은 값 |
func handleReceivedCommand(_ command: String, data: [String: Any]?) {
switch command {
case "ON_CLICK_BRAND_FAVORITE_BUTTON":
guard let identifier = data?["identifier"] as? String else {
return
}
// 앱·백엔드에서 팔로우를 처리하고, 확정된 최종 상태를 그대로 보냅니다
updateFavorite(identifier: identifier) { isFavorite in
ShopLive.sendCommandMessage(
command: "SET_BRAND_FAVORITE",
payload: ["identifier": identifier, "favorite": isFavorite])
}
case "ON_CHANGED_BRAND_FAVORITE":
// 팔로우 상태가 바뀐 결과를 알려줍니다
break
default:
break
}
}
override fun onReceivedCommand(context: Context, command: String, data: JSONObject) {
when (command) {
"ON_CLICK_BRAND_FAVORITE_BUTTON" -> {
val identifier = data.getString("identifier")
// 앱·백엔드에서 팔로우를 처리하고, 확정된 최종 상태를 그대로 보냅니다
updateFavorite(identifier) { isFavorite ->
ShopLive.sendCommandMessage(
"SET_BRAND_FAVORITE",
mapOf("identifier" to identifier, "favorite" to isFavorite)
)
}
}
"ON_CHANGED_BRAND_FAVORITE" -> {
// 팔로우 상태가 바뀐 결과를 알려줍니다
val identifier = data.getString("identifier")
}
}
}
favorite 으로 최종 상태를 계산하지 마세요
이 값이 클릭 직전 상태인지 클릭 후 원하는 상태인지는 캠페인 테마에 따라 다릅니다.
그래서 받은 값을 뒤집어 보내는 방식(!favorite)은 테마가 바뀌면 정반대로 동작합니다.
플레이어가 하트를 이미 갱신해 둔 테마에서는 그 갱신을 옛 상태로 덮어써 하트가 되돌아갑니다.
SET_BRAND_FAVORITE 은 "보여줄 상태" 를 절대값으로 지정하는 커맨드이므로,
앱은 자기 백엔드가 확정한 상태를 그대로 보내면 됩니다. 그러면 테마와 무관하게 맞습니다.
화면 표시도 앱이 가진 팔로우 상태를 기준으로 하세요.SET_BRAND_FAVORITE 을 보내지 않아도 화면이 맞습니다.
백엔드 처리가 실패해 되돌려야 하는 등 필요할 때만 보내되, 역시 절대값으로 보내세요.sendCommandMessage("SET_BRAND_FAVORITE", "{...}") 는 deprecated 로 표시돼 있습니다.
Map 을 받는 쪽을 쓰세요. deprecation 메시지가 가리키는
ShopLiveCommon.sendCommandMessage 는 실제로 존재하지 않습니다. ShopLive 쪽이 맞습니다.payload 에 nil 을 넘기면 아무것도 전송되지 않습니다
sendCommandMessage(command:payload:) 는 payload 가 nil 이면 그대로 반환합니다.
보낼 값이 없더라도 빈 딕셔너리를 넘기세요.앱 웹뷰에서 Shoplive 를 띄울 때
SDK 대신 앱이 직접 만든 웹뷰에 Shoplive 웹 플레이어를 띄우는 경우에 필요한 설정입니다.
iOS WKWebViewConfiguration 의 allowsInlineMediaPlayback 을
true 로 두어야 영상이 전체화면으로 튀지 않고 페이지 안에서 재생됩니다.
Shoplive 는 WKWebView 기준으로 서비스하며, Apple 이 지원을 끝낸 UIWebView 는 지원하지 않습니다.
Android shouldOverrideUrlLoading 안에서 webView.loadUrl(url) 을 호출하면 안 됩니다.
별도 스킴을 처리해야 할 때만 아래처럼 분기하세요.
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
// 별도 스킴이 있을 때만 여기서 처리합니다
val url = request?.url ?: return super.shouldOverrideUrlLoading(view, request)
if (url.toString().startsWith("market")) {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = url
}
startActivity(intent)
return true
}
return super.shouldOverrideUrlLoading(view, request)
}
}
상품을 눌렀을 때 새 창이 열리는 동작은 WebChromeClient.onCreateWindow 에서 받습니다.
아래는 지금 보고 있는 웹뷰에서 이어 여는 예시입니다.
webView.webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView?,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message?
): Boolean {
view ?: return true
resultMsg ?: return true
val subWebView = WebView(view.context)
subWebView.webViewClient = object : WebViewClient() {
// API 23 이하 단말을 지원한다면 String 오버로드도 함께 구현해야 합니다
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
url ?: return false
if (url.startsWith("http")) {
webView.loadUrl(url)
}
return true
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val url = request?.url?.toString() ?: return false
if (url.startsWith("http")) {
webView.loadUrl(url)
}
return true
}
}
val viewTransport = (resultMsg.obj as? WebView.WebViewTransport) ?: return true
viewTransport.webView = subWebView
resultMsg.sendToTarget()
return true
}
}
바깥 브라우저나 새 화면으로 열고 싶으면 위 두 shouldOverrideUrlLoading 안의
webView.loadUrl(url) 자리를 아래로 바꿉니다.
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(url)
}
startActivity(intent)
로딩 인디케이터 바꾸기
방송에 들어가 영상이 뜨기 전까지 보이는 인디케이터입니다. 기본은 흰색입니다.
색만 바꾸기
ShopLive.indicatorColor = UIColor.purple
ShopLive.setLoadingProgressColor(Color.RED)
이미지 애니메이션으로 바꾸기
var images: [UIImage] = []
for i in 1...11 {
images.append(.init(named: "loading\(i)")!)
}
ShopLive.setLoadingAnimation(images: images)
ShopLive.setLoadingAnimation(R.drawable.progress_animation)
Android 는 프레임을 animation-list 드로어블로 먼저 정의합니다.
res/drawable/progress_animation.xml:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/loading1" android:duration="100" />
<item android:drawable="@drawable/loading2" android:duration="100" />
<item android:drawable="@drawable/loading3" android:duration="100" />
<item android:drawable="@drawable/loading4" android:duration="100" />
</animation-list>
통화·이어폰 인터럽트 후 동작
재생 중 전화가 오거나 이어폰이 빠지면 영상이 멈춥니다. 인터럽트가 끝난 뒤 어떻게 할지 고릅니다.
// 이어폰이 빠져도 계속 재생 (기본값 false, 즉 정지)
ShopLive.setKeepPlayVideoOnHeadphoneUnplugged(true)
// 계속 재생하되 음소거로
ShopLive.setKeepPlayVideoOnHeadphoneUnplugged(true, isMute: true)
print(ShopLive.isKeepPlayVideoOnHeadPhoneUnplugged())
// 통화가 끝나면 자동으로 다시 재생 (iOS 기본값 false, 즉 멈춘 채 유지)
ShopLive.setAutoResumeVideoOnCallEnded(true)
print(ShopLive.isAutoResumeVideoOnCallEnded())
// 이어폰이 빠져도 계속 재생 (기본값 false, 즉 정지)
ShopLive.setKeepPlayVideoOnHeadphoneUnplugged(true, false)
// 계속 재생하되 음소거로
ShopLive.setKeepPlayVideoOnHeadphoneUnplugged(true, true)
// 통화가 끝나면 자동으로 다시 재생 (Android 기본값 true)
ShopLive.setAutoResumeVideoOnCallEnded(false)
false(멈춘 채 유지), Android 는 true(자동 재생)입니다.
두 플랫폼을 같게 맞추려면 양쪽 모두에서 명시적으로 설정하세요.setKeepPlayVideoOnHeadphoneUnplugged 는 인자 두 개가 모두 필요합니다
원문 예제의 1인자 호출(setKeepPlayVideoOnHeadphoneUnplugged(true))은 컴파일되지 않습니다.
두 번째 인자는 "계속 재생하되 음소거할지" 입니다. iOS 는 두 번째 인자에 기본값이 있어 생략할 수 있습니다.android.permission.READ_PHONE_STATE 권한이 필요합니다
권한이 없으면 통화로 인한 인터럽트를 감지하지 못해 자동 재생 설정도 동작하지 않습니다.ACTION_AUDIO_BECOMING_NOISY 로 받아 정지 · 무음을 처리합니다.
이 방송은 권한 없이 옵니다. 그래서 BLUETOOTH_CONNECT 런타임 권한은 요청하지 않아도 됩니다.
Android 12 이상 실기기에서 블루투스 끊김 무음이 그대로 동작하는지는 요확인입니다.완료 확인
- 음소거를 켜고 나갔다 다시 들어와도 음소거 상태가 유지된다
- (iOS·Android) 상품 링크를 눌렀을 때 설정한 대로 PIP·유지·종료 중 하나로 동작한다
- (브랜드 좋아요 사용 시) 버튼을 누르면
ON_CLICK_BRAND_FAVORITE_BUTTON이 들어오고, 되돌려준 상태가 버튼에 반영된다 - 로딩 인디케이터가 지정한 색 또는 이미지 애니메이션으로 보인다 (둘을 동시에 설정하지 않았다)
- 이어폰을 뽑거나 전화를 받았다 끊었을 때 설정한 대로 재생·정지된다
이전 ← 미션 10
데이터 필드 사전
재생·미리보기 API 를 오갈 때 실제로 주고받는 데이터 타입의 필드를 전수 정리합니다. 같은 개념이라도 플랫폼마다 필드 구성이 다른 지점이 있어 표에 그대로 남겼습니다. 빈칸은 "아직 안 적었다"가 아니라 그 필드가 그 플랫폼엔 없다는 뜻입니다.
PlayerData: 재생을 시작할 때 넘기는 데이터
iOS ShopLivePlayerData
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
campaignKey | String | 아니오 | 방송 식별 키. 생성자 필수 인자 |
keepWindowStateOnPlayExecuted | Bool | 아니오(기본 true) | play 실행 시 기존 창 상태 유지 여부 |
referrer | String? | 예 | 유입 경로 |
campaignHandler | ((ShopLivePlayerCampaign) -> ())? | 예 | 캠페인 정보 콜백(play/preview 공용) |
brandHandler | ((ShopLivePlayerBrand) -> ())? | 예 | 브랜드 정보 콜백(play/preview 공용) |
isEnabledVolumeKey | Bool | 아니오(기본 false) | 볼륨 키 사용 허용 여부 |
Android ShopLivePlayerData
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
campaignKey | String | 아니오 | 생성자 필수 인자 |
keepWindowStateOnPlayExecuted | Boolean | 아니오(기본 true) | iOS 와 동일 개념, 별도 setter 있음 |
referrer | String? | 예 | 별도 setter 있음 |
resizeMode | ShopLiveResizeMode? | 예 | iOS PlayerData 에는 없는 필드. 값: AUTO / CENTER_CROP / FIT |
Flutter ShopLivePlayerData
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
campaignKey | String | 아니오 | 생성자 필수 인자 |
keepWindowStateOnPlayExecuted | bool | 아니오(기본 false) | play 실행 시 기존 창 상태 유지 여부 |
referrer | String? | 예 | 유입 경로 |
keepWindowStateOnPlayExecuted 기본값이 네이티브와 반대입니다
Flutter 는 기본값이 false 라 값을 주지 않으면 네이티브 기본(true)과 반대로 매번 전체화면으로 엽니다.
같게 하려면 true 를 명시하세요.campaignHandler·brandHandler 클로저가 있어 메인(풀스크린)
플레이어에서도 타입 있는 Campaign+Brand 콜백을 받습니다. Android 는 다릅니다. 타입 있는 Brand 콜백은
Preview 전용(ShopLivePreviewData 의 onCampaignListener)이고, 메인 플레이어의 캠페인 정보는
PlayerData 가 아니라 전역 ShopLiveHandler.onCampaignInfo(JSONObject)(원시 JSON)로 오며,
브랜드 정보는 메인 플레이어 경로에 대응물이 전혀 없습니다. "브랜드 정보를 메인 플레이어에서도
구조화된 콜백으로 받을 수 있다"고 안내하면 Android 기준으로는 틀린 서술이 됩니다.Campaign (ShopLivePlayerCampaign)
iOS
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
title | String? | 예 | 캠페인 제목 |
campaignStatus | ShopLivePlayerCampaignStatus | 아니오(기본 READY) | 3가지 값만 옵니다(READY · ONAIR · CLOSED). 매칭되지 않는 문자열은 전부 READY 로 떨어집니다 |
Android
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
title | String? | 예 | 동일 개념 |
campaignStatus | ShopLivePlayerCampaignStatus? | 예 | 4가지 값: READY / ONAIR / CLOSING / CLOSED |
CLOSING 이 하나 더 있습니다. iOS 는 매칭되지 않는 문자열을 전부
READY 로 떨어뜨리므로, 서버가 "CLOSING"을 내려주면 iOS 클라이언트는 이를
READY(방송 대기중)로 오인하는 구조입니다(코드상 그렇게 동작함이 확인됨). 실제로 서버가 iOS
쪽에도 CLOSING 을 내려주는지는 서버 계약 문제라 요확인입니다.Brand (ShopLivePlayerBrand)
재생 중 콜백으로 오는 브랜드 정보입니다. iOS·Android 필드 구성은 완전히 같습니다. 위 PlayerData 항목에서 짚었듯 Android 는 이 타입을 메인 플레이어 콜백으로 받을 수 없고 Preview 전용입니다.
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
name | String? | 예 | 브랜드명 |
identifier | String? | 예 | 브랜드 식별자 |
imageUrl | String? | 예 | 브랜드 이미지 URL |
BrandData/SLBrandModel, 필드 구성이 다름)이 있습니다.
이 표는 Live 재생용 ShopLivePlayerBrand 한정이며 숏폼 Brand 는 범위 밖입니다.CommonUser (ShopLiveCommonUser)
iOS·Android 모두 같은 이름(ShopLiveCommonUser)을 씁니다. 필드 6개가 1:1 대응하고 값도 동일하지만,
enum 타입명 표기와 케이스 철자가 플랫폼마다 다릅니다. 코드 예제를 쓸 때 아래 표기를 그대로 옮기세요.
자세한 연동 방법은 미션 3 참고.
| 필드 | 타입 | nullable | 의미 |
|---|---|---|---|
userId | String | 아니오 | 필수 |
userName | String? | 예 | |
age | Int? | 예 | |
gender | iOS ShopliveCommonUserGender? / Android ShopLiveCommonUserGender? / Flutter ShopLiveCommonUserGender?(MALE · FEMALE · NEUTRAL) | 예 | 값은 동일(m/f/n). 아래 콜아웃 참고 |
userScore | Int? / Flutter int? | 예 | -100~100 은 서버 규칙(SDK 는 검사하지 않음) |
custom | iOS [String: Any]? / Android Map<String, Any?>?(기본값 빈 맵) / Flutter Map<String, dynamic>? | 예 | 1KB 이하는 서버 규칙(SDK 는 검사하지 않음) |
gender 를 비우면 Android 는 NEUTRAL 로 보냅니다. iOS 는 보내지 않습니다.ShopliveCommonUserGender(소문자 live)이고 케이스가 .male("m") ·
.female("f") · .netural("n") 입니다. netural 은
neutral 의 오타지만 실제 소스 코드의 케이스명이 이렇습니다. "고쳐서" 예제를 쓰면 컴파일되지 않습니다.
Android 는 ShopLiveCommonUserGender(대문자 Live)이고 .MALE("m") ·
.FEMALE("f") · .NEUTRAL("n") 로 철자가 정상입니다.구매전환 데이터의 custom 필드
미션 10 에서 다루는 ShopLiveEvent.sendConversionEvent 가 서버로 보내는 요청
바디에는 custom 이라는 필드가 하나 더 있습니다. 앱 코드가 넘기는 ShopLiveConversionData.custom
과 이름이 같아 혼동하기 쉬운데, 이 자리에 실제로 무엇이 담기는지가 플랫폼마다 다릅니다. SDK 소스로 직접
확인한 사실입니다.
| 플랫폼 | 서버 요청의 custom 필드에 실제로 담기는 것 | file:line |
|---|---|---|
| iOS | 호출부에서 넘긴 ShopLiveConversionData.custom 만 담깁니다 (data.custom?.toJSONString_SL()) | ShopLiveEvent.swift:24 |
| Android | ShopLiveConversionData 객체 전체(type·products·orderId·referrer·custom 전부)가 JSON 문자열로 담깁니다 (Gson().toJson(data)) | ShopLiveEvent.kt:22 |
custom 맵만 서버 custom 필드에 담는데, Android 는 호출에 쓴
ShopLiveConversionData 전체(상품 목록·주문번호 등 포함)를 JSON 문자열로 통째로 담습니다.
서버 로그·데이터 인사이트에서 이 필드를 분석할 때 플랫폼별로 파싱 방식을 다르게 짜야 합니다.이벤트 핸들러
플레이어 안에서 일어난 일은 전부 이벤트로 앱에 전달됩니다. 받는 방법은 플랫폼마다 다릅니다.
iOS 는 ShopLiveSDKDelegate 를 구현하고, Android 는 ShopLiveHandler 를 상속하며,
Flutter 는 Stream 을 구독합니다.
이벤트마다 언제 불리는지, 무엇이 실려 오는지, 앱이 뭘 해야 하는지를 먼저 적고 그 아래 플랫폼별 시그니처를 붙였습니다. 등록 방법과 실제 코드는 각 미션에서 다룹니다.
어느 한쪽에만 있는 이벤트는 그렇다고 적어 두었습니다. 빠뜨린 것이 아니라 확인해 봤더니 정말 없다는 뜻입니다.
handleNavigation 하나뿐입니다
나머지는 전부 선택입니다. iOS 는 handleNavigation 만 필수이고 나머지는
@objc optional, Android 도 handleNavigation 만 abstract 이고
나머지는 open 입니다.
예전 iOS 문서는 여러 메서드를 필수처럼 적어 두었지만, SDK 원본과 6건 이상 어긋납니다. 이 문서는 SDK 원본을 기준으로 삼았습니다.
상품·배너 링크 클릭
| 필드 | 타입 | 설명 |
|---|---|---|
url | iOS URL · Android·Flutter String | 그 상품·배너·쿠폰에 등록해 둔 상세 페이지 주소 |
상품인지 배너인지 쿠폰인지는 URL 로 구분해야 합니다. 링크 형식을 미리 정해 두면 분기하기 편합니다.
abstract). 예제는 미션 6.handleNavigation(with url: URL)
handleNavigation(context, url: String)
handleNavigation → {url}
쿠폰 발급 요청
| 필드 | 타입 | 설명 |
|---|---|---|
couponId | String | 어드민에서 지정한 쿠폰 식별자 |
| 결과 콜백 | iOS ShopLiveCouponResultAndroid ShopLiveHandlerCallback | 처리 결과를 되돌려 줄 객체. 넘길 값은 결과 회신 에 정리했습니다 |
iOS 인자 구성이 다른 구버전 오버로드 2개가 deprecated 입니다. 아래 시그니처가 현행입니다.
handleDownloadCoupon(with couponId: String, result: (ShopLiveCouponResult) -> Void)
handleDownloadCoupon(context, couponId: String, callback: ShopLiveHandlerCallback)
handleDownloadCoupon → {couponId}
커스텀 액션 팝업 클릭
| 필드 | 타입 | 설명 |
|---|---|---|
id | String | 팝업 식별자. 결과를 회신할 때 그대로 돌려줍니다 |
type | String | 팝업 종류 문자열입니다. SDK 는 플레이어 웹이 보낸 값을 그대로 전달합니다. 이전 문서 기준 COUPON · BANNER · NOTICE (요확인 — 웹 플레이어 계약). 보통 이 값으로 분기합니다 |
payload | iOS Any? · Android JSONObjectFlutter String | 어드민에 넣어 둔 값 |
| 결과 콜백 | iOS ShopLiveCustomActionResultAndroid ShopLiveHandlerCallback | 처리 결과를 되돌려 줄 객체. 결과 회신 참고 |
payload 는 어드민 입력값을 그대로 내려주므로 고정 스키마가 아닙니다. 한 번 찍어 보고 파싱하세요.
type 으로 분기해 처리한 뒤 결과를 회신합니다. 쿠폰과 회신 방식이 같습니다. 예제는 미션 5의 User define.iOS 구버전 오버로드 2개, Android payload: String 버전이 deprecated 입니다.
handleCustomAction(with id: String, type: String, payload: Any?, result: (ShopLiveCustomActionResult) -> Void)
handleCustomAction(context, id: String, type: String, payload: JSONObject, callback: ShopLiveHandlerCallback)
handleCustomAction → {id, type, payload: String}
방송 상태 변경
| 필드 | 타입 | 설명 |
|---|---|---|
campaignStatus | String | READY(방송 전) · ONAIR(방송 중) · CLOSED(종료) |
자세한 값 설명은 데이터 필드 사전 의 Campaign 항목에 있습니다.
handleChangeCampaignStatus(status: String)
onChangeCampaignStatus(context, campaignStatus: String)
changeCampaignStatus → {campaignStatus}
방송 정보
| 필드 | 타입 | 설명 |
|---|---|---|
campaignInfo | iOS [String: Any] · Android JSONObjectFlutter Map | 서버가 내려준 캠페인 메타데이터 묶음 |
SDK 가 정한 고정 필드가 아니라 서버 응답을 그대로 전달하는 통로라, 캠페인 설정에 따라 구성이 달라집니다.
handleCampaignInfo(campaignInfo: [String: Any])
onCampaignInfo(campaignInfo: JSONObject)
campaignInfo → {campaignInfo: Map}
플레이어 화면 생성·소멸
| 필드 | 타입 | 설명 |
|---|---|---|
status (iOS·Flutter)playerLifecycle (Android) | iOS·Flutter StringAndroid PlayerLifecycle | 아래 값 표 참고 |
isPipMode | Boolean (Android 전용) | 지금 PIP 면 true, 전체화면이면 false |
| 값 | 의미 | iOS | Android |
|---|---|---|---|
CREATED | 플레이어 화면이 만들어졌다 | 옴 | 옴 |
CLOSING | 닫히는 중 | 안 옴 | 옴 |
DESTROYED | 완전히 닫혔다 | 옴 | 옴 |
ShopLiveChangedPlayerStatus.fromJson 은 문자열이 'CREATED' 가 아니면
전부 destroyed 로 바꿔 버립니다. 그래서 Android 가 보낸 CLOSING 이
Flutter 에서는 "이미 닫혔다" 로 읽힙니다. 닫히는 중과 닫힌 뒤를 나눠 처리해야 한다면 이 값에 기대지 마세요.
iOS 는 애초에 CLOSING 을 보내지 않습니다. SDK 소스에서 이 콜백을 부르는 곳이
"CREATED" 와 "DESTROYED" 두 군데뿐인 것을 직접 확인했습니다.
Android 만 세 단계이고 iOS·Flutter 는 두 단계라고 보면 됩니다.
handleChangedPlayerStatus(status: String)
onChangedPlayerStatus(isPipMode: Boolean, playerLifecycle: PlayerLifecycle)
changedPlayerStatus
시청자 대화명 변경
| 필드 | 타입 | 설명 |
|---|---|---|
userId | String | 시청자 식별자. 우리가 넘긴 값 그대로입니다 |
userName | String | 시청자가 새로 지정한 대화명 |
iOS·Android 모두 두 값이 묶음(Map · JSONObject) 하나에 담겨 옵니다.
onSetUserName(_ payload: [String: Any])
onSetUserName(jsonObject: JSONObject)
userInfo → {userInfo: Map}
오류
| 필드 | 타입 | 설명 |
|---|---|---|
code | String | 에러 코드. 목록은 에러 코드 에 있습니다 |
message | String | 에러 메시지. 시청자에게 그대로 보여주기보다 로그용으로 쓰세요 |
Android 현행은 두 값이 ShopLiveCommonError 객체에 담겨 옵니다. error.code · error.message 로 꺼내 쓰세요.
-412(세션 만료)는 토큰을 새로 발급해 다시 재생하면 화면을 유지한 채 복구됩니다. 같은 번호라도 플랫폼마다 의미가 다를 수 있으니 표를 꼭 확인하세요. 9100번대는 상태 안내라 플레이어를 닫지 마세요.Android onError(context, code, message) 는 deprecated 입니다. 아래 ShopLiveCommonError 를 받는 쪽을 쓰세요.
handleError(code: String, message: String)
onError(context, error: ShopLiveCommonError)
error → {code, message}
플레이어 안 버튼 클릭 (커맨드)
| 필드 | 타입 | 설명 |
|---|---|---|
command | String | 무엇을 눌렀는지. 예: CLICK_PRODUCT_DETAIL · CLICK_PRODUCT_CART |
data | iOS [String: Any]? · Android JSONObjectFlutter Map | 누른 대상의 정보 |
data 는 캠페인에 등록한 상품 데이터를 그대로 내려주므로 서비스마다 필드가 다릅니다. 고정 스키마를 가정하지 마세요.
iOS handleReceivedCommand(_:with:) 는 deprecated 입니다. 둘 다 @objc optional 이라 잘못된 쪽을 구현해도 빌드는 통과하니, 값이 안 들어오면 시그니처부터 확인하세요.
handleReceivedCommand(_ command: String, data: [String: Any]?)
onReceivedCommand(context, command: String, data: JSONObject)
receivedCommand → {command, data: Map}
iOS 전용 화면 전환 추적
| 필드 | 타입 | 설명 |
|---|---|---|
command | String | 전환 이름. 아래 현행 6가지 · 레거시 4가지 중 하나 |
payload | Any? | lastStyle · currentStyle · isPreview. viewDidDisAppear 에만 viewHiddenActionType(String, 값은 화면 닫힘 의 사유)이 더 붙습니다. 레거시 willShopLiveOff · didShopLiveOff 의 payload 는 style(Int rawValue)입니다 |
현행 6가지(ShopLiveViewTrackEvent): viewWillDisAppear · viewDidDisAppear · pipWillAppear · pipDidAppear · fullScreenWillAppear · fullScreenDidAppear.
레거시 4가지: willShopLiveOn · didShopLiveOn · willShopLiveOff · didShopLiveOff. ShopLiveViewTrackEvent 에서 빠진 구 이름이지만 1.9.0 에서도 옵니다. 새 코드는 현행 6가지를 쓰세요.
Android Flutter 대응 이벤트가 없습니다. Android 는 onChangedPlayerStatus 의 isPipMode 로 비슷한 것을 알 수 있지만 전환 직전·직후 구분은 없습니다.
handleCommand(_ command: String, with payload: Any?)
iOS 전용 PIP 창 드래그
| 필드 | 타입 | 설명 |
|---|---|---|
state | UIGestureRecognizer.State | began · changed · ended 등 제스처 진행 상태 |
position | CGPoint | PIP 창 중심의 화면 좌표 |
Android Flutter 대응 이벤트가 없습니다. 화면 좌표를 다루는 iOS 고유 이벤트입니다.
playerPanGesture(state: UIGestureRecognizer.State, position: CGPoint)
iOS 전용 미리보기 창 터치
preview(data:completion:) completion 과 역할이 겹칩니다. 전체화면 전환은 보통 completion 쪽에서 처리하니, 이 이벤트는 별도 로깅 같은 용도로만 쓰세요.예전 문서에는 없던 이벤트입니다. Android Flutter 에는 대응이 없습니다.
handlePreviewTouchGesture()
동작 로그
| 필드 | 타입 | 설명 |
|---|---|---|
name | String | 무슨 일이 일어났는지 |
feature | String | click · show · action 셋 중 하나 |
campaignKey | String? · Flutter String(null 아님, 없으면 빈 문자열) | 어느 방송에서 일어난 일인지 |
payload | Map · Flutter Map<String, dynamic>(null 아님, 없으면 빈 맵) | 부가 정보. name 마다 구성이 다릅니다 |
name 만 걸러 쓰세요. 예를 들어 데모 앱은 player_active_seconds 를 건너뜁니다.log 가 아니라 onEvent 를 쓰세요.
iOS·Android 모두 예전의 log 계열(iOS 2종 · Android 1종)은 deprecated 입니다.
Flutter 만 예외로 스트림 이름이 log 하나뿐입니다.
Flutter log 스트림은 네이티브의 구버전 경로(iOS log(name:feature:campaign:payload:), Android onReceivedCommand 의 EVENT_LOG)를 받습니다.
현행 onEvent 는 연결돼 있지 않습니다.
onEvent(name: String, feature: ShopLiveLog.Feature, campaign: String, payload: [String: Any])
onEvent(context, data: ShopLiveLog.Data)
log → {name, feature, campaignKey, payload: Map}
공유 버튼 클릭
| 필드 | 타입 | 설명 |
|---|---|---|
url | String? | 공유할 링크. 형식은 setShareScheme 로 미리 정해 둡니다 |
campaign | ShopLivePlayerShareCampaign? | campaignKey · title · description · thumbnail |
받는 위치가 플랫폼마다 다릅니다.
iOS 는 ShopLiveSDKDelegate 가 아니라
ShopLivePlayerShareDelegate 라는 별도 프로토콜입니다.
Android 는 ShopLiveHandler 안에 다른 이벤트와 같이 있습니다.
Flutter 는 대응 스트림이 없어 기본 공유 시트만 쓸 수 있습니다.
ShopLivePlayerShareDelegate.handleShare(data: ShopLivePlayerShareData)
handleShare(context, data: ShopLivePlayerShareData)
Android 전용 시청자가 플레이어를 닫음
| 필드 | 타입 | 설명 |
|---|---|---|
isPipMode | Boolean | 닫을 때 PIP 상태였으면 true, 전체화면이었으면 false |
onClose)은 PIP 창 닫힘만 다루고 전체화면 종료는 제외합니다.예전 문서에는 없던 이벤트입니다. iOS 는 setCloseHandler 가 전체화면 종료까지 포괄하므로 이 콜백이 따로 없습니다. Flutter 는 대응이 없습니다.
onUserCloseAction(isPipMode: Boolean)
화면 닫힘 (사유 포함)
| 필드 | 타입 | 설명 |
|---|---|---|
type | ShopLivePresentType | 닫힌 화면이 앱 내 PIP 인지 OS PIP 인지. iOS 클로저는 reason 하나만 받고, 화면 종류는 reason.presentType 으로 꺼냅니다 |
reason | ShopLiveViewHiddenActionType | 닫힌 사유. 값은 아래 표에 있습니다 |
iOS 이 이벤트만 ShopLiveSDKDelegate 밖에 있습니다.
ShopLive.setCloseHandler 라는 별도 API 로 받으며, handleShare 와 같은 방식입니다.
미리보기가 닫힐 때만 받으려면 ShopLive.setPreviewCloseHandler(_:) 를 씁니다. 미리보기가 닫히면 setCloseHandler 와 함께 불립니다.
같은 이벤트처럼 보이지만 포착하는 화면이 서로 다릅니다. 전체화면 종료를 잡으려다 놓치기 쉬운 지점입니다.
| 플랫폼 | 알려주는 닫힘 | 이걸로는 못 잡는 것 |
|---|---|---|
iOS setCloseHandler | 전체화면 종료를 포함한 모든 화면 닫힘 | 없음 |
Android onClose | 앱 내 PIP·OS PIP 창이 닫힐 때 | 전체화면 플레이어 종료. 그쪽은 onUserCloseAction 을 쓰세요 |
Flutter previewClose | 미리보기 창이 닫힐 때 | 그 밖의 모든 닫힘 |
사유 값은 이름 기준 8가지가 두 플랫폼에 공통이고, 표기만 다릅니다
(iOS onBtnTapped ↔ Android ON_BTN_TAPPED). Android 에만 1가지(ON_UNKNOWN)가 더 있습니다.
| 닫힌 이유 | iOS | Android | Android 에서 실제로 오나 |
|---|---|---|---|
| 화면 밖으로 밀어서 닫음 | onSwipeOut | ON_SWIPE_OUT | 옴 |
| 닫기 버튼을 누름 | onBtnTapped | ON_BTN_TAPPED | 옴 |
| 일반적인 닫힘 | onClose | ON_CLOSE | 옴 |
| 다른 화면으로 교체되며 닫힘 | onReplace(미리보기 중 play() · preview() 재호출) | ON_REPLACE | 옴 |
| 사유를 알 수 없는 종료 | 없음 | ON_UNKNOWN | 옴 |
| 오류로 닫힘 | onError | ON_ERROR | 아직 안 옴 |
| PIP 에서 복귀하는 중 | onRestoringPip | ON_RESTORING_PIP | 아직 안 옴 |
| 링크 이동 처리로 닫힘 | onNavigationHandleClose | ON_NAVIGATION_HANDLE_CLOSE | 아직 안 옴 |
| 방송이 종료됨 | onBroadcastEnded | ON_BROADCAST_ENDED | 옴 — showPreviewPopup 으로 띄운 미리보기 팝업만. 레이아웃에 넣은 ShopLivePreview 는 닫히지 않고 30초 뒤 다시 시작합니다 |
"아직 안 옴" 은 예약된 값입니다. Android 소스 주석 기준으로 아직 어느 화면에서도 발생하지 않고, iOS 쪽 값 구성에 맞추려고 케이스만 만들어 둔 상태입니다. 지금은 이 값들로 분기해도 타지 않습니다.
Flutter previewClose 스트림은 ShopLivePreviewClose{campaignKey, reason}를 보냅니다. reason 은 네이티브 사유와 아래처럼 대응합니다.
Flutter reason | 네이티브 사유 (iOS / Android) |
|---|---|
closeButton | onBtnTapped / ON_BTN_TAPPED |
swipeOut | onSwipeOut / ON_SWIPE_OUT |
broadcastEnded | onBroadcastEnded / ON_BROADCAST_ENDED |
programmatic | 앱이 hidePreview() 를 호출함 |
unknown | 그 밖의 사유 |
Android 미리보기에 handler 를 따로 붙이면 전역 onClose 가 안 불립니다
ShopLivePreview.setHandler 나 ShopLivePreviewData.setHandler 로 미리보기
인스턴스에 handler 를 등록하면, 그 handler 만 호출되고 전역 onClose 는 호출되지 않습니다.
이 규칙은 ShopLive.showPreviewPopup 으로 띄운 팝업에만 적용됩니다.
레이아웃 XML 에 ShopLivePreview 를 직접 배치해 쓰는 배너형은 아예 이 콜백 대상이 아니고,
OnCloseActionListener 를 씁니다.
ShopLive.setCloseHandler(_ handler: (ShopLiveViewHiddenActionType) -> Void)
ShopLive.setPreviewCloseHandler(_ handler: (ShopLiveViewHiddenActionType) -> Void)
onClose(type: ShopLivePresentType, reason: ShopLiveViewHiddenActionType)
previewClose → ShopLivePreviewClose {campaignKey, reason}
미리보기 재생 상태
| 필드 | 타입 | 설명 |
|---|---|---|
| 재생 상태 | iOS enum · Android 데이터 클래스 | ready(준비됨) · playing(재생 중) · paused(일시정지) · buffering(버퍼링) · failed(실패) |
failed 일 때 미리보기를 숨기는 식으로 씁니다. 관련: 미션 9.iOS 와 Android 는 이름도 페이로드도 사실상 같습니다. iOS 는 소문자 enum case, Android 는 PascalCase 데이터 클래스라는 언어 관례 차이뿐이고, iOS 는 별도 프로토콜로 받습니다. Flutter 에는 대응 기능이 없습니다. SDK 전체를 뒤져도 같은 역할을 하는 스트림이 없습니다.
setOnPlaybackStateChanged 와 헷갈리지 마세요
미리보기 컴포넌트 목록에 있는 그 리스너는 ExoPlayer 의 재생 상태만 보는 다른 물건입니다.
이름이 비슷해 바꿔 쓰기 쉬우니 주의하세요.ShopLivePlayerPreviewDelegate.handlePlaybackEvent(_ event: ShopLivePreviewPlaybackEvent)
ShopLivePreview.setOnPlaybackEventListener(OnPlaybackEventListener)
쿠폰·커스텀 액션 처리 결과 회신
| 항목 | 이름 | 설명 |
|---|---|---|
| 성공 여부 | iOS success: BoolAndroid isDownloadSuccess/isSuccessFlutter success | 처리에 성공했는지 |
| 식별자 | couponId 또는 id | 어느 쿠폰·팝업에 대한 결과인지. Android 는 SDK 가 알아서 붙여 생략합니다 |
| 메시지 | message | 시청자에게 보여줄 문구 |
| 팝업 처리 | iOS status · Android·Flutter popupStatus/couponStatus | SHOW · HIDE · KEEP |
| 알림 방식 | alertType | ALERT · TOAST |
부르는 방식이 플랫폼마다 다릅니다. iOS Android 는 이벤트를 받을 때 SDK 가 함께 건네준 결과 객체에 값을 담습니다. Flutter 만 결과를 따로 보내는 메서드를 부릅니다. Flutter 쪽은 Stream 이 아니니 구독하지 마세요. 확인된 설계 차이입니다(요확인 아님).
// 쿠폰 결과. handleDownloadCoupon 의 result 로 넘긴다
ShopLiveCouponResult(couponId: String,
success: Bool,
message: String?,
status: ShopLiveResultStatus, // SHOW / HIDE / KEEP
alertType: ShopLiveResultAlertType) // ALERT / TOAST
// 커스텀 액션 결과. handleCustomAction 의 result 로 넘긴다
ShopLiveCustomActionResult(id: String,
success: Bool,
message: String?,
status: ShopLiveResultStatus,
alertType: ShopLiveResultAlertType)
// 두 콜백 모두 SDK 가 건네준 callback 객체에 담는다
callback.couponResult(
isDownloadSuccess = true,
message = successMessage,
couponStatus = ShopLive.CouponPopupStatus.HIDE, // SHOW / HIDE / KEEP
alertType = ShopLive.CouponPopupResultAlertType.TOAST // ALERT / TOAST
)
callback.customActionResult(
isSuccess = true,
message = successMessage,
couponStatus = ShopLive.CouponPopupStatus.HIDE,
alertType = ShopLive.CouponPopupResultAlertType.ALERT
)
// Flutter 만 스트림이 아니라 별도 메서드로 결과를 보낸다
await ShopLivePlayer().sendDownloadCouponResult(
couponId: couponId,
success: true,
message: successMessage,
popupStatus: "HIDE", // SHOW / HIDE / KEEP
alertType: "TOAST", // ALERT / TOAST
);
await ShopLivePlayer().sendCustomActionResult(
id: id,
success: true,
message: successMessage,
popupStatus: "HIDE",
alertType: "ALERT",
);
| 팝업 처리 값 | 플레이어가 하는 일 |
|---|---|
SHOW | 팝업을 계속 띄워 둡니다 |
HIDE | 팝업을 닫습니다. 발급에 성공했을 때 보통 이 값을 씁니다 |
KEEP | 지금 상태를 그대로 둡니다 |
| 알림 방식 값 | 플레이어가 하는 일 |
|---|---|
ALERT | 메시지를 확인 버튼이 있는 알림창으로 띄웁니다 |
TOAST | 메시지를 잠깐 떴다 사라지는 토스트로 띄웁니다 |
API · 옵션
플레이어의 동작을 세부 조정하는 옵션을 하는 일별로 묶어 전부 모았습니다. 어떻게 쓰는지는 각 미션에서 다루고, 여기서는 무엇이 있는지를 봅니다. 주석에 미션 번호가 붙은 항목은 그 미션에 사용 예제가 있습니다.
세 플랫폼이 제공하는 범위가 같지 않습니다. 특히 Flutter 는 iOS·Android 에 있는 옵션 상당수가 없습니다. 각 묶음 앞에 어느 플랫폼에 무엇이 빠져 있는지 적어 두었으니 먼저 확인하세요.
ShopLive.configure · ShopLive.authToken · ShopLive.user ·
ShopLive.setUser(ShopLiveUser) 는 레거시입니다. 아래 목록에 남겨 둔 것은 예전 코드를
읽을 때를 위한 것이고, 새로 쓸 때는 항상 ShopLiveCommon.* 경로를 쓰세요.사용자·인증
시청자가 누구인지 SDK 에 알려주고, 로그인 회원이라면 그 인증을 이어 붙입니다. 실제 연동 방법은 미션 3 에 있습니다.
Flutter 는 ShopLiveCommon() 인스턴스로 같은 일을 합니다. 예제는 미션 3 을 보세요.
// 레거시. 사용자 정보. 대체: ShopLiveCommon.setUser(user:accessKey:). 생성자 파라미터 레이블은 userName: (원문 name: 은 컴파일 안 됨) · 미션 3
ShopLive.user: ShopLiveCommonUser?
// 레거시. 사용자 정보 JWT(보안 인증). 대체: ShopLiveCommon.setAuthToken(authToken:) · 미션 3
ShopLive.authToken: String?
// 레거시(v2 에서 제거 예정). with accessKey: String 하나로 AccessKey 설정. 대체: ShopLiveCommon.setAccessKey(accessKey:)
ShopLive.configure
// setAuthToken 으로 넣은 JWT 조회. 없으면 setUser 로 생성된 JWT · 미션 3
ShopLiveCommon.getAuthToken() -> String?
// 사용자 정보 설정(accessKey 는 이미 설정된 값을 그대로 사용). accessKey 를 함께 지정하려면 ShopLiveCommon.setUser(accessKey, user) · 미션 3
ShopLive.setUser(user: ShopLiveCommonUser?)
// 레거시. ShopLiveUser 타입은 더 이상 쓰지 않습니다. 대체: 위 ShopLiveCommonUser 버전
ShopLive.setUser(user: ShopLiveUser)
// 사용자 정보 JWT 설정(보안 인증) · 미션 3
ShopLive.setAuthToken(authToken: String)
// 위 설정값 조회 · 미션 3
ShopLive.getAuthToken(): String?
// 레거시. AccessKey 설정. 대체: ShopLive.setAccessKey(accessKey:)
ShopLive.configure(accessKey: String)
PIP
전체화면과 작은 창을 오가고, 그 작은 창의 크기·위치·모양을 정합니다. 동작 설명은 미션 8 에 있습니다.
// 앱 내 PIP·OS PIP 진입을 통틀어 허용할지 여부(끄면 플레이어 이탈 시 자동 PIP 전환도 안 됨) · 기본값 true · 미션 8
setEnabledPictureInPictureMode(isEnabled: Bool)
// 앱 내 PIP 의 크기·위치·닫기 버튼 모양을 세부 설정(닫기 버튼 표시는 config.useCloseButton 프로퍼티로 지정하며, 레거시 useCloseButton(_:) 을 대체. 닫기 버튼 모양 = closeButtonConfig). 이전 개별 설정 API 들보다 우선 적용됨 · 미션 8
setInAppPipConfiguration(config: ShopLiveInAppPipConfiguration)
// 주어진 PIP 설정·영상비로 실제 렌더 크기를 계산
getPreviewSize(inAppPipConfiguration: ShopLiveInAppPipConfiguration, videoRatio: CGSize) -> CGSize
// Preview 화면 위에 커스텀 뷰를 얹음(닫기 버튼은 별도로 꺼야 함)
addSubViewToPreview(subView: UIView)
// 전체화면 → 앱 내 PIP 로 전환 · 기본값 scale 0.4 · 미션 8
startPictureInPicture(with position: ShopLive.PipPosition, scale: CGFloat) / startPictureInPicture()
// PIP → 전체화면으로 전환 · 미션 8
stopPictureInPicture()
// OS PIP 로 나갔다 앱으로 돌아올 때 마지막 재생 상태(PIP/전체화면)를 유지할지, 항상 전체화면으로 돌릴지 · 기본값 false(항상 전체화면) · 미션 8
setKeepWindowStyleOnReturnFromOsPip(_ keep: Bool)
// 위 설정값 조회 · 미션 8
isKeepWindowStyleOnReturnFromOsPip() -> Bool
// OS PIP 만 별도로 허용할지 여부(앱 내 PIP 는 이 설정과 무관) · 기본값 true · 미션 8
setEnabledOSPictureInPictureMode(isEnabled: Bool)
// 앱 내 PIP 가 이동할 수 있는 영역의 상하좌우 패딩. 값이 유효하지 않으면 false 를 반환하고 기본값 적용 · 기본값 top/left/right/bottom 20 · 미션 8
setPictureInPicturePadding(padding: UIEdgeInsets) -> Bool
// 앱 내 PIP 가 이동 가능한 영역 자체를 오프셋으로 제한. 값이 유효하지 않으면 false 반환 · 미션 8
setPictureInPictureFloatingOffset(offset: UIEdgeInsets) -> Bool
// 레거시. Preview·PIP 모드에서 닫기 버튼 표시 여부. 대체: setInAppPipConfiguration(config:) 의 useCloseButton 프로퍼티
useCloseButton(_ use: Bool)
// 플레이어 이탈 시 자동으로 PIP 로 전환할지 여부 · 미션 8
ShopLive.setEnabledPictureInPictureMode(isEnabled: Boolean)
// PIP 화면 시작 시 기본 화면 배율(RATIO_1X1 / 1X2 / 2X3 / 3X4 / 9X16) · 미션 8
ShopLive.setPIPRatio(pipRatio: ShopLivePIPRatio)
// 뒤로 가기로 화면을 나갈 때 종료 대신 PIP 로 전환할지 여부 · 미션 8
ShopLive.setEnterPipModeOnBackPressed(isEnterPipMode: Boolean)
// 전체화면 → PIP 전환. 레거시 무인자형 startPictureInPicture() 는 deprecated(이 시그니처로 shouldDelayForOverlayPermissionFlow = false 호출됨) · 미션 8
ShopLive.startPictureInPicture(shouldDelayForOverlayPermissionFlow: Boolean)
// PIP → 전체화면 전환 · 미션 8
ShopLive.stopPictureInPicture()
// 뒤로 가기로 화면을 나갈 때 종료 대신 PIP 로 전환할지 여부 · 미션 8
ShopLivePlayer().setEnterPipModeOnBackPressed(isEnterPipMode: bool)
// 전체화면 ↔ PIP 전환 · 미션 8
ShopLivePlayer().startPictureInPicture() / stopPictureInPicture()
// Preview·PIP 모드에서 닫기 버튼 표시 여부
ShopLivePlayer().useCloseButton(canUse: bool)
링크 이동·공유
상품이나 배너 링크를 눌렀을 때 플레이어를 어떻게 할지, 공유 링크를 어떤 형식으로 만들지 정합니다. 미션 6·미션 7 참고.
// 공유 팝업에 전달할 scheme/URL 지정. shareDelegate 를 넘기면 시스템 공유 대신 커스텀 공유 UI 를 직접 구현 · 미션 7
setShareScheme(_ scheme: String?, shareDelegate: ShopLivePlayerShareDelegate?)
// 상품·배너 선택 시 URL 하나만 클로저로 가로챈다. 등록하면 handleNavigation 델리게이트는 호출되지 않음(iOS 전용) · 미션 6
hookNavigation(navigation: @escaping (URL) -> Void)
// 상품·배너 링크 선택 후 플레이어의 다음 동작(PIP/KEEP/CLOSE) 지정 · 기본값 PIP · 미션 11
setNextActionOnHandleNavigation(actionType: ActionType)
// 위 설정값 조회 · 미션 11
getNextActionTypeOnHandleNavigation() -> ActionType
// 공유 팝업에 전달할 scheme/URL 지정 · 미션 7
ShopLive.setShareScheme(shareSchemeUrl: String?)
// 상품·배너 링크 선택 후 플레이어의 다음 동작(PIP/KEEP/CLOSE) 지정 · 미션 11
ShopLive.setNextActionOnHandleNavigation(type: ActionType)
// 공유 시 전달할 scheme/URL 지정 · 미션 7
ShopLivePlayer().setShareScheme(shareSchemeUrl: String)
// 상품·배너 링크 선택 후 다음 동작(show/hide/keep) 지정. 네이티브에는 문자열로 전달되는데 Android 수신부가 정수로 읽어 실제로는 무시된다(미션 11 참고) · 미션 11
ShopLivePlayer().setNextActionOnHandleNavigation(type: ShopLiveActionType)
화면·UI
플레이어의 겉모습을 우리 앱에 맞춥니다. 로딩 인디케이터·채팅 폰트·상태바·영상 비율 등입니다. 미션 11 참고.
Flutter 는 영상 비율만 조정할 수 있습니다. 나머지 UI 옵션은 대응 API 가 없습니다.
iOS ShopLiveCommon.setFontFamily 는 라이브 플레이어에 적용되지 않습니다(숏폼 · 에디터용). 그래서 아래 목록에서 뺐습니다.
// 채팅 입력창·전송 버튼 폰트 지정 · 기본값 시스템 폰트 · 미션 4
setChatViewFont(inputBoxFont: UIFont?, sendButtonFont: UIFont?)
// 영상 로딩 진행률을 이미지 시퀀스 애니메이션으로 표시
setLoadingAnimation(images: [UIImage])
// 일반 addSubview 로는 ShopLive 정책상 추가되지 않는 뷰를 강제로 얹음
playerWindow?.forceAddSubView(_:)
// 전체화면 재생 중 상태바 숨김/표시 (InAppPip 에서는 항상 자동으로 보임) · 기본값 true(보이기)
setVisibleStatusBar(isVisible: Bool)
// 로딩 인디케이터 색상 지정 · 미션 11
indicatorColor: UIColor
// 영상 종횡비 유지 방식 지정
setResizeMode(mode: ShopLiveResizeMode)
// 로딩 프로그레스 바 색상 지정 · 미션 11
ShopLive.setLoadingProgressColor(@ColorInt colorInt: Int)
// 로딩 프로그레스 바를 이미지 애니메이션으로 지정(0 이면 해제) · 미션 11
ShopLive.setLoadingAnimation(@DrawableRes drawableRes: Int)
// 채팅 입력창·전송 버튼 폰트를 공통으로 지정 · 미션 4
ShopLive.setChatViewTypeface(typeface: Typeface?)
// 화면의 상태바 숨김/표시 · 기본값 false(숨김)
ShopLive.setVisibleStatusBar(isVisible: Boolean)
// 상태바·내비게이션 바 색상 변경. play 전에 호출. ShopLivePlayerSystemBarColorData(statusBarColor · navigationBarColor 기본 검정 DEFAULT_COLOR, isTaskBarOpened 기본 false)
ShopLive.setSystemBarColor(data: ShopLivePlayerSystemBarColorData)
// 위 색상 설정을 기본값으로 되돌림. play 전에 호출
ShopLive.clearSystemBarColor()
// 플레이어 화면 위에 커스텀 DialogFragment 표시·종료
ShopLive.showDialogFragment(dialogFragment: DialogFragment?) / closeDialogFragment()
// 방송 화면 스크린샷 허용 여부
ShopLive.setPlayerScreenCaptureEnabled(isEnabled: Boolean)
// 영상 종횡비 유지 방식 지정 · 기본값 CENTER_CROP
ShopLive.setResizeMode(resizeMode: ShopLiveResizeMode)
// 영상 종횡비 유지 방식 지정
ShopLivePlayer().setResizeMode(mode: ShopLiveResizeMode)
소리·인터럽트
음소거 상태와, 전화가 오거나 이어폰이 빠졌을 때 재생을 어떻게 이어갈지 정합니다. 미션 11 참고.
Flutter 는 음소거와 다른 앱 소리와의 공존만 조정할 수 있습니다. 전화·이어폰 인터럽트 옵션은 없습니다.
// 통화 종료 후 영상으로 돌아왔을 때 자동 재생할지 설정 · 기본값 false(자동 재생 안 함) · 미션 11
setAutoResumeVideoOnCallEnded(_ autoResume: Bool)
// 위 설정값 조회 · 미션 11
isAutoResumeVideoOnCallEnded() -> Bool
// 이어폰(헤드셋) 연결이 끊겼을 때 재생을 계속할지(keepPlay, 필수 인자), 계속한다면 음소거로 처리할지(isMute). 호출하지 않으면 SDK 기본 동작은 정지 · 기본값 isMute 기본값 false · 미션 11
setKeepPlayVideoOnHeadphoneUnplugged(_ keepPlay: Bool, isMute: Bool = false)
// 위 설정값 조회 · 미션 11
isKeepPlayVideoOnHeadPhoneUnplugged() -> Bool
// 플레이어 시작 시 음소거 여부 · 기본값 false(사운드 켜짐) · 미션 11
setMuteWhenPlayStart(_ mute: Bool)
// 다른 앱 오디오와 함께 재생할지, 외부 오디오 재생 시 일시정지할지 · 기본값 true(함께 재생)
setMixWithOthers(isMixAudio: Bool)
// AVAudioSession.Category 직접 지정(iOS 전용 오디오 세션 제어)
setAudioSessionCategory(category: AVAudioSession.Category)
// 이어폰(헤드셋) 연결이 끊겼을 때 계속 재생할지(isKeep), 계속한다면 음소거로 처리할지(isMute). 두 인자 모두 필수이고 1인자 형태는 없음. 끊김은 ACTION_AUDIO_BECOMING_NOISY 로 감지해 권한 불필요(Android 12 이상 실기기 요확인) · 미션 11
ShopLive.setKeepPlayVideoOnHeadphoneUnplugged(isKeep: Boolean, isMute: Boolean)
// 통화 종료 후 영상 자동 재생 여부. READ_PHONE_STATE 권한 필요 · 미션 11
ShopLive.setAutoResumeVideoOnCallEnded(isAutoResume: Boolean)
// 플레이어 시작 시 음소거 여부 · 기본값 false(사운드 켜짐) · 미션 11
ShopLive.setMuteWhenPlayStart(isMuted: Boolean)
// 오디오 포커스를 다른 앱에 뺏겼을 때(전화 등) 이벤트를 직접 받아 처리. OnAudioFocusListener 는 onGain() · onLoss() 두 메서드
ShopLive.setSoundFocusHandling(listener: OnAudioFocusListener?)
// 다른 앱 오디오와 함께 재생할지, 외부 오디오 재생 시 일시정지할지 · 기본값 true(함께 재생)
ShopLive.setMixWithOthers(isMixAudio: Boolean)
// 플레이어 시작 시 음소거 여부 · 미션 11
ShopLivePlayer().setMuteWhenPlayStart(isMute: bool)
// 다른 앱 오디오와 함께 재생할지, 외부 오디오 재생 시 일시정지할지
ShopLivePlayer().setMixWithOthers(isMixAudio: bool)
데이터 전달
플레이어에 값을 실어 보내거나, 유입 경로를 통계에 남깁니다. 미션 2·미션 10 참고.
// 서버로 사전 정의된 command·payload 전달(예: 플레이어 위 토스트 표시)
// 토스트는 command "SHOW_LAYER_TOAST", payload 키 message(필수) · duration(ms) · position(TOP/BOTTOM/CENTER) — 이전 문서 근거 · 요확인
sendCommandMessage(command: String, payload: [String:Any]?)
// 로컬 스토리지 저장 기능 사용 여부
setUsingLocalStorage(_ use: Bool)
// 앱 버전 전달(호출하지 않아도 SDK 가 자동으로 읽어 서버에 전달)
setAppVersion(_ appVersion: String)
// 플레이어 실행 전 커스텀 데이터(key-value) 추가·삭제
addParameter(key: String, value: String) / removeParameter(key: String)
// UTM 파라미터를 데이터 인사이트에 함께 전달
setUtmSource(utmSource: String?) / setUtmMedium(utmMedium: String?) / setUtmCampaign(utmCampaign: String?) / setUtmContent(utmContent: String?)
// 서버로 사전 정의된 command·payload 전달
// 토스트는 command "SHOW_LAYER_TOAST", payload 키 message(필수) · duration(ms) · position(TOP/BOTTOM/CENTER) — 이전 문서 근거 · 요확인
ShopLive.sendCommandMessage(command: String?, payload: Map<String, Any?>)
// 앱 버전 전달(호출하지 않아도 SDK 가 자동으로 읽어 서버에 전달)
ShopLive.setAppVersion(appVersion: String)
// UTM 파라미터를 데이터 인사이트에 함께 전달
ShopLive.setUtmSource / setUtmMedium / setUtmCampaign / setUtmContent(String?)
// 플레이어 실행 전 커스텀 데이터(key-value) 추가·삭제
ShopLive.addParameter(key: String, value: String?) / removeParameter(key: String)
// Shoplive 기본 Web Landing 페이지 대신 지정한 URL 사용(사전 협의 필요)
ShopLive.setEndpoint(endpoint: String?)
// 지정한 URL 을 Web Landing 페이지로 사용(사전 협의 필요)
ShopLivePlayer().setEndpoint(endpoint: String?)
// 플레이어 실행 전 커스텀 데이터(key-value) 추가·삭제
ShopLivePlayer().addParameter(key: String, value: String?) / removeParameter(key: String)
// 서버로 사전 정의된 command·payload 전달
ShopLivePlayer().sendCommandMessage(command: String, payload: Map<String, dynamic>)
// UTM 파라미터를 데이터 인사이트에 함께 전달
ShopLiveCommon().setUtmSource / setUtmMedium / setUtmCampaign / setUtmContent(String?)
상태 조회·종료
지금 플레이어가 어떤 상태인지 묻고, 필요하면 닫습니다.
Flutter 는 종료만 할 수 있고, 상태를 되묻는 API 는 없습니다.
// 현재 플레이어 스타일 조회 (fullScreen / pip / unknown)
style: ShopLive.PresentationStyle
// 현재 플레이어 모드 조회 (play / preview / none)
playerMode: ShopLive.PlayerMode
// 현재 재생 중인 방송의 영상 비율 조회 (portrait / landscape / unknown)
orientationMode: ShopLive.VideoOrientation
// 플레이어가 표시 중인 UIWindow 인스턴스
playerWindow: ShopliveWindow?
// 플레이어가 표시 중인 UIViewController 인스턴스 (Alert 등을 present 할 때 사용)
viewController: ShopLiveViewController?
// 방송 진입 성공 여부 확인. handleReceivedCommand 로 ON_SUCCESS_CAMPAIGN_JOIN 이 온 뒤부터 true
isSuccessCampaignJoin() -> Bool
// 시청 중인 방송 종료
ShopLive.close()
// 루트 액티비티 종료 시 방송도 함께 종료할지 여부. false 면 앱 종료 시점에 직접 close() 호출 필요 · 기본값 true(함께 종료)
ShopLive.setAutoCloseWhenAppDestroyed(isAutoClosed: Boolean)
// 현재 화면 상태 조회 (None / InAppPIP / PIP / Full)
ShopLive.getStatus(): Status
// 미리보기 팝업 표시 중 여부
ShopLive.isShowingPreviewPopup(): Boolean
// 방송 진입 성공 여부 확인. 플레이어가 ON_SUCCESS_CAMPAIGN_JOIN 커맨드를 보낸 뒤부터 true
ShopLive.isSuccessCampaignJoin(): Boolean
// (선택) SDK 캐시 크기 조회 · 삭제. 둘 다 @WorkerThread 라 백그라운드에서 호출. SDK 가 마지막 Activity 파기 시 자동 정리하므로 보통은 부를 필요 없음
ShopLiveCommon.getCacheDirectoryBytes(context: Context): Long
ShopLiveCommon.deleteCacheFiles(ShopLiveCommon.getCacheDirectory(context)): Boolean
// 시청 중인 방송 종료
ShopLivePlayer().close()
방송 목록 조회
Access Key 로 만든 방송 목록과 각 방송의 상태를 받아 옵니다. 화면은 그리지 않습니다. 목록을 어떻게 보여 줄지는 앱이 정합니다.
호출 전에 setAccessKey 가 끝나 있어야 합니다.
Flutter 는 1.9.0 플러그인에 이 API 가 없습니다.
import ShopLiveSDK
import ShopliveSDKCommon // 요청·응답 타입을 직접 쓸 때 필요
let request = ShopLiveCampaignListRequest(page: 1, size: 20, statuses: [.onair, .ready], order: .descending)
// 콜백은 메인 스레드에서 정확히 한 번 온다
let task = ShopLive.API.getCampaigns(request) { result in
switch result {
case .success(let response):
for campaign in response.campaigns {
if case .onair = campaign.lifecycle.status {
print(campaign.campaignKey, campaign.title ?? "")
}
// showUserCount 가 false 면 시청자 수를 보여주지 않는다
if let metrics = campaign.metrics, metrics.showUserCount {
print(metrics.userCount ?? 0)
}
}
print(response.hasMore) // true 면 page 를 1 올려 다시 요청
case .failure(let error):
print(error.codes) // 9000 · 서버 코드 · 9900 · 9901
}
}
// 화면을 벗어날 때 task.cancel() 을 부른다
// iOS 13 이상 async 판. Task 를 취소하면 요청도 취소되고 CancellationError 를 던진다
let response = try await ShopLive.API.getCampaigns(ShopLiveCampaignListRequest(statuses: [.onair]))
val request = ShopLiveCampaignListRequest(
page = 1,
size = 20,
statuses = listOf(ShopLiveCampaignFilterStatus.ONAIR, ShopLiveCampaignFilterStatus.READY),
order = ShopLiveCampaignOrder.DESCENDING,
)
// 콜백은 메인 스레드에서 정확히 한 번 온다
val handle = ShopLiveCampaigns.getCampaigns(request, object : ShopLiveCampaignListListener {
override fun onData(response: ShopLiveCampaignListResponse) {
response.campaigns.forEach { campaign ->
if (campaign.lifecycle.status == ShopLiveCampaignStatus.OnAir) {
Log.d("Shoplive", campaign.campaignKey)
}
// showUserCount 가 false 면 시청자 수를 보여주지 않는다
val metrics = campaign.metrics
if (metrics != null && metrics.showUserCount) {
Log.d("Shoplive", "${metrics.userCount}")
}
}
// hasMore 가 true 면 page 를 1 올려 다시 요청
}
override fun onError(error: ShopLiveCommonError) {
Log.e("Shoplive", "${error.code}") // 9000 · 서버 코드 · 9900 · 9901 · 10000
}
})
// 화면을 벗어날 때 handle.cancel() 을 부른다. 취소하면 어떤 콜백도 오지 않는다
| 항목 | iOS | Android |
|---|---|---|
| 호출 | ShopLive.API.getCampaigns(_:completion:) → ShopLiveCancellable. async 판은 iOS 13 이상 | ShopLiveCampaigns.getCampaigns(request, listener) → ShopLiveCancellable. 요청을 생략하면 getCampaigns(listener = ...) 처럼 이름을 붙입니다 |
| 요청 | page(1부터) · size(1~20, 범위 밖이면 SDK 가 보정) · statuses(READY · ONAIR · CLOSED, 비우면 전체) · order(오름차순 · 내림차순). 보내지 않은 값은 서버 기본값(1 · 10 · 최신순)을 따릅니다 | |
| 응답 | campaigns(0건도 정상) · hasMore(다음 페이지 여부) | |
| 방송 상태 | campaign.lifecycle.status — .reserved · .ready · .onair · .closed · .unknown(String) | campaign.lifecycle.status — Reserved · Ready · OnAir · Closed · Unknown(raw) |
| 리허설 | 리허설 중에는 상태가 Ready 로 옵니다. lifecycle.rehearsal 로 구분하세요 | |
| 집계 | metrics 는 onair · closed 때만 옵니다. showUserCount 가 false 면 숫자를 보여주지 마세요 | |
| 콜백 | 메인 스레드에서 성공 · 실패 중 정확히 1회. cancel() 하면 오지 않습니다 | |
| 오류 코드 | 9000(accessKey 미설정) · 서버 코드 · 9900(네트워크) · 9901(파싱). 코드는 error.codes | 9000(accessKey 미설정) · 서버 코드 · 9900(네트워크, 15초 상한) · 9901(파싱) · 10000(그 밖). 코드는 error.code |
API · 재생 / 음소거 / 미리보기
방송을 띄우고 끄는 API 들입니다. API · 옵션 이 "이미 떠 있는 플레이어를 어떻게 꾸밀까" 라면, 여기는 "언제 띄우고 언제 닫을까" 입니다. 주석에 미션 번호가 붙은 항목은 그 미션에 사용 예제가 있습니다.
재생 시작·종료
방송을 재생하고 닫는 기본 API 입니다. 처음 연동한다면 미션 1 부터 보세요.
// campaignKey 로 방송 영상 재생 시작. ShopLivePlayerData 의 keepWindowStateOnPlayExecuted(기본 true, 실행 모드 유지)·referrer·campaignHandler·brandHandler·isEnabledVolumeKey 로 세부 동작 조정 · 미션 1
play(data: ShopLivePlayerData)
// campaignKey 로 미리보기 재생. campaignKey·referrer·campaignHandler·brandHandler·isEnabledVolumeKey 는 항상 적용되지만, isMuted·resolution(.LIVE/.PREVIEW, 기본 .PREVIEW)은 data 인자가 실제로 ShopLivePreviewData 타입일 때만 적용된다(ShopLivePlayerData 를 그대로 넘기면 무시됨). keepWindowStateOnPlayExecuted 는 이 경로에서 읽지 않는다(play 전용) · 미션 9
preview(data: ShopLivePlayerData, completion: (() -> Void)?)
// 시청 중인 방송 종료
close()
// Android SDK 사용을 위한 AccessKey 설정 · 미션 1
ShopLive.setAccessKey(accessKey: String)
// campaignKey 로 방송 영상 재생. keepWindowStateOnPlayExecuted(기본 true)·referrer 로 세부 동작 조정. intentPlay 는 Intent 객체만 만들어 반환(직접 startActivity 하고 싶을 때) · 미션 1
ShopLive.play(context: Context, data: ShopLivePlayerData) / ShopLive.intentPlay(...): Intent
// campaignKey 로 팝업형 미리보기 표시. ShopLivePreviewData 로 크기·마진·음소거·닫기버튼·스와이프 종료 등을 세부 조정 · 미션 9
ShopLive.showPreviewPopup(activity: Activity, data: ShopLivePreviewData)
// ShopLivePreviewData.useCustomAction 사용 시, 미리보기를 누른 화면 전환에 transitionAnimation 적용
ShopLive.setPreviewTransitionAnimation(activity: Activity, view: View?)
// 재생 중인 팝업형 미리보기 숨김
ShopLive.hidePreviewPopup()
// campaignKey 로 방송 영상 재생 시작 · 미션 1
ShopLivePlayer().play(data: ShopLivePlayerData)
// campaignKey 로 미리보기 표시 · 미션 9
ShopLivePlayer().showPreview(campaignKey: String)
// 미리보기 표시 옵션 지정 · 미션 9
ShopLivePlayer().setPreviewOption(data: ShopLivePlayerPreviewData)
// 표시 중인 미리보기 숨김
ShopLivePlayer().hidePreview()
재생 중 음소거
이미 재생 중인 방송의 소리를 끄고 켭니다.
Flutter 에는 재생 도중에 쓰는 음소거 API 가 없습니다. API · 옵션 의
setMuteWhenPlayStart 로 시작 시점의 음소거 여부만 정할 수 있습니다.
// 현재 재생 중인 영상 음소거
mute()
// 현재 재생 중인 영상 음소거 해제
unmute()
// 플레이어 음소거
ShopLive.mute()
// 플레이어 음소거 해제
ShopLive.unmute()
미리보기 컴포넌트
전체화면으로 들어가지 않고 작은 화면으로 방송을 미리 보여주는 컴포넌트입니다.
iOS 는 ShopLivePlayerPreview, Android 는 ShopLivePreview 입니다.
실제로 붙이는 방법과 화면 예시는 미션 9 에 있고, 여기서는 쓸 수 있는 항목만 모아 둡니다.
Flutter 에는 이 컴포넌트가 없습니다. 대신 위 재생 시작·종료 의
showPreview·setPreviewOption·hidePreview 를 씁니다.
두 컴포넌트는 항목 구성이 꽤 달라 플랫폼별로 나눠 실었습니다.
iOS ShopLivePlayerPreview
// ShopLivePlayerPreview 상태 초기화(campaignKey, campaignId, resizeMode, useCloseButton 등) · 미션 9
preview.action(.initialize)
// accessKey·campaignKey 주입 후 플레이어 로딩. 전역 accessKey 를 이 값으로 바꾼다 · 미션 9
preview.action(.start(accessKey:campaignKey:referrer:))
// 음소거 상태 설정 · 미션 9
preview.action(.setMuted(Bool))
// 추적용 식별자 주입 · 미션 9
preview.action(.setReferrer(String?))
// 재생 / 정지 / 정지+초기화 · 미션 9
preview.action(.play) / .pause / .stop
// 부모 뷰에서 제거하고 메모리 해제 시작 · 미션 9
preview.action(.close)
// 방송 다시 실행 · 미션 9
preview.action(.retry)
// 모서리 반경 설정 · 미션 9
preview.action(.setCornerRadius(CGFloat))
// 닫기 버튼 사용 여부. 1.9.0 에서 동작 없음(닫기 버튼 미지원)
preview.action(.useCloseButton(Bool))
// 디바이스 볼륨 버튼으로 음소거 해제 허용 여부 · 미션 9
preview.action(.setEnabledVolumeKey(isEnabledVolumeKey: Bool))
// 영상 종횡비 유지 방식 · 기본값 CENTER_CROP · 미션 9
preview.action(.setResizeMode(ShopLiveResizeMode))
// 미리보기 해상도(.LIVE/.PREVIEW) · 기본값 .PREVIEW · 미션 9
preview.action(.setResolutionType(ShopLivePlayerPreviewResolution))
// 이벤트를 구독하는 클로저 (Result) -> (). 케이스: log · handleReceivedCommand · avPlayerTimeControlStatus · avPlayerItemStatus · requestShowAlertController · didChangeCampaignStatus · onError · handleCommand · onSetUserName · handleShare · didChangeCampaignInfo · didChangeVideoDimension · handleShopLivePlayerCampaign · handleShopLivePlayerBrand · 미션 9
preview.resultHandler
// 재생 상태 이벤트(ready · playing · paused · buffering · failed)를 받는 델리게이트(ShopLivePlayerPreviewDelegate, weak)
preview.delegate
// 재생 중 여부 / 음소거 여부 / AVPlayerItem 상태 / campaignKey·재생 상태·음소거·재생 시간·캠페인·브랜드 정보 조회
preview.isPlaying() / preview.isMuted() / preview.getPlayerItemStatus() / preview.getPlaybackInfo()
Android ShopLivePreview
// 레이아웃 XML 에 직접 배치하는 미리보기 컴포넌트 · 미션 9
<cloud.shoplive.sdk.ShopLivePreview>
// 방송 key·referrer를 미리 설정 · 미션 9
setCampaignKey(campaignKey: String?) / setReferrer(referrer: String?)
// 미리보기 플레이어 초기 설정 · 미션 9
initializePlayer()
// 미리보기 재생 시작(내부적으로 initializePlayer→prepare→play 순서로 실행) · 미션 9
start() / start(accessKey:campaignKey:referrer:)
// 미리보기 영상 준비 / 재생 / 일시정지 / 정지 · 미션 9
prepare() / play() / pause() / stop()
// 미리보기 리소스 해제 · 미션 9
release()
// 미리보기 다시 실행 · 미션 9
retry()
// 음소거 상태 전환 · 미션 9
setMuted(isMuted: Boolean)
// 영상 종횡비 유지 방식 · 기본값 CENTER_CROP · 미션 9
setResizeMode(resizeMode: ShopLiveResizeMode)
// LifecycleOwner 등록 시 라이프사이클에 맞춰 자동 play/pause · 미션 9
setLifecycleObserver(owner: LifecycleOwner)
// 미리보기 시작·첫 프레임 렌더·클릭·종료·화면비 변경·재생 상태 변경 이벤트 리스너 등록
setOnInitializeListener / setOnRenderedFirstFrameListener / setOnClickListener / setOnCloseListener / setOnDimensionRatioListener / setOnPlaybackStateChanged
// 닫기 버튼 사용 여부 · 미션 9
useCloseButton(use: Boolean)
// 디바이스 볼륨 버튼으로 음소거 해제 허용 여부 · 미션 9
setEnabledVolumeKey(isEnabledVolumeKey: Boolean)
// 미리보기 해상도(LIVE/PREVIEW) · 기본값 PREVIEW · 미션 9
setResolutionType(resolutionType: ShopLivePreviewResolutionType)
// 미리보기 종료 후 이미지 표시 여부 · 기본값 true
setPostCampaignImageVisible(isPostCampaignImageVisible: Boolean)
// 미리보기 메모리 해제 · 미션 9
destroy()
// campaignKey·재생 상태·음소거·재생 시간·캠페인·브랜드 정보를 한 번에 조회
getPlaybackInfo(): ShopLivePreviewPlaybackInfo
Android ShopLivePreviewData 속성
showPreviewPopup · setPreviewOptions 에 넘기는 옵션입니다(미션 9).
크기 · 여백 · 모서리의 0 · -1 은 "지정 안 함"이고, 읽을 때 오른쪽 기본값으로 바뀝니다.
maxWidth · maxHeight 속성은 없습니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
height | Int(px) | 0 → 180dp | 창 높이 |
width | Int(px) | 0 → 180dp×9/16(약 101dp) | 창 너비 |
marginTop · marginBottom · marginLeft · marginRight | Int(px) | -1 → 16dp | 화면 가장자리와의 거리 |
canTransition | Boolean | true | 미리보기 → 플레이어 전환 허용 |
canVibrated | Boolean | true | 진동 허용 |
useCloseButton | Boolean | false | 닫기 버튼 표시 |
position | ShopLivePreviewPositionConfig | BOTTOM_RIGHT | 창 위치 |
radius | Float(px) | -1 → 10dp | 모서리 반경 |
referrer | String? | null | 유입 경로 |
enabledSwipeOut | Boolean | true | 화면 밖으로 밀어 닫기 |
isMuted | Boolean | true | 음소거로 시작 |
isEnabledVolumeKey | Boolean | false | 볼륨 버튼으로 음소거 해제 허용 |
resolutionType | ShopLivePreviewResolutionType | PREVIEW | 해상도(LIVE · PREVIEW) |
isPostCampaignImageVisible | Boolean | true | 방송 종료 후 이미지 표시 |
useCustomAction | Boolean | false | 미리보기를 눌렀을 때 SDK 기본 전환 대신 앱이 처리 |
onClickListener · onInitializeListener · onRenderedFirstFrameListener · onCloseListener · onCloseActionListener · onCloseButtonClickListener · onCampaignListener · onBrandListener · onPlaybackEventListener | 리스너 | null | 클릭 · 시작 · 첫 프레임 · 닫힘 · 닫힘(사유 포함) · 닫기 버튼 · 캠페인 · 브랜드 · 재생 상태 |
handler | ShopLiveHandler? | null | 이 미리보기 전용 핸들러(화면 닫힘 참고) |
backgroundColor | Int?(ColorInt) | null | 배경색 |
closeButtonConfig | CloseButtonConfig? | null | 닫기 버튼 모양(위치 · 크기 · 오프셋 · 색 · 그림자 · 이미지) |
Flutter ShopLivePlayerPreviewData
setPreviewOption 에 넘기는 옵션입니다. 값을 비우면 네이티브가 플랫폼마다 다른 기본값을 넣습니다.
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
campaignKey | String | (필수) | 방송 키 |
useCloseButton | bool | false | 닫기 버튼 표시 |
enableSwipeOut | bool | true | 화면 밖으로 밀어 닫기 |
referrer | String? | null | 유입 경로 |
pipRadius | double? | null → 0 | 모서리 반경 |
pipMaxSize | double? | null → iOS 300, Android 0 | 창 긴 변 크기. Android 는 너비 · 높이에 같은 값(dp)을 넣고, 0 이면 SDK 기본 크기로 처리하는 것으로 보입니다(요확인) |
marginTop · marginBottom · marginLeft · marginRight | double? | null → 0 | 가장자리와의 거리. Android 는 dp 를 px 로 바꿉니다 |
position | String? | iOS TOP_LEFT, Android BOTTOM_RIGHT | TOP_LEFT · TOP_RIGHT · BOTTOM_LEFT · BOTTOM_RIGHT. 이 4값 밖의 문자열은 두 플랫폼 모두 BOTTOM_RIGHT |
closeButtonConfig | ShopLiveCloseButtonConfig? | null | 닫기 버튼 모양. position 을 비우면 iOS TOP_LEFT, Android TOP_RIGHT |
position 은 꼭 지정하세요
비워 두면 iOS 는 왼쪽 위, Android 는 오른쪽 아래에 창이 뜹니다. 두 플랫폼을 맞추려면 값을 명시하세요.에러 코드
원문 문서의 네 갈래 분류(연결 / 방송 입장 / 방송 중 / 기타)를 그대로 유지합니다. 같은 코드 번호라도 플랫폼마다 의미가 다를 수 있습니다. 번호만 보고 플랫폼을 짐작하지 말고 아래 콜아웃을 꼭 읽어 보세요.
iOS ShopLiveErrorCases
서버 발신 코드(음수)와 SDK 내부 코드(9000+)가 하나의 enum 에 섞여 있습니다.
| 분류 | 코드 | 상수 | 의미 |
|---|---|---|---|
| 연결 | -102 | AuthenticationFailed | 인증 실패 |
| 연결 | -115 | GuestLoginNotAllowed | 게스트 로그인 불허 |
| 연결 | -200 | CustomAccountNotFound | 존재하지 않는 서비스(accessKey 오류) |
| 연결 | -201 | CustomAccountExpired | 고객사 계약 만료 |
| 연결 | -204 | InvalidSignature | JWT 서명 오류 |
| 연결 | -412 | ExpiredSession | 세션 만료 |
| 연결 / 방송 입장 | -500 | ServerError | 서버 오류(양쪽 공용) |
| 방송 입장 | -710 | CampaignNotFound | 방송 없음 |
| 방송 입장 | -711 | CampaignNotOnAir | 방송 중 아님 |
| 방송 중 | -411 | DuplicateSession | 중복 세션 |
| 기타(내부) | 9000 | NotInitializedAccessKey | accessKey 미설정 |
| 기타(내부) | 9001 | NotInitializedShareURL | 공유 URL 없음 |
| 기타(내부) | 9002 | NotInitializedShortformConfig | 서버 설정(숏폼 · 호스트) 누락 시. 숏폼 · 에디터에서도 발생 |
| 기타(내부) | 9003 | InValidURL | 잘못된 URL. Android 의 같은 코드와 의미가 다릅니다(아래 콜아웃) |
| 기타(내부) | 9300 | FailedEncoding | 인코딩 실패 |
| 기타(내부) | 9310 | RemovedVideo | 삭제된 영상 |
| 상태 안내(오류 아님) | 9100 | NoVideoForPip | PIP 전환 시 재생 중이던 영상 없음 |
| 상태 안내(오류 아님) | 9101 | NoPlayableStreamUrl | 준비 · 리허설 · 라이브 상태인데 재생 URL 을 아직 받지 못함 |
| 상태 안내(오류 아님) | 9102 | LiveNotBroadcasting | 라이브 상태인데 아직 송출 전 |
| 기타(내부) | 9501 | UnsupportedMedia | 미지원 코덱 |
| 기타(내부) | 9500 | UnsupportedOSpipMode | OS PIP 미지원(iOS 14 미만) |
| 기타(내부) | 9900 | FailedNetwork | 네트워크 실패 |
| 기타(내부) | 9901 | FailedJSONParsing | JSON 파싱 실패 |
| 기타(내부) | 10000 | UnexpectedError | 알 수 없는 오류 |
Android 두 체계로 분리
ShopLiveCommonErrorCode(SDK 내부 9xxx 대역)와 ShopLiveCampaignErrorCode(서버 응답
전용, 소스 주석에 "SDK 내부 9xxx 체계와 별개"라고 명시)가 따로 있습니다.
| 분류 | 코드 | 상수 | 의미 |
|---|---|---|---|
| 연결(서버 응답 · ShopLiveCampaignErrorCode) | -200 | SERVICE_NOT_EXIST | 존재하지 않는 액세스 키 |
| 연결(서버 응답 · ShopLiveCampaignErrorCode) | -201 | ACCESS_KEY_EXPIRED | 만료된 액세스 키. 요확인: 소스 자체에 "만료된 키를 구하지 못해 실제로 이 코드가 내려오는지 확인 못함" TODO 주석이 있습니다 |
| 기타(서버 응답 · ShopLiveCampaignErrorCode) | -510 | INVALID_PARAMETER | 잘못된 요청 인자(iOS 목록엔 이 코드 자체가 없음) |
| 기타(내부 · ShopLiveCommonErrorCode) | 9000 | NOT_INITIALIZED_ACCESS_KEY | accessKey 미설정 |
| 기타(내부) | 9001 | NOT_INITIALIZED_SHARE_URL | 공유 URL 없음 |
| 기타(내부) | 9002 | NOT_INITIALIZED_CONFIG | 서버 설정(숏폼 · 호스트) 누락 시. 숏폼 · 에디터에서도 발생 |
| 기타(내부) | 9003 | NOT_INITIALIZED_CAMPAIGN_KEY | 캠페인 키 미설정. iOS 의 같은 코드와 의미가 다릅니다(아래 콜아웃) |
| 기타(내부) | 9050 | REQUIRED_COMPONENT_ACTIVITY | ComponentActivity 필요(iOS 대응 없음) |
| 상태 안내(오류 아님) | 9101 | NO_PLAYABLE_STREAM_URL | 재생 URL 없음. onError 로 오며 방송 상태가 ONAIR 일 때만 |
| 상태 안내(오류 아님) | 9102 | LIVE_NOT_BROADCASTING | 아직 송출 전(세그먼트 안 옴). onError 로 오며 방송 상태가 ONAIR 일 때만 |
| 기타(내부) | 9300 | FAILED_ENCODING | 인코딩 실패 |
| 기타(내부) | 9301 | FAILED_VIDEO_ENCODING | 영상 인코딩 실패(iOS 대응 없음, iOS 는 9300 하나로 퉁침) |
| 기타(내부) | 9302 | FAILED_COVER_ENCODING | 커버 인코딩 실패(iOS 대응 없음) |
| 기타(내부) | 9310 | REMOVED_VIDEO | 삭제된 영상 |
| 기타(내부) | 9320 | WITHOUT_THUMBNAIL | 썸네일 없음(iOS 대응 없음) |
| 기타(내부) | 9500 | UNSUPPORTED_OS_PIP_MODE | OS PIP 미지원(Android 8 · API 26 미만) |
| 기타(내부) | 9501 | UNSUPPORTED_MEDIA | 미지원 코덱 |
| 기타(내부) | 9800 | FAILED_PLAYER | 플레이어 오류(iOS 대응 없음, iOS 엔 9800 대역 자체가 없음) |
| 기타(내부) | 9900 | FAILED_NETWORK | 네트워크 실패 |
| 기타(내부) | 9901 | FAILED_JSON_PARSING | JSON 파싱 실패 |
| 기타(내부) | 9902 | FAILED_FETCH_DATA | 데이터 조회 실패(iOS 대응 없음) |
| 기타(내부) | 10000 | UNEXPECTED_ERROR | 알 수 없는 오류 |
NoVideoForPip)에 대응하는 코드가 없습니다(9101 부터 시작).
연결/입장/중 코드 상당수(-102 · -115 · -204 · -412 · -500 · -710 · -711 · -411)는 Android 소스 어디에도 상수로
정의돼 있지 않습니다. 서버가 이 값을 내려주면 상수화 없이 ShopLiveCommonError 의 code
필드에 그대로 통과시킵니다. iOS 와 마찬가지로 "서버 계약 문서화" 용도라 두 플랫폼이 상충하지는 않습니다.Flutter
Flutter SDK/샘플에는 자체 에러 코드 상수·enum 이 없습니다. 네이티브(iOS/Android)에서 발생한
코드·메시지를 그대로 전달하는 범용 패스스루 클래스 ShopLivePlayerError{code: String, message: String}
뿐입니다. 위 iOS·Android 표의 코드 값이 문자열 그대로 넘어온다고 보면 됩니다. 관련:
레퍼런스의 이벤트 항목 "오류" 행.
플랫폼 부록
iOS Xcode 하위버전 대응
정규 배포(1.9.0)는 요구사항 표대로 iOS 15.0 · Swift 5 를 기준으로 합니다. Xcode 26 미만 환경에서 이 정식 배포 SDK 를 쓸 수 없거나 빌드가 실패할 수 있는 경우, 하위 호환용으로 별도 배포된 SDK 를 쓸 수 있습니다.
| Xcode 버전 | SDK 버전 | Swift 버전 |
|---|---|---|
| 16.0 | 1.8.1 | 6.0 |
| 16.4 | 1.8.2 · 1.8.4 | 6.1.2 |
표의 Swift 버전은 원문 문서 그대로이며 podspec 으로 재확인하지 못했습니다(요확인). 이 버전들은 압축 파일로 따로 배포돼 CocoaPods spec 저장소에 올라가 있지 않습니다. 실제로 내려받아야 한다면 Shoplive 담당자에게 요청하세요.
iOS 구버전 SPM 경로 (1.8.x 이하)
1.9.0 부터 SPM 배포 저장소가 shoplive-sdk-ios 하나로 통합됐습니다.
그 이전 경로였던 ios-sdk·common-ios 두 저장소는 1.8.x 이하 전용으로만 남아 있습니다.
새로 연동한다면 쓰지 마세요(설치 의 SPM 탭 참고).
Android ProGuard
따로 설정할 것이 없습니다. 난독화 규칙은 각 라이브러리가 자기 안에
consumer-rules.pro 로 넣어 배포합니다. 의존성만 추가하면 빌드할 때 앱의 규칙에 자동으로
합쳐지므로, 앱의 proguard-rules.pro 를 열어 볼 일이 없습니다.
어떤 모듈이 규칙을 들고 있는지 확인하고 싶다면
domestic 라인의 9개 모듈이 전부 자기 build.gradle 에
consumerProguardFiles "consumer-rules.pro" 를 선언하고 있고, 그 파일도 9개 모두 실재합니다
(선언 9개 · 파일 9개, SDK 소스에서 직접 확인).
shoplive-common · shoplive-exoplayer · shoplive-filter ·
shoplive-media3 · shoplive-network · shoplive-permission ·
shoplive-player · shoplive-short-form · shoplive-video-editor
내용은 알려진 것보다 많습니다. shoplive-network 는 Gson keep 규칙,
shoplive-player 는 Coil·OkHttp·코루틴 dontwarn,
shoplive-short-form 과 shoplive-video-editor 는 Glide 관련 규칙을 담고 있습니다.
어차피 자동으로 합쳐지므로 앱에서 따로 볼 필요는 없습니다.
FAQ · 트러블슈팅
자주 묻는 질문입니다. 본문 미션에서 이미 다룬 내용은 중복하지 않고 해당 미션으로 링크합니다.
공통
| 질문 | 답 |
|---|---|
| Hybrid 앱(WebView 기반)에도 적용할 수 있나요? | 네, 가능합니다. 네이티브 SDK 를 하이브리드 앱의 네이티브 레이어에 연동하는 방식으로 동작합니다. |
| Flutter·React Native 앱에도 적용할 수 있나요? | Flutter 는 이 문서가 다루는 shoplive_player 패키지로 지원합니다(설치 참고). React Native 샘플도 별도로 제공되나, 샘플 다운로드 URL 의 실제 접근 가능 여부는 확인되지 않았습니다. 요확인. |
| 영상 재생을 위한 별도 라이브러리를 추가로 설치해야 하나요? | 아니요. ExoPlayer/media3 등 필요한 재생 라이브러리가 SDK 내부에 이미 번들링돼 있어 별도 설치가 필요 없습니다. |
| UI 를 커스터마이징하면 앱 코드도 함께 수정해야 하나요? | 아니요. UI 커스터마이징은 SDK 가 제공하는 옵션 API(API · 옵션)로 처리되는 skin 수준이라, 앱 쪽 코드 수정은 필요 없습니다. |
iOS PIP
| 질문 | 답 |
|---|---|
| PIP 구현을 위해 앱단에서 추가로 해야 할 작업이 있나요? | OS PIP 를 쓰려면 있습니다. Xcode 프로젝트에서 Background Modes 캐퍼빌리티를 켜야 하고, 앱스토어 심사 제출 시 사용 목적과 재현 방법을 함께 기재해야 합니다. 절차는 미션 8(OS PIP)을 참고하세요. PIP 진입·전환 자체의 상태 관리(창 크기·위치 등)는 SDK 가 처리합니다. |
| 상품 클릭 시 PIP 로 자동 전환되나요? | 네. 링크를 누르면 SDK 가 handleNavigation 을 부르기 직전에 PIP 로 전환합니다(setNextActionOnHandleNavigation 기본 .PIP). .KEEP · .CLOSE 로 바꿀 수 있고, hookNavigation 을 등록하면 이 동작은 일어나지 않습니다(미션 11). |
Android 딥링크로 열면 PIP 전환이 어긋납니다
증상. 딥링크로 앱에 들어와 방송을 보다가 상품을 눌러 PIP 로 내려가면, 우리 앱 화면이 앞으로 나오지 않고 엉뚱한 앱이 보입니다. 앱 아이콘으로 실행했을 때는 멀쩡합니다.
원인. 플레이어는 PIP 로 내려갈 때 ActivityManager.appTasks 에서 앱의 Task 를 찾아
앞으로 올립니다. 이때 기준으로 삼는 표시가 CATEGORY_LAUNCHER 인데, 이 표시는 앱 아이콘으로
실행한 Task 에만 붙습니다. 딥링크로 만들어진 Task 에는 없어서, 백스택 모양에 따라 앱을 못 찾는 경우가
생깁니다. 백스택 모양은 링크를 띄운 쪽이 FLAG_ACTIVITY_NEW_TASK 를 붙였는지에 따라 달라지는데,
그건 우리가 정할 수 없습니다.
해결. 딥링크 진입점을 SchemeActivity 처럼 따로 두고, 거기서 실제 화면을
FLAG_ACTIVITY_NEW_TASK 로 띄우세요. 미션 2 의 Android 예제가 그 형태입니다.
이렇게 하면 링크를 누른 쪽이 어디든 백스택 모양이 같아집니다. 자세한 규칙은
Android 딥링크 문서에 있습니다.
재생 상태 문의
플레이어·프리뷰 상태값이 플랫폼마다 다르게 움직이는 것처럼 보인다면, 값 자체가 플랫폼마다 다른 경우가 있습니다. 레퍼런스의 이벤트 항목(플레이어 화면 생성·소멸, 화면 닫힘)과 데이터 필드 사전(Campaign 항목)의 플랫폼 차이 콜아웃을 먼저 확인하세요.
에러 코드 문의
같은 코드 번호라도 플랫폼마다 의미가 다를 수 있습니다. 코드 값만으로 플랫폼을 짐작하지 말고 레퍼런스의 에러 코드를 함께 확인하세요.