Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/HTTP_CONTROL_SERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ SkyEmu contains a web server that implements a REST-like API that can be used to
This interface provides access to the following functionality:
- Loading arbitrary ROM files
- Retrieving the emulated screen's image
- Real-time streaming of the emulated screen
- Reading/Writing arbitrary memory addresses in the emulated system
- Stepping the emulator a controlled number of frames
- Controlling user inputs for the emulator and emulated console
Expand Down Expand Up @@ -103,6 +104,38 @@ The paramater format specifies which image format to use. It can be set to png,

```<much larger png image of screen with save state embedded>```

# /stream command

Provides a continuous real-time stream of the emulated screen using multipart/x-mixed-replace (MJPEG-style streaming). This is much more efficient than repeatedly calling /screen for real-time display, as it maintains a single HTTP connection and continuously sends frames.

The parameter `fps` controls the frame rate (default: 30, min: 1, max: 60).

Frames are encoded as JPEG for optimal streaming performance.

**Example:**

```http://localhost:8080/stream```

**Result:**

Continuous stream of JPEG images at 30 FPS. This endpoint is designed to be opened in a browser or used with tools that support MJPEG streams (like VLC, ffmpeg, or img tags in HTML).

**Example (Custom FPS):**

```http://localhost:8080/stream?fps=60```

**Result:**

Continuous stream of JPEG images at 60 FPS.

**Usage in HTML:**

```html
<img src="http://localhost:8080/stream" alt="SkyEmu Stream">
```

**Note:** The stream will continue until the client disconnects. This endpoint is ideal for headless mode where you want to display the emulator output in real-time without the overhead of multiple HTTP requests.

# /read_byte command

Reads one or multiple bytes of data from the emulated system at addresses provided using parameters. The addr parameter can be repeated an arbitrary amount of times to read an arbitrary amount of bytes.
Expand Down
95 changes: 93 additions & 2 deletions src/http_control_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,97 @@ extern "C"{
#include <iostream>
#include <sstream>
#include <vector>
#include <chrono>
struct HCSServer{
hcs_callback callback;
hcs_callback callback;
hcs_stream_callback stream_callback;
httplib::Server svr;
std::recursive_mutex mutex;
std::thread thread;
int64_t port;
static void server_thread(HCSServer* server){
server->svr.set_tcp_nodelay(true);

// Register /stream endpoint with streaming support
server->svr.Get("/stream", [server](const httplib::Request& req, httplib::Response& res) {
if(!server->stream_callback) {
res.status = 503;
res.set_content("Streaming not available", "text/plain");
return;
}

// Parse FPS parameter (default 30)
int fps = 30;
auto fps_param = req.get_param_value("fps");
if(!fps_param.empty()) {
try {
fps = std::stoi(fps_param);
if(fps < 1) fps = 1;
if(fps > 60) fps = 60;
} catch (const std::invalid_argument&) {
fps = 30; // Default on invalid input
} catch (const std::out_of_range&) {
fps = 30; // Default on out of range
}
}

int frame_delay_ms = 1000 / fps;

// Use multipart/x-mixed-replace for MJPEG-style streaming
std::string boundary = "frame";
std::string content_type = "multipart/x-mixed-replace; boundary=" + boundary;

res.set_chunked_content_provider(
content_type,
[server, boundary, frame_delay_ms](size_t offset, httplib::DataSink& sink) {
uint64_t result_size = 0;
server->mutex.lock();
uint8_t* frame_data = server->stream_callback(&result_size);
server->mutex.unlock();

if(!frame_data || result_size == 0) {
return false; // End stream
}

// Send multipart frame header
std::ostringstream header;
header << "--" << boundary << "\r\n";
header << "Content-Type: image/jpeg\r\n";
header << "Content-Length: " << result_size << "\r\n\r\n";

std::string header_str = header.str();
if(!sink.write(header_str.c_str(), header_str.size())) {
free(frame_data);
return false;
}

// Send frame data
if(!sink.write((const char*)frame_data, result_size)) {
free(frame_data);
return false;
}

free(frame_data);

// Send trailing newline
if(!sink.write("\r\n", 2)) {
return false;
}

// Delay to control frame rate
std::this_thread::sleep_for(std::chrono::milliseconds(frame_delay_ms));

return true; // Continue streaming
}
);
});

server->svr.set_pre_routing_handler([server](const httplib::Request& req, httplib::Response& res) {
// Skip /stream as it's handled by dedicated route
if(req.path == "/stream") {
return httplib::Server::HandlerResponse::Unhandled;
}

std::vector<const char*> params;
for(auto &v :req.params){
params.push_back(v.first.c_str());
Expand Down Expand Up @@ -42,7 +124,8 @@ struct HCSServer{
std::cout<<"Terminating HCS: http://localhost:"<<server->port<<std::endl;
}
HCSServer(int64_t port, hcs_callback call){
callback = call;
callback = call;
stream_callback = NULL;
this->port = port;
thread = std::thread(server_thread,this);
}
Expand All @@ -67,6 +150,14 @@ extern "C"{
if(server)server->mutex.unlock();
}

void hcs_set_stream_callback(hcs_stream_callback stream_callback){
if(server){
server->mutex.lock();
server->stream_callback = stream_callback;
server->mutex.unlock();
}
}

void hcs_suspend_callbacks(){
if(server)server->mutex.lock();
}
Expand Down
10 changes: 10 additions & 0 deletions src/http_control_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@
// the call back will set mime_type to the desired mime type for the return;
//Returns malloc'd data for a handled response or NULL for a non-handled response.
typedef uint8_t* (*hcs_callback)(const char* cmd, const char** params, uint64_t* result_size, const char** mime_type);

//Streaming callback for /stream endpoint
// Called repeatedly to get new frames for streaming
// Returns malloc'd JPEG data for the current frame or NULL to end stream
// result_size will be set to the size of the returned data
typedef uint8_t* (*hcs_stream_callback)(uint64_t* result_size);

//Update the HCS, and start/kill the server if needed
void hcs_update(bool enable, int64_t port, hcs_callback callback);

//Set the streaming callback for /stream endpoint
void hcs_set_stream_callback(hcs_stream_callback stream_callback);

//Suspend and resume callbacks from multiple threads
void hcs_suspend_callbacks();
void hcs_resume_callbacks();
Expand Down
25 changes: 25 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ static uint32_t se_save_best_effort_state(se_core_state_t* state);
static bool se_load_best_effort_state(se_core_state_t* state,uint8_t *save_state_data, uint32_t size, uint32_t bess_offset);
static size_t se_get_core_size();
uint8_t* se_hcs_callback(const char* cmd, const char** params, uint64_t* result_size, const char** mime_type);
uint8_t* se_hcs_stream_callback(uint64_t* result_size);
void se_open_file_browser(bool clicked, float x, float y, float w, float h, void (*file_open_fn)(const char* dir), const char ** file_types,char * output_path);
void se_file_browser_accept(const char * path);
static void se_reset_core();
Expand Down Expand Up @@ -5327,6 +5328,7 @@ void se_update_frame() {
#ifdef ENABLE_HTTP_CONTROL_SERVER
hcs_update(gui_state.settings.http_control_server_enable,gui_state.settings.http_control_server_port,se_hcs_callback);
if(gui_state.settings.http_control_server_enable){
hcs_set_stream_callback(se_hcs_stream_callback);
for(int i=0;i<SE_NUM_KEYBINDS;++i)emu_state.joy.inputs[i]+=gui_state.hcs_joypad.inputs[i];
}
hcs_suspend_callbacks();
Expand Down Expand Up @@ -6987,6 +6989,29 @@ uint8_t* se_hcs_callback(const char* cmd, const char** params, uint64_t* result_
return NULL;
}

uint8_t* se_hcs_stream_callback(uint64_t* result_size){
*result_size = 0;

// Return NULL if no ROM is loaded
if(!emu_state.rom_loaded) {
return NULL;
}

// Get the current screen data
uint8_t* imdata = (uint8_t*)malloc(SE_MAX_SCREENSHOT_SIZE);
int out_width = 0, out_height = 0;
se_screenshot(imdata, &out_width, &out_height);

// Encode as JPEG (more efficient for streaming than PNG)
// Note: se_png_write_context_t is used for both PNG and JPEG encoding
se_png_write_context_t jpeg_write_ctx = {0};
stbi_write_jpg_to_func(se_png_write_mem, &jpeg_write_ctx, out_width, out_height, 4, imdata, 85);
free(imdata);

*result_size = jpeg_write_ctx.size;
return jpeg_write_ctx.data;
}

#endif

static void frame(void) {
Expand Down
Loading