我不知道你是否解决了你的问题,但我有同样的问题,我设法让它以某种方式与 C++ 一起工作。我花了一些时间才弄清楚我必须做什么,我从来没有做过任何 HTTP 的东西,甚至是开发较少的即插即用驱动程序,所以我将逐步解释我是如何做到的,正如我希望我被解释过的那样。
在消息的末尾我提供了一个指向我的整个文件的链接,请随意尝试。
我使用 boost asio 库来解决每个与网络相关的问题,甚至更多(一切都是异步的,这是一个很棒的库,但对于像我这样无知的人来说很难掌握......)。我的大部分函数都是从文档中的示例部分复制粘贴的,这解释了为什么我的代码在某些地方很尴尬。这是我的主要功能,没什么特别的,我实例化了一个 asio::io_service,创建我的对象(我错误地命名为 multicast_manager)然后运行该服务:
#include <bunch_of_stuff>
using namespace std;
namespace basio = boost::asio;
int main(int argc, char* argv[]) {
try {
basio::io_service io_service;
multicast_manager m(io_service, basio::ip::address::from_string("239.255.255.250"));
io_service.run();
m.parse_description();
m.start_liveview();
io_service.reset();
io_service.run();
m.get_live_image();
io_service.reset();
io_service.run();
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
通过 ssdp 发现相机
首先,我们必须使用其 upnp (universal plug and play) 功能连接到相机。原理是每个upnp设备都在监听组播端口230.255.255.250:1900的M-SEARCH请求。这意味着如果您向此地址发送正确的消息,设备将通过告诉您它存在来回答,并为您提供使用它的信息。文档中给出了正确的消息。我在这样做时遇到了两个陷阱:首先,我省略了在消息末尾添加换行符,如the http standard 中所指定。所以你要发送的消息可以这样构建:
multicast_manager(basio::io_service& io_service, const basio::ip::address& multicast_address)
: endpoint_(multicast_address, 1900),
socket_(io_service, endpoint_.protocol())
{
stringstream os;
os << "M-SEARCH * HTTP/1.1\r\n";
os << "HOST: 239.255.255.250:1900\r\n";
os << "MAN: \"ssdp:discover\"\r\n";
os << "MX: 4\r\n";
os << "ST: urn:schemas-sony-com:service:ScalarWebAPI:1\r\n";
os << "\r\n";
message_ = os.str();
// ...
这部分重要的第二件事是检查消息是否发送到正确的网络接口。在我的情况下,即使它被禁用,它也会通过我的以太网卡出去,直到我更改了套接字中的正确选项,我用以下代码解决了这个问题:
// ...
socket_.set_option(basio::ip::multicast::outbound_interface(
basio::ip::address_v4::from_string("10.0.1.1")));
socket_.async_send_to(
basio::buffer(message_), endpoint_,
boost::bind(&multicast_manager::handle_send_to, this,
basio::placeholders::error));
}
现在我们听。我们从哪里听你可能会问你是否像我一样?什么端口,什么地址?好吧,我们不在乎:问题是,当我们发送消息时,我们定义了一个目标 ip 和端口(在端点构造函数中)。我们不一定要定义任何本地地址,它是我们自己的 ip 地址(事实上,我们确实定义了它,但只是为了让它知道可以选择哪个网络接口);而且我们没有定义任何本地端口,它实际上是自动选择的(我猜是操作系统?)。无论如何,重要的部分是任何收听多播组的人都会收到我们的消息并知道其来源,并将直接响应正确的 ip 和端口。所以这里不需要指定任何东西,不需要创建一个新的套接字,我们只需在一个瓶子里监听我们发送消息的同一个套接字:
void handle_send_to(const boost::system::error_code& error)
{
if (!error) {
socket_.async_receive(asio::buffer(data_),
boost::bind(&multicast_manager::handle_read_header, this,
basio::placeholders::error,
basio::placeholders::bytes_transferred));
}
}
如果一切顺利,答案如下:
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
EXT:
LOCATION: http://10.0.0.1:64321/DmsRmtDesc.xml
SERVER: UPnP/1.0 SonyImagingDevice/1.0
ST: urn:schemas-sony-com:service:ScalarWebAPI:1
USN: uuid:00000000-0005-0010-8000-10a5d09bbeda::urn:schemas-sony-com:service:ScalarWebAPI:1
X-AV-Physical-Unit-Info: pa=""; pl=;
X-AV-Server-Info: av=5.0; hn=""; cn="Sony Corporation"; mn="SonyImagingDevice"; mv="1.0";
为了解析这条消息,我重用了来自 boost http client example 的解析,但我一次完成了它,因为由于某种原因我无法使用 UDP 套接字执行 async_read_until。无论如何,重要的是相机收到了我们的信息;另一个重要部分是描述文件 DmsRmtDesc.xml 的位置。
检索和读取描述文件
我们需要获取 DmsRmtDesc.xml。这次我们将在指定的 IP 地址和端口处直接向摄像机发送 GET 请求。这个请求类似于:
GET /DmsRmtDesc.xml HTTP/1.1
Host: 10.0.0.1
Accept: */*
Connection: close
不要忘记额外的空行。我不知道 Connection:close 是什么意思。接受行指定您接受的答案的应用类型,这里我们将接受任何答案。我使用 boost http 客户端示例获取了文件,基本上我打开了一个到 10.0.0.1:64321 的套接字并接收到后面是文件内容的 HTPP 标头。现在我们有了一个 xml 文件,其中包含我们要使用的 Web 服务的地址。让我们再次使用 boost 解析它,我们要检索摄像头服务地址,也许是实时取景流地址:
namespace bpt = boost::property_tree;
bpt::ptree pt;
bpt::read_xml(content, pt);
liveview_url = pt.get<string>("root.device.av:X_ScalarWebAPI_DeviceInfo.av:X_ScalarWebAPI_ImagingDevice.av:X_ScalarWebAPI_LiveView_URL");
for (bpt::ptree::value_type &v : pt.get_child("root.device.av:X_ScalarWebAPI_DeviceInfo.av:X_ScalarWebAPI_ServiceList")) {
string service = v.second.get<string>("av:X_ScalarWebAPI_ServiceType");
if (service == "camera")
camera_service_url = v.second.get<string>("av:X_ScalarWebAPI_ActionList_URL");
}
完成后,我们可以开始向相机发送实际命令,并使用 API。
向相机发送命令
这个想法很简单,我们使用文档中提供的 json 格式构建命令,并通过 POST http 请求将其发送到相机服务。我们将启动 liveview 模式,因此我们发送 POST 请求(我们最终将不得不使用 boost property_tree 来构建我们的 json 字符串,这里我是手动完成的):
POST /sony/camera HTTP/1.1
Accept: application/json-rpc
Content-Length: 70
Content-Type: application/json-rpc
Host:http://10.0.0.1:10000/sony
{"method": "startLiveview","params" : [],"id" : 1,"version" : "1.0"}
我们将其发送到 10.0.0.1:10000 并等待答复:
HTTP/1.1 200 OK
Connection: close
Content-Length: 119
Content-Type: application/json
{"id":1,"result":["http://10.0.0.1:60152/liveview.JPG?%211234%21http%2dget%3a%2a%3aimage%2fjpeg%3a%2a%21%21%21%21%21"]}
我们第二次得到liveview url,我不知道哪个更好,它们是相同的......
无论如何,现在我们知道如何向相机发送命令并检索它的答案,我们仍然需要获取图像流。
从实时取景流中获取图像
我们有 liveview url,我们有 API 参考指南中的规范。首先,我们要求相机向我们发送流,因此我们向 10.0.0.1:60152 发送 GET 请求:
GET /liveview.JPG?%211234%21http%2dget%3a%2a%3aimage%2fjpeg%3a%2a%21%21%21%21%21 HTTP/1.1
Accept: image/jpeg
Host: 10.0.0.1
我们等待答案,这应该不会花很长时间。答案从通常的 HTTP 标头开始:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Pragma: no-cache
CACHE-CONTROL: no-cache
Content-Type: image/jpeg
transferMode.dlna.org: Interactive
Connection: Keep-Alive
Date: Wed, 09 Jul 2014 14:13:13 GMT
Server: UPnP/1.0 SonyImagingDevice/1.0
根据文档,这应该直接跟在 liveview 数据流之后,理论上包含在:
- 8 字节的公共标头指定我们是否确实处于实时取景模式。
- 128 字节的有效载荷数据给出了 jpg 数据的大小。
- n 字节的 jpeg 数据。
然后我们再次获得公共标头,无限期地直到我们关闭套接字。
在我的例子中,公共标头以“88\r\n”开头,所以我不得不丢弃它,而 jpg 数据之后是 10 个额外字节,然后才切换到下一帧,所以我不得不将其纳入帐户。我还必须自动检测 jpg 图像的开头,因为 jpg 数据以包含我忽略其含义的数字的文本开头。这些错误很可能是由于我做错了什么,或者我不了解我在这里使用的技术。
我的代码现在可以运行,但最后几位非常临时,它肯定需要更好的检查。
它还需要大量重构才能使用,但它显示了我猜每个步骤的工作原理......
Here is the entire file if you want to try it out.
And here is a working VS project on github.