no-name-here commited on
Commit
cc0df58
Β·
verified Β·
1 Parent(s): 379ba15

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -8
app.py CHANGED
@@ -1,5 +1,11 @@
1
  """
2
- Advanced Stream Bot β€” v3.2.0
 
 
 
 
 
 
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.2.0"
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[-8:]) if logs else "No logs yet."
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
  # ────────────���─────────────────────────────────