验证本地http可以查看webrtc.
This commit is contained in:
parent
70763da9b0
commit
ee1aaa1bf3
@ -17,11 +17,13 @@
|
|||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@babel/runtime": "^7.26.10",
|
"@babel/runtime": "^7.26.10",
|
||||||
|
"antd": "^5.24.4",
|
||||||
"babel-loader": "^10.0.0",
|
"babel-loader": "^10.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router": "^7.3.0",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.98.0",
|
||||||
"webpack-cli": "^6.0.1",
|
"webpack-cli": "^6.0.1",
|
||||||
"webpack-dev-server": "^5.2.0"
|
"webpack-dev-server": "^5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import WebRTCClient from "./WebRTCClient";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return <div>
|
||||||
<h1>
|
<h1>
|
||||||
从 Webpack 和 Babel 开始搭建 React 项目
|
从 Webpack 和 Babel 开始搭建 React 项目
|
||||||
</h1>
|
</h1>
|
||||||
)
|
<WebRTCClient />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
210
Frontend/src/WebRTCClient.jsx
Normal file
210
Frontend/src/WebRTCClient.jsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Button, Card, Row, Col, Typography, List, message } from 'antd';
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
// 生成随机ID
|
||||||
|
const randomId = (length) => {
|
||||||
|
return Array.from({ length }, () => Math.random().toString(36).charAt(2)).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebRTCClient = () => {
|
||||||
|
const [connectionStates, setConnectionStates] = useState({
|
||||||
|
iceConnection: 'new',
|
||||||
|
iceGathering: 'new',
|
||||||
|
signaling: 'stable',
|
||||||
|
dataChannel: []
|
||||||
|
});
|
||||||
|
const [sdpInfo, setSdpInfo] = useState({ offer: '', answer: '' });
|
||||||
|
const [mediaState, setMediaState] = useState({ started: false });
|
||||||
|
|
||||||
|
const clientId = useRef(randomId(10));
|
||||||
|
const websocket = useRef(null);
|
||||||
|
const pc = useRef(null);
|
||||||
|
const dc = useRef(null);
|
||||||
|
const videoRef = useRef(null);
|
||||||
|
const startTime = useRef(null);
|
||||||
|
|
||||||
|
// WebSocket 初始化
|
||||||
|
useEffect(() => {
|
||||||
|
websocket.current = new WebSocket(`wss://amass.fun/api/v1/webrtc/signal/${clientId.current}`);
|
||||||
|
|
||||||
|
websocket.current.onopen = () => {
|
||||||
|
message.success('信令服务器连接成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.current.onmessage = async (evt) => {
|
||||||
|
if (typeof evt.data !== 'string') return;
|
||||||
|
const message = JSON.parse(evt.data);
|
||||||
|
if (message.type === "offer") {
|
||||||
|
setSdpInfo(prev => ({ ...prev, offer: message.sdp }));
|
||||||
|
handleOffer(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => websocket.current?.close();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 状态更新方法
|
||||||
|
const updateConnectionState = (type, state) => {
|
||||||
|
setConnectionStates(prev => ({
|
||||||
|
...prev,
|
||||||
|
[type]: [...prev[type], state]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建PeerConnection
|
||||||
|
const createPeerConnection = () => {
|
||||||
|
const config = {
|
||||||
|
bundlePolicy: "max-bundle",
|
||||||
|
iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }]
|
||||||
|
};
|
||||||
|
|
||||||
|
const newPc = new RTCPeerConnection(config);
|
||||||
|
|
||||||
|
// ICE状态监听
|
||||||
|
newPc.addEventListener('iceconnectionstatechange', () => {
|
||||||
|
updateConnectionState('iceConnection', newPc.iceConnectionState);
|
||||||
|
});
|
||||||
|
|
||||||
|
newPc.addEventListener('icegatheringstatechange', () => {
|
||||||
|
updateConnectionState('iceGathering', newPc.iceGatheringState);
|
||||||
|
});
|
||||||
|
|
||||||
|
newPc.addEventListener('signalingstatechange', () => {
|
||||||
|
updateConnectionState('signaling', newPc.signalingState);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 媒体流处理(网页2关键逻辑)
|
||||||
|
newPc.ontrack = (evt) => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.srcObject = evt.streams[0];
|
||||||
|
videoRef.current.play().catch(err => message.error('视频播放失败'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 数据通道处理(网页1实现参考)
|
||||||
|
newPc.ondatachannel = (evt) => {
|
||||||
|
dc.current = evt.channel;
|
||||||
|
dc.current.onmessage = (event) => {
|
||||||
|
if (typeof event.data === 'string') {
|
||||||
|
setConnectionStates(prev => ({
|
||||||
|
...prev,
|
||||||
|
dataChannel: [...prev.dataChannel, `< ${event.data}`]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return newPc;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理Offer
|
||||||
|
const handleOffer = async (offer) => {
|
||||||
|
pc.current = createPeerConnection();
|
||||||
|
await pc.current.setRemoteDescription(offer);
|
||||||
|
await sendAnswer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送Answer
|
||||||
|
const sendAnswer = async () => {
|
||||||
|
const answer = await pc.current.createAnswer();
|
||||||
|
await pc.current.setLocalDescription(answer);
|
||||||
|
|
||||||
|
setSdpInfo(prev => ({ ...prev, answer: answer.sdp }));
|
||||||
|
|
||||||
|
websocket.current.send(JSON.stringify({
|
||||||
|
id: "server",
|
||||||
|
type: answer.type,
|
||||||
|
sdp: answer.sdp
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 控制方法
|
||||||
|
const startCall = () => {
|
||||||
|
setMediaState({ started: true });
|
||||||
|
websocket.current.send(JSON.stringify({ id: "server", type: "request" }));
|
||||||
|
startTime.current = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopCall = () => {
|
||||||
|
if (dc.current) dc.current.close();
|
||||||
|
if (pc.current) pc.current.close();
|
||||||
|
setMediaState({ started: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="WebRTC 视频通话" style={{ width: 800 }}>
|
||||||
|
<Row gutter={16}>
|
||||||
|
{/* 视频区域 */}
|
||||||
|
<Col span={16}>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
display: mediaState.started ? 'block' : 'none'
|
||||||
|
}}
|
||||||
|
muted
|
||||||
|
autoPlay
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={startCall}
|
||||||
|
disabled={!websocket.current?.readyState}
|
||||||
|
>
|
||||||
|
开始通话
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
onClick={stopCall}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
结束通话
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* 状态信息 */}
|
||||||
|
<Col span={8}>
|
||||||
|
<Card size="small" title="连接状态">
|
||||||
|
<Text strong>ICE Connection:</Text>
|
||||||
|
<Text code>{connectionStates.iceConnection}</Text>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Text strong>ICE Gathering:</Text>
|
||||||
|
<Text code>{connectionStates.iceGathering}</Text>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Text strong>Signaling:</Text>
|
||||||
|
<Text code>{connectionStates.signaling}</Text>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="SDP 信息" style={{ marginTop: 16 }}>
|
||||||
|
<Text strong>Offer SDP:</Text>
|
||||||
|
<pre style={{ fontSize: 10 }}>{sdpInfo.offer}</pre>
|
||||||
|
|
||||||
|
<Text strong>Answer SDP:</Text>
|
||||||
|
<pre style={{ fontSize: 10 }}>{sdpInfo.answer}</pre>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card size="small" title="数据通道" style={{ marginTop: 16 }}>
|
||||||
|
<List
|
||||||
|
size="small"
|
||||||
|
dataSource={connectionStates.dataChannel}
|
||||||
|
renderItem={item => (
|
||||||
|
<List.Item style={{ fontFamily: 'monospace' }}>
|
||||||
|
{item}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
style={{ maxHeight: 200, overflowY: 'auto' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebRTCClient;
|
88
Main/Application.cpp
Normal file
88
Main/Application.cpp
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
#include "Application.h"
|
||||||
|
#include "Core/IoContext.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
|
#include "Core/Singleton.h"
|
||||||
|
#include "HttpSession.h"
|
||||||
|
#include "Router/router.hpp"
|
||||||
|
#include "ServiceLogic.h"
|
||||||
|
#include "Settings.h"
|
||||||
|
#include "WebRTC/SignalServer.h"
|
||||||
|
#include <boost/asio/strand.hpp>
|
||||||
|
#include <boost/url/url_view.hpp>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
|
||||||
|
class ApplicationPrivate {
|
||||||
|
public:
|
||||||
|
std::shared_ptr<boost::urls::router<Application::RequestHandler>> router;
|
||||||
|
std::shared_ptr<boost::asio::ip::tcp::acceptor> acceptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
Application::Application() : m_d{new ApplicationPrivate()} {
|
||||||
|
using namespace boost::urls;
|
||||||
|
using namespace Core;
|
||||||
|
m_settings = Singleton<Settings>::construct();
|
||||||
|
m_ioContext = Singleton<IoContext>::construct(m_settings->threads());
|
||||||
|
m_d->router = std::make_shared<router<RequestHandler>>();
|
||||||
|
m_signalServer = std::make_shared<SignalServer>(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::insertUrl(std::string_view url, RequestHandler &&handler) {
|
||||||
|
m_d->router->insert(url, std::move(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::io_context &Application::ioContext() {
|
||||||
|
return *m_ioContext->ioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Application::exec() {
|
||||||
|
using namespace Core;
|
||||||
|
auto settings = Singleton<Settings>::instance();
|
||||||
|
ServiceLogic::staticFilesDeploy();
|
||||||
|
startAcceptHttpConnections(settings->server(), settings->port());
|
||||||
|
m_ioContext->run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::startAcceptHttpConnections(const std::string &address, uint16_t port) {
|
||||||
|
m_d->acceptor = std::make_shared<boost::asio::ip::tcp::acceptor>(*m_ioContext->ioContext());
|
||||||
|
boost::beast::error_code error;
|
||||||
|
boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::make_address(address), port);
|
||||||
|
m_d->acceptor->open(endpoint.protocol(), error);
|
||||||
|
if (error) {
|
||||||
|
LOG(error) << error.message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_d->acceptor->set_option(boost::asio::socket_base::reuse_address(true), error);
|
||||||
|
if (error) {
|
||||||
|
LOG(error) << error.message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_d->acceptor->bind(endpoint, error);
|
||||||
|
if (error) {
|
||||||
|
LOG(error) << error.message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_d->acceptor->listen(boost::asio::socket_base::max_listen_connections, error);
|
||||||
|
if (error) {
|
||||||
|
LOG(error) << error.message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
asyncAcceptHttpConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::asyncAcceptHttpConnections() {
|
||||||
|
auto socket = std::make_shared<boost::asio::ip::tcp::socket>(boost::asio::make_strand(*m_ioContext->ioContext()));
|
||||||
|
m_d->acceptor->async_accept(*socket, [self{shared_from_this()}, socket](const boost::system::error_code &error) {
|
||||||
|
if (error) {
|
||||||
|
if (error == boost::asio::error::operation_aborted) return;
|
||||||
|
LOG(error) << error.message();
|
||||||
|
} else {
|
||||||
|
auto session = std::make_shared<HttpSession>(std::move(*socket), self->m_d->router);
|
||||||
|
session->run();
|
||||||
|
}
|
||||||
|
self->asyncAcceptHttpConnections();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Danki
|
44
Main/Application.h
Normal file
44
Main/Application.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#ifndef __APPLICATION_H__
|
||||||
|
#define __APPLICATION_H__
|
||||||
|
|
||||||
|
#include "Router/matches.hpp"
|
||||||
|
#include <boost/beast/http/string_body.hpp>
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
class IoContext;
|
||||||
|
} // namespace Core
|
||||||
|
|
||||||
|
namespace boost {
|
||||||
|
namespace asio {
|
||||||
|
class io_context;
|
||||||
|
}
|
||||||
|
} // namespace boost
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
|
||||||
|
class ApplicationPrivate;
|
||||||
|
class Settings;
|
||||||
|
class HttpSession;
|
||||||
|
class SignalServer;
|
||||||
|
|
||||||
|
class Application : public std::enable_shared_from_this<Application> {
|
||||||
|
public:
|
||||||
|
using Request = boost::beast::http::request<boost::beast::http::string_body>;
|
||||||
|
using RequestHandler = std::function<void(HttpSession &, const Request &, const boost::urls::matches &)>;
|
||||||
|
Application();
|
||||||
|
boost::asio::io_context &ioContext();
|
||||||
|
void insertUrl(std::string_view url, RequestHandler &&handler);
|
||||||
|
int exec();
|
||||||
|
void startAcceptHttpConnections(const std::string &address, uint16_t port);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void asyncAcceptHttpConnections();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ApplicationPrivate *m_d = nullptr;
|
||||||
|
std::shared_ptr<Settings> m_settings;
|
||||||
|
std::shared_ptr<Core::IoContext> m_ioContext;
|
||||||
|
std::shared_ptr<SignalServer> m_signalServer;
|
||||||
|
};
|
||||||
|
} // namespace Danki
|
||||||
|
#endif // __APPLICATION_H__
|
@ -4,13 +4,20 @@ find_package(LibDataChannel REQUIRED)
|
|||||||
find_package(Boost COMPONENTS json REQUIRED)
|
find_package(Boost COMPONENTS json REQUIRED)
|
||||||
|
|
||||||
add_executable(PassengerStatistics main.cpp
|
add_executable(PassengerStatistics main.cpp
|
||||||
|
Application.h Application.cpp
|
||||||
Camera.h Camera.cpp
|
Camera.h Camera.cpp
|
||||||
|
HttpSession.h HttpSession.cpp
|
||||||
ImageUtilities.h ImageUtilities.cpp
|
ImageUtilities.h ImageUtilities.cpp
|
||||||
RtspServer.h RtspServer.cpp
|
RtspServer.h RtspServer.cpp
|
||||||
|
ResponseUtility.h ResponseUtility.cpp
|
||||||
|
ServiceLogic.h ServiceLogic.cpp
|
||||||
|
Settings.h Settings.cpp
|
||||||
VideoInput.h VideoInput.cpp
|
VideoInput.h VideoInput.cpp
|
||||||
|
|
||||||
WebRTC/Streamer.h WebRTC/Streamer.cpp
|
WebRTC/Streamer.h WebRTC/Streamer.cpp
|
||||||
WebRTC/Helpers.h WebRTC/Helpers.cpp
|
WebRTC/Helpers.h WebRTC/Helpers.cpp
|
||||||
|
WebRTC/SignalServer.h WebRTC/SignalServer.cpp
|
||||||
|
WebRTC/WebSocketSignalSession.h WebRTC/WebSocketSignalSession.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(PassengerStatistics
|
target_include_directories(PassengerStatistics
|
||||||
@ -28,6 +35,7 @@ target_link_directories(PassengerStatistics
|
|||||||
|
|
||||||
target_link_libraries(PassengerStatistics
|
target_link_libraries(PassengerStatistics
|
||||||
PRIVATE Kylin::Core
|
PRIVATE Kylin::Core
|
||||||
|
PRIVATE Kylin::Router
|
||||||
PRIVATE LibDataChannel::LibDataChannel
|
PRIVATE LibDataChannel::LibDataChannel
|
||||||
PRIVATE OpenSSL::SSL
|
PRIVATE OpenSSL::SSL
|
||||||
PRIVATE OpenSSL::Crypto
|
PRIVATE OpenSSL::Crypto
|
||||||
|
120
Main/HttpSession.cpp
Normal file
120
Main/HttpSession.cpp
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
#include "HttpSession.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
|
#include <boost/beast/http/read.hpp>
|
||||||
|
#include <boost/beast/version.hpp>
|
||||||
|
#include <boost/config.hpp>
|
||||||
|
#include <boost/stacktrace.hpp>
|
||||||
|
#include <boost/url/parse_path.hpp>
|
||||||
|
#include <boost/url/url_view.hpp>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
HttpSession::HttpSession(boost::asio::ip::tcp::socket &&socket,
|
||||||
|
const std::shared_ptr<boost::urls::router<RequestHandler>> &router)
|
||||||
|
: m_stream(std::move(socket)), m_router(router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpSession::run() {
|
||||||
|
doRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::beast::tcp_stream::executor_type HttpSession::executor() {
|
||||||
|
return m_stream.get_executor();
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::asio::ip::tcp::socket HttpSession::releaseSocket() {
|
||||||
|
return m_stream.release_socket();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpSession::errorReply(const Request &request, boost::beast::http::status status,
|
||||||
|
boost::beast::string_view message) {
|
||||||
|
using namespace boost::beast;
|
||||||
|
// invalid route
|
||||||
|
http::response<http::string_body> res{status, request.version()};
|
||||||
|
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
res.set(http::field::content_type, "text/html");
|
||||||
|
res.keep_alive(request.keep_alive());
|
||||||
|
res.body() = message;
|
||||||
|
res.prepare_payload();
|
||||||
|
|
||||||
|
reply(std::move(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpSession::doRead() {
|
||||||
|
// Construct a new parser for each message
|
||||||
|
m_parser.emplace();
|
||||||
|
|
||||||
|
// Apply a reasonable limit to the allowed size
|
||||||
|
// of the body in bytes to prevent abuse.
|
||||||
|
m_parser->body_limit(std::numeric_limits<std::uint64_t>::max());
|
||||||
|
m_parser->header_limit(std::numeric_limits<std::uint32_t>::max());
|
||||||
|
m_buffer.clear();
|
||||||
|
|
||||||
|
// Set the timeout.
|
||||||
|
m_stream.expires_after(std::chrono::seconds(30));
|
||||||
|
// clang-format off
|
||||||
|
boost::beast::http::async_read(m_stream, m_buffer, *m_parser, [self{shared_from_this()}](const boost::system::error_code &ec, std::size_t bytes_transferred) {
|
||||||
|
self->onRead(ec, bytes_transferred);
|
||||||
|
});
|
||||||
|
// clang-format on
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpSession::onRead(const boost::beast::error_code &error, std::size_t) {
|
||||||
|
using namespace boost::beast;
|
||||||
|
if (error) {
|
||||||
|
if (error == http::error::end_of_stream) {
|
||||||
|
boost::beast::error_code e;
|
||||||
|
m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, e);
|
||||||
|
} else if (error != boost::asio::error::operation_aborted) {
|
||||||
|
LOG(info) << error << " : " << error.message();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (m_router.expired()) {
|
||||||
|
LOG(error) << "router is null.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &request = m_parser->get();
|
||||||
|
auto path = boost::urls::parse_path(request.target());
|
||||||
|
if (!path) {
|
||||||
|
LOG(error) << request.target() << "failed, error: " << path.error().message();
|
||||||
|
errorReply(request, http::status::bad_request, "Illegal request-target");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto router = m_router.lock();
|
||||||
|
boost::urls::matches matches;
|
||||||
|
auto handler = router->find(*path, matches);
|
||||||
|
if (handler) {
|
||||||
|
try {
|
||||||
|
(*handler)(*this, request, matches);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
boost::stacktrace::stacktrace trace = boost::stacktrace::stacktrace::from_current_exception();
|
||||||
|
LOG(error) << e.what() << ", trace:\n" << trace;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "The resource '" << request.target() << "' was not found.";
|
||||||
|
auto message = oss.str();
|
||||||
|
errorReply(request, http::status::not_found, message);
|
||||||
|
LOG(error) << message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpSession::onWrite(boost::beast::error_code ec, std::size_t, bool close) {
|
||||||
|
if (ec) {
|
||||||
|
if (ec == boost::asio::error::operation_aborted) return;
|
||||||
|
std::cerr << "write: " << ec.message() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (close) {
|
||||||
|
// This means we should close the connection, usually because
|
||||||
|
// the response indicated the "Connection: close" semantic.
|
||||||
|
m_stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read another request
|
||||||
|
doRead();
|
||||||
|
}
|
||||||
|
} // namespace Danki
|
52
Main/HttpSession.h
Normal file
52
Main/HttpSession.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#ifndef HTTPSESSION_H
|
||||||
|
#define HTTPSESSION_H
|
||||||
|
|
||||||
|
#include "Router/router.hpp"
|
||||||
|
#include <boost/beast/core/flat_buffer.hpp>
|
||||||
|
#include <boost/beast/core/tcp_stream.hpp>
|
||||||
|
#include <boost/beast/http/parser.hpp>
|
||||||
|
#include <boost/beast/http/string_body.hpp>
|
||||||
|
#include <boost/beast/http/write.hpp>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
|
||||||
|
/** Represents an established HTTP connection
|
||||||
|
*/
|
||||||
|
class HttpSession : public std::enable_shared_from_this<HttpSession> {
|
||||||
|
void doRead();
|
||||||
|
void onWrite(boost::beast::error_code ec, std::size_t, bool close);
|
||||||
|
|
||||||
|
public:
|
||||||
|
using Request = boost::beast::http::request<boost::beast::http::string_body>;
|
||||||
|
using RequestHandler = std::function<void(HttpSession &, const Request &, const boost::urls::matches &)>;
|
||||||
|
HttpSession(boost::asio::ip::tcp::socket &&socket,
|
||||||
|
const std::shared_ptr<boost::urls::router<RequestHandler>> &router);
|
||||||
|
template <typename Response>
|
||||||
|
void reply(Response &&response) {
|
||||||
|
using ResponseType = typename std::decay_t<decltype(response)>;
|
||||||
|
auto sp = std::make_shared<ResponseType>(std::forward<decltype(response)>(response));
|
||||||
|
boost::beast::http::async_write(
|
||||||
|
m_stream, *sp, [self = shared_from_this(), sp](boost::beast::error_code ec, std::size_t bytes) {
|
||||||
|
self->onWrite(ec, bytes, sp->need_eof());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void errorReply(const Request &request, boost::beast::http::status status, boost::beast::string_view message);
|
||||||
|
boost::beast::tcp_stream::executor_type executor();
|
||||||
|
boost::asio::ip::tcp::socket releaseSocket();
|
||||||
|
void run();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onRead(const boost::beast::error_code &error, std::size_t);
|
||||||
|
|
||||||
|
private:
|
||||||
|
boost::beast::tcp_stream m_stream;
|
||||||
|
std::weak_ptr<boost::urls::router<RequestHandler>> m_router;
|
||||||
|
boost::beast::flat_buffer m_buffer{std::numeric_limits<std::uint32_t>::max()};
|
||||||
|
std::optional<boost::beast::http::request_parser<boost::beast::http::string_body>> m_parser;
|
||||||
|
};
|
||||||
|
} // namespace Danki
|
||||||
|
|
||||||
|
#endif // HTTPSESSION_H
|
51
Main/ResponseUtility.cpp
Normal file
51
Main/ResponseUtility.cpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#include "ResponseUtility.h"
|
||||||
|
#include "boost/beast.hpp"
|
||||||
|
|
||||||
|
namespace ResponseUtility {
|
||||||
|
|
||||||
|
std::string_view mimeType(std::string_view path) {
|
||||||
|
using boost::beast::iequals;
|
||||||
|
auto const ext = [&path] {
|
||||||
|
auto const pos = path.rfind(".");
|
||||||
|
if (pos == std::string_view::npos) return std::string_view{};
|
||||||
|
return path.substr(pos);
|
||||||
|
}();
|
||||||
|
if (iequals(ext, ".pdf")) return "Application/pdf";
|
||||||
|
if (iequals(ext, ".htm")) return "text/html";
|
||||||
|
if (iequals(ext, ".html")) return "text/html";
|
||||||
|
if (iequals(ext, ".php")) return "text/html";
|
||||||
|
if (iequals(ext, ".css")) return "text/css";
|
||||||
|
if (iequals(ext, ".txt")) return "text/plain";
|
||||||
|
if (iequals(ext, ".js")) return "application/javascript";
|
||||||
|
if (iequals(ext, ".json")) return "application/json";
|
||||||
|
if (iequals(ext, ".xml")) return "application/xml";
|
||||||
|
if (iequals(ext, ".swf")) return "application/x-shockwave-flash";
|
||||||
|
if (iequals(ext, ".flv")) return "video/x-flv";
|
||||||
|
if (iequals(ext, ".png")) return "image/png";
|
||||||
|
if (iequals(ext, ".jpe")) return "image/jpeg";
|
||||||
|
if (iequals(ext, ".jpeg")) return "image/jpeg";
|
||||||
|
if (iequals(ext, ".jpg")) return "image/jpeg";
|
||||||
|
if (iequals(ext, ".gif")) return "image/gif";
|
||||||
|
if (iequals(ext, ".bmp")) return "image/bmp";
|
||||||
|
if (iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
|
||||||
|
if (iequals(ext, ".tiff")) return "image/tiff";
|
||||||
|
if (iequals(ext, ".tif")) return "image/tiff";
|
||||||
|
if (iequals(ext, ".svg")) return "image/svg+xml";
|
||||||
|
if (iequals(ext, ".svgz")) return "image/svg+xml";
|
||||||
|
return "application/text";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pathCat(std::string_view base, std::string_view path) {
|
||||||
|
if (base.empty()) return std::string(path);
|
||||||
|
std::string result(base);
|
||||||
|
char constexpr path_separator = '/';
|
||||||
|
if (result.back() == path_separator && path.front() == path_separator) {
|
||||||
|
result.resize(result.size() - 1);
|
||||||
|
} else if (result.back() != path_separator && path.front() != path_separator) {
|
||||||
|
result.append("/");
|
||||||
|
}
|
||||||
|
result.append(path.data(), path.size());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} // namespace ResponseUtility
|
19
Main/ResponseUtility.h
Normal file
19
Main/ResponseUtility.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#ifndef RESPONSEUTILITY_H
|
||||||
|
#define RESPONSEUTILITY_H
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace ResponseUtility {
|
||||||
|
/**
|
||||||
|
* @brief Return a reasonable mime type based on the extension of a file.
|
||||||
|
*/
|
||||||
|
std::string_view mimeType(std::string_view path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Append an HTTP rel-path to a local filesystem path.The returned path is normalized for the
|
||||||
|
* platform.
|
||||||
|
*/
|
||||||
|
std::string pathCat(std::string_view base, std::string_view path);
|
||||||
|
} // namespace ResponseUtility
|
||||||
|
|
||||||
|
#endif // RESPONSEUTILITY_H
|
@ -1,4 +1,5 @@
|
|||||||
#include "RtspServer.h"
|
#include "RtspServer.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
#include <boost/asio/io_context.hpp>
|
#include <boost/asio/io_context.hpp>
|
||||||
#include <boost/asio/strand.hpp>
|
#include <boost/asio/strand.hpp>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@ -39,7 +40,12 @@ RtspServer::RtspServer(boost::asio::io_context &ioContext) : m_d(new RtspServerP
|
|||||||
config.ssl_is_path = 1;
|
config.ssl_is_path = 1;
|
||||||
mk_env_init(&config);
|
mk_env_init(&config);
|
||||||
|
|
||||||
mk_rtsp_server_start(554, 0);
|
uint16_t status = mk_rtsp_server_start(554, 0);
|
||||||
|
|
||||||
|
status = mk_rtc_server_start(7764);
|
||||||
|
if (status == 0) {
|
||||||
|
LOG(error) << "mk_rtc_server_start() failed.";
|
||||||
|
}
|
||||||
|
|
||||||
m_d->media = mk_media_create("__defaultVhost__", "live", "video", 0, 0, 0);
|
m_d->media = mk_media_create("__defaultVhost__", "live", "video", 0, 0, 0);
|
||||||
|
|
||||||
|
100
Main/ServiceLogic.cpp
Normal file
100
Main/ServiceLogic.cpp
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
#include "ServiceLogic.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
|
#include "Core/Singleton.h"
|
||||||
|
#include "HttpSession.h"
|
||||||
|
#include "Settings.h"
|
||||||
|
#include <boost/beast/http/file_body.hpp>
|
||||||
|
#include <boost/url/url_view.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace ServiceLogic {
|
||||||
|
using namespace boost::beast;
|
||||||
|
|
||||||
|
std::string extractToken(const std::string &cookieHeader, const std::string &tokenName = "access_token") {
|
||||||
|
// 格式示例:"access_token=abc123; Path=/; Expires=Wed, 21 Oct 2023 07:28:00 GMT"
|
||||||
|
size_t startPos = cookieHeader.find(tokenName + "=");
|
||||||
|
if (startPos == std::string::npos) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
startPos += tokenName.size() + 1; // 跳过 "token_name="
|
||||||
|
size_t endPos = cookieHeader.find(';', startPos);
|
||||||
|
if (endPos == std::string::npos) {
|
||||||
|
endPos = cookieHeader.size();
|
||||||
|
}
|
||||||
|
std::string token = cookieHeader.substr(startPos, endPos - startPos);
|
||||||
|
|
||||||
|
// 移除可能的引号和空格
|
||||||
|
token.erase(std::remove(token.begin(), token.end(), '"'), token.end());
|
||||||
|
token.erase(std::remove(token.begin(), token.end(), ' '), token.end());
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::beast::http::response<boost::beast::http::string_body>
|
||||||
|
serverError(const boost::beast::http::request<boost::beast::http::string_body> &request,
|
||||||
|
std::string_view errorMessage) {
|
||||||
|
using namespace boost::beast;
|
||||||
|
http::response<http::string_body> res{http::status::internal_server_error, request.version()};
|
||||||
|
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
res.set(http::field::content_type, "text/html");
|
||||||
|
res.keep_alive(request.keep_alive());
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "An error occurred: '" << errorMessage << "'";
|
||||||
|
res.body() = oss.str();
|
||||||
|
res.prepare_payload();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
http::response<http::string_body> badRequest(const http::request<http::string_body> &request, std::string_view why) {
|
||||||
|
http::response<http::string_body> res{http::status::bad_request, request.version()};
|
||||||
|
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
res.set(http::field::content_type, "text/html");
|
||||||
|
res.keep_alive(request.keep_alive());
|
||||||
|
res.body() = std::string(why);
|
||||||
|
res.prepare_payload();
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
void staticFilesDeploy() {
|
||||||
|
using namespace Core;
|
||||||
|
using namespace boost::urls;
|
||||||
|
auto application = Singleton<Danki::Application>::instance();
|
||||||
|
// clang-format off
|
||||||
|
application->insertUrl("/{path*}", [](Danki::HttpSession &session, const Danki::Application::Request &request, const matches &matches) {
|
||||||
|
using namespace boost::beast;
|
||||||
|
url_view view(request.target());
|
||||||
|
auto target = view.path();
|
||||||
|
LOG(info) << target;
|
||||||
|
if (target.find("..") != boost::beast::string_view::npos) {
|
||||||
|
session.reply(ServiceLogic::badRequest(request, "Illegal request-target"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto settings = Singleton<Danki::Settings>::instance();
|
||||||
|
std::string path = ResponseUtility::pathCat(settings->documentRoot(), target);
|
||||||
|
if (target.back() == '/') path.append("index.html");
|
||||||
|
if (std::filesystem::is_directory(path)) path.append("/index.html");
|
||||||
|
boost::beast::error_code ec;
|
||||||
|
http::file_body::value_type body;
|
||||||
|
body.open(path.c_str(), boost::beast::file_mode::scan, ec);
|
||||||
|
if (ec == boost::beast::errc::no_such_file_or_directory) {
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "The resource '" << target << "' was not found.";
|
||||||
|
LOG(error) << oss.str();
|
||||||
|
session.errorReply(request, http::status::not_found, oss.str());
|
||||||
|
return;
|
||||||
|
} else if (ec) {
|
||||||
|
session.reply(ServiceLogic::serverError(request, ec.message()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto const size = body.size();
|
||||||
|
http::response<http::file_body> res{std::piecewise_construct, std::make_tuple(std::move(body)),
|
||||||
|
std::make_tuple(http::status::ok, request.version())};
|
||||||
|
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
res.set(http::field::content_type, ResponseUtility::mimeType(path));
|
||||||
|
res.content_length(size);
|
||||||
|
res.keep_alive(request.keep_alive());
|
||||||
|
session.reply(std::move(res));
|
||||||
|
});
|
||||||
|
// clang-format on
|
||||||
|
}
|
||||||
|
} // namespace ServiceLogic
|
43
Main/ServiceLogic.h
Normal file
43
Main/ServiceLogic.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#ifndef SERVICELOGIC_H
|
||||||
|
#define SERVICELOGIC_H
|
||||||
|
|
||||||
|
#include "Application.h"
|
||||||
|
#include "ResponseUtility.h"
|
||||||
|
#include <boost/beast/http/message.hpp>
|
||||||
|
#include <boost/beast/http/string_body.hpp>
|
||||||
|
#include <boost/beast/http/vector_body.hpp>
|
||||||
|
#include <boost/beast/version.hpp>
|
||||||
|
#include <boost/uuid/uuid_generators.hpp>
|
||||||
|
#include <boost/uuid/uuid_io.hpp>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
using StringRequest = boost::beast::http::request<boost::beast::http::string_body>;
|
||||||
|
|
||||||
|
namespace ServiceLogic {
|
||||||
|
|
||||||
|
// Returns a server error response
|
||||||
|
boost::beast::http::response<boost::beast::http::string_body>
|
||||||
|
serverError(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view errorMessage);
|
||||||
|
|
||||||
|
boost::beast::http::response<boost::beast::http::string_body>
|
||||||
|
badRequest(const boost::beast::http::request<boost::beast::http::string_body> &request, std::string_view why);
|
||||||
|
|
||||||
|
template <class ResponseBody, class RequestBody>
|
||||||
|
boost::beast::http::response<ResponseBody> make_200(const boost::beast::http::request<RequestBody> &request,
|
||||||
|
typename ResponseBody::value_type body,
|
||||||
|
boost::beast::string_view content) {
|
||||||
|
boost::beast::http::response<ResponseBody> response{boost::beast::http::status::ok, request.version()};
|
||||||
|
response.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||||
|
response.set(boost::beast::http::field::content_type, content);
|
||||||
|
response.body() = body;
|
||||||
|
response.prepare_payload();
|
||||||
|
response.keep_alive(request.keep_alive());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
void staticFilesDeploy();
|
||||||
|
|
||||||
|
}; // namespace ServiceLogic
|
||||||
|
|
||||||
|
#endif // SERVICELOGIC_H
|
70
Main/Settings.cpp
Normal file
70
Main/Settings.cpp
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#include "Settings.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
|
#include <boost/algorithm/string/trim.hpp>
|
||||||
|
#include <boost/property_tree/ptree.hpp>
|
||||||
|
#include <boost/property_tree/xml_parser.hpp>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
constexpr auto SettingsFilePath = "settings.xml";
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
Settings::Settings() {
|
||||||
|
if (!std::filesystem::exists(SettingsFilePath)) {
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::save() {
|
||||||
|
using namespace boost::property_tree;
|
||||||
|
ptree ptree;
|
||||||
|
ptree.put("Application.Threads", std::thread::hardware_concurrency());
|
||||||
|
ptree.put("Application.SqlitePath", m_sqlitePath);
|
||||||
|
|
||||||
|
ptree.put("Application.HttpServer.Address", m_server);
|
||||||
|
ptree.put("Application.HttpServer.DocumentRoot", m_documentRoot);
|
||||||
|
ptree.put("Application.HttpServer.DocumentRoot.<xmlcomment>",
|
||||||
|
"静态网页文件存放位置,为空时网站统计将不判断页面页面是否存在");
|
||||||
|
|
||||||
|
xml_writer_settings<std::string> settings('\t', 1);
|
||||||
|
write_xml(SettingsFilePath, ptree, std::locale(), settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Settings::load() {
|
||||||
|
using namespace boost::property_tree;
|
||||||
|
ptree ptree;
|
||||||
|
try {
|
||||||
|
read_xml(SettingsFilePath, ptree);
|
||||||
|
m_sqlitePath = ptree.get<std::string>("Application.SqlitePath");
|
||||||
|
m_threads = ptree.get<uint32_t>("Application.Threads");
|
||||||
|
|
||||||
|
m_server = ptree.get<std::string>("Application.HttpServer.Address");
|
||||||
|
m_documentRoot = ptree.get<std::string>("Application.HttpServer.DocumentRoot");
|
||||||
|
boost::algorithm::trim(m_documentRoot);
|
||||||
|
} catch (const xml_parser_error &error) {
|
||||||
|
LOG(error) << "parse " << SettingsFilePath << " failed: " << error.message();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Settings::threads() const {
|
||||||
|
return m_threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Settings::server() const {
|
||||||
|
return m_server;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t Settings::port() const {
|
||||||
|
return m_port;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Settings::documentRoot() const {
|
||||||
|
return m_documentRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Settings::sqlitePath() const {
|
||||||
|
return m_sqlitePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Danki
|
30
Main/Settings.h
Normal file
30
Main/Settings.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#ifndef __SETTINGS_H__
|
||||||
|
#define __SETTINGS_H__
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
class Settings {
|
||||||
|
public:
|
||||||
|
Settings();
|
||||||
|
void save();
|
||||||
|
void load();
|
||||||
|
|
||||||
|
uint32_t threads() const;
|
||||||
|
std::string server() const;
|
||||||
|
uint16_t port() const;
|
||||||
|
std::string documentRoot() const;
|
||||||
|
std::string sqlitePath() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t m_threads = 1;
|
||||||
|
std::string m_server = "0.0.0.0";
|
||||||
|
uint16_t m_port = 80;
|
||||||
|
|
||||||
|
std::string m_documentRoot = "/data/sdcard/PassengerStatistics/web";
|
||||||
|
std::string m_sqlitePath = "database.sqlite";
|
||||||
|
};
|
||||||
|
} // namespace Danki
|
||||||
|
|
||||||
|
#endif // __SETTINGS_H__
|
41
Main/WebRTC/SignalServer.cpp
Normal file
41
Main/WebRTC/SignalServer.cpp
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#include "SignalServer.h"
|
||||||
|
#include "../Application.h"
|
||||||
|
#include "../HttpSession.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
|
#include "WebSocketSignalSession.h"
|
||||||
|
#include <boost/beast/websocket/rfc6455.hpp>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
SignalServer::SignalServer(Application &app) {
|
||||||
|
using namespace boost::urls;
|
||||||
|
// clang-format off
|
||||||
|
app.insertUrl("/api/v1/webrtc/signal/{id}", [this](HttpSession &session, const Application::Request &request, const matches &matches) {
|
||||||
|
auto id = matches.at("id");
|
||||||
|
if (boost::beast::websocket::is_upgrade(request)) {
|
||||||
|
auto ws = std::make_shared<WebSocketSignalSession>(session.releaseSocket(), *this, id);
|
||||||
|
ws->run(request);
|
||||||
|
} else {
|
||||||
|
LOG(error) << "webrtc client[" << id << "] not upgrade connection, request: " << std::endl << request;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// clang-format on
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalServer::join(const std::string &id, WebSocketSignalSession *client) {
|
||||||
|
m_clients.insert({id, client});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalServer::leave(const std::string &id) {
|
||||||
|
if (m_clients.count(id) > 0) {
|
||||||
|
m_clients.erase(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketSignalSession *SignalServer::client(const std::string &id) {
|
||||||
|
WebSocketSignalSession *ret = nullptr;
|
||||||
|
if (m_clients.count(id) > 0) {
|
||||||
|
ret = m_clients.at(id);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} // namespace Danki
|
24
Main/WebRTC/SignalServer.h
Normal file
24
Main/WebRTC/SignalServer.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#ifndef __SIGNALSERVER_H__
|
||||||
|
#define __SIGNALSERVER_H__
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
class WebSocketSignalSession;
|
||||||
|
|
||||||
|
class SignalServer {
|
||||||
|
public:
|
||||||
|
SignalServer(Application &app);
|
||||||
|
void join(const std::string &id, WebSocketSignalSession *client);
|
||||||
|
void leave(const std::string &id);
|
||||||
|
WebSocketSignalSession *client(const std::string &id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, WebSocketSignalSession *> m_clients;
|
||||||
|
};
|
||||||
|
} // namespace Danki
|
||||||
|
|
||||||
|
#endif // __SIGNALSERVER_H__
|
@ -7,6 +7,8 @@
|
|||||||
#include <boost/json/serialize.hpp>
|
#include <boost/json/serialize.hpp>
|
||||||
#include <rtc/rtc.hpp>
|
#include <rtc/rtc.hpp>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
|
||||||
class WebRTCStreamerPrivate {
|
class WebRTCStreamerPrivate {
|
||||||
public:
|
public:
|
||||||
WebRTCStreamerPrivate(boost::asio::io_context &ioContext) : strand{ioContext.get_executor()} {
|
WebRTCStreamerPrivate(boost::asio::io_context &ioContext) : strand{ioContext.get_executor()} {
|
||||||
@ -135,7 +137,13 @@ void Streamer::start(const std::string &signalServerAddress, uint16_t signalServ
|
|||||||
m_d->websocket = std::make_shared<rtc::WebSocket>(c);
|
m_d->websocket = std::make_shared<rtc::WebSocket>(c);
|
||||||
m_d->websocket->onOpen([]() { LOG(info) << "WebSocket connected, signaling ready"; });
|
m_d->websocket->onOpen([]() { LOG(info) << "WebSocket connected, signaling ready"; });
|
||||||
m_d->websocket->onClosed([]() { LOG(info) << "WebSocket closed"; });
|
m_d->websocket->onClosed([]() { LOG(info) << "WebSocket closed"; });
|
||||||
m_d->websocket->onError([](const std::string &error) { LOG(error) << "WebSocket failed: " << error; });
|
m_d->websocket->onError([this, signalServerAddress, signalServerPort](const std::string &error) {
|
||||||
|
LOG(error) << "WebSocket failed: " << error;
|
||||||
|
|
||||||
|
boost::asio::post(m_d->strand, [this, signalServerAddress, signalServerPort]() {
|
||||||
|
start(signalServerAddress, signalServerPort);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
m_d->websocket->onMessage([this](std::variant<rtc::binary, std::string> data) {
|
m_d->websocket->onMessage([this](std::variant<rtc::binary, std::string> data) {
|
||||||
if (!std::holds_alternative<std::string>(data)) return;
|
if (!std::holds_alternative<std::string>(data)) return;
|
||||||
@ -147,7 +155,7 @@ void Streamer::start(const std::string &signalServerAddress, uint16_t signalServ
|
|||||||
});
|
});
|
||||||
|
|
||||||
const std::string url =
|
const std::string url =
|
||||||
"wss://" + signalServerAddress + ":" + std::to_string(signalServerPort) + "/api/v1/webrtc/signal/" + localId;
|
"ws://" + signalServerAddress + ":" + std::to_string(signalServerPort) + "/api/v1/webrtc/signal/" + localId;
|
||||||
LOG(info) << "URL is " << url;
|
LOG(info) << "URL is " << url;
|
||||||
m_d->websocket->open(url);
|
m_d->websocket->open(url);
|
||||||
LOG(info) << "Waiting for signaling to be connected...";
|
LOG(info) << "Waiting for signaling to be connected...";
|
||||||
@ -175,7 +183,7 @@ void Streamer::push(const uint8_t *data, uint32_t size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Streamer::Streamer(boost::asio::io_context &ioContext) : m_d{new WebRTCStreamerPrivate(ioContext)} {
|
Streamer::Streamer(boost::asio::io_context &ioContext) : m_d{new WebRTCStreamerPrivate(ioContext)} {
|
||||||
rtc::InitLogger(rtc::LogLevel::Debug);
|
rtc::InitLogger(rtc::LogLevel::Info);
|
||||||
std::string stunServer = "stun:amass.fun:5349"; // ssl
|
std::string stunServer = "stun:amass.fun:5349"; // ssl
|
||||||
m_d->configuration.iceServers.emplace_back(stunServer);
|
m_d->configuration.iceServers.emplace_back(stunServer);
|
||||||
LOG(info) << "STUN server is " << stunServer;
|
LOG(info) << "STUN server is " << stunServer;
|
||||||
@ -185,3 +193,4 @@ Streamer::Streamer(boost::asio::io_context &ioContext) : m_d{new WebRTCStreamerP
|
|||||||
|
|
||||||
m_d->configuration.disableAutoNegotiation = true;
|
m_d->configuration.disableAutoNegotiation = true;
|
||||||
}
|
}
|
||||||
|
}
|
@ -9,6 +9,8 @@ namespace asio {
|
|||||||
class io_context;
|
class io_context;
|
||||||
}
|
}
|
||||||
} // namespace boost
|
} // namespace boost
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
class WebRTCStreamerPrivate;
|
class WebRTCStreamerPrivate;
|
||||||
|
|
||||||
class Streamer {
|
class Streamer {
|
||||||
@ -21,5 +23,6 @@ public:
|
|||||||
private:
|
private:
|
||||||
WebRTCStreamerPrivate *m_d = nullptr;
|
WebRTCStreamerPrivate *m_d = nullptr;
|
||||||
};
|
};
|
||||||
|
} // namespace Danki
|
||||||
|
|
||||||
#endif // __WEBRTCSTREAMER_H__
|
#endif // __WEBRTCSTREAMER_H__
|
109
Main/WebRTC/WebSocketSignalSession.cpp
Normal file
109
Main/WebRTC/WebSocketSignalSession.cpp
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#include "WebSocketSignalSession.h"
|
||||||
|
#include "Core/Logger.h"
|
||||||
|
#include "SignalServer.h"
|
||||||
|
#include <boost/json/parse.hpp>
|
||||||
|
#include <boost/json/serialize.hpp>
|
||||||
|
#include <boost/scope/scope_exit.hpp>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
WebSocketSignalSession::WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket, SignalServer &server,
|
||||||
|
const std::string &id)
|
||||||
|
: m_ws(std::move(socket)), m_server(server), m_id(id) {
|
||||||
|
m_server.join(m_id, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketSignalSession::~WebSocketSignalSession() {
|
||||||
|
m_server.leave(m_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketSignalSession::onAccept(boost::beast::error_code ec) {
|
||||||
|
if (ec) {
|
||||||
|
if (ec == boost::asio::error::operation_aborted || ec == boost::beast::websocket::error::closed) return;
|
||||||
|
LOG(error) << "accept: " << ec.message() << "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG(info) << "accept websocket target: " << m_target << ", id: " << m_id;
|
||||||
|
|
||||||
|
// Read a message
|
||||||
|
m_ws.async_read(m_buffer, boost::beast::bind_front_handler(&WebSocketSignalSession::onRead, shared_from_this()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketSignalSession::onRead(const boost::beast::error_code &error, std::size_t bytesTransferred) {
|
||||||
|
if (error) {
|
||||||
|
if (error == boost::asio::error::operation_aborted || error == boost::beast::websocket::error::closed) return;
|
||||||
|
LOG(error) << error << ": " << error.message();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::scope::scope_exit raii([this] {
|
||||||
|
m_buffer.consume(m_buffer.size()); // Clear the buffer
|
||||||
|
m_ws.async_read(m_buffer,
|
||||||
|
boost::beast::bind_front_handler(&WebSocketSignalSession::onRead, shared_from_this()));
|
||||||
|
});
|
||||||
|
if (!m_ws.got_text()) {
|
||||||
|
LOG(warning) << "current not supported binary message.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto message = boost::beast::buffers_to_string(m_buffer.data());
|
||||||
|
auto rootObject = boost::json::parse(message);
|
||||||
|
auto &root = rootObject.as_object();
|
||||||
|
if (root.contains("id")) {
|
||||||
|
if (!root.at("id").is_string()) {
|
||||||
|
LOG(warning) << "wrong format.";
|
||||||
|
m_ws.close(boost::beast::websocket::close_code::normal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto destinationId = std::string(root["id"].as_string());
|
||||||
|
auto destination = m_server.client(destinationId);
|
||||||
|
if (destination == nullptr) {
|
||||||
|
LOG(info) << "client " << destinationId << " not found.";
|
||||||
|
} else {
|
||||||
|
root["id"] = m_id;
|
||||||
|
auto reply = std::make_shared<std::string>(boost::json::serialize(root));
|
||||||
|
destination->send(reply);
|
||||||
|
}
|
||||||
|
LOG(info) << message;
|
||||||
|
} else if (root.contains("type")) {
|
||||||
|
auto &type = root.at("type").as_string();
|
||||||
|
if (type == "ping") {
|
||||||
|
boost::json::object object;
|
||||||
|
object["type"] = "pong";
|
||||||
|
send(boost::json::serialize(object));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG(info) << message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketSignalSession::send(const std::shared_ptr<std::string> &ss) {
|
||||||
|
boost::asio::post(m_ws.get_executor(), [ptr = weak_from_this(), ss]() {
|
||||||
|
if (ptr.expired()) return;
|
||||||
|
auto self = ptr.lock();
|
||||||
|
self->m_queue.push_back(ss);
|
||||||
|
if (self->m_queue.size() > 1) return; // 之前已经有发送了
|
||||||
|
self->m_ws.text();
|
||||||
|
self->m_ws.async_write(
|
||||||
|
boost::asio::buffer(*self->m_queue.front()),
|
||||||
|
boost::beast::bind_front_handler(&WebSocketSignalSession::on_write, self->shared_from_this()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketSignalSession::send(std::string &&message) {
|
||||||
|
return send(std::make_shared<std::string>(std::move(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketSignalSession::on_write(boost::beast::error_code ec, std::size_t) {
|
||||||
|
if (ec) {
|
||||||
|
// Don't report these
|
||||||
|
if (ec == boost::asio::error::operation_aborted || ec == boost::beast::websocket::error::closed) return;
|
||||||
|
std::cerr << "write: " << ec.message() << "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the string from the queue
|
||||||
|
m_queue.erase(m_queue.begin());
|
||||||
|
if (!m_queue.empty())
|
||||||
|
m_ws.async_write(boost::asio::buffer(*m_queue.front()),
|
||||||
|
boost::beast::bind_front_handler(&WebSocketSignalSession::on_write, shared_from_this()));
|
||||||
|
}
|
||||||
|
} // namespace Danki
|
58
Main/WebRTC/WebSocketSignalSession.h
Normal file
58
Main/WebRTC/WebSocketSignalSession.h
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#ifndef __WEBSOCKETSIGNALSESSION_H__
|
||||||
|
#define __WEBSOCKETSIGNALSESSION_H__
|
||||||
|
|
||||||
|
#include <boost/beast.hpp>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Danki {
|
||||||
|
|
||||||
|
class SignalServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Represents an active WebSocket connection to the server
|
||||||
|
*/
|
||||||
|
class WebSocketSignalSession : public std::enable_shared_from_this<WebSocketSignalSession> {
|
||||||
|
public:
|
||||||
|
WebSocketSignalSession(boost::asio::ip::tcp::socket &&socket, SignalServer &server, const std::string &id);
|
||||||
|
|
||||||
|
~WebSocketSignalSession();
|
||||||
|
|
||||||
|
template <class Body, class Allocator>
|
||||||
|
void run(boost::beast::http::request<Body, boost::beast::http::basic_fields<Allocator>> request) {
|
||||||
|
using namespace boost::beast::http;
|
||||||
|
using namespace boost::beast::websocket;
|
||||||
|
// Set suggested timeout settings for the websocket
|
||||||
|
m_ws.set_option(stream_base::timeout::suggested(boost::beast::role_type::server));
|
||||||
|
m_target = request.target();
|
||||||
|
// Set a decorator to change the Server of the handshake
|
||||||
|
m_ws.set_option(stream_base::decorator([](response_type &response) {
|
||||||
|
response.set(field::server, std::string(BOOST_BEAST_VERSION_STRING) + " websocket-chat-multi");
|
||||||
|
}));
|
||||||
|
// LOG(info) << request.base().target(); //get path
|
||||||
|
// Accept the websocket handshake
|
||||||
|
m_ws.async_accept(request, [self{shared_from_this()}](boost::beast::error_code ec) { self->onAccept(ec); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message
|
||||||
|
void send(const std::shared_ptr<std::string> &ss);
|
||||||
|
void send(std::string &&message);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onAccept(boost::beast::error_code ec);
|
||||||
|
void onRead(const boost::beast::error_code &error, std::size_t bytesTransferred);
|
||||||
|
void on_write(boost::beast::error_code ec, std::size_t bytes_transferred);
|
||||||
|
|
||||||
|
private:
|
||||||
|
boost::beast::websocket::stream<boost::beast::tcp_stream> m_ws;
|
||||||
|
SignalServer &m_server;
|
||||||
|
std::string m_id;
|
||||||
|
std::string m_target;
|
||||||
|
boost::beast::flat_buffer m_buffer;
|
||||||
|
std::vector<std::shared_ptr<std::string>> m_queue;
|
||||||
|
};
|
||||||
|
} // namespace Danki
|
||||||
|
|
||||||
|
#endif // __WEBSOCKETSIGNALSESSION_H__
|
@ -1,3 +1,4 @@
|
|||||||
|
#include "Application.h"
|
||||||
#include "Camera.h"
|
#include "Camera.h"
|
||||||
#include "Core/DateTime.h"
|
#include "Core/DateTime.h"
|
||||||
#include "Core/IoContext.h"
|
#include "Core/IoContext.h"
|
||||||
@ -8,47 +9,47 @@
|
|||||||
#include "WebRTC/Streamer.h"
|
#include "WebRTC/Streamer.h"
|
||||||
#include "rw_mpp_api.h"
|
#include "rw_mpp_api.h"
|
||||||
#include <boost/asio/signal_set.hpp>
|
#include <boost/asio/signal_set.hpp>
|
||||||
|
#include <boost/scope/scope_exit.hpp>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
int main(int argc, char const *argv[]) {
|
int main(int argc, char const *argv[]) try {
|
||||||
using namespace Core;
|
using namespace Core;
|
||||||
|
using namespace Danki;
|
||||||
LOG(info) << "app start...";
|
LOG(info) << "app start...";
|
||||||
int status = rw_mpp__init();
|
int status = rw_mpp__init();
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
LOG(error) << "rw_mpp__init() failed, status: " << status;
|
LOG(error) << "rw_mpp__init() failed, status: " << status;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
try {
|
boost::scope::scope_exit raii([] {
|
||||||
auto camera = Singleton<Camera>::construct();
|
LOG(info) << "app exit.";
|
||||||
auto ioContext = Singleton<IoContext>::construct(std::thread::hardware_concurrency());
|
rw_mpp__finalize();
|
||||||
auto rtsp = std::make_shared<RtspServer>(*ioContext->ioContext());
|
});
|
||||||
auto streamer = std::make_shared<Streamer>(*ioContext->ioContext());
|
|
||||||
streamer->start("amass.fun", 443);
|
|
||||||
|
|
||||||
auto video = std::make_shared<VideoInput>(2592, 1536);
|
auto application = Singleton<Application>::construct();
|
||||||
video->setPacketHandler([&](const uint8_t *data, uint32_t size) {
|
|
||||||
rtsp->push(data, size);
|
|
||||||
streamer->push(data, size);
|
|
||||||
});
|
|
||||||
video->start();
|
|
||||||
// video->startFileInput("/data/sdcard/HM1.264", 1280, 720);
|
|
||||||
video->startEncode();
|
|
||||||
boost::asio::signal_set signals(*ioContext->ioContext(), SIGINT);
|
|
||||||
signals.async_wait([&](boost::system::error_code const &, int signal) {
|
|
||||||
LOG(info) << "capture " << (signal == SIGINT ? "SIGINT" : "SIGTERM") << ",stop!";
|
|
||||||
video.reset();
|
|
||||||
rw_mpp__finalize();
|
|
||||||
ioContext->ioContext()->stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
ioContext->run(true);
|
auto camera = Singleton<Camera>::construct();
|
||||||
} catch (const boost::exception &e) {
|
auto rtsp = std::make_shared<RtspServer>(application->ioContext());
|
||||||
LOG(error) << "error";
|
auto streamer = std::make_shared<Streamer>(application->ioContext());
|
||||||
} catch (const std::exception &e) {
|
streamer->start("127.0.0.1", 80);
|
||||||
LOG(error) << e.what();
|
|
||||||
}
|
|
||||||
|
|
||||||
rw_mpp__finalize();
|
auto video = std::make_shared<VideoInput>(2592, 1536);
|
||||||
LOG(info) << "app exit.";
|
video->setPacketHandler([&](const uint8_t *data, uint32_t size) {
|
||||||
return 0;
|
rtsp->push(data, size);
|
||||||
|
streamer->push(data, size);
|
||||||
|
});
|
||||||
|
video->start();
|
||||||
|
video->startEncode();
|
||||||
|
boost::asio::signal_set signals(application->ioContext(), SIGINT);
|
||||||
|
signals.async_wait([&](boost::system::error_code const &, int signal) {
|
||||||
|
LOG(info) << "capture " << (signal == SIGINT ? "SIGINT" : "SIGTERM") << ",stop!";
|
||||||
|
video.reset();
|
||||||
|
rw_mpp__finalize();
|
||||||
|
application->ioContext().stop();
|
||||||
|
});
|
||||||
|
return application->exec();
|
||||||
|
} catch (const boost::exception &e) {
|
||||||
|
LOG(error) << "error";
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
LOG(error) << e.what();
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ function init() {
|
|||||||
echo "put ${BOOST_LIBDIR}/libboost_system.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
echo "put ${BOOST_LIBDIR}/libboost_system.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
||||||
echo "put ${BOOST_LIBDIR}/libboost_filesystem.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
echo "put ${BOOST_LIBDIR}/libboost_filesystem.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
||||||
echo "put ${BOOST_LIBDIR}/libboost_thread.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
echo "put ${BOOST_LIBDIR}/libboost_thread.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
||||||
# echo "put ${BOOST_LIBDIR}/libboost_url.so.1.84.0 /system/lib" | sftp -i resources/ssh_host_rsa_key_ok root@${TARGET_IP}
|
echo "put ${BOOST_LIBDIR}/libboost_url.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
||||||
echo "put ${BOOST_LIBDIR}/libboost_json.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
echo "put ${BOOST_LIBDIR}/libboost_json.so.1.87.0 /data/sdcard/PassengerStatistics/lib" | sftp danki
|
||||||
# echo "put ${BOOST_LIBDIR}/libboost_program_options.so.1.84.0 /system/lib" | sftp -i resources/ssh_host_rsa_key_ok root@${TARGET_IP}
|
# echo "put ${BOOST_LIBDIR}/libboost_program_options.so.1.84.0 /system/lib" | sftp -i resources/ssh_host_rsa_key_ok root@${TARGET_IP}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user