Download streamlit_app.py from Reoo88/Comnto1: direct link, hf CLI and curl.
- Browser
- Download file 6.14 kB
-
https://huggingface.co/spaces/Reoo88/Comnto1/resolve/main/streamlit_app.py
- Command line
-
hf download hf://spaces/Reoo88/Comnto1/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/Reoo88/Comnto1/resolve/main/streamlit_app.py
6.14 kB
| import os, time, threading, subprocess, requests | |
| import streamlit as st | |
| # ---------- إعدادات ---------- | |
| COMMENTS_URL = "https://reoo88-signserver.hf.space/comments" | |
| COMMENTS_FILE = "/tmp/comments.txt" | |
| FONT_PATH = "/usr/share/fonts/opentype/fonts-hosny-amiri/Amiri-Regular.ttf" | |
| MAX_LINES = 6 | |
| MAX_LINE_LEN = 50 | |
| RECONNECT_MAX = 5 | |
| # تهيئة ملف التعليقات | |
| if not os.path.exists(COMMENTS_FILE): | |
| with open(COMMENTS_FILE, "w", encoding="utf-8") as f: | |
| f.write(" ") | |
| text_lock = threading.Lock() | |
| stop_event = threading.Event() | |
| # ---------- تحديث التعليقات ---------- | |
| def fetch_comments_once(): | |
| try: | |
| r = requests.get(COMMENTS_URL, timeout=5) | |
| if r.status_code != 200: | |
| return | |
| lines = [ln.strip() for ln in r.text.strip().split("\n") if ln.strip()][-MAX_LINES:] | |
| lines = [(ln[:MAX_LINE_LEN] + "...") if len(ln) > MAX_LINE_LEN else ln for ln in lines] | |
| new_txt = "\n".join(lines) if lines else " " | |
| tmp = COMMENTS_FILE + ".tmp" | |
| with text_lock: | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| f.write(new_txt) | |
| os.replace(tmp, COMMENTS_FILE) | |
| print(f"✅ تم تحديث التعليقات ({len(lines)} سطر)") | |
| except Exception as e: | |
| print(f"❌ خطأ بالتعليقات: {e}") | |
| def comments_updater(): | |
| while not stop_event.is_set(): | |
| fetch_comments_once() | |
| for _ in range(30): | |
| if stop_event.is_set(): return | |
| time.sleep(0.1) | |
| # ---------- بناء أوامر ffmpeg/streamlink ---------- | |
| def build_ff_cmd(yt_key: str) -> list: | |
| drawtext = ( | |
| f"drawtext=" | |
| f"fontfile='{FONT_PATH}':" | |
| f"textfile='{COMMENTS_FILE}':reload=1:" | |
| f"text_shaping=1:" | |
| f"fontcolor=white:fontsize=28:line_spacing=10:" | |
| f"x=15:y=h-th-50:" | |
| f"box=1:boxcolor=black@0.55:boxborderw=12:" | |
| f"shadowcolor=black:shadowx=2:shadowy=2" | |
| ) | |
| return [ | |
| "ffmpeg", "-hide_banner", "-loglevel", "warning", | |
| "-thread_queue_size", "8192", "-re", | |
| "-i", "pipe:0", | |
| "-vf", f"scale=720:1280,{drawtext}", | |
| "-c:v", "libx264", "-preset", "veryfast", "-tune", "zerolatency", | |
| "-profile:v", "main", "-pix_fmt", "yuv420p", | |
| "-b:v", "2500k", "-maxrate", "2500k", "-bufsize", "5000k", | |
| "-r", "30", "-g", "60", "-keyint_min", "60", "-sc_threshold", "0", | |
| "-c:a", "aac", "-ar", "44100", "-b:a", "128k", "-ac", "2", | |
| "-f", "flv", | |
| "-flvflags", "no_duration_filesize+aac_seq_header_detect", | |
| "-rtmp_live", "live", | |
| f"rtmp://a.rtmp.youtube.com/live2/{yt_key}", | |
| ] | |
| def build_sl_cmd(username: str) -> list: | |
| return [ | |
| "streamlink", f"https://www.tiktok.com/@{username}/live", "best", | |
| "--hls-live-edge", "3", "--retry-streams", "3", "--retry-max", "5", "--stdout", | |
| ] | |
| # ---------- بدء البث ---------- | |
| def start_stream(yt_key, username): | |
| stop_event.clear() | |
| threading.Thread(target=comments_updater, daemon=True).start() | |
| time.sleep(2) | |
| for attempt in range(1, RECONNECT_MAX + 1): | |
| if stop_event.is_set(): break | |
| print(f"\n========== محاولة بث #{attempt}/{RECONNECT_MAX} ==========") | |
| sl_process = ffmpeg_process = None | |
| try: | |
| sl_process = subprocess.Popen( | |
| build_sl_cmd(username), | |
| stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, | |
| ) | |
| ffmpeg_process = subprocess.Popen( | |
| build_ff_cmd(yt_key), | |
| stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, | |
| stderr=subprocess.PIPE, bufsize=10**7, | |
| ) | |
| def log_ffmpeg(): | |
| for line in iter(ffmpeg_process.stderr.readline, b""): | |
| txt = line.decode(errors="ignore").rstrip() | |
| if txt: print(f"[ffmpeg] {txt}") | |
| threading.Thread(target=log_ffmpeg, daemon=True).start() | |
| bytes_streamed, start_ts = 0, time.time() | |
| while not stop_event.is_set(): | |
| if ffmpeg_process.poll() is not None: break | |
| data = sl_process.stdout.read(8192) | |
| if not data: break | |
| try: | |
| ffmpeg_process.stdin.write(data) | |
| bytes_streamed += len(data) | |
| except BrokenPipeError: | |
| break | |
| duration = time.time() - start_ts | |
| print(f"[STATS] بُثّ {bytes_streamed/(1024*1024):.1f} MB خلال {duration:.0f}s") | |
| if duration < 5: | |
| print("❌ يوتيوب أغلق الاتصال فوراً — تحقّق من Stream Key.") | |
| break | |
| except Exception as e: | |
| print(f"[ERROR] {e}") | |
| finally: | |
| for p in (ffmpeg_process, sl_process): | |
| if not p: continue | |
| try: | |
| if p.stdin: | |
| try: p.stdin.close() | |
| except Exception: pass | |
| if p.poll() is None: | |
| p.terminate() | |
| try: p.wait(timeout=5) | |
| except subprocess.TimeoutExpired: p.kill() | |
| except Exception: pass | |
| if not stop_event.is_set() and attempt < RECONNECT_MAX: | |
| time.sleep(5) | |
| print("[INFO] انتهت جلسة البث.") | |
| # ---------- واجهة Streamlit ---------- | |
| st.set_page_config(page_title="بث تيك توك + تعليقات", page_icon="📹") | |
| st.title("📹 بث تيك توك مع تعليقات عربية مباشرة") | |
| with st.form("stream_form"): | |
| user = st.text_input("معرف تيك توك (بدون @)", "majd.alban") | |
| key = st.text_input("YouTube Stream Key", type="password") | |
| if st.form_submit_button("🚀 ابدأ البث", type="primary"): | |
| if user and key: | |
| threading.Thread(target=start_stream, args=(key, user), daemon=True).start() | |
| st.success("✅ بدأ البث!") | |
| else: | |
| st.error("الرجاء إدخال المعرف ومفتاح البث.") | |