Vietlott Integration API Docs

Tài Liệu Kết Nối Livestream Vietlott

Hướng dẫn dành cho bên thứ ba, lập trình viên Web & Mobile App (Flutter, React Native, iOS, Android) kết nối phát sóng, đếm người xem đồng thời và tương tác chat trực tiếp.

1. Thông Tin Chung & Base URL

Hệ thống cung cấp API RESTful JSON chuẩn cùng hạ tầng streaming độ trễ thấp qua OvenMediaEngine và Centrifugo WebSocket.

Thành phần Địa chỉ kết nối (URL) Mô tả
API Base URL https://api-live.levanbac.io.vn RESTful API chính thức
WebRTC Server wss://watch.levanbac.io.vn/app Xem luồng video siêu nhanh (Độ trễ < 1s)
HLS / LL-HLS Server https://watch.levanbac.io.vn/app Xem qua HLS chuyển mã tự động (ABR 1080p, 720p...)
Realtime Chat WS wss://chat.levanbac.io.vn/connection/websocket Centrifugo WebSocket cho Live Chat

2. Luồng Phát Video Trực Tiếp (Playback)

Trước khi khởi tạo Video Player trên ứng dụng, client gọi API sau để lấy URL phát sóng:

GET /api/channels/:id/watch

Response mẫu (200 OK):

JSON Response
{
  "status": "success",
  "data": {
    "channel": {
      "id": 19,
      "name": "Kênh Vietlott 1",
      "status": "live",
      "public_id": "9df7466c-e934-4585-8bb3-4463af0c6a7d",
      "transcode_enabled": true
    },
    "playback": {
      "webrtc_url": "wss://watch.levanbac.io.vn/app/7fb4cacb4a4c893d5a0be963e5dc6246",
      "hls_url": "https://watch.levanbac.io.vn/app/7fb4cacb4a4c893d5a0be963e5dc6246/master.m3u8",
      "llhls_url": "https://watch.levanbac.io.vn/app/7fb4cacb4a4c893d5a0be963e5dc6246/llhls.m3u8"
    }
  }
        

2.5. Cơ Chế Dừng Phiên Livestream & Ngắt Luồng OBS Cưỡng Chế (Kick Stream & Khóa Quyền)

Khi bên thứ 3 hoặc quản trị viên gửi lệnh dừng livestream, hệ thống kích hoạt cơ chế bảo vệ 2 lớp để ngắt hoàn toàn kết nối OBS và chặn OBS tự động kết nối lại (Auto-Reconnect):

POST /api/channels/:id/stop

Header yêu cầu: X-Api-Key: your-secret-api-key-here. Tham số :id hỗ trợ cả ID số nguyên hoặc UUID public_id.

CƠ CHẾ HOẠT ĐỘNG 2 LỚP:
1. Lớp 1 - Ngắt kết nối Ingress TCP RTMP ngay lập tức: CMS tự động quét tìm stream active trên OvenMediaEngine và gửi lệnh DELETE /v1/vhosts/{vhost}/apps/{app}/streams/{streamName}. Bitrate trên OBS lập tức tụt về 0 kb/s, trạng thái chuyển thành Disconnected / Reconnecting... và luồng phát tới khán giả tắt ngay.
2. Lớp 2 - Khóa quyền phát sóng (is_active = false) - Chặn Auto-Reconnect: Khi OBS cố gắng kết nối lại, OME Admission Webhook lập tức phản hồi DENY (allowed: false), khiến OBS từ chối kết nối vĩnh viễn và dừng hẳn việc chiếm dụng tài nguyên máy chủ.

Mở lại quyền phát sóng cho phiên live tiếp theo:

POST /api/channels/:id/allow-stream

Chuyển is_active = true, cho phép streamer mở lại OBS phát sóng khi đến giờ live mới.

3. Cơ Chế Đếm Lượt Xem Chuẩn Xác (Viewer Heartbeat)

QUY TẮC BẮT BUỘC KHI TÍCH HỢP PLAYER:
1. Lưu cố định device_uid vào thiết bị (không đổi khi mở lại app).
2. Khi video đang chạy, gửi request Heartbeat định kỳ mỗi 8 giây.
3. Khi thoát màn hình video, gọi API Leave để hệ thống giảm số người xem ngay lập tức.

3.1. Gửi Heartbeat định kỳ (Mỗi 8 giây)

POST /api/channels/:id/viewers/heartbeat
JSON Request
{
  "viewer_uid": "device-uuid-vietlott-001"
}

3.2. Báo thoát khi đóng màn hình Player (Leave Event)

POST /api/channels/:id/viewers/leave
JSON Request
{
  "viewer_uid": "device-uuid-vietlott-001"
}

3.3. Lấy Số Liệu Thống Kê View Hiển Thị

GET /api/channels/:id/viewers
JSON Response
{
  "status": "success",
  "data": {
    "current_viewers": 28,  // Số người xem trực tiếp lúc này
    "peak_viewers": 150,    // Kỷ lục xem cùng lúc cao nhất trong phiên
    "total_views": 1420,    // Tổng lượt vào xem tích lũy
    "is_live": true         // Kênh đang phát sóng hay không
  }
}

3.4. Kho Video VOD & Phát HLS Bảo Mật (.m3u8)

Truy xuất danh sách video ghi hình từ MinIO S3. Mỗi video có sẵn hls_url bảo mật (chỉ dùng public_id, giấu kín 100% stream_key và không lo lỗi CORS):

GET /api/videos/s3
JSON Response
{
  "status": "success",
  "data": [
    {
      "public_id": "9df7466c-e934-4585-8bb3-4463af0c6a7d",
      "channel_name": "Kênh Vietlott 1",
      "files": [
        {
          "object_name": "recordings/default/app/9df7466c-e934-4585-8bb3-4463af0c6a7d/20260826032526_20260826033228.mp4",
          "size": 147757109,
          "last_modified": "2026-08-26T03:33:08Z",
          "hls_url": "https://api-live.levanbac.io.vn/api/vod/stream/9df7466c-e934-4585-8bb3-4463af0c6a7d/20260826032526_20260826033228/playlist.m3u8"
        }
      ]
    }
  ]
}

3.5. Tải Video Lên Hệ Thống (Upload Video VOD & Tự Động Phân Đoạn HLS)

Hệ thống cung cấp API chuẩn multipart/form-data để tải video MP4/MOV/MKV lên MinIO S3. Khi tải lên thành công, Server tự động phân đoạn HLS (.m3u8 & .ts) để phát bảo mật.

POST /api/videos/upload

Form Fields (multipart/form-data):

FieldKiểuBắt buộcMô tả
videoFile (Binary)File video định dạng .mp4, .mov, .mkv
titleStringTiêu đề video
public_idStringKhôngPublic ID kênh liên kết (Bảo mật, thay thế cho Stream Key)
descriptionStringKhôngMô tả chi tiết nội dung video
JSON Response (201 Created)
{
  "status": "success",
  "channel_public_id": "9df7466c-e934-4585-8bb3-4463af0c6a7d",
  "data": {
    "id": 12,
    "title": "Video Quay Số Ngày 26/08",
    "description": "Bản ghi quay số mở thưởng",
    "object_name": "recordings/default/app/9df7466c-e934-4585-8bb3-4463af0c6a7d/1787654321_video.mp4",
    "public_url": "https://s3.vtcmobile.vn/live-vietlott/recordings/default/app/9df7466c-e934-4585-8bb3-4463af0c6a7d/1787654321_video.mp4",
    "file_size": 14582910,
    "status": "active"
  },
  "hls_url": "https://s3.vtcmobile.vn/live-vietlott/hls/default/app/9df7466c-e934-4585-8bb3-4463af0c6a7d/1787654321_video/playlist.m3u8"
}

Code Mẫu Tích Hợp:

JavaScript (Có theo dõi % Progress Bar)
const formData = new FormData();
formData.append('title', 'Video Quay Thưởng 26/08');
formData.append('public_id', '9df7466c-e934-4585-8bb3-4463af0c6a7d');
formData.append('video', fileInput.files[0]);

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api-live.levanbac.io.vn/api/videos/upload', true);
xhr.setRequestHeader('X-Api-Key', 'your-secret-api-key-here');

xhr.upload.onprogress = (e) => {
  if (e.lengthComputable) {
    const percent = Math.round((e.loaded / e.total) * 100);
    console.log(`Đang tải lên: ${percent}%`);
  }
};

xhr.onload = () => {
  if (xhr.status === 201 || xhr.status === 200) {
    const res = JSON.parse(xhr.responseText);
    console.log('Upload thành công, HLS URL:', res.hls_url);
  }
};
xhr.send(formData);
cURL
curl -X POST "https://api-live.levanbac.io.vn/api/videos/upload" \
  -H "X-Api-Key: your-secret-api-key-here" \
  -F "title=Video Quay Thưởng 26/08" \
  -F "public_id=9df7466c-e934-4585-8bb3-4463af0c6a7d" \
  -F "description=Mô tả chi tiết video..." \
  -F "video=@/path/to/local/video.mp4"
Flutter / Dart
import 'package:http/http.dart' as http;
import 'dart:convert';

var uri = Uri.parse('https://api-live.levanbac.io.vn/api/videos/upload');
var req = http.MultipartRequest('POST', uri)
  ..headers['X-Api-Key'] = 'your-secret-api-key-here'
  ..fields['title'] = 'Video Quay Thưởng'
  ..fields['public_id'] = '9df7466c-e934-4585-8bb3-4463af0c6a7d'
  ..files.add(await http.MultipartFile.fromPath('video', filePath));

var streamedResponse = await req.send();
var res = await http.Response.fromStream(streamedResponse);
var data = jsonDecode(res.body);
print('HLS URL: ${data['hls_url']}');
React Native / Axios
import axios from 'axios';

const formData = new FormData();
formData.append('title', 'Video Quay Thưởng');
formData.append('public_id', '9df7466c-e934-4585-8bb3-4463af0c6a7d');
formData.append('video', {
  uri: fileUri,
  name: 'video.mp4',
  type: 'video/mp4',
});

const res = await axios.post('https://api-live.levanbac.io.vn/api/videos/upload', formData, {
  headers: {
    'X-Api-Key': 'your-secret-api-key-here',
    'Content-Type': 'multipart/form-data',
  },
  onUploadProgress: (e) => {
    const percent = Math.round((e.loaded * 100) / e.total);
    console.log(`Tiến trình: ${percent}%`);
  },
});
console.log('HLS Stream:', res.data.hls_url);

3.6. Cơ Chế Job Tự Động Quản Lý & Đồng Bộ VOD Lên S3 Từ File Gốc (Local Retention & DB Tracking)

Hệ thống tích hợp Background Worker tự động quét và đồng bộ các file video ghi hình MP4 từ máy chủ OvenMediaEngine lên lưu trữ đám mây S3 / MinIO, đồng thời tự động phân đoạn HLS đa phân giải chuẩn bảo mật:

CÁC CẢI TIẾN TRỌNG YẾU (THAY THẾ SCRIPT NGOÀI):
Bảo toàn file gốc & Quét đa thư mục (Multi-Directory Scanning): Tự động quét toàn bộ các thư mục bản ghi khả dụng trên máy chủ (/opt/ovenmediaengine/recordings, /data/ovenmedia/recordings). File MP4 gốc được giữ nguyên vẹn 100%, tuyệt đối không tự động xóa sau khi upload, đảm bảo an toàn lưu trữ và đối soát.
Cập nhật Database tức thì (Immediate Persistence): Ngay khi file MP4 hoàn tất tải lên S3, hệ thống lập tức cập nhật trạng thái is_synced = true, synced_at = NOW() vào bảng video_records và bảng videos theo cặp khóa chuẩn stream_key + file_name. Trạng thái không bị gián đoạn hay mất mát kể cả khi quá trình phân đoạn HLS tốn thời gian.
Dò tìm Audio AAC bằng ffprobe: File video ghi hình OME chứa nhiều audio track (Opus và AAC). Worker tự động phát hiện index chính xác của track AAC để remux HLS (-c copy), kèm cờ -nostdin và timeout an toàn, khắc phục hoàn toàn lỗi mất tiếng khi phát HLS trên trình duyệt Web/Mobile.
Kích hoạt Worker 2 chế độ: Tự động kích hoạt định kỳ qua Background Worker và ngay khi phiên live kết thúc / stop stream, đồng thời hỗ trợ gọi thủ công theo yêu cầu qua API hoặc nút bấm trên CMS.
Bắn Webhook recording.completed: Ngay khi file đồng bộ lên S3 và cập nhật VOD thành công, hệ thống tự động phát tán sự kiện Outbound Webhook recording.completed đến tất cả endpoint của bên thứ ba (chứa objectId là Channel ID, mediaId = null, link S3 MP4 & HLS, file_name, file_path và dung lượng file).

API Đồng Bộ Thủ Công VOD Lên S3 (Manual Sync):

POST /api/raw-recordings/sync

Header yêu cầu: X-Api-Key: your-secret-api-key-here

JSON Request Body:

Request
{
  "files": [
    "default/app/ch_7fb4cacb/20260917110000_20260917112000.mp4"
  ]
}

JSON Response Mẫu (200 OK):

Response
{
  "status": "success",
  "message": "Đã đồng bộ thành công 1/1 file lên S3 và lưu vào Database!",
  "data": {
    "total": 1,
    "success_count": 1,
    "results": [
      {
        "file": "default/app/ch_7fb4cacb/20260917110000_20260917112000.mp4",
        "status": "success",
        "hls_url": "https://s3.exo.io.vn/live-vietlott/hls/default/app/ch_7fb4cacb/20260917110000_20260917112000/playlist.m3u8"
      }
    ]
  }
}

API Tra Cứu Danh Sách Bản Ghi Gốc & Trạng Thái Đồng Bộ:

GET /api/raw-recordings

Trả về danh sách bản ghi gốc trên máy chủ. Trường is_synced_s3 chỉ có giá trị true khi file đã thực sự được tải lên S3 và được xác nhận trong cơ sở dữ liệu.

3.8. Cấu Hình Nhà Cung Cấp CDN Phân Phối VOD (Thay Thế Link S3)

Hệ thống cho phép cấu hình Nhà cung cấp CDN (ví dụ: VN Network, Akamai, Cloudflare) trực tiếp từ giao diện Admin. Khi một cấu hình CDN được kích hoạt làm Mặc định VOD, toàn bộ các đường dẫn phát HLS (hls_url, playlist.m3u8) và tải MP4 sẽ tự động thay thế địa chỉ MinIO S3 bằng domain CDN đã cấu hình.

API Quản Lý Nhà Cung Cấp CDN:

GET /api/cdn-providers Lấy danh sách nhà cung cấp & cấu hình
POST /api/cdn-providers Tạo mới nhà cung cấp
POST /api/cdn-providers/:id/configs Thêm cấu hình kết nối CDN
POST /api/cdn-providers/:id/configs/:config_id/set-default Kích hoạt làm CDN mặc định cho VOD
POST /api/cdn-providers/:id/configs/:config_id/test Kiểm tra kết nối CDN thời gian thực

Quy Tắc Chuyển Đổi URL Phát VOD:

  • Trước khi bật CDN (Origin S3):
    https://s3.levanbac.io.vn/live-vietlott/hls/default/app/ch_7fb4/20260918_1000/playlist.m3u8
  • Sau khi bật CDN (ví dụ https://cdnvnw-ori1-ottcore.vtccore.com/):
    https://cdnvnw-ori1-ottcore.vtccore.com/hls/default/app/ch_7fb4/20260918_1000/playlist.m3u8
  • Cơ chế Fallback: Nếu tắt cấu hình CDN hoặc xóa khỏi hệ thống, backend tự động fallback về link MinIO S3 mặc định mà không làm gián đoạn việc phát video.

3.9. Hướng Dẫn Các Bước Tích Hợp Biên Tập & Cắt Video (Step-by-Step Workflow)

Quy trình trọn gói dành cho đối tác / lập trình viên Web & Mobile App khi tích hợp tính năng biên tập, trích xuất đoạn clip trúng thưởng từ video ghi hình livestream gốc, lấy link HLS phát ngay tức thì và đẩy video lên kênh YouTube:

SƠ ĐỒ 6 BƯỚC TÍCH HỢP TOÀN DIỆN:

1. Quét File Gốc:
GET /api/videos/s3
2. Lấy Video ID:
POST /api/videos/ensure
3. Cắt Video FFmpeg:
POST /api/videos/:id/editor
4. Lắng Nghe SSE (Tùy chọn):
SSE video_trim_status
5. Phát Luồng HLS:
Nhúng hls_url vào Trình phát
6. Đẩy Lên YouTube (Tùy chọn):
POST /api/videos/:id/youtube

BƯỚC 1: Quét Danh Sách File Video Gốc (Raw Recordings Trên S3)

Ứng dụng gọi API này để hiển thị danh sách các file video MP4 ghi hình vừa livestream xong đang lưu trữ trên MinIO S3:

GET /api/videos/s3 Header: X-Api-Key: your-secret-api-key-here

Dữ liệu trả về gồm mảng các file video MP4 gom theo kênh, mỗi file có object_name, public_url, size và thời gian ghi hình.

BƯỚC 2: Khởi Tạo / Lấy Video ID Gốc (Ensure Video)

Khi người dùng chọn một file MP4 từ Bước 1, gọi API này để hệ thống tạo hoặc lấy bản ghi Video trong Database và trả về data.id:

POST /api/videos/ensure Header: X-Api-Key: your-secret-api-key-here
JSON Request Body
{
  "object_name": "recordings/default/app/0c8086da-3580-4dc7-9995-cd443c977b7a/20260917071252_20260917071327.mp4",
  "public_url": "https://cdn-vod.exo.io.vn/live-vietlott/recordings/default/app/0c8086da-3580-4dc7-9995-cd443c977b7a/20260917071252_20260917071327.mp4",
  "size": 20362703
}
JSON Response (200 OK)
{
  "status": "success",
  "hls_url": "https://cdn-vod.exo.io.vn/live-vietlott/hls/default/app/0c8086da-3580-4dc7-9995-cd443c977b7a/20260917071252_20260917071327/playlist.m3u8",
  "data": {
    "id": 69,
    "title": "20260917071252_20260917071327.mp4",
    "stream_key": "0c8086da-3580-4dc7-9995-cd443c977b7a",
    "object_name": "recordings/default/app/0c8086da-3580-4dc7-9995-cd443c977b7a/20260917071252_20260917071327.mp4"
  }
}

💡 Lưu lại data.id (ví dụ: 69) để truyền vào API cắt video ở Bước 3.

BƯỚC 3: Gọi API Cắt Video Bằng FFmpeg (Async Background Goroutine)

Gửi mốc thời gian bắt đầu (start_time_sec) và kết thúc (end_time_sec) vào ID video gốc:

POST /api/videos/:id/editor Header: X-Api-Key: your-secret-api-key-here | :id là ID video gốc (ví dụ: 69)
Tham số Body (JSON)KiểuBắt buộcMô tả
start_time_secIntegerGiây bắt đầu cắt (ví dụ: 10)
end_time_secIntegerGiây kết thúc cắt (ví dụ: 45)
titleStringKhôngTiêu đề clip video mới
descriptionStringKhôngMô tả nội dung clip đã cắt
JSON Response (200 OK - Tinh gọn ID & HLS URL phản hồi < 0.4s)
{
  "status": "success",
  "message": "Đã tạo video mới thành công và đang xử lý cắt video ngầm",
  "data": {
    "id": 70,
    "hls_url": "https://cdn-vod.exo.io.vn/live-vietlott/hls/trimmed/0c8086da-3580-4dc7-9995-cd443c977b7a/trimmed_70_v1/playlist.m3u8"
  }
}
3 QUY TẮC AN TOÀN TRỌNG YẾU:
Không ghi đè video gốc: Video cắt được tạo thành bản ghi Video MỚI HOÀN TOÀN với ID mới (70), video gốc (69) giữ nguyên 100%.
Chống timeout: FFmpeg render và upload ngầm trong Goroutine với context timeout 2 giờ, API phản hồi cho Client ngay lập tức trong < 0.4 giây.
Pre-computed Deterministic HLS URL: Link hls_url được tính toán cố định chuẩn xác, client có thể lưu vào DB hoặc nạp vào Player để chờ phát.

BƯỚC 4: Theo Dõi Trạng Thái Cắt Video Qua SSE (Tùy chọn)

Nếu ứng dụng muốn cập nhật trạng thái "Đang cắt..." ➔ "Đã hoàn thành" theo thời gian thực:

JavaScript EventSource (SSE)
const sse = new EventSource('https://api-live.levanbac.io.vn/api/events');
sse.addEventListener('video_trim_status', (e) => {
  const data = JSON.parse(e.data);
  // data = { video_id: 70, status: "completed", hls_url: "...", trimmed_url: "..." }
  if (data.status === 'completed') {
    console.log(`✅ Video #${data.video_id} đã hoàn tất cắt và sẵn sàng phát!`);
  }
});

BƯỚC 5: Nhúng Đường Dẫn HLS Vào Trình Phát (Web & Mobile App)

Sử dụng data.hls_url nhận được từ Bước 3 để phát luồng trên ứng dụng:

  • Web: Khởi tạo qua thư viện Hls.js (hoặc thẻ <video src="..."> trên Safari iOS).
  • Flutter: Khởi tạo VideoPlayerController.networkUrl(Uri.parse(hlsUrl)).
  • React Native: Truyền vào <Video source={{ uri: hlsUrl }} />.
  • Android: Cung cấp URL cho MediaItem.fromUri(hlsUrl) trên ExoPlayer.
  • iOS: Khởi tạo AVPlayer(url: URL(string: hlsUrl)!).

BƯỚC 6: Đẩy Video Đã Cắt Lên YouTube Bằng ID (Tùy chọn)

Khi muốn đăng tải đoạn video vừa cắt lên kênh YouTube chính thức của đài, chỉ cần truyền ID video vừa cắt vào URL:

POST /api/videos/:id/youtube Header: X-Api-Key: your-secret-api-key-here | :id là ID video vừa cắt (ví dụ: 70)
JSON Response (200 OK)
{
  "status": "success",
  "message": "Đã đẩy video lên YouTube thành công",
  "data": {
    "id": 70,
    "youtube_video_id": "8aJkF9zL2xM",
    "youtube_url": "https://youtu.be/8aJkF9zL2xM"
  }
}

CODE MẪU TRỌN GÓI TOÀN BỘ QUY TRÌNH:

JavaScript (Fetch Full Workflow)
const API_BASE = 'https://api-live.levanbac.io.vn';
const API_KEY = 'your-secret-api-key-here';
const headers = { 'Content-Type': 'application/json', 'X-Api-Key': API_KEY };

async function runVideoEditWorkflow(objectName, publicUrl, size, startSec, endSec, title) {
  // Bước 2: Ensure video record & lấy ID gốc
  const ensureRes = await fetch(`${API_BASE}/api/videos/ensure`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ object_name: objectName, public_url: publicUrl, size })
  });
  const { data: sourceVideo } = await ensureRes.json();
  console.log(`1️⃣ Video gốc ID: ${sourceVideo.id}`);

  // Bước 3: Cắt video FFmpeg Async (trả về video mới ID độc lập)
  const trimRes = await fetch(`${API_BASE}/api/videos/${sourceVideo.id}/editor`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      start_time_sec: startSec,
      end_time_sec: endSec,
      title: title
    })
  });
  const { data: newTrimmedVideo } = await trimRes.json();
  console.log(`2️⃣ Video cắt mới ID: ${newTrimmedVideo.id}`);
  console.log(`3️⃣ HLS URL phát video: ${newTrimmedVideo.hls_url}`);

  // Bước 6 (Tùy chọn): Đẩy lên YouTube
  const ytRes = await fetch(`${API_BASE}/api/videos/${newTrimmedVideo.id}/youtube`, {
    method: 'POST',
    headers: { 'X-Api-Key': API_KEY }
  });
  const { data: ytData } = await ytRes.json();
  console.log(`4️⃣ Đã đẩy YouTube thành công: ${ytData.youtube_url}`);

  return newTrimmedVideo;
}
cURL
# Bước 1: Quét danh sách file S3
curl -X GET "https://api-live.levanbac.io.vn/api/videos/s3" \
  -H "X-Api-Key: your-secret-api-key-here"

# Bước 2: Khởi tạo/Lấy ID video gốc
curl -X POST "https://api-live.levanbac.io.vn/api/videos/ensure" \
  -H "X-Api-Key: your-secret-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"object_name":"recordings/default/app/ch_key/video.mp4","public_url":"https://.../video.mp4","size":20362703}'

# Bước 3: Cắt video tạo video mới độc lập (nhận id mới và hls_url)
curl -X POST "https://api-live.levanbac.io.vn/api/videos/69/editor" \
  -H "X-Api-Key: your-secret-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"start_time_sec":10,"end_time_sec":45,"title":"Clip Jackpot"}'

# Bước 6: Đẩy video đã cắt lên YouTube bằng ID
curl -X POST "https://api-live.levanbac.io.vn/api/videos/70/youtube" \
  -H "X-Api-Key: your-secret-api-key-here"
Flutter / Dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class VideoEditorWorkflow {
  static const String baseUrl = 'https://api-live.levanbac.io.vn';
  static const String apiKey = 'your-secret-api-key-here';

  static Future> cutAndPublish({
    required String objectName,
    required String publicUrl,
    required int size,
    required int startSec,
    required int endSec,
    required String title,
  }) async {
    final headers = {'Content-Type': 'application/json', 'X-Api-Key': apiKey};

    // 1. Ensure video gốc
    final r1 = await http.post(Uri.parse('$baseUrl/api/videos/ensure'),
      headers: headers,
      body: jsonEncode({'object_name': objectName, 'public_url': publicUrl, 'size': size}),
    );
    final sourceId = jsonDecode(r1.body)['data']['id'];

    // 2. Cắt video
    final r2 = await http.post(Uri.parse('$baseUrl/api/videos/$sourceId/editor'),
      headers: headers,
      body: jsonEncode({'start_time_sec': startSec, 'end_time_sec': endSec, 'title': title}),
    );
    final trimmedData = jsonDecode(r2.body)['data']; // {'id': 70, 'hls_url': '...'}

    // 3. Đẩy YouTube
    await http.post(Uri.parse('$baseUrl/api/videos/${trimmedData['id']}/youtube'),
      headers: {'X-Api-Key': apiKey},
    );

    return trimmedData;
  }
}
React Native / Axios
import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api-live.levanbac.io.vn',
  headers: { 'X-Api-Key': 'your-secret-api-key-here' }
});

export const editAndPushWorkflow = async (objectName, publicUrl, size, startSec, endSec, title) => {
  // 1. Ensure video ID
  const { data: d1 } = await api.post('/api/videos/ensure', { object_name: objectName, public_url: publicUrl, size });
  const sourceId = d1.data.id;

  // 2. Cắt video
  const { data: d2 } = await api.post(`/api/videos/${sourceId}/editor`, {
    start_time_sec: startSec,
    end_time_sec: endSec,
    title
  });
  const newVideo = d2.data; // { id: 70, hls_url: '...' }

  // 3. Đẩy YouTube
  await api.post(`/api/videos/${newVideo.id}/youtube`);
  return newVideo;
};

4. Tích Hợp Khung Chat Thời Gian Thực

Live Chat hỗ trợ hàng chục ngàn người chat đồng thời qua WebSocket Centrifugo.

4.1. Lấy Token JWT Chat cho Khách 🔒 Yêu cầu API Key

POST /api/chat/token Header: X-Api-Key: your-secret-api-key-here
JSON Request
{
  "display_name": "NguyenVanA"
}

4.2. Gửi Tin Nhắn Chat 🔒 Yêu cầu API Key

POST /api/chat/send Header: X-Api-Key: your-secret-api-key-here
JSON Request
{
  "stream_id": 19,
  "user_id": "usr_99182312",
  "display_name": "NguyenVanA",
  "message": "Chúc mọi người may mắn trúng giải!"
}

4.3. Lấy Lịch Sử Tin Nhắn Chat 🔒 Yêu cầu API Key

GET /api/chat/history?stream_id=19&limit=50 Header: X-Api-Key: your-secret-api-key-here
JSON Response (HTTP 200)
{
  "status": "success",
  "messages": [
    {
      "id": 105,
      "viewer_uid": "usr_99182312",
      "display_name": "NguyenVanA",
      "content": "Chúc mọi người may mắn trúng giải!",
      "created_at": "2026-09-10T11:00:00Z"
    }
  ]
}

5. Outbound Webhooks (Nhận Sự Kiện Tự Động)

Hệ thống hỗ trợ gửi HTTP POST tự động đến máy chủ của bên thứ ba khi xảy ra các sự kiện trong vòng đời của phiên live và phiên ghi hình (VOD).

5.1. Danh Sách Sự Kiện Được Hỗ Trợ (Supported Events)

Tên Sự Kiện (eventName) Ý Nghĩa / Mô Tả Thời Điểm Kích Hoạt
live.started Bắt đầu livestream Publisher kết nối thành công và bắt đầu phát sóng
live.ended Kết thúc livestream Phiên livestream dừng lại (bao gồm thời lượng và thông tin file)
live.health_degraded Cảnh báo tín hiệu không ổn định Throughput publisher giảm sút trong 2 chu kỳ kiểm tra liên tiếp
recording.started Bắt đầu ghi phiên live Xác nhận record đã hoạt động và bắt đầu ghi hình vào ổ đĩa
recording.interrupted Record bị gián đoạn Mất tín hiệu publisher hoặc mạng chập chờn. Cảnh báo file có thể bị chia đoạn
recording.resumed Record tiếp tục Tín hiệu phục hồi sau khi gián đoạn, luồng ghi tiếp tục bình thường
recording.completed Hoàn thành file record thô Media Server hoàn thành ghi file MP4 thô trên đĩa. Bắt đầu pipeline xử lý VOD
recording.failed Record thất bại Gặp lỗi ghi hình hoặc không tìm thấy file ghi hình hoàn chỉnh sau phiên live
Cơ chế giám sát tín hiệu (Health Check):

CMS định kỳ kiểm tra throughput từ OvenMediaEngine theo mã luồng streamKey. Hệ thống áp dụng thuật toán kiểm tra kép kết hợp giữa lastThroughputInavgThroughputIn để loại bỏ hiện tượng cảnh báo sai khi có độ trễ khung hình I-frame. Ngưỡng an toàn là 50 KB/s (~400 kbps) và yêu cầu 2 chu kỳ liên tiếp (60s) mới kích hoạt cảnh báo live.health_degraded. Khi tín hiệu phục hồi, sự kiện recording.resumed sẽ tự động khôi phục trạng thái.

5.2. Cấu Trúc Payload JSON Chuẩn

Mọi sự kiện webhook đều được gửi dưới dạng JSON với cấu trúc thống nhất:

Webhook Standard JSON Payload
{
  "version": "1.0",
  "eventTime": 1789634567890,
  "eventId": "evt_1789634567890123456",
  "eventName": "recording.completed",
  "objectType": "livestream",
  "objectId": 19,
  "provider": null,
  "data": {
    "streamKey": "7fb4cacb4a4c893d5a0be963e5dc6246",
    "liveStreamId": 142,
    "mediaId": null,
    "fileName": "20260917110000_20260917112000.mp4",
    "mediaFileName": "/data/recordings/default/app/ch_7fb4cacb/20260917110000_20260917112000.mp4",
    "format": "mp4",
    "durationSeconds": 1200,
    "sizeBytes": 154829104,
    "currentStage": "completed",
    "targetId": 19,
    "targetData": {
      "channel_id": 19,
      "channel_name": "Kênh Vietlott 1",
      "public_id": "9df7466c-e934-4585-8bb3-4463af0c6a7d",
      "file_path": "/data/recordings/default/app/ch_7fb4cacb/20260917110000_20260917112000.mp4",
      "stage": "completed",
      "message": "Media Server hoàn thành file record thô. Bắt đầu pipeline xử lý VOD"
    },
    "externalUrl": "https://s3.exo.io.vn/live-vietlott/hls/default/app/ch_7fb4cacb/file/playlist.m3u8"
  }
}

5.3. Xác Thực API Key & Chữ Ký Bảo Mật

Khi Webhook Endpoint được cấu hình API Key, mỗi request gửi đi sẽ kèm theo các header: X-API-Key: {api_key}Authorization: Bearer {api_key} (kèm chữ ký X-Vietlott-Signature: sha256={hash}). Phía server nhận có thể kiểm tra trực tiếp API Key qua header để xác thực:

Node.js / Express Verify Example
const crypto = require('crypto');

app.post('/webhook-receiver', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-vietlott-signature'];
  const secret = 'your-configured-webhook-secret';

  const expectedSig = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
  if (signature !== expectedSig) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(req.body);
  console.log('Received event:', payload.eventName, payload.data);
  res.status(200).send('OK');
});

6. Cấu Hình & Tích Hợp Đồng Bộ YouTube (OAuth 2.0 & VOD Sync)

Hệ thống CMS Livestream Vietlott tích hợp sẵn module đồng bộ hóa video VOD tự động lên kênh YouTube thông qua YouTube Data API v3 chuẩn bảo mật Google OAuth 2.0 (Authorization Code Flow). Tính năng này cho phép bên thứ ba tự động đăng tải video quay số mở thưởng, quản lý trạng thái xuất bản, hẹn giờ phát sóng và theo dõi tiến trình upload thời gian thực.

Luồng hoạt động tổng thể (Workflow Overview):
  1. Khởi tạo ứng dụng Google: Tạo OAuth 2.0 Client ID trên Google Cloud Console với Redirect URI trỏ về /api/youtube/callback.
  2. Khai báo Credentials: Lưu Client ID & Client Secret vào CMS qua giao diện hoặc API POST /api/integrations/settings.
  3. Xác thực ủy quyền (OAuth2): Lấy URL từ GET /api/youtube/auth-url, chủ kênh YouTube thực hiện đăng nhập và cấp quyền youtube.upload.
  4. Lưu trữ Token an toàn: CMS tự động nhận authorization code, đổi lấy Access Token & Refresh Token, tự động gia hạn token vĩnh viễn trong Database.
  5. Đồng bộ VOD tự động: Khi kết thúc phiên live, pipeline cắt dựng VOD tự động đẩy video lên YouTube kèm tiêu đề, mô tả, tags và hẹn giờ phát sóng.
  6. Theo dõi tiến trình qua SSE: Lắng nghe event youtube_sync_progress và nhận link YouTube hoàn tất qua youtube_sync_success.

6.1. Hướng Dẫn Thiết Lập Trên Google Cloud Console

Trước khi kết nối, đơn vị phát triển bên thứ ba hoặc bộ phận vận hành cần tạo ứng dụng xác thực trên Google Cloud Console theo 4 bước sau:

1 Tạo Project & Bật API

Truy cập Google Cloud Console → Tạo project mới → Vào APIs & Services > Library → Tìm và bấm Enable cho YouTube Data API v3.

2 Cấu Hình Màn Hình Đồng Ý

Vào OAuth consent screen → Chọn External → Điền App Name & Email → Thêm scopes: .../auth/youtube.upload, .../auth/youtube → Thêm email kênh vào Test users.

3 Tạo OAuth Client ID

Vào Credentials → Create Credentials → OAuth client ID → Chọn Web application → Đặt tên ứng dụng (ví dụ: CMS Livestream YouTube).

4 Khai Báo Redirect URI

Tại mục Authorized redirect URIs, thêm đường dẫn callback chính xác: https://<domain-cms>/api/youtube/callback → Nhấn Create để nhận Client IDClient Secret.

6.2. Danh Sách REST API Quản Lý Kết Nối YouTube

POST /api/integrations/settings X-Api-Key Required

Lưu trực tiếp Google Client ID & Secret vào cơ sở dữ liệu CMS (bên thứ ba có thể cấu hình tự động mà không cần sửa file môi trường):

Request Body JSON
{
  "youtube_client_id": "1234567890-abcdef123456.apps.googleusercontent.com",
  "youtube_client_secret": "GOCSPX-xxxxxxYourClientSecretHere",
  "youtube_redirect_uri": "https://api-live.levanbac.io.vn/api/youtube/callback"
}
GET /api/youtube/auth-url X-Api-Key Required

Sinh đường dẫn URL ủy quyền Google OAuth2 với đầy đủ quyền upload và tự động refresh token:

Response JSON (200 OK)
{
  "status": "success",
  "data": {
    "auth_url": "https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=1234567890-abcdef.apps.googleusercontent.com&prompt=consent&redirect_uri=https%3A%2F%2Fapi-live.levanbac.io.vn%2Fapi%2Fyoutube%2Fcallback&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.upload+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=state"
  }
}
GET /api/youtube/callback?code={authorization_code} Public Webhook

Tiếp nhận authorization code từ Google sau khi chủ kênh hoàn tất xác thực. Backend CMS tự động trao đổi lấy token, xác định YouTube Channel ID thật và redirect người dùng về CMS Studio.

GET /api/youtube/status X-Api-Key Required

Kiểm tra trạng thái kết nối YouTube hiện tại của hệ thống:

Response JSON (200 OK)
{
  "status": "success",
  "is_connected": true,
  "data": {
    "channel_name": "Vietlott - Xổ Số Tự Chọn",
    "email": "channel-owner@gmail.com",
    "youtube_channel_id": "UCxxxxxxYourChannelID",
    "is_connected": true
  }
}
DELETE /api/integrations/disconnect/youtube X-Api-Key Required

Hủy liên kết tài khoản YouTube và xóa sạch Access/Refresh Token khỏi hệ thống.

6.3. Tự Động Xuất Bản & Đồng Bộ Video VOD Lên YouTube

Cách 1: Tự động hóa khi tạo/chỉnh sửa bản ghi VOD (Automatic Sync)

Khi gửi payload tạo hoặc cập nhật video (POST /api/videos hoặc PUT /api/videos/:id), bật cờ sync_youtube: true. Hệ thống sẽ tự động tải file MP4 từ MinIO/S3 và đẩy lên YouTube theo chế độ Resumable Upload:

POST /api/videos
{
  "title": "Trực Tiếp Quay Số Vietlott Keno Kỳ #01289 - 17/09/2026",
  "description": "Chương trình phát sóng trực tiếp kết quả quay số mở thưởng Keno Vietlott.",
  "tags": "vietlott, keno, xoso, livestream",
  "channel_id": 19,
  "sync_youtube": true,
  "scheduled_at": "2026-09-17T18:30:00Z"
}

Cách 2: Kích hoạt đồng bộ hàng loạt / thủ công qua API (Batch Sync)

POST /api/videos/batch-sync X-Api-Key Required
Request Body JSON
{
  "video_ids": [142],
  "platforms": ["youtube"]
}

6.4. Theo Dõi Tiến Trình Upload Real-time Qua Server-Sent Events (SSE)

Bên thứ ba có thể theo dõi tiến trình upload video lên YouTube theo thời gian thực bằng cách kết nối vào luồng SSE tại GET /api/events:

JavaScript EventSource Example
const eventSource = new EventSource('https://api-live.levanbac.io.vn/api/events');

// 1. Lắng nghe tiến độ upload (% hoàn thành)
eventSource.addEventListener('youtube_sync_progress', (e) => {
  const data = JSON.parse(e.data);
  console.log(`Video #${data.video_id} đang tải lên YouTube: ${data.progress}%`);
});

// 2. Lắng nghe thông báo upload thành công
eventSource.addEventListener('youtube_sync_success', (e) => {
  const data = JSON.parse(e.data);
  console.log(`✅ Upload YouTube thành công! Video #${data.video_id}`);
  console.log(`🔗 Link YouTube: ${data.youtube_url}`);
  console.log(`🆔 Video ID: ${data.youtube_video_id}`);
});

// 3. Lắng nghe khi có lỗi upload
eventSource.addEventListener('youtube_sync_error', (e) => {
  const data = JSON.parse(e.data);
  console.error(`❌ Lỗi đồng bộ YouTube video #${data.video_id}:`, data.error);
});

6.4. Đẩy Trực Tiếp 1 Video Lên YouTube Bằng ID (API Tinh Gọn)

Khi bên thứ ba hoặc client muốn đẩy một video cụ thể lên YouTube, chỉ cần truyền ID video trong URL path. Hệ thống tự động kiểm tra OAuth2, ưu tiên lấy video đã cắt nếu có:

POST /api/videos/:id/youtube Header: X-Api-Key: your-secret-api-key-here | :id là ID video cần đẩy
JSON Response (200 OK)
{
  "status": "success",
  "message": "Đã đẩy video lên YouTube thành công",
  "data": {
    "id": 70,
    "youtube_video_id": "8aJkF9zL2xM",
    "youtube_url": "https://youtu.be/8aJkF9zL2xM"
  }
}

Code Mẫu Tích Hợp Đẩy YouTube:

JavaScript (Fetch)
const res = await fetch('https://api-live.levanbac.io.vn/api/videos/70/youtube', {
  method: 'POST',
  headers: { 'X-Api-Key': 'your-secret-api-key-here' }
});
const { data } = await res.json();
console.log('YouTube Link:', data.youtube_url);
cURL
curl -X POST "https://api-live.levanbac.io.vn/api/videos/70/youtube" \
  -H "X-Api-Key: your-secret-api-key-here"
Flutter / Dart
final res = await http.post(
  Uri.parse('https://api-live.levanbac.io.vn/api/videos/70/youtube'),
  headers: {'X-Api-Key': 'your-secret-api-key-here'},
);
final data = jsonDecode(res.body)['data'];
print('YouTube URL: ${data['youtube_url']}');
React Native / Axios
const res = await axios.post('https://api-live.levanbac.io.vn/api/videos/70/youtube', null, {
  headers: { 'X-Api-Key': 'your-secret-api-key-here' }
});
console.log('YouTube URL:', res.data.data.youtube_url);

7. Nhật Ký Hệ Thống & Giám Sát Console (System Service Logs API)

Giám sát Realtime không cần SSH: Hệ thống tự động gom toàn bộ luồng console stdout/stderr từ backend Go, Gin HTTP routes, Worker đồng bộ VOD S3, FFMPEG HLS và Outbound Webhooks vào bộ nhớ đệm In-Memory Ring Buffer (tối đa 5.000 dòng). Admin có thể xem trực tiếp trên CMS hoặc bên thứ ba có thể truy vấn qua REST API / SSE.

7.1. Truy Vấn Danh Sách Logs (REST API)

Lấy danh sách các dòng log gần nhất kèm bộ lọc cấp độ hoặc tìm kiếm từ khóa:

GET /api/system/logs GET
// Headers yêu cầu:
// X-Api-Key: YOUR_API_KEY (hoặc ?api_key=...)

// Ví dụ: Lấy 300 dòng log gần nhất chứa từ khóa "s3"
GET https://api-live.levanbac.io.vn/api/system/logs?limit=300&search=s3&level=SYNC

// Response JSON (200 OK):
{
  "status": "success",
  "data": {
    "logs": [
      {
        "id": 1052,
        "timestamp": "2026-09-18T09:30:15.123Z",
        "time_str": "2026-09-18 09:30:15.123",
        "level": "SYNC",
        "message": "✅ Đã đồng bộ VOD lên S3: stream_key=ch_7fb4, file=sample.mp4",
        "raw": "2026/09/18 09:30:15 ✅ Đã đồng bộ VOD lên S3: stream_key=ch_7fb4, file=sample.mp4"
      }
    ],
    "total": 1,
    "stats": {
      "total_in_memory": 450,
      "errors": 0,
      "warnings": 1,
      "active_streams": 1
    }
  }
}

7.2. Stream Nhật Ký Thời Gian Thực (SSE Live Tail)

Tương tự lệnh tail -f hoặc docker logs -f, client có thể kết nối Server-Sent Events (SSE) để nhận log ngay tức thì:

JavaScript Server-Sent Events Example SSE
const apiKey = 'YOUR_API_KEY';
const eventSource = new EventSource(`https://api-live.levanbac.io.vn/api/system/logs/stream?api_key=${apiKey}`);

eventSource.addEventListener('log', (e) => {
  const logEntry = JSON.parse(e.data);
  console.log(`[${logEntry.time_str}] [${logEntry.level}] ${logEntry.raw}`);
});

eventSource.onerror = (err) => {
  console.error('Mất kết nối log stream, trình duyệt sẽ tự thử lại...', err);
};

7.3. Tải File Log & Xóa Bộ Nhớ Đệm

  • Tải file log (.log): GET /api/system/logs/download?limit=5000&api_key=YOUR_API_KEY
  • Xóa bộ đệm RAM: POST /api/system/logs/clear (Cần header X-Api-Key)

8. Mã Mẫu Tích Hợp (SDK Code Samples)

JavaScript + Hls.js Example
// 1. Khởi tạo Device ID cố định trong localStorage
function getDeviceId() {
  let id = localStorage.getItem('app_device_id');
  if (!id) {
    id = 'web-' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
    localStorage.setItem('app_device_id', id);
  }
  return id;
}

const API_BASE = 'https://api-live.levanbac.io.vn';
const CHANNEL_ID = 19;
let heartbeatTimer = null;

// 2. Mở luồng & Bắt đầu gửi Heartbeat mỗi 8s
async function startPlayback() {
  const res = await fetch(`${API_BASE}/api/channels/${CHANNEL_ID}/watch`);
  const { data } = await res.json();

  const video = document.getElementById('myVideoPlayer');
  if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(data.playback.hls_url);
    hls.attachMedia(video);
    video.play();
  }

  // Gửi heartbeat định kỳ 8 giây
  sendHeartbeat();
  heartbeatTimer = setInterval(sendHeartbeat, 8000);
}

function sendHeartbeat() {
  fetch(`${API_BASE}/api/channels/${CHANNEL_ID}/viewers/heartbeat`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ viewer_uid: getDeviceId() })
  }).catch(() => {});
}

// 3. Thoát hoặc đóng tab -> Báo server giảm view tức thì
function stopPlayback() {
  if (heartbeatTimer) clearInterval(heartbeatTimer);
  const url = `${API_BASE}/api/channels/${CHANNEL_ID}/viewers/leave`;
  const payload = JSON.stringify({ viewer_uid: getDeviceId() });
  
  if (navigator.sendBeacon) {
    navigator.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
  } else {
    fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true });
  }
}

window.addEventListener('beforeunload', stopPlayback);
Flutter / Dart Class
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

class VietlottLivestreamSDK {
  static const String baseUrl = 'https://api-live.levanbac.io.vn';
  final int channelId;
  Timer? _timer;
  String? _deviceId;

  VietlottLivestreamSDK({required this.channelId});

  Future _getDeviceId() async {
    if (_deviceId != null) return _deviceId!;
    final prefs = await SharedPreferences.getInstance();
    _deviceId = prefs.getString('vietlott_device_uid');
    if (_deviceId == null) {
      _deviceId = 'flutter-${DateTime.now().millisecondsSinceEpoch}';
      await prefs.setString('vietlott_device_uid', _deviceId!);
    }
    return _deviceId!;
  }

  // Khởi động Player & Gửi Heartbeat
  Future startLive() async {
    final res = await http.get(Uri.parse('$baseUrl/api/channels/$channelId/watch'));
    final json = jsonDecode(res.body);
    final hlsUrl = json['data']['playback']['hls_url'];

    _sendHeartbeat();
    _timer = Timer.periodic(const Duration(seconds: 8), (_) => _sendHeartbeat());

    return hlsUrl;
  }

  Future _sendHeartbeat() async {
    final uid = await _getDeviceId();
    await http.post(
      Uri.parse('$baseUrl/api/channels/$channelId/viewers/heartbeat'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'viewer_uid': uid}),
    );
  }

  // Dừng phát & Báo thoát tức thì
  Future dispose() async {
    _timer?.cancel();
    final uid = await _getDeviceId();
    await http.post(
      Uri.parse('$baseUrl/api/channels/$channelId/viewers/leave'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'viewer_uid': uid}),
    );
  }
}
React Native TypeScript Hook
import { useEffect, useRef } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

const API_BASE = 'https://api-live.levanbac.io.vn';

export const useViewerHeartbeat = (channelId: number) => {
  const timerRef = useRef(null);

  const getUid = async () => {
    let uid = await AsyncStorage.getItem('vietlott_device_uid');
    if (!uid) {
      uid = 'rn-' + Math.random().toString(36).substring(2) + Date.now();
      await AsyncStorage.setItem('vietlott_device_uid', uid);
    }
    return uid;
  };

  const sendHeartbeat = async () => {
    const uid = await getUid();
    fetch(`${API_BASE}/api/channels/${channelId}/viewers/heartbeat`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ viewer_uid: uid }),
    }).catch(() => {});
  };

  const sendLeave = async () => {
    const uid = await getUid();
    fetch(`${API_BASE}/api/channels/${channelId}/viewers/leave`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ viewer_uid: uid }),
    }).catch(() => {});
  };

  useEffect(() => {
    sendHeartbeat();
    timerRef.current = setInterval(sendHeartbeat, 8000);

    return () => {
      if (timerRef.current) clearInterval(timerRef.current);
      sendLeave();
    };
  }, [channelId]);
};

8. Bảng Mã Lỗi (HTTP Status Codes)

Mã HTTP Ý nghĩa Giải thích & Hướng xử lý
200 OK Thành công Yêu cầu đã được xử lý chính xác
404 Not Found Không tìm thấy Kênh không tồn tại hoặc ID kênh không đúng
401 Unauthorized Không có quyền Thiếu hoặc sai X-Api-Key khi gọi các endpoint quản trị
429 Rate Limit Quá nhiều yêu cầu Gửi tin nhắn chat vượt quá 5 tin / 10s