Reoo88 commited on
Commit
23e9e2b
·
verified ·
1 Parent(s): 2264c03

Revert assistant HD changes; restore original relay behavior

Browse files
Files changed (8) hide show
  1. Dockerfile +1 -5
  2. README.md +0 -21
  3. app.py +1 -3
  4. index.html +0 -1
  5. relay.py +0 -14
  6. test_quality_profile.py +0 -36
  7. verify.sh +0 -1
  8. youtube_manager.py +1 -15
Dockerfile CHANGED
@@ -6,7 +6,7 @@ WORKDIR /app
6
  COPY requirements.txt .
7
  RUN pip install --no-cache-dir -U -r requirements.txt
8
  COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py stream_monitor.py tiktok_auth.py ingest.py index.html ./
9
- COPY verify.sh verify_tokens.py probe_live.py test_watchdog.py test_lifecycle.py test_relay.py test_source_reconnect.py test_ingest_routes.py test_transition_ingest.py test_concurrency.py test_flv_bridge.py test_adaptive_format.py test_regressions.py test_completion.py test_channel_switch.py test_dashboard.js test_quality_profile.py ./
10
 
11
  # TikTok Live Monitor → Direct Relay → YouTube. No local video files.
12
  ENV TZ=Asia/Baghdad \
@@ -62,9 +62,5 @@ ENV TZ=Asia/Baghdad \
62
  PYTHONUNBUFFERED=1 \
63
  YOUTUBE_VIDEO_MODE=original
64
 
65
- # Keep this broadcaster's output HD even when TikTok starts with an SD rendition.
66
- # Override with {} in Space variables to restore the legacy per-group policy.
67
- ENV YOUTUBE_ACCOUNT_SEND_MODES='{"muhajer_iraqi":"fixed720"}'
68
-
69
  EXPOSE 7860
70
  CMD ["sh", "-c", "exec uvicorn app:app --host 0.0.0.0 --port ${PORT:-7860} --timeout-graceful-shutdown 65 --no-access-log"]
 
6
  COPY requirements.txt .
7
  RUN pip install --no-cache-dir -U -r requirements.txt
8
  COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py stream_monitor.py tiktok_auth.py ingest.py index.html ./
9
+ COPY verify.sh verify_tokens.py probe_live.py test_watchdog.py test_lifecycle.py test_relay.py test_source_reconnect.py test_ingest_routes.py test_transition_ingest.py test_concurrency.py test_flv_bridge.py test_adaptive_format.py test_regressions.py test_completion.py test_channel_switch.py test_dashboard.js ./
10
 
11
  # TikTok Live Monitor → Direct Relay → YouTube. No local video files.
12
  ENV TZ=Asia/Baghdad \
 
62
  PYTHONUNBUFFERED=1 \
63
  YOUTUBE_VIDEO_MODE=original
64
 
 
 
 
 
65
  EXPOSE 7860
66
  CMD ["sh", "-c", "exec uvicorn app:app --host 0.0.0.0 --port ${PORT:-7860} --timeout-graceful-shutdown 65 --no-access-log"]
README.md CHANGED
@@ -112,24 +112,3 @@ PASSWORD=test DATA_DIR="$(mktemp -d)" sh verify.sh
112
  This application uses YouTube API Services to allow authenticated users to broadcast live content to their own YouTube channels.
113
  - By using this app, users agree to be bound by the [YouTube Terms of Service](https://www.youtube.com/t/terms) and [Google Privacy Policy](https://www.google.com/policies/privacy).
114
  - We do not store or share personal user data with third parties.
115
-
116
-
117
- ## Per-account HD output (2026-10-10)
118
-
119
- `YOUTUBE_ACCOUNT_SEND_MODES` is an optional JSON object mapping usernames to
120
- `matchgop`, `fixed720`, or `fixed1080`. It takes precedence over the legacy
121
- `YOUTUBE_FORCE_TRANSCODE_USERS` group for listed accounts only, and uses the same
122
- encoder-slot limits. Invalid optional settings fall back to the legacy policy.
123
-
124
- The image sets `{"muhajer_iraqi":"fixed720"}`: 720×1280 H.264, 4000 kbit/s unless
125
- `VIDEO_BITRATE` overrides it. This avoids retaining a 360×640 matchgop canvas when
126
- TikTok later supplies HD. It does not create detail missing from an SD source.
127
- Other accounts retain their existing policy. Set the Space variable
128
- `YOUTUBE_ACCOUNT_SEND_MODES` to `{}` to restore the legacy group policy (restart
129
- required). The dashboard and `[QUALITY]` log distinguish encoder output from
130
- source metadata; these are not measurements of YouTube's playback renditions.
131
-
132
- Checks: `python test_quality_profile.py` covers override isolation, invalid config,
133
- persisted SD canvas, and unchanged matchgop locking. A local synthetic 1080×1920
134
- FFmpeg input was also encoded successfully as 720×1280 H.264 at a configured
135
- 4000 kbit/s. Actual YouTube availability still requires a live playback check.
 
112
  This application uses YouTube API Services to allow authenticated users to broadcast live content to their own YouTube channels.
113
  - By using this app, users agree to be bound by the [YouTube Terms of Service](https://www.youtube.com/t/terms) and [Google Privacy Policy](https://www.google.com/policies/privacy).
114
  - We do not store or share personal user data with third parties.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -356,7 +356,7 @@ def source_brief(item):
356
  nums=[int(n) for n in re.findall(r'\d+',res)]
357
  pretty=''
358
  if len(nums)>=2:
359
- h=min(nums[0],nums[1])
360
  pretty='%dp'%h if h>=144 else '%sx%s'%(nums[0],nums[1])
361
  elif nums:
362
  pretty='%dp'%nums[0]
@@ -970,8 +970,6 @@ def state():
970
  'last_data_time':rs.get('last_data_time'),'relay_alive':rs.get('relay_alive'),
971
  'session_title':rs.get('session_title') or '','restarts':rs.get('restarts') or 0,
972
  'source_age':rs.get('source_age'),'send_mode':rs.get('send_mode') or '',
973
- 'output_resolution':rs.get('output_resolution') or '',
974
- 'output_video_bitrate':rs.get('output_video_bitrate') or '',
975
  'youtube_health':str(ye.get('youtube_health') or ''),
976
  'youtube_issues':str(ye.get('youtube_issues') or '')[:120],
977
  'youtube_status':str(ye.get('youtube_status') or ('relay' if rs.get('relay_alive') else (t.get('state') or '')))})
 
356
  nums=[int(n) for n in re.findall(r'\d+',res)]
357
  pretty=''
358
  if len(nums)>=2:
359
+ h=max(nums[0],nums[1])
360
  pretty='%dp'%h if h>=144 else '%sx%s'%(nums[0],nums[1])
361
  elif nums:
362
  pretty='%dp'%nums[0]
 
970
  'last_data_time':rs.get('last_data_time'),'relay_alive':rs.get('relay_alive'),
971
  'session_title':rs.get('session_title') or '','restarts':rs.get('restarts') or 0,
972
  'source_age':rs.get('source_age'),'send_mode':rs.get('send_mode') or '',
 
 
973
  'youtube_health':str(ye.get('youtube_health') or ''),
974
  'youtube_issues':str(ye.get('youtube_issues') or '')[:120],
975
  'youtube_status':str(ye.get('youtube_status') or ('relay' if rs.get('relay_alive') else (t.get('state') or '')))})
index.html CHANGED
@@ -224,7 +224,6 @@ async function refresh(){
224
  <p>@${esc(e.username)} · ${esc(e.phase||'')} · YT ${esc(e.youtube_status||'—')}</p>
225
  <p>المصدر: ${esc(e.source||'—')}</p>
226
  <p>وضع الإرسال الفعلي: ${esc(e.send_mode||'قيد التحديد')}</p>
227
- <p>خرج المرسل إلى يوتيوب: ${esc(e.output_resolution||'غير معروف')} · ${esc(e.output_video_bitrate||'—')}</p>
228
  ${e.pending_send_mode?`<p>الترميز الثابت ينتظر خانة: ${esc(e.pending_send_mode)}</p>`:''}
229
  <small>${esc(e.status||e.note||'')} · ${Math.floor(e.output_seconds||0)} ث</small>
230
  ${e.broadcast_id?`<p><a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=${encodeURIComponent(e.broadcast_id)}">فتح على يوتيوب</a></p>`:''}
 
224
  <p>@${esc(e.username)} · ${esc(e.phase||'')} · YT ${esc(e.youtube_status||'—')}</p>
225
  <p>المصدر: ${esc(e.source||'—')}</p>
226
  <p>وضع الإرسال الفعلي: ${esc(e.send_mode||'قيد التحديد')}</p>
 
227
  ${e.pending_send_mode?`<p>الترميز الثابت ينتظر خانة: ${esc(e.pending_send_mode)}</p>`:''}
228
  <small>${esc(e.status||e.note||'')} · ${Math.floor(e.output_seconds||0)} ث</small>
229
  ${e.broadcast_id?`<p><a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=${encodeURIComponent(e.broadcast_id)}">فتح على يوتيوب</a></p>`:''}
relay.py CHANGED
@@ -564,16 +564,6 @@ class Sender:
564
  '-x264-params','nal-hrd=cbr:force-cfr=1:open-gop=0:scenecut=0:keyint=%s:min-keyint=30'%os.getenv('YOUTUBE_GOP','60'),
565
  '-pix_fmt','yuv420p','-fps_mode','cfr',
566
  '-colorspace','bt709','-color_primaries','bt709','-color_trc','bt709']
567
- # Distinguish the source label from the actual encoder canvas in diagnostics.
568
- if mode=='copy':
569
- output_w=int((candidate or {}).get('width') or 0)
570
- output_h=int((candidate or {}).get('height') or 0)
571
- elif mode=='matchgop':
572
- output_w,output_h=self._gop_w,self._gop_h
573
- else:
574
- output_w,output_h=width,height
575
- self.set(output_resolution=('%sx%s'%(output_w,output_h)) if output_w and output_h else '',
576
- output_video_bitrate=bitrate if mode!='copy' else 'source')
577
  # Network output goes through the fifo pseudo-muxer instead of straight to flv.
578
  # fifo separates encoding from the RTMPS connection and reconnects transparently
579
  # when YouTube drops it, rather than killing the whole FFmpeg process. That is
@@ -1032,10 +1022,6 @@ class Sender:
1032
  'إعلان Enhanced-RTMP: hvc1' if '-rtmp_enhanced_codecs' in ' '.join(args)
1033
  else 'FLV تقليدي: لا إعلان hvc1', self.active_mode or 'unknown')) # لا إعادة ترميز في هذا الوضع: انقطاع copy لا يترك عملية ورائها
1034
 
1035
- self.log(user, '[QUALITY] output=%s bitrate=%s mode=%s' % (
1036
- self.data.get('output_resolution') or 'unknown',
1037
- self.data.get('output_video_bitrate') or 'unknown', self.active_mode))
1038
-
1039
  def read_errors():
1040
  # يصنّف أخطاء FFmpeg المختارة؛ لا يُسرّب stderr خامًا ولا روابط البث — الذيل
1041
  # يُخفى مفتاحه ورابطه قبل أي تخزين أو طباعة.
 
564
  '-x264-params','nal-hrd=cbr:force-cfr=1:open-gop=0:scenecut=0:keyint=%s:min-keyint=30'%os.getenv('YOUTUBE_GOP','60'),
565
  '-pix_fmt','yuv420p','-fps_mode','cfr',
566
  '-colorspace','bt709','-color_primaries','bt709','-color_trc','bt709']
 
 
 
 
 
 
 
 
 
 
567
  # Network output goes through the fifo pseudo-muxer instead of straight to flv.
568
  # fifo separates encoding from the RTMPS connection and reconnects transparently
569
  # when YouTube drops it, rather than killing the whole FFmpeg process. That is
 
1022
  'إعلان Enhanced-RTMP: hvc1' if '-rtmp_enhanced_codecs' in ' '.join(args)
1023
  else 'FLV تقليدي: لا إعلان hvc1', self.active_mode or 'unknown')) # لا إعادة ترميز في هذا الوضع: انقطاع copy لا يترك عملية ورائها
1024
 
 
 
 
 
1025
  def read_errors():
1026
  # يصنّف أخطاء FFmpeg المختارة؛ لا يُسرّب stderr خامًا ولا روابط البث — الذيل
1027
  # يُخفى مفتاحه ورابطه قبل أي تخزين أو طباعة.
test_quality_profile.py DELETED
@@ -1,36 +0,0 @@
1
- """HD output must not inherit a persisted SD matchgop canvas."""
2
- import os, unittest
3
- from unittest.mock import patch
4
- from relay import Sender
5
- from youtube_manager import ManagedYouTube
6
-
7
- class QualityProfileTests(unittest.TestCase):
8
- def test_specific_profile_does_not_change_other_accounts(self):
9
- with patch.dict(os.environ, {'YOUTUBE_ACCOUNT_SEND_MODES':'{"muhajer_iraqi":"fixed720"}',
10
- 'YOUTUBE_FORCE_TRANSCODE_USERS':'muhajer_iraqi,other',
11
- 'YOUTUBE_FORCE_TRANSCODE_MODE':'matchgop'}):
12
- self.assertEqual(ManagedYouTube.forced_mode('@MUHAJER_IRAQI'),'fixed720')
13
- self.assertEqual(ManagedYouTube.forced_mode('other'),'matchgop')
14
- self.assertEqual(ManagedYouTube.forced_mode('unlisted'),'')
15
- def test_invalid_configuration_retains_legacy_policy(self):
16
- for value in ('bad', '[]', '{"other":"copy"}'):
17
- with patch.dict(os.environ, {'YOUTUBE_ACCOUNT_SEND_MODES':value,
18
- 'YOUTUBE_FORCE_TRANSCODE_USERS':'other','YOUTUBE_FORCE_TRANSCODE_MODE':'matchgop'}):
19
- self.assertEqual(ManagedYouTube.forced_mode('other'),'matchgop')
20
- def test_fixed720_ignores_old_sd_canvas_for_all_sources(self):
21
- with patch.dict(os.environ, {'VIDEO_BITRATE':'','YOUTUBE_X264_PRESET':'ultrafast'}):
22
- s=Sender('/bin/true',lambda *a:None)
23
- s.sticky_mode='fixed720';s._no_copy_recovery=True
24
- s._gop_w,s._gop_h=360,640
25
- for w,h in ((360,640),(720,1280),(1080,1920)):
26
- c={'width':w,'height':h,'codec':'h264','media':'flv'}
27
- cmd=s.command('/tmp/test.flv',c)
28
- self.assertIn('scale=w=720:h=1280',cmd[cmd.index('-vf')+1])
29
- self.assertEqual(cmd[cmd.index('-b:v')+1],'4000k')
30
- self.assertEqual(s.snapshot()['output_resolution'],'720x1280')
31
- def test_unlisted_matchgop_still_keeps_canvas(self):
32
- s=Sender('/bin/true',lambda *a:None)
33
- s.lock_gold({'width':360,'height':640})
34
- self.assertEqual(s.lock_gold({'width':1080,'height':1920}),(360,640))
35
-
36
- if __name__=='__main__':unittest.main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
verify.sh CHANGED
@@ -17,7 +17,6 @@ assert '/api/download' not in (root/'app.py').read_text()
17
  print('PASS no recorder / no video download API')
18
  PY
19
 
20
- python3 test_quality_profile.py
21
  python3 test_concurrency.py "$ROOT"
22
  python3 test_channel_switch.py "$ROOT"
23
  if command -v node >/dev/null 2>&1; then
 
17
  print('PASS no recorder / no video download API')
18
  PY
19
 
 
20
  python3 test_concurrency.py "$ROOT"
21
  python3 test_channel_switch.py "$ROOT"
22
  if command -v node >/dev/null 2>&1; then
youtube_manager.py CHANGED
@@ -2,7 +2,7 @@
2
  Each event carries the name/title/hashtags of the TikTok account it relays, and each
3
  account gets its own reusable ingestion stream so concurrent events never mix.
4
  Fail closed on ambiguous remote creates."""
5
- import os, re, subprocess, threading, time, uuid, json
6
  from datetime import datetime, timezone, timedelta
7
  from zoneinfo import ZoneInfo
8
  from urllib.parse import urlparse, urlunparse, quote
@@ -142,18 +142,6 @@ class ManagedYouTube:
142
  @staticmethod
143
  def forced_mode(user):
144
  """Optional per-account reliability mode; never forces every relay to burn CPU."""
145
- # Per-account overrides precede the legacy group mode. This lets one SD-pinned
146
- # event use a fixed HD canvas without changing other running broadcasters.
147
- name=str(user or '').strip().lower().lstrip('@')
148
- try:
149
- profiles=json.loads(os.getenv('YOUTUBE_ACCOUNT_SEND_MODES','{}'))
150
- if isinstance(profiles,dict):
151
- for account,mode in profiles.items():
152
- if str(account).strip().lower().lstrip('@') == name:
153
- if mode in ('matchgop','fixed720','fixed1080'):
154
- return mode
155
- except (ValueError,TypeError):
156
- pass # Invalid optional config must not disable the legacy safety policy.
157
  users={x.strip().lower().lstrip('@') for x in
158
  os.getenv('YOUTUBE_FORCE_TRANSCODE_USERS','').split(',') if x.strip()}
159
  if str(user or '').lower().lstrip('@') not in users:
@@ -430,8 +418,6 @@ class ManagedYouTube:
430
  'status':s.get('status',''),'source':s.get('source',''),'output_seconds':s.get('output_seconds',0),
431
  'send_mode':s.get('send_mode') or ev.get('sender_mode',''),
432
  'pending_send_mode':s.get('pending_send_mode') or ev.get('sender_pending_mode',''),
433
- 'output_resolution':s.get('output_resolution') or '',
434
- 'output_video_bitrate':s.get('output_video_bitrate') or '',
435
  # عمر آخر بايت من المصدر + حدّ الحارس: اللوحة تُظهر «المصدر صامت 12 ث» بدل أن
436
  # يظن المشاهد أن البث ميت بينما الجلسة تعمل (ما حدث في تجمّد @viggoo313).
437
  # صمت المصدر لحظة آخر قفل: يشرح في اللوحة سبب 187 (جوع تغذية لا رفض مستقبِل).
 
2
  Each event carries the name/title/hashtags of the TikTok account it relays, and each
3
  account gets its own reusable ingestion stream so concurrent events never mix.
4
  Fail closed on ambiguous remote creates."""
5
+ import os, re, subprocess, threading, time, uuid
6
  from datetime import datetime, timezone, timedelta
7
  from zoneinfo import ZoneInfo
8
  from urllib.parse import urlparse, urlunparse, quote
 
142
  @staticmethod
143
  def forced_mode(user):
144
  """Optional per-account reliability mode; never forces every relay to burn CPU."""
 
 
 
 
 
 
 
 
 
 
 
 
145
  users={x.strip().lower().lstrip('@') for x in
146
  os.getenv('YOUTUBE_FORCE_TRANSCODE_USERS','').split(',') if x.strip()}
147
  if str(user or '').lower().lstrip('@') not in users:
 
418
  'status':s.get('status',''),'source':s.get('source',''),'output_seconds':s.get('output_seconds',0),
419
  'send_mode':s.get('send_mode') or ev.get('sender_mode',''),
420
  'pending_send_mode':s.get('pending_send_mode') or ev.get('sender_pending_mode',''),
 
 
421
  # عمر آخر بايت من المصدر + حدّ الحارس: اللوحة تُظهر «المصدر صامت 12 ث» بدل أن
422
  # يظن المشاهد أن البث ميت بينما الجلسة تعمل (ما حدث في تجمّد @viggoo313).
423
  # صمت المصدر لحظة آخر قفل: يشرح في اللوحة سبب 187 (جوع تغذية لا رفض مستقبِل).