Allmine API
Live Stream

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-status
  • POST /api/v1/live-stream/:id/replay-rentals
  • GET /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ı
  • recordingUrl dolu olmalı
  • isActiveReplayOnCreatorProfile = true
  • replayCreditPrice > 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 active yapılır.
  • Yeni kiralamada expiresAt = now + 24 saat olur.
  • 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 %20
  • duocrowd ve duoself: 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 id

Request

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 id

Request

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 400 döner.
  • Yayın yoksa 404 döner.
  • Bakiye yetersizse 400 dö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 kiralamalar
  • expired: süresi dolmuş kiralamalar
  • refunded: 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/:id
  • GET /api/v1/live-stream/past-streams
  • GET /api/v1/live-stream/past-streams v2
  • GET /api/v1/live-stream/user/:userId/past-streams
  • GET /api/v1/live-stream/user/:userId/past-streams v2
  • GET /api/v1/live-stream/my-past-streams
  • GET /api/v1/live-stream/my-past-streams v2
  • GET /api/v1/live-stream/my-broadcasted-streams
  • GET /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.remainingSeconds veya replayAccess.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

On this page