Shoplive Developers
한국어KOEnglishEN日本語JA

Shoplive V1 モバイルSDK連携ガイド

はじめに

概要 & 動作要件

Shoplive V1 モバイルSDKは、アプリにライブコマースの視聴(Player)を連携します。 ストリーミング・チャット・オーバーレイはSDKが担当し、アプリは配信を表示したあと、ユーザー・商品・クーポンをつなぐだけで済みます。
ボタン一つでフルスクリーンのライブを開く、ディープリンクからの流入、他の画面に移っても小さなウィンドウで見続ける(PIP)、商品・クーポン連携、購入コンバージョン計測までをサポートします。すべてを組み込む必要はなく、自社アプリに 必要なミッションだけを選んで 適用すれば十分です。

すぐに始める →

プラットフォーム最小推奨最新バージョン
iOSiOS 15 · Swift 5 · CocoaPods 1.9 以上—1.9.0
AndroidAndroid 4.4(API 19). shoplive-media3 を使う場合は Android 6.0(API 23)再生エンジン内蔵(ExoPlayer の別途インストール不要 · インストール の再生エンジンモジュールを1つだけ宣言)1.9.0
FlutterDart SDK >=2.17.1 <4.0.0 · Flutter >=2.5.0 · iOS 15.0 · Android minSdk 19 · JDK 17—1.9.01
  1. Flutter shoplive_player 1.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_player 1.9.0 からショートフォーム API とショートフォーム SDK の依存関係が外れました。1.8.x でショートフォーム API を使っていたコードは、アップグレード前に削除する必要があります。
  • Xcode が 26 より低い場合は 正式リリース版のインストールやビルドが失敗することがあります。その場合に使える下位互換ビルドを プラットフォーム付録 にまとめてあります。
1.9.0 は iOS Deployment Target が 15.0 に上がるなど、1.8.x と変わった点があります。上げる前に 1.9.0リリースノートを先に確認してください。

モジュール構成

SDK は役割ごとに分かれています。どの役割が必要かをまず見て、インストール時に使う実際の名前はカード下部で確認してください。Flutter はこれらすべてを1つのパッケージが包んでいるため、選ぶものはありません。

ミッションマップ

実装したいものを下から探して、そのミッションへ直接進んでください。順番どおりに読む必要はありません。ただし、すべてのミッションは インストールとミッション1を終えていることを前提としています。

実装したいことミッション
ボタンを押すとライブが全画面で開くミッション1・プレーヤーを表示する
プッシュ・SMSのリンクをタップすると直接ライブが開くミッション2・ディープリンクで表示する
ログイン済み会員として視聴させる(ゲスト・簡単ログイン・JWT)ミッション3・ユーザー情報の連携
視聴者にチャットをさせるミッション4・チャット
配信中にクーポンを発行するミッション5・クーポン
商品をタップしたら自社の詳細ページに遷移させるミッション6・商品クリック
視聴者に配信リンクを共有させるミッション7・配信リンクの共有
別の画面に移動しても小さいウィンドウで表示し続けるミッション8・PIP
配信前・配信中にプレビューを表示するミッション9・配信プレビュー
購入コンバージョンを計測するミッション10・購入コンバージョン計測
色・動作を自社アプリに合わせて変更するミッション11・UI・動作のカスタマイズ

次へ → インストール

はじめに

インストール

目標プロジェクトにShoplive SDKを追加し、ビルドが正常に通る状態にする
事前準備ビルドが通る iOS・Android・Flutter プロジェクト。プラットフォーム別の正確なバージョンは 動作要件表 にあります

使うプラットフォームの項目だけ見れば十分です。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 2行を両方とも記述する必要があります Podfileにsourceを一度でも記述すると、デフォルトCDNが自動適用されなくなります。 公式CDN行を書き忘れると、Shoplive SDK以外のすべてのpodのインストールが失敗します。
pod名に注意 Shoplive specリポジトリのREADMEではpod 'ShopliveSDK'と案内されていますが、実際のspecにはその名前が 存在しません。ShopLive·ShopliveSDKCommon·ShopliveShortformSDKの3つだけが存在するため、 上の例のようにShopLiveと記述します。

1.9.0からSPM配布リポジトリがshoplive-sdk-ios 一つに統合されました。1.8.x までは ios-sdkとcommon-ios 2つのリポジトリを別々に取得する必要がありましたが、新規に連携するなら統合リポジトリだけで十分です。

  1. XcodeでFile → Add Package Dependencies…を開き、https://github.com/shoplive/shoplive-sdk-iosを入力します。
  2. 依存関係ルールをUp to Next Major Version、基準バージョン1.9.0に指定します。
  3. 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")
        ]
    )
]
このリポジトリは V1 と V3 が共用しています。必ずバージョンを明示してください 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つだけ 入れてください。両方入れるとクラスが重複してビルドが壊れます。

4つすべてを宣言してください 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"
}
Android 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) {
        // リンク遷移処理
    }
})
Android Handler を登録しないとボタンが反応しません プレーヤーは表示されますが、商品をタップしても詳細ページに行かず、クーポンを受け取ってもアプリは気づきません。どのイベントを受け取れるかは イベントハンドラ にまとめてあります。
ProGuard・R8 は手を加える必要がありません 難読化ルールは各ライブラリが自身に含めて配布しているため、依存関係を追加するだけで自動的に適用されます。確認したい場合は プラットフォーム付録 にどのモジュールがどのルールを持っているかを記載しています。

Flutter インストール

flutter pub add 'shoplive_player:^1.9.0'
# pubspec.yaml
dependencies:
  shoplive_player: ^1.9.0
必ずバージョンを付けてください バージョンを付けないと V3(3.x)パッケージがインストールされます。3.x には 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 ではない)が追加されている

前へ ← 概要 & 動作要件次へ → クイックスタート

はじめに

クイックスタート

目標2つのキーだけで配信を画面に表示する
事前準備Shoplive Adminで Access Key・Campaign Key を発行
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にあります。

配信前(UPCOMING)と配信中(LIVE)のプレーヤー画面の比較 1 2 3 4 5 6 7 8
左 · 配信前UPCOMING 右 · 配信中LIVE
LIVEバッジ・視聴者数・クーポンバブル・チャットオーバーレイは配信が始まってから表示されます。そのため番号は右の画面にだけ付けています。
番号要素どこで扱うか
1LIVEバッジSDKが配信状態に応じて自動的に表示します(別途連携不要)
2視聴者数・いいね数SDKが自動的に更新します(別途連携不要)。ミッション11のブランドいいね(フォロー)ボタンとは別の要素です。この数字は表示するだけで、そちらはキャンペーン設定が別途必要です。
3ミュートボタンミッション11
4その他・共有・閉じる(右上アイコン列)共有は ミッション7、画面を離れたときに小さなウィンドウになる動作は ミッション8 で扱います
5クーポンバブルミッション5
6チャットテキストオーバーレイミッション4
7チャットを開く・いいねボタン(右下アイコン列)チャットを開く操作は ミッション4 で扱います。いいねボタンが ミッション11のブランドいいね(フォロー)と同じ機能かどうかはまだ確認できていません(要確認)
8商品カードミッション6
この図にだけ見えて表にない要素があります 上の図は V3 ガイドから持ってきたものです。プレーヤーを描画する WebView を V1 と V3 が共用しているため、画面構成は同じです。ただし図に見える キャンペーンタイトル·お知らせピル·イベントポップアップは V1 でも常に表示されるか確認できていないため、表には入れていません(要確認)。キャンペーン設定によって変わる部分の可能性があるため、連携を始めるときに実際のキャンペーン画面で確認するか、Shoplive の担当者に問い合わせてください。

前へ ← クイックスタート次へ → デモアプリで確認する

画面 & デモ

デモアプリで確認する

Shopliveの組織にはデモリポジトリが複数ありますが、ほとんどがV3用です。V1 で連携しながら V3 デモを開くと API がまったく異なり混乱します。まず下の表で区別してください。

リポジトリプラットフォームSDKそのまま使えるか
shoplive-sdk-flutter FlutterV1(1.9.0 タグ)✅ タグに切り替えてから実行(下記)
ios-sdk-sample iOSV1⚠️ Podfileを直さないとビルドできない(下記)
mobile-sdk-demo iOS · AndroidV3❌ V1 と API が異なり、参考用にも使えません
android-sdk-sampleAndroid— ❌ デモではなくAAR/POM配布リポジトリ
V1 Androidデモアプリは公開されているものがありません 上の表のとおり、Android側でV1のサンプルを含む公開リポジトリは見つかりませんでした。Androidはこのガイドの例をお使いいただき、 実行可能なデモが必要な場合は連携を開始する際にShoplive担当者に依頼してくださいしてください。社内配布版があるかどうかは 要確認です。

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
1.9.0 タグに切り替えてください デフォルトブランチは V3 なので、1.9.0 タグに切り替えないと V3 の例が開きます。

iOS まず Podfile を修正する必要があります

Xcodeプロジェクト(swift/ShopLiveSwiftSample)にはPIP設定・オプション設定・ユーザー情報など 複数のメニューが入っています。ただしリポジトリにコミットされたPodfileはそのままでは使えません。

コミットされたPodfileはビルドできません Shoplive podの行がすべてコメントアウトされており(コメントに書かれたバージョンも1.8.2)、platform :ios, '11.0'のうえ、 Shoplive specリポジトリのsourceも抜けています。インストールのPodfile例(source 2行を含む)に置き換え、バージョンを1.9.0に指定した後pod install --repo-updateを実行してください。

前へ ← 画面用語ガイド次へ → ミッション1・プレーヤーを表示する

ミッション

ミッション1・プレーヤーを表示する

目標Access Keyで初期化し、Campaign Keyで配信を再生する
事前準備インストール完了・Access Key・Campaign Key(発行案内)
  1. アプリ起動時(1回)にAccess KeyでSDKを初期化します。
  2. 視聴開始のタイミングでCampaign Keyを使って再生を呼び出します。
  3. (任意)低画質・ミュートのプレビューで視聴を促すことができます。
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}"));
iOS Objective-C から使う場合 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}");
プラットフォームごとの違い iOS ShopLive.preview(data:completion:)のcompletionはプレビューをタップしたとき 呼ばれます。本再生に切り替えるかはアプリが決めます。旧バージョンの preview(with campaignKey:)はdeprecatedです。 FlutterはsetPreviewOption(オプション)とshowPreview(表示)が分かれているため、 先にオプションを設定する必要があり、閉じるときはhidePreview()を使います。 AndroidはAPI · オプションのsetPreviewOptionsでプレビューウィンドウの形を別途指定します。

完了確認

  • アプリ起動直後にAccess Keyの初期化が1回済んでいる
  • ボタンタップ → 2~3秒以内に配信が再生される
  • 再生失敗時にエラーコールバック(iOS handleError / Android onError)にコード・メッセージが記録される

前へ ← デモアプリで確認する次へ → ミッション2・ディープリンクで表示する

ミッション

ミッション2・ディープリンクで表示する

目標ディープリンクから流入した広告パラメータをSDKに渡し、そのリンクの配信へ直接遷移させる
事前準備ミッション1完了

広告・プッシュ・SMS に付くリンクには通常、 どの配信を開くかとどこから流入したかがクエリとして一緒に載ってきます。アプリがそのリンクで開かれたら、クエリを一つずつ取り出して SDK に渡し、プレーヤーを起動すれば十分です。

このミッションは https://xxxx/yyyy?ak=…&ck=…&utm_source=… の形のリンクを受け取った前提で進めます。クエリは種類ごとに渡し方が違うので、まず下の表でどのクエリをどう処理するか確認してください。

クエリ値SDKへの渡し方
akAccess KeysetAccessKey
ckCampaign Keyプレーヤー実行時にcampaignKeyとして使用
utm_source · utm_medium
utm_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>
iOS・Flutter にはこの手順がありません リンクを 受け取るものまではSDKと関係のないプラットフォーム標準の設定です。iOS は Xcode で URL Types(カスタムスキーム)や Associated Domains(Universal Links)を設定し、Flutter は 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));
  }
}
iOS 引数なしの ShopLive.play() はありません 古いドキュメントではパラメータを設定したあと ShopLive.play() で終わらせるとありますが、そう書くと コンパイルできません。 現在使えるのは play(data:) 一つだけです。リンクから取り出した campaignKey を上の例のように play(data:) に載せて渡してください。
utm の値を addParameter で渡すと統計に反映されません setUtmSource のような専用 setter で入れた値だけが Shoplive 管理画面の統計で流入経路として集計されます。 addParameter で入れると、ただのカスタムクエリとして付くだけです。utm の4つは必ず専用 setter を使ってください。
Android ディープリンクから入ると PIP への切り替えがずれることがあります 上の Android の例が入口を SchemeActivity として別に置き、 FLAG_ACTIVITY_NEW_TASK で次の画面を表示している理由です。なぜそうする必要があるのかと症状は FAQ・トラブルシューティング にまとめてあります。

完了確認

  • ディープリンクでアプリを開くと、リンクのck配信がすぐに再生される
  • utmパラメータ付きのリンクから入った場合、Shoplive管理画面の統計に流入経路が記録される
  • (Android)ディープリンクから入った状態で商品を押してPIPに切り替わっても、アプリ画面が正常に前面に出てくる

前へ ← ミッション1次へ → ミッション3・ユーザー情報の連携

ミッション

ミッション3・ユーザー情報の連携

目標チャット・クリックのログが自社サービスのユーザー基準で蓄積されるようにする
事前準備ミッション1完了・(セキュア認証の場合)バックエンドでの JWT 発行準備 (セキュア認証用の JWT を作る)

プレーヤーに 視聴者が誰なのか を伝えると、チャットに名前が表示され、商品クリック・購入ログがその人単位で蓄積されます。伝えなくても配信は見られますが、視聴者を区別できません。

ユーザー情報を渡すもので、ユーザーを認証するものではありません ログイン(ID・パスワード、ソーシャルログインなど)はすべてアプリ側で行います。setUser·setAuthToken はいまの視聴者が誰かを Shoplive に伝えるだけで、チャット・視聴・統計がその会員に紐づきます。Shoplive はユーザーの資格情報を確認しません。セキュア認証(setAuthToken で渡す JWT)では、その情報が顧客企業のサーバーが発行し改ざんされていないか(署名)を Shoplive が確認でき、簡単認証(setUser)ではその確認なしに情報をそのまま渡します。

ログイン状態で選ぶ — 推奨の設定

ユーザー推奨理由
未ログイン ゲスト 会員情報を渡す必要がありません。表示名だけ欲しい場合は別途指定できます
ログイン (推奨) セキュア認証 — バックエンドが署名した JWT 改ざんできないため、会員ランク別の入場制限・購入連携・正確な視聴者統計に使えます
ログイン(バックエンド作業が難しい場合) 簡単認証 — ID・名前などをアプリから直接渡す 署名がないため、アプリ側で値をすり替えられます。識別・統計には十分ですが、権限の検証には使わないでください
ログアウト・ユーザー情報のリセット ユーザー情報をリセットしてからプレーヤーを再起動 ログアウトした会員のユーザー情報(JWT・プロフィール)が残らないよう消去します。変更は次の再生から反映されるため、プレーヤーを再起動してください
推奨の流れ アプリ起動(未ログイン)→ ゲスト → ログイン中:再生のたびに直前でバックエンドから新しく発行した JWT を受け取り setAuthToken → ログアウト時にユーザー情報をリセット。 変更したユーザー情報は次の再生から反映されます。すでに再生中の場合は、ゲスト → ログインアカウントへの切り替えと同じようにプレーヤーを再起動してください。
iOS 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);
iOS Objective-C から使う場合 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(セキュア認証)を使用してください。salt のポリシーは Shoplive チームと合意した値を推奨します。

セキュア認証用の JWT を作る(顧客企業のバックエンド)

セキュア認証に使う JWT は、Shoplive 担当者から発行された Secret Key で顧客企業のバックエンドが署名します。アプリはログイン直後にバックエンドから JWT を受け取り、setAuthToken に渡すだけです。アプリ側のコードはセキュア認証ユーザーアカウントの連携にあります。

  1. Shoplive 担当者から Secret Key を受け取ります — base64 でエンコードされた文字列です
  2. ログインしたユーザー情報で下記の payload を作り、HS256 で署名します
  3. アプリから要求されるたびに(再生の直前ごとに)JWT を新しく発行して返します。アプリに JWT を保存して使い回さないでください
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();
Secret Key はバックエンドだけに アプリのコードやバンドルに入れると、誰でも取り出して他のユーザーになりすましたトークンを作れます。簡単認証は署名がないため、権限の検証には使えません。

セキュア認証ユーザーアカウントの連携

上でバックエンドが署名した 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}"));
iOS setUserJWT は存在しない API です SDKのコードコメントがShopLiveCommon.setUserJWTを指している場合がありますが、実際にはそのAPIは存在しません。必ずShopLiveCommon.setAuthToken(authToken:)を使用してください。
Android getAuthToken() が2系統あります ShopLive.getAuthToken()はsetAuthToken()で設定した値のみを返します。setUser()で発行されたJWTを読み取るにはShopLiveCommon.getAuthToken()を使用してください。
Flutter setUser は accessKey が必須引数 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}");
});
Flutter は元のガイドにこのイベントがありませんが、SDK には存在します。 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です。
Flutter clearAuth() は iOS では accessKey まで消去します Flutter の 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・チャット

目標チャット権限を確認し、必要であればチャットフォントを自社サービスのトーンに合わせる
事前準備ミッション3完了
  1. Shoplive 管理画面で 配信詳細情報 → チャット権限に入り、ゲストチャットの許可可否を確認します。
  2. ゲストチャットをブロックしている場合、ミッション3のログイン誘導フローがそのまま適用されます。
  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)
Flutter はチャットフォント変更 API を公開していません。チャット UI はネイティブレイヤー(iOS/Android)でレンダリングされるため、フォントを変えるには各ネイティブプロジェクトの設定が必要です。
ライブ配信中はShoplive管理画面からチャットを直接管理できます(メッセージ削除・ユーザー制限など)。

完了確認

  • 管理画面のチャット権限設定とアプリの実際の動作(ゲスト許可の有無)が一致している
  • (フォント変更時)チャット入力欄・送信ボタンに指定したフォントが適用される

前へ ← ミッション3次へ → ミッション5・クーポン

ミッション

ミッション5・クーポン

目標クーポンバブルの3種類のクリックイベント(LINK・Download coupon・User define)に対応するコールバックを実装する
事前準備インストールのHandler登録(Android)・ミッション3完了

クーポンクリックイベントを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",
  );
});
Android パラメータ名は couponStatus です customActionResultの3番目の引数名はcouponStatusです。カスタムアクションなのに名前がクーポン系と同じです。SDK ソースがそう定義しているため、そのまま使います。Flutter の同等の引数名は popupStatusと異なるため、プラットフォームをまたいでコードをコピーする際は注意してください。
User define クーポン設定
User define クーポンに payload を設定する画面

完了確認

  • LINKクーポンをクリックすると、指定した方式(Safari・WebView・外部ブラウザ)で遷移する
  • Download couponをクリックすると、発行結果ポップアップ(SHOW/HIDE/KEEP)が意図どおり表示される
  • User defineをクリックすると、payloadのid・typeがログに正常に出力される

前へ ← ミッション4次へ → ミッション6・商品クリック

ミッション

ミッション6・商品クリック

目標配信画面の商品・カートのクリックをアプリが受け取り、商品詳細・カート画面へ遷移させる
事前準備ミッション5完了・インストールのHandler登録(Android)
  1. 商品一覧で商品やカートボタンを押すと、SDKがCLICK_PRODUCT_DETAIL·CLICK_PRODUCT_CARTコマンドをアプリに伝えます。
  2. 商品・バナーに詳細URLが設定されている場合、同じクリックがhandleNavigationとしても伝えられます。
  3. 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;
  }
});
payloadフィールドはまずログで確認してください コマンドに載って届く商品情報は、キャンペーンに登録した商品データをそのまま渡す構造のため、フィールド構成はサービスごとに異なります。 固定のスキーマを想定せず、実際のpayloadを一度出力してからパースしてください。
iOS 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}");
});
商品を1つ押してもコールバックが2回来ます

詳細 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)")
}
hookNavigationを登録するとhandleNavigationは呼ばれません hook が登録されていると、SDK はそのクロージャだけを呼んですぐ抜けます。 handleNavigation デリゲートはもちろん、リンクを押したあとにプレーヤーを PIP に下げたり閉じたりする動作(setNextActionOnHandleNavigation)も実行されません。どちらか一方だけを使ってください。

完了確認

  • 商品一覧で商品を押すとCLICK_PRODUCT_DETAILがログに記録される
  • カートボタンを押すとCLICK_PRODUCT_CARTが記録される
  • 詳細URLが設定された商品を押すとhandleNavigationでURLが伝えられる
  • (iOS、hookNavigation使用時)handleNavigationの代わりにhookクロージャのみが呼ばれる

前へ ← ミッション5次へ → ミッション7・配信リンクの共有

ミッション

ミッション7・配信リンクの共有

目標視聴中の配信リンクをシステムの共有シートまたはアプリ独自の共有UIに出力する
事前準備ミッション1完了
  1. 共有ボタンが出力するリンク形式(URI SchemeまたはWeb URL)をsetShareSchemeで指定します。
  2. 別途処理しない場合、SDKがOS標準の共有シートを表示します。
  3. アプリデザインに合わせた共有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",
);
iOS 原文の custom:パラメータは存在しません 実際のシグネチャはsetShareScheme(_ scheme: String?, shareDelegate: ShopLivePlayerShareDelegate?)です。 クロージャを渡すcustom:というラベルはSDKになく、そのまま使うとコンパイルできません。 標準の共有シートを使う場合はshareDelegate: nilを渡してください。
iOS scheme と shareDelegate の両方に nil を渡すと何も起きません SDKは警告ログを出すだけで即座にreturnするため、共有設定がまるごと無視されます。少なくともどちらか一方を指定してください。

標準のシステム共有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()
}
Android handleShareの2番目の引数はStringではありません 原文にはhandleShare(context: Context, shareUrl: String)と書かれていますが、そのようなシグネチャは存在しません。 実際はhandleShare(context: Context, data: ShopLivePlayerShareData)で、 data.url以外にdata.campaign(campaignKey·title·description·thumbnail)も一緒に届きます。
Flutterには共有データをアプリに渡すストリームがなく、setShareSchemeでリンク形式のみを指定し、 共有UIは各ネイティブの標準共有シートをそのまま使います。
共有されたリンクをタップしてアプリが開いたときに配信へつなげる処理はミッション2で扱います。

完了確認

  • プレーヤーの共有ボタンを押すと、指定したscheme・URLが入った共有シートが表示される
  • 共有されたリンクにak·ckクエリがそのまま含まれている
  • (カスタムUI使用時)標準の共有シートの代わりにアプリの共有画面が表示される

前へ ← ミッション6次へ → ミッション8・PIP

ミッション

ミッション8・PIP

目標配信を見ながら別の画面に移動しても、小さいウィンドウで再生を続ける
事前準備ミッション1完了

PIP は2種類あります。 アプリ内PIP は自社アプリ内だけに浮かぶ小さなウィンドウで、 OS PIP はホーム画面や他のアプリの上まで上がるシステムウィンドウです。作る主体が違うため設定も別々です。プラットフォームごとに使える範囲も異なります。

プラットフォームアプリ内PIPOS PIP
iOSサポートサポート(プロジェクトでBackground Modesの有効化が必要)
Android廃止。 呼び出しても動作しませんサポート(Android 8.0以上)
Flutter専用オプションなし。切り替え API のみで、実際の動作は各ネイティブに従います
iOS アプリ内 PIP ウィンドウと プレビューウィンドウは同じウィンドウです 位置・サイズ・角・閉じるボタンの設定を2つの機能が共有します。片方で合わせればもう片方にもそのまま適用されるため、同じ値を2回入れる必要はありません。一方 OS PIP は OS がアプリの外に描くウィンドウなので全く別物で、設定も別々です。Android はアプリ内 PIP 自体が廃止されており比較する窓がなく、プレビューは 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 · heightCGFloat30ボタンのサイズ
offsetX · offsetYCGFloat3角からの距離
colorUIColor.whiteボタンの色
shadowOffsetX · shadowOffsetYCGFloatなし影のオフセット
shadowBlurCGFloatなし影のぼかし半径
shadowBlurStyle.normal · .solid · .inner · .outerなし影のぼかしスタイル
shadowColorUIColorなし影の色
imageStrStringなしボタン画像の URL
iOS PIP をオフにすると小さくなるのではなくプレーヤーが閉じます setEnabledPictureInPictureMode はデフォルト値が true です。これを false にすると、PIP への切り替えが要求されたときに小さなウィンドウへ下がる代わりに プレーヤーが終了します。 「PIP 機能だけをオフにしてフルスクリーンは維持」ではないので注意してください。
Android アプリ内 PIP は廃止されました showInAppPip()·hideInAppPip() を呼び出しても何も起きず、 isInAppPipMode() は常に false を返します。 setEnabledPictureInPictureMode(isEnabled, useInAppPip) の2番目の引数も無視されます。Android で小さなウィンドウが必要なら下の OS PIPを使用してください。
Flutter 設定はプレビューと一体です アプリ内 PIP 専用の API はなく、 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 に下がりません。

Xcode Background Modes設定
Xcode で Background Modes をオンにした状態
ShopLive.setEnabledOSPictureInPictureMode(isEnabled: true)
OS PIPもsetEnabledPictureInPictureModeがオンになっている必要があります 2つのフラグはAND条件です。setEnabledPictureInPictureMode(isEnabled: false)のままにしておくと、setEnabledOSPictureInPictureMode(isEnabled: true)を呼び出してもOS PIPコントローラーは作られません。 「アプリ内PIPだけオフにしてOS PIPは使う」という設定はできません。
iOS アプリ審査に説明を添えて提出する必要があります OS PIP を使うとバックグラウンドオーディオ権限が有効になるため、App Store 審査に なぜこの権限が必要なのかと レビュアーが 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 Manifest に追加するものはありません プレーヤーActivityのandroid:supportsPictureInPicture="true"と SYSTEM_ALERT_WINDOW権限はSDKモジュールのManifestにすでに宣言されており、Manifestマージによってアプリに反映されます。
Android 「他のアプリの上に表示」権限を事前に取得することを推奨します バックグラウンドから画面を表示する制限(BAL)に引っかかり、PIP切り替えが失敗することがあります。 下のヘルパーでオーバーレイ権限を先に取得しておくと、この問題を避けられます。
ShopLivePermissionManager.request(this, ShopLivePermissionType.DRAW_OVERLAY) { hasPermission ->
    // hasPermission == true ならオーバーレイ権限が許可された状態です
}

このヘルパーは別モジュールのため、使うには依存関係の追加が必要です。

// app/build.gradle
implementation "cloud.shoplive:shoplive-permission:$shoplive_sdk_version"
Android PIP が使えない端末・設定では黙って素通りしません OS PIP自体がAndroid 8.0(API 26)以上の機能のため、それより下のバージョンで切り替えを要求すると案内ダイアログが表示されるか、 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();
Android 引数なしの startPictureInPicture()は非推奨です startPictureInPicture(shouldDelayForOverlayPermissionFlow: Boolean)を使ってください。 trueを渡すと、権限案内画面が片付く時間を少し置いてからPIPに切り替わります。
FlutterのPIPはネイティブの動作にそのまま従います プラグインは iOS ではアプリ内 PIP を、Android では OS PIP を呼び出します。同じ Dart コードでも2つの OS で結果の画面が異なります。

iOS 専用 PIP 移動領域と余白

アプリ内PIPが下部タブバーや必須ボタンを覆わないよう、移動できる領域とPIP自体の余白を制限できます。

アプリのタブバー
offset.top offset.bottom
LIVE
padding
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))
2つの関数は Bool を返します。戻り値を必ず確認してください オフセットとパディングを合わせた領域が画面の縦・横を超える場合、値は適用されずfalseを返します。 無視すると、設定が静かに反映されないまま動作します。

完了確認

  • (iOS)プレーヤーで下にスワイプするとアプリ内の小さいウィンドウに切り替わり、音声が途切れない
  • (iOS)Background Modesをオンにしてホームに出ると、システムPIPウィンドウで再生が続く
  • (Android)ホームに出ると、指定した比率のOS PIPウィンドウに切り替わる
  • startPictureInPicture·stopPictureInPicture呼び出しで切り替え・復帰が動作する
  • (iOS、オフセット設定時)2つの関数の戻り値がtrueで、PIPが指定した領域の外に出ない

前へ ← ミッション7次へ → ミッション9・配信プレビュー

ミッション

ミッション9・配信プレビュー

目標プレーヤーに入らなくても小さいウィンドウで配信をミュート再生し、視聴への導線を作る
事前準備ミッション1 完了。Access Key が先に設定されている必要があります

プレビューはデフォルトがミュートで、ウィンドウをタップすると全画面プレーヤーに切り替えられます。 AndroidではActivityの上に表示するPopup Previewが推奨方式です。

iOS アプリ内PIPウィンドウとプレビューウィンドウは同じウィンドウです 位置・サイズ・角・閉じるボタンの設定を2つの機能が共有します。片方で合わせればもう片方にもそのまま適用されます。アプリの外に出る OS PIPは別物であり、設定も別々です。Android はアプリ内 PIP が廃止されており 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 であらかじめ入れておけば十分です。

Android 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")
Android 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()
        }
    }
}
Android ShopLivePreviewビューのstart()とprepare()は例外をスローすることがあります accessKeyとcampaignKeyが設定されていない状態で 引数なしのstart()またはprepare()を呼び出すと ShopLiveCommonErrorが発生します。
Android RecyclerView内で使う場合 ViewHolderがattachになったときpreview.start()を、detachになったときは必ず preview.release()を呼び出してください。セルが再利用されるのに前のインスタンスをrelease()しないと、 画面外にスクロールしたプレビューが再生状態のまま残ってしまいます。
Android このバナーも上で保存しておいたポップアップのサイズを静かに上書きすることがあります バナービューとPopup Previewは内部的に同じShopLivePreviewクラスです。ポップアップもこのビューをインフレートして表示する wrapper にすぎません。画面比を受信すると(内部コールバック、バナー・ポップアップの区別なく同じように実行)、そのサイズを ShopLive.setPreviewOptions で保存しておいた値の width/height にそのまま書き戻します。保存された値があるときだけ起きる一方向の副作用です。ポップアップ自身も再生中に同じ経路を通りますが、それは自分の値を書き直すだけなので目立たず、 バナーのようにポップアップUIなしで表示され続けるインスタンスがあると、ユーザーが気づかないうちに 保存値が変わっていることがあります。

完了確認

  • プレビューウィンドウがミュート状態で表示され、配信映像が再生される
  • ウィンドウをタップすると全画面プレーヤーに切り替わる
  • 閉じるボタンまたは画面外へのスワイプでプレビューが終了する
  • (Android)プレーヤー実行中にプレビューを要求しても重複して表示されない
  • バナー型プレビュービュー(iOS ShopLivePlayerPreview · Android ShopLivePreview)がリスト内でもスクロールに合わせて正しく開始・解放される

前へ ← ミッション8次へ → ミッション10・購入コンバージョン計測

ミッション

ミッション10・購入コンバージョン計測

目標購入が確定した時点でコンバージョンイベントを送り、「この配信を見て購入した」を記録する
事前準備ミッション3 完了。視聴した人と購入した人を同じユーザーとして紐づける識別子が先に設定されている必要があります

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タイプ説明
typeString?購入コンバージョンは"purchase" · Androidは省略不可
productsList<ShopLiveConversionProductData>?購入した商品一覧・Androidは省略不可
orderIdString?注文番号
referrerString?流入経路
customMap<String, Any?>?一緒に残す値(例: campaignKey)
ShopLiveConversionProductDataタイプ説明
productIdString?Shoplive固有の商品ID
customerProductIdString?クライアント固有の商品ID
skuString?商品SKU
urlString?商品URL
purchaseQuantityInt?購入数量
purchaseUnitPriceDouble?購入単価
Android typeとproductsは省略できません この2つの引数だけデフォルト値がないため、渡す値がなくてもnullを明示しないとコンパイルできません。 残り(orderId·referrer·custom)とShopLiveConversionProductDataのすべての引数にはデフォルト値があるので省略できます。iOSはすべてデフォルト値があるため、必要なものだけ渡せばよいです。
orderIdは数値ではなく文字列です 原文の表にはInt?と書かれていますが、実際の定義はiOS・AndroidともにString?です。 注文番号が数字だけで構成されていても文字列として入れてください。
Android purchaseUnitPriceに整数リテラルを使うとコンパイルできません 型がDouble?なのに、Kotlinは1000をDouble に自動変換しません。 1000.0と書いてください。
Flutter 購入コンバージョンイベント API がありません Flutter shoplive_player 1.9.0 には sendConversionEvent · setAnonId がありません。 以前のドキュメントの Dart の例(ShopLiveEvent.sendConversionEvent)はコンパイルできません。 必要な場合は、iOS · Android のネイティブコードから上の例を直接呼び出してください(プラットフォームチャネル)。
デモアプリはテストのため商品クリック時点で送信します 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}")
Flutter 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・動作のカスタマイズ

目標音・ローディング画面・リンク遷移後の動作など、プレーヤーの細かい動作を自社サービスに合わせて変更する
事前準備ミッション1 完了。ここに出てくる設定はすべて任意なので、必要なものだけ選んで使えば十分です

ミュート状態の維持

ユーザーがミュートをオンにしたままプレーヤーを離れ、再度入ると、デフォルト値(音オン)に戻ります。 イベントコールバックでミュート状態を受け取っておき、次の再生時に戻してください。

// 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)
iOS 引数ラベル actionType:を忘れないでください 原文の例のShopLive.setNextActionOnHandleNavigation(.PIP)はコンパイルできません。 実際のシグネチャはsetNextActionOnHandleNavigation(actionType: ActionType)です。
Flutter 同じ名前のメソッドがありますが設定が反映されません ShopLivePlayerにsetNextActionOnHandleNavigationがあり、オートコンプリートにも出てきますが、 渡した値がネイティブまで届きません。Dartは'show'·'hide'·'keep' 文字列を送りますが(ShopLiveActionType.parseValue()の戻り値の型がStringです)、 ネイティブ側はこの値を整数として読み取ります。そのため実際には Androidは何を渡しても常にPIPとして動作し、iOSは何も起こりません(デフォルトのPIPを維持)。 Dartの列挙型がshow·hide·keepという、上の表の PIP·KEEP·CLOSEと名前からして違うものになっているのも同じ問題の表れです。 Flutterでこの動作が必要な場合はShoplive担当者に問い合わせてください。
iOS hookNavigationを使うとこの設定は無視されます ミッション6のhookNavigationを登録すると、SDKはそのクロージャのみを呼び出し、 リンク遷移後の動作をスキップします。両方の方式を同時に期待しないでください。

ブランドいいね(フォロー)

プレーヤー内にブランド画像といいね(フォロー)ボタンを表示し、その状態をアプリが持つユーザーデータと合わせます。

まずShopliveとの協議が必要です キャンペーン側でこの機能をオンにしてもらわないとボタンは表示されません。 ask@shoplive.cloudでお問い合わせください。

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で渡してください。
いつ送るべきかは確認できていません。 要確認 このコマンドはプレーヤーが実行された後に送る します。それより前に呼び出すと静かに破棄されます。iOS はプレーヤーの WebView がまだない場合は送信自体をスキップし、Android は初期化前なら「not initialized」のログだけ残して戻ります。 どちらもエラーコールバックが来ないため、失敗しても気づきにくいです。 ただしどのコールバック以降が安全かは確認できていません。 連携を始めるときに Shoplive の担当者と一緒に確認し、それまでは実際のキャンペーンで反映されているか目視で確認してください。

2) ボタンが押されたときに受け取って処理する。 ミッション6と同じコマンドコールバックで届きます。 フォローと解除は同じイベントです。コマンド名が別にあるわけではなく、 favoriteというbooleanが1つ乗ってくるだけです。 アプリがすべきことは、自分側のフォロー処理を行った後、その結果確定した状態を SET_BRAND_FAVORITEで送り返すことです。

ON_CLICK_BRAND_FAVORITE_BUTTON payloadタイプいつ届くか
identifierString常に
favoriteBool常に来ます。ただし クリック直前の状態かクリック後の状態かはキャンペーンテーマによって異なります(下の注意参照)
brandString(ブランド名)一部のキャンペーンテーマのみ。この値が無いこともあるので依存しないでください
nameStringbrandと同じ条件・同じ値
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を送らなくても画面は正しいままです。 バックエンド処理が失敗して元に戻す必要がある場合など、必要なときだけ送り、その際も絶対値として送ってください。
Android payload を JSON 文字列として渡す方は非推奨です 原文のsendCommandMessage("SET_BRAND_FAVORITE", "{...}")は非推奨と表示されています。 Mapを受け取る方を使ってください。非推奨メッセージが指す ShopLiveCommon.sendCommandMessage は実際には存在しません。 ShopLiveの方が正しいです。
iOS 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>
色と画像アニメーションは同時には適用されません Androidはどちらか一方を設定すると、もう一方がリセットされ最後に呼び出した方だけが残ります。 iOSは画像を1枚でも入れると画像アニメーションが優先され、色の設定は使われません。 どちらか一方だけを使ってください。

通話・イヤホン割り込み後の動作

再生中に電話がかかってきたり、イヤホンが外れたりすると映像が停止します。割り込みが終わった後の動作を選びます。

// イヤホンが抜けても再生を継続 (デフォルト 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)
通話終了後の自動再生のデフォルト値がプラットフォームごとに異なります iOSはfalse(停止したまま維持)、Androidはtrue(自動再生)です。 両プラットフォームを揃えたい場合は両方で明示的に設定してください。
Android setKeepPlayVideoOnHeadphoneUnpluggedは引数が2つとも必要です 原文の例にある1引数呼び出し(setKeepPlayVideoOnHeadphoneUnplugged(true))はコンパイルできません。 2番目の引数は「再生を続けるがミュートするか」です。iOSは2番目の引数にデフォルト値があるため省略できます。
Android 通話検知には android.permission.READ_PHONE_STATE権限が必要です 権限がないと通話による割り込みを検知できず、自動再生の設定も機能しません。
Android イヤホン切断の処理に権限は不要です SDK はイヤホン(Bluetooth を含む)の切断をシステムブロードキャスト ACTION_AUDIO_BECOMING_NOISY で受け取り、停止・ミュートを処理します。 このブロードキャストは権限なしで届きます。そのため BLUETOOTH_CONNECT ランタイム権限をリクエストする必要はありません。 Android 12 以上の実機で Bluetooth 切断時のミュートがそのまま動作するかは 要確認です。

完了確認

  • ミュートをオンにして離れ、再度入ってもミュート状態が維持される
  • (iOS・Android)商品リンクを押したとき、設定した通りPIP・維持・終了のいずれかで動作する
  • (ブランドいいね使用時)ボタンを押すとON_CLICK_BRAND_FAVORITE_BUTTONが届き、送り返した状態がボタンに反映される
  • ローディングインジケーターが指定した色または画像アニメーションで表示される(両方同時に設定していない)
  • イヤホンを抜いたり、電話に出て切ったりしたとき、設定した通りに再生・停止する

前へ ← ミッション10

リファレンス

データフィールド辞典

再生・プレビュー API をやり取りする際に実際に受け渡すデータ型のフィールドを網羅してまとめます。同じ概念でもプラットフォームごとにフィールド構成が異なる箇所があり、表にそのまま残しました。空欄は「まだ書いていない」ではなく そのフィールドがそのプラットフォームには存在しないという意味です。

PlayerData: 再生を開始するときに渡すデータ

iOS ShopLivePlayerData

フィールドタイプnullable意味
campaignKeyStringいいえ配信識別キー。コンストラクタ必須引数
keepWindowStateOnPlayExecutedBoolいいえ(デフォルトtrue)play実行時に既存ウィンドウの状態を維持するかどうか
referrerString?はい流入経路
campaignHandler((ShopLivePlayerCampaign) -> ())?はいキャンペーン情報コールバック(play/preview共通)
brandHandler((ShopLivePlayerBrand) -> ())?はいブランド情報コールバック(play/preview共通)
isEnabledVolumeKeyBoolいいえ(デフォルトfalse)ボリュームキー使用を許可するかどうか

Android ShopLivePlayerData

フィールドタイプnullable意味
campaignKeyStringいいえコンストラクタ必須引数
keepWindowStateOnPlayExecutedBooleanいいえ(デフォルトtrue)iOSと同じ概念、別途setterあり
referrerString?はい別途setterあり
resizeModeShopLiveResizeMode?はいiOSのPlayerDataにはないフィールド。値: AUTO / CENTER_CROP / FIT

Flutter ShopLivePlayerData

フィールドタイプnullable意味
campaignKeyStringいいえコンストラクタ必須引数
keepWindowStateOnPlayExecutedboolいいえ(デフォルトfalse)play実行時に既存ウィンドウの状態を維持するかどうか
referrerString?はい流入経路
Flutter keepWindowStateOnPlayExecuted のデフォルト値がネイティブと逆です Flutter はデフォルト値が false なので、値を渡さないとネイティブのデフォルト(true)と逆に毎回フルスクリーンで開きます。 同じにするには true を明示してください。
Androidのメインプレーヤーは、Brand情報を構造化されたコールバックとして受け取れません iOSはPlayerData自体にcampaignHandler·brandHandler クロージャがあり、メイン(フルスクリーン)プレーヤーでも型付きの Campaign+Brand コールバックを受け取れます。Android は異なります。型付きの Brand コールバックは Preview専用(ShopLivePreviewDataのonCampaignListener)で、メインプレーヤーのキャンペーン情報は PlayerDataではなくグローバルなShopLiveHandler.onCampaignInfo(JSONObject)(生JSON)として届き、 ブランド情報はメインプレーヤー経路に対応物が一切ありません。「ブランド情報をメインプレーヤーでも 構造化されたコールバックとして受け取れる」と案内すると、Android基準では誤った記述になります。

Campaign (ShopLivePlayerCampaign)

iOS

フィールドタイプnullable意味
titleString?はいキャンペーンタイトル
campaignStatusShopLivePlayerCampaignStatusいいえ(デフォルトREADY)3つの値のみ 来ます(READY・ONAIR・CLOSED)。マッチしない文字列はすべて READY に落ちます

Android

フィールドタイプnullable意味
titleString?はい同一概念
campaignStatusShopLivePlayerCampaignStatus?はい4つの値: READY / ONAIR / CLOSING / CLOSED
iOSのenumにはCLOSINGがありません Androidのサーバー状態値には、iOSにはないCLOSINGがもう1つあります。iOSは一致しない文字列をすべて READYに落とすため、サーバーが"CLOSING"を返すと、iOSクライアントはこれを READY(配信待機中)と誤認する構造です(コード上そう動作することを確認済み)。実際にサーバーがiOS 側にもCLOSINGを返すかはサーバー契約の問題のため要確認です。

Brand (ShopLivePlayerBrand)

再生中のコールバックで来るブランド情報です。iOS・Android のフィールド構成は完全に同じです。上の PlayerData の項目で触れたとおり Androidはこの型をメインプレーヤーのコールバックとして受け取れず、Preview専用です。

フィールドタイプnullable意味
nameString?はいブランド名
identifierString?はいブランド識別子
imageUrlString?はいブランド画像URL
iOSショートフォームには別の型(BrandData/SLBrandModel、フィールド構成が異なる)があります。この表は Live再生用のShopLivePlayerBrand限定であり、ショートフォームのBrandは範囲外です。

CommonUser (ShopLiveCommonUser)

iOS・Androidともに同じ名前(ShopLiveCommonUser)を使います。6つのフィールドが1:1対応し、値も同じですが、 enum の型名表記とケースの綴りがプラットフォームごとに異なります。 コード例を書くときは以下の表記をそのまま写してください。詳しい連携方法は ミッション3参照。

フィールドタイプnullable意味
userIdStringいいえ必須
userNameString?はい
ageInt?はい
genderiOS ShopliveCommonUserGender? / Android ShopLiveCommonUserGender? / Flutter ShopLiveCommonUserGender?(MALE · FEMALE · NEUTRAL)はい値は同一(m/f/n)。下のコールアウト参照
userScoreInt? / Flutter int?はい-100〜100 はサーバーのルール(SDK は検査しない)
customiOS [String: Any]? / Android Map<String, Any?>?(デフォルト値は空のマップ) / Flutter Map<String, dynamic>?はい1KB 以下はサーバーのルール(SDK は検査しない)
Flutter gender を空にするとプラットフォームごとに送り方が異なります Flutter で gender を空にすると、Android は NEUTRAL を送ります。iOS は送りません。
iOSのgender enumのcaseはタイプミスも含めてそのまま使います 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
AndroidShopLiveConversionDataオブジェクトを、丸ごと(type・products・orderId・referrer・custom すべて)が JSON 文字列として入ります (Gson().toJson(data))ShopLiveEvent.kt:22
名前は同じですが入る内容が異なります iOSは呼び出し側が渡した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 が登録された 商品・バナーを押すか、LINK 系クーポンを押したとき。
受け取るもの
フィールドタイプ説明
urliOS URL · Android·Flutter Stringその商品・バナー・クーポンに登録しておいた詳細ページのアドレス

商品かバナーかクーポンかは URL で判別する必要があります。 リンク形式をあらかじめ決めておくと分岐が楽になります。

することURL を見て自社アプリの該当画面へ送ります。 3プラットフォームを通じて唯一の必須実装です(iOS は optional ではなく、Android は abstract)。例は ミッション6.
handleNavigation(with url: URL)
handleNavigation(context, url: String)
handleNavigation → {url}

クーポン発行リクエスト

いつ視聴者が Download 系クーポンボタンを押したとき。プレーヤーは発行を自分で行わず、アプリに渡します。
受け取るもの
フィールドタイプ説明
couponIdString管理画面で指定したクーポン識別子
結果コールバックiOS ShopLiveCouponResult
Android ShopLiveHandlerCallback
処理結果を返すためのオブジェクト。渡す値は 結果の返信 にまとめています
すること自社バックエンドに発行を依頼したあと 必ずそのコールバックで結果を返信してください。返信しないとプレーヤーのローディング表示が解除されません。返信方法は下の クーポン・カスタムアクションの処理結果の返信、例は ミッション5.

iOS 引数構成が異なる旧バージョンのオーバーロード2つが deprecated です。以下のシグネチャが現行です。

handleDownloadCoupon(with couponId: String, result: (ShopLiveCouponResult) -> Void)
handleDownloadCoupon(context, couponId: String, callback: ShopLiveHandlerCallback)
handleDownloadCoupon → {couponId}

カスタムアクションポップアップのクリック

いつ管理画面で User define で作ったポップアップを視聴者が押したとき。クーポンではない任意の動作をアプリに任せたいときに使います。
受け取るもの
フィールドタイプ説明
idStringポップアップ識別子。結果を返信するときにそのまま返します
typeStringポップアップの種類を表す文字列です。SDK はプレイヤーの Web が送った値をそのまま渡します。以前のドキュメントでは COUPON · BANNER · NOTICE (要確認 — Web プレイヤーの仕様)。通常はこの値で分岐します
payloadiOS Any? · Android JSONObject
Flutter String
管理画面に入れておいた値
結果コールバックiOS ShopLiveCustomActionResult
Android 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}

配信状態変更

いつ配信が 開始・終了するとき。視聴中に配信状態が変わるたびに来ます。
受け取るもの
フィールドタイプ説明
campaignStatusStringREADY(配信前)・ ONAIR(配信中)・ CLOSED(終了)

詳しい値の説明は データフィールド辞典 の Campaign 項目にあります。

すること配信が終わったときに自社画面を片付けたり、配信前・配信中で別のバナーを出したりするのに使います。
handleChangeCampaignStatus(status: String)
onChangeCampaignStatus(context, campaignStatus: String)
changeCampaignStatus → {campaignStatus}

配信情報

いつプレーヤーがキャンペーン情報をサーバーから受け取った直後、 再生が始まる前に一度.
受け取るもの
フィールドタイプ説明
campaignInfoiOS [String: Any] · Android JSONObject
Flutter Map
サーバーが返したキャンペーンのメタデータ一式

SDK が定めた固定フィールドではなく、サーバー応答をそのまま渡す通り道なので、キャンペーン設定によって構成が変わります。

すること特定のフィールドが常にあると仮定せず、実際の payload を一度出力してから必要な値だけ取り出してください。
handleCampaignInfo(campaignInfo: [String: Any])
onCampaignInfo(campaignInfo: JSONObject)
campaignInfo → {campaignInfo: Map}

プレーヤー画面の生成・消滅

いつプレーヤー画面が 作られるときと消えるとき。配信状態ではなく 自社アプリ画面の寿命です。
受け取るもの
フィールドタイプ説明
status (iOS·Flutter)
playerLifecycle (Android)
iOS·Flutter String
Android PlayerLifecycle
下の値の表を参照
isPipModeBoolean (Android 専用)今 PIP なら true、フルスクリーンなら false
することプレーヤーを表示・終了するときに自社アプリ側の状態を合わせるのに使います。たとえば閉じるときに下部タブを戻す、といった具合です。
値意味iOSAndroid
CREATEDプレーヤー画面が作られた来る来る
CLOSING閉じている最中来ない来る
DESTROYED完全に閉じた来る来る
Flutter 「閉じている最中」を区別できません Flutter の 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

視聴者の表示名変更

いつ視聴者がチャット欄で 表示名を自分で変更したとき.
受け取るもの
フィールドタイプ説明
userIdString視聴者の識別子。こちらが渡した値そのままです
userNameString視聴者が新しく指定した表示名

iOS・Android とも2つの値が一つの束(Map・JSONObject)に入って来ます。

すること自社サービスの会員情報と合わせる必要があるなら、ここで受け取ってバックエンドに反映します。何もしなくてもプレーヤー内では変更後の名前で表示されます。ユーザー設定は ミッション3.
onSetUserName(_ payload: [String: Any])
onSetUserName(jsonObject: JSONObject)
userInfo → {userInfo: Map}

エラー

いつ再生・入場・認証が失敗したとき。誤ったキャンペーンキー、期限切れトークン、ネットワーク障害などがすべてここに来ます。
受け取るもの
フィールドタイプ説明
codeStringエラーコード。一覧は エラーコード にあります
messageStringエラーメッセージ。視聴者にそのまま見せるよりログ用に使ってください

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}

プレーヤー内のボタンクリック (コマンド)

いつ視聴者がプレーヤー内で何かを押したとき。商品クリック・カート・ブランドのいいねなどがすべてここに来ます。
受け取るもの
フィールドタイプ説明
commandString何を押したか。例: CLICK_PRODUCT_DETAIL · CLICK_PRODUCT_CART
dataiOS [String: Any]? · Android JSONObject
Flutter Map
押した対象の情報

data はキャンペーンに登録した商品データをそのまま渡すため、 サービスごとにフィールドが異なります。 固定スキーマを前提にしないでください。

することcommand 名で分岐して処理します。クーポン・ログイン要求など他のコマンドも同じコールバックに来るので、必ず分岐してください。例は ミッション6、いいねの処理は ミッション11.

iOS handleReceivedCommand(_:with:) は deprecated です。どちらも @objc optional なので誤った方を実装してもビルドは通ります。値が来ないときはまずシグネチャを確認してください。

handleReceivedCommand(_ command: String, data: [String: Any]?)
onReceivedCommand(context, command: String, data: JSONObject)
receivedCommand → {command, data: Map}

iOS 専用 画面遷移の追跡

いつプレーヤー画面が フルスクリーン ↔ PIP ↔ 終了 へ移るとき。遷移の直前と直後にそれぞれ来ます。
受け取るもの
フィールドタイプ説明
commandString遷移名。下の現行6種類・レガシー4種類のいずれか
payloadAny?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種類を使ってください。

すること画面遷移に合わせて自社アプリの UI を付けたり外したりする用途です。 上のコマンド受信とはまったく別のイベントなので混同しないでください。あちらは視聴者のクリック、こちらは画面遷移です。

Android Flutter に対応イベントはありません。Android は onChangedPlayerStatusのisPipMode で似たことは分かりますが、遷移の直前・直後の区別はありません。

handleCommand(_ command: String, with payload: Any?)

iOS 専用 PIP ウィンドウのドラッグ

いつ視聴者がアプリ内 PIP ウィンドウを指でドラッグしている間ずっと。
受け取るもの
フィールドタイプ説明
stateUIGestureRecognizer.Statebegan · changed · ended などジェスチャーの進行状態
positionCGPointPIP ウィンドウ中心の画面座標
することPIP の位置に合わせて自社アプリの他の UI をよけさせるなど、座標が必要なときだけ使えば十分です。ほとんどの場合は実装不要です。

Android Flutter に対応イベントはありません。画面座標を扱う iOS 固有のイベントです。

playerPanGesture(state: UIGestureRecognizer.State, position: CGPoint)

iOS 専用 プレビューウィンドウのタッチ

いつ視聴者がプレビューウィンドウを押したとき。
受け取るもの何も値はありません。「押された」という事実だけが来ます。
することミッション9のpreview(data:completion:) の completion と役割が重なります。フルスクリーンへの遷移は通常 completion 側で処理するため、このイベントは別途ログを取るなどの用途にとどめてください。

古いドキュメントにはなかったイベントです。 Android Flutter には対応がありません。

handlePreviewTouchGesture()

動作ログ

いつプレーヤー内で何かが表示されたり押されたりするたびに。分析・統計用にすべて流れ出ます。
受け取るもの
フィールドタイプ説明
nameString何が起きたか
featureStringclick · show · action の3つのいずれか
campaignKeyString? · Flutter String(null ではない。ない場合は空文字列)どの配信で起きたことか
payloadMap · 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}

共有ボタンのクリック

いつ視聴者がプレーヤー右上の共有ボタンを押したとき。
受け取るもの
フィールドタイプ説明
urlString?共有するリンク。形式は setShareScheme であらかじめ決めておきます
campaignShopLivePlayerShareCampaign?campaignKey · title · description · thumbnail
することアプリのデザインに合わせた共有画面を出したいときだけ 実装してください。実装しなければ SDK が OS 標準の共有シートを表示します。例は ミッション7.

受け取る場所がプラットフォームごとに異なります。 iOSはShopLiveSDKDelegate ではなく ShopLivePlayerShareDelegate という別のプロトコルです。 AndroidはShopLiveHandler の中に他のイベントと一緒にあります。 Flutter は対応ストリームがないため、標準の共有シートのみ使えます。

ShopLivePlayerShareDelegate.handleShare(data: ShopLivePlayerShareData)
handleShare(context, data: ShopLivePlayerShareData)

Android専用 視聴者がプレーヤーを閉じた

いつ視聴者が フルスクリーンプレーヤーを直接閉じたとき。
受け取るもの
フィールドタイプ説明
isPipModeBoolean閉じたときに PIP 状態だったなら true、フルスクリーンだったなら false
することAndroidでフルスクリーン終了を捉えるにはこのコールバックを使う必要があります。 下の 画面が閉じた(onClose)は PIP ウィンドウの閉じのみを扱い、フルスクリーン終了は除外します。

古いドキュメントにはなかったイベントです。 iOSはsetCloseHandler がフルスクリーン終了まで包含するため、このコールバックは別途ありません。 Flutter には対応がありません。

onUserCloseAction(isPipMode: Boolean)

画面が閉じた (理由付き)

いつPIP ウィンドウやプレビューウィンドウが閉じるとき。画面インスタンスごとに 正確に一度 来ます。PIP ↔ フルスクリーンのように モードが変わるのは閉じではなく 遷移なので来ません。
受け取るもの
フィールドタイプ説明
typeShopLivePresentType閉じた画面がアプリ内 PIP か OS PIP か。iOS のクロージャは reason だけを受け取り、画面の種類は reason.presentType で取り出します
reasonShopLiveViewHiddenActionType閉じた理由。値は下の表にあります
すること閉じた理由によって処理を変えたいときに使います。たとえば配信終了で閉じた場合と視聴者が自分で閉じた場合を区別して別の画面を出せます。関連: ミッション9.

iOS このイベントだけが ShopLiveSDKDelegate の外にあります。 ShopLive.setCloseHandler という別の API で受け取り、 handleShare と同じ方式です。 プレビューが閉じたときだけ受け取るには ShopLive.setPreviewCloseHandler(_:) を使います。プレビューが閉じると setCloseHandler と一緒に呼ばれます。

3つのプラットフォームが知らせる「閉じた」の範囲が異なります

同じイベントに見えますが、捉える画面がそれぞれ異なります。フルスクリーン終了を捕まえようとして取りこぼしやすい箇所です。

プラットフォーム知らせる閉じ方これでは捉えられないもの
iOS setCloseHandlerフルスクリーン終了を含むすべての画面の閉じなし
Android onCloseアプリ内 PIP・OS PIP ウィンドウが閉じるとき フルスクリーンプレーヤーの終了。 そちらは onUserCloseAction を使ってください
Flutter previewCloseプレビューウィンドウが閉じるときそれ以外のすべての閉じ

理由の値は名前ベースで8種類が両プラットフォーム共通で、表記だけが異なります (iOS onBtnTapped ↔ Android ON_BTN_TAPPED)。Android にだけ1つ(ON_UNKNOWN)が多くあります。

閉じた理由iOSAndroidAndroid で実際に来るか
画面外に押し出して閉じたonSwipeOutON_SWIPE_OUT来る
閉じるボタンを押したonBtnTappedON_BTN_TAPPED来る
一般的な閉じonCloseON_CLOSE来る
他の画面に置き換えられて閉じたonReplace(プレビュー中に play() · preview() を再呼び出し)ON_REPLACE来る
理由不明の終了なしON_UNKNOWN来る
エラーで閉じたonErrorON_ERRORまだ来ない
PIP から復帰中onRestoringPipON_RESTORING_PIPまだ来ない
リンク遷移処理で閉じたonNavigationHandleCloseON_NAVIGATION_HANDLE_CLOSEまだ来ない
配信が終了したonBroadcastEndedON_BROADCAST_ENDED来る — showPreviewPopup で表示したプレビューポップアップのみ。レイアウトに配置した ShopLivePreview は閉じず、30秒後に再開します

「まだ来ない」は予約された値です。 Android ソースのコメント基準でまだどの画面でも発生せず、iOS 側の値構成に合わせてケースだけ作ってある状態です。現時点ではこれらの値で分岐しても通りません。

Flutter previewClose ストリームは ShopLivePreviewClose{campaignKey, reason}を送ります。reason はネイティブの理由と次のように対応します。

Flutter reasonネイティブの理由(iOS / Android)
closeButtononBtnTapped / ON_BTN_TAPPED
swipeOutonSwipeOut / ON_SWIPE_OUT
broadcastEndedonBroadcastEnded / 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 全体を探しても同じ役割のストリームはありません。

Android setOnPlaybackStateChanged と混同しないでください プレビューコンポーネント のリストにあるそのリスナーは ExoPlayer の再生状態だけを見る 別物です。名前が似ていて取り違えやすいので注意してください。
ShopLivePlayerPreviewDelegate.handlePlaybackEvent(_ event: ShopLivePreviewPlaybackEvent)
ShopLivePreview.setOnPlaybackEventListener(OnPlaybackEventListener)

クーポン・カスタムアクションの処理結果の返信

いつ上の クーポン発行リクエスト·カスタムアクションポップアップのクリックをアプリが処理した直後。これだけが アプリからプレーヤーへ向かう 方向です。
送る値
項目名前説明
成否iOS success: Bool
Android isDownloadSuccess/isSuccess
Flutter success
処理に成功したか
識別子couponIdまたはidどのクーポン・ポップアップに対する結果か。Android は SDK が自動で付けるため省略します
メッセージmessage視聴者に見せる文言
ポップアップ処理iOS status · Android·Flutter popupStatus/couponStatusSHOW · HIDE · KEEP
通知方式alertTypeALERT · 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() を呼ぶ。キャンセルするとコールバックは一切届かない
項目iOSAndroid
呼び出し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.codes9000(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 プロパティはありません。

プロパティタイプデフォルト値説明
heightInt(px)0 → 180dpウィンドウの高さ
widthInt(px)0 → 180dp×9/16(約 101dp)ウィンドウの幅
marginTop · marginBottom · marginLeft · marginRightInt(px)-1 → 16dp画面端からの距離
canTransitionBooleantrueプレビュー → プレイヤーへの遷移を許可
canVibratedBooleantrue振動を許可
useCloseButtonBooleanfalse閉じるボタンを表示
positionShopLivePreviewPositionConfigBOTTOM_RIGHTウィンドウの位置
radiusFloat(px)-1 → 10dp角の半径
referrerString?null流入経路
enabledSwipeOutBooleantrue画面外へスワイプして閉じる
isMutedBooleantrueミュートで開始
isEnabledVolumeKeyBooleanfalse音量ボタンでのミュート解除を許可
resolutionTypeShopLivePreviewResolutionTypePREVIEW解像度(LIVE · PREVIEW)
isPostCampaignImageVisibleBooleantrue配信終了後に画像を表示
useCustomActionBooleanfalseプレビューをタップしたとき、SDK の既定の遷移の代わりにアプリが処理
onClickListener · onInitializeListener · onRenderedFirstFrameListener · onCloseListener · onCloseActionListener · onCloseButtonClickListener · onCampaignListener · onBrandListener · onPlaybackEventListenerリスナーnullクリック · 開始 · 最初のフレーム · 閉じる · 閉じる(理由付き) · 閉じるボタン · キャンペーン · ブランド · 再生状態
handlerShopLiveHandler?nullこのプレビュー専用のハンドラ(画面が閉じた を参照)
backgroundColorInt?(ColorInt)null背景色
closeButtonConfigCloseButtonConfig?null閉じるボタンの見た目(位置 · サイズ · オフセット · 色 · 影 · 画像)

Flutter ShopLivePlayerPreviewData

setPreviewOption に渡すオプションです。値を空にすると、ネイティブがプラットフォームごとに異なるデフォルト値を入れます。

フィールドタイプデフォルト値説明
campaignKeyString(必須)配信キー
useCloseButtonboolfalse閉じるボタンを表示
enableSwipeOutbooltrue画面外へスワイプして閉じる
referrerString?null流入経路
pipRadiusdouble?null → 0角の半径
pipMaxSizedouble?null → iOS 300, Android 0ウィンドウの長辺のサイズ。Android は幅 · 高さに同じ値(dp)を入れ、0 の場合は SDK の既定サイズとして扱うようです(要確認)
marginTop · marginBottom · marginLeft · marginRightdouble?null → 0端からの距離。Android は dp を px に変換します
positionString?iOS TOP_LEFT, Android BOTTOM_RIGHTTOP_LEFT · TOP_RIGHT · BOTTOM_LEFT · BOTTOM_RIGHT。この4値以外の文字列は、両プラットフォームとも BOTTOM_RIGHT
closeButtonConfigShopLiveCloseButtonConfig?null閉じるボタンの見た目。position を空にすると iOS TOP_LEFT, Android TOP_RIGHT
Flutter position は必ず指定してください 空のままだと iOS は左上、Android は右下にウィンドウが表示されます。両プラットフォームを揃えるには値を明示してください。
リファレンス

エラーコード

原文ドキュメントの4分類(接続 / 配信入場 / 配信中 / その他)をそのまま維持します。 同じコード番号でもプラットフォームごとに意味が異なることがあります。 番号だけを見てプラットフォームを推測せず、下のコールアウトを必ず読んでください。

iOS ShopLiveErrorCases

サーバー発信のコード(負の値)とSDK内部コード(9000+)が1つのenumに混在しています。

分類コード定数意味
接続-102AuthenticationFailed認証失敗
接続-115GuestLoginNotAllowedゲストログイン不許可
接続-200CustomAccountNotFound存在しないサービス(accessKeyエラー)
接続-201CustomAccountExpiredクライアントの契約満了
接続-204InvalidSignatureJWT署名エラー
接続-412ExpiredSessionセッション満了
接続 / 配信入場-500ServerErrorサーバーエラー(両方共通)
配信入場-710CampaignNotFound配信なし
配信入場-711CampaignNotOnAir配信中でない
配信中-411DuplicateSession重複セッション
その他(内部)9000NotInitializedAccessKeyaccessKey未設定
その他(内部)9001NotInitializedShareURL共有URLなし
その他(内部)9002NotInitializedShortformConfigサーバー設定(ショートフォーム · ホスト)が欠けているとき。ショートフォーム · エディターでも発生
その他(内部)9003InValidURL不正な URL。 Androidの同じコードと意味が異なります(下のコールアウト)
その他(内部)9300FailedEncodingエンコード失敗
その他(内部)9310RemovedVideo削除された映像
状態の通知(エラーではない)9100NoVideoForPipPIP切り替え時に再生中の映像なし
状態の通知(エラーではない)9101NoPlayableStreamUrl準備 · リハーサル · ライブ状態なのに再生 URL をまだ受け取っていない
状態の通知(エラーではない)9102LiveNotBroadcastingライブ状態だがまだ送出前
その他(内部)9501UnsupportedMedia非対応コーデック
その他(内部)9500UnsupportedOSpipModeOS PIP非対応(iOS 14未満)
その他(内部)9900FailedNetworkネットワーク失敗
その他(内部)9901FailedJSONParsingJSON解析失敗
その他(内部)10000UnexpectedError不明なエラー
接続/配信入場/配信中のコード(-102 ~ -411の範囲)は、SDKがクライアントコード内で直接スローするのでは なく、サーバーがレスポンスボディで返す値を、SDKがそのまま載せて伝えるする通り道です。事実上「サーバー契約の文書化用」の enum であり、SDK が能動的に検査・分岐する値ではありません。

Android 2体系に分離

ShopLiveCommonErrorCode(SDK内部の9xxx帯)とShopLiveCampaignErrorCode(サーバー応答 専用、ソースコメントに「SDK内部の9xxx体系とは別」と明記)が別々に存在します。

分類コード定数意味
接続(サーバー応答 · ShopLiveCampaignErrorCode)-200SERVICE_NOT_EXIST存在しないアクセスキー
接続(サーバー応答 · ShopLiveCampaignErrorCode)-201ACCESS_KEY_EXPIRED期限切れのアクセスキー。 要確認: ソース自体に「期限切れのキーが入手できず、実際にこのコードが返るか確認できていない」というTODOコメントがあります
その他(サーバー応答 · ShopLiveCampaignErrorCode)-510INVALID_PARAMETER不正なリクエスト引数(iOSの一覧にはこのコード自体がありません)
その他(内部 · ShopLiveCommonErrorCode)9000NOT_INITIALIZED_ACCESS_KEYaccessKey未設定
その他(内部)9001NOT_INITIALIZED_SHARE_URL共有URLなし
その他(内部)9002NOT_INITIALIZED_CONFIGサーバー設定(ショートフォーム · ホスト)が欠けているとき。ショートフォーム · エディターでも発生
その他(内部)9003NOT_INITIALIZED_CAMPAIGN_KEYキャンペーンキー未設定。 iOSの同じコードと意味が異なります(下のコールアウト)
その他(内部)9050REQUIRED_COMPONENT_ACTIVITYComponentActivityが必要(iOS対応なし)
状態の通知(エラーではない)9101NO_PLAYABLE_STREAM_URL再生 URL なし。onError で届き、配信状態が ONAIR のときだけ
状態の通知(エラーではない)9102LIVE_NOT_BROADCASTINGまだ送出前(セグメントが届かない)。onError で届き、配信状態が ONAIR のときだけ
その他(内部)9300FAILED_ENCODINGエンコード失敗
その他(内部)9301FAILED_VIDEO_ENCODING映像エンコード失敗(iOS対応なし、iOSは9300 1つにまとめている)
その他(内部)9302FAILED_COVER_ENCODINGカバーエンコード失敗(iOS対応なし)
その他(内部)9310REMOVED_VIDEO削除された映像
その他(内部)9320WITHOUT_THUMBNAILサムネイルなし(iOS対応なし)
その他(内部)9500UNSUPPORTED_OS_PIP_MODEOS PIP非対応(Android 8・API 26未満)
その他(内部)9501UNSUPPORTED_MEDIA非対応コーデック
その他(内部)9800FAILED_PLAYERプレーヤーエラー(iOS対応なし、iOSには9800帯自体がありません)
その他(内部)9900FAILED_NETWORKネットワーク失敗
その他(内部)9901FAILED_JSON_PARSINGJSON解析失敗
その他(内部)9902FAILED_FETCH_DATAデータ取得失敗(iOS対応なし)
その他(内部)10000UNEXPECTED_ERROR不明なエラー
AndroidにはiOSの9100(NoVideoForPip)に対応するコードがありません(9101 から始まります)。接続/入場/配信中のコードの多く(-102 · -115 · -204 · -412 · -500 · -710 · -711 · -411)は Android ソースのどこにも定数として定義されていません。サーバーがこの値を返すと、定数化せずに ShopLiveCommonErrorのcode フィールドにそのまま通します。iOSと同様「サーバー契約の文書化」用途のため、両プラットフォームが矛盾することはありません。
9100番台はエラーではなく状態の通知です 9100 · 9101 · 9102 はエラーコールバックで届きますが、再生を終了せよという意味ではありません。配信状態を知らせる値なので、 プレイヤーを閉じないでください。 必要なら「まもなく始まります」のような案内だけを表示してください。
コード9003はプラットフォームごとに完全に異なる意味を持ちます iOSの9003は「不正なURL」で、Androidの9003は「キャンペーンキー未設定」です。同じ数字を見てプラットフォームを取り違えると、全く見当違いの原因で診断することになります。エラーコードはプラットフォーム間で番号が再利用され、同じコードが別の意味を持つことがある点を常に念頭に置いてください。

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.01.8.16.0
16.41.8.2 · 1.8.46.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 を開く必要がありません。

古いインストールガイドの「ProGuard ルールの手動追加」の節はもう当てはまりません 以前はルールをアプリ側に直接書き写す必要がありましたが、今は不要です。古いドキュメントを見て書き写したルールがあっても、そのままにして問題ありません。
どのモジュールがルールを持っているか確認したい場合

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

症状。 ディープリンクでアプリに入って配信を見ていて、商品を押して PIP に下がると、自社アプリの画面が前に出ず、見当違いのアプリが表示されます。アプリアイコンから起動したときは問題ありません。

原因。 プレーヤーは PIP に下がるとき ActivityManager.appTasks からアプリの Task を探して前面に出します。このとき基準にする印が CATEGORY_LAUNCHER ですが、この印はアプリアイコンから起動した Task にしか付きません。ディープリンクで作られた Task にはないため、バックスタックの形によってはアプリを見つけられないことがあります。バックスタックの形はリンクを開いた側が FLAG_ACTIVITY_NEW_TASK を付けたかどうかで変わりますが、それは自分たちでは決められません。

解決。 ディープリンクの入口を SchemeActivity のように別に置き、そこから実際の画面を FLAG_ACTIVITY_NEW_TASK で表示してください。 ミッション2の Android 例がその形です。こうすればリンクを押した側がどこであってもバックスタックの形が同じになります。詳しいルールは Androidのディープリンクドキュメントにあります。

再生状態に関するお問い合わせ

プレーヤー・プレビューの状態値がプラットフォームごとに違う動きに見える場合、値そのものがプラットフォームごとに異なることがあります。 リファレンスのイベント項目(プレーヤー画面の生成・消滅、画面が閉じた)と データフィールド辞典(Campaign項目)のプラットフォーム差異コールアウトを先に確認してください。

エラーコードに関するお問い合わせ

同じコード番号でもプラットフォームごとに意味が異なることがあります。コード値だけでプラットフォームを推測せず、 リファレンスのエラーコードも併せて確認してください。