FlightGear next
ScreenshotUriHandler.cxx
Go to the documentation of this file.
1// ScreenshotUriHandler.cxx -- Provide screenshots via http
2//
3// Started by Curtis Olson, started June 2001.
4// osg support written by James Turner
5// Ported to new httpd infrastructure by Torsten Dreyer
6//
7// Copyright (C) 2014 Torsten Dreyer
8//
9// This program is free software; you can redistribute it and/or
10// modify it under the terms of the GNU General Public License as
11// published by the Free Software Foundation; either version 2 of the
12// License, or (at your option) any later version.
13//
14// This program is distributed in the hope that it will be useful, but
15// WITHOUT ANY WARRANTY; without even the implied warranty of
16// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17// General Public License for more details.
18//
19// You should have received a copy of the GNU General Public License
20// along with this program; if not, write to the Free Software
21// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
24
25#include <osgDB/Registry>
26#include <osgDB/ReaderWriter>
27#include <osgUtil/SceneView>
28#include <osgViewer/Viewer>
29
30#include <Canvas/canvas_mgr.hxx>
31#include <simgear/canvas/Canvas.hxx>
32
33#include <simgear/threads/SGQueue.hxx>
34#include <simgear/structure/Singleton.hxx>
35#include <Main/globals.hxx>
36#include <Viewer/renderer.hxx>
37
38#include <queue>
39
40using std::string;
41using std::vector;
42using std::list;
43
44namespace sc = simgear::canvas;
45
46namespace flightgear {
47namespace http {
48
50
52public:
53 virtual void imageReady(osg::ref_ptr<osg::Image>) = 0;
55 {
56 }
57};
58
60public:
61 virtual void stringReady(const std::string &) = 0;
63 {
64 }
65};
66
69 string format;
70 osg::ref_ptr<osg::Image> image;
71
76
78 {
80 format = other.format;
81 image = other.image;
82 }
83
85 {
87 format = other.format;
88 image = other.image;
89 return *this;
90 }
91
92};
93
94class ImageCompressor: public OpenThreads::Thread {
95public:
97 {
98 }
99 virtual void run();
100 void addTask(ImageCompressionTask & task);
101 private:
102 typedef SGBlockingQueue<ImageCompressionTask> TaskList;
103 TaskList _tasks;
104};
105
106typedef simgear::Singleton<ImageCompressor> ImageCompressorSingleton;
107
109{
110 osg::ref_ptr<osgDB::ReaderWriter::Options> options = new osgDB::ReaderWriter::Options("JPEG_QUALITY 80 PNG_COMPRESSION 9");
111
112 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor is running");
113 for (;;) {
114 ImageCompressionTask task = _tasks.pop();
115 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor has an image");
116 if ( NULL != task.stringReadyListener) {
117 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor checking for writer for " << task.format);
118 osgDB::ReaderWriter* writer = osgDB::Registry::instance()->getReaderWriterForExtension(task.format);
119 if (!writer)
120 continue;
121
122 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor compressing to " << task.format);
123 std::stringstream outputStream;
124 osgDB::ReaderWriter::WriteResult wr;
125 wr = writer->writeImage(*task.image, outputStream, options);
126
127 if (wr.success()) {
128 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor compressed to " << task.format);
129 task.stringReadyListener->stringReady(outputStream.str());
130 }
131 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor done for this image" << task.format);
132 }
133 }
134 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor exiting");
135}
136
138{
139 _tasks.push(task);
140}
141
146class ScreenshotCallback: public osg::Camera::DrawCallback {
147public:
149 : _min_delta_tick(1.0/8.0)
150 {
151 _previousFrameTick = osg::Timer::instance()->tick();
152 }
153
154 virtual void operator ()(osg::RenderInfo& renderInfo) const
155 {
156 osg::Timer_t n = osg::Timer::instance()->tick();
157 double dt = osg::Timer::instance()->delta_s(_previousFrameTick,n);
158 if (dt < _min_delta_tick)
159 return;
160 _previousFrameTick = n;
161
162 bool hasSubscribers = false;
163 {
164 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
165 hasSubscribers = !_subscribers.empty();
166
167 }
168 if (hasSubscribers) {
169 osg::ref_ptr<osg::Image> image = new osg::Image;
170 const osg::Viewport* vp = renderInfo.getState()->getCurrentViewport();
171 image->readPixels(vp->x(), vp->y(), vp->width(), vp->height(), GL_RGB, GL_UNSIGNED_BYTE);
172 {
173 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
174 while (!_subscribers.empty()) {
175 try {
176 _subscribers.back()->imageReady(image);
177 }
178 catch (...) {
179 }
180 _subscribers.pop_back();
181
182 }
183 }
184 }
185 }
186
187 void subscribe(ImageReadyListener * subscriber)
188 {
189 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
190 _subscribers.push_back(subscriber);
191 }
192
194 {
195 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
196 _subscribers.remove( subscriber );
197 }
198
199private:
200 mutable list<ImageReadyListener*> _subscribers;
201 mutable OpenThreads::Mutex _lock;
202 mutable double _previousFrameTick;
203 double _min_delta_tick;
204};
205
207
209public:
210 ScreenshotRequest(const string & window, const string & type, bool stream)
211 : _type(type), _stream(stream)
212 {
213 if ( NULL == osgDB::Registry::instance()->getReaderWriterForExtension(_type))
214 throw sg_format_exception("Unsupported image type: " + type, type);
215
216 osg::Camera * camera = findLastCamera(globals->get_renderer()->getViewerBase(), window);
217 if ( NULL == camera)
218 throw sg_error("Can't find a camera for window '" + window + "'");
219
220 // add our ScreenshotCallback to the camera
221 if ( NULL == camera->getFinalDrawCallback()) {
222 // TODO: are we leaking the Callback on reinit?
223 camera->setFinalDrawCallback(new ScreenshotCallback());
224 }
225
226 _screenshotCallback = dynamic_cast<ScreenshotCallback*>(camera->getFinalDrawCallback());
227 if ( NULL == _screenshotCallback)
228 throw sg_error("Can't find ScreenshotCallback");
229
231 }
232
234 {
235 _screenshotCallback->unsubscribe(this);
236 }
237
238 virtual void imageReady(osg::ref_ptr<osg::Image> rawImage)
239 {
240 // called from a rendering thread, not from the main loop
242 task.image = rawImage;
243 task.format = _type;
244 task.stringReadyListener = this;
245 ImageCompressorSingleton::instance()->addTask(task);
246 }
247
249 {
250 _screenshotCallback->subscribe(this);
251 }
252
253 mutable OpenThreads::Mutex _lock;
254
255 virtual void stringReady(const string & s)
256 {
257 // called from the compressor thread
258 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
259 _compressedData = s;
260 }
261
263 {
264 string reply;
265 {
266 // called from the main loop
267 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
268 reply = _compressedData;
269 _compressedData.clear();
270 }
271 return reply;
272 }
273
274 osg::Camera* findLastCamera(osgViewer::ViewerBase * viewer, const string & windowName)
275 {
276 osgViewer::ViewerBase::Windows windows;
277 viewer->getWindows(windows);
278
279 osgViewer::GraphicsWindow* window = NULL;
280
281 if (!windowName.empty()) {
282 for (osgViewer::ViewerBase::Windows::iterator itr = windows.begin(); itr != windows.end(); ++itr) {
283 if ((*itr)->getTraits()->windowName == windowName) {
284 window = *itr;
285 break;
286 }
287 }
288 }
289
290 if ( NULL == window) {
291 if (!windowName.empty()) {
292 SG_LOG(SG_NETWORK, SG_INFO, "requested window " << windowName << " not found, using first window");
293 }
294 window = *windows.begin();
295 }
296
297 SG_LOG(SG_NETWORK, SG_DEBUG, "Looking for last Camera of window '" << window->getTraits()->windowName << "'");
298
299 osg::GraphicsContext::Cameras& cameras = window->getCameras();
300 osg::Camera* lastCamera = 0;
301 for (osg::GraphicsContext::Cameras::iterator cam_itr = cameras.begin(); cam_itr != cameras.end(); ++cam_itr) {
302 if (lastCamera) {
303 if ((*cam_itr)->getRenderOrder() > lastCamera->getRenderOrder()) {
304 lastCamera = (*cam_itr);
305 }
306 if ((*cam_itr)->getRenderOrder() == lastCamera->getRenderOrder()
307 && (*cam_itr)->getRenderOrderNum() >= lastCamera->getRenderOrderNum()) {
308 lastCamera = (*cam_itr);
309 }
310 } else {
311 lastCamera = *cam_itr;
312 }
313 }
314
315 return lastCamera;
316 }
317
318 bool isStream() const
319 {
320 return _stream;
321 }
322
323 const string & getType() const
324 {
325 return _type;
326 }
327
328private:
329 string _type;
330 bool _stream;
331 string _compressedData;
332 ScreenshotCallback * _screenshotCallback;
333};
334
337class CanvasImageRequest : public ConnectionData, public simgear::canvas::CanvasImageReadyListener, StringReadyListener {
338public:
340 sc::CanvasPtr canvas;
341 int connected = 0;
342
343 CanvasImageRequest(const string & window, const string & type, int canvasindex, bool stream)
344 : _type(type), _stream(stream) {
345 SG_LOG(SG_NETWORK, SG_DEBUG, "CanvasImageRequest:");
346
347 if (NULL == osgDB::Registry::instance()->getReaderWriterForExtension(_type))
348 throw sg_format_exception("Unsupported image type: " + type, type, {}, false /* don't report */);
349
350 auto canvas_mgr = globals->get_subsystem<CanvasMgr>();
351 if (!canvas_mgr) {
352 SG_LOG(SG_NETWORK, SG_WARN, "CanvasImage:CanvasMgr not found");
353 } else {
354 canvas = canvas_mgr->getCanvas(canvasindex);
355 if (!canvas) {
356 throw sg_error("CanvasImage:Canvas not found for index " + std::to_string(canvasindex), {}, false /* don't report */);
357 } else {
358 SG_LOG(SG_NETWORK, SG_DEBUG, "CanvasImage:Canvas found for index " << canvasindex);
359 //SG_LOG(SG_NETWORK, SG_DEBUG, "CanvasImageRequest: found camera " << camera << ", width=" << canvas->getSizeX() << ", height=%d\n" << canvas->getSizeY());
360
361 SGConstPropertyNode_ptr canvasnode = canvas->getProps();
362 if (canvasnode) {
363 string canvasname = canvasnode->getStringValue("name");
364 if (!canvasname.empty()) {
365 SG_LOG(SG_NETWORK, SG_INFO, "CanvasImageRequest: node=" << canvasnode->getDisplayName().c_str() << ", canvasname =" << canvasname);
366 }
367 }
368 //Looping until success is no option
369 connected = canvas->subscribe(this);
370 }
371 }
372 }
373
374 // Assumption: when unsubscribe returns,there might just be a compressor thread running,
375 // causing a crash when the deconstructor finishes. Rare, but might happen. Just wait to be sure.
377 if (currenttask){
378 SG_LOG(SG_NETWORK, SG_ALERT, "CanvasImage: task running, pausing for 15 seconds");
379 SGTimeStamp::sleepFor(SGTimeStamp::fromSec(15));
380 }
381
382 if (canvas && connected){
383 canvas->unsubscribe(this);
384 }
385 }
386
387 virtual void imageReady(osg::ref_ptr<osg::Image> rawImage) {
388 SG_LOG(SG_NETWORK, SG_DEBUG, "CanvasImage:imageReady");
389 // called from a rendering thread, not from the main loop
391 currenttask = &task;
392 task.image = rawImage;
393 task.format = _type;
394 task.stringReadyListener = this;
395 ImageCompressorSingleton::instance()->addTask(task);
396 }
397
399 connected = canvas->subscribe(this);
400 }
401
402 mutable OpenThreads::Mutex _lock;
403
404 virtual void stringReady(const string & s) {
405 SG_LOG(SG_NETWORK, SG_DEBUG, "CanvasImage:stringReady");
406
407 // called from the compressor thread
408 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
409 _compressedData = s;
410 // allow destructor
411 currenttask = NULL;
412 }
413
414 string getCanvasImage() {
415 string reply;
416 {
417 // called from the main loop
418 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
419 reply = _compressedData;
420 _compressedData.clear();
421 }
422 return reply;
423 }
424
425 bool isStream() const {
426 return _stream;
427 }
428
429 const string & getType() const {
430 return _type;
431 }
432
433private:
434 string _type;
435 bool _stream;
436 string _compressedData;
437};
438
440 : URIHandler(uri)
441{
442}
443
445{
446 ImageCompressorSingleton::instance()->cancel();
447 //ImageCompressorSingleton::instance()->join();
448}
449
450const static string KEY_SCREENSHOT("ScreenshotUriHandler::ScreenshotRequest");
451const static string KEY_CANVASIMAGE("ScreenshotUriHandler::CanvasImageRequest");
452#define BOUNDARY "--fgfs-screenshot-boundary"
453
454bool ScreenshotUriHandler::handleGetRequest(const HTTPRequest & request, HTTPResponse & response, Connection * connection)
455{
456 if (!ImageCompressorSingleton::instance()->isRunning())
457 ImageCompressorSingleton::instance()->start();
458
459 string type = request.RequestVariables.get("type");
460 if (type.empty()) type = "jpg";
461
462 // string camera = request.RequestVariables.get("camera");
463 string window = request.RequestVariables.get("window");
464
465 bool stream = (!request.RequestVariables.get("stream").empty());
466
467 int canvasindex = -1;
468 string s_canvasindex = request.RequestVariables.get("canvasindex");
469 if (!s_canvasindex.empty()) canvasindex = atoi(s_canvasindex.c_str());
470
471 SGSharedPtr<ScreenshotRequest> screenshotRequest;
472 SGSharedPtr<CanvasImageRequest> canvasimageRequest;
473 try {
474 SG_LOG(SG_NETWORK, SG_DEBUG, "new ScreenshotRequest("<<window<<","<<type<<"," << stream << "," << canvasindex <<")");
475 if (canvasindex == -1)
476 screenshotRequest = new ScreenshotRequest(window, type, stream);
477 else
478 canvasimageRequest = new CanvasImageRequest(window, type, canvasindex, stream);
479 }
480 catch (sg_format_exception & ex)
481 {
482 SG_LOG(SG_NETWORK, SG_INFO, ex.getFormattedMessage());
483 response.Header["Content-Type"] = "text/plain";
484 response.StatusCode = 410;
485 response.Content = ex.getFormattedMessage();
486 return true;
487 }
488 catch (sg_error & ex)
489 {
490 SG_LOG(SG_NETWORK, SG_INFO, ex.getFormattedMessage());
491 response.Header["Content-Type"] = "text/plain";
492 response.StatusCode = 500;
493 response.Content = ex.getFormattedMessage();
494 return true;
495 }
496
497 if (!stream) {
498 response.Header["Content-Type"] = string("image/").append(type);
499 response.Header["Content-Disposition"] = string("inline; filename=\"fgfs-screen.").append(type).append("\"");
500 } else {
501 response.Header["Content-Type"] = string("multipart/x-mixed-replace; boundary=" BOUNDARY);
502
503 }
504
505 if (canvasindex == -1)
506 connection->put(KEY_SCREENSHOT, screenshotRequest);
507 else
508 connection->put(KEY_CANVASIMAGE, canvasimageRequest);
509 return false; // call me again thru poll
510}
511
513{
514 SGSharedPtr<ConnectionData> data = connection->get(KEY_SCREENSHOT);
515 if (data) {
516 ScreenshotRequest * screenshotRequest = dynamic_cast<ScreenshotRequest*>(data.get());
517 if ( NULL == screenshotRequest) return true; // Should not happen, kill the connection
518
519 const string & screenshot = screenshotRequest->getScreenshot();
520 if (screenshot.empty()) {
521 SG_LOG(SG_NETWORK, SG_DEBUG, "No screenshot available.");
522 return false; // not ready yet, call again.
523 }
524
525 SG_LOG(SG_NETWORK, SG_DEBUG, "Screenshot is ready, size=" << screenshot.size());
526
527 if (screenshotRequest->isStream()) {
528 std::ostringstream ss;
529 ss << BOUNDARY << "\r\nContent-Type: image/";
530 ss << screenshotRequest->getType() << "\r\nContent-Length:";
531 ss << screenshot.size() << "\r\n\r\n";
532 connection->write(ss.str().c_str(), ss.str().length());
533 }
534
535 connection->write(screenshot.data(), screenshot.size());
536
537 if (screenshotRequest->isStream()) {
538 screenshotRequest->requestScreenshot();
539 // continue until user closes connection
540 return false;
541 }
542
543 // single screenshot, send terminating chunk
544 connection->remove(KEY_SCREENSHOT);
545 connection->write("", 0);
546 return true; // done.
547 } // Screenshot
548
549 // CanvasImage
550 data = connection->get(KEY_CANVASIMAGE);
551 CanvasImageRequest * canvasimageRequest = dynamic_cast<CanvasImageRequest*> (data.get());
552 if (NULL == canvasimageRequest) return true; // Should not happen, kill the connection
553
554 if (!canvasimageRequest->connected) {
555 SG_LOG(SG_NETWORK, SG_INFO, "CanvasImageRequest: not connected. Resubscribing");
556 canvasimageRequest->requestCanvasImage();
557 }
558
559 const string & canvasimage = canvasimageRequest->getCanvasImage();
560 if (canvasimage.empty()) {
561 SG_LOG(SG_NETWORK, SG_INFO, "No canvasimage available.");
562 return false; // not ready yet, call again.
563 }
564
565 SG_LOG(SG_NETWORK, SG_DEBUG, "CanvasImage is ready, size=" << canvasimage.size());
566
567 if (canvasimageRequest->isStream()) {
568 std::ostringstream ss;
569 ss << BOUNDARY << "\r\nContent-Type: image/";
570 ss << canvasimageRequest->getType() << "\r\nContent-Length:";
571 ss << canvasimage.size() << "\r\n\r\n";
572 connection->write(ss.str().c_str(), ss.str().length());
573 }
574 connection->write(canvasimage.data(), canvasimage.size());
575 if (canvasimageRequest->isStream()) {
576 canvasimageRequest->requestCanvasImage();
577 // continue until user closes connection
578 return false;
579 }
580
581 // single canvasimage, send terminating chunk
582 connection->remove(KEY_CANVASIMAGE);
583 connection->write("", 0);
584 return true; // done.
585}
586
587} // namespace http
588} // namespace flightgear
bool options(int, char **)
Definition JSBSim.cpp:568
#define BOUNDARY
virtual void stringReady(const string &s)
virtual void imageReady(osg::ref_ptr< osg::Image > rawImage)
CanvasImageRequest(const string &window, const string &type, int canvasindex, bool stream)
SGSharedPtr< ConnectionData > get(const std::string &key)
virtual void write(const char *data, size_t len)=0
void put(const std::string &key, SGSharedPtr< ConnectionData > value)
void remove(const std::string &key)
std::string get(const std::string &key) const
void addTask(ImageCompressionTask &task)
virtual void imageReady(osg::ref_ptr< osg::Image >)=0
Based on osgworks ScreenCapture.cpp.
virtual void operator()(osg::RenderInfo &renderInfo) const
void subscribe(ImageReadyListener *subscriber)
void unsubscribe(ImageReadyListener *subscriber)
ScreenshotRequest(const string &window, const string &type, bool stream)
virtual void stringReady(const string &s)
osg::Camera * findLastCamera(osgViewer::ViewerBase *viewer, const string &windowName)
virtual void imageReady(osg::ref_ptr< osg::Image > rawImage)
ScreenshotUriHandler(const std::string &uri="/screenshot/")
virtual bool poll(Connection *connection)
This method gets called from the httpd if the preceding handleRequest() or poll() method returned fal...
virtual bool handleGetRequest(const HTTPRequest &request, HTTPResponse &response, Connection *connection)
Convenience method for GET Requests, gets called by handleRequest if not overridden.
virtual void stringReady(const std::string &)=0
URIHandler(const std::string &uri)
FGGlobals * globals
Definition globals.cxx:142
static const string KEY_SCREENSHOT("ScreenshotUriHandler::ScreenshotRequest")
static const string KEY_CANVASIMAGE("ScreenshotUriHandler::CanvasImageRequest")
simgear::Singleton< ImageCompressor > ImageCompressorSingleton
FlightPlan.hxx - defines a full flight-plan object, including departure, cruise, arrival information ...
Definition Addon.cxx:53
static int atoi(const string &str)
Definition options.cxx:113
ImageCompressionTask & operator=(const ImageCompressionTask &other)
ImageCompressionTask(const ImageCompressionTask &other)