Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
-
Advanced Stream Bot β v3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
- Live view: status+log bubble auto-edits every 2s (logg.py pattern)
|
| 4 |
- Pause/Resume/Refresh/Close live view controls
|
| 5 |
- Force Reboot button + /reboot command β last-resort recovery
|
|
@@ -37,7 +43,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
|
| 37 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
# VERSION & APP
|
| 39 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
-
APP_VERSION = "3.
|
| 41 |
|
| 42 |
app = FastAPI(title="Advanced Stream Bot", version=APP_VERSION)
|
| 43 |
scheduler = BackgroundScheduler(timezone="UTC")
|
|
@@ -391,7 +397,7 @@ def _build_live_view_text(chat_id: int, mode: str = "status") -> str:
|
|
| 391 |
else:
|
| 392 |
status = compose_status_message(chat_id, include_config=False, include_logs=False)
|
| 393 |
logs = session.get('live_log_lines_user', [])
|
| 394 |
-
log_tail = "\n".join(logs[-
|
| 395 |
pause_note = "\n\n<i>βΈ Auto-updates paused β tap βΆοΈ Resume to continue</i>" if is_paused else ""
|
| 396 |
|
| 397 |
# Only show log tail while actively streaming
|
|
@@ -870,6 +876,9 @@ def compose_status_message(chat_id: int, include_config: bool = False, include_l
|
|
| 870 |
|
| 871 |
lines += [
|
| 872 |
"",
|
|
|
|
|
|
|
|
|
|
| 873 |
f"π¬ <b>Input:</b> <code>{esc(cur_url)}</code> ({idx+1}/{len(playlist)})",
|
| 874 |
f"π <b>Loop:</b> <code>{session.get('current_loop_iteration', 0)+1}/{loop_total}</code>",
|
| 875 |
]
|
|
@@ -1034,6 +1043,48 @@ def stream_engine_thread_target(chat_id: int):
|
|
| 1034 |
|
| 1035 |
active_output_container = None
|
| 1036 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
try:
|
| 1038 |
with lock:
|
| 1039 |
session['streaming_state'] = "starting"
|
|
@@ -1046,11 +1097,16 @@ def stream_engine_thread_target(chat_id: int):
|
|
| 1046 |
session['stop_gracefully_flag'] = False
|
| 1047 |
session['reconnect_attempt'] = 0
|
| 1048 |
session['last_frame_time'] = None
|
|
|
|
| 1049 |
session['pyav_objects'] = {
|
| 1050 |
"input_container": None, "output_container": None,
|
| 1051 |
"video_out_stream": None, "audio_out_stream": None,
|
| 1052 |
"logo_image_pil": None,
|
| 1053 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
|
| 1055 |
append_user_live_log(chat_id, f"Stream engine started β {session['output_url']}")
|
| 1056 |
|
|
@@ -1267,15 +1323,22 @@ def stream_engine_thread_target(chat_id: int):
|
|
| 1267 |
except Exception as e_logo:
|
| 1268 |
pass
|
| 1269 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1270 |
for out_pkt in active_video_out_stream.encode(frame):
|
| 1271 |
active_output_container.mux(out_pkt)
|
| 1272 |
with lock:
|
| 1273 |
-
session['frames_encoded'] += 1
|
| 1274 |
session['bytes_sent'] += out_pkt.size
|
| 1275 |
-
session['last_frame_time'] = datetime.datetime.now()
|
| 1276 |
-
# Push live stats to SSE subscribers every 100 frames
|
| 1277 |
-
if session['frames_encoded'] % 100 == 0:
|
| 1278 |
-
threading.Thread(target=_push_state_sse, args=(chat_id,), daemon=True).start()
|
| 1279 |
|
| 1280 |
elif active_audio_out_stream and packet.stream.type == 'audio' and in_a_streams and packet.stream.index == in_a_streams[0].index:
|
| 1281 |
for frame in packet.decode():
|
|
@@ -1372,6 +1435,13 @@ def stream_engine_thread_target(chat_id: int):
|
|
| 1372 |
notify_state_change(chat_id, "error", f"Fatal error: {esc(str(e_fatal))}")
|
| 1373 |
|
| 1374 |
finally:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1375 |
append_user_live_log(chat_id, "Finalizing streamβ¦")
|
| 1376 |
|
| 1377 |
# Flush encoders
|
|
@@ -3046,6 +3116,54 @@ async def get_status_endpoint(chat_id: int):
|
|
| 3046 |
}
|
| 3047 |
|
| 3048 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3049 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3050 |
# MAIN
|
| 3051 |
# ββββββββββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββββββββββ
|
|
|
|
| 1 |
"""
|
| 2 |
+
Advanced Stream Bot β v3.3.0
|
| 3 |
+
- Fixed: frames_encoded now counts decoded input frames (encoder buffering no longer causes 0)
|
| 4 |
+
- Fixed: live_log_lines_user cleared on stream start so stale logs don't persist
|
| 5 |
+
- Fixed: last_sent reset on stream start so bubble refreshes immediately
|
| 6 |
+
- Fixed: raw FFmpeg logs via queue-drained thread (bitrate, fps, codec init, errors)
|
| 7 |
+
- Fixed: log tail display increased 8β15 lines
|
| 8 |
+
- New: GET /poll/{chat_id} endpoint β returns pending live-view edit for proactive refresh
|
| 9 |
- Live view: status+log bubble auto-edits every 2s (logg.py pattern)
|
| 10 |
- Pause/Resume/Refresh/Close live view controls
|
| 11 |
- Force Reboot button + /reboot command β last-resort recovery
|
|
|
|
| 43 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
# VERSION & APP
|
| 45 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
APP_VERSION = "3.3.0"
|
| 47 |
|
| 48 |
app = FastAPI(title="Advanced Stream Bot", version=APP_VERSION)
|
| 49 |
scheduler = BackgroundScheduler(timezone="UTC")
|
|
|
|
| 397 |
else:
|
| 398 |
status = compose_status_message(chat_id, include_config=False, include_logs=False)
|
| 399 |
logs = session.get('live_log_lines_user', [])
|
| 400 |
+
log_tail = "\n".join(logs[-15:]) if logs else "No logs yet."
|
| 401 |
pause_note = "\n\n<i>βΈ Auto-updates paused β tap βΆοΈ Resume to continue</i>" if is_paused else ""
|
| 402 |
|
| 403 |
# Only show log tail while actively streaming
|
|
|
|
| 876 |
|
| 877 |
lines += [
|
| 878 |
"",
|
| 879 |
+
f"β± <b>Uptime:</b> <code>{get_uptime(session.get('stream_start_time'))}</code>",
|
| 880 |
+
f"π <b>Frames:</b> <code>{session.get('frames_encoded', 0):,}</code>",
|
| 881 |
+
f"π€ <b>Sent:</b> <code>{session.get('bytes_sent', 0) / (1024*1024):.2f} MB</code>",
|
| 882 |
f"π¬ <b>Input:</b> <code>{esc(cur_url)}</code> ({idx+1}/{len(playlist)})",
|
| 883 |
f"π <b>Loop:</b> <code>{session.get('current_loop_iteration', 0)+1}/{loop_total}</code>",
|
| 884 |
]
|
|
|
|
| 1043 |
|
| 1044 |
active_output_container = None
|
| 1045 |
|
| 1046 |
+
# ββ Raw FFmpeg/libav log capture via queue ββββββββββββββββββββββββββββββββ
|
| 1047 |
+
# PyAV's av.logging.set_log_callback installs a C-level callback. We route
|
| 1048 |
+
# every libav message into a thread-safe queue and drain it in a background
|
| 1049 |
+
# thread so we never call Python from a C signal context.
|
| 1050 |
+
_fflog_queue: queue.Queue = queue.Queue(maxsize=500)
|
| 1051 |
+
_fflog_stop = threading.Event()
|
| 1052 |
+
|
| 1053 |
+
def _fflog_drain():
|
| 1054 |
+
_LEVELS = {0:"PANIC",8:"FATAL",16:"ERROR",24:"WARN",32:"INFO",40:"VERBOSE"}
|
| 1055 |
+
_last = ""
|
| 1056 |
+
while not _fflog_stop.is_set() or not _fflog_queue.empty():
|
| 1057 |
+
try:
|
| 1058 |
+
level, msg = _fflog_queue.get(timeout=0.3)
|
| 1059 |
+
except queue.Empty:
|
| 1060 |
+
continue
|
| 1061 |
+
msg = msg.strip()
|
| 1062 |
+
if not msg or msg == _last:
|
| 1063 |
+
continue
|
| 1064 |
+
# Skip high-volume noise
|
| 1065 |
+
if any(s in msg for s in ("Past duration", "deprecated pixel format",
|
| 1066 |
+
"DTS", "PTS", "non monotonous")):
|
| 1067 |
+
continue
|
| 1068 |
+
_last = msg
|
| 1069 |
+
lvl = _LEVELS.get(level, f"L{level}")
|
| 1070 |
+
prefix = f"[ffmpeg/{lvl}]" if level <= 24 else "[ffmpeg]"
|
| 1071 |
+
append_user_live_log(chat_id, f"{prefix} {msg}")
|
| 1072 |
+
|
| 1073 |
+
def _fflog_callback(level, message):
|
| 1074 |
+
try:
|
| 1075 |
+
_fflog_queue.put_nowait((level, message))
|
| 1076 |
+
except queue.Full:
|
| 1077 |
+
pass
|
| 1078 |
+
|
| 1079 |
+
_fflog_thread = threading.Thread(target=_fflog_drain, name=f"FFlogDrain-{chat_id}", daemon=True)
|
| 1080 |
+
_fflog_thread.start()
|
| 1081 |
+
try:
|
| 1082 |
+
av.logging.set_log_level(av.logging.VERBOSE)
|
| 1083 |
+
av.logging.set_log_callback(_fflog_callback)
|
| 1084 |
+
except Exception as _e_avlog:
|
| 1085 |
+
append_user_live_log(chat_id, f"[warn] av log callback unavailable: {_e_avlog}")
|
| 1086 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1087 |
+
|
| 1088 |
try:
|
| 1089 |
with lock:
|
| 1090 |
session['streaming_state'] = "starting"
|
|
|
|
| 1097 |
session['stop_gracefully_flag'] = False
|
| 1098 |
session['reconnect_attempt'] = 0
|
| 1099 |
session['last_frame_time'] = None
|
| 1100 |
+
session['live_log_lines_user'] = [] # clear stale logs from previous run
|
| 1101 |
session['pyav_objects'] = {
|
| 1102 |
"input_container": None, "output_container": None,
|
| 1103 |
"video_out_stream": None, "audio_out_stream": None,
|
| 1104 |
"logo_image_pil": None,
|
| 1105 |
}
|
| 1106 |
+
# Also reset last_sent so the live bubble always refreshes immediately
|
| 1107 |
+
with _live_view_lock:
|
| 1108 |
+
if chat_id in _live_views:
|
| 1109 |
+
_live_views[chat_id]["last_sent"] = ""
|
| 1110 |
|
| 1111 |
append_user_live_log(chat_id, f"Stream engine started β {session['output_url']}")
|
| 1112 |
|
|
|
|
| 1323 |
except Exception as e_logo:
|
| 1324 |
pass
|
| 1325 |
|
| 1326 |
+
# Count decoded input frames immediately.
|
| 1327 |
+
# The encoder buffers many frames before emitting
|
| 1328 |
+
# the first output packet, so counting only encoded
|
| 1329 |
+
# output kept stats stuck at 0 for a long time.
|
| 1330 |
+
with lock:
|
| 1331 |
+
session['frames_encoded'] += 1
|
| 1332 |
+
session['last_frame_time'] = datetime.datetime.now()
|
| 1333 |
+
_fc = session['frames_encoded']
|
| 1334 |
+
# Push live stats every 30 decoded frames (~1s at 30fps)
|
| 1335 |
+
if _fc % 30 == 0:
|
| 1336 |
+
threading.Thread(target=_push_state_sse, args=(chat_id,), daemon=True).start()
|
| 1337 |
+
|
| 1338 |
for out_pkt in active_video_out_stream.encode(frame):
|
| 1339 |
active_output_container.mux(out_pkt)
|
| 1340 |
with lock:
|
|
|
|
| 1341 |
session['bytes_sent'] += out_pkt.size
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1342 |
|
| 1343 |
elif active_audio_out_stream and packet.stream.type == 'audio' and in_a_streams and packet.stream.index == in_a_streams[0].index:
|
| 1344 |
for frame in packet.decode():
|
|
|
|
| 1435 |
notify_state_change(chat_id, "error", f"Fatal error: {esc(str(e_fatal))}")
|
| 1436 |
|
| 1437 |
finally:
|
| 1438 |
+
# Shut down raw ffmpeg log capture
|
| 1439 |
+
try:
|
| 1440 |
+
av.logging.restore_default_callback()
|
| 1441 |
+
except Exception:
|
| 1442 |
+
pass
|
| 1443 |
+
_fflog_stop.set()
|
| 1444 |
+
|
| 1445 |
append_user_live_log(chat_id, "Finalizing streamβ¦")
|
| 1446 |
|
| 1447 |
# Flush encoders
|
|
|
|
| 3116 |
}
|
| 3117 |
|
| 3118 |
|
| 3119 |
+
@app.get("/poll/{chat_id}")
|
| 3120 |
+
async def poll_pending_edit(chat_id: int):
|
| 3121 |
+
"""
|
| 3122 |
+
Lightweight polling endpoint β returns the pending live-view edit (if any)
|
| 3123 |
+
as a Telegram Bot API method dict, then clears it.
|
| 3124 |
+
|
| 3125 |
+
The Telegram Mini App / web dashboard can call this every 2s and POST the
|
| 3126 |
+
returned payload directly to api.telegram.org/bot<TOKEN>/editMessageText to
|
| 3127 |
+
achieve real-time live-view updates without waiting for an incoming webhook.
|
| 3128 |
+
|
| 3129 |
+
If no edit is pending, returns {"pending": false}.
|
| 3130 |
+
Also always includes a fresh status snapshot so callers can update their UI.
|
| 3131 |
+
"""
|
| 3132 |
+
if chat_id not in user_sessions:
|
| 3133 |
+
raise HTTPException(status_code=404, detail="No session for this chat_id")
|
| 3134 |
+
|
| 3135 |
+
# Force-build a fresh live-view edit right now
|
| 3136 |
+
_do_live_view_edit(chat_id, force=True)
|
| 3137 |
+
|
| 3138 |
+
with _outbound_queue_lock:
|
| 3139 |
+
msgs = _outbound_message_queue.pop(chat_id, [])
|
| 3140 |
+
|
| 3141 |
+
# Pick the most recent editMessageText (last one wins β it's the freshest)
|
| 3142 |
+
edit_msg = None
|
| 3143 |
+
for m in reversed(msgs):
|
| 3144 |
+
if m.get("method") == "editMessageText":
|
| 3145 |
+
edit_msg = m
|
| 3146 |
+
break
|
| 3147 |
+
# Re-queue any non-edit messages (sendMessage notifications etc.)
|
| 3148 |
+
for m in msgs:
|
| 3149 |
+
if m.get("method") != "editMessageText":
|
| 3150 |
+
enqueue_outbound_message(chat_id, m)
|
| 3151 |
+
|
| 3152 |
+
session = get_user_session(chat_id)
|
| 3153 |
+
return {
|
| 3154 |
+
"pending": edit_msg is not None,
|
| 3155 |
+
"edit": edit_msg,
|
| 3156 |
+
"status": {
|
| 3157 |
+
"state": session.get("streaming_state", "idle"),
|
| 3158 |
+
"frames_encoded": session.get("frames_encoded", 0),
|
| 3159 |
+
"bytes_sent": session.get("bytes_sent", 0),
|
| 3160 |
+
"uptime_str": get_uptime(session.get("stream_start_time")),
|
| 3161 |
+
"logs": session.get("live_log_lines_user", [])[-15:],
|
| 3162 |
+
},
|
| 3163 |
+
"ts": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
| 3164 |
+
}
|
| 3165 |
+
|
| 3166 |
+
|
| 3167 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 3168 |
# MAIN
|
| 3169 |
# ββββββββββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββββββββββ
|