mirror of
https://github.com/niellun/FastCarPlay.git
synced 2026-06-07 09:38:25 +02:00
Initial commit. Version 0.1
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Generated files
|
||||
out/
|
||||
headers/
|
||||
src/autogen
|
||||
|
||||
# General files
|
||||
.*/
|
||||
.DS*
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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 .
|
||||
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:
|
||||
- 
|
||||
- 
|
||||
- 
|
||||
|
||||
The project is licenced under GPL-3 licence. See LICENCE for details.
|
||||
The project is using  font. See FONT_LICENCE for details.
|
||||
|
||||
# Finally
|
||||
If you have any questions, suggestions or you find problems running this feel free to open issue.
|
||||
|
||||
@@ -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
|
||||
@@ -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(); });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 auto‑registers 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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 32‑bit 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 null‑terminated, 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");
|
||||
}
|
||||
@@ -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 |
@@ -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 */
|
||||
@@ -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.
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user