Implementing Donations and Gifts During Live Streaming in Mobile Apps
Donations in a stream aren't just payments. It's a real-time event: user sends a gift → animation flies over the video → viewers see the donor's name in the feed → streamer gets notified. Between the button press and animation appearance should be less than a second. This requires thoughtful integration: payment processor → backend → WebSocket → client.
Architecture: Three Parallel Streams
A donation flows through three independent layers simultaneously:
- Payment stream — charging via Stripe/IAP/Google Play Billing with confirmation
- Real-time stream — WebSocket event to all broadcast viewers
- Donation feed — UI counter update and log scroll
An error in one stream shouldn't block others. Gift animation shows after payment confirmation, not before.
Virtual Currency: Why Not Direct Payments
Most streaming apps use virtual currency (coins, crystals) instead of direct transactions:
- App Store and Google Play take 30% on in-app purchases, but virtual currency splits the coin purchase (IAP) from coin spending (server logic) — deduction happens on the server, App Store isn't involved
- User buys a coin pack via IAP, spends anytime — async from the payment flow
- Small donor aggregation: sending 5 rubles directly is expensive due to fees, sending 5 pre-bought coins is cheap
// Purchase coins via Google Play Billing
val productDetails = // loaded via BillingClient.queryProductDetailsAsync
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
)
)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
Gift Types: GiftItem Object
data class GiftItem(
val id: String,
val name: String, // "Rose", "Rocket", "Crown"
val coinCost: Int, // cost in coins
val animationUrl: String, // Lottie JSON or MP4
val displayDurationMs: Long // how long to show animation
)
Gift animations are Lottie (JSON, ~50-200 KB) or short MP4s (~500 KB). Lottie is preferable: scales without artifacts, supports transparency, doesn't require a media decoder.
Real-time: WebSocket Gift Event
After deducting coins on the server (atomic DB transaction) — publish an event to the broadcast's WebSocket channel:
{
"type": "gift",
"streamId": "stream-abc123",
"senderId": "user-456",
"senderName": "Alexey",
"senderAvatar": "https://cdn.example.com/avatars/456.jpg",
"giftId": "gift-rocket",
"giftName": "Rocket",
"coinAmount": 50,
"timestamp": "2024-06-15T14:30:01.234Z"
}
The server must verify coin balance before publishing. Never trust the client: client says "send rocket for 50 coins" → server checks balance, deducts, then publishes the event. Not the other way.
Client: Animation Queue
Multiple viewers can send gifts simultaneously. Can't show all animations in parallel — the screen becomes chaos. Need a queue:
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
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()
}
}
}
On Android, a similar queue using Lottie AnimationView and LinkedList<GiftEvent> with Handler.
Donation Feed: RecyclerView with Prepend
New donations are added to the list beginning, not end:
class DonationAdapter : RecyclerView.Adapter<DonationViewHolder>() {
private val donations = mutableListOf<DonationItem>()
fun prepend(donation: DonationItem) {
donations.add(0, donation)
notifyItemInserted(0)
recyclerView.scrollToPosition(0)
}
}
Handling Offline Viewers
Viewers can reconnect mid-stream. On reconnect, don't replay all missed animations — show only the donation log as text and last N events.
Top Donors: Real-time Aggregation
// Redis ZSET for top donors of the broadcast
// ZINCRBY stream:{streamId}:donations {coinAmount} {userId}
// ZREVRANGE stream:{streamId}:donations 0 9 WITHSCORES — top 10
Updates on each donation, broadcast to all viewers every 5–10 seconds via separate WebSocket channel.
Included in Work
- Server-side coin deduction logic (atomic transaction)
- WebSocket event broadcast to all viewers
- Gift animation queue on client (Lottie)
- Donation feed with prepend logic
- Real-time top donors
- Reconnect and state recovery handling
Timeline
5 days. Server-side with WebSocket and coin billing — 2 days. Client-side with animations and feed — 2 days. Integration, testing, edge cases — 1 day. Pricing is calculated individually.







