diff --git a/docs/HTTP_CONTROL_SERVER.md b/docs/HTTP_CONTROL_SERVER.md index 33e5719c2..8a5dffee8 100644 --- a/docs/HTTP_CONTROL_SERVER.md +++ b/docs/HTTP_CONTROL_SERVER.md @@ -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 @@ -103,6 +104,38 @@ The paramater format specifies which image format to use. It can be set to png, `````` +# /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 +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. diff --git a/src/http_control_server.cpp b/src/http_control_server.cpp index e0f59ad21..b2af4e165 100644 --- a/src/http_control_server.cpp +++ b/src/http_control_server.cpp @@ -6,15 +6,97 @@ extern "C"{ #include #include #include +#include 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 params; for(auto &v :req.params){ params.push_back(v.first.c_str()); @@ -42,7 +124,8 @@ struct HCSServer{ std::cout<<"Terminating HCS: http://localhost:"<port<port = port; thread = std::thread(server_thread,this); } @@ -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(); } diff --git a/src/http_control_server.h b/src/http_control_server.h index cdd974d22..2be49e6b4 100644 --- a/src/http_control_server.h +++ b/src/http_control_server.h @@ -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(); diff --git a/src/main.c b/src/main.c index bfeb5874a..beaea9542 100644 --- a/src/main.c +++ b/src/main.c @@ -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(); @@ -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