import os import sys import time import psutil import subprocess import threading import gradio as gr import gdown STREAM_PROCESS = None STREAM_LOGS = [] IS_STREAMING = False IS_DOWNLOADING = False START_TIME = None STREAM_LOCK = threading.Lock() DEFAULT_KEY = os.getenv("YOUTUBE_STREAM_KEY", "sztg-vq5m-7855-t6hz-69zu") VOLUME_DIR = "/mv" READY_DIR = "ready_mvs" PLAYLIST_FILE = "playlist.txt" # 10 MV TriLyric 1080p chuẩn hóa 4Mbps & GOP 2s từ Google Drive Folder 1AeALxZS5fG4EHJ7JpxoNbs6IX58aRx_j MVS = [ {"id": "15932kX_Yzm6yKJ_kXIq8WspjUiuNyOF7", "name": "ready_01_MV_TieuNgaoDongA_full_v4_trilyric.mp4", "title": "MV 01: Tiếu Ngạo Đông A"}, {"id": "1mr3aO6675NJhu3RZIMaI1KXQjiV9i9TM", "name": "ready_02_MV_LaughterBlueSea_Full_TriLyric_1080p.mp4", "title": "MV 02: Laughter in the Blue Sea"}, {"id": "1Grsnc11Fh7m0A3AiD5jbnWZMeLxXMAez", "name": "ready_03_MV_CamKiemGiangHo_Full_TriLyric_1080p.mp4", "title": "MV 03: Cầm Kiếm Giang Hồ"}, {"id": "11FXBf6WAo7w-4rc14Cc_RT8jmGVAm8N0", "name": "ready_04_MV_DrinkingAloneLucDau_Full_TriLyric_1080p.mp4", "title": "MV 04: Độc Ẩm Lục Đầu Giang"}, {"id": "1IIyLFeCDTWR2sFRz6teYoaSUI974qLH0", "name": "ready_05_MV_WindPhuVanPeak_Full_TriLyric_1080p.mp4", "title": "MV 05: Gió Đỉnh Phù Vân"}, {"id": "1haOcPs4Ofkf6nhaw97iaZr_eRsWT36xU", "name": "ready_06_MV_BenVangCanChen_Full_TriLyric_1080p.mp4", "title": "MV 06: Bến Vắng Cạn Chén"}, {"id": "1F6j-j7pQ5-vrmXB3ah3-5ezkHK5j_64L", "name": "ready_07_MV_ThaoThaoBachDang_Full_TriLyric_1080p.mp4", "title": "MV 07: Thao Thao Bạch Đằng"}, {"id": "1BmAkgv1hh4Ltr1AZqrI_lkOWGSiJma2N", "name": "ready_08_MV_CarefreeLaughter_Full_TriLyric_1080p.mp4", "title": "MV 08: Tiếu Ngạo Nhân Sinh"}, {"id": "1VAbjxj1FlZhFNnvu6oKFWBE1icAhVo4J", "name": "ready_09_MV_GacKiemLoGom_Full_TriLyric_1080p.mp4", "title": "MV 09: Gác Kiếm Lò Gốm"}, {"id": "17slGHDwvY36YLf6_AW1emaYxN70GJ9rM", "name": "ready_10_MV_VanDamKhaiHoan_Full_TriLyric_1080p.mp4", "title": "MV 10: Vạn Dặm Khải Hoàn"} ] def append_log(msg): timestamp = time.strftime("%Y-%m-%d %H:%M:%S") log_line = f"[{timestamp}] {msg}" print(log_line) STREAM_LOGS.append(log_line) if len(STREAM_LOGS) > 150: STREAM_LOGS.pop(0) def _detect_ready_dir(): if os.path.isdir(VOLUME_DIR): missing_vol = [mv for mv in MVS if not (os.path.exists(os.path.join(VOLUME_DIR, mv["name"])) and os.path.getsize(os.path.join(VOLUME_DIR, mv["name"])) > 1024*1024)] if not missing_vol: return VOLUME_DIR return "ready_mvs" READY_DIR = _detect_ready_dir() def ensure_all_mvs_ready(): global IS_DOWNLOADING if READY_DIR == VOLUME_DIR: append_log(f"[VOLUME] Dùng 10 MV read-only từ volume {VOLUME_DIR} - bỏ qua tải Google Drive.") else: os.makedirs(READY_DIR, exist_ok=True) missing = [mv for mv in MVS if not (os.path.exists(os.path.join(READY_DIR, mv["name"])) and os.path.getsize(os.path.join(READY_DIR, mv["name"])) > 1024*1024)] if not missing: append_log("[OK] Tất cả 10 MV chuẩn 4Mbps (GOP 2s) đã sẵn sàng trong thư mục.") generate_playlist() return True IS_DOWNLOADING = True append_log(f"Đang tải {len(missing)} MV chuẩn 4Mbps từ Google Drive Folder mới...") for idx, mv in enumerate(missing, 1): target = os.path.join(READY_DIR, mv["name"]) url = f"https://drive.google.com/uc?id={mv['id']}" append_log(f"[{idx}/{len(missing)}] Đang tải: {mv['title']}...") try: gdown.download(url, target, quiet=True) if os.path.exists(target) and os.path.getsize(target) > 1024*1024: append_log(f"-> Đã tải xong: {mv['name']} ({os.path.getsize(target)/(1024*1024):.1f} MB - Chuẩn 4Mbps GOP 2s)") else: append_log(f"[CẢNH BÁO] File {mv['name']} tải không thành công.") except Exception as e: append_log(f"[LỖI TẢI] {mv['title']}: {str(e)}") IS_DOWNLOADING = False generate_playlist() return True def generate_playlist(): if READY_DIR != VOLUME_DIR: os.makedirs(READY_DIR, exist_ok=True) lines = [] for mv in MVS: fpath = os.path.join(READY_DIR, mv["name"]) if os.path.exists(fpath) and os.path.getsize(fpath) > 1024*1024: lines.append(f"file '{fpath.replace(chr(92), '/')}'") if lines: with open(PLAYLIST_FILE, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") append_log(f"Đã cập nhật playlist {PLAYLIST_FILE} ({len(lines)} MV chuẩn 4Mbps).") return True return False def kill_existing_ffmpeg(): killed = 0 for proc in psutil.process_iter(['pid', 'name']): try: if 'ffmpeg' in proc.info['name'].lower(): proc.kill() killed += 1 except Exception: pass if killed > 0: append_log(f"Đã dọn dẹp {killed} tiến trình FFmpeg cũ.") def stream_worker(key: str): global STREAM_PROCESS, IS_STREAMING, START_TIME if not STREAM_LOCK.acquire(blocking=False): append_log("[CẢNH BÁO] stream_worker đã đang chạy. Bỏ qua lời gọi trùng lặp.") return try: kill_existing_ffmpeg() ensure_all_mvs_ready() if not os.path.exists(PLAYLIST_FILE) or os.path.getsize(PLAYLIST_FILE) == 0: append_log("[LỖI] Playlist trống! Không thể phát Live.") IS_STREAMING = False return IS_STREAMING = True START_TIME = time.time() rtmp_url = f"rtmp://a.rtmp.youtube.com/live2/{key}" append_log(f"Bắt đầu đẩy luồng RTMP lên YouTube Live (Chuẩn 4Mbps, GOP 2s, Concat Pass-through -c:v copy)...") cmd = [ "ffmpeg", "-re", "-f", "concat", "-safe", "0", "-stream_loop", "-1", "-i", PLAYLIST_FILE, "-c:v", "copy", "-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-f", "flv", rtmp_url ] while IS_STREAMING: try: kill_existing_ffmpeg() append_log("Khởi động FFmpeg Stream Engine...") STREAM_PROCESS = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True ) for line in STREAM_PROCESS.stdout: if not IS_STREAMING: break if "frame=" in line or "bitrate=" in line or "fps=" in line: if len(STREAM_LOGS) > 0 and ("frame=" in STREAM_LOGS[-1] or "bitrate=" in STREAM_LOGS[-1]): STREAM_LOGS[-1] = f"[{time.strftime('%H:%M:%S')}] {line.strip()}" else: append_log(line.strip()) elif any(kw in line.lower() for kw in ["error", "warning", "opening"]): append_log(line.strip()) STREAM_PROCESS.wait() if IS_STREAMING: append_log("[CẢNH BÁO] Luồng ngắt quãng. Tự động kết nối lại sau 10s...") time.sleep(10) except Exception as e: append_log(f"[LỖI ENGINE] {str(e)}") time.sleep(10) finally: kill_existing_ffmpeg() STREAM_LOCK.release() def start_stream(key): global IS_STREAMING if not key or not key.strip(): return "Vui lòng nhập YouTube Stream Key!" if IS_STREAMING: return "Luồng Live đã đang chạy!" t = threading.Thread(target=stream_worker, args=(key.strip(),), daemon=True) t.start() return "Đã kích hoạt lệnh phát Live 24/7!" def stop_stream(): global IS_STREAMING, STREAM_PROCESS IS_STREAMING = False kill_existing_ffmpeg() if STREAM_PROCESS: try: STREAM_PROCESS.terminate() except Exception: pass append_log("Đã gửi lệnh dừng live.") return "Đã dừng Live." def get_status(): uptime_str = "Chưa phát" if IS_STREAMING and START_TIME: elapsed = int(time.time() - START_TIME) h, rem = divmod(elapsed, 3600) m, s = divmod(rem, 60) uptime_str = f"{h}h {m}m {s}s" status_txt = "🔴 ĐANG LIVE 24/7" if IS_STREAMING else "⚪ ĐÃ DỪNG" if IS_DOWNLOADING: status_txt += " (Đang tải MV chuẩn 4Mbps...)" cpu = f"{psutil.cpu_percent()}%" ram = f"{psutil.virtual_memory().percent}%" mv_data = [] for idx, mv in enumerate(MVS, 1): fpath = os.path.join(READY_DIR, mv["name"]) exists = os.path.exists(fpath) and os.path.getsize(fpath) > 1024*1024 sz = f"{os.path.getsize(fpath)/(1024*1024):.1f} MB" if exists else "Chưa có" st = "✅ Chuẩn 4Mbps" if exists else "⏳ Chờ tải" mv_data.append([idx, mv["title"], sz, st]) logs_txt = "\n".join(STREAM_LOGS[-30:]) return status_txt, uptime_str, cpu, ram, mv_data, logs_txt if DEFAULT_KEY: threading.Thread(target=stream_worker, args=(DEFAULT_KEY,), daemon=True).start() else: threading.Thread(target=ensure_all_mvs_ready, daemon=True).start() with gr.Blocks(title="YouTube 24/7 Live Streamer") as demo: gr.Markdown("# 🎬 Đông A Tiêu Dao Lục - YouTube Live Stream 24/7 (Chuẩn 4Mbps / GOP 2s)") gr.Markdown("Nguồn video: Google Drive Folder `1AeALxZS5fG4EHJ7JpxoNbs6IX58aRx_j` | Vận hành theo chuẩn **`livestream-ops-master`** (Concat Demuxer, GOP 2s, 0 Drop frames).") with gr.Row(): status_box = gr.Textbox(label="Trạng thái", value="Đang khởi tạo...", interactive=False) uptime_box = gr.Textbox(label="Thời gian Live", value="0s", interactive=False) cpu_box = gr.Textbox(label="CPU", value="0%", interactive=False) ram_box = gr.Textbox(label="RAM", value="0%", interactive=False) with gr.Row(): key_input = gr.Textbox(label="YouTube Stream Key", value=DEFAULT_KEY, type="password", scale=3) start_btn = gr.Button("▶️ Bắt Đầu Live", variant="primary", scale=1) stop_btn = gr.Button("⏹️ Dừng Live", variant="stop", scale=1) gr.Markdown("### 📂 Danh mục 10 MV TriLyric 1080p (Chuẩn hóa 4Mbps & GOP 2s)") mv_table = gr.Dataframe( headers=["#", "Tên MV", "Dung lượng", "Trạng thái"], datatype=["number", "str", "str", "str"], value=[] ) gr.Markdown("### 📜 Nhật ký phát sóng (Live Logs)") logs_box = gr.TextArea(label="FFmpeg Logs", lines=10, interactive=False) start_btn.click(fn=start_stream, inputs=[key_input], outputs=[status_box]) stop_btn.click(fn=stop_stream, inputs=[], outputs=[status_box]) timer = gr.Timer(value=5) timer.tick(fn=get_status, outputs=[status_box, uptime_box, cpu_box, ram_box, mv_table, logs_box]) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)