验证本地http可以查看webrtc.
This commit is contained in:
parent
70763da9b0
commit
ee1aaa1bf3
@ -17,9 +17,11 @@
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/runtime": "^7.26.10",
|
||||
"antd": "^5.24.4",
|
||||
"babel-loader": "^10.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^7.3.0",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.0"
|
||||
|
@ -1,9 +1,13 @@
|
||||
import React from "react";
|
||||
import WebRTCClient from "./WebRTCClient";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
return <div>
|
||||
<h1>
|
||||
从 Webpack 和 Babel 开始搭建 React 项目
|
||||
</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)
|
||||
|
||||
add_executable(PassengerStatistics main.cpp
|
||||
Application.h Application.cpp
|
||||
Camera.h Camera.cpp
|
||||
HttpSession.h HttpSession.cpp
|
||||
ImageUtilities.h ImageUtilities.cpp
|
||||
RtspServer.h RtspServer.cpp
|
||||
ResponseUtility.h ResponseUtility.cpp
|
||||
ServiceLogic.h ServiceLogic.cpp
|
||||
Settings.h Settings.cpp
|
||||
VideoInput.h VideoInput.cpp
|
||||
|
||||
WebRTC/Streamer.h WebRTC/Streamer.cpp
|
||||
WebRTC/Helpers.h WebRTC/Helpers.cpp
|
||||
WebRTC/SignalServer.h WebRTC/SignalServer.cpp
|
||||
WebRTC/WebSocketSignalSession.h WebRTC/WebSocketSignalSession.cpp
|
||||
)
|
||||
|
||||
target_include_directories(PassengerStatistics
|
||||
@ -28,6 +35,7 @@ target_link_directories(PassengerStatistics
|
||||
|
||||
target_link_libraries(PassengerStatistics
|
||||
PRIVATE Kylin::Core
|
||||
PRIVATE Kylin::Router
|
||||
PRIVATE LibDataChannel::LibDataChannel
|
||||
PRIVATE OpenSSL::SSL
|
||||
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 "Core/Logger.h"
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <chrono>
|
||||
@ -39,7 +40,12 @@ RtspServer::RtspServer(boost::asio::io_context &ioContext) : m_d(new RtspServerP
|
||||
config.ssl_is_path = 1;
|
||||
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);
|
||||
|
||||
|
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 <rtc/rtc.hpp>
|
||||
|
||||
namespace Danki {
|
||||
|
||||
class WebRTCStreamerPrivate {
|
||||
public:
|
||||
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->onOpen([]() { LOG(info) << "WebSocket connected, signaling ready"; });
|
||||
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) {
|
||||
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 =
|
||||
"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;
|
||||
m_d->websocket->open(url);
|
||||
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)} {
|
||||
rtc::InitLogger(rtc::LogLevel::Debug);
|
||||
rtc::InitLogger(rtc::LogLevel::Info);
|
||||
std::string stunServer = "stun:amass.fun:5349"; // ssl
|
||||
m_d->configuration.iceServers.emplace_back(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;
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ namespace asio {
|
||||
class io_context;
|
||||
}
|
||||
} // namespace boost
|
||||
|
||||
namespace Danki {
|
||||
class WebRTCStreamerPrivate;
|
||||
|
||||
class Streamer {
|
||||
@ -21,5 +23,6 @@ public:
|
||||
private:
|
||||
WebRTCStreamerPrivate *m_d = nullptr;
|
||||
};
|
||||
} // namespace Danki
|
||||
|
||||
#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 "Core/DateTime.h"
|
||||
#include "Core/IoContext.h"
|
||||
@ -8,22 +9,29 @@
|
||||
#include "WebRTC/Streamer.h"
|
||||
#include "rw_mpp_api.h"
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/scope/scope_exit.hpp>
|
||||
#include <fstream>
|
||||
|
||||
int main(int argc, char const *argv[]) {
|
||||
int main(int argc, char const *argv[]) try {
|
||||
using namespace Core;
|
||||
using namespace Danki;
|
||||
LOG(info) << "app start...";
|
||||
int status = rw_mpp__init();
|
||||
if (status != 0) {
|
||||
LOG(error) << "rw_mpp__init() failed, status: " << status;
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
boost::scope::scope_exit raii([] {
|
||||
LOG(info) << "app exit.";
|
||||
rw_mpp__finalize();
|
||||
});
|
||||
|
||||
auto application = Singleton<Application>::construct();
|
||||
|
||||
auto camera = Singleton<Camera>::construct();
|
||||
auto ioContext = Singleton<IoContext>::construct(std::thread::hardware_concurrency());
|
||||
auto rtsp = std::make_shared<RtspServer>(*ioContext->ioContext());
|
||||
auto streamer = std::make_shared<Streamer>(*ioContext->ioContext());
|
||||
streamer->start("amass.fun", 443);
|
||||
auto rtsp = std::make_shared<RtspServer>(application->ioContext());
|
||||
auto streamer = std::make_shared<Streamer>(application->ioContext());
|
||||
streamer->start("127.0.0.1", 80);
|
||||
|
||||
auto video = std::make_shared<VideoInput>(2592, 1536);
|
||||
video->setPacketHandler([&](const uint8_t *data, uint32_t size) {
|
||||
@ -31,24 +39,17 @@ int main(int argc, char const *argv[]) {
|
||||
streamer->push(data, size);
|
||||
});
|
||||
video->start();
|
||||
// video->startFileInput("/data/sdcard/HM1.264", 1280, 720);
|
||||
video->startEncode();
|
||||
boost::asio::signal_set signals(*ioContext->ioContext(), SIGINT);
|
||||
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();
|
||||
ioContext->ioContext()->stop();
|
||||
application->ioContext().stop();
|
||||
});
|
||||
|
||||
ioContext->run(true);
|
||||
return application->exec();
|
||||
} catch (const boost::exception &e) {
|
||||
LOG(error) << "error";
|
||||
} catch (const std::exception &e) {
|
||||
LOG(error) << e.what();
|
||||
}
|
||||
|
||||
rw_mpp__finalize();
|
||||
LOG(info) << "app exit.";
|
||||
return 0;
|
||||
}
|
||||
|
@ -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_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_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_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