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