Files
qt5-legatus-legacy/Lts/Tools/rtp_sender.py
T
2026-08-13 18:33:10 +02:00

263 lines
8.6 KiB
Python
Executable File

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# @RDE_HEADER@
# $Id$
# $HeadURL$
# $Author$
# Created by Christian Fröbel, NLE, 2019-01-29
#
import ctypes
import socket
import sys
import textwrap
import time
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from collections import namedtuple
from contextlib import ExitStack, contextmanager
from queue import Empty, Queue
from threading import Thread
_G711_PAYLOAD_TYPE = 0x08
@contextmanager
def autoShutdown(target):
try:
yield target
finally:
target.initShutdown()
@contextmanager
def autoJoin(thread):
try:
yield thread
finally:
thread.join()
@contextmanager
def startThreadContext(target, thread):
with ExitStack() as stack:
thread.start()
stack.enter_context(autoJoin(thread))
stack.enter_context(autoShutdown(target))
yield
class _RtpHeaderStruct(ctypes.BigEndianStructure):
_pack_ = 1
_fields_ = [
('version', ctypes.c_uint, 2),
('padding', ctypes.c_uint, 1),
('extension', ctypes.c_uint, 1),
('csrcCount', ctypes.c_uint, 4),
('marker', ctypes.c_uint, 1),
('payloadType', ctypes.c_uint, 7),
('sequenceNumber', ctypes.c_uint, 16),
('timestamp', ctypes.c_uint, 32),
('ssrc', ctypes.c_uint, 32)
]
class _RtpHeader(object):
def __init__(self, payloadType, sequenceNumber, timestamp, ssrc):
self._buf = bytearray(ctypes.sizeof(_RtpHeaderStruct))
self._header = _RtpHeaderStruct.from_buffer(self._buf)
self._setFixedValues()
self._header.payloadType = payloadType
self._header.sequenceNumber = sequenceNumber
self._header.timestamp = timestamp
self._header.ssrc = ssrc
@property
def sequenceNumber(self):
return self._header.sequenceNumber
@sequenceNumber.setter
def sequenceNumber(self, no):
self._header.sequenceNumber = no
@property
def timestamp(self):
return self._header.timestamp
@timestamp.setter
def timestamp(self, t):
self._header.timestamp = t
@property
def buffer(self):
return self._buf
def _setFixedValues(self):
self._header.version = 2
self._header.padding = 0
self._header.extension = 0
self._header.csrcCount = 0
self._header.marker = 0
class _PacketBuilder(object):
def __init__(self, ssrc):
self._seqNo = 0
self._timestamp = 0
self._header = _RtpHeader(_G711_PAYLOAD_TYPE, self._seqNo, self._timestamp, ssrc)
def build(self, payload):
self._header.sequenceNumber = self._seqNo
self._header.timestamp = self._timestamp
packet = self._header.buffer + payload
self._seqNo += 1
self._timestamp += len(payload) # given that one sample is represented as one byte (which is true for G.711)
return packet
class _Reader(object):
def __init__(self, pathToDataFile, chunkSize, dataQueue):
assert chunkSize > 0
self._pathToDataFile = pathToDataFile
self._chunkSize = chunkSize
self._dataQueue = dataQueue
self._doRun = True
def run(self):
while self._doRun:
lastChunk = bytes()
lastChunk = self._readFile(lastChunk)
def initShutdown(self):
self._doRun = False
def _readFile(self, lastChunk):
with open(self._pathToDataFile, 'rb') as inputStream:
wasIncomplete = len(lastChunk) < self._chunkSize
if wasIncomplete:
restCount = self._chunkSize - len(lastChunk)
restChunk = inputStream.read(restCount)
chunk = lastChunk + restChunk
if len(chunk) < self._chunkSize:
return chunk
self._dataQueue.put(chunk)
while self._doRun:
chunk = inputStream.read(self._chunkSize)
if len(chunk) < self._chunkSize:
return chunk
self._dataQueue.put(chunk)
class _Sender(object):
def __init__(self, target):
self._dest = (target.host, target.port)
self._socket = socket.socket(type=socket.SOCK_DGRAM)
def send(self, buf):
self._socket.sendto(buf, self._dest)
class _QueueSender(object):
def __init__(self, senders, dataQueue, packetBuilder, chunkSize):
self._senders = senders
self._dataQueue = dataQueue
self._packetBuilder = packetBuilder
self._doRun = True
self._nextTick = None
self._tick = chunkSize/8000 # für G.711, 8kHz
def run(self):
while True:
try:
chunk = self._dataQueue.get(timeout=1)
except Empty:
if self._doRun:
continue
else:
break
self._send(chunk)
self._dataQueue.task_done()
def initShutdown(self):
self._doRun = False
def _send(self, chunk):
packet = self._packetBuilder.build(chunk)
if self._nextTick is None:
self._nextTick = time.monotonic() + self._tick
self._sendNowToAll(packet)
return
waitFor = self._nextTick - time.monotonic()
assert waitFor >= 0, 'cannot send fast enough'
time.sleep(waitFor)
self._sendNowToAll(packet)
self._nextTick += self._tick
def _sendNowToAll(self, chunk):
for sender in self._senders:
sender.send(chunk)
_Target = namedtuple('_Target', ('host', 'port'))
def _createTarget(s, defaultPort):
parts = s.split(':')
assert len(parts) <= 2, 'invalid format for a target: {}'.format(s)
if len(parts) == 1:
return _Target(parts[0], defaultPort)
else:
return _Target(parts[0], int(parts[1]))
def _createTargets(targetDefs, defaultPort):
return [_createTarget(t, defaultPort) for t in targetDefs]
def _parseCmdLine():
parser = ArgumentParser(description=textwrap.dedent('''
The program reads the given raw audio data file and sends RTP packets of the given size
to the target hosts (can be more than one). When tha data file is exhausted, it starts
reading from the front again until Ctrl-C is pressed.
The data file must be raw G.711 data, 8kHz.
Given a sound file (e.g. wav or ogg), a raw G.711 file at 8kHz can be generated using gstreamer
in the following way:
$ gst-launch-1.0 filesrc location=<path to sound file> ! decodebin ! audioconvert ! audio/x-raw, format=S16LE, layout=interleaved ! \\
audioresample ! audio/x-raw, rate=8000 ! alawenc ! filesink location=g711.raw
Alternatively you can also stream the data files stored under /data/AVS for some radio channel;
they are stored in the required format.
'''),
formatter_class=RawDescriptionHelpFormatter)
parser.add_argument('--target', action='append', dest='targets', required=True, help='destination in the form host[:port]; can be given multiple times')
parser.add_argument('--port', '-p', type=int, metavar='N', default=8001, help='destination port if not specified in target definition; default: 8001')
parser.add_argument('--file', '-f', metavar='PATH', required=True, help='path to the data file')
parser.add_argument('--ssrc', type=int, metavar='N', required=True, help='SSRC to be used')
parser.add_argument('--chunk-size', '-s', type=int, metavar='N', default=320, help='size of the playload to use for each rtp packet; default: 320')
options = parser.parse_args()
assert len(options.targets) > 0, 'Need at least one target definition'
options.targets=_createTargets(options.targets, options.port)
del options.port
return options
def _main():
options = _parseCmdLine()
dataQueue = Queue(maxsize=25)
reader = _Reader(options.file, options.chunk_size, dataQueue)
readerThread = Thread(target=reader.run)
senders = [_Sender(target) for target in options.targets]
queueSender = _QueueSender(senders, dataQueue, _PacketBuilder(options.ssrc), options.chunk_size)
senderThread = Thread(target=queueSender.run)
with ExitStack() as stack:
stack.enter_context(startThreadContext(queueSender, senderThread))
stack.enter_context(startThreadContext(reader, readerThread))
while True:
try:
time.sleep(0.5)
except KeyboardInterrupt:
break
if __name__ == '__main__':
_main()