Skip to content

Commit 0768108

Browse files
feat: Add op47 decklink vanc strategy for subtitles
* Adds op47 vanc packets to decklink frames based on pushed (amcp APPLY) data * Support configuring base64 encoded dummy header for time filling
1 parent 1559bc7 commit 0768108

File tree

6 files changed

+155
-3
lines changed

6 files changed

+155
-3
lines changed

src/modules/decklink/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ set(SOURCES
1313
consumer/vanc.cpp
1414
consumer/vanc.h
1515
consumer/vanc_scte104_strategy.cpp
16+
consumer/vanc_op47_strategy.cpp
1617

1718
producer/decklink_producer.cpp
1819
producer/decklink_producer.h

src/modules/decklink/consumer/config.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@ vanc_configuration parse_vanc_config(const boost::property_tree::wptree& vanc_tr
5959
vanc_configuration vanc_config;
6060

6161
vanc_config.enable = true;
62+
vanc_config.op47_line = vanc_tree.get(L"op47-line", vanc_config.op47_line);
63+
vanc_config.op47_line_field2 = vanc_tree.get(L"op47-line-field2", vanc_config.op47_line_field2);
64+
vanc_config.enable_op47 = vanc_config.op47_line > 0;
6265
vanc_config.scte104_line = vanc_tree.get(L"scte104-line", vanc_config.scte104_line);
6366
vanc_config.enable_scte104 = vanc_config.scte104_line > 0;
67+
vanc_config.op47_dummy_header = vanc_tree.get(L"op47-dummy-header", L"");
6468

6569
return vanc_config;
6670
};

src/modules/decklink/consumer/config.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,13 @@ struct port_configuration
5050

5151
struct vanc_configuration
5252
{
53-
bool enable = false;
54-
bool enable_scte104 = false;
55-
uint8_t scte104_line = 0;
53+
bool enable = false;
54+
bool enable_op47 = false;
55+
bool enable_scte104 = false;
56+
uint8_t op47_line = 0;
57+
uint8_t op47_line_field2 = 0;
58+
uint8_t scte104_line = 0;
59+
std::wstring op47_dummy_header;
5660
};
5761

5862
struct hdr_meta_configuration

src/modules/decklink/consumer/vanc.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ decklink_vanc::decklink_vanc(const vanc_configuration& config)
7171
if (config.enable_scte104) {
7272
strategies_.push_back(create_scte104_strategy(config.scte104_line));
7373
}
74+
if (config.enable_op47) {
75+
strategies_.push_back(
76+
create_op47_strategy(config.op47_line, config.op47_line_field2, config.op47_dummy_header));
77+
}
7478
}
7579

7680
std::vector<caspar::decklink::com_ptr<IDeckLinkAncillaryPacket>> decklink_vanc::pop_packets(bool field2)

src/modules/decklink/consumer/vanc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class decklink_vanc
5656
bool try_push_data(const std::vector<std::wstring>& params);
5757
};
5858

59+
std::shared_ptr<decklink_vanc_strategy>
60+
create_op47_strategy(uint8_t line_number, uint8_t line_number_2, const std::wstring& dummy_header);
5961
std::shared_ptr<decklink_vanc_strategy> create_scte104_strategy(uint8_t line_number);
6062

6163
std::shared_ptr<decklink_vanc> create_vanc(const vanc_configuration& config);
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#include "../util/base64.hpp"
2+
#include "vanc.h"
3+
4+
#include <locale>
5+
6+
#include <boost/lexical_cast.hpp>
7+
#include <common/param.h>
8+
#include <common/ptree.h>
9+
#include <mutex>
10+
#include <numeric>
11+
#include <queue>
12+
#include <vector>
13+
14+
namespace caspar { namespace decklink {
15+
16+
const uint8_t OP47_DID = 0x43;
17+
const uint8_t OP47_SDID = 0x02;
18+
19+
class vanc_op47_strategy : public decklink_vanc_strategy
20+
{
21+
private:
22+
static const std::wstring Name;
23+
24+
mutable std::mutex mutex_;
25+
uint8_t line_number_;
26+
uint8_t line_number_2_;
27+
uint8_t sd_line_;
28+
uint16_t counter_;
29+
30+
std::vector<uint8_t> dummy_header_;
31+
std::queue<std::vector<uint8_t>> packets_;
32+
33+
public:
34+
vanc_op47_strategy(uint8_t line_number, uint8_t line_number_2, const std::wstring& dummy_header)
35+
: line_number_(line_number)
36+
, line_number_2_(line_number_2)
37+
, sd_line_(21)
38+
, counter_(1)
39+
, dummy_header_(dummy_header.empty() ? std::vector<uint8_t>() : base64_decode(dummy_header))
40+
{
41+
}
42+
43+
virtual bool has_data() const override
44+
{
45+
std::lock_guard<std::mutex> lock(mutex_);
46+
return !packets_.empty() || !dummy_header_.empty();
47+
}
48+
virtual vanc_packet pop_packet(bool field2) override
49+
{
50+
std::lock_guard<std::mutex> lock(mutex_);
51+
52+
if (field2 && line_number_2_ == 0) {
53+
return {0, 0, 0, {}};
54+
}
55+
56+
if (packets_.empty()) {
57+
return {OP47_DID, OP47_SDID, field2 ? line_number_2_ : line_number_, sdp_encode(dummy_header_)};
58+
}
59+
auto packet = packets_.front();
60+
packets_.pop();
61+
62+
return {OP47_DID, OP47_SDID, field2 ? line_number_2_ : line_number_, packet};
63+
}
64+
65+
virtual bool try_push_data(const std::vector<std::wstring>& params) override
66+
{
67+
std::lock_guard<std::mutex> lock(mutex_);
68+
69+
try {
70+
for (size_t index = 1; index < params.size(); ++index) {
71+
packets_.push(sdp_encode(base64_decode(params.at(index))));
72+
}
73+
} catch (const boost::bad_lexical_cast& e) {
74+
CASPAR_LOG(error) << "Failed to parse OP47 parameters: " << e.what();
75+
return false;
76+
}
77+
78+
return true;
79+
}
80+
81+
virtual const std::wstring& get_name() const override { return vanc_op47_strategy::Name; }
82+
83+
private:
84+
std::vector<uint8_t> sdp_encode(const std::vector<uint8_t>& packet)
85+
{
86+
if (packet.size() != 45) {
87+
throw std::runtime_error("Invalid packet size for OP47: " + std::to_string(packet.size()));
88+
}
89+
90+
// The following is based on the specification "Free TV Australia Operational Practice OP- 47"
91+
92+
std::vector<uint8_t> result(103);
93+
result[0] = 0x51; // identifier
94+
result[1] = 0x15; // identifier
95+
result[2] = static_cast<uint8_t>(result.size()); // size of the packet
96+
result[3] = 0x02; // format-code
97+
98+
result[4] = sd_line_ | 0xE0; // VBI packet descriptor (odd field)
99+
result[5] = sd_line_ | 0x60; // VBI packet descriptor (even field)
100+
result[6] = 0; // VBI packet descriptor (not used)
101+
result[7] = 0; // VBI packet descriptor (not used)
102+
result[8] = 0; // VBI packet descriptor (not used)
103+
104+
memcpy(result.data() + 9, packet.data(), packet.size());
105+
memcpy(result.data() + 9 + packet.size(), packet.data(), packet.size());
106+
result[99] = 0x74; // footer id
107+
result[100] = (counter_ & 0xFF00) >> 8; // footer sequence counter
108+
result[101] = counter_ & 0x00FF; // footer sequence counter
109+
result[102] = 0x0; // SPD checksum, will be set when calculated
110+
111+
auto sum = accumulate(result.begin(), result.end(), (uint8_t)0);
112+
result[102] = ~sum + 1;
113+
114+
counter_++; // this is rolling over at 65535 by design
115+
116+
return result;
117+
}
118+
119+
std::vector<uint8_t> base64_decode(const std::wstring& encoded)
120+
{
121+
std::vector<char> buffer(encoded.size());
122+
std::use_facet<std::ctype<wchar_t>>(std::locale())
123+
.narrow(encoded.data(), encoded.data() + encoded.size(), '?', buffer.data());
124+
auto str = std::string(buffer.data(), buffer.size());
125+
return base64::decode_into<std::vector<uint8_t>>(str);
126+
}
127+
};
128+
129+
const std::wstring vanc_op47_strategy::Name = L"OP47";
130+
131+
std::shared_ptr<decklink_vanc_strategy>
132+
create_op47_strategy(uint8_t line_number, uint8_t line_number_2, const std::wstring& dummy_header)
133+
{
134+
return std::make_shared<vanc_op47_strategy>(line_number, line_number_2, dummy_header);
135+
}
136+
137+
}} // namespace caspar::decklink

0 commit comments

Comments
 (0)