Relay a Dante multicast stream to AES67. This assumes the AES67 device is synced to the same PTP master, as no PTP timestamping is done (timestamp from Dante is copied to AES67 RTP packet)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const dgram = require('dgram'); | |
const client = dgram.createSocket({ type: 'udp4', reuseAddr: true }); | |
const sdp = require('./sdp'); | |
//config | |
const addr = '10.10.1.100'; | |
const danteMulticast = '239.255.220.221'; | |
const aes67Multicast = '239.69.1.122'; | |
const samplerate = 48000; | |
const channels = 2; | |
const encoding = 'L24'; | |
const name = 'Dante Multicast Relay'; | |
const sessID = Math.round(Date.now() / 1000); | |
const sessVersion = sessID; | |
const ptpMaster = '08-00-00-ff-fe-00-00-1f:0'; | |
//rtp specific vars | |
var seqNum = 0; | |
client.on('listening', function() { | |
client.addMembership(danteMulticast, addr); | |
client.setMulticastInterface(addr); | |
}); | |
client.on('message', function(buffer, remote) { | |
//read values from buffer | |
var channelCount = buffer.readUInt8(0); | |
var timestampSeconds = buffer.readUInt32BE(1); | |
//bytes 6 and 7 seem to be always 0x00, maybe reserved bytes | |
var timestampMedia = buffer.readUInt16BE(7); | |
var pcmData = buffer.slice(9); | |
//calculate media timestamp for rtp | |
var timestampRTP = ((timestampSeconds * samplerate) + timestampMedia) & 0xffffffff; | |
//create RTP header | |
var rtpHeader = Buffer.alloc(12); | |
rtpHeader.writeUInt16BE(0x8061, 0); | |
rtpHeader.writeUInt16BE(seqNum, 2); | |
rtpHeader.writeInt32BE(timestampRTP, 4); | |
rtpHeader.writeUInt32BE(0xaf12af34, 8); | |
//create and send RTP packet | |
var rtpBuffer = Buffer.concat([rtpHeader, pcmData]); | |
client.send(rtpBuffer, 5004, aes67Multicast); | |
//increase seqnum | |
seqNum = (seqNum + 1) % 65536; | |
}); | |
client.bind(4321); | |
sdp.start(addr, aes67Multicast, samplerate, channels, encoding, name, sessID, sessVersion, ptpMaster); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This works excellently with Dante VSC (by far the most reliable windows driver I've found). As mmoduu said, you'll have to change the payload type to 97 and at least for me, when I'm trying to receive an 8 ch stream with the Merging ALSA driver, I have to map it to 8 channels.
Using an AVIO as the boundary clock between PTPv1 and v2. You can probably do this yourself or there's a project on here that describes how to intercept and then 'spawn' Dante VSC's PTP process to connect to a v2 source. It's for Mac but the premise should in theory work for Windows too if you re-write the code.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is amazing! It works very smooth with my i7-7567U. I have an AES67 box that acts as a PTPv2 master, Audinate AVIO dongle on the network, and this same computer runs Dante Virtual Soundcard.. I routed this computers Dante to the AVIO and then used that stream to be replicated with your script. Only issue was that the payload type was not correct on the SDP (RTP stream had payload type 97, but SDP told it was 96), but that was an easy fix of course.