Reoo88 commited on
Commit
438da7d
·
verified ·
1 Parent(s): 6b1f746

Real YouTube loading fix: never delay copy reconnect once the event is live; dump_extra on H264 copy IDR

Browse files
Files changed (2) hide show
  1. relay.py +4 -0
  2. youtube_manager.py +40 -15
relay.py CHANGED
@@ -341,6 +341,10 @@ class Sender:
341
  enhanced=self.enhanced_advertisement(mode,candidate) if network else []
342
  if mode=='copy':
343
  video=['-c:v','copy']
 
 
 
 
344
  else:
345
  width,height,bitrate=(1080,1920,'6000k') if mode=='fixed1080' else (720,1280,'4000k')
346
  # VIDEO_BITRATE was documented in the README but never read anywhere; it now caps the
 
341
  enhanced=self.enhanced_advertisement(mode,candidate) if network else []
342
  if mode=='copy':
343
  video=['-c:v','copy']
344
+ # Repeat SPS/PPS on every IDR so a YouTube viewer who hits loading can
345
+ # resync without leaving the app. Video remains copy (no re-encode).
346
+ if not self.hevc_payload(candidate):
347
+ video += ['-bsf:v','dump_extra=freq=keyframe']
348
  else:
349
  width,height,bitrate=(1080,1920,'6000k') if mode=='fixed1080' else (720,1280,'4000k')
350
  # VIDEO_BITRATE was documented in the README but never read anywhere; it now caps the
youtube_manager.py CHANGED
@@ -698,11 +698,25 @@ class ManagedYouTube:
698
  self.log(user or 'system', '[WATCHDOG] أُلغي تأجيل إعادة الاتصال (%d حدث) — %s' % (dropped, why))
699
  return dropped
700
 
 
 
 
 
 
 
 
 
701
  def start_sender(self,key):
702
- # عقاب الوتيرة يجب أن يصمد أمام نقرات tick(): أي أن إعادة الاتصال المؤجّلة بسبب رفض
703
- # الوجهة (187) لا تُبطلها دورة رصد جاءت بعد 15 ث. بدون هذا القفل يبقى التأجيل شكليًا
704
- # لأن tick/drive_event يستدعيان start_sender كل دورة.
 
705
  until=self.retry_at.get(key)
 
 
 
 
 
706
  if until and time.monotonic()<until:
707
  left=int(round(until-time.monotonic()))
708
  # الصمت هنا هو بالضبط «بالانتظار بلا سبب» الذي شكا منه المستخدم: القفل صحيح لكن يجب
@@ -766,7 +780,7 @@ class ManagedYouTube:
766
  return
767
  if hub.busy(ev['username']):
768
  return
769
- if time.monotonic()-float(self._spawn_at.get(ev['username']) or 0)<8:
770
  return
771
  # لا نلمس entry هنا: بعد موت خيط الإرسال تُبقي tick() القيد الميت في self.senders،
772
  # و`entry['candidate']=None` على قيد ميت كان يرمي TypeError عند كل دورة مشغّل
@@ -812,7 +826,8 @@ class ManagedYouTube:
812
  if not since:
813
  entry['idle_since']=time.monotonic()
814
  continue
815
- if time.monotonic()-since<20:
 
816
  continue
817
  self.log(ev.get('username') or key,
818
  '[RELAY] @%s sender thread without FFmpeg; recycle'%ev.get('username'))
@@ -959,7 +974,11 @@ class ManagedYouTube:
959
  # Skip the idle wait right after a resolution change; otherwise keep polling.
960
  if restart_at and time.monotonic()-restart_at<30:restart_at=0.0;continue
961
  gap=float(getattr(sender,'last_source_gap',0) or 0)
962
- wait=0.5 if gap>=8 else self._backoff(user)
 
 
 
 
963
  if stop.wait(wait):return
964
  def mark_stream_stable(self, user):
965
  """الحافة تقبل التدفق: تُرمى نافذة العقاب وملاحظتها وعدّادها. تُستدعى من المرسل عند أول
@@ -1054,15 +1073,21 @@ class ManagedYouTube:
1054
  except Exception as e:
1055
  self.log(user, '[WATCHDOG] تعذر تجديد عنوان الاستقبال: ' + self.safe_error(e)[:120])
1056
  if n >= 3:
1057
- # أرضية 20 ث × تصاعد هندسي: retry_min=1 في الصورة كان يجعل الطلعة الثالثة «2 ث»
1058
- # (لا شيء يتغيّر فعليًا)، لذا الحدّ الأدنى 20 ث ثم 40 و80 حتى 120.
1059
- wait = min(120.0, max(20.0, self.retry_min) * (2 ** (n - 3)))
1060
- self.retry[user] = max(self.retry.get(user, 0) or 0, wait)
1061
- for k in [k for k, ev in self.journal.read()['events'].items() if ev.get('username') == user]:
1062
- self.retry_at[k] = time.monotonic() + wait # يقفل start_sender من tick() نفسه
1063
- self.log(user, '[WATCHDOG] الوجهة أغلقت الاتصال بعد %.0f ث (رمز %s) — المرة %d: تأجيل إعادة الاتصال إلى %d ث بدل النقر على حافة ترفض%s'
1064
- % (lasted, code, n, int(self._backoff(user)),
1065
- '؛ السبب المعلوم: الحدث لم يُشغَّل بعد' if not_started else ''))
 
 
 
 
 
 
1066
  return
1067
 
1068
  def _backoff(self,user):
 
698
  self.log(user or 'system', '[WATCHDOG] أُلغي تأجيل إعادة الاتصال (%d حدث) — %s' % (dropped, why))
699
  return dropped
700
 
701
+
702
+ def _yt_public(self, ev):
703
+ """Viewers can already open the watch URL — any delay is a YouTube loading spinner."""
704
+ if not ev:
705
+ return False
706
+ if ev.get('was_live'):
707
+ return True
708
+ return str(ev.get('youtube_status') or '') in ('live', 'testing', 'liveStarting')
709
  def start_sender(self,key):
710
+ ev=self.journal.read()['events'].get(key)
711
+ public=self._yt_public(ev)
712
+ # عقاب الوتيرة يجب أن يصمد أمام نقرات tick() قبل أن يصبح الحدث علنيًا.
713
+ # بعد live أي تأخير = دائرة تحميل أمام المشاهد — نلغي القفل ونعيد copy فورًا.
714
  until=self.retry_at.get(key)
715
+ if public:
716
+ if until:
717
+ self.retry_at.pop(key, None)
718
+ self._gate_note_at.pop(key, None)
719
+ until=None
720
  if until and time.monotonic()<until:
721
  left=int(round(until-time.monotonic()))
722
  # الصمت هنا هو بالضبط «بالانتظار بلا سبب» الذي شكا منه المستخدم: القفل صحيح لكن يجب
 
780
  return
781
  if hub.busy(ev['username']):
782
  return
783
+ if (not public) and time.monotonic()-float(self._spawn_at.get(ev['username']) or 0)<8:
784
  return
785
  # لا نلمس entry هنا: بعد موت خيط الإرسال تُبقي tick() القيد الميت في self.senders،
786
  # و`entry['candidate']=None` على قيد ميت كان يرمي TypeError عند كل دورة مشغّل
 
826
  if not since:
827
  entry['idle_since']=time.monotonic()
828
  continue
829
+ grace=2.0 if self._yt_public(ev) else 20.0
830
+ if time.monotonic()-since<grace:
831
  continue
832
  self.log(ev.get('username') or key,
833
  '[RELAY] @%s sender thread without FFmpeg; recycle'%ev.get('username'))
 
974
  # Skip the idle wait right after a resolution change; otherwise keep polling.
975
  if restart_at and time.monotonic()-restart_at<30:restart_at=0.0;continue
976
  gap=float(getattr(sender,'last_source_gap',0) or 0)
977
+ ev_now=(self.journal.read()['events'].get(key) or {})
978
+ if self._yt_public(ev_now):
979
+ wait=0.5
980
+ else:
981
+ wait=0.5 if gap>=8 else self._backoff(user)
982
  if stop.wait(wait):return
983
  def mark_stream_stable(self, user):
984
  """الحافة تقبل التدفق: تُرمى نافذة العقاب وملاحظتها وعدّادها. تُستدعى من المرسل عند أول
 
1073
  except Exception as e:
1074
  self.log(user, '[WATCHDOG] تعذر تجديد عنوان الاستقبال: ' + self.safe_error(e)[:120])
1075
  if n >= 3:
1076
+ public=any(self._yt_public(ev) for ev in _evs)
1077
+ if public:
1078
+ # الحدث أمام المشاهد: أي تأجيل = دائرة تحميل. نعيد copy على نفس المفتاح فورًا.
1079
+ for k in [k for k, ev in self.journal.read()['events'].items() if ev.get('username') == user]:
1080
+ self.retry_at.pop(k, None)
1081
+ self.retry[user] = self.retry_min
1082
+ self.log(user, '[WATCHDOG] يوتيوب أغلق RTMPS والحدث ما زال live — إعادة copy فورًا على نفس المفتاح (بلا تأجيل %d×)'%n)
1083
+ else:
1084
+ wait = min(120.0, max(20.0, self.retry_min) * (2 ** (n - 3)))
1085
+ self.retry[user] = max(self.retry.get(user, 0) or 0, wait)
1086
+ for k in [k for k, ev in self.journal.read()['events'].items() if ev.get('username') == user]:
1087
+ self.retry_at[k] = time.monotonic() + wait
1088
+ self.log(user, '[WATCHDOG] الوجهة أغلقت الاتصال بعد %.0f ث (رمز %s) — المرة %d: تأجيل إعادة الاتصال إلى %d ث بدل النقر على حافة ترفض%s'
1089
+ % (lasted, code, n, int(self._backoff(user)),
1090
+ '؛ السبب المعلوم: الحدث لم يُشغَّل بعد' if not_started else ''))
1091
  return
1092
 
1093
  def _backoff(self,user):