Update streamlit_app.py
Browse files- streamlit_app.py +36 -63
streamlit_app.py
CHANGED
|
@@ -11,9 +11,9 @@ from TikTokLive import TikTokLiveClient
|
|
| 11 |
from TikTokLive.events import CommentEvent, DisconnectEvent, ConnectEvent
|
| 12 |
from TikTokLive.client.errors import UserOfflineError, UserNotFoundError
|
| 13 |
|
| 14 |
-
# ---------- ت
|
| 15 |
-
FONT_PATH = "
|
| 16 |
-
FONT_URL = "https://github.com/mokhtarbsaid/rare-arabic-fonts/raw/main/palestine/palestine-regular.ttf"
|
| 17 |
|
| 18 |
def download_font():
|
| 19 |
if not os.path.exists(FONT_PATH):
|
|
@@ -24,56 +24,41 @@ def download_font():
|
|
| 24 |
|
| 25 |
download_font()
|
| 26 |
|
| 27 |
-
#
|
| 28 |
comments_list = []
|
| 29 |
-
MAX_COMMENTS =
|
| 30 |
lock = threading.Lock()
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def add_fake_comment():
|
| 37 |
-
global fake_counter
|
| 38 |
-
fake_counter += 1
|
| 39 |
-
fake_comments = [
|
| 40 |
-
f"زائر {fake_counter}: بالتوفيق",
|
| 41 |
-
f"محب {fake_counter}: بث رائع",
|
| 42 |
-
f"صديق {fake_counter}: متابع من العراق",
|
| 43 |
-
]
|
| 44 |
-
return fake_comments[fake_counter % 3]
|
| 45 |
-
|
| 46 |
-
def create_comment_overlay():
|
| 47 |
-
width, height = 720, 1280
|
| 48 |
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
| 49 |
draw = ImageDraw.Draw(img)
|
|
|
|
| 50 |
try:
|
|
|
|
| 51 |
font = ImageFont.truetype(FONT_PATH, 28)
|
| 52 |
except:
|
| 53 |
font = ImageFont.load_default()
|
| 54 |
-
|
| 55 |
with lock:
|
| 56 |
current = comments_list[-MAX_COMMENTS:]
|
| 57 |
-
|
|
|
|
| 58 |
y_offset = height - 60
|
| 59 |
for comment in reversed(current):
|
|
|
|
| 60 |
draw.text((12, y_offset + 2), comment, font=font, fill=(0, 0, 0, 180))
|
|
|
|
| 61 |
draw.text((10, y_offset), comment, font=font, fill=(255, 255, 255, 255))
|
| 62 |
-
y_offset -=
|
| 63 |
-
|
| 64 |
buf = BytesIO()
|
| 65 |
img.save(buf, format="PNG")
|
| 66 |
buf.seek(0)
|
| 67 |
return buf.read()
|
| 68 |
|
| 69 |
-
# ---------- مول
|
| 70 |
-
def fake_comment_generator():
|
| 71 |
-
while fake_comments_enabled:
|
| 72 |
-
time.sleep(3)
|
| 73 |
-
with lock:
|
| 74 |
-
comments_list.append(add_fake_comment())
|
| 75 |
-
|
| 76 |
-
# ---------- عميل تعليقات تيك توك ----------
|
| 77 |
def run_tiktok_client(username):
|
| 78 |
while True:
|
| 79 |
try:
|
|
@@ -83,7 +68,7 @@ def run_tiktok_client(username):
|
|
| 83 |
|
| 84 |
@client.on(ConnectEvent)
|
| 85 |
async def on_connect(event: ConnectEvent):
|
| 86 |
-
|
| 87 |
|
| 88 |
@client.on(CommentEvent)
|
| 89 |
async def on_comment(event: CommentEvent):
|
|
@@ -93,26 +78,25 @@ def run_tiktok_client(username):
|
|
| 93 |
|
| 94 |
@client.on(DisconnectEvent)
|
| 95 |
async def on_disconnect(event: DisconnectEvent):
|
| 96 |
-
|
| 97 |
|
| 98 |
-
st.session_state["client_status"] = "🔄 جاري الاتصال بـ TikTok..."
|
| 99 |
loop.run_until_complete(client.start())
|
| 100 |
-
except UserOfflineError:
|
| 101 |
-
|
| 102 |
-
except UserNotFoundError:
|
| 103 |
-
st.session_state["client_status"] = "❌ اسم المستخدم غير موجود"
|
| 104 |
except Exception as e:
|
| 105 |
-
|
| 106 |
time.sleep(10)
|
| 107 |
|
| 108 |
-
# ---------- تشغيل البث
|
| 109 |
def start_streaming(yt_key, tiktok_username):
|
|
|
|
| 110 |
streamlink_cmd = f"streamlink https://www.tiktok.com/@{tiktok_username}/live best --hls-live-edge 3 --stdout"
|
| 111 |
|
|
|
|
| 112 |
ffmpeg_cmd = (
|
| 113 |
-
f"ffmpeg -thread_queue_size 8192 -i pipe:0 "
|
| 114 |
-
f"-f image2pipe -r
|
| 115 |
-
f"-filter_complex \"[0:v]scale=720:1280[vid];[vid][1:v]overlay=0:0\" "
|
| 116 |
f"-map \"[outv]\" -map 0:a? "
|
| 117 |
f"-c:v libx264 -preset ultrafast -tune zerolatency -b:v 1500k -maxrate 2000k -bufsize 3000k -g 30 "
|
| 118 |
f"-c:a aac -ar 48000 -b:a 192k -ac 2 "
|
|
@@ -122,9 +106,12 @@ def start_streaming(yt_key, tiktok_username):
|
|
| 122 |
try:
|
| 123 |
print(f"[INFO] بدء البث من @{tiktok_username} إلى يوتيوب...")
|
| 124 |
|
|
|
|
| 125 |
sl_process = subprocess.Popen(streamlink_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
|
|
|
| 126 |
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 127 |
|
|
|
|
| 128 |
def feed_overlay():
|
| 129 |
while ffmpeg_process.poll() is None:
|
| 130 |
try:
|
|
@@ -133,11 +120,12 @@ def start_streaming(yt_key, tiktok_username):
|
|
| 133 |
ffmpeg_process.stdin.flush()
|
| 134 |
except:
|
| 135 |
break
|
| 136 |
-
time.sleep(
|
| 137 |
ffmpeg_process.stdin.close()
|
| 138 |
|
| 139 |
threading.Thread(target=feed_overlay, daemon=True).start()
|
| 140 |
|
|
|
|
| 141 |
while ffmpeg_process.poll() is None:
|
| 142 |
data = sl_process.stdout.read(8192)
|
| 143 |
if not data:
|
|
@@ -155,40 +143,25 @@ def start_streaming(yt_key, tiktok_username):
|
|
| 155 |
st.set_page_config(page_title="بث تيك توك مع تعليقات")
|
| 156 |
st.title("📱 بث تيك توك إلى يوتيوب مع تعليقات متحركة")
|
| 157 |
|
| 158 |
-
if "client_status" not in st.session_state:
|
| 159 |
-
st.session_state["client_status"] = "⏳ في انتظار البدء..."
|
| 160 |
-
|
| 161 |
with st.form(key="stream_form"):
|
| 162 |
tiktok_user = st.text_input("معرف مستخدم تيك توك (بدون @)").strip()
|
| 163 |
yt_stream_key = st.text_input("مفتاح بث يوتيوب (RTMP Key)", type="password").strip()
|
| 164 |
-
enable_fake = st.checkbox("تفعيل التعليقات الوهمية للتجربة", value=True)
|
| 165 |
submit_button = st.form_submit_button(label="ابدأ البث الآن")
|
| 166 |
|
| 167 |
if submit_button:
|
| 168 |
if not tiktok_user or not yt_stream_key:
|
| 169 |
st.error("يرجى إدخال اسم المستخدم ومفتاح البث.")
|
| 170 |
else:
|
| 171 |
-
# ت
|
| 172 |
-
fake_comments_enabled = enable_fake
|
| 173 |
-
|
| 174 |
-
if fake_comments_enabled:
|
| 175 |
-
threading.Thread(target=fake_comment_generator, daemon=True).start()
|
| 176 |
-
|
| 177 |
threading.Thread(target=run_tiktok_client, args=(tiktok_user,), daemon=True).start()
|
|
|
|
| 178 |
threading.Thread(target=start_streaming, args=(yt_stream_key, tiktok_user), daemon=True).start()
|
| 179 |
st.success("تم بدء العمليات في الخلفية. انتظر ظهور البث على يوتيوب.")
|
| 180 |
|
| 181 |
-
# عرض حالة اتصال التعليقات
|
| 182 |
-
st.subheader("📡 حالة اتصال التعليقات")
|
| 183 |
-
status_text = st.empty()
|
| 184 |
-
status_text.text(st.session_state["client_status"])
|
| 185 |
-
|
| 186 |
# عرض التعليقات الحية
|
| 187 |
st.subheader("💬 أحدث التعليقات")
|
| 188 |
placeholder = st.empty()
|
| 189 |
-
|
| 190 |
while True:
|
| 191 |
-
status_text.text(st.session_state["client_status"])
|
| 192 |
with lock:
|
| 193 |
current = comments_list[-8:] if comments_list else ["لا توجد تعليقات بعد"]
|
| 194 |
placeholder.text("\n".join(current))
|
|
|
|
| 11 |
from TikTokLive.events import CommentEvent, DisconnectEvent, ConnectEvent
|
| 12 |
from TikTokLive.client.errors import UserOfflineError, UserNotFoundError
|
| 13 |
|
| 14 |
+
# ---------- إعدادات ثابتة ----------
|
| 15 |
+
FONT_PATH = "arial.ttf" # تأكد من وجود ملف خط في مجلد المشروع
|
| 16 |
+
FONT_URL = "https://github.com/mokhtarbsaid/rare-arabic-fonts/raw/main/palestine/palestine-regular.ttf" # خط احتياطي
|
| 17 |
|
| 18 |
def download_font():
|
| 19 |
if not os.path.exists(FONT_PATH):
|
|
|
|
| 24 |
|
| 25 |
download_font()
|
| 26 |
|
| 27 |
+
# متغيرات التعليقات
|
| 28 |
comments_list = []
|
| 29 |
+
MAX_COMMENTS = 10
|
| 30 |
lock = threading.Lock()
|
| 31 |
|
| 32 |
+
# ---------- دالة توليد صورة التعليقات ----------
|
| 33 |
+
def create_comment_overlay(width=720, height=1280):
|
| 34 |
+
"""تولد صورة PNG شفافة تحتوي على أحدث التعليقات"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
| 36 |
draw = ImageDraw.Draw(img)
|
| 37 |
+
|
| 38 |
try:
|
| 39 |
+
# استخدام حجم خط مناسب للتعليقات
|
| 40 |
font = ImageFont.truetype(FONT_PATH, 28)
|
| 41 |
except:
|
| 42 |
font = ImageFont.load_default()
|
| 43 |
+
|
| 44 |
with lock:
|
| 45 |
current = comments_list[-MAX_COMMENTS:]
|
| 46 |
+
|
| 47 |
+
# رسم التعليقات من الأسفل إلى الأعلى
|
| 48 |
y_offset = height - 60
|
| 49 |
for comment in reversed(current):
|
| 50 |
+
# ظل أسود خفيف
|
| 51 |
draw.text((12, y_offset + 2), comment, font=font, fill=(0, 0, 0, 180))
|
| 52 |
+
# نص أبيض
|
| 53 |
draw.text((10, y_offset), comment, font=font, fill=(255, 255, 255, 255))
|
| 54 |
+
y_offset -= 45 # تباعد عمودي بين التعليقات
|
| 55 |
+
|
| 56 |
buf = BytesIO()
|
| 57 |
img.save(buf, format="PNG")
|
| 58 |
buf.seek(0)
|
| 59 |
return buf.read()
|
| 60 |
|
| 61 |
+
# ---------- عميل تيك توك (لجلب التعليقات) ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
def run_tiktok_client(username):
|
| 63 |
while True:
|
| 64 |
try:
|
|
|
|
| 68 |
|
| 69 |
@client.on(ConnectEvent)
|
| 70 |
async def on_connect(event: ConnectEvent):
|
| 71 |
+
print("✅ تم الاتصال بغرفة البث لجلب التعليقات")
|
| 72 |
|
| 73 |
@client.on(CommentEvent)
|
| 74 |
async def on_comment(event: CommentEvent):
|
|
|
|
| 78 |
|
| 79 |
@client.on(DisconnectEvent)
|
| 80 |
async def on_disconnect(event: DisconnectEvent):
|
| 81 |
+
print("🔌 انقطع اتصال التعليقات، إعادة المحاولة...")
|
| 82 |
|
|
|
|
| 83 |
loop.run_until_complete(client.start())
|
| 84 |
+
except (UserOfflineError, UserNotFoundError):
|
| 85 |
+
print("⚠️ المستخدم غير متصل حالياً")
|
|
|
|
|
|
|
| 86 |
except Exception as e:
|
| 87 |
+
print(f"❌ خطأ في اتصال التعليقات: {e}")
|
| 88 |
time.sleep(10)
|
| 89 |
|
| 90 |
+
# ---------- تشغيل البث ----------
|
| 91 |
def start_streaming(yt_key, tiktok_username):
|
| 92 |
+
# 1. أمر streamlink لجلب الفيديو
|
| 93 |
streamlink_cmd = f"streamlink https://www.tiktok.com/@{tiktok_username}/live best --hls-live-edge 3 --stdout"
|
| 94 |
|
| 95 |
+
# 2. أمر ffmpeg مع استقبال صورة التعليقات من أنبوب
|
| 96 |
ffmpeg_cmd = (
|
| 97 |
+
f"ffmpeg -thread_queue_size 8192 -i pipe:0 " # الفيديو من streamlink
|
| 98 |
+
f"-f image2pipe -r 1 -i pipe:1 " # صورة التعليقات من الأنبوب الثاني
|
| 99 |
+
f"-filter_complex \"[0:v]scale=720:1280[vid];[vid][1:v]overlay=0:0\" " # تراكب الصورة على الفيديو
|
| 100 |
f"-map \"[outv]\" -map 0:a? "
|
| 101 |
f"-c:v libx264 -preset ultrafast -tune zerolatency -b:v 1500k -maxrate 2000k -bufsize 3000k -g 30 "
|
| 102 |
f"-c:a aac -ar 48000 -b:a 192k -ac 2 "
|
|
|
|
| 106 |
try:
|
| 107 |
print(f"[INFO] بدء البث من @{tiktok_username} إلى يوتيوب...")
|
| 108 |
|
| 109 |
+
# تشغيل streamlink
|
| 110 |
sl_process = subprocess.Popen(streamlink_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
| 111 |
+
# تشغيل ffmpeg مع أنبوبين (stdin للفيديو والصورة)
|
| 112 |
ffmpeg_process = subprocess.Popen(ffmpeg_cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 113 |
|
| 114 |
+
# خيط لتغذية صورة التعليقات
|
| 115 |
def feed_overlay():
|
| 116 |
while ffmpeg_process.poll() is None:
|
| 117 |
try:
|
|
|
|
| 120 |
ffmpeg_process.stdin.flush()
|
| 121 |
except:
|
| 122 |
break
|
| 123 |
+
time.sleep(1) # تحديث كل ثانية
|
| 124 |
ffmpeg_process.stdin.close()
|
| 125 |
|
| 126 |
threading.Thread(target=feed_overlay, daemon=True).start()
|
| 127 |
|
| 128 |
+
# تغذية الفيديو من streamlink
|
| 129 |
while ffmpeg_process.poll() is None:
|
| 130 |
data = sl_process.stdout.read(8192)
|
| 131 |
if not data:
|
|
|
|
| 143 |
st.set_page_config(page_title="بث تيك توك مع تعليقات")
|
| 144 |
st.title("📱 بث تيك توك إلى يوتيوب مع تعليقات متحركة")
|
| 145 |
|
|
|
|
|
|
|
|
|
|
| 146 |
with st.form(key="stream_form"):
|
| 147 |
tiktok_user = st.text_input("معرف مستخدم تيك توك (بدون @)").strip()
|
| 148 |
yt_stream_key = st.text_input("مفتاح بث يوتيوب (RTMP Key)", type="password").strip()
|
|
|
|
| 149 |
submit_button = st.form_submit_button(label="ابدأ البث الآن")
|
| 150 |
|
| 151 |
if submit_button:
|
| 152 |
if not tiktok_user or not yt_stream_key:
|
| 153 |
st.error("يرجى إدخال اسم المستخدم ومفتاح البث.")
|
| 154 |
else:
|
| 155 |
+
# تشغيل عميل التعليقات
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
threading.Thread(target=run_tiktok_client, args=(tiktok_user,), daemon=True).start()
|
| 157 |
+
# بدء البث
|
| 158 |
threading.Thread(target=start_streaming, args=(yt_stream_key, tiktok_user), daemon=True).start()
|
| 159 |
st.success("تم بدء العمليات في الخلفية. انتظر ظهور البث على يوتيوب.")
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
# عرض التعليقات الحية
|
| 162 |
st.subheader("💬 أحدث التعليقات")
|
| 163 |
placeholder = st.empty()
|
|
|
|
| 164 |
while True:
|
|
|
|
| 165 |
with lock:
|
| 166 |
current = comments_list[-8:] if comments_list else ["لا توجد تعليقات بعد"]
|
| 167 |
placeholder.text("\n".join(current))
|