الأدلة التقنية

لماذا يتقطع الصوت في مكالمات VoIP؟ قياس Jitter وPacket Loss

نفسد الشبكة عمداً بين هاتفين ومقسم Asterisk، ونقيس بدرجة MOS أثر التأخير والفقد على الصوت، وكيف يحمي QoS المكالمة.

لماذا يتقطع الصوت في مكالمات VoIP؟ قياس Jitter وPacket Loss

في تتبع مكالمة SIP عبر الشبكة أصلحنا المكالمة التي لا يصل فيها الصوت أصلاً، أي مشكلة «هل يصل الصوت؟». أما الشكوى التي سوف تسمعها أكثر بعد ذلك فهي من نوع آخر: الصوت يصل، ولكنه يتقطع، أو يأتي كأنه من آلة، أو يتأخر فيتكلم الطرفان في الوقت نفسه ثم يسكتان معاً. وهذه المشكلات لا تظهر في إعداد المقسم، وإنما في الشبكة بين الهاتف والمقسم، ولا يمكن أن تصلحها إذا لم تستطع قياسها.

المواضيع في السلسلة:

وفي هذا المقال لن نكتفي بتعريف المصطلحات، وإنما سوف نبني تجربة نفسد فيها الشبكة عمداً بين هاتف ومقسم 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 في تخطيط الشبكات. والصورة التالية تبين مثالين:

شريطان أفقيان يجمعان التأخير من الفم إلى الأذن: مكالمة في المدينة نفسها 25 ms للترميز والحزمة و30 ms للشبكة و60 ms لل Jitter Buffer و5 ms للتشغيل بمجموع 120 ms دون خط 150 ms، ومكالمة عبر مركز بيانات بعيد وشبكة Wi-Fi سيئة 25 و120 و120 و5 بمجموع 270 ms بعد خط 150 ms من توصية ITU-T G.114
ال Jitter Buffer جزء من التأخير، فكل زيادة فيه تأخذ من الحوار

ولاحظ في المثال الثاني أن الشبكة وحدها لم تتجاوز الحد، ولكن الشبكة السيئة جعلت الهاتف يكبر ال Jitter Buffer، فتجاوز المجموع الحد. وهذا يقودنا إلى العطل الثاني.

الاهتزاز Jitter وال Jitter Buffer

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

والحل هو ال Jitter Buffer: مخزن صغير في الهاتف المستقبل يحتفظ بالحزم مدة قصيرة قبل تشغيلها، فيعيد ترتيبها ويشغلها بالإيقاع الصحيح. وهنا المقايضة كلها: المخزن الصغير يعطي تأخيراً قليلاً ولكنه يرمي كل حزمة تصل بعد موعد تشغيلها، والمخزن الكبير يشغل كل الحزم ولكنه يؤخر المكالمة كلها، كما تبين الصورة التالية:

ثماني حزم خرجت بفواصل منتظمة كل 20 ms ووصلت بفواصل غير منتظمة، والحزمة 3 متأخرة والحزمة 5 وصلت بعد 6، ومع Jitter Buffer صغير تظهر فراغات مكان الحزمتين 3 و5 لأنهما وصلتا بعد موعد تشغيلهما، ومع 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:

شبكة مركز البيانات lab-vq-wan فيها حاسوب Alice بالتحويلة 1001 على 10.88.10.21 تسجل ما تسمعه في heard.wav ومقسم Asterisk على 10.88.10.10، وشبكة المكتب lab-vq-lan فيها هاتف Bob بالتحويلة 1002 على 10.88.20.22 يشغل جملة منطوقة من speech.wav، وبينهما راوتر بعنوانين 10.88.10.2 و10.88.20.254 يشغل الأداة impair بخيارات delay وjitter وloss وrate وqueue وprio
نفسد اتجاهاً واحداً فقط، من Bob إلى المقسم، فتسمع 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-10
out/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 NBG.711 WBOpus NBOpus WB
شبكة سليمة3.992.894.123.89
فقد 3%3.512.563.302.71
فقد 10%2.481.842.431.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=1

Alice عرضت 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%NBWB
مع FEC متفق عليه في SDP3.483.13
دون FEC2.612.04

بفقد 10% وصلت درجة Opus مع FEC إلى 3.48، أي قريباً من درجة G.711 بفقد 3%، والفقد نفسه دون FEC أعطى 2.61. لذلك لا تقم بالحكم على قدرة الترميز على تحمل الفقد من إعداد الهاتف وحده، والسبب أن FEC يحتاج اتفاق الطرفين في كل جزء من المكالمة، وكل B2BUA في الطريق قد يسقطه. افتح SDP في الالتقاط، وتأكد أن useinbandfec=1 موجود في العرض والرد على كل جزء.

💡
الأرقام أعلاه من جملة واحدة مولدة بالحاسوب ومن فقد عشوائي منتظم، والفقد في الشبكات الحقيقية يأتي غالباً على شكل دفعات Bursts، وهو أصعب على FEC. استخدم الأرقام للمقارنة بين الحالات في التجربة نفسها، وليس كوعد بدرجة معينة على شبكتك.

الاهتزاز وحجم ال 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بالملي ثانيةNBWB
1-2 حزمة20 إلى 402.732.25
5-10 حزم100 إلى 2003.972.85
10-20 حزمة (مكالمة واحدة)200 إلى 4003.992.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/null
mode=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 لكل مكالمة:

حالتان لخط رفع سرعته 512 kbit/s يشترك فيه رفع ملف من حاسوب ومكالمة G.711: دون QoS طابور واحد تقف فيه حزم الصوت بين الحزم الكبيرة فضاعت 327 حزمة صوت من 562 وكان التأخير نحو 750 ms ودرجة MOS 1.19، ومع QoS طابور مستقل للحزم المعلمة EF برقم DSCP 46 يرسل أولاً فلم تضع أي حزمة صوت وكان التأخير نحو 12 ms والدرجة 3.99
العلامة وحدها لا تفعل شيئاً، والفرق يصنعه الجهاز الذي يقرؤها عند أبطأ نقطة

في النتائج أعلاه لاحظ التالي:

  • بطابور واحد ضاعت 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 أن تحمي مكالمتك بعد أن تخرج من مكتبك، والذي تملكه هو أن تجعل الطابور الوحيد المزدحم في الطريق عندك.
⚠️
لا تقم بإعطاء الأولوية لكل ما يحمل EF من أي جهاز دون حد أعلى، والسبب أن أي برنامج يستطيع أن يعلم حزمه EF ليمر قبل غيره. الطابور ذو الأولوية يجب أن يكون له حد من سرعة الخط، يكفي عدد المكالمات المتوقع في الوقت نفسه، وما زاد عليه يعامل كبقية الحزم.

قبل أن تحكم على جودة مكالماتك

التجربة أعطتنا أرقاماً لكل عطل على حدة، وفي الإنتاج تأتي الأعطال مجتمعة، ولذلك اتبع هذا الترتيب عندما تصلك شكوى:

  1. اسأل عن الاتجاه: هل الذي يسمع الصوت المتقطع هو المتصل أم المستقبل؟ فالمشكلة غالباً في الطريق إلى من يسمع العيب.
  2. اقرأ إحصاءات المكالمة من الطرفين: pjsip show channelstats في Asterisk أثناء المكالمة، وإحصاءات الهاتف نفسه، فالمقسم يعرف ما استقبله هو، والهاتف يعرف ما استقبله هو، وكما رأينا في المقال السابق كل طرف يرى نصف الحقيقة فقط.
  3. إذا كان الفقد صفراً والصوت متقطعاً فابحث عن الاهتزاز وحزم ال Jitter Buffer المرمية، وليس عن الفقد.
  4. إذا كانت الشكوى في أوقات معينة من اليوم فابحث عن الازدحام في خط الرفع، وقارن بوقت النسخ الاحتياطي ومزامنة الملفات.
  5. افتح SDP في الالتقاط وتأكد من الترميز المتفق عليه فعلاً، ومن FEC إذا كنت تعتمد عليه، فما في إعداد الهاتف ليس بالضرورة ما اتفق عليه الطرفان.

جدول التشخيص السريع

العرضالسبب الأرجحأين تنظر
كلمات ناقصة وصوت معدنيفقد الحزمعمود Lost في pjsip show channelstats وإحصاءات الهاتف
تقطع والفقد صفراهتزاز أكبر من ال Jitter BufferJitter في الإحصاءات، وحزم مرمية في الهاتف مثل 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.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.