Initial commit. Version 0.1

This commit is contained in:
Niellune
2025-05-20 16:05:26 +03:00
parent 9139cb4fd4
commit 7f3ea88b21
30 changed files with 2537 additions and 1 deletions
+8
View File
@@ -0,0 +1,8 @@
# Generated files
out/
headers/
src/autogen
# General files
.*/
.DS*
+40
View File
@@ -0,0 +1,40 @@
Open Sans
Designed by Steve Matteson
License
Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)
This Font Software is licensed under the SIL Open Font License, Version 1.1 . This license is copied below, and is also available with a FAQ at: https://openfontlicense.org
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
+60
View File
@@ -0,0 +1,60 @@
# Compiler
CXX := g++
SRC_DIR := ./src
OUT_DIR := ./out
RES_DIR := $(SRC_DIR)/resource
GEN_DIR := $(SRC_DIR)/autogen
BUILD_DIR := $(OUT_DIR)/$(BUILD_TYPE)
# File lists
SRCS := $(shell find $(SRC_DIR) -type f -name '*.cpp')
OBJS=$(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))
RES := $(shell find $(RES_DIR) -type f ! -name '*.h' -name '*.*')
RES_SRC := $(patsubst $(RES_DIR)/%,$(GEN_DIR)/%.cpp,$(RES))
# Targets
TARGET_NAME := app
# Build types
.PHONY: all debug release clean build
all: debug
LDOPTIONS := -lSDL2 -lSDL2_ttf -lavformat -lavcodec -lavutil -lswscale -lusb-1.0
LDFLAGS :=
CXXCOMMON := -Wall -Isrc
debug: BUILD_TYPE := debug
debug: CXXFLAGS := -g -O0 -fsanitize=address -fno-omit-frame-pointer
debug: LDFLAGS += -fsanitize=address -fno-omit-frame-pointer
debug: TARGET := $(TARGET_NAME)-debug
debug: prepare
release: BUILD_TYPE := release
release: CXXFLAGS := -O2
release: TARGET := $(TARGET_NAME)
release: prepare
prepare: $(RES_SRC)
$(MAKE) BUILD_TYPE=$(BUILD_TYPE) TARGET=$(OUT_DIR)/$(TARGET) CXXFLAGS="$(CXXFLAGS)" LDFLAGS="$(LDFLAGS)" build
build: $(TARGET)
$(GEN_DIR)/%.cpp: $(RES_DIR)/%
@mkdir -p $(GEN_DIR)
xxd -i -n $(basename $(notdir $<)) $< > $@
$(TARGET): $(OBJS)
@mkdir -p $(OUT_DIR)
$(CXX) $(LDFLAGS) $(OBJS) -o $(TARGET) $(LDOPTIONS)
@echo "Build complete: $(TARGET)"
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXXCOMMON) $(CXXFLAGS) -c $< -o $@
clean:
@rm -rf $(OUT_DIR)
@rm -rf $(GEN_DIR)
@echo "Clean complete"
+86 -1
View File
@@ -1,2 +1,87 @@
# FastCarPlay
Car Play Receiver for dongles
This is C++ implementation of carplay receiver for "Autobox" dongles.
The purpose of the project was to make application lightweight to run on Raspberry PI Zero 2W and use hardware decoding.
# Dongles
The dongles are readily available from Amazon or Aliexpress labeled by !["Carlinkit"](https://www.carlinkit.com/).
Devices might have different vendor and product id's. Check your with lsusb and update settings if necessary.
# Setup
## Dependencies
The project is based on SDL2, FFMPEG, LIBUSB. It use XXD for resource embedding.
```
sudo apt install build-essential xxd libsdl2-dev libsdl2-ttf-dev libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libusb-1.0-0-dev
```
To run the application you also need to install runtime
```
sudo apt install ffmpeg libsdl2-2.0-0 libsdl2-ttf-2.0-0 libavformat59 libavcodec61 libavutil57 libswscale7 libusb-1.0-0
```
## Build and run
The application can be started with settings file. Sample of the settings file can be found in settings.txt
The project is using make. From the repository root run following
```
make clean
make release
./out/app ./settings.txt
```
## Customisation
You can change font and background images by replacing files in ./src/resource
- background.bmp for background image. Use BMP format only.
- font.ttf for font. Use TTF fdrmat only
The names of the file need to be exactly same. Resources are embedded in executable, remake the project to regenerate resources
```
make clean
make release
```
## Keys
The following keys have been mapped:
- Left - navigate left
- Right - navigate right
- Enter - select active item
- Backspace - Go back
- f - toggle fullscreen mode
# Status
What is working:
- Video
- Audio (multiple channels)
- Key navigation
- Simple touch
What is not working:
- Multi touch - i have no means to test it
- Microphone - that's next step for me to figure out how to feed sound
- Telephone - the listening part will work, but because there is no mic implementation you can't speak
## Notes
Regardless the resolution there are 2 types of settings
- source-width source-height source-fps - defines what video parameters will be requested from device
- width height fps - defines video drawing resolution and fps
The SDL will anyway scale the image to your screen/window size using internal HW scaling, so ideally you should use same values for width and height.
If the source parameters will be different the scaling will be applied, however that scaling is not hardware accelerated and can consume a lot of CPU.
You can set the 'scaler' in setting to define scaling algorithm. On my Raspbery Pi Zero 2W even easiest algorithm loads CPU to 100% and cause fram drop.
Increasing FPS above Source-FPS will cause app to run UI loop with less delays and do more event polling. This can increase responsivenes of the system, but also will make X11 to use more resources.
## Next plans
- Implement direct buffer transfer from video decoder to renderer (should reduce amount of memory copies and CPU load)
- Control audio buffers better (now system use 3 decoding threads but in reality only 2 required)
- Reduce music volume when there is navigation messages
- Add abilities to run script on device connect and device disconnect
# Acknowledgement
The project is inspired and based on great work done by other developers:
- ![pycarplay by electric-monk](https://github.com/electric-monk/pycarplay)
- ![carplay-receiver by harrylepotter](https://github.com/harrylepotter/carplay-receiver)
- ![react-carplay by rhysmorgan134](https://github.com/rhysmorgan134/react-carplay)
The project is licenced under GPL-3 licence. See LICENCE for details.
The project is using ![Open Sans](https://fonts.google.com/specimen/Open+Sans) font. See FONT_LICENCE for details.
# Finally
If you have any questions, suggestions or you find problems running this feel free to open issue.
+43
View File
@@ -0,0 +1,43 @@
# Dongle configuration
# Note that vendor id and product id are in decimals, not hex here
vendor-id = 4884 # 0x1314
product-id = 5408 # 0x1520
# Application starts in full screen
fullscreen = true
# Application drawing settings widthxheight
width = 720
height = 576
fps = 30
# Requested image from phone
source-width = 1080
source-height = 864
source-fps = 30
# Scaler algorithm if application drawing is differen from source image
# It's recommended to keep application and source values same cause scaling
# takes a lot of CPU and can cause artifacts on slow devices
# options
# SWS_FAST_BILINEAR 1
# SWS_BILINEAR 2
# SWS_BICUBIC 4
# SWS_X 8
# SWS_POINT 16
# SWS_AREA 32
# SWS_BICUBLIN 64
# SWS_GAUSS 128
# SWS_SINC 256
# SWS_LANCZOS 512
# SWS_SPLINE 1024
scaler = 1024
# Enable logging
logging = false
# Size of video and audio buffers. Increase if you see artifacts
queue-size = 32
# Font size for messgaes on screen
font-size = 30
+278
View File
@@ -0,0 +1,278 @@
#include "connector.h"
#include <stdexcept>
#include <iostream>
#include <condition_variable>
#include "helper/functions.h"
#include "helper/settings.h"
Connector::Connector(uint16_t videoPadding)
: _videoPadding(videoPadding)
{
int result = libusb_init(&_context);
if (result < 0)
throw std::runtime_error(std::string("Can't initialise USB: ") + libusb_error_name(result));
}
Connector::~Connector()
{
stop();
if (_device)
{
libusb_release_interface(_device, 0);
libusb_close(_device);
_device = nullptr;
}
if (_context)
{
libusb_exit(_context);
_context = nullptr;
}
}
void Connector::start(IProtocol *protocol)
{
_protocol = protocol;
if (_active)
stop();
_active = true;
_write_thread = std::thread(&Connector::write_loop, this);
_read_thread = std::thread(&Connector::read_loop, this);
}
void Connector::stop()
{
if (!_active)
return;
_active = false;
if (_read_thread.joinable())
_read_thread.join();
if (_write_thread.joinable())
_write_thread.join();
}
bool Connector::connect(uint16_t vendor_id, uint16_t product_id)
{
status("Searching for device");
std::cout << "Creating device handle" << std::endl;
_device = libusb_open_device_with_vid_pid(_context, vendor_id, product_id);
if (!_device)
{
status("Can't find device");
return false;
}
if (link())
return true;
libusb_close(_device);
_device = nullptr;
return false;
}
bool Connector::link()
{
status("Linking device");
std::cout << "Reset device" << std::endl;
if (libusb_reset_device(_device) < 0)
return false;
std::cout << "Set configuration" << std::endl;
if (libusb_set_configuration(_device, 1) < 0)
return false;
std::cout << "Claim interface" << std::endl;
if (libusb_claim_interface(_device, 0) < 0)
return false;
std::cout << "Get config descriptor" << std::endl;
libusb_device *dev = libusb_get_device(_device);
struct libusb_config_descriptor *config = nullptr;
if (libusb_get_active_config_descriptor(dev, &config) < 0)
return false;
for (int i = 0; i < config->interface[0].altsetting[0].bNumEndpoints; i++)
{
const struct libusb_endpoint_descriptor *ep = &config->interface[0].altsetting[0].endpoint[i];
if ((ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK) == LIBUSB_ENDPOINT_IN)
{
_endpoint_in = ep->bEndpointAddress;
std::cout << "Found input endpoint" << std::endl;
}
else
{
_endpoint_out = ep->bEndpointAddress;
std::cout << "Found output endpoint" << std::endl;
}
}
std::cout << "Free config descriptor" << std::endl;
libusb_free_config_descriptor(config);
return true;
}
void Connector::release()
{
if (_device)
{
libusb_release_interface(_device, 0);
libusb_close(_device);
_device = nullptr;
}
}
void Connector::status(const char *status)
{
std::cout << status << std::endl;
if (_protocol)
_protocol->onStatus(status);
}
void Connector::write_uint32_le(uint8_t *dst, uint32_t value)
{
dst[0] = value & 0xFF;
dst[1] = (value >> 8) & 0xFF;
dst[2] = (value >> 16) & 0xFF;
dst[3] = (value >> 24) & 0xFF;
}
int Connector::send(int cmd)
{
if (!_connected)
return 0;
int transferred;
uint8_t buffer[16];
write_uint32_le(&buffer[0], 0x55AA55AA);
write_uint32_le(&buffer[4], 0);
write_uint32_le(&buffer[8], cmd);
write_uint32_le(&buffer[12], ~cmd);
std::unique_lock<std::mutex> lock(_write_mutex);
libusb_bulk_transfer(_device, _endpoint_out, buffer, 16, &transferred, 0);
return transferred;
}
int Connector::send(int cmd, uint8_t *data, uint32_t size)
{
if (!_connected)
return 0;
int transferred;
uint8_t buffer[16];
write_uint32_le(&buffer[0], 0x55AA55AA);
write_uint32_le(&buffer[4], size);
write_uint32_le(&buffer[8], cmd);
write_uint32_le(&buffer[12], ~cmd);
std::unique_lock<std::mutex> lock(_write_mutex);
libusb_bulk_transfer(_device, _endpoint_out, buffer, 16, &transferred, 0);
libusb_bulk_transfer(_device, _endpoint_out, data, size, &transferred, 0);
return transferred;
}
void Connector::read_loop()
{
std::mutex mtx;
std::condition_variable cv;
Header header;
int transferred = 0;
uint8_t *data;
// Set thread name
setThreadName( "protocol-reader");
while (_active)
{
if (!_connected)
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::seconds(1), [&]()
{ return !_active.load(); });
continue;
}
int result = libusb_bulk_transfer(_device, _endpoint_in, reinterpret_cast<uint8_t *>(&header), sizeof(Header), &transferred, READ_TIMEOUT);
if (result == LIBUSB_ERROR_NO_DEVICE)
{
if (_protocol)
_protocol->onDevice(false);
_connected = false;
continue;
}
if (result != LIBUSB_SUCCESS || transferred != sizeof(Header))
{
continue;
}
int padding = 0;
if (header.type == 6)
padding = _videoPadding;
if (header.length > 0)
{
data = (uint8_t *)malloc(header.length + padding);
if (!data)
continue;
libusb_bulk_transfer(_device, _endpoint_in, data, header.length, &transferred, READ_TIMEOUT);
}
if (!_protocol)
free(data);
else
{
if (padding > 0)
std::fill(data + header.length, data + header.length + padding, 0);
_protocol->onData(header.type, header.length, data);
}
}
}
void Connector::write_loop()
{
std::mutex mtx;
std::condition_variable cv;
// Set thread name
setThreadName("protocol-writer");
while (_active)
{
_connected = connect(Settings::vendorid, Settings::productid);
if (_connected)
{
status("Starting device");
if (_protocol)
_protocol->onDevice(true);
status("Waiting for connecton");
while (_connected && _active)
{
send(170);
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::seconds(2), [&]()
{ return !_active.load(); });
}
}
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::seconds(1), [&]()
{ return !_active.load(); });
}
}
+65
View File
@@ -0,0 +1,65 @@
#ifndef SRC_CONNECTOR
#define SRC_CONNECTOR
#include <libusb-1.0/libusb.h>
#include <atomic>
#include <thread>
#include <mutex>
#include <string>
#include "helper/iprotocol.h"
#define READ_TIMEOUT 5000
#pragma pack(push, 1)
struct Header {
uint32_t magic;
uint32_t length;
uint32_t type;
uint32_t typecheck;
};
#pragma pack(pop)
class Connector
{
public:
Connector(uint16_t videoPadding);
~Connector();
void start(IProtocol *protocol);
void stop();
int send(int cmd, uint8_t *data, uint32_t size);
int send(int cmd);
static void write_uint32_le(uint8_t *dst, uint32_t value);
private:
void read_loop();
void write_loop();
bool connect(uint16_t vendor_id, uint16_t product_id);
bool link();
void release();
void status(const char* status);
libusb_context *_context = nullptr;
libusb_device_handle *_device = nullptr;
uint8_t _endpoint_in;
uint8_t _endpoint_out;
bool _connected;
std::thread _read_thread;
std::thread _write_thread;
std::mutex _write_mutex;
std::atomic<bool> _active = false;
u_int16_t _videoPadding;
IProtocol* _protocol = nullptr;
};
#endif /* SRC_CONNECTOR */
+252
View File
@@ -0,0 +1,252 @@
#include "decoder.h"
#include <iostream>
#include "helper/functions.h"
#include "helper/settings.h"
Decoder::Decoder()
{
}
Decoder::~Decoder()
{
stop();
}
void Decoder::start(RawQueue *data, VideoBuffer *vb, AVCodecID codecId)
{
if (_active)
stop();
_vb = vb;
_data = data;
_codecId = codecId;
_active = true;
_running = false;
_thread = std::thread(&Decoder::runner, this);
}
void Decoder::stop()
{
if (!_active)
return;
_active = false;
_data->notify();
if (_thread.joinable())
_thread.join();
}
// Initialize and select the best decoder (try HW first, then SW)
AVCodecContext *Decoder::load_codec(AVCodecID codec_id)
{
void *iter = nullptr;
const AVCodec *codec = nullptr;
AVCodecContext *result = nullptr;
// Try hardware-accelerated decoders by iterating registered codecs
while ((codec = av_codec_iterate(&iter)))
{
if (!av_codec_is_decoder(codec) || codec->id != codec_id)
continue;
if (!(codec->capabilities & AV_CODEC_CAP_HARDWARE))
continue;
result = avcodec_alloc_context3(codec);
if (!result)
{
std::cout << "Can't load HW codec " << codec->name << ": out of memory" << std::endl;
break;
}
int ret = avcodec_open2(result, codec, nullptr);
if (ret == 0)
{
std::cout << "Using HW decoder: " << codec->name << std::endl;
return result;
}
std::cout << "Can't load HW codec " << codec->name << ": " << Error::avErrorText(ret) << std::endl;
avcodec_free_context(&result);
}
// Fallback to software decoder
codec = avcodec_find_decoder(codec_id);
if (!codec)
{
std::cout << "Decoder not found for codec id " << codec_id << std::endl;
return nullptr;
}
result = avcodec_alloc_context3(codec);
if (!result)
{
std::cout << "Failed to allocate context for codec id " << codec_id << std::endl;
return nullptr;
}
int ret = avcodec_open2(result, codec, nullptr);
if (ret < 0)
{
std::cout << "Failed to open software decoder " << codec->name << ": " << Error::avErrorText(ret) << std::endl;
avcodec_free_context(&result);
return nullptr;
}
std::cout << "Using SW decoder " << codec->name << std::endl;
return result;
}
void Decoder::runner()
{
// Set thread name
setThreadName( "video-decoding");
// Load codec context
AVCodecContext *context = load_codec(_codecId);
if (_status.null(context, ("Can't find decoder for codec " + _codecId)))
return;
std::string codec = context->codec->name;
// Initialize parser for the codec
AVCodecParserContext *parser = av_parser_init(_codecId);
if (!_status.null(parser, "Can't initilise parser for codec " + codec))
{
// Allocate packet for decoding
AVPacket *packet = av_packet_alloc();
if (!_status.null(packet, "Can't allocate packet for codec " + codec))
{
// Allocate frame for decoded data
AVFrame *frame = av_frame_alloc();
if (!_status.null(frame, "Can't allocate frame for codec " + codec))
{
loop(context, parser, packet, frame); // Run decoding loop
av_frame_free(&frame);
}
av_packet_free(&packet);
}
av_parser_close(parser);
}
avcodec_free_context(&context);
if (_status.error())
std::cout << "Video decoder error: " << _status.message() << std::endl;
}
void Decoder::loop(AVCodecContext *context, AVCodecParserContext *parser, AVPacket *packet, AVFrame *frame)
{
SwsContext *sws = nullptr;
int sws_height = 0;
int sws_width = 0;
uint32_t counter = 0;
_running = true;
std::cout << "Video decoder loop started" << std::endl;
// Main decoding loop; runs until global_quit flag is set
while (_active)
{
// Get raw data segment from queue
RawEntry segment = _data->wait(_active);
if (!_active)
{
if (segment.data)
{
free(segment.data);
segment.data = nullptr;
}
continue;
}
uint8_t *data_ptr = segment.data + segment.offset; // Pointer to raw data buffer
int data_size = segment.size; // Size of raw data buffer
// Feed raw data into the parser and decoder
while (_active && data_size > 0)
{
uint8_t *paket_data;
int paket_size;
// printf("avparser offset %d size %d fullsize %d\n", data_ptr-segment.data, data_size, segment.size);
// Parse raw data into packets
int len = av_parser_parse2(parser, context,
&paket_data, &paket_size,
data_ptr, data_size,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
// Parsing error; break out
if (len < 0)
break;
// Move forward through segment
data_ptr += len;
data_size -= len;
if (paket_size <= 0)
continue;
// Load packet data
av_packet_unref(packet);
packet->data = paket_data;
packet->size = paket_size;
// Send packet to decoder
int send_ret = avcodec_send_packet(context, packet);
if (send_ret != 0)
{
std::cout << "Error decoding packet: " << Error::avErrorText(send_ret) << std::endl;
continue;
}
// Receive decoded frames
while (avcodec_receive_frame(context, frame) == 0 && _active)
{
if (!sws || sws_width != frame->width || sws_height != frame->height)
{
if (sws)
{
sws_freeContext(sws);
sws = nullptr;
}
sws_width = frame->width;
sws_height = frame->height;
sws = sws_getContext(sws_width, sws_height, (AVPixelFormat)frame->format,
_vb->width(), _vb->height(), AV_PIX_FMT_YUV420P,
Settings::scaler, nullptr, nullptr, nullptr);
if (!sws)
{
std::cout << "Error creating sws context" << std::endl;
_active = false;
continue;
}
}
// Write decoded frame into video buffer and get output frame pointer
const AVFrame *out = _vb->writeFrame(counter++);
// Scale frame to output format
sws_scale(sws,
frame->data, frame->linesize,
0, sws_height,
out->data,
out->linesize);
// Commit frame to buffer
_vb->commitFrame();
}
}
if (segment.data)
{
free(segment.data);
segment.data = nullptr;
}
}
_running = false;
if (sws)
{
sws_freeContext(sws);
sws = nullptr;
}
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef SRC_DECODER
#define SRC_DECODER
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
#include <atomic>
#include <thread>
#include "struct/video_buffer.h"
#include "struct/raw_queue.h"
class Decoder
{
public:
Decoder();
~Decoder();
void start(RawQueue *data, VideoBuffer *vb, AVCodecID codecId);
void stop();
private:
void runner();
void loop(AVCodecContext *context, AVCodecParserContext *parser, AVPacket *packet, AVFrame *frame);
static AVCodecContext *load_codec(AVCodecID codec_id);
std::thread _thread;
AVCodecID _codecId;
Error _status;
std::atomic<bool> _active = false;
std::atomic<bool> _running = false;
RawQueue *_data = nullptr;
VideoBuffer *_vb = nullptr;
};
#endif /* SRC_DECODER */
+88
View File
@@ -0,0 +1,88 @@
#ifndef SRC_ERROR
#define SRC_ERROR
#include <string>
#include <stdexcept>
extern "C"
{
#include <libavutil/error.h>
}
class Error
{
public:
Error()
: _error(false), _text("")
{
}
void set(const std::string &error)
{
_error = true;
_text = error;
}
bool null(const void *ptr, const std::string &message = "")
{
if (!ptr)
{
_text = message;
_error = true;
return true;
}
return false;
}
bool zero(u_int32_t id, const std::string &message = "")
{
if (id==0)
{
_text = message;
_error = true;
return true;
}
return false;
}
static const std::string avErrorText(int code)
{
char buf[AV_ERROR_MAX_STRING_SIZE] = {0};
if (av_strerror(code, buf, sizeof(buf)) == 0)
return buf;
return "Unknown error";
}
bool avFail(int code, const std::string &message = "")
{
if (code == 0)
return false;
_text = message + avErrorText(code);
_error = true;
return true;
}
bool error() const
{
return _error;
}
const std::string &message() const
{
return _text;
}
void throwError() const
{
if (_error)
throw std::runtime_error(_text);
}
private:
bool _error;
std::string _text;
};
#endif /* SRC_ERROR */
+24
View File
@@ -0,0 +1,24 @@
#ifndef SRC_HELPER_FUNCTIONS
#define SRC_HELPER_FUNCTIONS
#if defined(__linux__) || defined(__APPLE__)
#include <pthread.h>
#endif
inline void setThreadName(const char *name)
{
#if defined(__linux__)
pthread_setname_np(pthread_self(), name); // Linux: OK (limit 16 chars including null)
#elif defined(__APPLE__)
pthread_setname_np(name); // macOS: only current thread, OK
#else
(void)name; // suppress unused warning
#endif
}
inline void disable_cout()
{
std::cout.setstate(std::ios_base::failbit);
}
#endif /* SRC_HELPER_FUNCTIONS */
+16
View File
@@ -0,0 +1,16 @@
#ifndef SRC_PROTOCOL_ISENDER
#define SRC_PROTOCOL_ISENDER
#include <cstdint>
#include <functional>
using StatusCallback = void (*)(const char* status);
class IProtocol
{
public:
virtual void onData(uint32_t cmd, uint32_t length, uint8_t* data) = 0;
virtual void onStatus(const char* status) = 0;
virtual void onDevice(bool connected) = 0;
};
#endif /* SRC_PROTOCOL_ISENDER */
+75
View File
@@ -0,0 +1,75 @@
#include "settings.h"
#include <iostream>
#include <fstream>
void Settings::load(const std::string &filename)
{
std::ifstream file(filename);
if (!file.is_open())
{
std::cerr << "[Settings] Cannot open “" << filename << "" << std::endl;
return;
}
std::string line;
while (std::getline(file, line))
{
// strip comments
std::size_t p = line.find('#');
if (p != std::string::npos)
line.erase(p);
// trim
trim(line);
if (line.empty())
continue;
// split on '='
std::size_t eq = line.find('=');
if (eq == std::string::npos)
continue;
std::string key = line.substr(0, eq);
std::string value = line.substr(eq + 1);
trim(key);
trim(value);
// lookup in registry
bool found = false;
for (ISetting *setting : _settings())
{
if (setting->name == key)
{
setting->parse(value);
found = true;
break;
}
}
if (!found)
std::cerr << "[Settings] Unknown key “" << key << "" << std::endl;
}
}
void Settings::print()
{
for (ISetting *setting : _settings())
{
std::cout << "[Settings] " << setting->name << " = " << setting->asString() << "\n";
}
}
void Settings::trim(std::string &s)
{
// Left trim
s.erase(s.begin(),
std::find_if(s.begin(), s.end(),
[](unsigned char c)
{ return !std::isspace(c); }));
// Right trim
s.erase(std::find_if(s.rbegin(), s.rend(),
[](unsigned char c)
{ return !std::isspace(c); })
.base(),
s.end());
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef SRC_HELPER_SETTINGS
#define SRC_HELPER_SETTINGS
#include "settings_base.h"
// The singleton “Settings” namespace
class Settings
{
public:
static inline Setting<int> vendorid{"vendor-id", 4884};
static inline Setting<int> productid{"product-id ", 5408};
static inline Setting<bool> fullscreen{"fullscreen", false};
static inline Setting<int> width{"width", 720};
static inline Setting<int> height{"height", 576};
static inline Setting<int> fps{"fps", 60};
static inline Setting<int> sourceWidth{"source-width", 720};
static inline Setting<int> sourceHeight{"source-height", 576};
static inline Setting<int> sourceFps{"source-fps", 30};
static inline Setting<bool> logging{"logging", true};
static inline Setting<int> scaler{"scaler", 2};
static inline Setting<int> queue{"queue-size", 32};
static inline Setting<int> fontSize{"font-size", 30};
static void load(const std::string &filename);
static void print();
private:
static void trim(std::string &s);
};
#endif /* SRC_HELPER_SETTINGS */
+93
View File
@@ -0,0 +1,93 @@
#ifndef SRC_HELPER_SETTINGS_BASE
#define SRC_HELPER_SETTINGS_BASE
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <stdexcept>
// Base interface for any one setting
class ISetting
{
public:
std::string name;
virtual void parse(std::string &str) = 0;
virtual std::string asString() const = 0;
};
// Holds the global registry
inline std::vector<ISetting *> &_settings()
{
static std::vector<ISetting *> settings;
return settings;
}
// A “typed” setting that autoregisters itself
template <typename T>
class Setting : public ISetting
{
public:
T value;
Setting(std::string name_, T default_)
: value(default_)
{
name = std::move(name_);
_settings().push_back(this);
}
// allow using this as if it were a T
operator T() const { return value; }
Setting &operator=(T newValue)
{
value = newValue;
return *this;
}
// parse a string into T
void parse(std::string &str) override
{
try
{
std::transform(str.begin(), str.end(), str.begin(), ::tolower);
if constexpr (std::is_same_v<T, bool>)
{
if (str == "1" || str == "true")
value = true;
else if (str == "0" || str == "false")
value = false;
else
throw new std::runtime_error("Can't convert to boolean.");
}
else if constexpr (std::is_integral_v<T> && !std::is_same_v<T, bool>)
{
value = static_cast<T>(std::stoll(str));
}
else if constexpr (std::is_floating_point_v<T>)
{
value = static_cast<T>(std::stold(str));
}
else if constexpr (std::is_same_v<T, std::string>)
{
value = str;
}
}
catch (const std::exception &e)
{
std::cerr << "[Settings] failed to parse \"" << str
<< "\" for key \"" << name << "\": " << e.what() << std::endl;
}
}
std::string asString() const override
{
if constexpr (std::is_same_v<T, bool>)
return value ? "true" : "false";
else if constexpr (std::is_same_v<T, std::string>)
return value;
else
return std::to_string(value);
}
};
#endif /* SRC_HELPER_SETTINGS_BASE */
+393
View File
@@ -0,0 +1,393 @@
#include <SDL2/SDL.h> // Include SDL2 library headers for graphics and event handling
#include <SDL2/SDL_ttf.h>
extern "C"
{
#include <libavformat/avformat.h> // FFmpeg library for multimedia container format handling
#include <libavcodec/avcodec.h> // FFmpeg library for encoding/decoding
#include <libswscale/swscale.h> // FFmpeg library for image scaling and pixel format conversion
#include <libavutil/imgutils.h> // FFmpeg utility functions for image handling
}
#include <atomic> // C++ atomic types for thread-safe variables
#include <mutex> // C++ mutex for locking resources
#include <condition_variable> // C++ condition variable for thread synchronization
#include <cmath> // Math functions (not explicitly used here but included)
#include <cstdio> // Standard C I/O functions
#include <vector> // C++ dynamic array container
#include <string> // C++ string type
#include <unistd.h>
#include <iostream>
#include "resource/background.h"
#include "resource/font.h"
#include "helper/settings.h"
#include "helper/functions.h"
#include "ux/ufont.h"
#include "ux/uimage.h"
#include "struct/video_buffer.h"
#include "protocol.h"
#include "decoder.h"
#include "pcm_audio.h"
static const char *title = "Fast Car Play v0.1";
static int width = 0;
static int height = 0;
static SDL_Window *window = nullptr;
static SDL_Renderer *renderer = nullptr;
bool active = false;
static SDL_Texture *textTexture = nullptr;
static std::string textureText = "";
static SDL_Texture *imgTexture = nullptr;
static SDL_Texture *videoTexture = nullptr;
static bool mouseDown = false;
static bool fullscreen = false;
std::mutex statusMutex;
std::string statusText;
void onStatus(const char *status)
{
std::lock_guard<std::mutex> lock(statusMutex);
statusText = status;
}
void DrawText(UFont &font, std::string text)
{
if (!textTexture || textureText.compare(text) != 0)
{
if (textTexture)
SDL_DestroyTexture(textTexture);
textTexture = font.GetText(renderer, text.c_str(), {255, 255, 255, 255});
textureText = text;
}
if (!textTexture)
return;
int textW, textH;
SDL_QueryTexture(textTexture, nullptr, nullptr, &textW, &textH);
int windowW, windowH;
SDL_GetRendererOutputSize(renderer, &windowW, &windowH);
// Center text
SDL_Rect dstRect = {
(windowW - textW) / 2,
(windowH - textH) * 9 / 10,
textW,
textH};
SDL_RenderCopy(renderer, textTexture, nullptr, &dstRect);
}
void DrawImage(UImage &img)
{
if (!imgTexture)
imgTexture = img.GetImage(renderer);
if (!imgTexture)
return;
int windowW, windowH;
SDL_GetRendererOutputSize(renderer, &windowW, &windowH);
// Compute destination rectangle to center image
SDL_Rect dst = {
(windowW - img.Width) / 2, // x: center horizontally
(windowH - img.Height) / 2, // y: center vertically
img.Width, // width: original image width
img.Height // height: original image height
};
SDL_RenderCopy(renderer, imgTexture, nullptr, &dst);
}
void processKey(Protocol &protocol, SDL_Keysym key)
{
switch (key.sym)
{
case SDLK_f:
fullscreen = !fullscreen; // Toggle fullscreen mode
SDL_SetWindowFullscreen(window, fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
SDL_SetWindowBordered(window, fullscreen ? SDL_FALSE : SDL_TRUE);
break;
case SDLK_LEFT:
protocol.sendKey(100);
break;
case SDLK_RIGHT:
protocol.sendKey(101);
break;
case SDLK_RETURN:
protocol.sendKey(104);
protocol.sendKey(105);
break;
case SDLK_BACKSPACE:
protocol.sendKey(106);
break;
}
}
void processEvents(Protocol &protocol)
{
SDL_Event e;
while (SDL_PollEvent(&e))
{
switch (e.type)
{
case SDL_QUIT:
active = false;
break;
case SDL_MOUSEBUTTONDOWN:
{
mouseDown = true;
int window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
protocol.sendClick(1.0 * e.button.x / window_width, 1.0 * e.button.y / window_height, true);
break;
}
case SDL_MOUSEBUTTONUP:
{
mouseDown = false;
int window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
protocol.sendClick(1.0 * e.button.x / window_width, 1.0 * e.button.y / window_height, false);
break;
}
case SDL_MOUSEMOTION:
{
if (!mouseDown)
break;
int window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
protocol.sendMove(1.0 * e.motion.x / window_width, 1.0 * e.motion.y / window_height);
break;
}
case SDL_KEYDOWN:
{
processKey(protocol, e.key.keysym);
break;
}
}
}
}
void application()
{
fullscreen = Settings::fullscreen;
if (fullscreen)
{
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
SDL_SetWindowBordered(window, SDL_FALSE);
}
SDL_ShowWindow(window);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); // Set draw color to black
SDL_RenderClear(renderer); // Clear renderer to black
SDL_RenderPresent(renderer); // Present initial blank frame
std::cout << " > Application started" << std::endl;
VideoBuffer videoBuffer;
videoBuffer.allocate(Settings::width, Settings::height).throwError();
Protocol protocol(Settings::sourceWidth, Settings::sourceHeight, Settings::sourceFps, AV_INPUT_BUFFER_PADDING_SIZE);
Decoder decoder;
PcmAudio audio0, audio1, audio2;
decoder.start(&protocol.videoData, &videoBuffer, AV_CODEC_ID_H264);
audio0.start(&protocol.audioStream0);
audio1.start(&protocol.audioStream1);
audio2.start(&protocol.audioStream2);
protocol.start(onStatus);
UFont textFont(font, font_len, Settings::fontSize);
std::string status = "Initialising";
DrawText(textFont, status);
SDL_RenderPresent(renderer);
UImage image(background, background_len);
SDL_RenderClear(renderer);
DrawImage(image);
DrawText(textFont, status);
SDL_RenderPresent(renderer);
std::cout << " > Application loop" << std::endl;
bool dirty = true;
bool connected = false;
const int activeDelay = 1000 / Settings::fps;
const int inactiveDelay = 1000 / 5; // 5FPS
uint32_t frameDelay = inactiveDelay;
active = true;
uint32_t latestid = 0;
while (active)
{
Uint32 frameStart = SDL_GetTicks();
processEvents(protocol);
if (connected != protocol.phoneConnected)
{
connected = protocol.phoneConnected;
SDL_RenderClear(renderer);
DrawImage(image);
SDL_RenderPresent(renderer);
dirty = true;
frameDelay = connected ? activeDelay : inactiveDelay;
}
if (connected)
{
AVFrame *frame = nullptr;
uint32_t frameid = 0;
if (videoBuffer.getLatest(&frame, &frameid) && frameid != latestid)
{
// Update SDL texture with YUV frame data
SDL_UpdateYUVTexture(videoTexture, nullptr,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
latestid = frameid;
videoBuffer.consumeLatest();
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, videoTexture, nullptr, nullptr);
SDL_RenderPresent(renderer);
}
}
else
{
{
std::lock_guard<std::mutex> lock(statusMutex);
if (status != statusText)
{
status = statusText;
dirty = true;
}
}
if (dirty)
{
SDL_RenderClear(renderer);
DrawImage(image);
DrawText(textFont, status);
SDL_RenderPresent(renderer);
}
}
Uint32 frameTime = SDL_GetTicks() - frameStart;
if (frameTime < frameDelay)
{
SDL_Delay(frameDelay - frameTime); // Sleep only the remaining time
}
}
std::cout << " > Application stopping" << std::endl;
}
int main(int argc, char **argv)
{
std::cout << title << std::endl;
if (argc > 2)
{
std::cerr << " Usage: " << argv[0] << " [settings_file]" << std::endl;
return 1;
}
if (argc == 2)
{
Settings::load(argv[1]);
}
if (!Settings::logging)
disable_cout();
else
Settings::print();
// Initialize SDL video subsystem
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) != 0)
{
std::cerr << "[Main] SDL initialisation failed: " << SDL_GetError() << std::endl;
return 1;
}
if (TTF_Init() != 0)
{
std::cerr << "[Main] TTF initialisation failed: " << TTF_GetError() << std::endl;
SDL_Quit();
return 1;
}
SDL_DisplayMode displayMode;
if (SDL_GetCurrentDisplayMode(0, &displayMode) != 0)
{
std::cerr << "[Main] SDL get display mode failed: " << SDL_GetError() << std::endl;
TTF_Quit();
SDL_Quit();
return 1;
}
if (Settings::fullscreen)
{
width = displayMode.w;
height = displayMode.h;
}
else
{
width = Settings::width;
height = Settings::height;
}
// Create SDL window centered on screen, 800x600 size
window = SDL_CreateWindow(title,
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
width,
height,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN);
if (!window)
{
std::cerr << "[Main] SDL can't create window: " << SDL_GetError() << std::endl;
TTF_Quit();
SDL_Quit();
return 1;
}
// Create accelerated renderer for the window
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer)
{
videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
Settings::width, Settings::height);
if (videoTexture)
{
application();
SDL_DestroyTexture(videoTexture);
std::cout << " > Application finish" << std::endl;
}
else
{
std::cerr << "[Main] SDL can't create video texture: " << SDL_GetError() << std::endl;
}
SDL_DestroyRenderer(renderer);
}
else
{
std::cerr << "[Main] SDL can't create renderer: " << SDL_GetError() << std::endl;
}
if (textTexture)
SDL_DestroyTexture(textTexture);
if (imgTexture)
SDL_DestroyTexture(imgTexture);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
return 0;
}
+164
View File
@@ -0,0 +1,164 @@
#include "pcm_audio.h"
#include "helper/functions.h"
// Implementation
PcmAudio::PcmAudio() {}
PcmAudio::~PcmAudio()
{
stop();
}
void PcmAudio::setVolume(float vol)
{
if(vol<0)
{
_volume = 0;
return;
}
if(vol>1)
{
_volume = 1;
return;
}
_volume = vol;
}
void PcmAudio::start(RawQueue *data)
{
if (_active)
stop();
_data = data;
_active = true;
_thread = std::thread(&PcmAudio::runner, this);
}
void PcmAudio::stop()
{
if (!_active)
return;
_active = false;
_data->notify();
if (_thread.joinable())
_thread.join();
}
void PcmAudio::runner()
{
setThreadName("audio");
SDL_AudioDeviceID device = 0;
SDL_AudioSpec spec;
size_t bufferedBytes = 0;
bool unpaused = false;
size_t targetBytes = 0;
int rate = 0;
int channels = 0;
while (_active)
{
RawEntry segment = _data->wait(_active);
if (!_active)
{
if (segment.data)
free(segment.data);
break;
}
int config = 0;
memcpy(&config, segment.data, sizeof(int));
int newRate = 44100;
int newChannels = 2;
switch (config)
{
case 1:
newRate = 44100;
newChannels = 2;
break;
case 2:
newRate = 44100;
newChannels = 2;
break;
case 3:
newRate = 8000;
newChannels = 1;
break;
case 4:
newRate = 48000;
newChannels = 2;
break;
case 5:
newRate = 16000;
newChannels = 1;
break;
case 6:
newRate = 24000;
newChannels = 1;
break;
case 7:
newRate = 16000;
newChannels = 2;
break;
}
// If settings changed, (re)open audio device
if (device == 0 || rate != newRate || channels != newChannels)
{
rate = newRate;
channels = newChannels;
printf("PCM SETTING %d %d\n", rate, channels);
// Close existing device
if (device != 0)
{
SDL_CloseAudioDevice(device);
device = 0;
}
// Configure new spec
SDL_zero(spec);
spec.freq = rate;
spec.format = AUDIO_S16SYS;
spec.channels = static_cast<Uint8>(channels);
spec.samples = 4096;
spec.callback = nullptr;
device = SDL_OpenAudioDevice(nullptr, 0, &spec, nullptr, 0);
if (device == 0)
{
std::cerr << "Failed to open audio: " << SDL_GetError() << std::endl;
free(segment.data);
continue;
}
// Calculate new buffer target: 0.5s
targetBytes = rate * channels * sizeof(int16_t) / 2;
bufferedBytes = 0;
unpaused = false;
// Start paused
SDL_PauseAudioDevice(device, 1);
}
// Apply volume in-place
int16_t *samples = reinterpret_cast<int16_t *>(segment.data + segment.offset);
size_t count = segment.size / sizeof(int16_t);
for (size_t i = 0; i < count; ++i)
{
samples[i] = static_cast<int16_t>(samples[i] * _volume);
}
// Queue audio
SDL_QueueAudio(device, segment.data + segment.offset, segment.size);
bufferedBytes += segment.size;
free(segment.data);
// Unpause when enough buffered
if (!unpaused && bufferedBytes >= targetBytes)
{
SDL_PauseAudioDevice(device, 0);
unpaused = true;
}
}
if (device)
SDL_CloseAudioDevice(device);
}
+39
View File
@@ -0,0 +1,39 @@
#ifndef SRC_PCM_AUDIO
#define SRC_PCM_AUDIO
#include <iostream>
#include <thread>
#include <atomic>
#include <SDL2/SDL.h>
#include "struct/raw_queue.h"
#include "helper/error.h"
class PcmAudio {
public:
PcmAudio();
~PcmAudio();
// Start playing raw PCM data from queue
void start(RawQueue* data);
void stop();
void setVolume(float vol);
private:
void runner();
void loop(SDL_AudioDeviceID device);
RawQueue* _data = nullptr;
int _sampleRate = 0;
int _channels = 0;
float _volume = 1.0f;
std::thread _thread;
std::atomic<bool> _active{false};
};
#endif /* SRC_PCM_AUDIO */
+279
View File
@@ -0,0 +1,279 @@
#include "protocol.h"
#include <cstring>
Protocol::Protocol(uint16_t width, uint16_t height, uint16_t fps, uint16_t padding)
: connector(padding),
videoData(Settings::queue),
audioStream0(Settings::queue),
audioStream1(Settings::queue),
audioStream2(Settings::queue),
phoneConnected(false),
_width(width),
_height(height),
_fps(fps)
{
}
Protocol::~Protocol()
{
stop();
}
const char *Protocol::cmdString(int cmd)
{
switch (cmd)
{
case CMD_OPEN:
return "Open";
case CMD_PLUGGED:
return "Plugged";
case CMD_UNPLUGGED:
return "Unplugged";
case CMD_TOUCH:
return "Touch";
case CMD_VIDEO_DATA:
return "Video";
case CMD_AUDIO_DATA:
return "Audio";
case CMD_SEND_FILE:
return "File";
default:
return "Unknown";
}
}
void Protocol::start(StatusCallback onStatus)
{
_statusCallback = onStatus;
connector.start(this);
}
void Protocol::stop()
{
connector.stop();
}
void Protocol::sendInit(int width, int height, int fps)
{
uint8_t buf[28];
Connector::write_uint32_le(&buf[0], width);
Connector::write_uint32_le(&buf[4], height);
Connector::write_uint32_le(&buf[8], fps);
Connector::write_uint32_le(&buf[12], 5);
Connector::write_uint32_le(&buf[16], 49152);
Connector::write_uint32_le(&buf[20], 2);
Connector::write_uint32_le(&buf[24], 2);
connector.send(1, buf, 28);
}
void Protocol::sendKey(int key)
{
printf("Send key %d", key);
uint8_t buf[4];
Connector::write_uint32_le(&buf[0], key);
connector.send(8, buf, 4);
}
void Protocol::sendFile(const char *filename, const char *value)
{
uint32_t len = strlen(value);
if (len > 16)
{
throw std::invalid_argument("String too long (max 16 bytes)");
}
// note: we send only the ASCII bytes, no trailing '\0'
sendFile(filename,
reinterpret_cast<const uint8_t *>(value),
static_cast<uint32_t>(len));
}
// overload for a single 32bit integer
void Protocol::sendFile(const char *filename, int value)
{
uint8_t buf[4];
Connector::write_uint32_le(buf, value);
sendFile(filename, buf, 4);
}
void Protocol::sendClick(float x, float y, bool down)
{
uint8_t buf[16];
Connector::write_uint32_le(buf, down ? 14 : 16);
Connector::write_uint32_le(buf + 4, int(10000 * x));
Connector::write_uint32_le(buf + 8, int(10000 * y));
Connector::write_uint32_le(buf + 12, 0);
connector.send(5, buf, 16);
}
void Protocol::sendMove(float dx, float dy)
{
uint8_t buf[16];
Connector::write_uint32_le(buf, 15);
Connector::write_uint32_le(buf + 4, int(10000 * dx));
Connector::write_uint32_le(buf + 8, int(10000 * dy));
Connector::write_uint32_le(buf + 12, 0);
connector.send(5, buf, 16);
}
void Protocol::sendFile(const char *filename, const uint8_t *data, uint32_t length)
{
// filename is assumed nullterminated, so strlen + 1 to include the '\0'
uint32_t fn_len = strlen(filename) + 1;
// Total buffer size: 4 (fn_len) + fn_len + 4 (content_len) + content_len
uint32_t total = 4 + fn_len + 4 + length;
std::vector<uint8_t> result(total);
uint8_t *buf = result.data();
// 1) filename length (LE)
Connector::write_uint32_le(buf, fn_len);
buf += 4;
// 2) filename bytes (including the '\0')
std::memcpy(buf, filename, fn_len);
buf += fn_len;
// 3) content length (LE)
Connector::write_uint32_le(buf, length);
buf += 4;
// 4) content bytes
std::memcpy(buf, data, length);
connector.send(CMD_SEND_FILE, result.data(), total);
}
void Protocol::onStatus(const char *status)
{
if (_statusCallback)
_statusCallback(status);
}
void Protocol::onDevice(bool connected)
{
if (connected)
{
sendInit(_width, _height, _fps);
sendFile("/tmp/night_mode", 0); // 0==day, 1==night
sendFile("/tmp/hand_drive_mode", 0); // 0==left, 1==right
sendFile("/tmp/charge_mode", 0);
sendFile("/etc/box_name", "CarPlay");
}
else
{
}
}
void Protocol::onData(uint32_t cmd, uint32_t length, uint8_t *data)
{
bool dispose = true;
switch (cmd)
{
case CMD_VIDEO_DATA:
{
if (length <= 20)
break;
videoData.push(data, 20, length - 20);
dispose = false;
break;
}
case CMD_AUDIO_DATA:
{
if (length <= 13)
{
print_message(cmd, length, data);
break;
}
int channel = 0;
memcpy(&channel, data + 8, sizeof(int));
if (channel == 0)
{
audioStream0.push(data, 12, length - 12);
dispose = false;
break;
}
if (channel == 1)
{
audioStream1.push(data, 12, length - 12);
dispose = false;
break;
}
if (channel == 2)
{
audioStream2.push(data, 12, length - 12);
dispose = false;
break;
}
print_message(cmd, length, data);
break;
}
case CMD_PLUGGED:
{
phoneConnected = true;
break;
}
case CMD_UNPLUGGED:
{
phoneConnected = false;
break;
}
default:
print_message(cmd, length, data);
break;
}
if (dispose && length > 0 && data)
free(data);
}
void Protocol::print_ints(uint32_t length, uint8_t *data, uint16_t max)
{
if (data && length >= 4)
{
printf(" > ");
size_t count = length / 4;
for (size_t i = 0; (i < count) & (i < max); ++i)
{
uint32_t val =
((uint32_t)data[i * 4 + 0]) |
((uint32_t)data[i * 4 + 1] << 8) |
((uint32_t)data[i * 4 + 2] << 16) |
((uint32_t)data[i * 4 + 3] << 24);
printf("%u ", val);
}
printf("\n");
}
}
void Protocol::print_bytes(uint32_t length, uint8_t *data, uint16_t max)
{
if (data && length >= 4)
{
printf(" > ");
for (size_t i = 0; (i < length) & (i < max); ++i)
{
printf("%d ", data[i]);
}
printf("\n");
}
}
void Protocol::print_message(uint32_t cmd, uint32_t length, uint8_t *data)
{
printf("Cmd: %-3u %-10s Size: %-6u > ", cmd, cmdString(cmd), length);
if (data && length > 0)
for (size_t i = 0; i < 40 && i < length; ++i)
{
char ch = static_cast<char>(data[i]);
printf("%c", isprint(ch) ? ch : '.');
}
// for (int i = 0; i < length && i < 10; i++)
// printf("%-4u", data[i]);
printf("\n");
}
+65
View File
@@ -0,0 +1,65 @@
#ifndef SRC_PROTOCOL
#define SRC_PROTOCOL
#include "struct/raw_queue.h"
#include "helper/iprotocol.h"
#include "helper/settings.h"
#include "connector.h"
#define MAGIC 0x55aa55aa
#define CMD_OPEN 1
#define CMD_PLUGGED 2
#define CMD_UNPLUGGED 4
#define CMD_TOUCH 5
#define CMD_VIDEO_DATA 6
#define CMD_AUDIO_DATA 7
#define CMD_SEND_FILE 153
class Protocol : public IProtocol
{
public:
Protocol(uint16_t width, uint16_t height, uint16_t fps, uint16_t padding);
~Protocol();
Protocol(const Protocol &) = delete;
Protocol &operator=(const Protocol &) = delete;
static const char *cmdString(int cmd);
void start(StatusCallback onStatus);
void stop();
void sendKey(int key);
void sendInit(int width, int height, int fps);
void sendFile(const char *filename, const uint8_t *data, uint32_t length);
void sendFile(const char *filename, const char *value);
void sendFile(const char *filename, int value);
void sendClick(float x, float y, bool down);
void sendMove(float dx, float dy);
Connector connector;
RawQueue videoData;
RawQueue audioStream0;
RawQueue audioStream1;
RawQueue audioStream2;
bool phoneConnected;
private:
void onStatus(const char *status) override;
void onDevice(bool connected) override;
void onData(uint32_t cmd, uint32_t length, uint8_t *data) override;
void print_message(uint32_t cmd, uint32_t length, uint8_t *data);
void print_ints(uint32_t length, uint8_t *data, uint16_t max);
void print_bytes(uint32_t length, uint8_t *data, uint16_t max);
uint16_t _width;
uint16_t _height;
uint16_t _fps;
StatusCallback _statusCallback = nullptr;
};
#endif /* SRC_PROTOCOL */
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+7
View File
@@ -0,0 +1,7 @@
#ifndef SRC_IMG_BACKGROUND
#define SRC_IMG_BACKGROUND
extern unsigned char background[];
extern unsigned int background_len;
#endif /* SRC_IMG_BACKGROUND */
+7
View File
@@ -0,0 +1,7 @@
#ifndef SRC_RESOURCE_SST_REGULAR
#define SRC_RESOURCE_SST_REGULAR
extern unsigned char font[];
extern unsigned int font_len;
#endif /* SRC_RESOURCE_SST_REGULAR */
Binary file not shown.
+69
View File
@@ -0,0 +1,69 @@
#include "raw_queue.h"
RawQueue::RawQueue(uint16_t capacity)
: _buffer(capacity), _head(0), _tail(0), _size(0), _capacity(capacity)
{
}
RawQueue::~RawQueue()
{
clear();
}
bool RawQueue::push(uint8_t *data, int offset, int size)
{
std::lock_guard<std::mutex> lock(_mutex);
if (_size == _buffer.size())
{
free(data);
return false; // queue full
}
_buffer[_tail] = RawEntry{data, offset, size};
_tail = (_tail + 1) % _capacity;
_size++;
_condition.notify_one();
return true;
}
RawEntry RawQueue::wait(const std::atomic<bool> &reading)
{
std::unique_lock<std::mutex> lock(_mutex);
_condition.wait(lock, [&]
{ return !reading.load() || _size > 0; });
if (!reading || _size == 0)
return RawEntry{nullptr, 0, 0};
RawEntry entry = _buffer[_head];
_head = (_head + 1) % _capacity;
_size--;
return entry;
}
void RawQueue::clear()
{
std::lock_guard<std::mutex> lock(_mutex);
// Free any remaining buffers
while (_size > 0)
{
RawEntry &e = _buffer[_head];
if (e.data)
{
free(e.data);
e.data = nullptr;
}
_head = (_head + 1) % _capacity;
_size--;
}
// Reset indices
_head = _tail = 0;
}
void RawQueue::notify()
{
_condition.notify_all();
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef SRC_RAW_QUEUE
#define SRC_RAW_QUEUE
#include <cstdint>
#include <vector>
#include <atomic>
#include <mutex>
#include <condition_variable>
struct RawEntry
{
uint8_t *data;
int offset;
int size;
};
// Single entry: raw buffer pointer + metadata
class RawQueue
{
public:
RawQueue(uint16_t capacity = 256);
~RawQueue();
// Non-blocking push: returns false if full
bool push(uint8_t *data, int offset, int size);
// Blocks until an entry is available or reader_active == false
RawEntry wait(const std::atomic<bool> &reading);
// Clears the queue and frees any pending buffers
void clear();
// Unlock queus
void notify();
private:
std::vector<RawEntry> _buffer;
uint16_t _head;
uint16_t _tail;
uint16_t _size;
uint16_t _capacity;
std::mutex _mutex;
std::condition_variable _condition;
};
#endif
+111
View File
@@ -0,0 +1,111 @@
#include "video_buffer.h"
extern "C"
{
#include <libavutil/imgutils.h>
}
#include <stdexcept>
#include <string>
#include "error.h"
VideoBuffer::VideoBuffer()
: _width(0), _height(0)
{
}
// Allocate two YUV420P frames for double buffering and initialize to black
Error VideoBuffer::allocate(uint16_t width, uint16_t height)
{
_width = width;
_height = height;
deallocate();
reset();
Error e;
for (uint8_t i = 0; i < BUFFER_VIDEO_FRAMES; ++i)
{
// Allocate AVFrame
_frames[i] = av_frame_alloc();
if (e.null(_frames[i], "Failed to allocate AVFrame"))
break;
_frames[i]->format = AV_PIX_FMT_YUV420P;
_frames[i]->width = width;
_frames[i]->height = height;
// Allocate data buffer with 32 byte allingment
if (e.avFail(av_frame_get_buffer(_frames[i], 32), "Failed to allocate AVFrame buffer"))
break;
// Set Y plane to black (0)
memset(_frames[i]->data[0], 0, _frames[i]->linesize[0] * height);
// Set U plane to 128 (neutral)
memset(_frames[i]->data[1], 128, _frames[i]->linesize[1] * (height / 2));
// Set V plane to 128 (neutral)
memset(_frames[i]->data[2], 128, _frames[i]->linesize[2] * (height / 2));
}
return e;
}
VideoBuffer::~VideoBuffer()
{
deallocate();
}
void VideoBuffer::deallocate()
{
for (uint8_t i = 0; i < BUFFER_VIDEO_FRAMES; ++i)
{
if (_frames[i])
{
// Free the frame itself
av_frame_free(&_frames[i]);
// Clear
_frames[i] = nullptr;
}
}
}
bool VideoBuffer::getLatest(AVFrame **frame, uint32_t *id)
{
_reading.store(_latest.load());
int index = _reading.load();
if (index < 0)
return false;
*frame = _frames[index];
*id = _ids[index];
return true;
}
void VideoBuffer::consumeLatest()
{
_reading.store(-1);
}
const AVFrame *VideoBuffer::writeFrame(uint32_t id)
{
int index = _writing.load();
while (index == _reading.load() || index == _latest.load())
{
index = (index + 1) % BUFFER_VIDEO_FRAMES;
}
_writing.store(index);
_ids[index] = id;
return _frames[index];
}
void VideoBuffer::commitFrame()
{
_latest.store(_writing.load());
}
void VideoBuffer::reset()
{
_writing.store(0);
_reading.store(-1);
_latest.store(-1);
for (uint8_t i = 0; i < BUFFER_VIDEO_FRAMES; i++)
{
_ids[i] = 0;
}
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef SRC_VIDEO_BUFFER
#define SRC_VIDEO_BUFFER
extern "C"
{
#include <libavutil/frame.h> // For AVFrame
}
#include <atomic>
#include "helper/error.h"
#define BUFFER_VIDEO_FRAMES 3
class VideoBuffer
{
public:
VideoBuffer();
~VideoBuffer();
Error allocate(uint16_t width, uint16_t height);
uint16_t width() const { return _width; };
uint16_t height() const { return _height; };
void reset();
bool getLatest(AVFrame **frame, uint32_t *id);
void consumeLatest();
const AVFrame *writeFrame(uint32_t id);
void commitFrame();
private:
void deallocate();
uint16_t _width;
uint16_t _height;
std::atomic<int8_t> _latest;
std::atomic<int8_t> _reading;
std::atomic<int8_t> _writing;
AVFrame *_frames[BUFFER_VIDEO_FRAMES] = {nullptr, nullptr, nullptr};
uint32_t _ids[BUFFER_VIDEO_FRAMES];
};
#endif /* SRC_VIDEO_BUFFER */
+62
View File
@@ -0,0 +1,62 @@
#ifndef SRC_UX_UFONT
#define SRC_UX_UFONT
#include <SDL2/SDL_ttf.h>
#include <iostream>
class UFont
{
public:
UFont(const void *font_data, int data_size, int ptsize)
{
SDL_RWops *font_rw = SDL_RWFromConstMem(font_data, data_size);
if (!font_rw)
{
std::cerr << "[UX] SDL can't open font: " << SDL_GetError() << std::endl;
return;
}
_font = TTF_OpenFontRW(font_rw, 1, ptsize);
if (!_font)
{
std::cerr << "[UX] SDL can't load font: " << TTF_GetError() << std::endl;
}
};
~UFont()
{
if (_font)
{
TTF_CloseFont(_font);
_font = nullptr;
}
};
SDL_Texture *GetText(SDL_Renderer *renderer, const char *text, SDL_Color color)
{
if (!_font)
return nullptr;
SDL_Surface *textSurface = TTF_RenderText_Blended(_font, text, color);
if (!textSurface)
{
std::cerr << "[UX] Failed to create text surface: " << TTF_GetError() << std::endl;
return nullptr;
}
SDL_Texture *textTexture = SDL_CreateTextureFromSurface(renderer, textSurface);
SDL_FreeSurface(textSurface);
if (!textTexture)
{
std::cerr << "[UX] Failed to create text texture: " << TTF_GetError() << std::endl;
return nullptr;
}
return textTexture;
}
private:
TTF_Font *_font = nullptr;
};
#endif /* SRC_UX_UFONT */
+52
View File
@@ -0,0 +1,52 @@
#ifndef SRC_UX_UIMAGE
#define SRC_UX_UIMAGE
#include <SDL2/SDL.h>
#include <iostream>
class UImage
{
public:
UImage(const void *img_data, int img_size)
{
SDL_RWops *img_rw = SDL_RWFromConstMem(img_data, img_size);
if (!img_rw)
{
std::cerr << "[UX] SDL can't open image: " << SDL_GetError() << std::endl;
return;
}
_surface = SDL_LoadBMP_RW(img_rw, 1);
if (!_surface)
{
std::cerr << "[UX] Failed to create image surface: " << SDL_GetError() << std::endl;
return;
}
Width = _surface->w;
Height = _surface->h;
};
~UImage()
{
if (_surface)
{
SDL_FreeSurface(_surface);
_surface = nullptr;
}
};
SDL_Texture *GetImage(SDL_Renderer *renderer)
{
return SDL_CreateTextureFromSurface(renderer, _surface);
}
int Width = 0;
int Height = 0;
private:
SDL_Surface *_surface = nullptr;
};
#endif