EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / AI / เร่งคลิปจอเป็นช่วง ๆ แต่ให้นาฬิกาบนภาพเดินตามเวลาต้นฉบับ
AI

เร่งคลิปจอเป็นช่วง ๆ แต่ให้นาฬิกาบนภาพเดินตามเวลาต้นฉบับ

โดย Ton-Or ·20 ต.ค. 2026·อ่าน 9 นาที
เร่งคลิปจอเป็นช่วง ๆ แต่ให้นาฬิกาบนภาพเดินตามเวลาต้นฉบับ

เร่งคลิปทั้งเส้นด้วยตัวคูณเดียวเป็นเรื่องง่าย แต่พอต้องเร่งช่วงรอแรงกว่าช่วงลงมือ แล้วยังต้องให้คนดูรู้ว่าของจริงใช้เวลาไปเท่าไร ลำดับของตัวกรองในสายท่อจะกลายเป็นเรื่องเป็นเรื่องตายทันที

พอดีผมอัดจอไว้ตอนนั่งทำงานชิ้นหนึ่งยาว 12 นาทีกว่า อยากตัดเป็นไทม์แลปส์ให้เหลือราวสี่นาที แต่ไม่อยากเร่งทั้งเส้นด้วยตัวคูณเดียว เพราะครึ่งหนึ่งของคลิปคือหน้าจอนิ่ง ๆ ระหว่างรอประมวลผล ถ้าเร่งเท่ากันหมด ช่วงที่มีอะไรให้ดูจริงจะวูบผ่านไปเร็วเกินจนตามไม่ทัน ส่วนช่วงที่จอนิ่งก็ยังกินเวลาอยู่ดี ที่สำคัญคือผมอยากให้มีนาฬิกาบนภาพบอกเวลาจริงของต้นฉบับด้วย คนดูจะได้รู้ว่าที่เห็นวูบ ๆ นี่ของจริงกินเวลาไปกี่นาที

ตอนลงมือผมเจอกับดักที่ไม่คาดคิดคือ นาฬิกาบนภาพเดินเร็วตามความเร็วที่เร่ง ทั้งที่ต้องการให้มันเดินตามเวลาต้นฉบับ แก้อยู่หลายรอบกว่าจะรู้ว่าเป็นเรื่องลำดับตัวกรองล้วน ๆ บทความนี้ผมเลยเขียนเฉพาะเรื่องสายท่อของ filtergraph

บทความนี้เป็นภาคเทคนิค ไม่ใช่ตอนเริ่มต้น

ถ้ายังไม่เคยแตะ ffmpeg มาก่อน ให้อ่าน ใช้ Claude ตัดวิดีโอด้วยการพิมพ์สั่งเป็นภาษาไทย ก่อน ตอนนั้นว่าด้วยการสั่งตัด ย่อ รวม ใส่เพลง และเปิดการเร่งด้วยการ์ดจอ ตอนนี้ไม่เล่าซ้ำเรื่องพวกนั้นเลย เริ่มจากจุดที่คลิปอยู่ในมือแล้วและต้องเขียน filtergraph เอง

สายท่อทั้งเส้นวัดความเคลื่อนไหวsignalstats->แบ่งช่วงนิ่ง / ทำงาน->คิดตัวคูณให้ได้ความยาวเป้า->สร้าง filtergraphsplit trim setpts->เรนเดอร์concat แล้วเข้ารหัส

วัดก่อนว่าช่วงไหนจอนิ่ง ช่วงไหนมีอะไรเกิดขึ้น

ผมไม่ใช้วิธีนั่งดูแล้วจดเวลาเอง เพราะคลิปยาวสิบสองนาทีมีช่วงสลับไปมาหลายสิบครั้ง วิธีที่เร็วกว่าคือให้ ffmpeg วัดให้ ใช้ตัวกรอง tblend แบบ difference เทียบเฟรมติดกัน แล้วส่งเข้า signalstats เพื่ออ่านค่าความสว่างเฉลี่ยของภาพผลต่าง ถ้าจอไม่ขยับ ภาพผลต่างจะดำสนิทและค่าจะเข้าใกล้ศูนย์

ผมสุ่มแค่ 2 เฟรมต่อวินาทีก็พอ เพราะเราต้องการรู้แค่ว่าช่วงไหนขยับ ไม่ได้ต้องการความละเอียดระดับเฟรม แล้วย่อภาพลงด้วยเพื่อให้วัดเร็วขึ้นเยอะ

ffmpeg -hide_banner -i screen.mp4 \
  -vf "fps=2,scale=480:-2,tblend=all_mode=difference,signalstats,metadata=print" \
  -an -f null - 2> motion.log

ล็อกที่ได้เป็นแบบนี้ ค่า YAVG คือตัวที่ผมเอาไปใช้ตัดสิน

frame:1     pts:1       pts_time:0.5
lavfi.signalstats.YAVG=0.121
frame:2     pts:2       pts_time:1
lavfi.signalstats.YAVG=0.098
frame:3     pts:3       pts_time:1.5
lavfi.signalstats.YAVG=4.317
frame:4     pts:4       pts_time:2
lavfi.signalstats.YAVG=6.882

เส้นแบ่งที่ผมใช้คือ 0.8 ต่ำกว่านี้ถือว่านิ่ง สูงกว่านี้ถือว่ามีอะไรเกิดขึ้น ค่านี้ไม่ใช่ค่ามาตรฐาน ต้องเปิดล็อกดูของคลิปตัวเองแล้วเลือกเอง คลิปที่มีเคอร์เซอร์กะพริบหรือมีนาฬิกาในระบบเดินอยู่มุมจอจะมีพื้นค่าสูงกว่าปกติเสมอ

รวมช่วงย่อยให้เป็นก้อนก่อน ไม่งั้นได้ร้อยกว่าท่อน

ถ้าเอาผลดิบมาตัดเลยจะได้ช่วงยิบย่อยเป็นร้อย เพราะระหว่างพิมพ์คนเราหยุดคิดเป็นวินาที ๆ อยู่แล้ว ผมเลยใส่สามขั้นก่อนคือ ขยายช่วงทำงานออกด้านละหนึ่งวินาทีเผื่อบริบท ยุบช่วงที่สั้นกว่าสามวินาทีเข้ากับเพื่อนบ้าน แล้วรวมช่วงชนิดเดียวกันที่ติดกันเข้าด้วยกัน

THRESH, DILATE_S, MIN_SEG_S, DT = 0.8, 1.0, 3.0, 0.5

active = [v > THRESH for v in vals]
d = int(round(DILATE_S / DT))
act2 = active[:]
for i, a in enumerate(active):
    if a:
        for j in range(max(0, i - d), min(len(active), i + d + 1)):
            act2[j] = True
active = act2

segs, i = [], 0
while i < len(active):                    # ตัดเป็นช่วงตามชนิด
    j = i
    while j < len(active) and active[j] == active[i]:
        j += 1
    segs.append([i * DT, j * DT, active[i]])
    i = j

merged = []                               # ยุบช่วงสั้นเข้ากับตัวก่อนหน้า
for s in segs:
    if merged and (s[1] - s[0]) < MIN_SEG_S:
        merged[-1][1] = s[1]
    elif merged and merged[-1][2] == s[2]:
        merged[-1][1] = s[1]
    else:
        merged.append(s[:])

ของผมเหลือ 49 ช่วงพอดี ซึ่งเป็นจำนวนที่ยังจัดการไหว ถ้าได้เกินร้อยให้ขยับ MIN_SEG_S ขึ้น เพราะทุกช่วงจะกลายเป็นหนึ่งสาขาใน filtergraph และเป็นหนึ่งอินพุตของ concat ยิ่งเยอะยิ่งกินหน่วยความจำตอนเรนเดอร์

ตัวคูณของช่วงทำงานตั้งเอง ตัวคูณของช่วงนิ่งให้คำนวณย้อนจากความยาวเป้าหมาย

ตรงนี้ผมใช้วิธีคิดง่าย ๆ คือช่วงทำงานผมอยากให้ดูรู้เรื่อง เลยตรึงไว้ที่ 3 เท่า ส่วนช่วงนิ่งปล่อยให้เป็นตัวแปรที่แก้สมการออกมาให้ได้ความยาวปลายทางตามที่ตั้งไว้ แล้วหนีบไม่ให้เกินขอบเขตที่ยอมรับได้

TARGET_OUT, ACTIVE_SPEED, IDLE_CLAMP = 230.0, 3.0, (6.0, 16.0)

active_total = sum(e - st for st, e, a in final if a)
idle_total   = sum(e - st for st, e, a in final if not a)

out_active = active_total / ACTIVE_SPEED
remain     = max(TARGET_OUT - out_active, 10.0)
idle_speed = min(max(idle_total / remain, IDLE_CLAMP[0]), IDLE_CLAMP[1])
est_out    = out_active + idle_total / idle_speed
print(f"ACTIVE={ACTIVE_SPEED}x IDLE={idle_speed:.1f}x -> ~{est_out:.0f}s")

เหตุผลที่ต้องหนีบขอบบนไว้ที่ 16 เท่าคือ ถ้าปล่อยให้คำนวณอิสระแล้วคลิปมีช่วงนิ่งยาวมาก ตัวคูณจะพุ่งไปสี่ห้าสิบเท่า ผลคือช่วงนิ่งหายไปในพริบตาจนคนดูงงว่าภาพกระโดด ส่วนขอบล่างที่ 6 เท่ามีไว้กันอีกทาง คือถ้าคลิปแทบไม่มีช่วงนิ่งเลย ตัวคูณจะลงมาเท่ากับช่วงทำงานจนแยกไม่ออกว่าเร่งไปทำไม

732 วิความยาวต้นฉบับ
49ช่วงหลังยุบ
3xช่วงทำงาน
6-16xช่วงนิ่ง
230 วิความยาวเป้า
30 fpsปลายทาง

หัวใจของเรื่อง drawtext ต้องมาก่อน setpts

ตรงนี้คือประเด็นหลักของทั้งบทความ ตัวกรอง drawtext ที่สั่งพิมพ์ %{pts:hms} มันอ่านค่าเวลาของเฟรม ณ จุดที่ตัวมันเองยืนอยู่ในสายท่อ ไม่ใช่เวลาของต้นฉบับ พอเราเอา setpts ไปหารเวลาลงเพื่อเร่งภาพ เฟรมที่ไหลออกมาจะมีเวลาใหม่ที่สั้นลงตามตัวคูณ ถ้า drawtext ยืนอยู่หลัง setpts มันก็อ่านเวลาใหม่นั้นไปพิมพ์ นาฬิกาบนภาพเลยเดินเร็วสามเท่าหกเท่าตามที่เราเร่ง

วางผิด: setpts ก่อน drawtexttrimตัดช่วงsetptsเวลา = T หารตัวคูณdrawtextอ่านเวลา ณ จุดนี้นาฬิกาเดินเร็วตามตัวคูณที่เร่งวางถูก: drawtext ก่อน setptstrimตัดช่วงdrawtextอ่านเวลาต้นฉบับ Tsetptsเวลา = T หารตัวคูณนาฬิกาตรงต้นฉบับกระโดดเป็นช่วง ๆ
drawtext อ่านเวลาของเฟรม ณ ตำแหน่งที่ยืนในสายท่อ ถ้ายืนหลัง setpts จะอ่านเวลาที่ถูกหารตัวคูณไปแล้ว
วางผิด

trim แล้ว setpts แล้วค่อย drawtext

นาฬิกาอ่านเวลาที่ถูกเร่งแล้ว คลิป 12 นาทีจะขึ้นตัวเลขวิ่งจบที่ราวสี่นาที เท่ากับบอกเวลาของคลิปปลายทาง ซึ่งคนดูเห็นจากแถบเล่นอยู่แล้ว ไม่มีประโยชน์

วางถูก

trim แล้ว drawtext แล้วค่อย setpts

นาฬิกาอ่านเวลาของเฟรมตอนที่ยังไม่ถูกเร่ง ตัวเลขบนภาพเลยกระโดดเป็นช่วง ๆ ตามตัวคูณ และจบที่ 12 นาทีกว่าตามความจริง คนดูเห็นได้ทันทีว่าช่วงไหนโดนเร่งแรง

ค่าที่พิมพ์ออกมาเป็นเวลาที่นับจากจุดเริ่มของ trim ในช่วงนั้น ไม่ใช่จากศูนย์ของไฟล์ต้นฉบับ ถ้าตัดต้นคลิปทิ้งไปสามวินาทีครึ่งแล้วอยากให้นาฬิกาตรงกับไฟล์เดิมเป๊ะ ต้องบวกค่าชดเชยเข้าไปเอง ผมไม่ได้บวก เพราะคลิปนี้ผมนับเวลาจากจุดที่เริ่มทำงานจริง ไม่ได้นับจากตอนที่กดอัด

ข้อความไทยต้องระบุฟอนต์เอง และเครื่องหมายทวิภาคต้องหลบ

สองเรื่องนี้ทำผมเสียเวลาไปพอสมควร เรื่องแรกคือ drawtext ไม่มีฟอนต์ไทยติดมาให้ ถ้าไม่ระบุ fontfile จะได้กล่องสี่เหลี่ยมเรียงกันเป็นแถว หรือไม่ก็ไม่ขึ้นอะไรเลย เรื่องที่สองคือเครื่องหมายทวิภาคเป็นตัวคั่นพารามิเตอร์ของตัวกรอง ถ้าเขียน %{pts:hms} ตรง ๆ ffmpeg จะอ่านว่าเป็นพารามิเตอร์ตัวใหม่แล้วฟ้องว่าไม่รู้จักตัวเลือกชื่อ hms

# นาฬิกาเวลาต้นฉบับ กับป้ายกำกับภาษาไทย
CLOCK = ("drawtext=fontfile=consolab.ttf:text='%{pts\\:hms}':fontsize=54:"
         "fontcolor=white:box=1:[email protected]:boxborderw=9:x=36:y=30")
CLABEL = ("drawtext=fontfile=tahoma.ttf:text='เวลาจริงต้นฉบับ (ถูกเร่ง speed)':"
          "fontsize=15:fontcolor=0x9fd0ff:box=1:[email protected]:"
          "boxborderw=5:x=38:y=96")
OVL = CLOCK + "," + CLABEL
อย่าใส่พาธเต็มของฟอนต์ในตัวกรอง

บนวินโดวส์พาธอย่าง C:\work\fonts\tahoma.ttf มีทั้งทวิภาคหลังชื่อไดรฟ์และแบ็กสแลช ซึ่งต้องหลบซ้อนกันหลายชั้นจนแทบไม่มีใครเขียนถูกในครั้งแรก ทางที่เสียเวลาน้อยที่สุดคือคัดลอกไฟล์ฟอนต์มาไว้ในโฟลเดอร์งาน แล้วอ้างชื่อไฟล์เปล่า ๆ พร้อมกับ cd เข้าโฟลเดอร์นั้นก่อนรัน ฟอนต์ตัวเลขกับฟอนต์ไทยแยกกันได้ ผมใช้ฟอนต์ความกว้างคงที่กับนาฬิกาเพื่อไม่ให้ตัวเลขขยับซ้ายขวาตอนวินาทีเปลี่ยน

อ้างอินพุตซ้ำไม่ได้ ต้อง split ออกเป็นกิ่งเท่าจำนวนช่วง

ใน filtergraph หนึ่งป้ายใช้ได้ครั้งเดียว จะเขียน [0:v]trim=... สี่สิบเก้าครั้งไม่ได้ ffmpeg จะฟ้องทันทีว่าป้ายถูกใช้ไปแล้ว ทางแก้คือ split ออกเป็นกิ่งตามจำนวนช่วงที่จะตัด แล้วแต่ละกิ่งค่อยไป trim ของตัวเอง

อีกเรื่องที่ต้องมีทุกกิ่งคือ fps กับ format ก่อนเข้า concat ถ้าปล่อยให้แต่ละกิ่งจบด้วยอัตราเฟรมหรือรูปแบบพิกเซลไม่เท่ากัน ผลคือภาพกระตุกตรงรอยต่อ หรือไม่ก็ได้ข้อความฟ้องตรง ๆ ว่าอินพุตของ concat ไม่ตรงกัน

K = len(final)
parts = ["[0:v]split=%d%s" % (K, "".join(f"[s{k}]" for k in range(K)))]
for k, (st, e, a) in enumerate(final):
    spd = ACTIVE_SPEED if a else idle_speed
    parts.append(f"[s{k}]trim=start={st:.3f}:end={e:.3f},{OVL},"
                 f"setpts=(PTS-STARTPTS)/{spd:.4f},fps=30,format=yuv420p[v{k}]")
graph = ";\n".join(parts) + ";\n" + \
        "".join(f"[v{k}]" for k in range(K)) + f"concat=n={K}:v=1:a=0[outv]"
open("filtergraph.txt", "w", encoding="utf-8").write(graph)

หน้าตาของบรรทัดที่ได้ออกมาเป็นแบบนี้ สังเกตลำดับให้ดีคือ trim แล้ว drawtext สองตัว แล้วค่อย setpts แล้วปิดท้ายด้วย fps กับ format

[0:v]split=49[s0][s1][s2]...[s48];
[s0]trim=start=3.500:end=17.500,drawtext=fontfile=consolab.ttf:text='%{pts\:hms}'
    :fontsize=54:fontcolor=white:box=1:[email protected]:boxborderw=9:x=36:y=30,
    drawtext=fontfile=tahoma.ttf:text='เวลาจริงต้นฉบับ (ถูกเร่ง speed)':fontsize=15
    :fontcolor=0x9fd0ff:box=1:[email protected]:boxborderw=5:x=38:y=96,
    setpts=(PTS-STARTPTS)/3.0000,fps=30,format=yuv420p[v0];
[s1]trim=start=17.500:end=20.500,...,setpts=(PTS-STARTPTS)/6.0000,fps=30,
    format=yuv420p[v1];
...
[v0][v1][v2]...[v48]concat=n=49:v=1:a=0[outv]

setpts=(PTS-STARTPTS)/N ต้องลบ STARTPTS ออกก่อนหารเสมอ เพราะหลัง trim เวลาของเฟรมยังนับต่อจากต้นไฟล์อยู่ ถ้าหารตรง ๆ โดยไม่ลบฐานออก ช่วงท้าย ๆ ของคลิปจะได้เวลาเริ่มต้นที่ไม่ใช่ศูนย์ แล้ว concat จะต่อกันเพี้ยน

กราฟยาวเกินบรรทัดคำสั่ง ให้อ่านจากไฟล์

กราฟของผมยาวสองหมื่นกว่าตัวอักษร ยัดลงบรรทัดคำสั่งไม่ไหวแน่นอน โดยเฉพาะบนวินโดวส์ที่มีเพดานความยาวบรรทัด ทางออกคือเขียนกราฟลงไฟล์แล้วให้ ffmpeg อ่านเอา

cd /c/work/video
ffmpeg -hide_banner -y -i screen.mp4 \
  -filter_complex_script filtergraph.txt -map "[outv]" \
  -c:v h264_nvenc -preset p5 -rc vbr -cq 23 -movflags +faststart \
  timelapse.mp4

การเข้ารหัสด้วย h264_nvenc ทำให้รอบนี้จบเร็วกว่าเดิมหลายเท่า งานนี้ตัวถอดรหัสกับตัวกรองทำงานบนซีพียูอยู่แล้ว การ์ดจอช่วยได้เฉพาะขั้นเข้ารหัส แต่แค่ขั้นเดียวก็ช่วยได้เยอะเพราะเป็นขั้นที่หนักที่สุด ค่า cq 23 ผมว่ากำลังพอดีสำหรับคลิปหน้าจอที่มีตัวหนังสือเยอะ ถ้าตั้งสูงกว่านี้ตัวอักษรเล็ก ๆ จะเริ่มเลอะ

ตอนประกอบร่างสุดท้ายผมเอาการ์ดเปิดกับการ์ดปิดมาต่อหัวท้าย ตรงนี้ต้องบังคับให้ทุกอินพุตมีขนาด อัตราเฟรม และรูปแบบพิกเซลตรงกันอีกรอบ เพราะไฟล์ภาพนิ่งกับไฟล์วิดีโอมันคนละชนิดกันโดยธรรมชาติ

ffmpeg -hide_banner -y \
 -loop 1 -t 4 -i card_intro.png \
 -i timelapse.mp4 \
 -loop 1 -t 4 -i card_outro.png \
 -filter_complex "[0:v]scale=1280:720,fps=30,format=yuv420p[a];\
[1:v]scale=1280:720,fps=30,drawtext=fontfile=tahoma.ttf:text='example.com':\
fontsize=23:fontcolor=white:box=1:[email protected]:boxborderw=9:x=38:y=h-52,\
format=yuv420p[b];\
[2:v]scale=1280:720,fps=30,format=yuv420p[c];\
[a][b][c]concat=n=3:v=1[outv]" \
 -map "[outv]" -c:v h264_nvenc -preset p5 -rc vbr -cq 23 -movflags +faststart \
 final.mp4
echo "EXIT=$?"

สรุปกับดักที่ผมเจอ เรียงตามลำดับที่เสียเวลาให้มากที่สุด

  1. drawtext อยู่หลัง setpts นาฬิกาเดินตามเวลาที่ถูกเร่ง ย้ายมาไว้ก่อน setpts แล้วจบ
  2. ไม่ได้ระบุ fontfile ข้อความไทยกลายเป็นกล่องสี่เหลี่ยม ต้องชี้ไฟล์ฟอนต์เองทุกครั้ง
  3. ทวิภาคใน %{pts:hms} ไม่ได้หลบ ffmpeg ฟ้องว่าไม่รู้จักตัวเลือก ต้องใส่แบ็กสแลชหน้าทวิภาค
  4. อ้าง [0:v] ซ้ำหลายครั้ง ต้อง split ออกเป็นกิ่งเท่าจำนวนช่วงก่อนเสมอ
  5. ลืม fps กับ format ในบางกิ่ง concat ฟ้องหรือภาพกระตุกตรงรอยต่อ ต้องใส่ให้ครบทุกกิ่ง
  6. setpts หารโดยไม่ลบ STARTPTS ช่วงท้ายเวลาเริ่มไม่ใช่ศูนย์ ต่อกันแล้วเพี้ยน
  7. ยัดกราฟลงบรรทัดคำสั่ง พอยาวหลักหมื่นตัวอักษรต้องย้ายไปไฟล์แล้วใช้ filter_complex_script

ส่วนตัวผมว่าสิ่งที่ทำให้ไทม์แลปส์แบบนี้ดูรู้เรื่องกว่าการเร่งทั้งเส้น ไม่ใช่ความเนียนของภาพ แต่เป็นนาฬิกามุมจอนี่แหละ เพราะพอตัวเลขกระโดดจากนาทีที่สองไปนาทีที่ห้าในสองวินาที คนดูจะเข้าใจเองทันทีว่าช่วงนั้นคือช่วงนั่งรอ ไม่ต้องมีคำบรรยายอะไรเพิ่ม ใครจะเอาไปปรับตัวคูณหรือเปลี่ยนตัวเข้ารหัสตามเครื่องตัวเองก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย