في تتبع مكالمة SIP عبر الشبكة أصلحنا المكالمة التي لا يصل فيها الصوت أصلاً، أي مشكلة «هل يصل الصوت؟». أما الشكوى التي سوف تسمعها أكثر بعد ذلك فهي من نوع آخر: الصوت يصل، ولكنه يتقطع، أو يأتي كأنه من آلة، أو يتأخر فيتكلم الطرفان في الوقت نفسه ثم يسكتان معاً. وهذه المشكلات لا تظهر في إعداد المقسم، وإنما في الشبكة بين الهاتف والمقسم، ولا يمكن أن تصلحها إذا لم تستطع قياسها.
المواضيع في السلسلة:
- مصطلحات الاتصالات الهاتفية VoIP للمبتدئين
- تتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟
- لماذا يتقطع الصوت في مكالمات VoIP؟ قياس Jitter وPacket Loss (الموضوع الحالي)
- ربط المقسم PBX بشبكة الهاتف عبر SIP Trunk والأرقام DID
- دليل Asterisk العملي: من التحويلة Extension إلى الرد الآلي IVR
- FreePBX: مقسم هاتف PBX كامل تديره من المتصفح
- FreeSWITCH للمنصات الكبيرة ومراكز الاتصال Contact Center
- 3CX أم FreePBX أم FreeSWITCH؟ كيف تختار المقسم PBX
- هواتف SIP المكتبية والتطبيقات: لماذا لا يرن الجوال النائم؟
- مركز اتصال Call Center على Asterisk: الطابور وتقرير SLA
- تأمين المقسم من الاحتيال على المكالمات Toll Fraud
- موازنة الحمل Load Balancing أمام خوادم Asterisk مع Kamailio
وفي هذا المقال لن نكتفي بتعريف المصطلحات، وإنما سوف نبني تجربة نفسد فيها الشبكة عمداً بين هاتف ومقسم Asterisk، ونرسل الجملة نفسها في كل مرة، ثم نقيس ما وصل بدرجة من 1 إلى 5، حتى ترى بالأرقام ماذا يفعل كل عطل بالصوت، وسوف نتناول ما يلي:
- الأعطال الأربعة التي تفسد الصوت: التأخير Latency، والاهتزاز Jitter، وفقد الحزم Packet Loss، والازدحام عند أبطأ خط في الطريق.
- كيف تقاس الجودة: درجة MOS، ومن أين تأتي، وما الذي تقرؤه في إحصاءات المقسم والهاتف.
- تجربة في Docker: هاتفان وAsterisk وراوتر يضيف التأخير والفقد، مع قياس الجودة بأداة PESQ.
- مقارنة G.711 وOpus تحت الفقد، وأثر حجم ال Jitter Buffer، ولماذا لم يعمل تصحيح الأخطاء FEC عندما مرت المكالمة عبر المقسم.
- جودة الخدمة QoS بالعلامة DSCP: الفرق بين خط مزدحم يعامل كل الحزم بالتساوي وخط يقدم الصوت.
أربعة أشياء تفسد الصوت في الشبكة
كما رأينا في مقال المصطلحات، الهاتف يقطع الصوت إلى أجزاء صغيرة، غالباً كل 20 ms، ويضع كل جزء في حزمة RTP مستقلة، فيرسل 50 حزمة في الثانية في كل اتجاه. والطرف الآخر يحتاج أن يشغل هذه الحزم بالترتيب وبالإيقاع نفسه، أي حزمة كل 20 ms بالضبط. وكل ما يفسد الصوت في الشبكة يرجع إلى أن هذا الإيقاع لم يصل كما خرج، وذلك بأحد أربعة أشكال:
| العطل | ماذا يحدث للحزم | ماذا يسمع المستخدم |
|---|---|---|
| التأخير Latency | كل الحزم تصل، ولكن بعد وقت طويل | صدى، وتداخل في الكلام، والطرفان يقاطعان بعضهما |
| الاهتزاز Jitter | الحزم تصل بفواصل غير منتظمة، وأحياناً بغير ترتيبها | تقطع خفيف، أو لا شيء إذا امتصه ال Jitter Buffer |
| فقد الحزم Packet Loss | بعض الحزم لا تصل أبداً | كلمات ناقصة، وصوت معدني، وفراغات قصيرة |
| الازدحام Congestion | طابور طويل عند أبطأ خط، فيجتمع التأخير والفقد معاً | المكالمة تفسد كلما رفع أحد في المكتب ملفاً كبيراً |
ولاحظ أن الصوت لا يعيد إرسال ما ضاع كما يفعل TCP، والسبب أن الحزمة التي تصل بعد موعدها لا قيمة لها، فلا أحد يريد أن يسمع كلمة قيلت قبل ثانية. لذلك يعمل الصوت فوق UDP، ويتحمل الفقد بدل أن ينتظر.
التأخير Latency: كم يحتمل الحوار؟
التأخير الذي يهم هو من فم المتكلم إلى أذن السامع Mouth-to-Ear، وهو مجموع عدة أجزاء: وقت جمع الصوت في حزمة وترميزه، ثم الطريق في الشبكة في اتجاه واحد، ثم ال Jitter Buffer عند المستقبل، ثم فك الترميز والتشغيل. وتقول توصية ITU-T G.114 إن أغلب التطبيقات لا تتأثر بشكل ملحوظ إذا بقي التأخير في اتجاه واحد دون 150 ms، وتنصح بألا يتجاوز 400 ms في تخطيط الشبكات. والصورة التالية تبين مثالين:

ولاحظ في المثال الثاني أن الشبكة وحدها لم تتجاوز الحد، ولكن الشبكة السيئة جعلت الهاتف يكبر ال Jitter Buffer، فتجاوز المجموع الحد. وهذا يقودنا إلى العطل الثاني.
الاهتزاز Jitter وال Jitter Buffer
الاهتزاز Jitter هو التفاوت في زمن وصول الحزم، أي أن الحزم خرجت كل 20 ms ووصلت مرة بعد 5 ms ومرة بعد 45 ms. ويعرفه RFC 3550 رقماً واحداً يحسبه كل طرف من فرق زمن الوصول بين كل حزمتين متتاليتين، ويرسله إلى الطرف الآخر في تقارير RTCP، وهو الرقم الذي سوف تراه في إحصاءات الهاتف والمقسم.
والحل هو ال Jitter Buffer: مخزن صغير في الهاتف المستقبل يحتفظ بالحزم مدة قصيرة قبل تشغيلها، فيعيد ترتيبها ويشغلها بالإيقاع الصحيح. وهنا المقايضة كلها: المخزن الصغير يعطي تأخيراً قليلاً ولكنه يرمي كل حزمة تصل بعد موعد تشغيلها، والمخزن الكبير يشغل كل الحزم ولكنه يؤخر المكالمة كلها، كما تبين الصورة التالية:

وأغلب الهواتف اليوم تستخدم Jitter Buffer متكيفاً Adaptive يكبر ويصغر بحسب ما يقيسه من اهتزاز، وسوف نرى في التجربة ماذا يحدث عندما يكون المخزن أصغر من اهتزاز الشبكة.
فقد الحزم Packet Loss وما يفعله الترميز Codec
عندما تضيع حزمة لا يستطيع المستقبل أن ينتظرها، فيملأ مكانها بشيء ما: صمت، أو تقدير للصوت المفقود من الحزم التي قبلها، وهو ما يسمى إخفاء الفقد Packet Loss Concealment واختصاراً PLC. وبعض الترميزات تذهب أبعد من ذلك، فترميز Opus يستطيع أن يضع في كل حزمة نسخة مضغوطة جداً من الحزمة التي قبلها، فإذا ضاعت حزمة يستخرجها المستقبل من الحزمة التالية، وهذا هو تصحيح الأخطاء داخل الحزمة In-band FEC. ولكن هذا لا يعمل إلا إذا اتفق الطرفان عليه في SDP بالعلامة useinbandfec=1 كما في RFC 7587، وسوف نرى في التجربة أن هذا الاتفاق قد يضيع في الطريق دون أن تعلم.
كيف نقيس الجودة؟ درجة MOS
الطريقة الأصلية لقياس جودة الصوت هي أن يسمع مجموعة من الناس عينات من المكالمات ويعطي كل منهم درجة من 1 إلى 5، ثم يؤخذ المتوسط، وهذا هو متوسط رأي المستمعين Mean Opinion Score واختصاراً MOS، وطريقته موصوفة في ITU-T P.800:
| الدرجة | المعنى |
|---|---|
| 5 | ممتاز Excellent |
| 4 | جيد Good، وهو ما تتوقعه من مكالمة عادية سليمة |
| 3 | مقبول Fair، تفهم الكلام ولكنك تلاحظ العيب |
| 2 | ضعيف Poor، تطلب من الطرف الآخر أن يعيد كلامه |
| 1 | سيئ Bad |
ولأنه لا يوجد عندك فريق من المستمعين في كل مرة، ظهرت خوارزميات تقارن الصوت الأصلي بما وصل وتعطي درجة قريبة من رأي الناس، وأشهرها PESQ في التوصية ITU-T P.862، والجيل الأحدث منها POLQA في ITU-T P.863. وهناك أيضاً طريقة ثالثة لا تحتاج الصوت أصلاً، وهي نموذج E-model في ITU-T G.107 الذي يحسب درجة تقديرية من أرقام الشبكة، أي التأخير والفقد ونوع الترميز، وهي الدرجة التي تراها في أغلب لوحات مراقبة المكالمات.
وفي التجربة سوف نستخدم PESQ عبر مكتبة pesq للغة Python، لأنها تقيس الصوت الذي وصل فعلاً، وتنتج نوعين من الدرجات: درجة النطاق الضيق Narrowband على 8 kHz، وهي الحد الطبيعي لترميز G.711، ودرجة النطاق العريض Wideband على 16 kHz، وهي التي تظهر فرق الترميزات الحديثة. ويجدر الإشارة هنا إلى أن أعلى درجة تعطيها PESQ هي نحو 4.5 وليست 5، فلا تتوقع 5 من أي مكالمة.
خريطة التجربة
نحتاج ثلاثة أشياء: هاتف Bob في شبكة المكتب يرسل جملة مسجلة، ومقسم Asterisk وهاتف Alice في شبكة أخرى تمثل مركز البيانات، وبينهما راوتر نستطيع أن نتحكم في سلوكه، فنضيف التأخير أو الاهتزاز أو الفقد أو نبطئ الخط، ثم نسجل ما سمعته Alice ونقارنه بما قاله Bob:

والأداة المعروفة لإفساد الشبكة على Linux هي tc netem، وهي جزء من نواة Linux، ولكنك إذا جربت على Docker Desktop في Windows أو macOS فسوف تجد أن نواته لا تحتوي netem أصلاً، وهذا ما تقوله إعدادات النواة نفسها:
docker run --rm --privileged ubuntu:24.04 sh -c 'zcat /proc/config.gz | grep -E "SCH_NETEM|SCH_TBF|SCH_PRIO"'# CONFIG_NET_SCH_PRIO is not set
# CONFIG_NET_SCH_TBF is not set
# CONFIG_NET_SCH_NETEM is not setلذلك كتبنا أداة صغيرة باسم impair تعمل في الراوتر بدل netem: الراوتر يمرر كل حزمة قادمة من شبكة المكتب إلى واجهة افتراضية tun0، فتقرؤها الأداة وتؤخرها أو ترميها أو تضعها في طابور خط بطيء، ثم تعيدها إلى النواة فتكمل طريقها إلى المقسم. وإذا كنت تعمل على Linux عادي فتستطيع استخدام netem مباشرة بدلها، والأفكار في المقال كلها هي نفسها.
تجهيز ال Image والأدوات
نبني Image واحدة من Ubuntu 24.04 فيها Asterisk، والهاتف البرمجي baresip الذي استخدمناه في المقال السابق، وtcpdump، وiperf3 لصنع الازدحام، وespeak-ng لتوليد جملة منطوقة بالعربية بدل تسجيل صوتك، وPython لتشغيل أداة الراوتر. قم بإنشاء مجلد للتجربة وضع فيه الملف Dockerfile:
FROM ubuntu:24.04
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
asterisk=1:20.6.0~dfsg+~cs6.13.40431414-2build5 \
baresip-core=1.0.0-4build14 \
tcpdump=4.99.4-3ubuntu4.24.04.1 \
iperf3=3.16-1build2 \
espeak-ng=1.51+dfsg-12build1 \
python3-minimal=3.12.3-0ubuntu2.1 \
sox iproute2 iptables iputils-ping \
&& rm -rf /var/lib/apt/lists/*
COPY impair.py /usr/local/bin/impair
RUN chmod +x /usr/local/bin/impairوالملف impair.py هو أداة الراوتر، وهذا هو كاملاً:
#!/usr/bin/env python3
# A small user-space link emulator (like tc netem) for routers whose kernel has no netem.
# Every packet routed into tun0 is delayed, dropped or queued, then handed back to the kernel.
# impair [--delay MS] [--jitter MS] [--loss PCT] [--rate KBIT] [--queue PKTS] [--prio]
# --rate and --queue emulate a slow uplink with a drop-tail queue; --prio gives DSCP EF (voice) its own queue, served first.
import argparse, fcntl, heapq, os, random, select, signal, struct, sys, time
p = argparse.ArgumentParser()
p.add_argument('--delay', type=float, default=0); p.add_argument('--jitter', type=float, default=0)
p.add_argument('--loss', type=float, default=0); p.add_argument('--rate', type=float, default=0)
p.add_argument('--queue', type=int, default=50); p.add_argument('--prio', action='store_true')
a = p.parse_args()
tun = os.open('/dev/net/tun', os.O_RDWR)
fcntl.ioctl(tun, 0x400454ca, struct.pack('16sH', b'tun0', 0x0001 | 0x1000)) # TUNSETIFF, IFF_TUN | IFF_NO_PI
os.set_blocking(tun, False)
stats = {'in': 0, 'lost': 0, 'queue_drop': 0, 'out': 0}
signal.signal(signal.SIGTERM, lambda *_: (print(stats, flush=True), sys.exit(0)))
queues = ([], []) # 0 = EF (voice), 1 = everything else
link_free = 0.0 # when the emulated uplink finishes sending the current packet
wire = [] # (time to deliver, seq, packet): the delay line
seq = 0
while True:
now = time.monotonic()
timeout = min([w[0] for w in wire[:1]] + [link_free if any(queues) else now + 1]) - now
if select.select([tun], [], [], max(timeout, 0))[0]:
while True:
try: pkt = os.read(tun, 65535)
except BlockingIOError: break
stats['in'] += 1
if random.random() * 100 < a.loss: stats['lost'] += 1; continue
q = queues[0] if a.prio and pkt[1] >> 2 == 46 else queues[1]
if a.rate and len(q) >= a.queue: stats["queue_drop"] += 1; continue
q.append(pkt)
now = time.monotonic()
while any(queues) and link_free <= now: # the uplink sends one packet at a time at --rate
pkt = (queues[0] or queues[1]).pop(0)
link_free = max(link_free, now) + (len(pkt) * 8 / (a.rate * 1000) if a.rate else 0)
d = max(0.0, random.gauss(a.delay, a.jitter)) / 1000
seq += 1; heapq.heappush(wire, (link_free + d, seq, pkt))
while wire and wire[0][0] <= now:
os.write(tun, heapq.heappop(wire)[2]); stats['out'] += 1في الكود أعلاه لاحظ التالي:
--lossيرمي الحزمة بنسبة عشوائية، و--delayو--jitterيؤخرانها بتوزيع طبيعي متوسطه delay وانحرافه jitter، فتصل بعض الحزم قبل التي خرجت قبلها كما يحدث في الشبكات الحقيقية.--rateيحول الراوتر إلى خط بطيء يرسل حزمة واحدة في كل مرة بالسرعة المحددة، و--queueهو طول الطابور، وما يزيد عليه يرمى، وهذا هو سلوك أغلب الراوترات المنزلية والمكتبية.--prioيقرأ البايت الثاني من ترويسة IP، وهو حقل DSCP، فإذا كانت قيمته 46 أي EF وضع الحزمة في طابور مستقل يرسل أولاً، وسوف نشرح هذا في قسم QoS.- عند إيقاف الأداة تطبع عداداتها: ما دخل، وما رمي عشوائياً، وما رمي لامتلاء الطابور، وما خرج.
ونحتاج Image ثانية صغيرة لحساب درجة PESQ، فيها Python ومكتبة pesq، ومعها السكربت mos.py الذي يحمل الجملة الأصلية وما سمعته Alice، ويبحث عن موضع الجملة داخل التسجيل، ثم يحسب الدرجتين:
mkdir -p pesq
cat > pesq/Dockerfile <<'EOF'
FROM python:3.12.7-bookworm
RUN pip install --no-cache-dir numpy==2.1.2 scipy==1.14.1 soundfile==0.12.1 pesq==0.0.4
EOF
cat > pesq/mos.py <<'EOF'
# usage: python mos.py <reference.wav> <heard.wav>...
# Prints PESQ MOS-LQO for each recording: narrowband (8 kHz, ITU-T P.862) and wideband (16 kHz, P.862.2)
import sys, numpy as np, soundfile as sf
from math import gcd
from scipy.signal import resample_poly, correlate
from pesq import pesq
def load(path, rate):
x, sr = sf.read(path, dtype='float64')
if x.ndim > 1: x = x.mean(axis=1)
g = gcd(rate, sr)
return resample_poly(x, rate // g, sr // g) if sr != rate else x
def score(ref, deg, rate, mode):
lag = int(np.argmax(correlate(deg, ref, mode='valid', method='fft'))) # where the sentence starts in the call
return pesq(rate, ref, deg[lag:lag + len(ref)], mode)
for path in sys.argv[2:]:
nb = score(load(sys.argv[1], 8000), load(path, 8000), 8000, 'nb')
wb = score(load(sys.argv[1], 16000), load(path, 16000), 16000, 'wb')
print(f"{path:28} NB {nb:.2f} WB {wb:.2f}")
EOF
docker build -t lab-vq-tools:1 .
docker build -t lab-vq-pesq:1 pesqثم نولد الجملة المنطوقة بثلاث صيغ: 8 kHz لترميز G.711، و48 kHz بقناتين لأن baresip يرفض ملفاً لا يطابق معدل Opus، و16 kHz للمرجع:
mkdir -p sounds
docker run --rm -v "$PWD/sounds:/sounds" lab-vq-tools:1 sh -c '
espeak-ng -v ar -s 150 -w /sounds/raw.wav "مرحبا، هذه مكالمة تجريبية لقياس جودة الصوت. نرسل هذه الجملة، ثم نقارن ما وصل بما خرج."
sox /sounds/raw.wav -r 8000 -c 1 -b 16 /sounds/speech8k.wav pad 0.5 0.5
sox /sounds/raw.wav -r 48000 -c 2 -b 16 /sounds/speech48k.wav pad 0.5 0.5
soxi -D /sounds/speech8k.wav'والمخرج هو طول الجملة بالثواني، وهو مناسب لأن PESQ صممت لعينات من 8 إلى 12 ثانية تقريباً:
11.130125الشبكتان والراوتر والمقسم
الآن نقوم بإنشاء الشبكتين وتشغيل الراوتر، ونجعل كل ما يأتي من شبكة المكتب يمر عبر tun0 بقاعدة توجيه Policy Routing، ثم نشغل المقسم:
docker network create --subnet 10.88.10.0/24 lab-vq-wan
docker network create --subnet 10.88.20.0/24 lab-vq-lan
docker run -d --name lab-vq-router --network lab-vq-wan --ip 10.88.10.2 \
--cap-add NET_ADMIN --cap-add NET_RAW --device /dev/net/tun \
--sysctl net.ipv4.ip_forward=1 --sysctl net.ipv4.conf.all.rp_filter=0 --sysctl net.ipv4.conf.default.rp_filter=0 \
lab-vq-tools:1 sleep infinity
docker network connect --ip 10.88.20.254 lab-vq-lan lab-vq-router
docker exec lab-vq-router sh -c '
ip tuntap add dev tun0 mode tun && ip link set tun0 up
ip route add default dev tun0 table 100
ip rule add iif eth1 lookup 100'
docker run -d --name lab-vq-pbx --network lab-vq-wan --ip 10.88.10.10 --cap-add NET_ADMIN --cap-add NET_RAW \
-v "$PWD/asterisk/pjsip.conf:/etc/asterisk/pjsip.conf" -v "$PWD/asterisk/extensions.conf:/etc/asterisk/extensions.conf" \
-v "$PWD/asterisk/rtp.conf:/etc/asterisk/rtp.conf" -v "$PWD/asterisk/logger.conf:/etc/asterisk/logger.conf" \
lab-vq-tools:1 sh -c 'ip route add 10.88.20.0/24 via 10.88.10.2; exec asterisk -f'في الأوامر أعلاه لاحظ التالي:
- القاعدة
ip rule add iif eth1 lookup 100تعني أن كل حزمة تدخل من واجهة المكتبeth1تتبع الجدول 100، والجدول 100 يرسلها إلىtun0، فتمر على الأداة. وعندما تعيدها الأداة تدخل منtun0وليس منeth1، فتتبع الجدول العادي وتخرج إلى المقسم دون أن تدور في حلقة. - أطفأنا
rp_filterلأن النواة ترى حزمة عنوانها من شبكة المكتب تدخل منtun0، فترفضها إذا كان الفحص مفعلاً. - لم نستخدم الخيار
--internalللشبكتين هذه المرة، والسبب أن Docker يرمي في الشبكة الداخلية كل حزمة عنوانها من خارج الشبكة نفسها، والراوتر هنا يمرر حزم شبكة المكتب كما هي دون NAT. ولا يوجد منفذ مفتوح على جهازك في الحالتين لأننا لم نستخدم-p.
وإعداد المقسم هو نفسه الذي وصلنا إليه في المقال السابق مع ثلاثة أسطر جديدة للعلامات، وهذا هو الملف asterisk/pjsip.conf:
[global]
type=global
user_agent=ArabRoot-Lab-PBX
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
tos=cs5
[phone](!)
type=endpoint
context=office
disallow=all
allow=ulaw
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
tos_audio=ef
[1001](phone)
auth=1001
aors=1001
[1001]
type=auth
auth_type=userpass
username=1001
password=Lab-1001-pass
[1001]
type=aor
max_contacts=1
remove_existing=yes
[1002](phone)
auth=1002
aors=1002
[1002]
type=auth
auth_type=userpass
username=1002
password=Lab-1002-pass
[1002]
type=aor
max_contacts=1
remove_existing=yesوالسطر allow=ulaw هو الذي سوف نغيره إلى opus عند مقارنة الترميزين، وtos وtos_audio سوف نعود لهما في قسم QoS. وبقية الملفات قصيرة:
cat > asterisk/extensions.conf <<'EOF'
[office]
exten => _10XX,1,Dial(PJSIP/${EXTEN},20)
same => n,Hangup()
EOF
printf '[general]\nrtpstart=10000\nrtpend=10099\n' > asterisk/rtp.conf
printf '[general]\n[logfiles]\nconsole => notice,warning,error,verbose\n' > asterisk/logger.confالهاتفان والسكربتات
السكربت phones.sh يكتب إعداد الهاتفين، ويأخذ معاملين: حجم ال Jitter Buffer بعدد الحزم، وملف الجملة الذي يرسله الهاتف:
cat > phones.sh <<'EOF'
#!/bin/sh
# usage: ./phones.sh <jitter_buffer_delay frames, e.g. 5-10> <speech8k|speech48k>
JB=${1:-5-10}
SRC=${2:-speech8k}
for p in alice:1001 bob:1002; do n=${p%%:*}; ext=${p#*:}
mkdir -p $n
cat > $n/config <<CFG
poll_method epoll
sip_listen 0.0.0.0:5060
audio_source aufile,/sounds/$SRC.wav
audio_player alsa,null
audio_alert alsa,null
rtp_ports 20000-20019
rtp_tos 184
rtp_stats yes
jitter_buffer_delay $JB
opus_bitrate 24000
opus_inbandfec yes
opus_packet_loss 10
module_path /usr/lib/baresip/modules
module g711.so
module opus.so
module aufile.so
module menu.so
module_app account.so
module alsa.so
module sndfile.so
snd_path /rec
CFG
echo "<sip:[email protected]>;auth_pass=Lab-$ext-pass;answermode=auto" > $n/accounts
done
echo "module stdio.so" >> alice/config
EOF
chmod +x phones.shفي الإعداد أعلاه لاحظ التالي:
jitter_buffer_delay 5-10هو الحد الأدنى والأعلى لل Jitter Buffer بعدد الحزم، وكل حزمة 20 ms، أي من 100 إلى 200 ms، وهي القيمة الافتراضية في baresip 1.0.rtp_stats yesيجعل الهاتف يطبع عند نهاية المكالمة ما أرسل وما استقبل وما فقد والاهتزاز الذي قاسه.rtp_tos 184يضع العلامة EF على حزم الصوت، و184 بالنظام العشري هي 0xb8، وسوف نراها في الالتقاط.opus_inbandfec yesوopus_packet_loss 10يطلبان من مرمز Opus أن يضيف FEC وأن يتوقع فقداً بنسبة 10%.- الوحدة
sndfileتحفظ ما سمعه كل هاتف في ملف ينتهي بdec.wav، ومنه نحسب الدرجة.
وثلاثة سكربتات صغيرة توفر علينا التكرار: bob.sh يعيد تشغيل هاتف Bob في شبكة المكتب، وlink.sh يعيد تشغيل أداة الراوتر بشروط جديدة ويطبع عدادات التشغيل السابق، وcall.sh يشغل هاتف Alice ويتصل ب Bob، ويطلب من المقسم إحصاءات المكالمة في منتصفها، ثم يحفظ ما سمعته Alice:
cat > bob.sh <<'EOF'
#!/bin/sh
docker rm -f lab-vq-bob >/dev/null 2>&1
docker run -d --name lab-vq-bob --network lab-vq-lan --ip 10.88.20.22 --cap-add NET_ADMIN --cap-add NET_RAW \
-v "$PWD/bob:/conf" -v "$PWD/sounds:/sounds:ro" -v "$PWD/rec-bob:/rec" \
lab-vq-tools:1 sh -c 'ip route replace default via 10.88.20.254; exec baresip -f /conf'
sleep 5
EOF
cat > link.sh <<'EOF'
#!/bin/sh
# usage: ./link.sh [impair options]
docker exec lab-vq-router sh -c 'pkill -TERM -f ^python3; sleep 0.5; cat /tmp/impair.log 2>/dev/null'
docker exec -d lab-vq-router sh -c "exec impair $* > /tmp/impair.log 2>&1"
EOF
cat > call.sh <<'EOF'
#!/bin/sh
# usage: [TARGET=…] ./call.sh <name> one call from Alice (1001) to Bob (1002 through the PBX, or TARGET)
mkdir -p rec-alice rec-bob; rm -f rec-alice/* rec-bob/*
(sleep 6; echo "/dial ${TARGET:-1002}"; sleep 9; docker exec lab-vq-pbx asterisk -rx 'pjsip show channelstats' > $1-stats.txt; sleep 7; echo "/hangup"; sleep 2; echo "q") | \
docker run -i --rm --name lab-vq-alice --network lab-vq-wan --ip 10.88.10.21 --cap-add NET_ADMIN \
-v "$PWD/alice:/conf" -v "$PWD/sounds:/sounds:ro" -v "$PWD/rec-alice:/rec" \
lab-vq-tools:1 sh -c 'ip route add 10.88.20.0/24 via 10.88.10.2; exec baresip -f /conf' 2>&1 | tr '\r' '\n' > $1-alice.log
mkdir -p out/$1; cp rec-alice/*dec.wav out/$1/heard.wav
cat $1-stats.txt
EOF
chmod +x bob.sh link.sh call.shوأخيراً scenario.sh الذي يجمع كل ذلك في أمر واحد: يختار الترميز في المقسم، ويكتب إعداد الهاتفين، ويضبط الراوتر، ثم يجري المكالمة ويطبع أهم الأسطر:
cat > scenario.sh <<'EOF'
#!/bin/sh
# usage: ./scenario.sh <name> <ulaw|opus> <jitter buffer frames> [impair options]
name=$1 codec=$2 jb=$3; shift 3
sed -i "s/^allow=.*/allow=$codec/" asterisk/pjsip.conf
docker exec lab-vq-pbx asterisk -rx 'pjsip reload' >/dev/null
[ $codec = opus ] && src=speech48k || src=speech8k
./phones.sh $jb $src; ./bob.sh >/dev/null
./link.sh "$@" >/dev/null
./call.sh $name | grep -E "1002-"
./link.sh | sed "s/^/link: /"
grep -a -A8 "terminated" $name-alice.log | grep -E "lost|jitter"
EOF
chmod +x scenario.shالمكالمة السليمة: نقطة البداية
قبل أن نفسد شيئاً نحتاج أن نعرف أفضل درجة ممكنة في هذه التجربة، لذلك نبدأ براوتر لا يغير شيئاً:
./scenario.sh g711-clean ulaw 5-10
docker run --rm -v "$PWD:/w" -w /w lab-vq-pesq:1 python pesq/mos.py sounds/speech48k.wav out/g711-clean/heard.wavوالمخرج سوف يكون كما يلي، وقد أضفنا إليه إحصاءات المقسم كاملة وإحصاءات Alice من آخر سجلها:
...........Receive......... .........Transmit..........
BridgeId ChannelId ........ UpTime.. Codec. Count Lost Pct Jitter Count Lost Pct Jitter RTT....
===========================================================================================================
3130141f 1001-00000000 00:00:10 ulaw 489 0 0 0.005 493 0 0 0.006 0.000
3130141f 1002-00000001 00:00:10 ulaw 493 0 0 0.007 489 0 0 0.008 0.001
audio Transmit: Receive:
packets: 598 602
avg. bitrate: 64.0 64.4 (kbit/s)
errors: 0 0
pkt.report: 501 505
lost: 0 0
jitter: 7.1 7.5 (ms)
out/g711-clean/heard.wav NB 3.99 WB 2.89في المخرج أعلاه لاحظ التالي:
- جدول المقسم فيه سطر لكل طرف من المكالمة، وفي كل سطر ما استقبله المقسم وما أرسله، والفقد Lost ونسبته، والاهتزاز Jitter بالثواني، وزمن الذهاب والعودة RTT من تقارير RTCP.
- Alice استقبلت 602 حزمة دون فقد، باهتزاز 7.5 ms، وهو اهتزاز الأجهزة نفسها في Docker لأن الراوتر لم يضف شيئاً.
- درجة النطاق الضيق 3.99 هي سقف G.711 في هذه التجربة بهذه الجملة، وكل نتيجة بعد ذلك تقارن بها. ودرجة النطاق العريض 2.89 أقل لأن G.711 يقطع كل ما فوق 4 kHz من الصوت، وهذا ليس عطلاً وإنما حد الترميز نفسه.
ونجري المكالمة نفسها بترميز Opus، والمقسم هنا يمرر Opus كما هو دون تحويل Transcoding لأن الطرفين يستخدمانه:
./scenario.sh opus-clean opus 5-10out/opus-clean/heard.wav NB 4.12 WB 3.89وهنا يظهر سبب انتشار Opus: على شبكة سليمة، وبنحو 22 kbit/s فقط بدل 64، أعطى درجة نطاق عريض 3.89 مقابل 2.89 ل G.711، أي صوتاً أوضح بثلث ال Bandwidth تقريباً.
فقد الحزم: G.711 أمام Opus
الآن نجعل الراوتر يرمي 3% من حزم Bob، ثم 10%، مع كل ترميز:
./scenario.sh g711-loss3 ulaw 5-10 --loss 3
./scenario.sh opus-loss3 opus 5-10 --loss 3
./scenario.sh g711-loss10 ulaw 5-10 --loss 10
./scenario.sh opus-loss10 opus 5-10 --loss 10والمخرج لأول مكالمة سوف يكون كما يلي:
4f62e51b 1002-00000019 00:00:11 ulaw 464 7 1 0.009 450 0 0 0.010 0.001
link: {'in': 588, 'lost': 13, 'queue_drop': 0, 'out': 575}
lost: 0 12
jitter: 11.3 21.8 (ms)الراوتر رمى 13 حزمة من 588 مرت به، والمقسم رأى 7 مفقودة في منتصف المكالمة عندما سألناه، وAlice عدت 12 في نهايتها، والفرق بين الأرقام سببه أن كل عداد أخذ في لحظة مختلفة، وأن الراوتر يرى حزم SIP وتقارير RTCP أيضاً. ولأن الفقد عشوائي، فكل مكالمة تختلف عن الأخرى، لذلك كررنا كل حالة أربع مرات وأخذنا المتوسط:
| الحالة | G.711 NB | G.711 WB | Opus NB | Opus WB |
|---|---|---|---|---|
| شبكة سليمة | 3.99 | 2.89 | 4.12 | 3.89 |
| فقد 3% | 3.51 | 2.56 | 3.30 | 2.71 |
| فقد 10% | 2.48 | 1.84 | 2.43 | 1.88 |
وهذه النتيجة قد تبدو من الوهلة الأولى غريبة، فقد فعلنا FEC في Opus وطلبنا منه أن يتوقع فقداً بنسبة 10%، ومع ذلك خسر Opus ميزته كلها تقريباً، وأصبح مع الفقد في مستوى G.711. فهل Opus لا يتحمل الفقد؟ لا، والإجابة في SDP. قم بالتقاط رسائل SIP على المقسم أثناء مكالمة Opus:
docker exec -d lab-vq-pbx sh -c 'timeout 25 tcpdump -n -s0 -i eth0 -w /tmp/sdp.pcap port 5060'
./scenario.sh opus-sdp opus 5-10
docker exec lab-vq-pbx sh -c 'tcpdump -r /tmp/sdp.pcap -n -A 2>/dev/null | grep -E "IP 10|a=fmtp:(96|107)"'والمخرج سوف يكون كما يلي، بعد أن اختصرناه إلى رسائل المكالمة:
08:32:29.550715 IP 10.88.10.21.5060 > 10.88.10.10.5060: SIP: INVITE sip:[email protected] SIP/2.0
a=fmtp:96 stereo=1;sprop-stereo=1;maxaveragebitrate=24000;useinbandfec=1
08:32:29.555085 IP 10.88.10.10.5060 > 10.88.20.22.5060: SIP: INVITE sip:[email protected]:5060 SIP/2.0
a=fmtp:107 maxaveragebitrate=24000;sprop-stereo=1
08:32:29.760049 IP 10.88.20.22.5060 > 10.88.10.10.5060: SIP: SIP/2.0 200 Answering
a=fmtp:107 stereo=1;sprop-stereo=1;maxaveragebitrate=24000;useinbandfec=1Alice عرضت useinbandfec=1، ولكن المقسم عندما أنشأ الطرف الثاني من المكالمة نحو Bob، كما يفعل أي B2BUA شرحناه في مقال المصطلحات، كتب عرضاً جديداً ليس فيه هذه العلامة، فلم يعلم Bob أن الطرف الآخر يستطيع استخدام FEC. والمكالمة تعمل بشكل طبيعي في الحالتين، فلا يوجد خطأ في أي سجل، والفرق لا يظهر إلا عندما تفقد الحزم.
ولكي نتأكد أن FEC هو الفرق فعلاً، نجعل Alice تتصل ب Bob مباشرة دون المقسم، مرة مع FEC ومرة دونه، بفقد 10% في الحالتين، وهذا هو السكربت direct.sh:
cat > direct.sh <<'EOF'
#!/bin/sh
# usage: ./direct.sh <name> <yes|no: Opus in-band FEC> [impair options] Opus call straight from Alice to Bob, no PBX
name=$1 fec=$2; shift 2
./phones.sh 5-10 speech48k
sed -i "/g711.so/d; s/^opus_inbandfec.*/opus_inbandfec $fec/" alice/config bob/config
./bob.sh >/dev/null; ./link.sh "$@" >/dev/null
TARGET=sip:[email protected] ./call.sh $name >/dev/null
./link.sh | sed "s/^/link: /"
grep -a -A8 "terminated" $name-alice.log | grep -E "lost|jitter"
EOF
chmod +x direct.sh
./direct.sh d-fec-yes yes --loss 10
./direct.sh d-fec-no no --loss 10ومتوسط أربع مكالمات لكل حالة سوف يكون كما يلي:
| Opus مباشرة بفقد 10% | NB | WB |
|---|---|---|
| مع FEC متفق عليه في SDP | 3.48 | 3.13 |
| دون FEC | 2.61 | 2.04 |
بفقد 10% وصلت درجة Opus مع FEC إلى 3.48، أي قريباً من درجة G.711 بفقد 3%، والفقد نفسه دون FEC أعطى 2.61. لذلك لا تقم بالحكم على قدرة الترميز على تحمل الفقد من إعداد الهاتف وحده، والسبب أن FEC يحتاج اتفاق الطرفين في كل جزء من المكالمة، وكل B2BUA في الطريق قد يسقطه. افتح SDP في الالتقاط، وتأكد أن useinbandfec=1 موجود في العرض والرد على كل جزء.
الاهتزاز وحجم ال Jitter Buffer
الآن نترك الحزم كلها تصل، ولكن نؤخر كل حزمة 60 ms في المتوسط مع اهتزاز 20 ms، ونجرب ثلاثة أحجام لل Jitter Buffer في الهاتف: صغير جداً من حزمة إلى حزمتين، والافتراضي من 5 إلى 10، وكبير من 10 إلى 20:
./scenario.sh g711-jb1 ulaw 1-2 --delay 60 --jitter 20
./scenario.sh g711-jb5 ulaw 5-10 --delay 60 --jitter 20
./scenario.sh g711-jb10 ulaw 10-20 --delay 60 --jitter 20وهذا ما طبعته Alice في نهاية مكالمتين من التكرار الثاني، الأولى بالمخزن الصغير والثانية بالافتراضي:
audio Transmit: Receive:
packets: 601 602
avg. bitrate: 63.9 64.0 (kbit/s)
errors: 0 48
pkt.report: 501 506
lost: 0 -1
jitter: 3.3 25.6 (ms)
audio Transmit: Receive:
packets: 599 601
avg. bitrate: 64.0 64.2 (kbit/s)
errors: 0 0
pkt.report: 501 502
lost: 0 0
jitter: 3.5 24.3 (ms)الاهتزاز المقاس نحو 25 ms في الحالتين، وعداد الفقد يقول 0 أو حتى -1، لأن الحزم كلها وصلت فعلاً، والرقم السالب سببه أن بعضها وصل بغير ترتيبه. ولكن في المكالمة الأولى ظهر في عمود الاستقبال errors: 48، وهي حزم وصلت ولم يجد لها ال Jitter Buffer الصغير مكاناً في وقتها فرماها. وهذا ما تقوله الدرجات، بمتوسط أربع مكالمات لكل حالة:
| Jitter Buffer | بالملي ثانية | NB | WB |
|---|---|---|---|
| 1-2 حزمة | 20 إلى 40 | 2.73 | 2.25 |
| 5-10 حزم | 100 إلى 200 | 3.97 | 2.85 |
| 10-20 حزمة (مكالمة واحدة) | 200 إلى 400 | 3.99 | 2.89 |
في الجدول أعلاه لاحظ التالي:
- المخزن الصغير أنزل الدرجة من 3.99 إلى 2.73 في المتوسط، مع أن الشبكة لم تفقد حزمة واحدة، وتفاوتت مكالماته الأربع بين 2.20 و3.38 بحسب ما صادفته من اهتزاز. وهذا أخطر ما في الاهتزاز: المقسم يقول «لا يوجد فقد» والمستخدم يسمع صوتاً متقطعاً.
- المخزن الافتراضي امتص الاهتزاز كله تقريباً، فعادت الدرجة إلى سقفها.
- مضاعفة المخزن مرة أخرى لم تحسن شيئاً، لأنه لم يبق شيء يحتاج امتصاصاً، وكل ما فعلته أنها أضافت تأخيراً قد يصل إلى 400 ms، أي أسوأ من حد G.114 كله. لذلك لا تقم بتكبير ال Jitter Buffer لحل كل مشكلة صوت، والسبب أنه يحول التقطع إلى تأخير، والتأخير لا تقيسه PESQ ولكن يشعر به المتكلم.
جودة الخدمة QoS: من يمر أولاً؟
كل ما سبق أعطال تحدث في الإنترنت بين المكتب والمقسم، ولا تملك عليها سلطة كبيرة غير اختيار مزود أفضل أو مقسم أقرب. أما العطل الأخير فهو الأكثر حدوثاً في المكاتب، ويحدث في خط الإنترنت الخاص بك: الخط للرفع Upload أبطأ بكثير من التنزيل، وعندما يرفع موظف ملفاً كبيراً أو تبدأ النسخة الاحتياطية إلى السحابة، يمتلئ طابور الراوتر، وحزم الصوت الصغيرة تقف في الطابور نفسه خلف الحزم الكبيرة.
والحل له جزءان: أن تعلم كل حزمة صوت بعلامة تقول «أنا صوت»، وأن يكون عند أبطأ نقطة في الطريق جهاز يقرأ العلامة ويقدم صاحبها. والعلامة هي حقل DSCP في ترويسة IP، والقيم المتفق عليها في RFC 4594 هي EF، أي الرقم 46، للصوت، وهي فئة معرفة في RFC 3246 للحزم التي تحتاج أقل تأخير، وCS5 لرسائل التجهيز Signaling مثل SIP. وفي إعدادنا وضع baresip العلامة على صوته بالخيار rtp_tos 184، ووضعها Asterisk بالخيارين tos_audio=ef وtos=cs5، ولنتأكد من ذلك في الالتقاط:
docker exec -d lab-vq-pbx sh -c 'timeout 25 tcpdump -n -v -s0 -i eth0 -w /tmp/tos.pcap udp'
./scenario.sh tos ulaw 5-10
docker exec lab-vq-pbx sh -c 'tcpdump -r /tmp/tos.pcap -n -v 2>/dev/null | grep -A1 "tos 0xa0" | head -2'
docker exec lab-vq-pbx sh -c 'tcpdump -r /tmp/tos.pcap -n -v "src 10.88.20.22 and udp portrange 20000-20019" 2>/dev/null | head -2'
docker exec lab-vq-pbx sh -c 'tcpdump -r /tmp/tos.pcap -n -v "src 10.88.10.10 and udp portrange 10000-10099" 2>/dev/null | head -2'والمخرج سوف يكون كما يلي:
11:40:48.463704 IP (tos 0xa0, ttl 64, id 18702, offset 0, flags [DF], proto UDP (17), length 502)
10.88.10.10.5060 > 10.88.10.21.5060: SIP, length: 474
11:40:50.785105 IP (tos 0xb8, ttl 62, id 22600, offset 0, flags [DF], proto UDP (17), length 44)
10.88.20.22.20013 > 10.88.10.10.10063: UDP, length 16
11:40:50.887040 IP (tos 0xb8, ttl 64, id 18915, offset 0, flags [DF], proto UDP (17), length 200)
10.88.10.10.10060 > 10.88.10.21.20010: UDP, length 172رسائل SIP من المقسم تحمل 0xa0 وهي CS5، وصوت Bob وصوت المقسم يحملان 0xb8 وهي EF. ولاحظ أن ttl 62 في حزمة Bob يعني أن الراوتر وجهها مرتين، مرة إلى tun0 ومرة إلى المقسم. وبقيت رسائل SIP من الهاتفين دون علامة tos 0x0، لأن baresip يعلم الصوت فقط.
الآن نصنع الازدحام: الراوتر يصبح خطاً للرفع سرعته 512 kbit/s بطابور طوله 40 حزمة، وحاسوب في المكتب، وهو هنا ال Container نفسه الذي فيه Bob، يرفع 1 Mbit/s عبر iperf3 إلى المقسم أثناء المكالمة. ونجري المكالمة مرة بطابور واحد للجميع، ومرة بطابور مستقل للحزم المعلمة EF:
docker exec -d lab-vq-pbx iperf3 -s -p 5201
cat > qos.sh <<'EOF'
#!/bin/sh
# usage: ./qos.sh <name> [impair options] G.711 call while Bob's PC uploads 1 Mbit/s of UDP through the same uplink
name=$1; shift
./phones.sh 5-10 speech8k; ./bob.sh >/dev/null
./link.sh "$@" >/dev/null
docker exec -d lab-vq-bob iperf3 -c 10.88.10.10 -u -b 1M -l 1200 -t 30
./call.sh $name | grep -E "1002-"
./link.sh | sed "s/^/link: /"
grep -a -A8 "terminated" $name-alice.log | grep -E "lost|jitter"
EOF
chmod +x qos.sh
./qos.sh qos-fifo --rate 512 --queue 40
./qos.sh qos-prio --rate 512 --queue 40 --prioوالمخرج سوف يكون كما يلي:
e7fe573f 1002-00000005 00:00:09 ulaw 168 171 101 0.000 414 0 0 0.034 0.000
link: {'in': 3755, 'lost': 0, 'queue_drop': 1995, 'out': 1749}
lost: 0 327
jitter: 4.7 13.8 (ms)
16f75f5d 1002-00000007 00:00:10 ulaw 528 0 0 0.009 438 0 0 0.011 0.016
link: {'in': 3756, 'lost': 0, 'queue_drop': 1696, 'out': 2020}
lost: 0 0
jitter: 9.8 13.2 (ms)ولقياس التأخير الذي أضافه الطابور أرسلنا ping معلماً بالعلامة EF من حاسوب Bob أثناء الرفع، في الحالتين:
for m in "" "--prio"; do
./link.sh --rate 512 --queue 40 $m >/dev/null
docker exec -d lab-vq-bob iperf3 -c 10.88.10.10 -u -b 1M -l 1200 -t 15; sleep 3
echo "mode=${m:-fifo}"
docker exec lab-vq-bob ping -Q 0xb8 -c 20 -i 0.2 -q 10.88.10.10 | tail -2; sleep 10
done
./link.sh >/dev/nullmode=fifo
20 packets transmitted, 6 received, 70% packet loss, time 3951ms
rtt min/avg/max/mdev = 720.729/748.775/772.045/16.410 ms, pipe 4
mode=--prio
20 packets transmitted, 20 received, 0% packet loss, time 3815ms
rtt min/avg/max/mdev = 3.031/12.203/20.954/5.636 msوالصورة التالية تجمع النتيجة مع درجة PESQ لكل مكالمة:

في النتائج أعلاه لاحظ التالي:
- بطابور واحد ضاعت 327 حزمة من 562 حزمة صوت، أي أكثر من نصف الكلام، والتأخير قفز إلى نحو 750 ms لأن كل حزمة تنتظر 40 حزمة كبيرة قبلها، والدرجة 1.19، أي مكالمة لا يمكن فهمها.
- بطابور مستقل لل EF لم تضع حزمة صوت واحدة، وبقي التأخير نحو 12 ms، وعادت الدرجة إلى 3.99، مع أن الرفع نفسه ما زال يملأ الخط، والراوتر ما زال يرمي 1696 حزمة منه. أي أن QoS لا يزيد سرعة الخط، وإنما يقرر من يتحمل الازدحام.
- عمود Count في جدول المقسم للمكالمة الأولى يقول 168 مستقبلة و171 مفقودة بنسبة 101%، وهذا لأن التقارير نفسها تتأخر وتضيع في الازدحام، فلا تعتمد على دقة هذه الأرقام عندما يكون الخط مخنوقاً، وإنما على اتجاهها.
QoS في شبكتك الحقيقية
قد يتساءل البعض: إذا كانت الهواتف تعلم حزمها تلقائياً، فلماذا يشتكي المستخدمون؟ والإجابة أن العلامة مجرد رقم في الترويسة، ولا قيمة له إلا إذا قرأه الجهاز الذي عنده الطابور، وفي الواقع هناك عدة أماكن يضيع فيها:
- الراوتر في المكتب: هو المكان الأهم، لأن خط الرفع هو أبطأ نقطة في الغالب، وأغلب الراوترات المكتبية تحتاج أن تفعل فيها QoS بنفسك، أو تحدد فيها السرعة الحقيقية للخط أقل منها بقليل، حتى يتكون الطابور في الراوتر الذي تتحكم فيه وليس في جهاز المزود.
- السويتش Switch: بعض السويتشات تمسح العلامة أو تتجاهلها، فاقرأ إعداد الثقة Trust فيها، أو ضع الهواتف في VLAN مستقلة للصوت.
- Wi-Fi: الشبكة اللاسلكية لها طوابيرها الخاصة، وتربط قيم DSCP بفئات الوصول فيها، وأغلب مشاكل الصوت في التطبيقات على الجوال تبدأ من هنا.
- الإنترنت نفسه: مزودو الخدمة لا يحترمون علاماتك عادة، فلا تتوقع من EF أن تحمي مكالمتك بعد أن تخرج من مكتبك، والذي تملكه هو أن تجعل الطابور الوحيد المزدحم في الطريق عندك.
قبل أن تحكم على جودة مكالماتك
التجربة أعطتنا أرقاماً لكل عطل على حدة، وفي الإنتاج تأتي الأعطال مجتمعة، ولذلك اتبع هذا الترتيب عندما تصلك شكوى:
- اسأل عن الاتجاه: هل الذي يسمع الصوت المتقطع هو المتصل أم المستقبل؟ فالمشكلة غالباً في الطريق إلى من يسمع العيب.
- اقرأ إحصاءات المكالمة من الطرفين:
pjsip show channelstatsفي Asterisk أثناء المكالمة، وإحصاءات الهاتف نفسه، فالمقسم يعرف ما استقبله هو، والهاتف يعرف ما استقبله هو، وكما رأينا في المقال السابق كل طرف يرى نصف الحقيقة فقط. - إذا كان الفقد صفراً والصوت متقطعاً فابحث عن الاهتزاز وحزم ال Jitter Buffer المرمية، وليس عن الفقد.
- إذا كانت الشكوى في أوقات معينة من اليوم فابحث عن الازدحام في خط الرفع، وقارن بوقت النسخ الاحتياطي ومزامنة الملفات.
- افتح SDP في الالتقاط وتأكد من الترميز المتفق عليه فعلاً، ومن FEC إذا كنت تعتمد عليه، فما في إعداد الهاتف ليس بالضرورة ما اتفق عليه الطرفان.
جدول التشخيص السريع
| العرض | السبب الأرجح | أين تنظر |
|---|---|---|
| كلمات ناقصة وصوت معدني | فقد الحزم | عمود Lost في pjsip show channelstats وإحصاءات الهاتف |
| تقطع والفقد صفر | اهتزاز أكبر من ال Jitter Buffer | Jitter في الإحصاءات، وحزم مرمية في الهاتف مثل errors في baresip |
| الطرفان يتكلمان معاً ثم يسكتان | تأخير أكبر من 150 ms في اتجاه واحد | RTT في الإحصاءات، وحجم ال Jitter Buffer، وبعد المقسم |
| المكالمات تفسد عند رفع الملفات | ازدحام خط الرفع دون QoS | علامة tos 0xb8 في الالتقاط، وإعداد QoS في الراوتر |
| Opus لا يتحمل الفقد كما يجب | FEC لم يتفق عليه في جزء من المكالمة | useinbandfec=1 في SDP على كل جزء |
إزالة التجربة
docker rm -f lab-vq-alice lab-vq-bob lab-vq-router lab-vq-pbx
docker network rm lab-vq-wan lab-vq-lan
docker rmi lab-vq-tools:1 lab-vq-pesq:1تدريب عملي
- أعد تجربة الاهتزاز بالمخزن الصغير مع ترميز Opus بدل G.711، وقارن الدرجة، ثم فسر النتيجة من سجل Alice.
- في تجربة الازدحام اجعل سرعة الخط 128 kbit/s مع
--prio، واحسب كم مكالمة G.711 يتحملها الخط قبل أن يمتلئ طابور الصوت نفسه.
الخلاصة
- الصوت يحتاج أن يصل بالإيقاع الذي خرج به، والتأخير والاهتزاز والفقد والازدحام أربعة أشكال لكسر هذا الإيقاع، ولكل منها عرض مختلف عند المستخدم.
- قس الجودة بما وصل فعلاً: درجة MOS من PESQ أو POLQA، أو تقدير E-model من أرقام الشبكة، ولا تكتف بأن المكالمة «اتصلت».
- على شبكة سليمة أعطى Opus صوتاً أوضح من G.711 بنحو ثلث ال Bandwidth، ولكنه بالفقد لم يتفوق إلا عندما اتفق الطرفان على FEC، والمقسم في الوسط أسقط هذا الاتفاق دون أي خطأ.
- ال Jitter Buffer الصغير يرمي حزماً وصلت سليمة، والكبير يضيف تأخيراً، فاختر حجمه من الاهتزاز الذي تقيسه وليس من الرغبة في إخفاء كل مشكلة.
- QoS لا يزيد سرعة الخط، وإنما يقرر من يتحمل الازدحام، والعلامة EF لا تفيد إلا إذا قرأها الراوتر عند أبطأ نقطة في الطريق، وهي غالباً خط الرفع في مكتبك.
وفي المقال القادم من السلسلة بإذن الله نخرج من شبكة المكتب إلى شبكة الهاتف العامة: نربط المقسم بمزود خدمة عبر SIP Trunk، ونوجه الأرقام المباشرة DID إلى التحويلات.
دليلمصطلحات الاتصالات الهاتفية VoIP للمبتدئينشرح مبسط لمصطلحات الهاتف عبر الإنترنت، وما يمر في الشبكة بين ضغط زر الاتصال ووصول الصوت إلى الطرف الآخر.
دليلتتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟نبني مقسماً وهاتفين خلف NAT في تجربة عملية، ونتتبع المكالمة حزمة بحزمة حتى نعرف لماذا يضيع الصوت وكيف نصلحه.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وبناء التجربة على Asterisk 20.6.0 وbaresip 1.0.0 من حزم Ubuntu 24.04 في Docker، مع راوتر يضيف التأخير والفقد بأداة في مساحة المستخدم بدل tc netem، وقياس الجودة بمكتبة pesq 0.0.4.