dragonflight/services/capture/dur-patch.py
Zac Gaetano e3be8745d3 fix(capture): restore frame counting + growing MXF for Premiere
The previous sed/python in-place edits on the node broke capture: the
hires stderr parser was written with literal 0x08 BACKSPACE bytes instead
of regex word boundaries, so it never matched ffmpeg output.
framesReceived stayed 0, the shutdown handler saw "no frames" and marked
every asset as an error even though video was captured. The ffmpeg base
args had also been changed to -progress pipe:2, whose key=value output
puts frame= and fps= on separate lines and does not match a combined
regex.

Fixes:
- Parser: single robust regex matching ffmpeg's classic -stats line
  (frame= and fps= together). No backspace bytes, no word boundaries.
- ffmpeg base args back to -stats (drop -progress pipe:2).

Growing-file (Premiere edit-while-record), per bmx thread 87ac5750 and
Drastic/Softron edit-while-ingest docs:
- raw2bmx clip type op1a -> rdd9 (Sony XDCAM / RDD-9, the flavour Premiere
  reads while growing) with --index-follows so the IndexTableSegment is
  written in the same partition as the essence it indexes (lets a reader
  re-scanning body partitions seek toward the record head). NOT --avid-gf
  (Avid OP-Atom, Media-Composer-only, needs a companion AAF).
- dur-patch.py: overwrite header Duration=-1 to 0 immediately at
  clip-open (Premiere rejects -1 on import), then track the live frame
  count every 3s from the last body partition IndexTableSegment. Shipped
  as services/capture/dur-patch.py (/app/dur-patch.py in the image).

Deployed to wild-dragon-capture:latest on zampp2 via overlay build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:00:23 -04:00

67 lines
2.3 KiB
Python

import sys,time,struct,os
BODY=bytes([6,14,43,52,2,5,1,1,13,1,2,1,1,3])
ITS=bytes([6,14,43,52,2,83,1,1,13,1,2,1,1,16,1,0])
def get_frames(p):
try:
sz=os.path.getsize(p)
if sz<50000:return 0
with open(p,"rb") as f:
f.seek(max(0,sz-3000000));d=f.read()
pos=len(d)
while True:
i=d.rfind(BODY,0,pos)
if i<0:break
pos=i;bs=i+16;b0=d[bs]
if b0==0x83:bl,bv=4,struct.unpack(">I",b"\x00"+d[bs+1:bs+4])[0]
elif b0==0x82:bl,bv=3,struct.unpack(">H",d[bs+1:bs+3])[0]
else:bl,bv=1,b0
if len(d)<bs+bl+48:break
ibc=struct.unpack(">Q",d[bs+bl+40:bs+bl+48])[0]
if not ibc:continue
itd=d[i+16+bl+bv:i+16+bl+bv+ibc]
ii=itd.find(ITS)
if ii<0:break
ib0=itd[ii+16]
if ib0==0x83:ivl,ivv=4,struct.unpack(">I",b"\x00"+itd[ii+17:ii+20])[0]
elif ib0==0x82:ivl,ivv=3,struct.unpack(">H",itd[ii+17:ii+19])[0]
else:ivl,ivv=1,ib0
sd=itd[ii+16+ivl:ii+16+ivl+ivv];p2=isp=dur=0
while p2<len(sd)-3:
t=struct.unpack(">H",sd[p2:p2+2])[0];l=struct.unpack(">H",sd[p2+2:p2+4])[0]
if p2+4+l>len(sd):break
v=sd[p2+4:p2+4+l]
if t==0x3F0C and l==8:isp=struct.unpack(">q",v)[0]
elif t==0x3F0D and l==8:dur=struct.unpack(">q",v)[0]
p2+=4+l
return max(0,isp+dur)
except:return 0
return 0
mxf=sys.argv[1]
while not os.path.exists(mxf) or os.path.getsize(mxf)<20000:time.sleep(0.3)
with open(mxf,"rb") as f:hdr=f.read(200000)
offs=[]
for pat in [b"\x02\x02\x00\x08\xff\xff\xff\xff\xff\xff\xff\xff",b"\x30\x02\x00\x08\xff\xff\xff\xff\xff\xff\xff\xff"]:
p=0
while True:
i=hdr.find(pat,p)
if i<0:break
offs.append(i+4);p=i+1
print("[dur-patch] %d Duration fields"%len(offs),flush=True)
def patch(val):
if not offs:return
try:
with open(mxf,"r+b") as f:
for o in offs:f.seek(o);f.write(struct.pack(">q",val))
except Exception as e:print("[dur-patch] err:",e,flush=True)
# Premiere rejects a growing OP1a/RDD9 whose header Duration is -1 on import
# (bmx thread 87ac5750: Premiere prefers 0). raw2bmx writes -1 at clip-open, so
# overwrite every -1 with 0 immediately, BEFORE any frames exist, then track the
# live frame count every 3s.
patch(0)
print("[dur-patch] Duration=0 (initial)",flush=True)
while True:
time.sleep(3)
fc=get_frames(mxf)
if fc>0:
patch(fc)
print("[dur-patch] Duration=%d"%fc,flush=True)