mirror of
https://github.com/eatfishfish/openpilot-server.git
synced 2026-08-22 04:42:32 +00:00
增加远程、本地查看摄像头功能
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
Import('env', 'arch', 'common', 'messaging', 'visionipc')
|
||||
|
||||
if arch != "Darwin":
|
||||
streamup_src = 'streamup845.cc' if arch == "larch64" else 'streamup.cc'
|
||||
libs = [visionipc, messaging, common,
|
||||
'avcodec', 'avutil',
|
||||
'OpenCL',
|
||||
'pthread']
|
||||
if arch == "larch64":
|
||||
libs += ['yuv']
|
||||
env.Library('fp', [streamup_src], LIBS=libs)
|
||||
env.Program('h264_streamer', ['main.cc', streamup_src], LIBS=libs)
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "msgq/visionipc/visionipc_client.h"
|
||||
#ifdef QCOM2
|
||||
#include "selfdrive/fp/streamup845.h"
|
||||
#else
|
||||
#include "selfdrive/fp/streamup.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int DEFAULT_FPS = 20;
|
||||
constexpr int DEFAULT_BITRATE = 2'000'000;
|
||||
|
||||
struct StreamConfig {
|
||||
VisionStreamType type;
|
||||
const char *camera;
|
||||
const char *thread_name;
|
||||
};
|
||||
|
||||
void stream_camera(const StreamConfig config, ExitHandler *do_exit) {
|
||||
util::set_thread_name(config.thread_name);
|
||||
|
||||
while (!*do_exit) {
|
||||
VisionIpcClient client("camerad", config.type, true);
|
||||
if (!client.connect(false)) {
|
||||
util::sleep_for(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
const VisionBuf &info = client.buffers[0];
|
||||
if (info.width == 0 || info.height == 0) {
|
||||
fprintf(stderr, "%s invalid VisionBuf size\n", config.camera);
|
||||
util::sleep_for(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s H.264 streamer connected: %zux%zu\n",
|
||||
config.camera, info.width, info.height);
|
||||
|
||||
AmdH264VaapiEncoder encoder(
|
||||
static_cast<int>(info.width),
|
||||
static_cast<int>(info.height),
|
||||
config.camera,
|
||||
DEFAULT_FPS,
|
||||
DEFAULT_BITRATE,
|
||||
"");
|
||||
|
||||
while (!*do_exit && client.is_connected()) {
|
||||
VisionIpcBufExtra extra = {};
|
||||
VisionBuf *buf = client.recv(&extra, 1000);
|
||||
if (!buf) {
|
||||
continue;
|
||||
}
|
||||
if (buf->get_frame_id() != extra.frame_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!encoder.encode_frame(buf)) {
|
||||
util::sleep_for(50);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s VisionIPC disconnected, reconnecting\n", config.camera);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
ExitHandler do_exit;
|
||||
const std::vector<StreamConfig> streams = {
|
||||
{VISION_STREAM_ROAD, "roadCameraState", "h264_road"},
|
||||
{VISION_STREAM_WIDE_ROAD, "wideRoadCameraState", "h264_wide"},
|
||||
};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(streams.size());
|
||||
for (const StreamConfig &config : streams) {
|
||||
threads.emplace_back(stream_camera, config, &do_exit);
|
||||
}
|
||||
for (std::thread &thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
#include "selfdrive/fp/streamup845.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <arpa/inet.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "third_party/libyuv/include/libyuv.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/opt.h>
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto RECONNECT_DELAY = std::chrono::seconds(2);
|
||||
|
||||
bool write_all(int fd, const uint8_t *data, size_t size) {
|
||||
while (size > 0) {
|
||||
const ssize_t written = send(fd, data, size, MSG_NOSIGNAL);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
data += written;
|
||||
size -= static_cast<size_t>(written);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AmdH264VaapiEncoder::AmdH264VaapiEncoder(int width, int height, const std::string &camera,
|
||||
int fps, int bitrate, const std::string &device_path,
|
||||
const std::string &host, int port)
|
||||
: width_(width),
|
||||
height_(height),
|
||||
fps_(fps),
|
||||
bitrate_(bitrate),
|
||||
camera_(camera),
|
||||
host_(host),
|
||||
port_(port),
|
||||
next_reconnect_(std::chrono::steady_clock::now()) {
|
||||
(void)device_path;
|
||||
if (const char *host_env = getenv("H264_WS_HOST"); host_env && host_env[0] != '\0') {
|
||||
host_ = host_env;
|
||||
}
|
||||
if (const char *port_env = getenv("H264_WS_PORT"); port_env && port_env[0] != '\0') {
|
||||
port_ = std::max(1, atoi(port_env));
|
||||
}
|
||||
if (const char *fps_env = getenv("H264_WS_FPS"); fps_env && fps_env[0] != '\0') {
|
||||
fps_ = std::max(1, atoi(fps_env));
|
||||
}
|
||||
if (const char *bitrate_env = getenv("H264_WS_BITRATE"); bitrate_env && bitrate_env[0] != '\0') {
|
||||
bitrate_ = std::max(1, atoi(bitrate_env));
|
||||
}
|
||||
|
||||
gop_size_ = fps_ > 0 ? fps_ * 2 : 40;
|
||||
sw_frame_ = av_frame_alloc();
|
||||
convert_buf_.resize(static_cast<size_t>(width_) * height_ * 3 / 2);
|
||||
}
|
||||
|
||||
AmdH264VaapiEncoder::~AmdH264VaapiEncoder() {
|
||||
close_socket();
|
||||
close();
|
||||
av_frame_free(&sw_frame_);
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::open() {
|
||||
if (opened_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
codec_ = avcodec_find_encoder(AV_CODEC_ID_H264);
|
||||
if (!codec_) {
|
||||
LOGE("FFmpeg H.264 encoder not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
codec_ctx_ = avcodec_alloc_context3(codec_);
|
||||
if (!codec_ctx_) {
|
||||
LOGE("avcodec_alloc_context3 failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
codec_ctx_->width = width_;
|
||||
codec_ctx_->height = height_;
|
||||
codec_ctx_->time_base = AVRational{1, fps_};
|
||||
codec_ctx_->framerate = AVRational{fps_, 1};
|
||||
codec_ctx_->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
codec_ctx_->bit_rate = bitrate_;
|
||||
codec_ctx_->gop_size = gop_size_;
|
||||
codec_ctx_->max_b_frames = 0;
|
||||
codec_ctx_->color_range = AVCOL_RANGE_MPEG;
|
||||
codec_ctx_->colorspace = AVCOL_SPC_BT709;
|
||||
codec_ctx_->color_primaries = AVCOL_PRI_BT709;
|
||||
codec_ctx_->color_trc = AVCOL_TRC_BT709;
|
||||
|
||||
AVDictionary *opts = nullptr;
|
||||
av_dict_set(&opts, "preset", "ultrafast", 0);
|
||||
av_dict_set(&opts, "tune", "zerolatency", 0);
|
||||
av_dict_set(&opts, "x264-params", "annexb=1:repeat-headers=1", 0);
|
||||
const int ret = avcodec_open2(codec_ctx_, codec_, &opts);
|
||||
av_dict_free(&opts);
|
||||
if (ret < 0) {
|
||||
LOGE("avcodec_open2(H.264) failed: %d", ret);
|
||||
release();
|
||||
return false;
|
||||
}
|
||||
|
||||
opened_ = true;
|
||||
frame_idx_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AmdH264VaapiEncoder::release() {
|
||||
if (codec_ctx_) {
|
||||
avcodec_free_context(&codec_ctx_);
|
||||
}
|
||||
opened_ = false;
|
||||
}
|
||||
|
||||
void AmdH264VaapiEncoder::close() {
|
||||
if (!opened_ && !codec_ctx_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (codec_ctx_) {
|
||||
avcodec_send_frame(codec_ctx_, nullptr);
|
||||
drain_packets(nullptr);
|
||||
}
|
||||
release();
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::drain_packets(std::vector<uint8_t> *out) {
|
||||
AVPacket *pkt = av_packet_alloc();
|
||||
if (!pkt) {
|
||||
LOGE("av_packet_alloc failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const int ret = avcodec_receive_packet(codec_ctx_, pkt);
|
||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
||||
av_packet_free(&pkt);
|
||||
return true;
|
||||
}
|
||||
if (ret < 0) {
|
||||
LOGE("avcodec_receive_packet failed: %d", ret);
|
||||
av_packet_free(&pkt);
|
||||
return false;
|
||||
}
|
||||
if (out) {
|
||||
out->insert(out->end(), pkt->data, pkt->data + pkt->size);
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
}
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::ensure_connected() {
|
||||
if (socket_fd_ >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now < next_reconnect_) {
|
||||
return false;
|
||||
}
|
||||
next_reconnect_ = now + RECONNECT_DELAY;
|
||||
|
||||
addrinfo hints = {};
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
addrinfo *result = nullptr;
|
||||
const std::string port = std::to_string(port_);
|
||||
if (getaddrinfo(host_.c_str(), port.c_str(), &hints, &result) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) {
|
||||
const int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (fd < 0) {
|
||||
continue;
|
||||
}
|
||||
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) {
|
||||
socket_fd_ = fd;
|
||||
break;
|
||||
}
|
||||
::close(fd);
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
|
||||
if (socket_fd_ < 0) {
|
||||
return false;
|
||||
}
|
||||
if (!send_handshake() || !send_camera_register()) {
|
||||
close_socket();
|
||||
return false;
|
||||
}
|
||||
|
||||
has_viewer_ = false;
|
||||
restart_encoder_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::send_handshake() {
|
||||
const std::string path = "/ingest?camera=" + camera_;
|
||||
const std::string request =
|
||||
"GET " + path + " HTTP/1.1\r\n"
|
||||
"Host: " + host_ + ":" + std::to_string(port_) + "\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: ZHJhZ29ucGlsb3QtaDI2NA==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n";
|
||||
if (!write_all(socket_fd_, reinterpret_cast<const uint8_t *>(request.data()), request.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string response;
|
||||
char buffer[512];
|
||||
while (response.find("\r\n\r\n") == std::string::npos && response.size() < 4096) {
|
||||
const ssize_t size = recv(socket_fd_, buffer, sizeof(buffer), 0);
|
||||
if (size <= 0) {
|
||||
return false;
|
||||
}
|
||||
response.append(buffer, static_cast<size_t>(size));
|
||||
}
|
||||
return response.find(" 101 ") != std::string::npos;
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::send_camera_register() {
|
||||
return send_packet(0, reinterpret_cast<const uint8_t *>(camera_.data()), camera_.size());
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::send_packet(uint8_t cmd, const uint8_t *data, size_t size) {
|
||||
if (size > 0xffffff) {
|
||||
LOGE("H.264 websocket payload too large: %zu", size);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> packet;
|
||||
packet.reserve(size + 4);
|
||||
packet.push_back(cmd);
|
||||
packet.push_back(static_cast<uint8_t>((size >> 16) & 0xff));
|
||||
packet.push_back(static_cast<uint8_t>((size >> 8) & 0xff));
|
||||
packet.push_back(static_cast<uint8_t>(size & 0xff));
|
||||
packet.insert(packet.end(), data, data + size);
|
||||
return send_ws_binary(packet.data(), packet.size());
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::send_ws_binary(const uint8_t *data, size_t size) {
|
||||
std::vector<uint8_t> header = {0x82};
|
||||
if (size <= 125) {
|
||||
header.push_back(static_cast<uint8_t>(size));
|
||||
} else if (size <= 0xffff) {
|
||||
header.push_back(126);
|
||||
header.push_back(static_cast<uint8_t>((size >> 8) & 0xff));
|
||||
header.push_back(static_cast<uint8_t>(size & 0xff));
|
||||
} else {
|
||||
header.push_back(127);
|
||||
for (int shift = 56; shift >= 0; shift -= 8) {
|
||||
header.push_back(static_cast<uint8_t>((static_cast<uint64_t>(size) >> shift) & 0xff));
|
||||
}
|
||||
}
|
||||
return write_all(socket_fd_, header.data(), header.size()) && write_all(socket_fd_, data, size);
|
||||
}
|
||||
|
||||
void AmdH264VaapiEncoder::read_control_messages() {
|
||||
if (socket_fd_ < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
uint8_t data[4096];
|
||||
const ssize_t size = recv(socket_fd_, data, sizeof(data), MSG_DONTWAIT);
|
||||
if (size == 0) {
|
||||
close_socket();
|
||||
return;
|
||||
}
|
||||
if (size < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
|
||||
break;
|
||||
}
|
||||
close_socket();
|
||||
return;
|
||||
}
|
||||
rx_buffer_.insert(rx_buffer_.end(), data, data + size);
|
||||
}
|
||||
|
||||
size_t offset = 0;
|
||||
while (rx_buffer_.size() - offset >= 2) {
|
||||
const uint8_t *header = rx_buffer_.data() + offset;
|
||||
const uint8_t opcode = header[0] & 0x0f;
|
||||
const bool masked = (header[1] & 0x80) != 0;
|
||||
uint64_t payload_size = header[1] & 0x7f;
|
||||
size_t header_size = 2;
|
||||
|
||||
if (payload_size == 126) {
|
||||
if (rx_buffer_.size() - offset < 4) break;
|
||||
payload_size = (static_cast<uint64_t>(header[2]) << 8) | header[3];
|
||||
header_size = 4;
|
||||
} else if (payload_size == 127) {
|
||||
if (rx_buffer_.size() - offset < 10) break;
|
||||
payload_size = 0;
|
||||
for (size_t index = 2; index < 10; ++index) {
|
||||
payload_size = (payload_size << 8) | header[index];
|
||||
}
|
||||
header_size = 10;
|
||||
}
|
||||
|
||||
if (payload_size > 1024 * 1024) {
|
||||
close_socket();
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t mask_size = masked ? 4 : 0;
|
||||
const uint64_t frame_size = header_size + mask_size + payload_size;
|
||||
if (frame_size > rx_buffer_.size() - offset) break;
|
||||
|
||||
const uint8_t *mask = masked ? header + header_size : nullptr;
|
||||
const uint8_t *payload_data = header + header_size + mask_size;
|
||||
std::vector<uint8_t> payload(payload_data, payload_data + payload_size);
|
||||
if (masked) {
|
||||
for (size_t index = 0; index < payload.size(); ++index) {
|
||||
payload[index] ^= mask[index % 4];
|
||||
}
|
||||
}
|
||||
offset += static_cast<size_t>(frame_size);
|
||||
|
||||
if (opcode == 0x8) {
|
||||
close_socket();
|
||||
return;
|
||||
}
|
||||
if ((opcode == 0x1 || opcode == 0x2) && payload.size() >= 5 && payload[0] == 2) {
|
||||
const bool has_viewer = payload[4] == '1';
|
||||
if (has_viewer && !has_viewer_) {
|
||||
restart_encoder_ = true;
|
||||
}
|
||||
has_viewer_ = has_viewer;
|
||||
} else if ((opcode == 0x1 || opcode == 0x2) && payload.size() >= 4 && payload[0] == 3) {
|
||||
restart_encoder_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset > 0) {
|
||||
rx_buffer_.erase(rx_buffer_.begin(), rx_buffer_.begin() + offset);
|
||||
}
|
||||
}
|
||||
|
||||
void AmdH264VaapiEncoder::close_socket() {
|
||||
if (socket_fd_ >= 0) {
|
||||
::close(socket_fd_);
|
||||
socket_fd_ = -1;
|
||||
}
|
||||
has_viewer_ = false;
|
||||
restart_encoder_ = true;
|
||||
rx_buffer_.clear();
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::fps_limited() const {
|
||||
if (fps_ <= 0 || last_sent_.time_since_epoch().count() == 0) {
|
||||
return false;
|
||||
}
|
||||
const auto interval = std::chrono::microseconds(1000000 / fps_);
|
||||
return std::chrono::steady_clock::now() - last_sent_ < interval;
|
||||
}
|
||||
|
||||
bool AmdH264VaapiEncoder::encode_frame(const VisionBuf *buf) {
|
||||
if (!buf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensure_connected()) {
|
||||
return true;
|
||||
}
|
||||
read_control_messages();
|
||||
if (!has_viewer_ || fps_limited()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool force_keyframe = false;
|
||||
if (restart_encoder_) {
|
||||
close();
|
||||
force_keyframe = true;
|
||||
restart_encoder_ = false;
|
||||
}
|
||||
|
||||
if (!opened_ && !open()) {
|
||||
restart_encoder_ = true;
|
||||
return false;
|
||||
}
|
||||
if (buf->width != static_cast<size_t>(width_) || buf->height != static_cast<size_t>(height_)) {
|
||||
LOGE("input size mismatch: got %zux%zu expect %dx%d", buf->width, buf->height, width_, height_);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t *y = convert_buf_.data();
|
||||
uint8_t *u = y + static_cast<size_t>(width_) * height_;
|
||||
uint8_t *v = u + static_cast<size_t>(width_ / 2) * (height_ / 2);
|
||||
const int convert_ret = libyuv::NV12ToI420(
|
||||
buf->y, static_cast<int>(buf->stride),
|
||||
buf->uv, static_cast<int>(buf->stride),
|
||||
y, width_,
|
||||
u, width_ / 2,
|
||||
v, width_ / 2,
|
||||
width_, height_);
|
||||
if (convert_ret != 0) {
|
||||
LOGE("NV12ToI420 failed: %d", convert_ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
av_frame_unref(sw_frame_);
|
||||
sw_frame_->format = AV_PIX_FMT_YUV420P;
|
||||
sw_frame_->width = width_;
|
||||
sw_frame_->height = height_;
|
||||
sw_frame_->data[0] = y;
|
||||
sw_frame_->data[1] = u;
|
||||
sw_frame_->data[2] = v;
|
||||
sw_frame_->linesize[0] = width_;
|
||||
sw_frame_->linesize[1] = width_ / 2;
|
||||
sw_frame_->linesize[2] = width_ / 2;
|
||||
sw_frame_->pts = frame_idx_;
|
||||
sw_frame_->pict_type =
|
||||
(force_keyframe || (gop_size_ > 0 && frame_idx_ % gop_size_ == 0))
|
||||
? AV_PICTURE_TYPE_I
|
||||
: AV_PICTURE_TYPE_NONE;
|
||||
|
||||
const int ret = avcodec_send_frame(codec_ctx_, sw_frame_);
|
||||
if (ret < 0) {
|
||||
LOGE("avcodec_send_frame failed: %d", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encoded;
|
||||
if (!drain_packets(&encoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
++frame_idx_;
|
||||
if (encoded.empty()) {
|
||||
return true;
|
||||
}
|
||||
if (!send_packet(1, encoded.data(), encoded.size())) {
|
||||
close_socket();
|
||||
return false;
|
||||
}
|
||||
last_sent_ = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
// Snapdragon 845 H.264 encoding through FFmpeg.
|
||||
// Input NV12 frames are converted to I420 with libyuv before encoding.
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "msgq/visionipc/visionbuf.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/frame.h>
|
||||
}
|
||||
|
||||
class AmdH264VaapiEncoder {
|
||||
public:
|
||||
AmdH264VaapiEncoder(int width, int height, const std::string &camera,
|
||||
int fps = 20,
|
||||
int bitrate = 5'000'000,
|
||||
const std::string &device_path = "",
|
||||
const std::string &host = "127.0.0.1",
|
||||
int port = 8089);
|
||||
~AmdH264VaapiEncoder();
|
||||
|
||||
bool open();
|
||||
void close();
|
||||
bool encode_frame(const VisionBuf *buf);
|
||||
|
||||
bool is_open() const { return opened_; }
|
||||
|
||||
private:
|
||||
bool drain_packets(std::vector<uint8_t> *out);
|
||||
bool ensure_connected();
|
||||
bool send_handshake();
|
||||
bool send_camera_register();
|
||||
bool send_packet(uint8_t cmd, const uint8_t *data, size_t size);
|
||||
bool send_ws_binary(const uint8_t *data, size_t size);
|
||||
void read_control_messages();
|
||||
void close_socket();
|
||||
bool fps_limited() const;
|
||||
void release();
|
||||
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
int fps_ = 0;
|
||||
int bitrate_ = 0;
|
||||
std::string camera_;
|
||||
std::string host_;
|
||||
int port_ = 8089;
|
||||
|
||||
AVCodecContext *codec_ctx_ = nullptr;
|
||||
AVFrame *sw_frame_ = nullptr;
|
||||
const AVCodec *codec_ = nullptr;
|
||||
std::vector<uint8_t> convert_buf_;
|
||||
|
||||
bool opened_ = false;
|
||||
int socket_fd_ = -1;
|
||||
bool has_viewer_ = false;
|
||||
bool restart_encoder_ = true;
|
||||
int64_t frame_idx_ = 0;
|
||||
int gop_size_ = 0;
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
std::chrono::steady_clock::time_point last_sent_;
|
||||
std::chrono::steady_clock::time_point next_reconnect_;
|
||||
};
|
||||
@@ -0,0 +1,358 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenPilot H.264 Preview</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1117;
|
||||
--panel: #121c24;
|
||||
--text: #edf7f6;
|
||||
--muted: #8aa0a8;
|
||||
--accent: #38d39f;
|
||||
--warn: #ffb86b;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-family: "Aptos Display", "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 18% 18%, rgba(56, 211, 159, 0.18), transparent 28rem),
|
||||
linear-gradient(145deg, #071017 0%, #101820 52%, #172016 100%);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(1100px, 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 28px;
|
||||
background: rgba(18, 28, 36, 0.82);
|
||||
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 18px 22px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(22px, 4vw, 38px);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.status {
|
||||
min-width: 116px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
color: #08120f;
|
||||
background: var(--warn);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status.live {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.camera-switcher {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 14px 22px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 999px;
|
||||
padding: 10px 16px;
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
button.active {
|
||||
color: #07120f;
|
||||
background: var(--accent);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: relative;
|
||||
background: #020607;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 22px;
|
||||
color: var(--muted);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>H.264 Camera Preview</h1>
|
||||
<div id="status" class="status">连接中</div>
|
||||
</header>
|
||||
<nav class="camera-switcher" aria-label="选择摄像头">
|
||||
<button class="active" data-camera="roadCameraState">Road Camera</button>
|
||||
<button data-camera="wideRoadCameraState">Wide Road Camera</button>
|
||||
</nav>
|
||||
<section class="stage">
|
||||
<canvas id="frame"></canvas>
|
||||
<div id="empty" class="empty">等待摄像头推送画面...</div>
|
||||
</section>
|
||||
<footer>
|
||||
<span>端口 8089 / WebSocket: <strong>/stream?camera=...</strong></span>
|
||||
<span id="meta">0 frames</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById("frame");
|
||||
const context = canvas.getContext("2d");
|
||||
const empty = document.getElementById("empty");
|
||||
const statusEl = document.getElementById("status");
|
||||
const meta = document.getElementById("meta");
|
||||
const cameraButtons = [...document.querySelectorAll("[data-camera]")];
|
||||
let currentCamera = "roadCameraState";
|
||||
let frameCount = 0;
|
||||
let chunkCount = 0;
|
||||
let decoder = null;
|
||||
let decoderConfigured = false;
|
||||
let timestamp = 0;
|
||||
let ws = null;
|
||||
|
||||
function findNalUnits(data) {
|
||||
const units = [];
|
||||
let start = -1;
|
||||
let index = 0;
|
||||
while (index + 3 < data.length) {
|
||||
let prefixLength = 0;
|
||||
if (data[index] === 0 && data[index + 1] === 0 && data[index + 2] === 1) {
|
||||
prefixLength = 3;
|
||||
} else if (index + 4 < data.length &&
|
||||
data[index] === 0 && data[index + 1] === 0 &&
|
||||
data[index + 2] === 0 && data[index + 3] === 1) {
|
||||
prefixLength = 4;
|
||||
}
|
||||
if (!prefixLength) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (start >= 0) {
|
||||
units.push(data.subarray(start, index));
|
||||
}
|
||||
start = index + prefixLength;
|
||||
index = start;
|
||||
}
|
||||
if (start >= 0 && start < data.length) {
|
||||
units.push(data.subarray(start));
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
function inspectH264(data) {
|
||||
let key = false;
|
||||
let codec = null;
|
||||
for (const nal of findNalUnits(data)) {
|
||||
if (!nal.length) continue;
|
||||
const type = nal[0] & 0x1f;
|
||||
key ||= type === 5;
|
||||
if (type === 7 && nal.length >= 4) {
|
||||
codec = `avc1.${[nal[1], nal[2], nal[3]]
|
||||
.map((value) => value.toString(16).padStart(2, "0"))
|
||||
.join("").toUpperCase()}`;
|
||||
}
|
||||
}
|
||||
return { key, codec };
|
||||
}
|
||||
|
||||
function closeDecoder() {
|
||||
if (decoder) {
|
||||
try {
|
||||
decoder.close();
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
decoder = null;
|
||||
decoderConfigured = false;
|
||||
timestamp = 0;
|
||||
}
|
||||
|
||||
function createDecoder() {
|
||||
if (!("VideoDecoder" in window)) {
|
||||
statusEl.textContent = "浏览器不支持 WebCodecs";
|
||||
empty.textContent = "请使用支持 WebCodecs H.264 的 Chrome/Edge 浏览器";
|
||||
return false;
|
||||
}
|
||||
decoder = new VideoDecoder({
|
||||
output: (videoFrame) => {
|
||||
if (canvas.width !== videoFrame.displayWidth || canvas.height !== videoFrame.displayHeight) {
|
||||
canvas.width = videoFrame.displayWidth;
|
||||
canvas.height = videoFrame.displayHeight;
|
||||
}
|
||||
context.drawImage(videoFrame, 0, 0, canvas.width, canvas.height);
|
||||
videoFrame.close();
|
||||
empty.style.display = "none";
|
||||
frameCount += 1;
|
||||
meta.textContent = `${frameCount} frames / ${currentCamera}`;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error("H.264 decoder error", error);
|
||||
statusEl.textContent = "解码错误";
|
||||
statusEl.classList.remove("live");
|
||||
closeDecoder();
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetFrameState() {
|
||||
closeDecoder();
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
empty.style.display = "grid";
|
||||
empty.textContent = "等待 H.264 关键帧...";
|
||||
frameCount = 0;
|
||||
chunkCount = 0;
|
||||
meta.textContent = `0 frames / ${currentCamera}`;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const wsProtocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const camera = encodeURIComponent(currentCamera);
|
||||
ws = new WebSocket(`${wsProtocol}//${location.host}/stream?camera=${camera}`);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
ws.onopen = () => {
|
||||
statusEl.textContent = "Live";
|
||||
statusEl.classList.add("live");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = new Uint8Array(event.data);
|
||||
const info = inspectH264(data);
|
||||
|
||||
if (!decoderConfigured) {
|
||||
if (!info.key || !info.codec) {
|
||||
return;
|
||||
}
|
||||
if (!decoder && !createDecoder()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
decoder.configure({
|
||||
codec: info.codec,
|
||||
optimizeForLatency: true,
|
||||
hardwareAcceleration: "prefer-hardware",
|
||||
});
|
||||
decoderConfigured = true;
|
||||
} catch (error) {
|
||||
console.error("VideoDecoder configure failed", error);
|
||||
statusEl.textContent = "不支持该 H.264";
|
||||
closeDecoder();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
decoder.decode(new EncodedVideoChunk({
|
||||
type: info.key ? "key" : "delta",
|
||||
timestamp,
|
||||
data,
|
||||
}));
|
||||
timestamp += 50_000;
|
||||
chunkCount += 1;
|
||||
} catch (error) {
|
||||
console.error("VideoDecoder decode failed", error);
|
||||
closeDecoder();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
statusEl.textContent = "重连中";
|
||||
statusEl.classList.remove("live");
|
||||
setTimeout(connect, 1200);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", closeDecoder);
|
||||
|
||||
cameraButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const nextCamera = button.dataset.camera;
|
||||
if (nextCamera === currentCamera) {
|
||||
return;
|
||||
}
|
||||
currentCamera = nextCamera;
|
||||
cameraButtons.forEach((item) => item.classList.toggle("active", item === button));
|
||||
if (ws) {
|
||||
ws.onclose = null;
|
||||
ws.close();
|
||||
}
|
||||
statusEl.textContent = "连接中";
|
||||
statusEl.classList.remove("live");
|
||||
resetFrameState();
|
||||
connect();
|
||||
});
|
||||
});
|
||||
|
||||
resetFrameState();
|
||||
connect();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python3
|
||||
import asyncio
|
||||
import base64
|
||||
from collections import deque
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlsplit
|
||||
|
||||
from openpilot.common.params import Params
|
||||
|
||||
HOST = os.getenv("H264_WS_HOST", "0.0.0.0")
|
||||
PORT = int(os.getenv("H264_WS_PORT", "8089"))
|
||||
PUSH_FPS = float(os.getenv("H264_WS_FPS", "20"))
|
||||
ATHENA_HOST = os.getenv("ATHENA_HOST", "")
|
||||
REMOTE_MAX_LATENCY = float(os.getenv("H264_REMOTE_MAX_LATENCY", "1.5"))
|
||||
HTML_PATH = Path(__file__).with_name("viewer.html")
|
||||
WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
|
||||
def get_dongle_id():
|
||||
return os.getenv("DONGLE_ID") or Params().get("DongleId") or ""
|
||||
|
||||
|
||||
class MjpegWebsocketServer:
|
||||
def __init__(self, push_fps):
|
||||
self.push_fps = push_fps
|
||||
self.viewers = {}
|
||||
self.ingests = {}
|
||||
self.remote_viewers = {}
|
||||
self.remote_clients = {}
|
||||
self.dongle_id = get_dongle_id()
|
||||
|
||||
async def handle_client(self, reader, writer):
|
||||
try:
|
||||
request = await self.read_http_request(reader)
|
||||
if not request:
|
||||
return
|
||||
method, path, headers = request
|
||||
if headers.get("upgrade", "").lower() == "websocket":
|
||||
await self.handle_websocket(path, headers, reader, writer)
|
||||
else:
|
||||
await self.handle_http(method, path, writer)
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def read_http_request(self, reader):
|
||||
data = await reader.readuntil(b"\r\n\r\n")
|
||||
lines = data.decode("iso-8859-1").split("\r\n")
|
||||
parts = lines[0].split()
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
headers = {}
|
||||
for line in lines[1:]:
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
headers[key.strip().lower()] = value.strip()
|
||||
return parts[0], parts[1], headers
|
||||
|
||||
async def handle_http(self, method, path, writer):
|
||||
route = urlsplit(path).path
|
||||
if method != "GET" or route not in ("/", "/viewer.html"):
|
||||
await self.send_http(writer, "404 Not Found", b"not found", "text/plain")
|
||||
return
|
||||
body = HTML_PATH.read_bytes()
|
||||
await self.send_http(writer, "200 OK", body, "text/html; charset=utf-8")
|
||||
|
||||
async def send_http(self, writer, status, body, content_type):
|
||||
writer.write(
|
||||
f"HTTP/1.1 {status}\r\n"
|
||||
f"Content-Type: {content_type}\r\n"
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
"Connection: close\r\n\r\n".encode("ascii") + body
|
||||
)
|
||||
await writer.drain()
|
||||
|
||||
async def handle_websocket(self, path, headers, reader, writer):
|
||||
key = headers.get("sec-websocket-key")
|
||||
url = urlsplit(path)
|
||||
route = url.path
|
||||
camera = parse_qs(url.query).get("camera", ["roadCameraState"])[0]
|
||||
if not key or route not in ("/stream", "/ingest"):
|
||||
writer.write(b"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n")
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
accept = base64.b64encode(hashlib.sha1((key + WS_GUID).encode("ascii")).digest()).decode("ascii")
|
||||
writer.write(
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Accept: {accept}\r\n\r\n".encode("ascii")
|
||||
)
|
||||
await writer.drain()
|
||||
|
||||
if route == "/stream":
|
||||
await self.viewer_loop(camera, reader, writer)
|
||||
else:
|
||||
await self.ingest_loop(camera, reader, writer)
|
||||
|
||||
async def viewer_loop(self, camera, reader, writer):
|
||||
self.viewers.setdefault(camera, set()).add(writer)
|
||||
await self.notify_ingests(camera)
|
||||
await self.request_keyframe(camera)
|
||||
try:
|
||||
while True:
|
||||
opcode, _ = await self.read_ws_frame(reader)
|
||||
if opcode == 0x8:
|
||||
break
|
||||
finally:
|
||||
self.viewers.get(camera, set()).discard(writer)
|
||||
await self.notify_ingests(camera)
|
||||
|
||||
async def ingest_loop(self, camera, reader, writer):
|
||||
self.ingests.setdefault(camera, set()).add(writer)
|
||||
await self.notify_ingests(camera)
|
||||
try:
|
||||
while True:
|
||||
opcode, payload = await self.read_ws_frame(reader)
|
||||
if opcode == 0x8:
|
||||
break
|
||||
if opcode == 0x2:
|
||||
cmd, body = self.parse_ingest_packet(payload)
|
||||
if cmd == 0:
|
||||
old_camera = camera
|
||||
camera = body.decode("utf-8", errors="ignore") or camera
|
||||
if old_camera != camera:
|
||||
self.ingests.get(old_camera, set()).discard(writer)
|
||||
await self.notify_ingests(old_camera)
|
||||
self.ingests.setdefault(camera, set()).add(writer)
|
||||
await self.notify_ingests(camera)
|
||||
elif cmd == 1:
|
||||
await self.broadcast_frame(camera, body)
|
||||
finally:
|
||||
self.ingests.get(camera, set()).discard(writer)
|
||||
|
||||
async def notify_ingests(self, camera):
|
||||
has_viewer = bool(self.viewers.get(camera)) or self.remote_viewers.get(camera, False)
|
||||
message = self.make_ingest_packet(2, b"1" if has_viewer else b"0")
|
||||
stale = []
|
||||
for writer in self.ingests.get(camera, set()):
|
||||
try:
|
||||
await self.write_ws_frame(writer, message, opcode=0x2)
|
||||
except (ConnectionError, OSError):
|
||||
stale.append(writer)
|
||||
for writer in stale:
|
||||
self.ingests.get(camera, set()).discard(writer)
|
||||
|
||||
async def request_keyframe(self, camera):
|
||||
message = self.make_ingest_packet(3, b"")
|
||||
stale = []
|
||||
for writer in self.ingests.get(camera, set()):
|
||||
try:
|
||||
await self.write_ws_frame(writer, message, opcode=0x2)
|
||||
except (ConnectionError, OSError):
|
||||
stale.append(writer)
|
||||
for writer in stale:
|
||||
self.ingests.get(camera, set()).discard(writer)
|
||||
|
||||
def parse_ingest_packet(self, payload):
|
||||
if len(payload) < 4:
|
||||
return None, b""
|
||||
cmd = payload[0]
|
||||
length = (payload[1] << 16) | (payload[2] << 8) | payload[3]
|
||||
if 4 + length > len(payload):
|
||||
return None, b""
|
||||
return cmd, payload[4:4 + length]
|
||||
|
||||
def make_ingest_packet(self, cmd, payload):
|
||||
if len(payload) > 0xffffff:
|
||||
raise ValueError("payload too large")
|
||||
return bytes((cmd, (len(payload) >> 16) & 0xff, (len(payload) >> 8) & 0xff, len(payload) & 0xff)) + payload
|
||||
|
||||
@staticmethod
|
||||
def is_h264_keyframe(payload):
|
||||
index = 0
|
||||
while index + 4 <= len(payload):
|
||||
if payload[index:index + 4] == b"\x00\x00\x00\x01":
|
||||
nal_start = index + 4
|
||||
index = nal_start
|
||||
elif payload[index:index + 3] == b"\x00\x00\x01":
|
||||
nal_start = index + 3
|
||||
index = nal_start
|
||||
else:
|
||||
index += 1
|
||||
continue
|
||||
if nal_start < len(payload) and payload[nal_start] & 0x1f == 5:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def broadcast_frame(self, camera, payload):
|
||||
viewers = self.viewers.get(camera, set())
|
||||
remote_client = await self.ensure_remote_client(camera)
|
||||
if not viewers and not self.remote_viewers.get(camera, False):
|
||||
return
|
||||
|
||||
stale = []
|
||||
for writer in viewers:
|
||||
try:
|
||||
await self.write_ws_frame(writer, payload, opcode=0x2)
|
||||
except (ConnectionError, OSError):
|
||||
stale.append(writer)
|
||||
for writer in stale:
|
||||
viewers.discard(writer)
|
||||
if stale:
|
||||
await self.notify_ingests(camera)
|
||||
if remote_client and self.remote_viewers.get(camera, False):
|
||||
await remote_client.send_h264(payload)
|
||||
|
||||
async def read_ws_frame(self, reader):
|
||||
header = await reader.readexactly(2)
|
||||
opcode = header[0] & 0x0f
|
||||
masked = (header[1] & 0x80) != 0
|
||||
length = header[1] & 0x7f
|
||||
if length == 126:
|
||||
length = struct.unpack("!H", await reader.readexactly(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack("!Q", await reader.readexactly(8))[0]
|
||||
|
||||
mask = await reader.readexactly(4) if masked else b""
|
||||
payload = await reader.readexactly(length) if length else b""
|
||||
if masked:
|
||||
payload = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
|
||||
return opcode, payload
|
||||
|
||||
async def write_ws_frame(self, writer, payload, opcode=0x2):
|
||||
header = bytearray([0x80 | opcode])
|
||||
length = len(payload)
|
||||
if length <= 125:
|
||||
header.append(length)
|
||||
elif length <= 0xffff:
|
||||
header.extend((126, *struct.pack("!H", length)))
|
||||
else:
|
||||
header.extend((127, *struct.pack("!Q", length)))
|
||||
writer.write(bytes(header) + payload)
|
||||
await writer.drain()
|
||||
|
||||
async def ensure_remote_client(self, camera):
|
||||
if not ATHENA_HOST:
|
||||
return None
|
||||
client = self.remote_clients.get(camera)
|
||||
if client:
|
||||
return client
|
||||
client = RemoteH264Client(ATHENA_HOST, self.dongle_id, camera, self)
|
||||
self.remote_clients[camera] = client
|
||||
asyncio.create_task(client.run())
|
||||
return client
|
||||
|
||||
async def start_remote_clients(self):
|
||||
if not ATHENA_HOST:
|
||||
return
|
||||
for camera in ("roadCameraState", "wideRoadCameraState"):
|
||||
await self.ensure_remote_client(camera)
|
||||
|
||||
|
||||
class RemoteH264Client:
|
||||
def __init__(self, athena_host, dongle_id, camera, server):
|
||||
self.athena_host = athena_host
|
||||
self.dongle_id = dongle_id
|
||||
self.camera = camera
|
||||
self.server = server
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.connected = False
|
||||
self.write_lock = asyncio.Lock()
|
||||
self.pending_frames = deque()
|
||||
self.pending_event = asyncio.Event()
|
||||
self.waiting_for_keyframe = True
|
||||
|
||||
async def run(self):
|
||||
while True:
|
||||
try:
|
||||
await self.connect()
|
||||
sender_task = asyncio.create_task(self.send_loop())
|
||||
reader_task = asyncio.create_task(self.read_loop())
|
||||
try:
|
||||
done, pending = await asyncio.wait(
|
||||
(reader_task, sender_task),
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
await asyncio.gather(*pending, return_exceptions=True)
|
||||
for task in done:
|
||||
task.result()
|
||||
finally:
|
||||
sender_task.cancel()
|
||||
reader_task.cancel()
|
||||
except (ConnectionError, OSError, asyncio.IncompleteReadError, asyncio.TimeoutError):
|
||||
pass
|
||||
finally:
|
||||
self.connected = False
|
||||
self.waiting_for_keyframe = True
|
||||
self.clear_pending_frames()
|
||||
self.server.remote_viewers[self.camera] = False
|
||||
await self.server.notify_ingests(self.camera)
|
||||
if self.writer:
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def connect(self):
|
||||
url = self.normalize_url()
|
||||
parsed = urlsplit(url)
|
||||
port = parsed.port or (443 if parsed.scheme == "wss" else 80)
|
||||
ssl_enabled = parsed.scheme == "wss"
|
||||
self.reader, self.writer = await asyncio.open_connection(parsed.hostname, port, ssl=ssl_enabled)
|
||||
base_path = (parsed.path or "").rstrip("/")
|
||||
path = f"{base_path}/h264/ingest?dongle_id={self.dongle_id}&camera={self.camera}"
|
||||
key = base64.b64encode(os.urandom(16)).decode("ascii")
|
||||
request = (
|
||||
f"GET {path} HTTP/1.1\r\n"
|
||||
f"Host: {parsed.netloc}\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"\r\n"
|
||||
)
|
||||
self.writer.write(request.encode("ascii"))
|
||||
await self.writer.drain()
|
||||
response = await self.reader.readuntil(b"\r\n\r\n")
|
||||
if b" 101 " not in response:
|
||||
raise ConnectionError("remote websocket upgrade failed")
|
||||
self.connected = True
|
||||
await self.send_packet(0, self.camera.encode("utf-8"))
|
||||
if self.server.remote_viewers.get(self.camera, False):
|
||||
await self.server.request_keyframe(self.camera)
|
||||
|
||||
def normalize_url(self):
|
||||
if self.athena_host.startswith(("ws://", "wss://", "http://", "https://")):
|
||||
return self.athena_host.replace("http://", "ws://", 1).replace("https://", "wss://", 1).rstrip("/")
|
||||
return "ws://" + self.athena_host.rstrip("/")
|
||||
|
||||
async def read_loop(self):
|
||||
while True:
|
||||
opcode, payload = await self.server.read_ws_frame(self.reader)
|
||||
if opcode == 0x8:
|
||||
raise ConnectionError("remote closed")
|
||||
if opcode == 0x2:
|
||||
cmd, body = self.server.parse_ingest_packet(payload)
|
||||
if cmd == 2 and body:
|
||||
had_viewer = self.server.remote_viewers.get(self.camera, False)
|
||||
self.server.remote_viewers[self.camera] = body[:1] == b"1"
|
||||
await self.server.notify_ingests(self.camera)
|
||||
if not had_viewer and self.server.remote_viewers[self.camera]:
|
||||
self.waiting_for_keyframe = True
|
||||
self.clear_pending_frames()
|
||||
await self.server.request_keyframe(self.camera)
|
||||
elif cmd == 3:
|
||||
self.waiting_for_keyframe = True
|
||||
self.clear_pending_frames()
|
||||
await self.server.request_keyframe(self.camera)
|
||||
|
||||
async def send_h264(self, payload):
|
||||
is_keyframe = self.server.is_h264_keyframe(payload)
|
||||
if self.waiting_for_keyframe:
|
||||
if not is_keyframe:
|
||||
return
|
||||
self.waiting_for_keyframe = False
|
||||
|
||||
now = time.monotonic()
|
||||
if self.pending_frames and now - self.pending_frames[0][0] > REMOTE_MAX_LATENCY:
|
||||
self.clear_pending_frames()
|
||||
if not is_keyframe:
|
||||
self.waiting_for_keyframe = True
|
||||
await self.server.request_keyframe(self.camera)
|
||||
return
|
||||
|
||||
self.pending_frames.append((now, payload))
|
||||
self.pending_event.set()
|
||||
|
||||
def clear_pending_frames(self):
|
||||
self.pending_frames.clear()
|
||||
self.pending_event.clear()
|
||||
|
||||
async def send_loop(self):
|
||||
while True:
|
||||
await self.pending_event.wait()
|
||||
while self.pending_frames:
|
||||
queued_at, frame = self.pending_frames.popleft()
|
||||
if time.monotonic() - queued_at > REMOTE_MAX_LATENCY:
|
||||
self.waiting_for_keyframe = True
|
||||
self.clear_pending_frames()
|
||||
await self.server.request_keyframe(self.camera)
|
||||
break
|
||||
await asyncio.wait_for(
|
||||
self.send_packet(1, frame),
|
||||
timeout=REMOTE_MAX_LATENCY,
|
||||
)
|
||||
if not self.pending_frames:
|
||||
self.pending_event.clear()
|
||||
|
||||
async def send_packet(self, cmd, payload):
|
||||
if not self.connected or not self.writer:
|
||||
return
|
||||
packet = self.server.make_ingest_packet(cmd, payload)
|
||||
async with self.write_lock:
|
||||
await self.write_client_ws_frame(packet, opcode=0x2)
|
||||
|
||||
async def write_client_ws_frame(self, payload, opcode=0x2):
|
||||
header = bytearray([0x80 | opcode])
|
||||
length = len(payload)
|
||||
if length <= 125:
|
||||
header.append(0x80 | length)
|
||||
elif length <= 0xffff:
|
||||
header.extend((0x80 | 126, *struct.pack("!H", length)))
|
||||
else:
|
||||
header.extend((0x80 | 127, *struct.pack("!Q", length)))
|
||||
|
||||
mask = os.urandom(4)
|
||||
masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
|
||||
self.writer.write(bytes(header) + mask + masked)
|
||||
await self.writer.drain()
|
||||
|
||||
|
||||
async def async_main():
|
||||
server = MjpegWebsocketServer(PUSH_FPS)
|
||||
await server.start_remote_clients()
|
||||
tcp_server = await asyncio.start_server(server.handle_client, HOST, PORT)
|
||||
print(f"H.264 websocket server listening on http://{HOST}:{PORT}, fps={PUSH_FPS:g}")
|
||||
async with tcp_server:
|
||||
await tcp_server.serve_forever()
|
||||
|
||||
|
||||
def main():
|
||||
asyncio.run(async_main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user