Live Stream replay rental — mobil ve web
Replay kiralama client entegrasyonu
Live Stream Replay Rental - Mobile/Web Integration
Migration rehberi
Yapılandırma sırası için Migration şablonu. Yeni düzenlemelerde Amaç → Önkoşullar → Endpoint → Request/Response → Hata kodları → Client adımları → İlgili sayfalar bölümlerini tercih edin.
Endpointler
GET /api/v1/live-stream/:id/replay-statusPOST /api/v1/live-stream/:id/replay-rentalsGET /api/v1/live-stream/my-replay-rentals- Auth:
Authorization: Bearer <JWT>zorunlu
Kapsam
Replay kiralama 24 saatlik erişim verir. Kullanıcı kiraladığı süre boyunca recordingUrl alır, süre dolduğunda yayın listede kalabilir ama recordingUrl dönmez.
Bir yayın replay kiralanabilir olması için:
status = ended- soft delete edilmemiş olmalı
recordingUrldolu olmalıisActiveReplayOnCreatorProfile = truereplayCreditPrice > 0- Duo yayınlarda gelir paylaşımı yapılacak guest bulunmalı
Owner, broadcaster ve guest kullanıcıları kendi yayın replay'ini ödeme yapmadan izleyebilir.
Backend Davranışı
Kiralama kaydı backend'de user + liveStream için tek row olarak tutulur.
- Aktif kiralama varken tekrar
POST /replay-rentalsçağrılırsa ücret kesilmez. - Süresi dolmuş veya iade edilmiş kayıt tekrar kiralanırsa aynı kayıt tekrar
activeyapılır. - Yeni kiralamada
expiresAt = now + 24 saatolur. - Yetersiz bakiye varsa kiralama yapılmaz ve transaction oluşturulmaz.
- Yayın kiralama penceresi içinde silinirse aktif ve pending kiralamalar tam iade edilir.
- Gelir dağıtımı 24 saatlik pencere bittikten sonra cron ile yapılır.
Gelir oranları:
solo: creator%80, platform%20duocrowdveduoself: creator%40, guest%40, platform%20
Platform payı ayrı bir kullanıcıya kredi olarak yazılmaz.
Replay Access Alanı
LiveStreamResponseDto içine replayAccess eklendi. Geçmiş yayın listeleri ve detail response'larında mobile/web bu alanla UI kararını verebilir.
{
"recordingUrl": null,
"replayCreditPrice": 50,
"isActiveReplayOnCreatorProfile": true,
"replayAccess": {
"status": "not_rented",
"canRent": true,
"canViewRecording": false,
"price": 50,
"rentedAt": null,
"expiresAt": null,
"remainingSeconds": null
}
}recordingUrl sadece replayAccess.canViewRecording = true ise dolu gelir.
Status Değerleri
replayAccess.status ve rentalStatus değerleri:
type ReplayAccessStatus =
| 'owner'
| 'active'
| 'expired'
| 'refunded'
| 'not_rented'
| 'unavailable';Anlamları:
owner: Kullanıcı owner/broadcaster/guest, ücretsiz izleyebilir.active: Aktif kiralama var.expired: Kiralama süresi dolmuş.refunded: Kiralama iade edilmiş.not_rented: Kullanıcı bu yayını kiralamamış.unavailable: Yayın replay için uygun değil.
unavailableReason değerleri:
type ReplayUnavailableReason =
| 'stream_deleted'
| 'stream_not_ended'
| 'recording_unavailable'
| 'replay_disabled'
| 'replay_price_missing'
| 'duo_guest_missing';1. Replay Status Kontrolü
GET /api/v1/live-stream/:id/replay-status
Yayın id ile kullanıcının replay erişim ve kiralama durumunu döner. Kiralama butonu, player açma ve fiyat gösterimi için ana endpoint budur.
Path Params
id: string // live stream idRequest
GET /api/v1/live-stream/65f000000000000000000001/replay-status
Authorization: Bearer <JWT>Response
Backend standart BaseResponseDto<T> wrapper döner. Asıl payload data içindedir.
{
"isSuccess": true,
"statusCode": 200,
"data": {
"streamId": "65f000000000000000000001",
"streamStatus": "ended",
"isDeleted": false,
"isReplayAvailable": true,
"unavailableReason": null,
"price": 50,
"rentalDurationHours": 24,
"rentalStatus": "not_rented",
"canRent": true,
"canViewRecording": false,
"rentedAt": null,
"expiresAt": null,
"remainingSeconds": null,
"title": "Yayın başlığı",
"thumbnailUrl": "https://cdn.example.com/thumb.jpg",
"creator": {
"_id": "65f000000000000000000010",
"username": "creator",
"name": "Creator",
"surname": "User",
"profilePhoto": null
},
"liveStreamType": "solo",
"endedAt": "2026-05-13T10:00:00.000Z",
"recordingUrl": null
},
"errors": [],
"timestamp": "2026-05-13T12:00:00.000Z"
}Aktif erişim varsa:
{
"isSuccess": true,
"statusCode": 200,
"data": {
"streamId": "65f000000000000000000001",
"streamStatus": "ended",
"isReplayAvailable": true,
"price": 50,
"rentalDurationHours": 24,
"rentalStatus": "active",
"canRent": false,
"canViewRecording": true,
"rentedAt": "2026-05-13T12:00:00.000Z",
"expiresAt": "2026-05-14T12:00:00.000Z",
"remainingSeconds": 86400,
"recordingUrl": "https://cdn.example.com/replay.m3u8"
},
"errors": [],
"timestamp": "2026-05-13T12:00:00.000Z"
}2. Replay Kiralama
POST /api/v1/live-stream/:id/replay-rentals
Replay'i 24 saatliğine kiralar. Request body yoktur.
Path Params
id: string // live stream idRequest
POST /api/v1/live-stream/65f000000000000000000001/replay-rentals
Authorization: Bearer <JWT>Response
Response shape GET /replay-status ile aynıdır.
Başarılı kiralama:
{
"isSuccess": true,
"statusCode": 201,
"data": {
"streamId": "65f000000000000000000001",
"streamStatus": "ended",
"isDeleted": false,
"isReplayAvailable": true,
"unavailableReason": null,
"price": 50,
"rentalDurationHours": 24,
"rentalStatus": "active",
"canRent": false,
"canViewRecording": true,
"rentedAt": "2026-05-13T12:00:00.000Z",
"expiresAt": "2026-05-14T12:00:00.000Z",
"remainingSeconds": 86400,
"recordingUrl": "https://cdn.example.com/replay.m3u8"
},
"errors": [],
"timestamp": "2026-05-13T12:00:00.000Z"
}Notlar:
- Aktif kiralama varsa aynı response döner, tekrar ücret kesilmez.
- Owner/broadcaster/guest için ödeme alınmaz.
- Replay uygun değilse
400döner. - Yayın yoksa
404döner. - Bakiye yetersizse
400döner.
3. Kiraladığım Replay Listesi
GET /api/v1/live-stream/my-replay-rentals
Kullanıcının replay kiralama geçmişini döner. Süresi dolmuş kiralamalar listede kalabilir.
Query
page?: number // default: 1, min: 1
limit?: number // default: 20, min: 1, max: 100
status?: 'active' | 'expired' | 'refunded'Status filtre davranışı:
active: sadece süresi devam eden aktif kiralamalarexpired: süresi dolmuş kiralamalarrefunded: iade edilmiş kiralamalar- status verilmezse tüm kiralamalar
Request
GET /api/v1/live-stream/my-replay-rentals?page=1&limit=20&status=active
Authorization: Bearer <JWT>Response
list[].stream alanı ayrı bir replay objesi değildir. Backend burada mevcut LiveStreamResponseDto döner. Mapping şu şekilde yapılır:
stream = LiveStreamReplayAccessService.mapToResponseDto(liveStream, userId)Bu mapping LiveStreamMapper.mapToResponseDto üzerine replayAccess ekler ve recordingUrl değerini sadece aktif erişim varsa dolu bırakır.
{
"isSuccess": true,
"statusCode": 200,
"data": {
"list": [
{
"id": "65f000000000000000000100",
"streamId": "65f000000000000000000001",
"replayAccess": {
"status": "active",
"canRent": false,
"canViewRecording": true,
"price": 50,
"rentedAt": "2026-05-13T12:00:00.000Z",
"expiresAt": "2026-05-14T12:00:00.000Z",
"remainingSeconds": 82000
},
"pricePaid": 50,
"stream": {
"id": "65f000000000000000000001",
"title": "Yayın başlığı",
"liveStreamType": "solo",
"channelName": "stream-channel",
"broadcasters": [],
"guests": [],
"creator": {
"_id": "65f000000000000000000010",
"username": "creator",
"name": "Creator",
"surname": "User",
"profilePhoto": null
},
"thumbnailUrl": "https://cdn.example.com/thumb.jpg",
"recording": true,
"recordingUrl": "https://cdn.example.com/replay.m3u8",
"status": "ended",
"accessType": "free",
"price": 0,
"interest": "music",
"durationGoal": null,
"motivation": null,
"isActiveReplayOnCreatorProfile": true,
"replayCreditPrice": 50,
"startedAt": "2026-05-13T09:30:00.000Z",
"plannedStartDate": null,
"endedAt": "2026-05-13T10:00:00.000Z",
"plannedEndDate": null,
"createdAt": "2026-05-13T09:00:00.000Z",
"updatedAt": "2026-05-13T12:00:00.000Z",
"role": null,
"fundingGoal": null,
"collectedFunding": null,
"fundingPercentage": null,
"miniCrowdFundings": [],
"replayAccess": {
"status": "active",
"canRent": false,
"canViewRecording": true,
"price": 50,
"rentedAt": "2026-05-13T12:00:00.000Z",
"expiresAt": "2026-05-14T12:00:00.000Z",
"remainingSeconds": 82000
}
},
"createdAt": "2026-05-13T12:00:00.000Z",
"updatedAt": "2026-05-13T12:00:00.000Z"
}
],
"pagination": {
"currentPage": 1,
"itemsPerPage": 20,
"totalItems": 1,
"totalPages": 1,
"hasNextPage": false,
"hasPrevPage": false
}
},
"errors": [],
"timestamp": "2026-05-13T12:00:00.000Z"
}Expired örnek:
{
"replayAccess": {
"status": "expired",
"canRent": true,
"canViewRecording": false,
"price": 50,
"rentedAt": "2026-05-12T12:00:00.000Z",
"expiresAt": "2026-05-13T12:00:00.000Z",
"remainingSeconds": null
},
"stream": {
"recordingUrl": null
}
}Etkilenen Mevcut Endpointler
Aşağıdaki live stream response'larında replayAccess alanı dönebilir ve recordingUrl erişim kuralına göre maskelenir:
GET /api/v1/live-stream/:idGET /api/v1/live-stream/past-streamsGET /api/v1/live-stream/past-streamsv2GET /api/v1/live-stream/user/:userId/past-streamsGET /api/v1/live-stream/user/:userId/past-streamsv2GET /api/v1/live-stream/my-past-streamsGET /api/v1/live-stream/my-past-streamsv2GET /api/v1/live-stream/my-broadcasted-streamsGET /api/v1/live-stream/my-watched-streams
Mobile/web tarafında player açmak için her zaman recordingUrl kontrol edilmeli. replayAccess.canViewRecording = false ise player açılmamalı.
TypeScript Tipleri
type ReplayAccessStatus =
| 'owner'
| 'active'
| 'expired'
| 'refunded'
| 'not_rented'
| 'unavailable';
type ReplayUnavailableReason =
| 'stream_deleted'
| 'stream_not_ended'
| 'recording_unavailable'
| 'replay_disabled'
| 'replay_price_missing'
| 'duo_guest_missing';
type ReplayAccess = {
status: ReplayAccessStatus;
canRent: boolean;
canViewRecording: boolean;
price: number | null;
rentedAt: string | null;
expiresAt: string | null;
remainingSeconds: number | null;
};
type ReplayStatusResponse = {
streamId: string;
streamStatus: string;
isDeleted: boolean;
isReplayAvailable: boolean;
unavailableReason: ReplayUnavailableReason | null;
price: number | null;
rentalDurationHours: 24;
rentalStatus: ReplayAccessStatus;
canRent: boolean;
canViewRecording: boolean;
rentedAt: string | null;
expiresAt: string | null;
remainingSeconds: number | null;
title: string;
thumbnailUrl: string | null;
creator: unknown | null;
liveStreamType: 'solo' | 'duoself' | 'duocrowd';
endedAt: string | null;
recordingUrl: string | null;
};
type LiveStreamResponse = {
id: string;
title: string;
liveStreamType: 'solo' | 'duoself' | 'duocrowd';
channelName: string;
broadcasters: unknown[];
guests: unknown[];
creator: unknown | null;
thumbnailUrl: string | null;
recording: boolean;
recordingUrl: string | null;
replayAccess: ReplayAccess | null;
status: string;
accessType: 'free' | 'paid';
price: number;
interest: string | null;
durationGoal: number | null;
motivation: string | null;
isActiveReplayOnCreatorProfile: boolean | null;
replayCreditPrice: number | null;
startedAt: string | null;
plannedStartDate: string | null;
endedAt: string | null;
plannedEndDate: string | null;
createdAt: string;
updatedAt: string;
role: 'host' | 'guest' | 'audience' | null;
fundingGoal: number | null;
collectedFunding: number | null;
fundingPercentage: number | null;
miniCrowdFundings: unknown[];
};
type ReplayRentalListItem = {
id: string;
streamId: string;
replayAccess: ReplayAccess;
pricePaid: number;
stream: LiveStreamResponse;
createdAt: string;
updatedAt: string;
};
type Pagination = {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
hasNextPage: boolean;
hasPrevPage: boolean;
};
type PaginatedResponse<T> = {
list: T[];
pagination: Pagination;
};
type BaseResponseDto<T> = {
isSuccess: boolean;
statusCode: number;
data: T;
errors?: string[];
timestamp: string;
};Mobile/Web UI Kararları
- Kirala butonu:
replayAccess.canRent === true - Oynat butonu:
replayAccess.canViewRecording === true && recordingUrl != null - Fiyat:
replayAccess.price - Geri sayım:
replayAccess.remainingSecondsveyareplayAccess.expiresAt - Süresi dolmuş kiralamada tekrar kirala:
replayAccess.status === 'expired' && replayAccess.canRent === true - İade edilmiş kiralamada tekrar kirala:
replayAccess.status === 'refunded' && replayAccess.canRent === true - Replay kapalı/uygun değil mesajı:
unavailableReason