Skip to content

Instantly share code, notes, and snippets.

@philhartung
Last active March 8, 2022 16:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save philhartung/d2d26e02ac2c1f870ca79fc9c06d225c to your computer and use it in GitHub Desktop.
Save philhartung/d2d26e02ac2c1f870ca79fc9c06d225c to your computer and use it in GitHub Desktop.
PTPv2 client and RTP relay with timestamping for AES67. Also includes SAP/SDP.
var sdp = require('./sdp');
//SDP Stuff
var samplerate = 48000;
var channels = 2;
var multicastAddr = '239.69.0.111';
var addr = '192.168.1.1';
var encoding = 'L24';
var name = 'RPi 4';
var sessID = Math.floor(Date.now() / 1000);
var sessVersion = sessID;
var ptpMaster;
var sync = false;
//ptp settings
var ptp_domain = 0;
var startSDP = function(){
sdp.start(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster);
}
//RTP Server
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
server.on('listening', function() {
server.setMulticastInterface(addr);
console.log('[RTP] relay server started');
});
server.on('message', function(message, remote) {
if(!sync)
return;
message.writeInt32BE(Math.ceil(rtp_timestamp() / 48)*48 + 48, 4);
server.send(message, 5004, multicastAddr);
});
server.bind(5003);
//PTPv2
var ptpClientEvent = dgram.createSocket('udp4');
var ptpClientGeneral = dgram.createSocket('udp4');
var ptpMulticastAddrs = ['224.0.1.129', '224.0.1.130', '224.0.1.131', '224.0.1.132'];
//vars
var t1, ts1, t2, ts2;
var offset = [0, 0];
var sync_seq;
var req_seq = 0;
//functions
//creates ptp delay_req buffer
var ptp_delay_req = function(){
var length = 52;
var buffer = Buffer.alloc(length);
buffer.writeUInt8(1, 0);
buffer.writeUInt8(2, 1);
buffer.writeUInt16BE(length, 2);
buffer.writeUInt16BE(++req_seq, 30);
return buffer;
}
//convert PTP timestamp to rtp timestamp
var rtp_timestamp = function(){
var time = ptp_time();
return ((time[0] * samplerate) + Math.round((time[1] * samplerate) / 1000000000)) & 0xffffffff;
}
//calculated ptp time
var ptp_time = function(){
var time = process.hrtime();
var timeS = time[0] - offset[0];
var timeNS = time[1] - offset[1];
return [timeS, timeNS];
}
//event msg client
ptpClientEvent.on('listening', function() {
console.log('[PTP] event msg client started');
ptpClientEvent.addMembership(ptpMulticastAddrs[ptp_domain], addr);
});
ptpClientEvent.on('message', function(buffer, remote) {
var recv_ts = process.hrtime();//safe timestamp for ts1
//read values from buffer
var type = buffer.readUInt8(0) & 0x0f;
var version = buffer.readUInt8(1);
var length = buffer.readUInt16BE(2);
var domain = buffer.readUInt8(4);
var flags = buffer.readUInt16BE(6);
var source = buffer.toString('hex', 20, 28).match(/.{1,2}/g).join('-')+':0';
var sourceAlt = buffer.toString('hex', 20, 28).match(/.{1,2}/g).join(':');
var sequence = buffer.readUInt16BE(30);
if(version != 2 || domain != ptp_domain)//check for version 2 and domain 0
return;
if(type != 0)//only process sync messages
return;
//do we have a new ptp master?
if(source != ptpMaster){
ptpMaster = source;
console.log('[PTP] New master', sourceAlt, 'on domain', ptp_domain);
sync = false;
}
//save sequence number
sync_seq = sequence;
//check if master is two step or not
if((flags & 0x0200) == 0x0200){
//two step, wait for follow_up msg for accurate t1
ts1 = recv_ts;
}else{
//got accurate t1 (no follow_up msg)
ts1 = recv_ts;
//read t1 timestamp
var tsS = (buffer.readUInt16BE(34) << 4) + buffer.readUInt32BE(36);
var tsNS = buffer.readUInt32BE(40);
t1 = [tsS, tsNS];
//send delay_req
ptpClientEvent.send(ptp_delay_req(), 319, ptpMulticastAddrs[ptp_domain], function(err){
t2 = process.hrtime();
});
}
});
ptpClientEvent.bind(319, ptpMulticastAddrs[ptp_domain]);
//general msg Client
ptpClientGeneral.on('listening', function() {
console.log('[PTP] general msg client started');
ptpClientGeneral.addMembership(ptpMulticastAddrs[ptp_domain], addr);
});
ptpClientGeneral.on('message', function(buffer, remote) {
//safe timestamp for ts2
var recv_ts = process.hrtime();
//read values from buffer
var type = buffer.readUInt8(0) & 0x0f;
var version = buffer.readUInt8(1);
var length = buffer.readUInt16BE(2);
var domain = buffer.readUInt8(4);
var flags = buffer.readUInt16BE(6);
var source = buffer.toString('hex', 20, 28).match(/.{1,2}/g).join('-')+':0';
var sequence = buffer.readUInt16BE(30);
//check for version 2 and domain
if(version != 2 || domain != ptp_domain)
return;
if(type == 0x08 && sync_seq == sequence){ //follow up msg with current seq
//read t1 timestamp
var tsS = (buffer.readUInt16BE(34) << 4) + buffer.readUInt32BE(36);
var tsNS = buffer.readUInt32BE(40);
t1 = [tsS, tsNS];
//send delay_req
ptpClientEvent.send(ptp_delay_req(), 319, ptpMulticastAddrs[ptp_domain], function(err){
t2 = process.hrtime();
});
}else if(type == 0x09 && req_seq == sequence){ //delay_rsp msg
//read ts2 timestamp
var tsS = (buffer.readUInt16BE(34) << 4) + buffer.readUInt32BE(36);
var tsNS = buffer.readUInt32BE(40);
ts2 = [tsS, tsNS];
//calc offset
offset[0] = 0.5 * (ts1[0] - t1[0] - ts2[0] + t2[0]);
offset[1] = 0.5 * (ts1[1] - t1[1] - ts2[1] + t2[1]);
//check if the clock was synced before
if(!sync){
console.log('[PTP] Synced to Master');
sync = true;
//start sdp
startSDP();
}
}
});
ptpClientGeneral.bind(320, ptpMulticastAddrs[ptp_domain]);
var dgram = require('dgram');
var socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
var constructSDPMsg = function(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster){
var sapHeader = Buffer.alloc(8);
var sapContentType = Buffer.from('application/sdp\0');
var ip = addr.split('.');
//write version/options
sapHeader.writeUInt8(0x20);
//write hash
sapHeader.writeUInt16LE(0xefef, 2);
//write ip
sapHeader.writeUInt8(parseInt(ip[0]), 4);
sapHeader.writeUInt8(parseInt(ip[1]), 5);
sapHeader.writeUInt8(parseInt(ip[2]), 6);
sapHeader.writeUInt8(parseInt(ip[3]), 7);
var sdpConfig = [
'v=0',
'o=- '+sessID+' '+sessVersion+' IN IP4 '+addr,
's='+name,
'c=IN IP4 '+multicastAddr+'/32',
't=0 0',
'a=clock-domain:PTPv2 0',
'm=audio 5004 RTP/AVP 96',
'a=rtpmap:96 '+encoding+'/'+samplerate+'/'+channels,
'a=sync-time:0',
'a=framecount:48',
'a=ptime:1',
'a=mediaclk:direct=0',
'a=ts-refclk:ptp=IEEE1588-2008:'+ptpMaster,
'a=recvonly',
''
];
var sdpBody = Buffer.from(sdpConfig.join('\r\n'));
return Buffer.concat([sapHeader, sapContentType, sdpBody]);
}
exports.start = function(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster){
sdpMSG = constructSDPMsg(addr, multicastAddr, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster);
socket.bind(9875, function(){
socket.setMulticastInterface(addr);
socket.send(sdpMSG, 9875, '239.255.255.255', function(err){});
});
setInterval(function(){
socket.send(sdpMSG, 9875, '239.255.255.255', function(err){});
}, 30*1000);
}
#!/bin/sh
gst-launch-1.0 alsasrc device=hw:1 buffer-time=1000 !\
audioconvert !\
audio/x-raw, format=S24BE, channels=2, rate=48000 !\
rtpL24pay min-ptime=1000000 max-ptime=1000000 !\
application/x-rtp, clock-rate=48000, channels=2 !\
udpsink host=192.168.1.1 port=5003
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment