Интеграция в мобильное приложение
Плеер ShopStory воспроизводит стрим, показывает товары и чат. Когда пользователь нажимает «Купить» или открывает товар — плеер отправляет событие в ваше приложение через bridge.
Плеер всегда работает через WebView (WKWebView на iOS, WebView на Android) — нативного SDK для плеера нет. Но для списка стримов есть два варианта.
Два варианта интеграции
Вариант A: Всё через WebView (быстрый)
И список стримов, и плеер открываются в WebView. Минимум нативного кода.
Нативное приложение → WebView со списком стримов →
→ тап на стрим → плеер открывается в том же WebView
Как реализовать: откройте в WebView страницу https://<ваш-домен>/live/?wv=1. ShopStory отрисует список стримов и плеер.
Плюсы: минимум работы, обновления UI приходят автоматически. Минусы: нет контроля над дизайном списка стримов, не нативный UX.
Вариант B: Нативный список + WebView плеер (рекомендуемый)
Список стримов отрисовывает ваше приложение через API /v3/streams. При тапе на стрим — открывается WebView с плеером.
Нативное приложение → API /v3/streams → нативный список стримов →
→ тап на стрим → WebView с плеером
Как реализовать: запросите стримы через API, отрисуйте нативные карточки, по тапу откройте WebView с URL плеера.
Плюсы: полный контроль над дизайном списка, нативный UX, можно фильтровать стримы. Минусы: больше работы на стороне приложения.
Независимо от выбранного варианта, сам плеер стрима всегда открывается в WebView. Остальная часть этой страницы описывает настройку WebView для плеера.
Перед началом
Для интеграции вам понадобится:
applicationIdвашего проекта в ShopStory (выдаётся при подключении).- URL страницы стримов —
https://<ваш-домен>/live/или fallbackhttps://app.shopstory.live/sdk/. - ID стрима (
translationId) — получите через GET /v3/streams или из личного кабинета.
Как получить ID стрима (вариант B)
Ваше приложение запрашивает список стримов через API и показывает их в нативном UI. При тапе на стрим — открывает WebView с плеером.
GET https://app.shopstory.live/v3/streams?applicationId=<ваш-id>
Ответ содержит массив стримов с полем id — это и есть translationId для URL плеера. Подробнее: Каталог стримов.
Быстрый старт
1. Откройте URL стрима в WebView
https://<ваш-домен>/live/?wv=1#/translation/<translationId>
Параметр wv=1 обязателен — он активирует мобильный режим плеера.
2. Настройте WebView
- iOS (Swift)
- Android (Kotlin)
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true // видео внутри страницы, не fullscreen
config.mediaTypesRequiringUserActionForPlayback = [] // автовоспроизведение без тапа
config.userContentController.add(self, name: "add-product")
config.userContentController.add(self, name: "open-product")
let webView = WKWebView(frame: view.bounds, configuration: config)
val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false // автовоспроизведение
webView.addJavascriptInterface(ShopstoryBridge(), "shopstory")
3. Обработайте события от плеера
- iOS (Swift)
- Android (Kotlin)
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard let json = message.body as? String,
let data = json.data(using: .utf8),
let product = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return }
let feedProductId = product["feedProductId"] as? String ?? ""
let url = product["url"] as? String ?? ""
switch message.name {
case "add-product":
CartManager.shared.addProduct(id: feedProductId)
case "open-product":
navigator.openProduct(url: url)
default: break
}
}
inner class ShopstoryBridge {
@JavascriptInterface
fun addProduct(json: String) {
val product = JSONObject(json)
val feedProductId = product.optString("feedProductId")
CartManager.addProduct(feedProductId)
}
@JavascriptInterface
fun openProduct(json: String) {
val product = JSONObject(json)
val url = product.optString("url")
navigator.openProduct(url)
}
}
4. Добавьте кнопку «Закрыть»
В мобильном режиме плеер скрывает свою встроенную кнопку навигации — пользователь не сможет выйти из просмотра без вашей помощи. Варианты:
- Нативная кнопка поверх WebView — крестик или стрелку в safe area (пример в полном коде ниже). Плеер резервирует 50px слева в шапке, чтобы ваша кнопка не перекрывала заголовок.
- Системная навигация — разместите WebView внутри
UINavigationController(iOS) илиActivityс ActionBar (Android).
Типичный флоу пользователя
Нативный список стримов → тап на стрим → WebView с плеером →
→ пользователь смотрит, тапает «Купить» →
→ bridge-событие add-product → нативное приложение добавляет в корзину →
→ пользователь закрывает WebView (нативная кнопка)
Когда стрим заканчивается: страница перезагружается и плеер переключается в режим записи (VOD). Пользователь может продолжить просмотр или закрыть WebView. Закрывать WebView программно не нужно.
Параметры URL
| Параметр | Обязательный | Описание |
|---|---|---|
wv=1 | Да | Включает мобильный режим. Без этого параметра: события add-product / open-product не будут отправляться, кнопка «Купить» будет открывать URL в новой вкладке вместо отправки события в приложение. |
safe_area=1 | По ситуации | Добавляет отступы в плеере под вырез экрана (чёлка, Dynamic Island). См. подробности ниже. |
startTime=<сек> | Нет | Начать воспроизведение записи с указанной секунды. Предназначен для VOD — на live-стримах seek может не сработать. |
hide_chat=1 | Нет | Скрыть чат эфира. Полезно если хотите максимально чистый вид плеера. |
Когда использовать safe_area=1
- Используйте, если ваш WebView занимает весь экран (edge-to-edge) и вы не добавляете свои отступы под вырез. Плеер сам сдвинет заголовок и элементы управления вниз.
- Не используйте, если ваш WebView уже размещён ниже safe area (например, внутри
UINavigationControllerили ниже собственного toolbar) — иначе отступ удвоится и сверху будет лишнее пустое пространство.
В примерах используется <ваш-домен>. Подставьте домен вашего интернет-магазина, где установлен ShopStory (например, www.example.ru). Если у вас нет собственной страницы стримов — используйте app.shopstory.live/sdk/ с параметром x-force-appid=<ваш-applicationId> (этот параметр заменяет привязку к домену).
События bridge
Плеер отправляет два события:
| Событие | Когда | iOS | Android |
|---|---|---|---|
add-product | Пользователь нажал «Купить» | messageHandlers['add-product'] | shopstory.addProduct() |
open-product | Пользователь открыл товар | messageHandlers['open-product'] | shopstory.openProduct() |
После нажатия «Купить» кнопка в плеере автоматически меняется на «В корзине».
Payload
Payload передаётся как JSON-строка (не объект). Распарсите её для доступа к полям.
{
"name": "Название товара",
"feedProductId": "12345",
"feedProductGroupId": "",
"url": "https://example.ru/product/12345/",
"vendorCode": "ART-001"
}
| Поле | Всегда присутствует | Описание |
|---|---|---|
name | Да | Название товара |
feedProductId | Да | ID товара из фида. Используйте это поле для добавления в корзину. |
feedProductGroupId | Нет | Группа товара (для вариаций). Может быть пустой строкой. |
url | Да | URL карточки товара на сайте. Используйте для перехода к товару. |
vendorCode | Нет | Артикул товара. Может отсутствовать. |
Полный пример
- iOS (Swift)
- Android (Kotlin)
import WebKit
class ShopstoryWebViewController: UIViewController, WKScriptMessageHandler {
private var webView: WKWebView!
private let streamUrl: String // URL вида "https://example.ru/live/?wv=1#/translation/123"
init(streamUrl: String) {
self.streamUrl = streamUrl
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// 1. Настройка WebView
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
config.userContentController.add(self, name: "add-product")
config.userContentController.add(self, name: "open-product")
webView = WKWebView(frame: view.bounds, configuration: config)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
// 2. Кнопка «Закрыть»
let closeButton = UIButton(type: .system)
closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
closeButton.tintColor = .white
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.addTarget(self, action: #selector(close), for: .touchUpInside)
view.addSubview(closeButton)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
closeButton.widthAnchor.constraint(equalToConstant: 32),
closeButton.heightAnchor.constraint(equalToConstant: 32),
])
// 3. Загрузка стрима
if let url = URL(string: streamUrl) {
webView.load(URLRequest(url: url))
}
}
@objc private func close() {
dismiss(animated: true)
}
// 4. Обработка событий
func userContentController(
_ uc: WKUserContentController, didReceive message: WKScriptMessage
) {
guard let json = message.body as? String,
let data = json.data(using: .utf8),
let product = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return }
let feedProductId = product["feedProductId"] as? String ?? ""
let url = product["url"] as? String ?? ""
switch message.name {
case "add-product":
CartManager.shared.addProduct(id: feedProductId)
case "open-product":
navigator.openProduct(url: url)
default: break
}
}
}
Как открыть из списка стримов:
let url = "https://example.ru/live/?wv=1#/translation/\(stream.id)"
let vc = ShopstoryWebViewController(streamUrl: url)
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
import android.webkit.JavascriptInterface
import android.webkit.WebView
import org.json.JSONObject
class ShopstoryWebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val streamUrl = intent.getStringExtra("streamUrl") ?: return finish()
// 1. Настройка WebView
val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(ShopstoryBridge(), "shopstory")
// 2. Кнопка «Назад» в ActionBar
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// 3. Загрузка стрима
webView.loadUrl(streamUrl)
setContentView(webView)
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
// 4. Обработка событий
inner class ShopstoryBridge {
@JavascriptInterface
fun addProduct(json: String) {
val product = JSONObject(json)
CartManager.addProduct(product.optString("feedProductId"))
}
@JavascriptInterface
fun openProduct(json: String) {
val product = JSONObject(json)
navigator.openProduct(product.optString("url"))
}
}
}
Как открыть из списка стримов:
val url = "https://example.ru/live/?wv=1#/translation/${stream.id}"
val intent = Intent(this, ShopstoryWebViewActivity::class.java)
intent.putExtra("streamUrl", url)
startActivity(intent)
Чек-лист
- URL содержит
?wv=1 - (iOS)
allowsInlineMediaPlayback = trueиmediaTypesRequiringUserActionForPlayback = [] - (Android)
mediaPlaybackRequiresUserGesture = false - Обработчики зарегистрированы до загрузки URL
- Реализована нативная кнопка «Закрыть» / «Назад»
-
add-product→ добавление в корзину поfeedProductId -
open-product→ переход в карточку товара поurl
Частые проблемы
| Симптом | Решение |
|---|---|
| События не приходят | Проверьте ?wv=1 в URL и регистрацию обработчиков до loadRequest |
| Видео открывается в fullscreen-плеере (iOS) | Добавьте allowsInlineMediaPlayback = true |
| Видео не воспроизводится автоматически (Android) | Добавьте mediaPlaybackRequiresUserGesture = false |
| Контент перекрывается вырезом экрана | Добавьте &safe_area=1 в URL |
| Двойной отступ под вырез | Уберите safe_area=1 — ваше приложение уже обрабатывает safe area |
| Payload приходит как строка | Распарсите через JSONSerialization (iOS) или JSONObject (Android) |
| Android: события не приходят | Имя interface должно быть "shopstory" (iOS: "add-product" и "open-product") |
Отладка
-
(iOS) Включите Safari Web Inspector для отладки WebView:
#if DEBUG
if #available(iOS 16.4, *) { webView.isInspectable = true }
#endifЗатем: Safari → Develop → Simulator (или устройство) → выберите страницу.
-
При нажатии «Купить» без зарегистрированных обработчиков в консоли появится
global.shopstory.addProduct is not a function— это значит bridge не настроен. -
Убедитесь что
feedProductIdв payload совпадает с<offer id>из вашего товарного фида.
Следующие шаги:
- Каталог стримов — GET /v3/streams — получение списка стримов для нативного UI
- Мини-плеер и проверка стрима — виджет на карточке товара
- Quickstart — первые запросы к API