1 // ScreenshotUriHandler.cxx -- Provide screenshots via http
3 // Started by Curtis Olson, started June 2001.
4 // osg support written by James Turner
5 // Ported to new httpd infrastructure by Torsten Dreyer
7 // Copyright (C) 2014 Torsten Dreyer
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.
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.
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.
23 #include "ScreenshotUriHandler.hxx"
25 #include <osgDB/Registry>
26 #include <osgDB/ReaderWriter>
27 #include <osgUtil/SceneView>
28 #include <osgViewer/Viewer>
30 #include <simgear/threads/SGQueue.hxx>
31 #include <simgear/structure/Singleton.hxx>
32 #include <Main/globals.hxx>
33 #include <Viewer/renderer.hxx>
36 #include <boost/lexical_cast.hpp>
42 namespace flightgear {
45 ///////////////////////////////////////////////////////////////////////////
47 class ImageReadyListener {
49 virtual void imageReady(osg::ref_ptr<osg::Image>) = 0;
50 virtual ~ImageReadyListener()
55 class StringReadyListener {
57 virtual void stringReady(const std::string &) = 0;
58 virtual ~StringReadyListener()
63 struct ImageCompressionTask {
64 StringReadyListener * stringReadyListener;
66 osg::ref_ptr<osg::Image> image;
68 ImageCompressionTask()
70 stringReadyListener = NULL;
73 ImageCompressionTask(const ImageCompressionTask & other)
75 stringReadyListener = other.stringReadyListener;
76 format = other.format;
80 ImageCompressionTask & operator =(const ImageCompressionTask & other)
82 stringReadyListener = other.stringReadyListener;
83 format = other.format;
90 class ImageCompressor: public OpenThreads::Thread {
96 void addTask(ImageCompressionTask & task);
98 typedef SGBlockingQueue<ImageCompressionTask> TaskList;
102 typedef simgear::Singleton<ImageCompressor> ImageCompressorSingleton;
104 void ImageCompressor::run()
106 osg::ref_ptr<osgDB::ReaderWriter::Options> options = new osgDB::ReaderWriter::Options("JPEG_QUALITY 80 PNG_COMPRESSION 9");
108 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor is running");
110 ImageCompressionTask task = _tasks.pop();
111 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor has an image");
112 if ( NULL != task.stringReadyListener) {
113 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor checking for writer for " << task.format);
114 osgDB::ReaderWriter* writer = osgDB::Registry::instance()->getReaderWriterForExtension(task.format);
118 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor compressing to " << task.format);
119 std::stringstream outputStream;
120 osgDB::ReaderWriter::WriteResult wr;
121 wr = writer->writeImage(*task.image, outputStream, options);
124 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor compressed to " << task.format);
125 task.stringReadyListener->stringReady(outputStream.str());
127 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor done for this image" << task.format);
130 SG_LOG(SG_NETWORK, SG_DEBUG, "ImageCompressor exiting");
133 void ImageCompressor::addTask(ImageCompressionTask & task)
139 * Based on <a href="http://code.google.com/p/osgworks">osgworks</a> ScreenCapture.cpp
142 class ScreenshotCallback: public osg::Camera::DrawCallback {
145 : _min_delta_tick(1.0/8.0)
147 _previousFrameTick = osg::Timer::instance()->tick();
150 virtual void operator ()(osg::RenderInfo& renderInfo) const
152 osg::Timer_t n = osg::Timer::instance()->tick();
153 double dt = osg::Timer::instance()->delta_s(_previousFrameTick,n);
154 if (dt < _min_delta_tick)
156 _previousFrameTick = n;
158 bool hasSubscribers = false;
160 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
161 hasSubscribers = !_subscribers.empty();
164 if (hasSubscribers) {
165 osg::ref_ptr<osg::Image> image = new osg::Image;
166 const osg::Viewport* vp = renderInfo.getState()->getCurrentViewport();
167 image->readPixels(vp->x(), vp->y(), vp->width(), vp->height(), GL_RGB, GL_UNSIGNED_BYTE);
169 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
170 while (!_subscribers.empty()) {
172 _subscribers.back()->imageReady(image);
176 _subscribers.pop_back();
183 void subscribe(ImageReadyListener * subscriber)
185 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
186 _subscribers.push_back(subscriber);
189 void unsubscribe(ImageReadyListener * subscriber)
191 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
192 _subscribers.remove( subscriber );
196 mutable list<ImageReadyListener*> _subscribers;
197 mutable OpenThreads::Mutex _lock;
198 mutable double _previousFrameTick;
199 double _min_delta_tick;
202 ///////////////////////////////////////////////////////////////////////////
204 class ScreenshotRequest: public ConnectionData, public ImageReadyListener, StringReadyListener {
206 ScreenshotRequest(const string & window, const string & type, bool stream)
207 : _type(type), _stream(stream)
209 if ( NULL == osgDB::Registry::instance()->getReaderWriterForExtension(_type))
210 throw sg_format_exception("Unsupported image type: " + type, type);
212 osg::Camera * camera = findLastCamera(globals->get_renderer()->getViewer(), window);
214 throw sg_error("Can't find a camera for window '" + window + "'");
216 // add our ScreenshotCallback to the camera
217 if ( NULL == camera->getFinalDrawCallback()) {
218 //TODO: are we leaking the Callback on reinit?
219 camera->setFinalDrawCallback(new ScreenshotCallback());
222 _screenshotCallback = dynamic_cast<ScreenshotCallback*>(camera->getFinalDrawCallback());
223 if ( NULL == _screenshotCallback)
224 throw sg_error("Can't find ScreenshotCallback");
229 virtual ~ScreenshotRequest()
231 _screenshotCallback->unsubscribe(this);
234 virtual void imageReady(osg::ref_ptr<osg::Image> rawImage)
236 // called from a rendering thread, not from the main loop
237 ImageCompressionTask task;
238 task.image = rawImage;
240 task.stringReadyListener = this;
241 ImageCompressorSingleton::instance()->addTask(task);
244 void requestScreenshot()
246 _screenshotCallback->subscribe(this);
249 mutable OpenThreads::Mutex _lock;
251 virtual void stringReady(const string & s)
253 // called from the compressor thread
254 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
258 string getScreenshot()
262 // called from the main loop
263 OpenThreads::ScopedLock<OpenThreads::Mutex> lock(_lock);
264 reply = _compressedData;
265 _compressedData.clear();
270 osg::Camera* findLastCamera(osgViewer::ViewerBase * viewer, const string & windowName)
272 osgViewer::ViewerBase::Windows windows;
273 viewer->getWindows(windows);
275 osgViewer::GraphicsWindow* window = NULL;
277 if (false == windowName.empty()) {
278 for (osgViewer::ViewerBase::Windows::iterator itr = windows.begin(); itr != windows.end(); ++itr) {
279 if ((*itr)->getTraits()->windowName == windowName) {
286 if ( NULL == window) {
287 if (false == windowName.empty()) {
288 SG_LOG(SG_NETWORK, SG_INFO, "requested window " << windowName << " not found, using first window");
290 window = *windows.begin();
293 SG_LOG(SG_NETWORK, SG_DEBUG, "Looking for last Camera of window '" << window->getTraits()->windowName << "'");
295 osg::GraphicsContext::Cameras& cameras = window->getCameras();
296 osg::Camera* lastCamera = 0;
297 for (osg::GraphicsContext::Cameras::iterator cam_itr = cameras.begin(); cam_itr != cameras.end(); ++cam_itr) {
299 if ((*cam_itr)->getRenderOrder() > lastCamera->getRenderOrder()) {
300 lastCamera = (*cam_itr);
302 if ((*cam_itr)->getRenderOrder() == lastCamera->getRenderOrder()
303 && (*cam_itr)->getRenderOrderNum() >= lastCamera->getRenderOrderNum()) {
304 lastCamera = (*cam_itr);
307 lastCamera = *cam_itr;
314 bool isStream() const
319 const string & getType() const
327 string _compressedData;
328 ScreenshotCallback * _screenshotCallback;
331 ScreenshotUriHandler::ScreenshotUriHandler(const char * uri)
336 ScreenshotUriHandler::~ScreenshotUriHandler()
338 ImageCompressorSingleton::instance()->cancel();
339 //ImageCompressorSingleton::instance()->join();
342 const static string KEY("ScreenshotUriHandler::ScreenshotRequest");
343 #define BOUNDARY "--fgfs-screenshot-boundary"
345 bool ScreenshotUriHandler::handleGetRequest(const HTTPRequest & request, HTTPResponse & response, Connection * connection)
347 if (!ImageCompressorSingleton::instance()->isRunning())
348 ImageCompressorSingleton::instance()->start();
350 string type = request.RequestVariables.get("type");
351 if (type.empty()) type = "jpg";
353 // string camera = request.RequestVariables.get("camera");
354 string window = request.RequestVariables.get("window");
356 bool stream = (false == request.RequestVariables.get("stream").empty());
358 SGSharedPtr<ScreenshotRequest> screenshotRequest;
360 SG_LOG(SG_NETWORK, SG_DEBUG, "new ScreenshotRequest("<<window<<","<<type<<"," << stream << ")");
361 screenshotRequest = new ScreenshotRequest(window, type, stream);
363 catch (sg_format_exception & ex)
365 SG_LOG(SG_NETWORK, SG_INFO, ex.getFormattedMessage());
366 response.Header["Content-Type"] = "text/plain";
367 response.StatusCode = 410;
368 response.Content = ex.getFormattedMessage();
371 catch (sg_error & ex)
373 SG_LOG(SG_NETWORK, SG_INFO, ex.getFormattedMessage());
374 response.Header["Content-Type"] = "text/plain";
375 response.StatusCode = 500;
376 response.Content = ex.getFormattedMessage();
380 if (false == stream) {
381 response.Header["Content-Type"] = string("image/").append(type);
382 response.Header["Content-Disposition"] = string("inline; filename=\"fgfs-screen.").append(type).append("\"");
384 response.Header["Content-Type"] = string("multipart/x-mixed-replace; boundary=" BOUNDARY);
388 connection->put(KEY, screenshotRequest);
389 return false; // call me again thru poll
392 bool ScreenshotUriHandler::poll(Connection * connection)
395 SGSharedPtr<ConnectionData> data = connection->get(KEY);
396 ScreenshotRequest * screenshotRequest = dynamic_cast<ScreenshotRequest*>(data.get());
397 if ( NULL == screenshotRequest) return true; // Should not happen, kill the connection
399 const string & screenshot = screenshotRequest->getScreenshot();
400 if (screenshot.empty()) {
401 SG_LOG(SG_NETWORK, SG_DEBUG, "No screenshot available.");
402 return false; // not ready yet, call again.
405 SG_LOG(SG_NETWORK, SG_DEBUG, "Screenshot is ready, size=" << screenshot.size());
407 if (screenshotRequest->isStream()) {
408 string s( BOUNDARY "\r\nContent-Type: image/");
409 s.append(screenshotRequest->getType()).append("\r\nContent-Length:");
410 s += boost::lexical_cast<string>(screenshot.size());
412 connection->write(s.c_str(), s.length());
415 connection->write(screenshot.data(), screenshot.size());
417 if (screenshotRequest->isStream()) {
418 screenshotRequest->requestScreenshot();
419 // continue until user closes connection
423 // single screenshot, send terminating chunk
424 connection->remove(KEY);
425 connection->write("", 0);
426 return true; // done.
430 } // namespace flightgear