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つだけ宣言) | 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 はこれらすべてを1つのパッケージが包んでいるため、選ぶものはありません。
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
上のネイティブモジュールをすべて包んだ1つのパッケージです。
Flutter shoplive_player
ミッションマップ
実装したいものを下から探して、そのミッションへ直接進んでください。順番どおりに読む必要はありません。ただし、すべてのミッションは インストールとミッション1を終えていることを前提としています。
| 実装したいこと | ミッション |
|---|---|
| ボタンを押すとライブが全画面で開く | ミッション1・プレーヤーを表示する |
| プッシュ・SMSのリンクをタップすると直接ライブが開く | ミッション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の3つだけが存在するため、
上の例のようにShopLiveと記述します。1.9.0からSPM配布リポジトリがshoplive-sdk-ios 一つに統合されました。1.8.x までは ios-sdkとcommon-ios 2つのリポジトリを別々に取得する必要がありましたが、新規に連携するなら統合リポジトリだけで十分です。
- XcodeでFile → Add Package Dependencies…を開き、
https://github.com/shoplive/shoplive-sdk-iosを入力します。 - 依存関係ルールをUp to Next Major Version、基準バージョン1.9.0に指定します。
ShopLiveSDK·ShopliveSDKCommon2つの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のうち1つだけ 入れてください。両方入れるとクラスが重複してビルドが壊れます。
shoplive-sdk-coreはshoplive-common · shoplive-network · 再生エンジンを
推移的依存関係として含みません。そのためすべて宣言する必要があります。1つでも欠けると、ビルドは通っても実行時にクラスが見つかりません。// 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 の先頭に下の3行を入れ、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: "YOUR_ACCESS_KEY")
// 2) 視聴開始
ShopLive.play(data: .init(campaignKey: "YOUR_CAMPAIGN_KEY"))
// 1) アプリ起動時に1回
ShopLive.setAccessKey("YOUR_ACCESS_KEY")
ShopLive.setHandler(myHandler)
// 2) 視聴開始
ShopLive.play(this, ShopLivePlayerData("YOUR_CAMPAIGN_KEY"))
final common = ShopLiveCommon();
final player = ShopLivePlayer();
// 1) アプリ起動時に1回
common.setAccessKey(accessKey: 'YOUR_ACCESS_KEY');
// 2) 視聴開始
player.play(data: ShopLivePlayerData(campaignKey: 'YOUR_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 2行を含む)に置き換え、バージョンを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)で視聴を促す
配信中のライブを低画質・ミュートのプレビューで先に見せ、タップで本再生に切り替わるよう促すことができます。 3プラットフォームすべてで提供されますしますが、呼び出し方が異なります。iOS はデータオブジェクトを渡し、Flutter はオプション設定と表示が 2つの呼び出しに分かれています。
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・ディープリンクで表示する
広告・プッシュ・SMS に付くリンクには通常、 どの配信を開くかとどこから流入したかがクエリとして一緒に載ってきます。アプリがそのリンクで開かれたら、クエリを一つずつ取り出して 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 が関与する区間は クエリパラメータの受け渡しからで、そこからは3つのプラットフォームとも同じです。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>
クエリパラメータの受け渡し
ここからは3つのプラットフォームとも同じです。URL からクエリを一つずつ取り出し、種類に応じて渡せば十分です。
ak は Access Key として、 utm で始まる4つはそれぞれ専用の 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 の4つは必ず専用 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"} |
署名の例です。2 つのコードは同じトークンを作ります — 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()を混同していない
前へ ← ミッション2次へ → ミッション4・チャット
ミッション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)
完了確認
- 管理画面のチャット権限設定とアプリの実際の動作(ゲスト許可の有無)が一致している
- (フォント変更時)チャット入力欄・送信ボタンに指定したフォントが適用される
前へ ← ミッション3次へ → ミッション5・クーポン
ミッション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クーポンをクリックすると、指定した方式(Safari・WebView・外部ブラウザ)で遷移する
- Download couponをクリックすると、発行結果ポップアップ(SHOW/HIDE/KEEP)が意図どおり表示される
- User defineをクリックすると、payloadのid・typeがログに正常に出力される
前へ ← ミッション4次へ → ミッション6・商品クリック
ミッション6・商品クリック
- 商品一覧で商品やカートボタンを押すと、SDKが
CLICK_PRODUCT_DETAIL·CLICK_PRODUCT_CARTコマンドをアプリに伝えます。 - 商品・バナーに詳細URLが設定されている場合、同じクリックが
handleNavigationとしても伝えられます。 - iOSではリンク遷移のみを1つのクロージャで受け取る
hookNavigationも提供しています。
商品・カートクリックコマンド
コマンド名は3プラットフォームで共通です。コマンド受信コールバックにはクーポン・ログイン要求など他のコマンドも一緒に届くため、 コマンド名で分岐して処理してください。
// 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:)は非推奨です
引数が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が両方 呼ばれます。Web プレーヤーが1回のクリックを2つの別々のメッセージとして送るためです。(詳細 URL がない商品は handleNavigation なしでコマンドだけ来ます。)
そのため両方で画面遷移を実装すると 詳細画面が2回開きます。
どちらか一方だけで 遷移を処理してください。詳細 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またはWeb 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)
// または Web 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)
// または Web 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の2番目の引数は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 は2種類あります。 アプリ内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 とプレビューウィンドウは同じウィンドウなので、設定も同じ呼び出し 1 つで行います
player.setPreviewOption(
data: ShopLivePlayerPreviewData(
campaignKey: 'YOUR_CAMPAIGN_KEY',
useCloseButton: true,
enableSwipeOut: true,
pipRadius: 10,
pipMaxSize: 400,
position: 'BOTTOM_RIGHT',
),
);
ウィンドウサイズは pipSize で決めます。 pipMaxSize(長辺基準、残りの辺は映像比率に合わせて自動)・
pipFixedWidth·pipFixedHeightのうち1つだけ 受け取るコンストラクタに分かれているので、望む基準を一つ選んで入れてください。
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) の2番目の引数も無視されます。Android で小さなウィンドウが必要なら下の OS PIPを使用してください。setPreviewOption(ShopLivePlayerPreviewData) 一つが iOS では setInAppPipConfigurationとsetPictureInPicturePadding に、Android では ShopLivePreviewData につながります。
ミッション9 のプレビュー設定と同じ呼び出しなので、一度指定すれば十分です。
代わりに Flutter に全くないものもあります。OS PIP 有効化トグル(setEnabledPictureInPictureMode系)と
Androidのオーバーレイ権限API(ShopLivePermissionManager)は Flutter SDK にないため、必要ならネイティブコードを直接呼ぶ必要があります。
古いドキュメントを見て連携した場合: なくなった API 3つ
pipScale·setEnabledPipSwipeOut·ShopLive.useCloseButton(_:) は deprecated です。3つとも上の例の 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がオンになっている必要があります
2つのフラグは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()は非推奨です
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、オフセット設定時)2つの関数の戻り値が
trueで、PIPが指定した領域の外に出ない
前へ ← ミッション7次へ → ミッション9・配信プレビュー
ミッション9・配信プレビュー
プレビューはデフォルトがミュートで、ウィンドウをタップすると全画面プレーヤーに切り替えられます。
AndroidではActivityの上に表示するPopup Previewが推奨方式です。
showPreviewPopup 一つで処理します。プレビューの開始
let previewData = ShopLivePreviewData(
campaignKey: "YOUR_CAMPAIGN_KEY",
keepWindowStateOnPlayExecuted: true,
referrer: nil,
isMuted: true,
isEnabledVolumeKey: false,
resolution: .PREVIEW
)
ShopLive.preview(data: previewData) {
// プレビューウィンドウをタップしたときに全画面へ切り替えるには
ShopLive.play(data: .init(campaignKey: "YOUR_CAMPAIGN_KEY"))
}
ShopLive.showPreviewPopup(this, ShopLivePreviewData("YOUR_CAMPAIGN_KEY").apply {
useCloseButton = true
enabledSwipeOut = true
resolutionType = ShopLivePreviewResolutionType.LIVE
onClickListener = View.OnClickListener { view ->
// プレビュー → プレイヤー遷移アニメーション
ShopLive.setPreviewTransitionAnimation(this@MainActivity, view)
ShopLive.play(this@MainActivity, ShopLivePlayerData("YOUR_CAMPAIGN_KEY"))
}
})
final player = ShopLivePlayer();
// 1) 先にオプションを指定
player.setPreviewOption(
data: ShopLivePlayerPreviewData(
campaignKey: 'YOUR_CAMPAIGN_KEY',
useCloseButton: true,
enableSwipeOut: true,
pipRadius: 10,
pipMaxSize: 400,
position: 'BOTTOM_RIGHT',
),
);
// 2) プレビュー開始
player.showPreview(campaignKey: 'YOUR_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) は引数3つすべてが deprecated です。accessKey は ミッション1のようにShopLiveCommon.setAccessKey であらかじめ入れておけば十分です。
showPreviewPopupがnullを返すことがあります
すでにプレーヤーが表示されている、または対象 Activity が終了中の場合は、プレビューを表示せず nullを返します。
戻り値を使うコードならnull処理を忘れないでください。オプション全体とデフォルト値は Android ShopLivePreviewData プロパティ ·
Flutter ShopLivePlayerPreviewData の表にあります。
Android 保存しておいたオプションで表示する
プレビューオプションをアプリ起動時に一度保存しておけば、以降はcampaignKeyだけ渡して表示できます。
// オプションを 1 回保存 — campaignKey は showPreviewPopup 呼び出し時の値で上書きされます
ShopLive.setPreviewOptions(ShopLivePreviewData("YOUR_CAMPAIGN_KEY").apply {
useCloseButton = true
enabledSwipeOut = true
})
// 表示するときは campaignKey のみ
ShopLive.showPreviewPopup(this, "YOUR_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日以内に購入した場合、これを購入コンバージョンとして記録します。 コンバージョンは最後に視聴した動画を基準に判定され、ライブ(リプレイ)とショートフォームはそれぞれ別々にカウントされるため、 1回の購入が両方にそれぞれコンバージョンとして記録されることがあります。
購入完了時点でコンバージョンイベントを送る
購入が確定する画面(注文完了ページなど)でsendConversionEventを1回呼び出します。
渡した商品識別値が視聴記録とマッピングされ、視聴と購入の関連が記録されます。
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は省略できません
この2つの引数だけデフォルト値がないため、渡す値がなくても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を呼び出しています。実際のサービスでは購入が確定した後に1回だけ 呼び出してください。クリックのたびに送ると、買っていない商品まで転換として記録されます。視聴者と購入者を同一人物として紐づける
コンバージョンは「この人が見て、この人が買った」がつながって初めて記録されます。 コンバージョンイベントは呼び出した瞬間のユーザー識別値を読み取って一緒に送信するため、 視聴時に使ったものと同じ値が購入時にも設定されている必要があります。
| 状況 | 設定すること |
|---|---|
| ログインユーザー | ミッション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が1回だけ呼ばれる - 呼び出し時点で、視聴時と同じユーザー識別値(
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キーを入れて初期状態を渡す例を掲載していますが、
現行のWebプレーヤーはcustom内のキーを読みません。
customはユーザー情報に載ってバックエンドに送られる値であって、プレーヤーUIは変えません
(ミッション3で使ったあのフィールドです)。初期状態も上のSET_BRAND_FAVORITEで送ってください。
加えて、その原文例が使うShopLiveUserとaddCustomParameterはdeprecated です。ユーザー情報は ミッション3のようにShopLiveCommonUserで渡してください。2) ボタンが押されたときに受け取って処理する。 ミッション6と同じコマンドコールバックで届きます。
フォローと解除は同じイベントです。コマンド名が別にあるわけではなく、
favoriteというbooleanが1つ乗ってくるだけです。
アプリがすべきことは、自分側のフォロー処理を行った後、その結果確定した状態を
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", "{...}")は非推奨と表示されています。
Mapを受け取る方を使ってください。非推奨メッセージが指す
ShopLiveCommon.sendCommandMessage は実際には存在しません。 ShopLiveの方が正しいです。payloadにnilを渡すと何も送信されません
sendCommandMessage(command:payload:)はpayloadがnilの場合、そのまま返します。
送る値が無くても、空の辞書を渡してください。自社アプリのWebViewにShopliveを表示する場合
SDKを使わず、アプリが自前で作ったWebViewにShoplive Webプレーヤーを表示する場合に必要な設定です。
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でそのまま開き続ける例です。
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
}
}
外部ブラウザや新しい画面で開きたい場合は、上の2つの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は引数が2つとも必要です
原文の例にある1引数呼び出し(setKeepPlayVideoOnHeadphoneUnplugged(true))はコンパイルできません。
2番目の引数は「再生を続けるがミュートするか」です。iOSは2番目の引数にデフォルト値があるため省略できます。android.permission.READ_PHONE_STATE権限が必要です
権限がないと通話による割り込みを検知できず、自動再生の設定も機能しません。ACTION_AUDIO_BECOMING_NOISY で受け取り、停止・ミュートを処理します。
このブロードキャストは権限なしで届きます。そのため BLUETOOTH_CONNECT ランタイム権限をリクエストする必要はありません。
Android 12 以上の実機で Bluetooth 切断時のミュートがそのまま動作するかは 要確認です。完了確認
- ミュートをオンにして離れ、再度入ってもミュート状態が維持される
- (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がもう1つあります。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)で、caseが.male("m") ·
.female("f") · .netural("n") です。 neturalは
neutralのタイプミスですが、実際のソースコードのcase名がこうなっています。「直して」例を書くとコンパイルできません。
AndroidはShopLiveCommonUserGender(大文字のLive)で.MALE("m") ·
.FEMALE("f") · .NEUTRAL("n")と綴りが正常です。購入転換データの customフィールド
ミッション10で扱うShopLiveEvent.sendConversionEvent がサーバーに送るリクエストの
ボディには、もう1つのフィールドがあります。呼び名は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 はプレイヤーの Web が送った値をそのまま渡します。以前のドキュメントでは COUPON · BANNER · NOTICE (要確認 — Web プレイヤーの仕様)。通常はこの値で分岐します |
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" の2か所だけであることを直接確認しました。
Android だけが3段階で、iOS・Flutter は2段階と考えれば十分です。
handleChangedPlayerStatus(status: String)
onChangedPlayerStatus(isPipMode: Boolean, playerLifecycle: PlayerLifecycle)
changedPlayerStatus
視聴者の表示名変更
| フィールド | タイプ | 説明 |
|---|---|---|
userId | String | 視聴者の識別子。こちらが渡した値そのままです |
userName | String | 視聴者が新しく指定した表示名 |
iOS・Android とも2つの値が一つの束(Map・JSONObject)に入って来ます。
onSetUserName(_ payload: [String: Any])
onSetUserName(jsonObject: JSONObject)
userInfo → {userInfo: Map}
エラー
| フィールド | タイプ | 説明 |
|---|---|---|
code | String | エラーコード。一覧は エラーコード にあります |
message | String | エラーメッセージ。視聴者にそのまま見せるよりログ用に使ってください |
Android 現行は2つの値が 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 の3つのいずれか |
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 ケース、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 · オプション
プレーヤーの動作を細かく調整するオプションを 役割ごとに まとめてすべて集めました。使い方は各ミッションで扱い、ここでは 何があるかを見ます。コメントに ミッション番号が付いている項目は、そのミッションに使用例があります。
3つのプラットフォームが提供する範囲は同じではありません。特に 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 1つで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)。引数は2つとも必須で、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() の2メソッド
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()
// ルートActivity終了時に配信も一緒に終了するかどうか。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)
// コールバックはメインスレッドで正確に1回届く
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,
)
// コールバックはメインスレッドで正確に1回届く
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 を使います。
2つのコンポーネントは項目構成がかなり異なるため、プラットフォームごとに分けて掲載しています。
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 は右下にウィンドウが表示されます。両プラットフォームを揃えるには値を明示してください。エラーコード
原文ドキュメントの4分類(接続 / 配信入場 / 配信中 / その他)をそのまま維持します。 同じコード番号でもプラットフォームごとに意味が異なることがあります。 番号だけを見てプラットフォームを推測せず、下のコールアウトを必ず読んでください。
iOS ShopLiveErrorCases
サーバー発信のコード(負の値)とSDK内部コード(9000+)が1つの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 2体系に分離
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 1つにまとめている) |
| その他(内部) | 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の2つのリポジトリは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 ケーパビリティをオンにし、App Store 審査提出時に使用目的と再現方法を併せて記載する必要があります。手順は ミッション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項目)のプラットフォーム差異コールアウトを先に確認してください。
エラーコードに関するお問い合わせ
同じコード番号でもプラットフォームごとに意味が異なることがあります。コード値だけでプラットフォームを推測せず、 リファレンスのエラーコードも併せて確認してください。