Реализация донатов/подарков во время прямой трансляции в мобильном приложении
Донаты в стриме — это не просто платёж. Это real-time событие: пользователь отправил подарок → анимация прилетает поверх видео → зрители видят имя донатера в ленте → стример получает уведомление. Между нажатием кнопки и появлением анимации должно проходить меньше секунды. Это требует продуманной связки: платёжный процессор → бекенд → WebSocket → клиент.
Архитектура: три параллельных потока
Донат проходит через три независимых слоя одновременно:
- Платёжный поток — списание через Stripe/IAP/Google Play Billing с подтверждением
- Real-time поток — WebSocket-событие всем зрителям трансляции
- Лента донатов — обновление UI-счётчика и скролл-лога
Ошибка в одном потоке не должна блокировать остальные. Анимация подарка показывается после подтверждения платежа, не до.
Виртуальная валюта: почему не прямые платежи
Большинство стриминговых приложений используют виртуальную валюту (монеты, кристаллы) вместо прямых транзакций. Причины:
- App Store и Google Play берут 30% с in-app покупок, но виртуальная валюта позволяет разделить покупку монет (IAP) и трату монет (серверная логика) — списание происходит на сервере, App Store не участвует
- Пользователь покупает пачку монет через IAP, тратит в любой момент — асинхронно от платёжного флоу
- Агрегация мелких доноров: отправить 5 рублей напрямую — дорого по комиссиям, отправить 5 монет из купленных ранее — дёшево
// Покупка монет через Google Play Billing
val productDetails = // загружены через BillingClient.queryProductDetailsAsync
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
)
)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
Типы подарков: GiftItem объект
data class GiftItem(
val id: String,
val name: String, // "Роза", "Ракета", "Корона"
val coinCost: Int, // стоимость в монетах
val animationUrl: String, // Lottie JSON или MP4
val displayDurationMs: Long // сколько показывать анимацию
)
Анимации подарков — Lottie (JSON, ~50-200 KB) или короткие MP4 (~500 KB). Lottie предпочтительнее: масштабируется без артефактов, поддерживает прозрачность, не требует медиадекодера.
Real-time: WebSocket событие подарка
После списания монет на сервере (атомарная операция в транзакции БД) — публикуем событие в WebSocket-канал трансляции:
{
"type": "gift",
"streamId": "stream-abc123",
"senderId": "user-456",
"senderName": "Алексей",
"senderAvatar": "https://cdn.example.com/avatars/456.jpg",
"giftId": "gift-rocket",
"giftName": "Ракета",
"coinAmount": 50,
"timestamp": "2024-06-15T14:30:01.234Z"
}
Сервер обязан проверить баланс монет до публикации. Никогда не доверяйте клиенту: клиент говорит «отправить ракету за 50 монет» → сервер проверяет баланс, списывает, затем публикует событие. Не наоборот.
Клиент: очередь анимаций
Одновременно несколько зрителей могут отправить подарки. Нельзя показывать все анимации параллельно — экран превратится в хаос. Нужна очередь:
class GiftAnimationQueue {
private var queue: [GiftEvent] = []
private var isPlaying = false
func enqueue(_ event: GiftEvent) {
queue.append(event)
if !isPlaying { playNext() }
}
private func playNext() {
guard !queue.isEmpty else { isPlaying = false; return }
isPlaying = true
let event = queue.removeFirst()
showGiftAnimation(event) { [weak self] in
// completion после окончания анимации
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self?.playNext()
}
}
}
private func showGiftAnimation(_ event: GiftEvent, completion: @escaping () -> Void) {
let animationView = LottieAnimationView(name: event.giftId)
animationView.frame = overlayView.bounds
overlayView.addSubview(animationView)
animationView.play { _ in
animationView.removeFromSuperview()
completion()
}
}
}
На Android аналогичная очередь через Lottie AnimationView и LinkedList<GiftEvent> с Handler.
Лента донатов: RecyclerView с prepend
Новые донаты добавляются в начало списка, не в конец:
class DonationAdapter : RecyclerView.Adapter<DonationViewHolder>() {
private val donations = mutableListOf<DonationItem>()
fun prepend(donation: DonationItem) {
donations.add(0, donation)
notifyItemInserted(0)
recyclerView.scrollToPosition(0)
}
}
Обработка оффлайн-зрителей
Зрители могут переподключиться в середине трансляции. При переподключении не нужно воспроизводить все пропущенные анимации — показываем только лог донатов текстом и последние N событий.
Топ донатеров: агрегация в реальном времени
// Redis ZSET для топ донатеров трансляции
// ZINCRBY stream:{streamId}:donations {coinAmount} {userId}
// ZREVRANGE stream:{streamId}:donations 0 9 WITHSCORES — топ 10
Обновляется при каждом донате, рассылается всем зрителям раз в 5-10 секунд через отдельный WebSocket-канал.
Что входит в работу
- Серверная логика списания монет (атомарная транзакция)
- WebSocket-рассылка событий всем зрителям
- Очередь анимаций подарков на клиенте (Lottie)
- Лента донатов с prepend-логикой
- Топ донатеров в реальном времени
- Обработка reconnect и восстановления состояния
Сроки
5 дней. Серверная часть с WebSocket и биллингом монет — 2 дня. Клиентская часть с анимациями и лентой — 2 дня. Интеграция, тестирование, edge cases — 1 день. Стоимость рассчитывается индивидуально.







