| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "fpp-pch.h" | ||
| 2 | |||
| 3 | #include "fpp-json.h" | ||
| 4 | |||
| 5 | #include "../Warnings.h" | ||
| 6 | #include "../log.h" | ||
| 7 | |||
| 8 | #include "GenericUDPOutput.h" | ||
| 9 | #include "UDPOutput.h" | ||
| 10 | #include "ChannelOutputSetup.h" | ||
| 11 | |||
| 12 | #include "Plugin.h" | ||
| 13 | class GenericUDPPlugin : public FPPPlugins::Plugin, public FPPPlugins::ChannelOutputPlugin { | ||
| 14 | public: | ||
| 15 | 49 | GenericUDPPlugin() : | |
| 16 |
2/4✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
|
147 | FPPPlugins::Plugin("GenericUDP") { |
| 17 | 49 | } | |
| 18 | 56 | virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) override { | |
| 19 |
2/6✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 56 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
112 | return new GenericUDPOutput(startChannel, channelCount); |
| 20 | } | ||
| 21 | }; | ||
| 22 | |||
| 23 | extern "C" { | ||
| 24 | 49 | FPPPlugins::Plugin* createPlugin() { | |
| 25 |
2/6✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 49 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
98 | return new GenericUDPPlugin(); |
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | static const std::string GENERIC_UDP_TYPE = "GenericUDP"; | ||
| 30 | |||
| 31 | class GenericUDPOutputData : public UDPOutputData { | ||
| 32 | public: | ||
| 33 | 52 | GenericUDPOutputData(const Json::Value& config) : | |
| 34 | UDPOutputData(config), | ||
| 35 | 52 | count(0), | |
| 36 | 52 | isMulticast(false), | |
| 37 | 52 | isBroadcast(false) { | |
| 38 |
2/4✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
|
52 | tokens = config["tokens"].asString(); |
| 39 |
3/4✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✓ Branch 3 taken 2 times.
|
52 | port = config["port"].asInt(); |
| 40 | |||
| 41 | // s_addr is left unset when the output is inactive, so it has to start | ||
| 42 | // zeroed: the address classification below reads it either way. | ||
| 43 | 50 | memset(&udpAddress, 0, sizeof(udpAddress)); | |
| 44 | 50 | udpAddress.sin_family = AF_INET; | |
| 45 | 50 | udpAddress.sin_port = htons(port); | |
| 46 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | if (active) { |
| 47 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | udpAddress.sin_addr.s_addr = toInetAddr(ipAddress, valid); |
| 48 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 50 times.
|
50 | if (!valid) { |
| 49 | ✗ | AddOutputConfigWarning(14, "Could not resolve host name " + ipAddress + " - disabling output"); | |
| 50 | ✗ | active = false; | |
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | // s_addr is network byte order, so the octets have to be read out of the | ||
| 55 | // host-order value: on a little-endian host the top byte of the raw word | ||
| 56 | // is the LAST octet, not the first. | ||
| 57 | 50 | uint32_t hostAddr = ntohl(udpAddress.sin_addr.s_addr); | |
| 58 | 50 | int firstOctet = (hostAddr >> 24) & 0xFF; | |
| 59 | 50 | int lastOctet = hostAddr & 0xFF; | |
| 60 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 48 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
50 | if (firstOctet >= 224 && firstOctet <= 239) { |
| 61 | 2 | isMulticast = true; | |
| 62 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 48 times.
|
48 | } else if (lastOctet == 255) { |
| 63 | // Directed (x.y.z.255) and limited (255.255.255.255) broadcast. This | ||
| 64 | // is matched deliberately - the old byte-swapped math happened to | ||
| 65 | // catch the same addresses, so dropping it silently would take | ||
| 66 | // working broadcast configs with it. | ||
| 67 | ✗ | isBroadcast = true; | |
| 68 | } | ||
| 69 | |||
| 70 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | std::string nt = tokens; |
| 71 | |||
| 72 | 50 | std::string token; | |
| 73 | 50 | std::vector<uint8_t> bytes; | |
| 74 |
2/2✓ Branch 0 taken 50 times.
✓ Branch 1 taken 50 times.
|
100 | while (!nt.empty()) { |
| 75 | 50 | std::string::size_type pos = nt.find_first_of(' '); | |
| 76 |
2/2✓ Branch 0 taken 8 times.
✓ Branch 1 taken 42 times.
|
50 | if (pos < nt.size()) { |
| 77 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | token = nt.substr(0, pos); |
| 78 |
1/2✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
|
8 | nt = nt.substr(pos + 1); |
| 79 | } else { | ||
| 80 |
1/2✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
|
42 | token = nt; |
| 81 |
1/2✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
|
42 | nt = ""; |
| 82 | } | ||
| 83 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | if (token.size() > 2) { |
| 84 |
2/2✓ Branch 0 taken 48 times.
✓ Branch 1 taken 2 times.
|
50 | if (token[0] == '{') { |
| 85 |
3/4✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✓ Branch 3 taken 6 times.
|
48 | if (token == "{data}") { |
| 86 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 41 times.
|
42 | if (bytes.size() > 0) { |
| 87 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | udpBuffers.push_back(bytes); |
| 88 | } | ||
| 89 |
1/2✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
|
42 | channelIovecs.push_back(udpBuffers.size()); |
| 90 | 42 | bytes.clear(); | |
| 91 |
1/2✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
|
42 | udpBuffers.push_back(bytes); |
| 92 |
11/18✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 5 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 5 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 5 times.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 5 times.
|
6 | } else if (token == "{P}" || token == "{p}" || token == "{P1}" || token == "{p1}") { |
| 93 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | bytes.push_back(channelCount / 3); |
| 94 |
11/18✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 4 times.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 4 times.
|
5 | } else if (token == "{S}" || token == "{s}" || token == "{S1}" || token == "{s1}") { |
| 95 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | bytes.push_back(channelCount); |
| 96 |
3/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
|
4 | } else if (token == "{S2}") { |
| 97 | // Upper case is low byte first and lower case high | ||
| 98 | // byte first, as these have always gone on the wire; | ||
| 99 | // the helpers they called were named the other way | ||
| 100 | // round. | ||
| 101 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | addLittleEndian(bytes, channelCount); |
| 102 |
3/4✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
|
3 | } else if (token == "{s2}") { |
| 103 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | addBigEndian(bytes, channelCount); |
| 104 |
3/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
|
2 | } else if (token == "{P2}") { |
| 105 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | addLittleEndian(bytes, channelCount / 3); |
| 106 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | } else if (token == "{p2}") { |
| 107 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | addBigEndian(bytes, channelCount / 3); |
| 108 | } else { | ||
| 109 | ✗ | LogWarn(VB_CHANNELOUT, "Unknown token -%s-\n", token.c_str()); | |
| 110 | ✗ | for (int x = 0; x < token.size(); x++) { | |
| 111 | ✗ | bytes.push_back((uint8_t)token[x]); | |
| 112 | } | ||
| 113 | } | ||
| 114 |
5/6✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
|
2 | } else if (token[0] == '0' && token[1] == 'x') { |
| 115 | 1 | int i = std::strtoul(token.c_str(), 0, 16); | |
| 116 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | bytes.push_back(i); |
| 117 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | } else if (token[0] == '#') { |
| 118 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | int i = std::strtoul(token.substr(1).c_str(), 0, 16); |
| 119 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | bytes.push_back(i); |
| 120 | } | ||
| 121 | } else { | ||
| 122 | ✗ | for (int x = 0; x < token.size(); x++) { | |
| 123 | ✗ | bytes.push_back((uint8_t)token[x]); | |
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 49 times.
|
50 | if (!bytes.empty()) { |
| 128 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | udpBuffers.push_back(bytes); |
| 129 | } | ||
| 130 |
1/2✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
|
50 | udpIovecs.resize(udpBuffers.size()); |
| 131 |
2/2✓ Branch 0 taken 44 times.
✓ Branch 1 taken 50 times.
|
94 | for (int x = 0; x < udpBuffers.size(); x++) { |
| 132 | // data(), not &[0]: the {data} placeholder is an empty vector | ||
| 133 | 44 | udpIovecs[x].iov_base = udpBuffers[x].data(); | |
| 134 | 44 | udpIovecs[x].iov_len = udpBuffers[x].size(); | |
| 135 | } | ||
| 136 |
2/2✓ Branch 0 taken 42 times.
✓ Branch 1 taken 50 times.
|
92 | for (auto idx : channelIovecs) { |
| 137 | 42 | udpIovecs[idx].iov_len = channelCount; | |
| 138 | } | ||
| 139 | 60 | } | |
| 140 | 200 | virtual ~GenericUDPOutputData() { | |
| 141 | 200 | } | |
| 142 | |||
| 143 | 48 | virtual const std::string& GetOutputTypeString() const override { | |
| 144 | 48 | return GENERIC_UDP_TYPE; | |
| 145 | } | ||
| 146 | |||
| 147 | 2 | void addLittleEndian(std::vector<uint8_t>& bytes, int v) { | |
| 148 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | bytes.push_back(v & 0xFF); |
| 149 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | bytes.push_back((v >> 8) & 0xFF); |
| 150 | 2 | } | |
| 151 | 2 | void addBigEndian(std::vector<uint8_t>& bytes, int v) { | |
| 152 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | bytes.push_back((v >> 8) & 0xFF); |
| 153 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | bytes.push_back(v & 0xFF); |
| 154 | 2 | } | |
| 155 | 50 | virtual bool IsPingable() override { | |
| 156 |
3/4✓ Branch 0 taken 48 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
|
50 | return !isMulticast && !isBroadcast; |
| 157 | } | ||
| 158 | |||
| 159 | 6171 | virtual void PrepareData(unsigned char* channelData, | |
| 160 | UDPOutputMessages& msgs) override { | ||
| 161 |
4/8✓ Branch 0 taken 6171 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6171 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6171 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6171 times.
✗ Branch 7 not taken.
|
6171 | if (valid && active && NeedToOutputFrame(channelData, startChannel - 1, 0, channelCount)) { |
| 162 | struct mmsghdr msg; | ||
| 163 | 6171 | memset(&msg, 0, sizeof(msg)); | |
| 164 | |||
| 165 | 6171 | msg.msg_hdr.msg_name = &udpAddress; | |
| 166 | 6171 | msg.msg_hdr.msg_namelen = sizeof(sockaddr_in); | |
| 167 | 6171 | msg.msg_hdr.msg_iov = &udpIovecs[0]; | |
| 168 | 6171 | msg.msg_hdr.msg_iovlen = udpIovecs.size(); | |
| 169 | 6171 | msg.msg_len = 0; | |
| 170 |
2/2✓ Branch 0 taken 5984 times.
✓ Branch 1 taken 6171 times.
|
12155 | for (int x = 0; x < udpIovecs.size(); x++) { |
| 171 | 5984 | msg.msg_len += udpIovecs[x].iov_len; | |
| 172 | } | ||
| 173 | |||
| 174 | 6171 | count++; | |
| 175 | 6171 | int start = startChannel - 1; | |
| 176 |
2/2✓ Branch 0 taken 5842 times.
✓ Branch 1 taken 6171 times.
|
12013 | for (auto idx : channelIovecs) { |
| 177 | 5842 | udpIovecs[idx].iov_base = (void*)(channelData + start); | |
| 178 | } | ||
| 179 | |||
| 180 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6171 times.
|
6171 | if (isBroadcast) { |
| 181 | ✗ | msgs[BROADCAST_MESSAGES_KEY].push_back(msg); | |
| 182 |
2/2✓ Branch 0 taken 869 times.
✓ Branch 1 taken 5302 times.
|
6171 | } else if (isMulticast) { |
| 183 | // With the other multicast: that socket has IP_MULTICAST_IF on | ||
| 184 | // the output interface. One keyed by the group's address | ||
| 185 | // doesn't, and its packets followed the default route instead. | ||
| 186 |
2/4✓ Branch 0 taken 869 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 869 times.
✗ Branch 3 not taken.
|
869 | msgs[MULTICAST_MESSAGES_KEY].push_back(msg); |
| 187 | } else { | ||
| 188 |
2/4✓ Branch 0 taken 5302 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5302 times.
✗ Branch 3 not taken.
|
5302 | msgs[udpAddress.sin_addr.s_addr].push_back(msg); |
| 189 | } | ||
| 190 |
1/2✓ Branch 0 taken 6171 times.
✗ Branch 1 not taken.
|
6171 | SaveFrame(&channelData[startChannel - 1], channelCount); |
| 191 | 6171 | skippedFrames = 0; | |
| 192 | } else { | ||
| 193 | ✗ | skippedFrames++; | |
| 194 | } | ||
| 195 | 6171 | } | |
| 196 | |||
| 197 | 5 | virtual void DumpConfig() override { | |
| 198 | 5 | } | |
| 199 | |||
| 200 | int port; | ||
| 201 | int count; | ||
| 202 | |||
| 203 | bool isMulticast; | ||
| 204 | bool isBroadcast; | ||
| 205 | |||
| 206 | std::string tokens; | ||
| 207 | |||
| 208 | sockaddr_in udpAddress; | ||
| 209 | std::vector<int> channelIovecs; | ||
| 210 | std::vector<struct iovec> udpIovecs; | ||
| 211 | std::vector<std::vector<uint8_t>> udpBuffers; | ||
| 212 | }; | ||
| 213 | |||
| 214 | 56 | GenericUDPOutput::GenericUDPOutput(unsigned int startChannel, unsigned int channelCount) : | |
| 215 | 56 | ChannelOutput(startChannel, channelCount) { | |
| 216 | 56 | } | |
| 217 | 224 | GenericUDPOutput::~GenericUDPOutput() { | |
| 218 | // deleted without Close() when Init() fails; a no-op otherwise | ||
| 219 | 112 | UDPOutput::UnregisterExternalOutput(this); | |
| 220 | 224 | } | |
| 221 | |||
| 222 | 56 | int GenericUDPOutput::Init(Json::Value config) { | |
| 223 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 45 times.
|
56 | if (!UDPOutput::INSTANCE) { |
| 224 | 11 | LogErr(VB_CHANNELOUT, "GenericUDPOutput Requires the UDP outputs to be enabled\n"); | |
| 225 | } | ||
| 226 | 56 | config.removeMember("type"); | |
| 227 | // Registered by owner, not handed over as data: the UDP output is rebuilt | ||
| 228 | // whenever the E1.31/DDP config is saved and has to recreate this, and | ||
| 229 | // Close() has to be able to take it back out. | ||
| 230 |
3/6✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 56 times.
✗ Branch 5 not taken.
|
56 | UDPOutput::RegisterExternalOutput(this, [config]() -> UDPOutputData* { |
| 231 |
4/6✓ Branch 0 taken 50 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 50 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
|
102 | return new GenericUDPOutputData(config); |
| 232 | }); | ||
| 233 |
2/4✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 56 times.
✗ Branch 3 not taken.
|
56 | return ChannelOutput::Init(config); |
| 234 | } | ||
| 235 | 56 | int GenericUDPOutput::Close(void) { | |
| 236 | 56 | UDPOutput::UnregisterExternalOutput(this); | |
| 237 | 56 | return ChannelOutput::Close(); | |
| 238 | } | ||
| 239 | |||
| 240 | 119 | void GenericUDPOutput::GetRequiredChannelRanges(const std::function<void(int, int)>& addRange) { | |
| 241 | 119 | addRange(m_startChannel, m_startChannel + m_channelCount - 1); | |
| 242 | 119 | } | |
| 243 | |||
| 244 | 6138 | int GenericUDPOutput::SendData(unsigned char* channelData) { | |
| 245 | 6138 | return m_channelCount; | |
| 246 | } | ||
| 247 |