Allmine API
Live Stream

Live Stream Gateway

Live Stream Gateway Dökümantasyonu

streamStarted event'inin uçtan uca entegrasyonu için: docs/live-stream-started-integration.md

Giriş ve Genel Bakış

Live Stream Gateway, canlı yayın uygulaması için WebSocket tabanlı gerçek zamanlı iletişim sağlayan bir servistir. Bu gateway, yayın izleyicilerinin sayısını takip etme, yayın durumu güncellemeleri, ban işlemleri ve süre uzatma gibi olayları yönetir.

Teknik Detaylar

  • Namespace: /live-stream
  • Protokol: WebSocket (Socket.IO)
  • CORS: Tüm origin'lere açık (origin: "*")
  • Metodlar: GET, POST

WebSocket Endpoint'leri

  • Local: ws://localhost:3000/live-stream
  • Development: wss://dev.allmine.win/live-stream
  • Staging: wss://staging.allmine.win/live-stream
  • Production: wss://api.allmine.win/live-stream

Bağlantı Bilgileri

React Native'de Bağlantı Kurma

React Native'de Socket.IO kullanmak için socket.io-client paketini kurmanız gerekir:

npm install socket.io-client
# veya
yarn add socket.io-client

Temel Bağlantı Örneği

import { io, Socket } from 'socket.io-client';

const BASE_URL = 'https://api.allmine.win'; // veya ortamınıza göre
const socket: Socket = io(`${BASE_URL}/live-stream`, {
  transports: ['websocket'],
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionAttempts: 5,
  reconnectionDelayMax: 5000,
});

// Bağlantı durumu dinleme
socket.on('connect', () => {
  console.log('WebSocket bağlantısı kuruldu:', socket.id);
});

socket.on('disconnect', (reason) => {
  console.log('WebSocket bağlantısı kesildi:', reason);
});

socket.on('connect_error', (error) => {
  console.error('Bağlantı hatası:', error);
});

Authentication

Şu anda gateway'de authentication zorunluluğu yoktur. Ancak gelecekte JWT token ile kimlik doğrulama eklenebilir. Bu durumda bağlantı şu şekilde yapılabilir:

const socket = io(`${BASE_URL}/live-stream`, {
  auth: {
    token: 'your-jwt-token-here'
  },
  // ... diğer ayarlar
});

Room Yapısı

Gateway iki tür room yapısı kullanır:

1. Stream Room (stream:{streamId})

Belirli bir yayına katılan tüm kullanıcılar bu room'a eklenir. Bu room'a katıldığınızda:

  • O yayının viewer count güncellemelerini alırsınız
  • Yayına özel eventleri (süre uzatma, ban işlemleri) alırsınız
  • Yayın bittiğinde otomatik olarak bağlantınız kesilir

Örnek Room Adı: stream:507f1f77bcf86cd799439011

2. Overview Room (streams:overview)

Homepage veya yayın listesi sayfaları için kullanılır. Bu room'a katıldığınızda:

  • Tüm aktif yayınların listesini alırsınız
  • Yayın bittiğinde bildirim alırsınız
  • Tüm yayınların viewer count güncellemelerini alırsınız

Room Adı: streams:overview

Client'tan Server'a Gönderilen Eventler

1. joinStream

Belirli bir yayına katılmak için kullanılır.

Payload:

{
  streamId: string; // MongoDB ObjectId formatında
}

Örnek:

socket.emit('joinStream', {
  streamId: '507f1f77bcf86cd799439011'
});

Başarılı Yanıt: joinedStream eventi gelir Hata Durumu: joinStreamError eventi gelir


2. leaveStream

Belirli bir yayından ayrılmak için kullanılır.

Payload:

{
  streamId: string; // MongoDB ObjectId formatında
}

Örnek:

socket.emit('leaveStream', {
  streamId: '507f1f77bcf86cd799439011'
});

Başarılı Yanıt: leftStream eventi gelir Hata Durumu: leaveStreamError eventi gelir


3. joinOverview

Global overview room'una katılmak için kullanılır. Homepage veya yayın listesi sayfalarında kullanılmalıdır.

Payload: Yok (boş)

Örnek:

socket.emit('joinOverview');

Başarılı Yanıt: joinedOverview eventi gelir Hata Durumu: joinOverviewError eventi gelir


4. leaveOverview

Global overview room'undan ayrılmak için kullanılır.

Payload: Yok (boş)

Örnek:

socket.emit('leaveOverview');

Başarılı Yanıt: leftOverview eventi gelir Hata Durumu: leaveOverviewError eventi gelir

Server'dan Client'a Gönderilen Eventler

Başarı Yanıt Eventleri

1. joinedStream

Yayına başarıyla katıldığınızda gelir.

Payload:

{
  streamId: string;
  viewerCount: number;
  success: boolean;
}

Örnek:

socket.on('joinedStream', (data) => {
  console.log(`Yayına katıldınız: ${data.streamId}`);
  console.log(`İzleyici sayısı: ${data.viewerCount}`);
});

2. leftStream

Yayından başarıyla ayrıldığınızda gelir.

Payload:

{
  streamId: string;
  viewerCount: number;
  success: boolean;
}

Örnek:

socket.on('leftStream', (data) => {
  console.log(`Yayından ayrıldınız: ${data.streamId}`);
  console.log(`Güncel izleyici sayısı: ${data.viewerCount}`);
});

3. joinedOverview

Overview room'una başarıyla katıldığınızda gelir.

Payload:

{
  activeStreams: Array<{
    streamId: string;
    viewerCount: number;
  }>;
  success: boolean;
}

Örnek:

socket.on('joinedOverview', (data) => {
  console.log('Aktif yayınlar:', data.activeStreams);
  // data.activeStreams = [
  //   { streamId: '507f1f77bcf86cd799439011', viewerCount: 15 },
  //   { streamId: '507f1f77bcf86cd799439012', viewerCount: 8 }
  // ]
});

4. leftOverview

Overview room'undan başarıyla ayrıldığınızda gelir.

Payload:

{
  success: boolean;
}

Örnek:

socket.on('leftOverview', (data) => {
  console.log('Overview room\'undan ayrıldınız');
});

Viewer Count Güncelleme Eventleri

5. viewerCountUpdated

Belirli bir yayının viewer count'u güncellendiğinde, o yayının room'undaki tüm kullanıcılara gönderilir.

Payload:

{
  streamId: string;
  viewerCount: number;
  timestamp: string; // ISO 8601 formatında
}

Örnek:

socket.on('viewerCountUpdated', (data) => {
  console.log(`Yayın ${data.streamId} izleyici sayısı: ${data.viewerCount}`);
  // UI'da viewer count'u güncelle
});

6. streamViewerUpdated

Overview room'undaki kullanıcılara, herhangi bir yayının viewer count güncellemesi geldiğinde gönderilir.

Payload:

{
  streamId: string;
  viewerCount: number;
  timestamp: string; // ISO 8601 formatında
}

Örnek:

socket.on('streamViewerUpdated', (data) => {
  // Homepage'deki yayın listesinde viewer count'u güncelle
  updateStreamViewerCount(data.streamId, data.viewerCount);
});

giftCountUpdated

Belirli bir yayının toplam hediye adedi güncellendiğinde stream room'undaki kullanıcılara gönderilir.

Bu event iki durumda tetiklenir:

  • joinStream sonrasında ilgili kullanıcıya başlangıç snapshot'u
  • Yayına yeni gift geldiğinde room'daki herkese güncel toplam

Payload:

{
  streamId: string;
  totalGiftCount: number;
  timestamp: string; // ISO 8601 formatında
}

Örnek:

socket.on('giftCountUpdated', (data) => {
  updateGiftCount(data.streamId, data.totalGiftCount);
});

Yayın Durumu Eventleri

7. streamStarted

Yayın gerçekten başladığında (startedAt set edildiğinde), ilgili stream room'undaki tüm kullanıcılara gönderilir.

Payload:

{
  streamId: string;   // Yayın ID
  startedAt: string;  // ISO 8601 format
  plannedEndDate: string | null; // ISO 8601 format veya null
  timestamp: string;  // ISO 8601 format
}

Örnek:

socket.on('streamStarted', (data) => {
  console.log('Yayın başladı:', data.streamId, data.startedAt, data.plannedEndDate);
});

8. streamEnded

Bir yayın bittiğinde, overview room'undaki tüm kullanıcılara gönderilir. Ayrıca yayın room'undaki tüm kullanıcıların bağlantısı otomatik olarak kesilir.

Payload:

{
  streamId: string;
}

Örnek:

socket.on('streamEnded', (data) => {
  console.log('Yayın bitti:', data.streamId);
  // Homepage'den yayını kaldır
  removeStreamFromList(data.streamId);
});

Süre Uzatma Eventi

9. streamTimeExtensionAdded

Bir kullanıcı yayına süre eklediğinde, o yayının room'undaki tüm kullanıcılara gönderilir.

Payload:

{
  id: string; // Time extension kaydının ID'si
  streamId: string;
  senderId: string;
  durationSeconds: number; // Eklenen süre (saniye cinsinden)
  senderSnapshot: {
    _id: string;
    username: string | null;
    name: string | null;
    surname: string | null;
    profilePhoto: {
      _id: string;
      url: string;
      variants: Array<{
        size: number;
        url: string;
      }>;
    } | null;
  };
  createdAt: string; // ISO 8601 formatında
}

Örnek:

socket.on('streamTimeExtensionAdded', (data) => {
  console.log(`${data.senderSnapshot.username} ${data.durationSeconds} saniye ekledi`);
  // UI'da bildirim göster
  showTimeExtensionNotification(data);
});

Ban İşlemi Eventleri

10. userBanned

Bir kullanıcı yayından banlandığında, o yayının room'undaki tüm kullanıcılara gönderilir.

Payload:

{
  targetUserId: string; // Banlanan kullanıcının ID'si
  actionType: string; // 'BLOCK' veya diğer action tipleri
  reason?: string; // Ban nedeni (opsiyonel)
  scope: string; // 'STREAM' veya 'BROADCASTER'
  timestamp: string; // ISO 8601 formatında
}

Örnek:

socket.on('userBanned', (data) => {
  console.log(`Kullanıcı banlandı: ${data.targetUserId}`);
  // Eğer banlanan kullanıcı sizseniz, yayından çıkın
  if (data.targetUserId === currentUserId) {
    // Yayından ayrıl ve hata mesajı göster
    handleUserBanned(data);
  }
});

11. userBanRevoked

Bir kullanıcının banı kaldırıldığında, o yayının room'undaki tüm kullanıcılara gönderilir.

Payload:

{
  targetUserId: string; // Ban kaldırılan kullanıcının ID'si
  timestamp: string; // ISO 8601 formatında
}

Örnek:

socket.on('userBanRevoked', (data) => {
  console.log(`Kullanıcının banı kaldırıldı: ${data.targetUserId}`);
  // UI'da güncelleme yap
});

12. userKickedFromStream

Kullanıcı yetersiz bakiye veya başka bir nedenle yayından atıldığında, o yayının room'undaki tüm kullanıcılara gönderilir.

Payload:

{
  streamId: string;
  userId: string; // Atılan kullanıcının ID'si
  reason: string; // Atılma nedeni (örn: 'Insufficient credit balance')
  timestamp: string; // ISO 8601 formatında
}

Örnek:

socket.on('userKickedFromStream', (data) => {
  if (data.userId === currentUserId) {
    console.log(`Yayından atıldınız: ${data.reason}`);
    // Yayından çık ve hata mesajı göster
    handleUserKicked(data);
  } else {
    console.log(`Kullanıcı yayından atıldı: ${data.userId}`);
  }
});

Hata Eventleri

13. joinStreamError

Yayına katılma işlemi başarısız olduğunda gelir.

Payload:

{
  streamId: string;
  error: string; // Hata mesajı
}

Örnek:

socket.on('joinStreamError', (data) => {
  console.error('Yayına katılma hatası:', data.error);
  // Kullanıcıya hata mesajı göster
});

14. leaveStreamError

Yayından ayrılma işlemi başarısız olduğunda gelir.

Payload:

{
  streamId: string;
  error: string; // Hata mesajı
}

15. joinOverviewError

Overview room'una katılma işlemi başarısız olduğunda gelir.

Payload:

{
  error: string; // Hata mesajı
}

16. leaveOverviewError

Overview room'undan ayrılma işlemi başarısız olduğunda gelir.

Payload:

{
  error: string; // Hata mesajı
}

Viewer Count Yönetimi

Otomatik Artırma/Azaltma Mekanizması

Viewer count yönetimi tamamen otomatiktir:

  1. Katılma: joinStream eventi gönderildiğinde viewer count otomatik olarak artırılır
  2. Ayrılma: leaveStream eventi gönderildiğinde viewer count otomatik olarak azaltılır
  3. Disconnect: Kullanıcı bağlantısı kesildiğinde, katıldığı tüm stream room'larından otomatik olarak çıkarılır ve viewer count'ları azaltılır
  4. Yayın Bitişi: Yayın bittiğinde viewer count sıfırlanır ve aktif stream listesinden çıkarılır

Cache Yönetimi

Viewer count'lar Redis veya in-memory cache'de tutulur:

  • Key Formatı: liveStream:{streamId}:viewerCount
  • Aktif Stream Listesi: activeStreams:list

Viewer Count'un 0 Olması

Bir yayının viewer count'u 0'a düştüğünde:

  • Yayın aktif stream listesinden otomatik olarak çıkarılır
  • Overview room'undaki kullanıcılara streamViewerUpdated eventi gönderilir (viewerCount: 0)

React Native Kullanım Örnekleri

TypeScript Tip Tanımlamaları

Önce tip tanımlamalarını oluşturalım:

// types/live-stream-socket.types.ts

export interface JoinStreamPayload {
  streamId: string;
}

export interface LeaveStreamPayload {
  streamId: string;
}

export interface JoinedStreamResponse {
  streamId: string;
  viewerCount: number;
  success: boolean;
}

export interface LeftStreamResponse {
  streamId: string;
  viewerCount: number;
  success: boolean;
}

export interface ActiveStream {
  streamId: string;
  viewerCount: number;
}

export interface JoinedOverviewResponse {
  activeStreams: ActiveStream[];
  success: boolean;
}

export interface ViewerCountUpdate {
  streamId: string;
  viewerCount: number;
  timestamp: string;
}

export interface GiftCountUpdated {
  streamId: string;
  totalGiftCount: number;
  timestamp: string;
}

export interface StreamStartedPayload {
  streamId: string;
  startedAt: string; // ISO 8601
  plannedEndDate: string | null; // ISO 8601 veya null
  timestamp: string; // ISO 8601
}

export interface StreamEndedPayload {
  streamId: string;
}

export interface StreamTimeExtensionPayload {
  id: string;
  streamId: string;
  senderId: string;
  durationSeconds: number;
  senderSnapshot: {
    _id: string;
    username: string | null;
    name: string | null;
    surname: string | null;
    profilePhoto: {
      _id: string;
      url: string;
      variants: Array<{
        size: number;
        url: string;
      }>;
    } | null;
  };
  createdAt: string;
}

export interface UserBannedPayload {
  targetUserId: string;
  actionType: string;
  reason?: string;
  scope: string;
  timestamp: string;
}

export interface UserBanRevokedPayload {
  targetUserId: string;
  timestamp: string;
}

export interface UserKickedFromStreamPayload {
  streamId: string;
  userId: string;
  reason: string;
  timestamp: string;
}

export interface SocketErrorPayload {
  streamId?: string;
  error: string;
}

Socket Hook (Custom Hook)

// hooks/useLiveStreamSocket.ts

import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';
import {
  JoinedStreamResponse,
  LeftStreamResponse,
  JoinedOverviewResponse,
  ViewerCountUpdate,
  StreamStartedPayload,
  StreamEndedPayload,
  StreamTimeExtensionPayload,
  UserBannedPayload,
  UserBanRevokedPayload,
  UserKickedFromStreamPayload,
  SocketErrorPayload,
} from '../types/live-stream-socket.types';

const BASE_URL = 'https://api.allmine.win'; // Ortamınıza göre değiştirin

export const useLiveStreamSocket = () => {
  const socketRef = useRef<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [socketId, setSocketId] = useState<string | null>(null);

  useEffect(() => {
    // Socket bağlantısını oluştur
    const socket = io(`${BASE_URL}/live-stream`, {
      transports: ['websocket'],
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionAttempts: 5,
      reconnectionDelayMax: 5000,
    });

    socketRef.current = socket;

    // Bağlantı eventleri
    socket.on('connect', () => {
      console.log('Live Stream Socket bağlandı:', socket.id);
      setIsConnected(true);
      setSocketId(socket.id);
    });

    socket.on('disconnect', (reason) => {
      console.log('Live Stream Socket bağlantısı kesildi:', reason);
      setIsConnected(false);
      setSocketId(null);
    });

    socket.on('connect_error', (error) => {
      console.error('Live Stream Socket bağlantı hatası:', error);
      setIsConnected(false);
    });

    // Cleanup
    return () => {
      socket.disconnect();
      socketRef.current = null;
    };
  }, []);

  return {
    socket: socketRef.current,
    isConnected,
    socketId,
  };
};

Stream Detay Sayfası Component'i

// components/StreamDetailScreen.tsx

import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useLiveStreamSocket } from '../hooks/useLiveStreamSocket';
import {
  JoinedStreamResponse,
  ViewerCountUpdate,
  GiftCountUpdated,
  StreamTimeExtensionPayload,
  UserBannedPayload,
  UserKickedFromStreamPayload,
} from '../types/live-stream-socket.types';

interface StreamDetailScreenProps {
  streamId: string;
  currentUserId?: string;
}

export const StreamDetailScreen: React.FC<StreamDetailScreenProps> = ({
  streamId,
  currentUserId,
}) => {
  const { socket, isConnected } = useLiveStreamSocket();
  const [viewerCount, setViewerCount] = useState<number>(0);
  const [giftCount, setGiftCount] = useState<number>(0);
  const [timeExtensions, setTimeExtensions] = useState<StreamTimeExtensionPayload[]>([]);

  useEffect(() => {
    if (!socket || !isConnected) return;

    // Yayına katıl
    socket.emit('joinStream', { streamId });

    // Event listener'ları tanımla
    const handleJoinedStream = (data: JoinedStreamResponse) => {
      console.log('Yayına katıldınız:', data);
      setViewerCount(data.viewerCount);
    };

    const handleViewerCountUpdate = (data: ViewerCountUpdate) => {
      console.log('Viewer count güncellendi:', data);
      setViewerCount(data.viewerCount);
    };

    const handleGiftCountUpdate = (data: GiftCountUpdated) => {
      console.log('Gift count güncellendi:', data);
      setGiftCount(data.totalGiftCount);
    };

    const handleTimeExtension = (data: StreamTimeExtensionPayload) => {
      console.log('Süre eklendi:', data);
      setTimeExtensions((prev) => [data, ...prev]);
      // Bildirim göster
      // showNotification(`${data.senderSnapshot.username} ${data.durationSeconds} saniye ekledi`);
    };

    const handleUserBanned = (data: UserBannedPayload) => {
      if (data.targetUserId === currentUserId) {
        // Kullanıcı banlandı, yayından çık
        socket.emit('leaveStream', { streamId });
        // Hata mesajı göster
        // showError('Bu yayından banlandınız');
      }
    };

    const handleUserKicked = (data: UserKickedFromStreamPayload) => {
      if (data.userId === currentUserId) {
        // Kullanıcı yayından atıldı
        socket.emit('leaveStream', { streamId });
        // Hata mesajı göster
        // showError(`Yayından atıldınız: ${data.reason}`);
      }
    };

    const handleError = (data: { streamId: string; error: string }) => {
      console.error('Yayın hatası:', data.error);
      // Hata mesajı göster
    };

    // Event listener'ları ekle
    socket.on('joinedStream', handleJoinedStream);
    socket.on('viewerCountUpdated', handleViewerCountUpdate);
    socket.on('giftCountUpdated', handleGiftCountUpdate);
    socket.on('streamTimeExtensionAdded', handleTimeExtension);
    socket.on('userBanned', handleUserBanned);
    socket.on('userKickedFromStream', handleUserKicked);
    socket.on('joinStreamError', handleError);

    // Cleanup
    return () => {
      // Yayından ayrıl
      socket.emit('leaveStream', { streamId });
      
      // Event listener'ları kaldır
      socket.off('joinedStream', handleJoinedStream);
      socket.off('viewerCountUpdated', handleViewerCountUpdate);
      socket.off('giftCountUpdated', handleGiftCountUpdate);
      socket.off('streamTimeExtensionAdded', handleTimeExtension);
      socket.off('userBanned', handleUserBanned);
      socket.off('userKickedFromStream', handleUserKicked);
      socket.off('joinStreamError', handleError);
    };
  }, [socket, isConnected, streamId, currentUserId]);

  if (!isConnected) {
    return (
      <View style={styles.container}>
        <Text>Bağlanılıyor...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.viewerCount}>İzleyici: {viewerCount}</Text>
      <Text style={styles.viewerCount}>Toplam Gift: {giftCount}</Text>
      
      {timeExtensions.length > 0 && (
        <View style={styles.extensionsContainer}>
          <Text style={styles.extensionsTitle}>Süre Eklemeleri:</Text>
          {timeExtensions.slice(0, 5).map((ext) => (
            <Text key={ext.id} style={styles.extensionText}>
              {ext.senderSnapshot.username} +{ext.durationSeconds}s
            </Text>
          ))}
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
  },
  viewerCount: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  extensionsContainer: {
    marginTop: 16,
  },
  extensionsTitle: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
  },
  extensionText: {
    fontSize: 14,
    marginBottom: 4,
  },
});

Homepage/Stream List Component'i

// components/StreamListScreen.tsx

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';
import { useLiveStreamSocket } from '../hooks/useLiveStreamSocket';
import {
  ActiveStream,
  JoinedOverviewResponse,
  ViewerCountUpdate,
  StreamEndedPayload,
} from '../types/live-stream-socket.types';

export const StreamListScreen: React.FC = () => {
  const { socket, isConnected } = useLiveStreamSocket();
  const [activeStreams, setActiveStreams] = useState<ActiveStream[]>([]);

  useEffect(() => {
    if (!socket || !isConnected) return;

    // Overview room'una katıl
    socket.emit('joinOverview');

    // Event listener'ları tanımla
    const handleJoinedOverview = (data: JoinedOverviewResponse) => {
      console.log('Overview\'a katıldınız:', data);
      setActiveStreams(data.activeStreams);
    };

    const handleStreamViewerUpdate = (data: ViewerCountUpdate) => {
      console.log('Stream viewer güncellendi:', data);
      setActiveStreams((prev) =>
        prev.map((stream) =>
          stream.streamId === data.streamId
            ? { ...stream, viewerCount: data.viewerCount }
            : stream
        ).filter((stream) => stream.viewerCount > 0) // 0 viewer olanları kaldır
      );
    };

    const handleStreamEnded = (data: StreamEndedPayload) => {
      console.log('Yayın bitti:', data);
      setActiveStreams((prev) =>
        prev.filter((stream) => stream.streamId !== data.streamId)
      );
    };

    const handleError = (data: { error: string }) => {
      console.error('Overview hatası:', data.error);
    };

    // Event listener'ları ekle
    socket.on('joinedOverview', handleJoinedOverview);
    socket.on('streamViewerUpdated', handleStreamViewerUpdate);
    socket.on('streamEnded', handleStreamEnded);
    socket.on('joinOverviewError', handleError);

    // Cleanup
    return () => {
      // Overview'dan ayrıl
      socket.emit('leaveOverview');
      
      // Event listener'ları kaldır
      socket.off('joinedOverview', handleJoinedOverview);
      socket.off('streamViewerUpdated', handleStreamViewerUpdate);
      socket.off('streamEnded', handleStreamEnded);
      socket.off('joinOverviewError', handleError);
    };
  }, [socket, isConnected]);

  if (!isConnected) {
    return (
      <View style={styles.container}>
        <Text>Bağlanılıyor...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Aktif Yayınlar</Text>
      <FlatList
        data={activeStreams}
        keyExtractor={(item) => item.streamId}
        renderItem={({ item }) => (
          <View style={styles.streamItem}>
            <Text style={styles.streamId}>{item.streamId}</Text>
            <Text style={styles.viewerCount}>
              {item.viewerCount} izleyici
            </Text>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>Aktif yayın yok</Text>
        }
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  streamItem: {
    padding: 12,
    backgroundColor: '#f0f0f0',
    borderRadius: 8,
    marginBottom: 8,
  },
  streamId: {
    fontSize: 16,
    fontWeight: '600',
  },
  viewerCount: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  emptyText: {
    textAlign: 'center',
    color: '#999',
    marginTop: 32,
  },
});

Tam Entegrasyon Örneği

// screens/LiveStreamScreen.tsx

import React, { useEffect, useRef } from 'react';
import { View, Button, Alert } from 'react-native';
import { useLiveStreamSocket } from '../hooks/useLiveStreamSocket';
import { StreamDetailScreen } from '../components/StreamDetailScreen';

export const LiveStreamScreen: React.FC<{ streamId: string }> = ({ streamId }) => {
  const { socket, isConnected } = useLiveStreamSocket();

  const handleJoinStream = () => {
    if (!socket || !isConnected) {
      Alert.alert('Hata', 'Socket bağlantısı yok');
      return;
    }

    socket.emit('joinStream', { streamId });
  };

  const handleLeaveStream = () => {
    if (!socket || !isConnected) {
      return;
    }

    socket.emit('leaveStream', { streamId });
  };

  return (
    <View style={{ flex: 1 }}>
      <View style={{ padding: 16, flexDirection: 'row', gap: 8 }}>
        <Button title="Yayına Katıl" onPress={handleJoinStream} />
        <Button title="Yayından Ayrıl" onPress={handleLeaveStream} />
      </View>
      
      <StreamDetailScreen streamId={streamId} />
    </View>
  );
};

Best Practices

1. Bağlantı Yönetimi

  • Tek Socket Instance: Uygulama genelinde tek bir socket instance kullanın (singleton pattern veya context API)
  • Reconnection: Socket.IO otomatik reconnection sağlar, ancak manuel kontrol de ekleyebilirsiniz
  • Connection State: Bağlantı durumunu takip edin ve kullanıcıya bilgi verin
// Bağlantı durumunu kontrol et
if (!socket || !socket.connected) {
  // Bağlantı yok, işlem yapma
  return;
}

2. Memory Leak Önleme

  • Event Listener Cleanup: useEffect cleanup fonksiyonunda tüm event listener'ları kaldırın
  • Component Unmount: Component unmount olduğunda socket'ten ayrılın (leaveStream, leaveOverview)
  • Ref Kullanımı: Socket instance'ını ref ile saklayın, state'e koymayın
useEffect(() => {
  // Event listener ekle
  socket.on('event', handler);
  
  return () => {
    // Cleanup: Event listener'ı kaldır
    socket.off('event', handler);
    // Room'lardan ayrıl
    socket.emit('leaveStream', { streamId });
  };
}, [socket, streamId]);

3. Reconnection Stratejileri

  • Otomatik Reconnection: Socket.IO varsayılan olarak otomatik reconnect yapar
  • Manuel Reconnection: Gerekirse manuel reconnect ekleyebilirsiniz
socket.on('disconnect', (reason) => {
  if (reason === 'io server disconnect') {
    // Server tarafından kesildi, manuel reconnect gerekebilir
    socket.connect();
  }
  // Diğer durumlarda otomatik reconnect çalışır
});

4. Error Handling

  • Tüm Error Eventlerini Dinleyin: joinStreamError, leaveStreamError, vb.
  • Kullanıcıya Bilgi Verin: Hataları kullanıcıya anlaşılır şekilde gösterin
  • Retry Mekanizması: Kritik işlemler için retry mekanizması ekleyin
const joinStreamWithRetry = async (streamId: string, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    try {
      socket.emit('joinStream', { streamId });
      // Başarılı olursa döngüden çık
      await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => reject(new Error('Timeout')), 5000);
        socket.once('joinedStream', () => {
          clearTimeout(timeout);
          resolve(true);
        });
        socket.once('joinStreamError', (error) => {
          clearTimeout(timeout);
          reject(error);
        });
      });
      return;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
};

5. Performance Optimizasyonları

  • Debounce/Throttle: Sık güncellenen eventler için debounce veya throttle kullanın
  • Conditional Updates: Sadece gerekli durumlarda state güncellemesi yapın
  • Memoization: Pahalı hesaplamalar için useMemo kullanın
// Viewer count güncellemelerini throttle et
const throttledUpdateViewerCount = useMemo(
  () => throttle((count: number) => {
    setViewerCount(count);
  }, 1000),
  []
);

socket.on('viewerCountUpdated', (data) => {
  throttledUpdateViewerCount(data.viewerCount);
});

6. Testing

  • Mock Socket: Testlerde socket'i mock'layın
  • Event Simulation: Event'leri manuel olarak tetikleyin
  • Connection State Testing: Farklı bağlantı durumlarını test edin
// Test örneği
import { io } from 'socket.io-client';

const mockSocket = {
  emit: jest.fn(),
  on: jest.fn(),
  off: jest.fn(),
  disconnect: jest.fn(),
  connected: true,
};

jest.mock('socket.io-client', () => ({
  io: jest.fn(() => mockSocket),
}));

7. Güvenlik

  • Input Validation: Client tarafında da input validation yapın
  • Rate Limiting: Çok sık event göndermeyi engelleyin
  • Authentication: Gelecekte authentication eklendiğinde token'ı güvenli şekilde saklayın

Özet

Live Stream Gateway, canlı yayın uygulamanız için güçlü bir WebSocket çözümü sunar. Bu dökümantasyonu takip ederek:

  • ✅ WebSocket bağlantısı kurabilirsiniz
  • ✅ Yayınlara katılıp ayrılabilirsiniz
  • ✅ Viewer count güncellemelerini alabilirsiniz
  • ✅ Yayın durumu değişikliklerini takip edebilirsiniz
  • ✅ Süre uzatma ve ban işlemlerini yönetebilirsiniz
  • ✅ Homepage için overview room'unu kullanabilirsiniz

Herhangi bir sorunuz veya sorununuz olursa, backend ekibiyle iletişime geçebilirsiniz.

On this page