P2P vs SFU vs mixing bridge: เลือก topology เสียง/วิดีโอจาก fan-out pattern ไม่ใช่จากความคุ้นเคย
สถาปัตยกรรม realtime media เลือกผิดตั้งแต่แรกแล้วเจ็บตอน scale — P2P ระเบิดที่ N คน, SFU เกิดมาเพื่อ many-to-many, mixing bridge เหมาะกับ broadcast ทิศทางเดียว. มาดูวิธีเลือกจากว่าใครพูดใครฟังและ bandwidth ที่ endpoint รับไหว
อ่าน ~8 นาที
ปัญหา: เลือก topology จากความคุ้นเคย แล้วเจ็บตอน scale
ผมเคยทำระบบสื่อสารเสียง/วิดีโอ realtime มาหลายตัว ทั้งแบบโทรหากันตัวต่อตัว, ห้องประชุมหลายคน, และแบบประกาศเสียงออกลำโพงทั้งหมู่บ้าน. สิ่งที่ผมเห็นซ้ำๆ คือทีมมักเลือก topology ของ media จากสิ่งที่เคยใช้ ไม่ใช่จากรูปแบบการไหลของเสียงจริง แล้วพอจำนวนคนโตขึ้นหรือทิศทางการสื่อสารเปลี่ยน ระบบก็พังในแบบที่แก้ทีหลังยากมาก เพราะ topology มันฝังลึกถึงชั้น signaling และ infra.
Thesis ของบทความนี้ตรงไปตรงมา: topology ของ realtime media มีสามแบบหลัก — P2P mesh, SFU, และ server-side mixing bridge — และแต่ละแบบเกิดมาแก้ปัญหาคนละอย่าง. P2P ดีสำหรับ 1:1, SFU สำหรับ many-to-many, mixing bridge สำหรับ broadcast ทิศทางเดียว. เกณฑ์เลือกที่ถูกคือ fan-out pattern (ใครพูดใครฟัง กี่คน) กับ bandwidth ที่ endpoint แต่ละตัวรับไหว ไม่ใช่ว่า library ไหนคุ้นมือ.
สามแบบ: P2P mesh, SFU (selective forwarding), MCU/mixing bridge
ก่อนจะเลือกได้ ต้องเข้าใจว่าแต่ละแบบ "media ไหลผ่านอะไร":
- P2P mesh — endpoint แต่ละตัวต่อ media ตรงถึงกันเป็นตาข่าย ไม่มี server กลางแตะเสียง. server มีหน้าที่แค่ signaling (แลก SDP/ICE) กับช่วย NAT traversal. latency ต่ำสุด เพราะไม่มี hop กลาง แต่ endpoint ต้อง encode/ส่ง stream แยกให้ทุกคนที่เหลือ.
- SFU (Selective Forwarding Unit) — endpoint ส่ง stream ขึ้น server ตัวเดียว server ก็ forward stream นั้นต่อไปให้ทุกคน โดย ไม่ decode ไม่ผสม แค่เลือกว่าจะส่งอันไหนไปให้ใคร. แต่ละคน upload แค่ครั้งเดียว แต่ download หลายเส้น (เส้นละคนพูด). นี่คือแกนของระบบประชุมสมัยใหม่.
- MCU / mixing bridge — server decode ทุก stream, ผสม (mix) เป็นเส้นเดียว แล้วส่งกลับ. endpoint เห็นแค่ stream เดียวเข้า/ออก เบามากฝั่ง client แต่ CPU ฝั่ง server หนัก เพราะต้อง transcode. เหมาะเวลา client อ่อน (เช่นอุปกรณ์ IoT/ลำโพง) หรือต้องการเสียงรวมเส้นเดียว.
หลักที่ผมยึด: ยิ่ง client อ่อนและ fan-out ยิ่งกว้าง ก็ยิ่งต้องดัน work ไปฝั่ง server — จาก P2P (ไม่มี server แตะ media) ไป SFU (server forward) ไป mixing bridge (server mix). ตัวเลือกไม่ได้เรียงว่า "อันไหนดีกว่า" แต่เรียงตามว่าภาระควรตกที่ใคร.
Bandwidth math: ทำไม P2P mesh ระเบิดที่ N คน
เหตุผลที่ P2P ดีมากที่ 1:1 แต่ใช้ไม่ได้ที่หลายคน อยู่ที่คณิตศาสตร์ล้วนๆ. ในห้อง mesh ที่มี N คน แต่ละคนต้องต่อ media ไปอีก N−1 คน ฉะนั้นจำนวน connection ทั้งห้องคือ N·(N−1)/2 — โตแบบ O(N²).
ที่เจ็บกว่าจำนวน connection คือ ภาระ upload ต่อ endpoint. สมมติวิดีโอเส้นละ 1.5 Mbps ให้ประมาณคร่าวๆ:
// mesh: แต่ละ endpoint ต้อง encode + upload แยก (N-1) เส้น
// stream แต่ละเส้นเป็นสำเนาอิสระ เพราะไม่มีตัวกลางช่วย fan-out
uploadPerPeer(N) = (N - 1) * 1.5 Mbps
// 2 คน -> 1.5 Mbps (สบาย เน็ตบ้านไหว)
// 5 คน -> 6.0 Mbps (เริ่มหนักฝั่ง uplink)
// 8 คน -> 10.5 Mbps (uplink บ้านทั่วไปเริ่มไม่ไหว)
// 12 คน -> 16.5 Mbps (ตาย: uplink + CPU encode N-1 เส้น)
// SFU: upload คงที่ 1 เส้นเสมอ ไม่ว่าห้องกี่คน
uploadPerPeer_SFU = 1.5 Mbps // download ต่างหากที่โตตามคนพูด
จุดตายของ mesh ไม่ใช่แค่ bandwidth แต่รวมถึง CPU ที่ต้อง encode วิดีโอ N−1 ชุดขนานกัน บนมือถือหรือ laptop ธรรมดา. ด้วยเหตุนี้ mesh จึงเหมาะกับ 1:1 หรือกลุ่มจิ๋ว 3-4 คนสูงสุด. พอเกินนั้น ต้องมีตัวกลางมา fan-out แทน endpoint — ซึ่งก็คือหน้าที่ของ SFU. บทเรียน transferable: ถ้า fan-out ต้องเกิดหลายเส้น อย่าให้ endpoint เป็นคนทำ fan-out ย้ายไปให้ตัวกลางที่ upload ครั้งเดียวแล้วกระจายต่อ.
One-way broadcast: mixing bridge + mute ขาเข้า = ฟังอย่างเดียว
มีอีกรูปแบบที่ทั้ง mesh และ SFU ไม่ใช่คำตอบที่ดีที่สุด: broadcast ทิศทางเดียว — คนพูดหนึ่งคน ปลายทางฟังจำนวนมาก และปลายทาง ห้ามพูดกลับ. ตัวอย่างที่ผมเจอคือระบบประกาศเสียงออกลำโพงหลายตัวพร้อมกัน. ปลายทางเป็นอุปกรณ์เสียงที่ไม่มีไมค์ ไม่ต้องการ interactivity แค่ต้องได้ยินเสียงประกาศ.
เคสนี้ mixing bridge ตอบโจทย์กว่า เพราะ (1) เราอยากได้เสียง เส้นเดียว ยิงเข้าทุกลำโพง (2) ปลายทางอ่อน จัดการ stream เดียวง่ายกว่า และ (3) เราต้อง บังคับให้เป็น one-way ที่ระดับ media เลย ไม่ใช่แค่ซ่อนปุ่มพูดใน UI. วิธีทำคือให้ทุกปลายทางเข้า bridge เดียวกัน แต่ mute ทิศขาเข้าของ leg ปลายทาง — เสียงจาก bridge ไปหาลำโพงได้ แต่เสียง (ถ้ามี) จากลำโพงไม่ถูกนับเข้า mix:
// pseudo-code สไตล์ควบคุม bridge ผ่าน media server API
// 1 session ประกาศ = 1 bridge (mixing)
const bridge = await media.createBridge({ type: 'mixing' });
// leg ของคนประกาศ: พูดเข้า mix ได้เต็ม (สองทาง)
await bridge.addChannel(announcerChannel);
// leg ของลำโพงปลายทาง: รับเสียงออกอย่างเดียว
for (const speaker of speakers) {
const leg = await originateAutoAnswer(speaker); // ลำโพงรับสายเอง
// mute 'in' = เสียงจากลำโพงไม่เข้า mix -> ฟังอย่างเดียว พูดกลับไม่ได้
await bridge.addChannel(leg, { mute: { direction: 'in' } });
}
หลักการที่ยกไปใช้ที่อื่นได้: ถ้า business rule คือ "ห้ามพูดกลับ" ให้บังคับที่ media layer ไม่ใช่ UI layer. mute ที่ระดับ bridge เป็นการรับประกันเชิงโครงสร้างว่าเสียงย้อนไม่มีทางหลุด แม้ client จะถูกดัดแปลง. ทิศทางของ media คือ contract ที่ต้อง enforce ให้ต่ำที่สุดในสแตก.
Session lifecycle: 1 session = 1 bridge และกับดัก state ใน memory
เมื่อเลือก mixing bridge แล้ว โครงที่ผมใช้คือ 1 session การประกาศ = 1 bridge สร้างตอนเริ่ม ทำลายตอนจบ. เรียบง่ายและ isolate กันดี. แต่มันเปิดกับดักคลาสสิกของ realtime media: session state มักอยู่ใน memory ของ process เพราะมันผูกกับ live channel/socket ที่ serialize ลง DB ตรงๆ ไม่ได้.
ผลคือพอ backend restart กลางคัน — deploy, crash, OOM — map ของ session หายหมด แต่ resource จริงยังค้าง: bridge ยังเปิด, ลำโพงยังค้างสถานะ "กำลังจะประกาศ", ช่องสายยังจองอยู่. เกิดเป็น orphan leg ที่ไม่มีเจ้าของ. บทเรียนที่ผมเจ็บมาแล้วคือ state ที่อยู่ใน memory ต้องมี reconciler ที่ถือ media server เป็น source of truth ไม่ใช่ถือ memory:
// ตอน boot: กวาด resource ที่ค้างจาก process รุ่นก่อน
async function onBootstrap() {
const liveBridges = await media.listBridges(); // ถามของจริง
for (const b of liveBridges) {
if (isOwnedByThisSystem(b) && !memory.has(b.id)) {
await media.destroyBridge(b.id); // ไม่มีเจ้าของ = เก็บทิ้ง
}
}
}
// janitor เป็นระยะ: ปิด session ที่นานเกิน + เก็บ leg กำพร้า
setInterval(async () => {
await teardownSessionsOlderThan(30 * 60_000); // กันค้างข้ามคืน
await sweepOrphanChannels(); // leg ที่ไม่มี session อ้าง
}, 60_000);
Transferable: ทุกครั้งที่ state สำคัญอยู่แค่ใน memory ให้ถามว่า "restart แล้วอะไรค้าง แล้วใครเก็บกวาด". คำตอบต้องเป็น reconciliation loop ที่ทำงานตอน boot และเป็นระยะ โดยเชื่อของจริงเสมอ.
Capacity planning: RTP port range กับ concurrent call จริง
อีกเพดานที่คนลืมจนไปเจอตอน production คือ จำนวน concurrent call ถูกจำกัดด้วย RTP port range. media server เปิด UDP port ช่วงหนึ่งไว้รับ-ส่ง RTP และแต่ละสายกินไปหลาย port. เช่นเปิดช่วง 10000–10200 (กว้าง 200 port) ก็รองรับได้ราวๆ 100 สายพร้อมกัน เพราะสายหนึ่งกินราวสอง port. ตัวเลขนี้ต้องคำนวณตั้งแต่วางระบบ ไม่ใช่ปล่อยให้ระบบไปชนเพดานเงียบๆ แล้วสายที่ 101 ต่อไม่ติดโดยไม่มี error ชัด.
// เพดานหยาบๆ ที่ต้องรู้ก่อน deploy
portRange = 10200 - 10000 // = 200 ports
portsPerCall = 2 // RTP + (บางที RTCP)
maxConcurrent = portRange / portsPerCall // ~100 สายพร้อมกัน
// อยากได้ 500 สาย? ต้องขยาย range + วางแผน bandwidth/CPU ตามด้วย
// อย่าลืม: mixing bridge ยัง +CPU transcode ต่อสาย เป็นอีกเพดานซ้อน
หลัก: capacity ของ realtime media เป็นเพดานหลายชั้นซ้อนกัน — port range, bandwidth, CPU (โดยเฉพาะ mixing ที่ต้อง transcode), และ file descriptor. ตัวที่ต่ำสุดคือเพดานจริง. เขียน capacity note ไว้ตั้งแต่วันแรกว่าตอนนี้เพดานอยู่ที่ตัวไหนและเท่าไร.
TURN cost: relay ทุก media มีค่า bandwidth ที่ต้องวางแผน
สุดท้าย ไม่ว่าเลือก topology ไหน พอมี client อยู่หลัง NAT/firewall ที่เจาะไม่ทะลุ media จะต้องวิ่งผ่าน TURN relay. STUN แค่ช่วยให้สองฝั่งรู้ public address แล้วต่อตรง แต่ TURN คือ server ที่ relay ทุก byte ของ media ให้ — ซึ่งแปลว่ามันกิน bandwidth ของ server คุณเต็มๆ ตามปริมาณ media จริง.
ในบางสภาพแวดล้อมองค์กรที่ P2P/host candidate ใช้ไม่ได้เลย เราต้องบังคับ relay อย่างเดียวเพื่อให้เสียงออกแน่ๆ — แลกมาด้วยว่า ทุกสายวิ่งผ่าน TURN หมด ค่า egress bandwidth จึงต้องคิดเผื่อ. ถ้า SFU/mixing อยู่ฝั่ง server แล้ว media วิ่งผ่าน TURN อีกชั้น เท่ากับจ่าย bandwidth สองต่อ. Transferable: TURN ไม่ฟรี — มันเป็นทั้งค่า bandwidth และ single point ที่ต้องเผื่อ capacity เหมือน media server ตัวหนึ่ง ไม่ใช่แค่ config บรรทัดเดียวใน ICE.
สรุป: เลือกจาก fan-out pattern ไม่ใช่จาก hype
topology ของ realtime media ไม่มีอันไหน "ดีที่สุด" มีแต่ "เหมาะกับ fan-out pattern ไหน". ก่อนเขียนโค้ดสักบรรทัด ให้ตอบก่อนว่า: ใครพูด กี่คน, ใครฟัง กี่คน, ปลายทางพูดกลับได้ไหม, และ endpoint แต่ละตัวแบก bandwidth/CPU ได้แค่ไหน. คำตอบพวกนี้จะบอก topology เอง.
- 1:1 หรือกลุ่มจิ๋ว → P2P mesh — latency ต่ำสุด ไม่มี server แตะ media; แต่ connection โต O(N²) และ endpoint ต้อง encode N−1 เส้น พอเกิน 3-4 คนก็ระเบิด
- many-to-many → SFU — upload คงที่ 1 เส้นต่อ endpoint, server forward ไม่ decode; download โตตามคนพูด เป็นมาตรฐานของห้องประชุม
- broadcast ทิศทางเดียว / client อ่อน → mixing bridge — เสียงรวมเส้นเดียว, บังคับ one-way ด้วย mute ทิศขาเข้าที่ media layer ไม่ใช่ UI
- state ใน memory ต้องมี reconciler — restart แล้วอะไรค้าง ใครเก็บ; เชื่อ media server เป็น source of truth ทั้งตอน boot และ janitor เป็นระยะ
- capacity เป็นเพดานหลายชั้น — RTP port range, bandwidth, CPU transcode, fd; คำนวณ concurrent call จริงตั้งแต่วันแรก อย่าไปชนเพดานเงียบๆ
- TURN relay มีต้นทุน — มัน relay media ทุก byte, กิน egress bandwidth ตามจริง; ถ้าบังคับ relay ต้องวางแผน capacity เหมือน media server อีกตัว
- เลือกจากทิศทางและจำนวน ไม่ใช่จาก library ที่คุ้นมือ — topology ฝังลึกถึง signaling และ infra แก้ทีหลังแพงมาก เลือกให้ถูกตั้งแต่แรก




