Arena.ai Agent commited on
Commit
7ea6aba
·
1 Parent(s): c343d8e

v2.45: port dual ingest and probed canvas fixes

Browse files
Files changed (8) hide show
  1. README.md +30 -0
  2. ingest.py +1 -1
  3. relay.py +18 -3
  4. test_ingest_routes.py +16 -0
  5. test_smart_mode.py +5 -3
  6. test_source_probe.py +142 -0
  7. verify.sh +1 -0
  8. youtube_manager.py +54 -8
README.md CHANGED
@@ -10,6 +10,36 @@ app_port: 7860
10
 
11
  # Reoo88/On — relay-v2.29-smart-mode
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ## تنظيف تلقائي عند غياب البث في v2.27
14
 
15
  عند غياب أي بث بشكل كامل ومستمر لمدة `YOUTUBE_IDLE_RESET_SECONDS` (الافتراضي
 
10
 
11
  # Reoo88/On — relay-v2.29-smart-mode
12
 
13
+ ## تثبيت الـcanvas على مقاس FLV الحقيقي في v2.45
14
+
15
+ أظهر القياس الدائم أن وصف مرشح TikTok قد يبقى على مقاس أقدم بينما ترويسة
16
+ الفيديو الفعلية مختلفة: مثلًا `432x864` في الوصف مقابل `640x1280` في
17
+ `STREAM_CONFIG`، و`540x960` مقابل `720x1280`. كان `matchgop` يثبّت الـcanvas
18
+ من الوصف قبل أن يصل المقاس الحقيقي، فيصغر الخرج من أول البث ويبقى ثابتًا.
19
+
20
+ أصبح فحص المصدر الموجود قبل الإرسال يقرأ أول سجل AVC/HEVC ضمن بادئة محدودة
21
+ (افتراضيًا 256 KiB و4 ثوانٍ، `YOUTUBE_FLV_PROBE_BYTES/SECONDS`). إذا احتوى
22
+ مقاسًا صالحًا، تُحفظ `probed_width/probed_height` بجانب المرشح ويستخدمها
23
+ `lock_gold` بدل الوصف. لا يتغير اسم الجودة أو تثبيت الـrendition. وإذا لم تظهر
24
+ ترويسة مقاس ضمن الحد، يبقى قبول توقيع FLV والسلوك السابق بلا تغيير.
25
+
26
+ هذا تصحيح لاختيار الـcanvas فقط؛ لا يغيّر `copy` أو `tee/global_header` أو
27
+ القص ومعدل البت، ولا يغيّر مهلة بدء YouTube أو الاسترداد. يغطي
28
+ `test_source_probe.py` التجزئة، والترويسة غير الصالحة أصغر من 32px، وعدم
29
+ إعادة كتابة وصف المرشح، وثبات المسار القديم عند غياب المقاس.
30
+
31
+ ## توحيد ترويسة H.264 مع الاستقبال المزدوج في v2.44
32
+
33
+ عند إعادة الترميز والنشر المتزامن إلى الاستقبالين الأساسي والاحتياطي عبر FFmpeg
34
+ `tee`، يضيف المرسِل الآن `-flags:v +global_header`. بذلك يقرأ كل مستقبل سجل
35
+ SPS/PPS نفسه منذ البداية بدل أن يلتقطا إعدادَي عرض مختلفين. الدليل الإنتاجي كان
36
+ `resolutionMismatch [error] Bad secondary stream` مع تغيّر حجم الصورة المعروضة،
37
+ وهو نمط يطابق حالة FFmpeg الموثقة ‎#11527 لواجهة `tee` مع يوتيوب.
38
+
39
+ لا يغيّر الإصلاح المقاس أو فلاتر القص أو معدل البت، ولا يضيف الترويسة إلى مسار
40
+ `copy` الذي يحمل سجل المفكك الأصلي أصلًا. ويبقى النشر المفرد بلا تغيير. يغطي
41
+ `test_ingest_routes.py` وجود الترويسة مع الترميز المزدوج وعدم إضافتها إلى النسخ.
42
+
43
  ## تنظيف تلقائي عند غياب البث في v2.27
44
 
45
  عند غياب أي بث بشكل كامل ومستمر لمدة `YOUTUBE_IDLE_RESET_SECONDS` (الافتراضي
ingest.py CHANGED
@@ -6,7 +6,7 @@ import os, threading, time
6
  from datetime import datetime
7
  from zoneinfo import ZoneInfo
8
 
9
- APP_VERSION = 'relay-v2.43-canvas-lock'
10
 
11
 
12
  def env_rtmp_destination():
 
6
  from datetime import datetime
7
  from zoneinfo import ZoneInfo
8
 
9
+ APP_VERSION = 'relay-v2.45-probed-canvas'
10
 
11
 
12
  def env_rtmp_destination():
relay.py CHANGED
@@ -469,8 +469,15 @@ class Sender:
469
  return (self._gop_w, self._gop_h)
470
  if candidate and not (w and h):
471
  try:
472
- w = int(candidate.get('width') or 0)
473
- h = int(candidate.get('height') or 0)
 
 
 
 
 
 
 
474
  except (TypeError, ValueError):
475
  w = h = 0
476
  try:
@@ -710,6 +717,12 @@ class Sender:
710
  # يعني تسليمًا فعليًا، بلا مخزن يكذب على الحارس.
711
  legs=self._dual_legs(destination,extra_destinations)
712
  if legs:
 
 
 
 
 
 
713
  output=['-f','tee',self._tee_spec(legs,enhanced)]
714
  else:
715
  output=['-flvflags','no_duration_filesize','-f','flv']
@@ -1208,7 +1221,9 @@ class Sender:
1208
  tele_meta={'candidate_label':candidate.get('label'),'candidate_brief':candidate.get('brief'),
1209
  'media':candidate.get('media'),'codec':candidate.get('codec'),
1210
  'quality_name':candidate.get('quality_name'),'width':candidate.get('width'),
1211
- 'height':candidate.get('height'),'declared_bitrate_kbps':candidate.get('bitrate')}
 
 
1212
  if isinstance(self.telemetry_context,dict):
1213
  tele_meta.update(self.telemetry_context)
1214
  self.telemetry_key=telemetry_start(user,tele_meta)
 
469
  return (self._gop_w, self._gop_h)
470
  if candidate and not (w and h):
471
  try:
472
+ # _select_source attaches dimensions parsed from the real FLV decoder record.
473
+ # Prefer them over TikTok's rendition label, which can remain at an earlier
474
+ # SD size while the served picture is already HD. Explicit w/h still wins.
475
+ pw,ph=int(candidate.get('probed_width') or 0),int(candidate.get('probed_height') or 0)
476
+ if pw>=32 and ph>=32:
477
+ w,h=pw,ph
478
+ else:
479
+ w = int(candidate.get('width') or 0)
480
+ h = int(candidate.get('height') or 0)
481
  except (TypeError, ValueError):
482
  w = h = 0
483
  try:
 
717
  # يعني تسليمًا فعليًا، بلا مخزن يكذب على الحارس.
718
  legs=self._dual_legs(destination,extra_destinations)
719
  if legs:
720
+ # Each tee leg opens its own FLV muxer. With x264, global extradata
721
+ # makes both YouTube ingests receive the same SPS/PPS from the start;
722
+ # without it YouTube can report resolutionMismatch / Bad secondary
723
+ # stream and change the displayed aspect ratio (FFmpeg ticket #11527).
724
+ # Copy already carries the bridge's decoder record and is unchanged.
725
+ if mode!='copy':video+=['-flags:v','+global_header']
726
  output=['-f','tee',self._tee_spec(legs,enhanced)]
727
  else:
728
  output=['-flvflags','no_duration_filesize','-f','flv']
 
1221
  tele_meta={'candidate_label':candidate.get('label'),'candidate_brief':candidate.get('brief'),
1222
  'media':candidate.get('media'),'codec':candidate.get('codec'),
1223
  'quality_name':candidate.get('quality_name'),'width':candidate.get('width'),
1224
+ 'height':candidate.get('height'),'probed_width':candidate.get('probed_width'),
1225
+ 'probed_height':candidate.get('probed_height'),
1226
+ 'declared_bitrate_kbps':candidate.get('bitrate')}
1227
  if isinstance(self.telemetry_context,dict):
1228
  tele_meta.update(self.telemetry_context)
1229
  self.telemetry_key=telemetry_start(user,tele_meta)
test_ingest_routes.py CHANGED
@@ -229,6 +229,22 @@ class DualIngestTests(unittest.TestCase):
229
  args=s.command(self.primary,{'codec':'h265'},extra_destinations=[self.backup])
230
  self.assertEqual(' '.join(args).count('rtmp_enhanced_codecs=hvc1'),2)
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  def test_command_single_destination_is_unchanged(self):
233
  s=Sender('/bin/true',lambda *a:None);s.mode_override='copy'
234
  args=s.command(self.primary,{'codec':'h264'})
 
229
  args=s.command(self.primary,{'codec':'h265'},extra_destinations=[self.backup])
230
  self.assertEqual(' '.join(args).count('rtmp_enhanced_codecs=hvc1'),2)
231
 
232
+ def test_transcoded_tee_uses_global_video_header_on_both_flv_legs(self):
233
+ # Production evidence: YouTube resolutionMismatch / Bad secondary stream
234
+ # while one x264 sender fed primary+backup through tee. FFmpeg #11527
235
+ # documents the matching aspect-ratio symptom and the global-header fix.
236
+ s=Sender('/bin/true',lambda *a:None);s.mode_override='matchgop'
237
+ args=s.command(self.primary,{'codec':'h264','width':540,'height':960},
238
+ extra_destinations=[self.backup])
239
+ self.assertIn('tee',args)
240
+ self.assertEqual(args[args.index('-flags:v')+1],'+global_header')
241
+
242
+ def test_copy_tee_does_not_add_encoder_only_global_header_flag(self):
243
+ s=Sender('/bin/true',lambda *a:None);s.mode_override='copy'
244
+ args=s.command(self.primary,{'codec':'h264'},extra_destinations=[self.backup])
245
+ self.assertIn('tee',args)
246
+ self.assertNotIn('-flags:v',args)
247
+
248
  def test_command_single_destination_is_unchanged(self):
249
  s=Sender('/bin/true',lambda *a:None);s.mode_override='copy'
250
  args=s.command(self.primary,{'codec':'h264'})
test_smart_mode.py CHANGED
@@ -72,13 +72,15 @@ class CopyRecoveryTests(unittest.TestCase):
72
 
73
  class CanvasCapTests(unittest.TestCase):
74
  def test_matchgop_canvas_is_capped_at_720_class(self):
75
- # 960x1920 must fit inside the 720x1280 box (aspect preserved -> the
76
- # canvas is 640x1280), never re-encode at the full 1920 height.
 
77
  sender = Sender('/bin/true', lambda *a: None)
78
  sender.mode_override = 'matchgop'
79
  args = sender.command('out.flv', {'codec': 'h265', 'width': 960, 'height': 1920})
80
  vf = args[args.index('-vf') + 1]
81
- self.assertIn('pad=640:1280', vf)
 
82
  self.assertNotIn('1920', vf)
83
 
84
  def test_safe_size_mapping_still_applies(self):
 
72
 
73
  class CanvasCapTests(unittest.TestCase):
74
  def test_matchgop_canvas_is_capped_at_720_class(self):
75
+ # 960x1920 is capped to a 640x1280 canvas, never re-encoded at the
76
+ # full 1920 height. Since v2.43 this tall 1:2 picture fills that canvas
77
+ # and center-crops the excess height instead of adding pillarbox bars.
78
  sender = Sender('/bin/true', lambda *a: None)
79
  sender.mode_override = 'matchgop'
80
  args = sender.command('out.flv', {'codec': 'h265', 'width': 960, 'height': 1920})
81
  vf = args[args.index('-vf') + 1]
82
+ self.assertIn('crop=640:1280', vf)
83
+ self.assertNotIn('pad=640:1280', vf)
84
  self.assertNotIn('1920', vf)
85
 
86
  def test_safe_size_mapping_still_applies(self):
test_source_probe.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Deterministic FLV pre-start metadata tests; no network or FFmpeg required."""
3
+ import os
4
+ import pathlib
5
+ import sys
6
+ import tempfile
7
+
8
+ ROOT=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else pathlib.Path(__file__).parent).resolve()
9
+ sys.path.insert(0,str(ROOT))
10
+ os.environ.setdefault('PASSWORD','test-pass-not-for-prod')
11
+ os.environ.setdefault('DATA_DIR',tempfile.mkdtemp(prefix='source-probe-test-'))
12
+ os.environ['YOUTUBE_FLV_PROBE_SECONDS']='0.2'
13
+
14
+ import youtube_manager as manager_module # noqa: E402
15
+ from relay import Sender # noqa: E402
16
+ from stream_monitor import video_dimensions # noqa: E402
17
+
18
+ passed=failed=0
19
+ def check(name,ok,detail=''):
20
+ global passed,failed
21
+ if ok:
22
+ passed+=1;print('PASS ',name)
23
+ else:
24
+ failed+=1;print('FAIL ',name,detail)
25
+
26
+
27
+ class Bits:
28
+ def __init__(self):self.data=[]
29
+ def bit(self,value):self.data.append(1 if value else 0)
30
+ def bits(self,value,count):
31
+ for shift in range(count-1,-1,-1):self.bit((value>>shift)&1)
32
+ def ue(self,value):
33
+ code=value+1;size=code.bit_length()
34
+ self.bits(0,size-1);self.bits(code,size)
35
+ def rbsp(self):
36
+ self.bit(1)
37
+ while len(self.data)%8:self.bit(0)
38
+ raw=bytes(sum(self.data[i+j]<<(7-j) for j in range(8)) for i in range(0,len(self.data),8))
39
+ out=bytearray();zeros=0
40
+ for byte in raw:
41
+ if zeros>=2 and byte<=3:
42
+ out.append(3);zeros=0
43
+ out.append(byte);zeros=zeros+1 if byte==0 else 0
44
+ return bytes(out)
45
+
46
+
47
+ def avcc(width,height):
48
+ """Minimal baseline H.264 SPS/PPS with uncropped, frame-only 4:2:0 dimensions."""
49
+ assert width%16==0 and height%16==0
50
+ b=Bits();b.bits(66,8);b.bits(0,8);b.bits(30,8) # profile/constraints/level
51
+ b.ue(0);b.ue(0);b.ue(0);b.ue(0);b.ue(1);b.bit(0)
52
+ b.ue(width//16-1);b.ue(height//16-1)
53
+ b.bit(1);b.bit(1);b.bit(0);b.bit(0)
54
+ sps=b'\x67'+b.rbsp();pps=bytes.fromhex('68ce0fc8')
55
+ return (bytes([1,66,0,30,0xff,0xe1])+len(sps).to_bytes(2,'big')+sps+
56
+ b'\x01'+len(pps).to_bytes(2,'big')+pps)
57
+
58
+
59
+ def tag(config,ts=0):
60
+ body=b'\x17\x00\x00\x00\x00'+config;size=len(body)
61
+ header=(b'\x09'+size.to_bytes(3,'big')+bytes([
62
+ (ts>>16)&255,(ts>>8)&255,ts&255,(ts>>24)&255,0,0,0]))
63
+ return header+body+(11+size).to_bytes(4,'big')
64
+
65
+
66
+ def flv(*tags):
67
+ return b'FLV\x01\x05\x00\x00\x00\x09\x00\x00\x00\x00'+b''.join(tags)
68
+
69
+
70
+ class Response:
71
+ def __init__(self,chunks):self.chunks=list(chunks);self.closed=False
72
+ def raise_for_status(self):return None
73
+ def iter_content(self):yield from self.chunks
74
+ def close(self):self.closed=True
75
+
76
+
77
+ def manager():
78
+ obj=manager_module.ManagedYouTube.__new__(manager_module.ManagedYouTube)
79
+ obj.headers_for=lambda user:{}
80
+ obj._unstable={};obj.starve={};obj._skip_probe_once={};obj._probe_note=''
81
+ obj.logs=[];obj.log=lambda user,text:obj.logs.append(str(text))
82
+ return obj
83
+
84
+
85
+ def candidate(width=432,height=864):
86
+ return {'url':'https://media.example.test/live.flv','media':'flv','codec':'h264',
87
+ 'quality_name':'hd','label':'hd','brief':'hd H264','width':width,'height':height}
88
+
89
+
90
+ def probe_with(chunks,cand=None):
91
+ obj=manager();responses=[]
92
+ original=manager_module.requests.get
93
+ def fake_get(*args,**kwargs):
94
+ response=Response(chunks);responses.append(response);return response
95
+ manager_module.requests.get=fake_get
96
+ try:result=obj.probe('alice',cand or candidate())
97
+ finally:manager_module.requests.get=original
98
+ return result,responses
99
+
100
+
101
+ small=avcc(16,16);real=avcc(640,1280)
102
+ check('test SPS generator carries exact dimensions',
103
+ video_dimensions('avc1',small)==(16,16) and video_dimensions('avc1',real)==(640,1280))
104
+
105
+ blob=flv(tag(real));chunks=[blob[i:i+7] for i in range(0,len(blob),7)]
106
+ result,responses=probe_with(chunks)
107
+ check('fragmented FLV probe returns actual decoder dimensions',
108
+ isinstance(result,dict) and (result.get('width'),result.get('height'))==(640,1280),str(result))
109
+ check('FLV probe closes the source response',bool(responses) and responses[0].closed)
110
+
111
+ blob=flv(tag(small),tag(real));split=13+len(tag(small))
112
+ result,_=probe_with([blob[:split],blob[split:]])
113
+ check('sub-32px startup config is not accepted as the final canvas',
114
+ isinstance(result,dict) and (result.get('width'),result.get('height'))==(640,1280),str(result))
115
+
116
+ result,responses=probe_with([b'FLV'])
117
+ check('signature-only FLV keeps the old truthy fallback',result is True,str(result))
118
+ check('signature-only fallback also closes the response',bool(responses) and responses[0].closed)
119
+ result,_=probe_with([b'NOT AN FLV'])
120
+ check('invalid FLV signature is still rejected',result is False,str(result))
121
+
122
+ obj=manager();obj.probe=lambda user,c:{'width':640,'height':1280,'codec':'avc1'}
123
+ selected=obj._select_source('alice',{'sources':[candidate()]})
124
+ check('source selection retains a usable candidate',len(selected)==1,str(selected))
125
+ if selected:
126
+ chosen=selected[0]
127
+ check('actual dimensions are attached without rewriting the rendition label',
128
+ (chosen.get('width'),chosen.get('height'))==(432,864) and
129
+ (chosen.get('probed_width'),chosen.get('probed_height'))==(640,1280) and
130
+ obj._source_pin(chosen)==obj._source_pin(candidate()),str(chosen))
131
+ sender=Sender('ffmpeg',lambda *args:None)
132
+ check('matchgop locks the actual HD canvas instead of the stale SD label',
133
+ sender.lock_gold(chosen)==(720,1280),str(sender.lock_gold(chosen)))
134
+ check('a declared/probed mismatch is surfaced in operational logs',
135
+ any('640x1280' in line and '432x864' in line for line in obj.logs),str(obj.logs))
136
+
137
+ fallback=Sender('ffmpeg',lambda *args:None)
138
+ check('without decoder metadata the old declared-canvas fallback is unchanged',
139
+ fallback.lock_gold(candidate())==(432,864),str(fallback.lock_gold(candidate())))
140
+
141
+ print('source probe tests: %d passed, %d failed'%(passed,failed))
142
+ raise SystemExit(1 if failed else 0)
verify.sh CHANGED
@@ -30,6 +30,7 @@ python3 test_source_reconnect.py "$ROOT"
30
  python3 test_ingest_routes.py "$ROOT"
31
  python3 test_transition_ingest.py "$ROOT"
32
  python3 test_flv_bridge.py "$ROOT"
 
33
  python3 test_adaptive_format.py "$ROOT"
34
  python3 test_completion.py "$ROOT"
35
  python3 test_telemetry.py "$ROOT"
 
30
  python3 test_ingest_routes.py "$ROOT"
31
  python3 test_transition_ingest.py "$ROOT"
32
  python3 test_flv_bridge.py "$ROOT"
33
+ python3 test_source_probe.py "$ROOT"
34
  python3 test_adaptive_format.py "$ROOT"
35
  python3 test_completion.py "$ROOT"
36
  python3 test_telemetry.py "$ROOT"
youtube_manager.py CHANGED
@@ -8,6 +8,7 @@ from zoneinfo import ZoneInfo
8
  from urllib.parse import urlparse, urlunparse, quote
9
  from curl_cffi import requests
10
  from relay import RelayStartError, Sender, SourceStalled, source_http_headers
 
11
  from stream_monitor import log_event
12
  from telemetry import record_event as telemetry_event
13
  import inspect
@@ -1552,11 +1553,16 @@ class ManagedYouTube:
1552
  def _source_magic(media):
1553
  return b'#EXTM3U' if media=='hls' else b'FLV'
1554
  def probe(self,user,candidate):
1555
- """Check the payload's first bytes before creating any public scheduled event.
1556
 
1557
- An FLV rendition must start with the 'FLV' signature, an HLS one with '#EXTM3U'
1558
- (TikTok's playlists are plain media playlists). Trusting the URL suffix — or the
1559
- caller's label — is exactly what made every .m3u8 source look broken.
 
 
 
 
 
1560
  """
1561
  media=(candidate or {}).get('media') or 'flv'
1562
  want=self._source_magic(media);got=bytearray()
@@ -1564,10 +1570,36 @@ class ManagedYouTube:
1564
  headers=source_http_headers(candidate.get('url'),user,self.headers_for),stream=True,timeout=20)
1565
  try:
1566
  r.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1567
  for chunk in r.iter_content():
1568
- got+=chunk
1569
- if len(got)>=len(want):return bytes(got[:len(want)])==want or (media=='hls' and bytes(got).startswith(b'#EXTM3U'))
1570
- return False
 
 
 
 
 
 
 
 
 
 
 
 
1571
  finally:r.close()
1572
  def codec_probe(self,candidate):
1573
  """ffprobe the real payload; TikTok's labels describe the block, not what is served now.
@@ -1742,7 +1774,21 @@ class ManagedYouTube:
1742
  for c in ordered:
1743
  tagged={**c,'user':user}
1744
  try:
1745
- if self.probe(user,c):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1746
  usable.append(tagged)
1747
  except Exception as e:
1748
  self._probe_note=self.safe_error(e)
 
8
  from urllib.parse import urlparse, urlunparse, quote
9
  from curl_cffi import requests
10
  from relay import RelayStartError, Sender, SourceStalled, source_http_headers
11
+ from flv_bridge import FLVBridge
12
  from stream_monitor import log_event
13
  from telemetry import record_event as telemetry_event
14
  import inspect
 
1553
  def _source_magic(media):
1554
  return b'#EXTM3U' if media=='hls' else b'FLV'
1555
  def probe(self,user,candidate):
1556
+ """Validate the payload and, for FLV, read its real decoder canvas when readily available.
1557
 
1558
+ An FLV rendition must start with the 'FLV' signature, an HLS one with '#EXTM3U'.
1559
+ TikTok's rendition label can describe a different size from the AVC/HEVC configuration
1560
+ actually served. The existing pre-start HTTP probe already opens the source, so parse its
1561
+ first decoder record here instead of locking ``matchgop`` to that unreliable label.
1562
+
1563
+ The return remains truthy/falsy for existing callers. A mapping with ``width``, ``height``
1564
+ and ``codec`` is returned when a real (>=32px) configuration arrives within the bounded
1565
+ prefix; plain True preserves the old signature-only behavior when it does not.
1566
  """
1567
  media=(candidate or {}).get('media') or 'flv'
1568
  want=self._source_magic(media);got=bytearray()
 
1570
  headers=source_http_headers(candidate.get('url'),user,self.headers_for),stream=True,timeout=20)
1571
  try:
1572
  r.raise_for_status()
1573
+ if media=='hls':
1574
+ for chunk in r.iter_content():
1575
+ got+=chunk
1576
+ if len(got)>=len(want):return bytes(got).startswith(b'#EXTM3U')
1577
+ return False
1578
+
1579
+ try:max_bytes=max(4096,int(os.getenv('YOUTUBE_FLV_PROBE_BYTES','262144')))
1580
+ except (TypeError,ValueError):max_bytes=262144
1581
+ try:probe_seconds=max(0.2,float(os.getenv('YOUTUBE_FLV_PROBE_SECONDS','4')))
1582
+ except (TypeError,ValueError):probe_seconds=4.0
1583
+ # Observational parser only: probing must not duplicate STREAM_CONFIG or bitrate
1584
+ # events in the persistent operational log. The real sender has its own bridge.
1585
+ bridge=FLVBridge(user=user,event_logger=lambda *args:None)
1586
+ deadline=time.monotonic()+probe_seconds;seen=0
1587
  for chunk in r.iter_content():
1588
+ if not chunk:continue
1589
+ seen+=len(chunk)
1590
+ if len(got)<len(want):got+=chunk[:len(want)-len(got)]
1591
+ if len(got)>=len(want) and bytes(got[:len(want)])!=want:return False
1592
+ try:bridge.feed(chunk)
1593
+ except (ValueError,IndexError):
1594
+ # A valid signature with a truncated prefix retains the old fallback: the
1595
+ # sender will try the candidate and its watchdog will classify any failure.
1596
+ return bytes(got[:len(want)])==want
1597
+ size=bridge.video_size
1598
+ if size and min(size)>=32:
1599
+ return {'width':int(size[0]),'height':int(size[1]),
1600
+ 'codec':str(bridge.video_codec or '')}
1601
+ if seen>=max_bytes or time.monotonic()>=deadline:break
1602
+ return len(got)>=len(want) and bytes(got[:len(want)])==want
1603
  finally:r.close()
1604
  def codec_probe(self,candidate):
1605
  """ffprobe the real payload; TikTok's labels describe the block, not what is served now.
 
1774
  for c in ordered:
1775
  tagged={**c,'user':user}
1776
  try:
1777
+ probed=self.probe(user,c)
1778
+ if probed:
1779
+ if isinstance(probed,dict):
1780
+ try:w,h=int(probed.get('width') or 0),int(probed.get('height') or 0)
1781
+ except (TypeError,ValueError):w=h=0
1782
+ if w>=32 and h>=32:
1783
+ # Keep the rendition label/pin unchanged; these fields describe the
1784
+ # actual decoder record and are used only for the constant canvas.
1785
+ tagged['probed_width'],tagged['probed_height']=w,h
1786
+ tagged['probed_codec']=str(probed.get('codec') or '')
1787
+ try:declared=(int(c.get('width') or 0),int(c.get('height') or 0))
1788
+ except (TypeError,ValueError):declared=(0,0)
1789
+ if declared!=(w,h):
1790
+ self.log(user,'[SOURCE] FLV config %sx%s يخالف وصف المرشح %sx%s؛ سيُثبَّت الخرج على المقاس الحقيقي'%(
1791
+ w,h,declared[0],declared[1]))
1792
  usable.append(tagged)
1793
  except Exception as e:
1794
  self._probe_note=self.safe_error(e)