Initial commit
This commit is contained in:
commit
b7d90b9afe
16245 files changed
+3396666
No files matched your search
Executable
+262
@@ -0,0 +1,262 @@
|
||||
#! /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()
|
||||
Reference in new issue
Block a user