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
23 #include "ScreenshotUriHandler.hxx"
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
40 using std::string;
41 using std::vector;
42 using std::list;
43
44 namespace sc = simgear::canvas;
45
46 namespace flightgear {
47 namespace http {
48
49 ///////////////////////////////////////////////////////////////////////////
50
51 class ImageReadyListener {
52 public:
53 virtual void imageReady(osg::ref_ptr<osg::Image>) = 0;
~ImageReadyListener()54 virtual ~ImageReadyListener()
55 {
56 }
57 };
58
59 class StringReadyListener {
60 public:
61 virtual void stringReady(const std::string &) = 0;
~StringReadyListener()62 virtual ~StringReadyListener()
63 {
64 }
65 };
66
67 struct ImageCompressionTask {
68 StringReadyListener * stringReadyListener;
69 string format;
70 osg::ref_ptr<osg::Image> image;
71
ImageCompressionTaskflightgear::http::ImageCompressionTask72 ImageCompressionTask()
73 {
74 stringReadyListener = NULL;
75 }
76
ImageCompressionTaskflightgear::http::ImageCompressionTask77 ImageCompressionTask(const ImageCompressionTask & other)
78 {
79 stringReadyListener = other.stringReadyListener;
80 format = other.format;
81 image = other.image;
82 }
83
operator =flightgear::http::ImageCompressionTask84 ImageCompressionTask & operator =(const ImageCompressionTask & other)
85 {
86 stringReadyListener = other.stringReadyListener;
87 format = other.format;
88 image = other.image;
89 return *this;
90 }
91
92 };
93
94 class ImageCompressor: public OpenThreads::Thread {
95 public:
ImageCompressor()96 ImageCompressor()
97 {
98 }
99 virtual void run();
100 void addTask(ImageCompressionTask & task);
101 private:
102 typedef SGBlockingQueue<ImageCompressionTask> TaskList;
103 TaskList _tasks;
104 };
105
106 typedef simgear::Singleton<ImageCompressor> ImageCompressorSingleton;
107
run()108 void ImageCompressor::run()
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
addTask(ImageCompressionTask & task)137 void ImageCompressor::addTask(ImageCompressionTask & task)
138 {
139 _tasks.push(task);
140 }
141
142 /**
143 * Based on <a href="http://code.google.com/p/osgworks">osgworks</a> ScreenCapture.cpp
144 *
145 */
146 class ScreenshotCallback: public osg::Camera::DrawCallback {
147 public:
ScreenshotCallback()148 ScreenshotCallback()
149 : _min_delta_tick(1.0/8.0)
150 {
151 _previousFrameTick = osg::Timer::instance()->tick();
152 }
153
operator ()(osg::RenderInfo & renderInfo) const154 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
subscribe(ImageReadyListener * subscriber)187 void subscribe(ImageReadyListener * subscriber)
188 {
189 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
190 _subscribers.push_back(subscriber);
191 }
192
unsubscribe(ImageReadyListener * subscriber)193 void unsubscribe(ImageReadyListener * subscriber)
194 {
195 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
196 _subscribers.remove( subscriber );
197 }
198
199 private:
200 mutable list<ImageReadyListener*> _subscribers;
201 mutable OpenThreads::Mutex _lock;
202 mutable double _previousFrameTick;
203 double _min_delta_tick;
204 };
205
206 ///////////////////////////////////////////////////////////////////////////
207
208 class ScreenshotRequest: public ConnectionData, public ImageReadyListener, StringReadyListener {
209 public:
ScreenshotRequest(const string & window,const string & type,bool stream)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()->getViewer(), 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
230 requestScreenshot();
231 }
232
~ScreenshotRequest()233 virtual ~ScreenshotRequest()
234 {
235 _screenshotCallback->unsubscribe(this);
236 }
237
imageReady(osg::ref_ptr<osg::Image> rawImage)238 virtual void imageReady(osg::ref_ptr<osg::Image> rawImage)
239 {
240 // called from a rendering thread, not from the main loop
241 ImageCompressionTask task;
242 task.image = rawImage;
243 task.format = _type;
244 task.stringReadyListener = this;
245 ImageCompressorSingleton::instance()->addTask(task);
246 }
247
requestScreenshot()248 void requestScreenshot()
249 {
250 _screenshotCallback->subscribe(this);
251 }
252
253 mutable OpenThreads::Mutex _lock;
254
stringReady(const string & s)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
getScreenshot()262 string getScreenshot()
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
findLastCamera(osgViewer::ViewerBase * viewer,const string & windowName)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 (false == 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 (false == 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
isStream() const318 bool isStream() const
319 {
320 return _stream;
321 }
322
getType() const323 const string & getType() const
324 {
325 return _type;
326 }
327
328 private:
329 string _type;
330 bool _stream;
331 string _compressedData;
332 ScreenshotCallback * _screenshotCallback;
333 };
334
335 /**
336 */
337 class CanvasImageRequest : public ConnectionData, public simgear::canvas::CanvasImageReadyListener, StringReadyListener {
338 public:
339 ImageCompressionTask *currenttask=NULL;
340 sc::CanvasPtr canvas;
341 int connected = 0;
342
CanvasImageRequest(const string & window,const string & type,int canvasindex,bool stream)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);
349
350 CanvasMgr* canvas_mgr = static_cast<CanvasMgr*> (globals->get_subsystem("Canvas"));
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));
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 const char *canvasname = canvasnode->getStringValue("name");
364 if (canvasname) {
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.
~CanvasImageRequest()376 virtual ~CanvasImageRequest() {
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
imageReady(osg::ref_ptr<osg::Image> rawImage)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
390 ImageCompressionTask task;
391 currenttask = &task;
392 task.image = rawImage;
393 task.format = _type;
394 task.stringReadyListener = this;
395 ImageCompressorSingleton::instance()->addTask(task);
396 }
397
requestCanvasImage()398 void requestCanvasImage() {
399 connected = canvas->subscribe(this);
400 }
401
402 mutable OpenThreads::Mutex _lock;
403
stringReady(const string & s)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
getCanvasImage()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
isStream() const425 bool isStream() const {
426 return _stream;
427 }
428
getType() const429 const string & getType() const {
430 return _type;
431 }
432
433 private:
434 string _type;
435 bool _stream;
436 string _compressedData;
437 };
438
ScreenshotUriHandler(const char * uri)439 ScreenshotUriHandler::ScreenshotUriHandler(const char * uri)
440 : URIHandler(uri)
441 {
442 }
443
~ScreenshotUriHandler()444 ScreenshotUriHandler::~ScreenshotUriHandler()
445 {
446 ImageCompressorSingleton::instance()->cancel();
447 //ImageCompressorSingleton::instance()->join();
448 }
449
450 const static string KEY_SCREENSHOT("ScreenshotUriHandler::ScreenshotRequest");
451 const static string KEY_CANVASIMAGE("ScreenshotUriHandler::CanvasImageRequest");
452 #define BOUNDARY "--fgfs-screenshot-boundary"
453
handleGetRequest(const HTTPRequest & request,HTTPResponse & response,Connection * connection)454 bool 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 = (false == 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 (false == 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
poll(Connection * connection)512 bool ScreenshotUriHandler::poll(Connection * connection)
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
589