diff --git a/CMakeLists.txt b/CMakeLists.txt index ba91e70..b018c23 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,11 @@ -# Copyright (c) 2013-2016, EPFL/Blue Brain Project +# Copyright (c) 2013-2017, EPFL/Blue Brain Project # Raphael Dumusc # Daniel Nachbaur cmake_minimum_required(VERSION 3.1 FATAL_ERROR) -project(Deflect VERSION 0.12.1) -set(Deflect_VERSION_ABI 5) +project(Deflect VERSION 0.13.0) +set(Deflect_VERSION_ABI 6) list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/CMake ${CMAKE_SOURCE_DIR}/CMake/common) diff --git a/README.md b/README.md index 0e37538..399f01b 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,16 @@ Welcome to Deflect, a C++ library for streaming pixels to other Deflect-based applications, for example [Tide](https://github.com/BlueBrain/Tide). -Deflect offers a stable API marked with version 1.5 (for the client part). +Deflect offers a stable API marked with version 1.6 (for the client part). ## Features Deflect provides the following functionality: * Stream pixels to a remote Server from one or multiple sources -* Register for receiving events from the Server -* Receive keyboard, mouse and multi-point touch gestures from the Server +* Stream stereo images from a distributed 3D application +* Receive input events from the Server and send data to it +* Transmitted events include keyboard, mouse and multi-point touch gestures * Compressed or uncompressed streaming * Fast multi-threaded JPEG compression (using libjpeg-turbo) diff --git a/apps/SimpleStreamer/main.cpp b/apps/SimpleStreamer/main.cpp index ee32627..c50e9a1 100644 --- a/apps/SimpleStreamer/main.cpp +++ b/apps/SimpleStreamer/main.cpp @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2014-2015, EPFL/Blue Brain Project */ +/* Copyright (c) 2014-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* */ /* Copyright (c) 2011 - 2012, The University of Texas at Austin. */ @@ -57,33 +57,31 @@ bool deflectInteraction = false; bool deflectCompressImage = true; +bool deflectStereoStreamLeft = false; +bool deflectStereoStreamRight = false; unsigned int deflectCompressionQuality = 75; -char* deflectHost = NULL; +std::string deflectHost; std::string deflectStreamId = "SimpleStreamer"; -deflect::Stream* deflectStream = NULL; +std::unique_ptr deflectStream; -void syntax( char* app ); +void syntax( int exitStatus ); void readCommandLineArguments( int argc, char** argv ); void initGLWindow( int argc, char** argv ); void initDeflectStream(); void display(); void reshape( int width, int height ); - void cleanup() { - delete deflectStream; + deflectStream.reset(); } int main( int argc, char** argv ) { readCommandLineArguments( argc, argv ); - if( deflectHost == NULL ) - { - syntax( argv[0] ); - return EXIT_FAILURE; - } + if( deflectHost.empty( )) + syntax( EXIT_FAILURE ); initGLWindow( argc, argv ); initDeflectStream(); @@ -94,16 +92,29 @@ int main( int argc, char** argv ) return EXIT_SUCCESS; } +void syntax( const int exitStatus ) +{ + std::cout << "Usage: simplestreamer [options] " << std::endl; + std::cout << "Stream a GLUT teapot to a remote host\n" << std::endl; + std::cout << "Options:" << std::endl; + std::cout << " -h, --help display this help" << std::endl; + std::cout << " -n set stream identifier (default: 'SimpleStreamer')" << std::endl; + std::cout << " -i enable interaction events (default: OFF)" << std::endl; + std::cout << " -u enable uncompressed streaming (default: OFF)" << std::endl; + std::cout << " -s enable stereo streaming, equivalent to -l -r (default: OFF)" << std::endl; + std::cout << " -l enable stereo streaming, left image only (default: OFF)" << std::endl; + std::cout << " -r enable stereo streaming, right image only (default: OFF)" << std::endl; + exit( exitStatus ); +} + void readCommandLineArguments( int argc, char** argv ) { for( int i = 1; i < argc; ++i ) { if( std::string( argv[i] ) == "--help" ) - { - syntax( argv[0] ); - ::exit( EXIT_SUCCESS ); - } - else if( argv[i][0] == '-' ) + syntax( EXIT_SUCCESS ); + + if( argv[i][0] == '-' ) { switch( argv[i][1] ) { @@ -120,8 +131,20 @@ void readCommandLineArguments( int argc, char** argv ) case 'u': deflectCompressImage = false; break; + case 's': + deflectStereoStreamLeft = true; + deflectStereoStreamRight = true; + break; + case 'l': + deflectStereoStreamLeft = true; + break; + case 'r': + deflectStereoStreamRight = true; + break; + case 'h': + syntax( EXIT_SUCCESS ); default: - syntax( argv[0] ); + std::cerr << "Unknown command line option: " << argv[i] << std::endl; ::exit( EXIT_FAILURE ); } } @@ -147,92 +170,142 @@ void initGLWindow( int argc, char** argv ) // the reshape function will be called on window resize glutReshapeFunc( reshape ); - glClearColor( 0.5, 0.5, 0.5, 1.0 ); - glEnable( GL_DEPTH_TEST ); glEnable( GL_LIGHTING ); glEnable( GL_LIGHT0 ); } - void initDeflectStream() { - deflectStream = new deflect::Stream( deflectStreamId, deflectHost ); + deflectStream.reset( new deflect::Stream( deflectStreamId, deflectHost )); if( !deflectStream->isConnected( )) { std::cerr << "Could not connect to host!" << std::endl; - delete deflectStream; - exit( 1 ); + deflectStream.reset(); + exit( EXIT_FAILURE ); } if( deflectInteraction && !deflectStream->registerForEvents( )) { std::cerr << "Could not register for events!" << std::endl; - delete deflectStream; - exit( 1 ); + deflectStream.reset(); + exit( EXIT_FAILURE ); } } - -void syntax( char* app ) -{ - std::cout << "Usage: " << app << " [options] " << std::endl; - std::cout << "Stream a GLUT teapot to a remote host\n" << std::endl; - std::cout << "Options:" << std::endl; - std::cout << " -n set stream identifier (default: 'SimpleStreamer')" << std::endl; - std::cout << " -i enable interaction events (default: OFF)" << std::endl; - std::cout << " -u enable uncompressed streaming (default: OFF)" << std::endl; -} - -void display() +struct Camera { // angles of camera rotation and zoom factor - static float angleX = 0.f; - static float angleY = 0.f; - static float offsetX = 0.f; - static float offsetY = 0.f; - static float zoom = 1.f; + float angleX = 0.f; + float angleY = 0.f; + float offsetX = 0.f; + float offsetY = 0.f; + float zoom = 1.f; - // Render the teapot - glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); - glMatrixMode( GL_PROJECTION ); - glLoadIdentity(); + void apply() + { + glMatrixMode( GL_PROJECTION ); + glLoadIdentity(); - const float size = 2.f; - glOrtho( -size, size, -size, size, -size, size ); + const float size = 2.f; + glOrtho( -size, size, -size, size, -size, size ); - glMatrixMode( GL_MODELVIEW ); - glLoadIdentity(); + glMatrixMode( GL_MODELVIEW ); + glLoadIdentity(); - glTranslatef( offsetX, -offsetY, 0.f ); + glTranslatef( offsetX, -offsetY, 0.f ); - glRotatef( angleX, 0.f, 1.f, 0.f ); - glRotatef( angleY, -1.f, 0.f, 0.f ); + glRotatef( angleX, 0.f, 1.f, 0.f ); + glRotatef( angleY, -1.f, 0.f, 0.f ); - glScalef( zoom, zoom, zoom ); - glutSolidTeapot( 1.f ); + glScalef( zoom, zoom, zoom ); + } +}; - // Grab the frame from OpenGL - const int windowWidth = glutGet( GLUT_WINDOW_WIDTH ); - const int windowHeight = glutGet( GLUT_WINDOW_HEIGHT ); +struct Image +{ + size_t width = 0; + size_t height = 0; + std::vector data; - const size_t imageSize = windowWidth * windowHeight * 4; - unsigned char* imageData = new unsigned char[imageSize]; - glReadPixels( 0, 0, windowWidth, windowHeight, GL_RGBA, GL_UNSIGNED_BYTE, - (GLvoid*)imageData ); + static Image readGlBuffer() + { + Image image; + image.width = glutGet( GLUT_WINDOW_WIDTH ); + image.height = glutGet( GLUT_WINDOW_HEIGHT ); + image.data.resize( image.width * image.height * 4 ); + glReadPixels( 0, 0, image.width, image.height, GL_RGBA, + GL_UNSIGNED_BYTE, (GLvoid*)image.data.data( )); + + deflect::ImageWrapper::swapYAxis( image.data.data(), image.width, + image.height, 4 ); + return image; + } +}; - // Send the frame through the stream - deflect::ImageWrapper deflectImage( (const void*)imageData, windowWidth, - windowHeight, deflect::RGBA ); +bool send( const Image& image, const deflect::View view ) +{ + deflect::ImageWrapper deflectImage( image.data.data(), image.width, + image.height, deflect::RGBA ); deflectImage.compressionPolicy = deflectCompressImage ? deflect::COMPRESSION_ON : deflect::COMPRESSION_OFF; deflectImage.compressionQuality = deflectCompressionQuality; - deflect::ImageWrapper::swapYAxis( (void*)imageData, windowWidth, - windowHeight, 4 ); - const bool success = deflectStream->send( deflectImage ); - deflectStream->finishFrame(); + deflectImage.view = view; + return deflectStream->send( deflectImage ) && + deflectStream->finishFrame(); +} + +bool timeout( const float sec ) +{ + using clock = std::chrono::system_clock; + static clock::time_point start = clock::now(); + return std::chrono::duration{ clock::now() - start }.count() > sec; +} + +void display() +{ + static Camera camera; + camera.apply(); + + bool success = false; + bool waitToStart = false; + static bool deflectFirstEventReceived = false; + if( deflectStereoStreamLeft || deflectStereoStreamRight ) + { + // Poor man's attempt to synchronise the start of separate stereo + // streams (waiting on first event from server or 5 sec. timeout). + // This does not prevent applications from going quickly out of sync. + // Real-world applications need a dedicated synchronization channel to + // ensure corresponding left-right views are rendered and sent together. + if( !(deflectStereoStreamLeft && deflectStereoStreamRight )) + waitToStart = !(deflectFirstEventReceived || timeout( 5 )); + + if( deflectStereoStreamLeft && !waitToStart ) + { + glClearColor( 0.7, 0.3, 0.3, 1.0 ); + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + glutSolidTeapot( 1.f ); + const auto leftImage = Image::readGlBuffer(); + success = send( leftImage, deflect::View::left_eye ); + } + if( deflectStereoStreamRight && !waitToStart ) + { + glClearColor( 0.3, 0.7, 0.3, 1.0 ); + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + glutSolidTeapot( 1.f ); + const auto rightImage = Image::readGlBuffer(); + success = (!deflectStereoStreamLeft || success) && + send( rightImage, deflect::View::right_eye ); + } + } + else + { + glClearColor( 0.5, 0.5, 0.5, 1.0 ); + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + glutSolidTeapot( 1.f ); + success = send( Image::readGlBuffer(), deflect::View::mono ); + } - delete [] imageData; glutSwapBuffers(); // increment rotation angle according to interaction, or by a constant rate @@ -250,15 +323,17 @@ void display() { const deflect::Event& event = deflectStream->getEvent(); + deflectFirstEventReceived = true; + switch( event.type ) { case deflect::Event::EVT_CLOSE: std::cout << "Received close..." << std::endl; - exit( 0 ); + exit( EXIT_SUCCESS ); case deflect::Event::EVT_PINCH: - zoom += std::copysign( std::sqrt( event.dx * event.dx + - event.dy * event.dy ), - event.dx + event.dy ); + camera.zoom += std::copysign( std::sqrt( event.dx * event.dx + + event.dy * event.dy ), + event.dx + event.dy ); break; case deflect::Event::EVT_PRESS: mouseX = event.mouseX; @@ -268,26 +343,26 @@ void display() case deflect::Event::EVT_RELEASE: if( event.mouseLeft ) { - angleX += (event.mouseX - mouseX) * 360.f; - angleY += (event.mouseY - mouseY) * 360.f; + camera.angleX += (event.mouseX - mouseX) * 360.f; + camera.angleY += (event.mouseY - mouseY) * 360.f; } mouseX = event.mouseX; mouseY = event.mouseY; break; case deflect::Event::EVT_PAN: - offsetX += event.dx; - offsetY += event.dy; + camera.offsetX += event.dx; + camera.offsetY += event.dy; mouseX = event.mouseX; mouseY = event.mouseY; break; case deflect::Event::EVT_KEY_PRESS: if( event.key == ' ' ) { - angleX = 0.f; - angleY = 0.f; - offsetX = 0.f; - offsetY = 0.f; - zoom = 1.f; + camera.angleX = 0.f; + camera.angleY = 0.f; + camera.offsetX = 0.f; + camera.offsetY = 0.f; + camera.zoom = 1.f; } break; default: @@ -297,8 +372,11 @@ void display() } else { - angleX += 1.f; - angleY += 1.f; + if( !waitToStart ) + { + camera.angleX += 1.f; + camera.angleY += 1.f; + } } if( !success ) @@ -306,12 +384,12 @@ void display() if( !deflectStream->isConnected( )) { std::cout << "Stream closed, exiting." << std::endl; - exit( 0 ); + exit( EXIT_SUCCESS ); } - else + else if( !waitToStart ) { std::cerr << "failure in deflectStreamSend()" << std::endl; - exit( 1 ); + exit( EXIT_FAILURE ); } } } diff --git a/deflect/CMakeLists.txt b/deflect/CMakeLists.txt index b8661be..1760f8d 100644 --- a/deflect/CMakeLists.txt +++ b/deflect/CMakeLists.txt @@ -1,13 +1,12 @@ -# Copyright (c) 2013-2016, EPFL/Blue Brain Project -# Raphael Dumusc -# Daniel Nachbaur +# Copyright (c) 2013-2017, EPFL/Blue Brain Project +# Raphael Dumusc +# Daniel Nachbaur set(DEFLECT_PUBLIC_HEADERS config.h Event.h EventReceiver.h - FrameDispatcher.h Frame.h ImageWrapper.h MTQueue.h @@ -20,12 +19,14 @@ set(DEFLECT_PUBLIC_HEADERS ) set(DEFLECT_HEADERS + FrameDispatcher.h ImageSegmenter.h MessageHeader.h NetworkProtocol.h ReceiveBuffer.h ServerWorker.h Socket.h + SourceBuffer.h StreamPrivate.h ) @@ -41,6 +42,7 @@ set(DEFLECT_SOURCES Server.cpp ServerWorker.cpp Socket.cpp + SourceBuffer.cpp Stream.cpp StreamPrivate.cpp StreamSendWorker.cpp diff --git a/deflect/Frame.h b/deflect/Frame.h index f7fb3e9..9bbf2d8 100644 --- a/deflect/Frame.h +++ b/deflect/Frame.h @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2014-2016, EPFL/Blue Brain Project */ +/* Copyright (c) 2014-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -62,6 +62,9 @@ class Frame /** The PixelStream uri to which this frame is associated. */ QString uri; + /** The view to which this frame belongs. */ + View view = View::mono; + /** Get the total dimensions of this frame. */ DEFLECT_API QSize computeDimensions() const; }; diff --git a/deflect/FrameDispatcher.cpp b/deflect/FrameDispatcher.cpp index 1cc85d3..c979aff 100644 --- a/deflect/FrameDispatcher.cpp +++ b/deflect/FrameDispatcher.cpp @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2015, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -43,6 +43,7 @@ #include "ReceiveBuffer.h" #include +#include namespace deflect { @@ -52,26 +53,49 @@ class FrameDispatcher::Impl public: Impl() {} - FramePtr consumeLatestFrame( const QString& uri ) + FramePtr consumeLatestMonoFrame( const QString& uri ) { FramePtr frame( new Frame ); frame->uri = uri; + frame->view = View::mono; ReceiveBuffer& buffer = streamBuffers[uri]; - - while( buffer.hasCompleteFrame( )) - frame->segments = buffer.popFrame(); - + while( buffer.hasCompleteMonoFrame( )) + frame->segments = buffer.popFrame( View::mono ); assert( !frame->segments.empty( )); // receiver will request a new frame once this frame was consumed - buffer.setAllowedToSend( false ); - + buffer.setAllowedToSend( false, View::mono ); return frame; } - typedef std::map StreamBuffers; - StreamBuffers streamBuffers; + std::pair consumeLatestStereoFrame( const QString& uri ) + { + FramePtr frameLeft( new Frame ); + frameLeft->uri = uri; + frameLeft->view = View::left_eye; + + FramePtr frameRight( new Frame ); + frameRight->uri = uri; + frameRight->view = View::right_eye; + + ReceiveBuffer& buffer = streamBuffers[uri]; + + while( buffer.hasCompleteStereoFrame( )) + { + frameLeft->segments = buffer.popFrame( View::left_eye ); + frameRight->segments = buffer.popFrame( View::right_eye ); + } + assert( !frameLeft->segments.empty( )); + assert( !frameRight->segments.empty( )); + + // receiver will request a new frame once this frame was consumed + buffer.setAllowedToSend( false, View::left_eye ); + buffer.setAllowedToSend( false, View::right_eye ); + return std::make_pair( std::move( frameLeft ), std::move( frameRight )); + } + + std::map streamBuffers; }; FrameDispatcher::FrameDispatcher() @@ -85,7 +109,7 @@ void FrameDispatcher::addSource( const QString uri, const size_t sourceIndex ) _impl->streamBuffers[uri].addSource( sourceIndex ); if( _impl->streamBuffers[uri].getSourceCount() == 1 ) - emit openPixelStream( uri ); + emit pixelStreamOpened( uri ); } void FrameDispatcher::removeSource( const QString uri, @@ -102,10 +126,11 @@ void FrameDispatcher::removeSource( const QString uri, void FrameDispatcher::processSegment( const QString uri, const size_t sourceIndex, - deflect::Segment segment ) + deflect::Segment segment, + const deflect::View view ) { if( _impl->streamBuffers.count( uri )) - _impl->streamBuffers[uri].insert( segment, sourceIndex ); + _impl->streamBuffers[uri].insert( segment, sourceIndex, view ); } void FrameDispatcher::processFrameFinished( const QString uri, @@ -115,18 +140,26 @@ void FrameDispatcher::processFrameFinished( const QString uri, return; ReceiveBuffer& buffer = _impl->streamBuffers[uri]; - buffer.finishFrameForSource( sourceIndex ); + try + { + buffer.finishFrameForSource( sourceIndex ); + } + catch( const std::runtime_error& ) + { + emit bufferSizeExceeded( uri ); + return; + } - if( buffer.isAllowedToSend() && buffer.hasCompleteFrame( )) - emit sendFrame( _impl->consumeLatestFrame( uri )); -} + if( buffer.isAllowedToSend( View::mono ) && buffer.hasCompleteMonoFrame( )) + emit sendFrame( _impl->consumeLatestMonoFrame( uri )); -void FrameDispatcher::deleteStream( const QString uri ) -{ - if( _impl->streamBuffers.count( uri )) + if( buffer.isAllowedToSend( View::left_eye ) && + buffer.isAllowedToSend( View::right_eye ) && + buffer.hasCompleteStereoFrame( )) { - _impl->streamBuffers.erase( uri ); - emit deletePixelStream( uri ); + const auto frames = _impl->consumeLatestStereoFrame( uri ); + emit sendFrame( frames.first ); + emit sendFrame( frames.second ); } } @@ -136,9 +169,28 @@ void FrameDispatcher::requestFrame( const QString uri ) return; ReceiveBuffer& buffer = _impl->streamBuffers[uri]; - buffer.setAllowedToSend( true ); - if( buffer.hasCompleteFrame( )) - emit sendFrame( _impl->consumeLatestFrame( uri )); + buffer.setAllowedToSend( true, View::mono ); + buffer.setAllowedToSend( true, View::left_eye ); + buffer.setAllowedToSend( true, View::right_eye ); + + if( buffer.hasCompleteMonoFrame( )) + emit sendFrame( _impl->consumeLatestMonoFrame( uri )); + + if( buffer.hasCompleteStereoFrame( )) + { + const auto frames = _impl->consumeLatestStereoFrame( uri ); + emit sendFrame( frames.first ); + emit sendFrame( frames.second ); + } +} + +void FrameDispatcher::deleteStream( const QString uri ) +{ + if( _impl->streamBuffers.count( uri )) + { + _impl->streamBuffers.erase( uri ); + emit pixelStreamClosed( uri ); + } } } diff --git a/deflect/FrameDispatcher.h b/deflect/FrameDispatcher.h index e1c53cc..36c69d6 100644 --- a/deflect/FrameDispatcher.h +++ b/deflect/FrameDispatcher.h @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2015, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -45,7 +45,6 @@ #include #include -#include namespace deflect { @@ -59,82 +58,95 @@ class FrameDispatcher : public QObject public: /** Construct a dispatcher */ - DEFLECT_API FrameDispatcher(); + FrameDispatcher(); /** Destructor. */ - DEFLECT_API ~FrameDispatcher(); + ~FrameDispatcher(); public slots: /** * Add a source of Segments for a Stream. * - * @param uri Identifier for the Stream + * @param uri Identifier for the stream * @param sourceIndex Identifier for the source in this stream */ - DEFLECT_API void addSource( QString uri, size_t sourceIndex ); + void addSource( QString uri, size_t sourceIndex ); /** - * Add a source of Segments for a Stream. + * Remove a source of Segments for a Stream. * - * @param uri Identifier for the Stream + * @param uri Identifier for the stream * @param sourceIndex Identifier for the source in this stream */ - DEFLECT_API void removeSource( QString uri, size_t sourceIndex ); + void removeSource( QString uri, size_t sourceIndex ); /** - * Process a new Segement. + * Process a new Segment. * - * @param uri Identifier for the Stream - * @param sourceIndex Identifier for the source in this stream - * @param segment The segment to process + * @param uri Identifier for the stream + * @param sourceIndex Identifier for the source in the stream + * @param segment to process + * @param view to which the segment belongs */ - DEFLECT_API void processSegment( QString uri, size_t sourceIndex, - deflect::Segment segment ); + void processSegment( QString uri, size_t sourceIndex, + deflect::Segment segment, deflect::View view ); /** * The given source has finished sending segments for the current frame. * - * @param uri Identifier for the Stream - * @param sourceIndex Identifier for the source in this stream + * @param uri Identifier for the stream + * @param sourceIndex Identifier for the source in the stream */ - DEFLECT_API void processFrameFinished( QString uri, size_t sourceIndex ); + void processFrameFinished( QString uri, size_t sourceIndex ); /** - * Delete an entire stream. + * Request the dispatching of a new frame for any stream (mono/stereo). + * + * A sendFrame() signal will be emitted for each of the view for which a + * frame becomes available. + * + * Stereo left/right frames will only be be dispatched together when both + * are available to ensure that the two eye channels remain synchronized. * - * @param uri Identifier for the Stream + * @param uri Identifier for the stream */ - DEFLECT_API void deleteStream( QString uri ); + void requestFrame( QString uri ); /** - * Called by the user to request the dispatching of a new frame. + * Delete all the buffers for a Stream. * - * A sendFrame() signal will be emitted as soon as a frame is available. * @param uri Identifier for the stream */ - DEFLECT_API void requestFrame( QString uri ); + void deleteStream( QString uri ); signals: /** * Notify that a PixelStream has been opened. * - * @param uri Identifier for the Stream + * @param uri Identifier for the stream */ - DEFLECT_API void openPixelStream( QString uri ); + void pixelStreamOpened( QString uri ); /** - * Notify that a pixel stream has been deleted. + * Notify that a pixel stream has been closed. * - * @param uri Identifier for the Stream + * @param uri Identifier for the stream */ - DEFLECT_API void deletePixelStream( QString uri ); + void pixelStreamClosed( QString uri ); /** * Dispatch a full frame. * - * @param frame The frame to dispatch + * @param frame The latest frame available for a stream + */ + void sendFrame( deflect::FramePtr frame ); + + /** + * Notify that a pixel stream has exceeded its maximum allowed size. + * + * @param uri Identifier for the stream */ - DEFLECT_API void sendFrame( deflect::FramePtr frame ); + void bufferSizeExceeded( QString uri ); private: class Impl; diff --git a/deflect/ImageWrapper.h b/deflect/ImageWrapper.h index 0bdcdd6..f2a21fd 100644 --- a/deflect/ImageWrapper.h +++ b/deflect/ImageWrapper.h @@ -1,6 +1,6 @@ /*********************************************************************/ -/* Copyright (c) 2013, EPFL/Blue Brain Project */ -/* Raphael Dumusc */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ /* All rights reserved. */ /* */ /* Redistribution and use in source and binary forms, with or */ @@ -42,6 +42,7 @@ #include #include +#include namespace deflect { @@ -86,9 +87,8 @@ struct ImageWrapper * @version 1.0 */ DEFLECT_API - ImageWrapper( const void* data, const unsigned int width, - const unsigned int height, const PixelFormat format, - const unsigned int x = 0, const unsigned int y = 0 ); + ImageWrapper( const void* data, unsigned int width, unsigned int height, + PixelFormat format, unsigned int x = 0, unsigned int y = 0 ); /** Pointer to the image data of size getBufferSize(). @version 1.0 */ const void* const data; @@ -120,6 +120,12 @@ struct ImageWrapper @version 1.0 */ //@} + /** + * The view that this image represents. + * @version 1.6 + */ + View view = View::mono; + /** * Get the number of bytes per pixel based on the pixelFormat. * @version 1.0 diff --git a/deflect/MessageHeader.h b/deflect/MessageHeader.h index e6aab26..e3153ea 100644 --- a/deflect/MessageHeader.h +++ b/deflect/MessageHeader.h @@ -67,7 +67,8 @@ enum MessageType MESSAGE_TYPE_EVENT = 9, MESSAGE_TYPE_QUIT = 12, MESSAGE_TYPE_SIZE_HINTS = 13, - MESSAGE_TYPE_DATA = 14 + MESSAGE_TYPE_DATA = 14, + MESSAGE_TYPE_IMAGE_VIEW = 15 }; #define MESSAGE_HEADER_URI_LENGTH 64 diff --git a/deflect/MetaTypeRegistration.cpp b/deflect/MetaTypeRegistration.cpp index bae65bd..b31e120 100644 --- a/deflect/MetaTypeRegistration.cpp +++ b/deflect/MetaTypeRegistration.cpp @@ -59,6 +59,7 @@ struct MetaTypeRegistration qRegisterMetaType< deflect::SizeHints >( "deflect::SizeHints" ); qRegisterMetaType< deflect::Event >( "deflect::Event" ); qRegisterMetaType< deflect::FramePtr >( "deflect::FramePtr" ); + qRegisterMetaType< deflect::View >( "deflect::View" ); } }; diff --git a/deflect/ReceiveBuffer.cpp b/deflect/ReceiveBuffer.cpp index d8934dc..fcb8d62 100644 --- a/deflect/ReceiveBuffer.cpp +++ b/deflect/ReceiveBuffer.cpp @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2015, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -40,27 +40,29 @@ #include "ReceiveBuffer.h" #include +#include -namespace deflect +namespace { +const size_t MAX_QUEUE_SIZE = 150; // stream blocked for ~5 seconds at 30Hz +const auto views = std::array{{ deflect::View::mono, + deflect::View::left_eye, + deflect::View::right_eye }}; +} -ReceiveBuffer::ReceiveBuffer() - : _lastFrameComplete( 0 ) - , _allowedToSend( false ) +namespace deflect { -} bool ReceiveBuffer::addSource( const size_t sourceIndex ) { assert( !_sourceBuffers.count( sourceIndex )); // TODO: This function must return false if the stream was already started! - // This requires an full adaptation of the Stream library (DISCL-241) - if ( _sourceBuffers.count( sourceIndex )) + // This requires a full adaptation of the Stream library (DISCL-241) + if( _sourceBuffers.count( sourceIndex )) return false; _sourceBuffers[sourceIndex] = SourceBuffer(); - _sourceBuffers[sourceIndex].segments.push( Segments( )); return true; } @@ -74,56 +76,117 @@ size_t ReceiveBuffer::getSourceCount() const return _sourceBuffers.size(); } -void ReceiveBuffer::insert( const Segment& segment, const size_t sourceIndex ) +void ReceiveBuffer::insert( const Segment& segment, const size_t sourceIndex, + const View view ) { assert( _sourceBuffers.count( sourceIndex )); - _sourceBuffers[sourceIndex].segments.back().push_back( segment ); + _sourceBuffers[sourceIndex].insert( segment, view ); } void ReceiveBuffer::finishFrameForSource( const size_t sourceIndex ) { assert( _sourceBuffers.count( sourceIndex )); - _sourceBuffers[sourceIndex].push(); + auto& buffer = _sourceBuffers[sourceIndex]; + + for( const auto view : views ) + { + if( buffer.isBackFrameEmpty( view )) + continue; + + if( buffer.getQueueSize( view ) > MAX_QUEUE_SIZE ) + throw std::runtime_error( "maximum queue size exceeded" ); + + buffer.push( view ); + } } -bool ReceiveBuffer::hasCompleteFrame() const +bool ReceiveBuffer::hasCompleteMonoFrame() const { assert( !_sourceBuffers.empty( )); // Check if all sources for Stream have reached the same index + const auto lastCompleteFrame = _getLastCompleteFrameIndex( View::mono ); for( const auto& kv : _sourceBuffers ) { const auto& buffer = kv.second; - if( buffer.backFrameIndex <= _lastFrameComplete ) + if( buffer.getBackFrameIndex( View::mono ) <= lastCompleteFrame ) return false; } return true; } -Segments ReceiveBuffer::popFrame() +bool ReceiveBuffer::hasCompleteStereoFrame() const +{ + std::set leftSources; + std::set rightSources; + + const auto lastFrameLeft = _getLastCompleteFrameIndex( View::left_eye ); + const auto lastFrameRight = _getLastCompleteFrameIndex( View::right_eye ); + + for( const auto& kv : _sourceBuffers ) + { + const auto& buffer = kv.second; + if( buffer.getBackFrameIndex( View::left_eye ) > lastFrameLeft ) + leftSources.insert( kv.first ); + if( buffer.getBackFrameIndex( View::right_eye ) > lastFrameRight ) + rightSources.insert( kv.first ); + } + + if( leftSources.empty() || rightSources.empty( )) + return false; + + std::set leftAndRight; + std::set_intersection( leftSources.begin(), leftSources.end(), + rightSources.begin(), rightSources.end(), + std::inserter( leftAndRight, leftAndRight.end( ))); + + // if at least one source sends both left AND right, assume all sources do. + if( !leftAndRight.empty( )) + return leftAndRight.size() == _sourceBuffers.size(); + + // otherwise, assume all streams send either left OR right. + return rightSources.size() + leftSources.size() == _sourceBuffers.size(); +} + +Segments ReceiveBuffer::popFrame( const View view ) { + const auto lastCompleteFrameIndex = _getLastCompleteFrameIndex( view ); + Segments frame; for( auto& kv : _sourceBuffers ) { auto& buffer = kv.second; - frame.insert( frame.end(), buffer.segments.front().begin(), - buffer.segments.front().end( )); - buffer.pop(); + if( buffer.getBackFrameIndex( view ) > lastCompleteFrameIndex ) + { + const auto& segments = buffer.getSegments( view ); + frame.insert( frame.end(), segments.begin(), segments.end( )); + buffer.pop( view ); + } } - ++_lastFrameComplete; + _incrementLastFrameComplete( view ); return frame; } -void ReceiveBuffer::setAllowedToSend( const bool enable ) +void ReceiveBuffer::setAllowedToSend( const bool enable, const View view ) +{ + _allowedToSend[as_underlying_type(view)] = enable; +} + +bool ReceiveBuffer::isAllowedToSend( const View view ) const +{ + return _allowedToSend[as_underlying_type(view)]; +} + +FrameIndex ReceiveBuffer::_getLastCompleteFrameIndex( const View view ) const { - _allowedToSend = enable; + return _lastFrameComplete[as_underlying_type(view)]; } -bool ReceiveBuffer::isAllowedToSend() const +void ReceiveBuffer::_incrementLastFrameComplete( const View view ) { - return _allowedToSend; + ++_lastFrameComplete[as_underlying_type(view)]; } } diff --git a/deflect/ReceiveBuffer.h b/deflect/ReceiveBuffer.h index 8244f70..e0bee78 100644 --- a/deflect/ReceiveBuffer.h +++ b/deflect/ReceiveBuffer.h @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2015, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -42,48 +42,17 @@ #include #include +#include #include #include -#include +#include #include namespace deflect { -typedef unsigned int FrameIndex; - -/** - * Buffer for a single source of segements. - */ -struct SourceBuffer -{ - SourceBuffer() : frontFrameIndex( 0 ), backFrameIndex( 0 ) {} - - /** The current indexes of the frame for this source */ - FrameIndex frontFrameIndex, backFrameIndex; - - /** The collection of segments */ - std::queue segments; - - /** Pop the first element of the buffer */ - void pop() - { - segments.pop(); - ++frontFrameIndex; - } - - /** Push a new element to the back of the buffer */ - void push() - { - segments.push( Segments( )); - ++backFrameIndex; - } -}; - -typedef std::map SourceBufferMap; - /** * Buffer Segments from (multiple) sources. * @@ -93,9 +62,6 @@ typedef std::map SourceBufferMap; class ReceiveBuffer { public: - /** Construct a Buffer */ - DEFLECT_API ReceiveBuffer(); - /** * Add a source of segments. * @param sourceIndex Unique source identifier @@ -117,34 +83,47 @@ class ReceiveBuffer * Insert a segement for the current frame and source. * @param segment The segment to insert * @param sourceIndex Unique source identifier + * @param view in which the segment should be inserted */ - DEFLECT_API void insert( const Segment& segment, size_t sourceIndex ); + DEFLECT_API void insert( const Segment& segment, size_t sourceIndex, + View view = View::mono ); /** * Call when the source has finished sending segments for the current frame. * @param sourceIndex Unique source identifier + * @throw std::runtime_error if the buffer exceeds its maximum size */ DEFLECT_API void finishFrameForSource( size_t sourceIndex ); - /** Does the Buffer have a new complete frame (from all sources) */ - DEFLECT_API bool hasCompleteFrame() const; + /** Does the Buffer have a new complete mono frame (from all sources) */ + DEFLECT_API bool hasCompleteMonoFrame() const; + + /** Does the Buffer have a new complete stereo frame (from all sources) */ + DEFLECT_API bool hasCompleteStereoFrame() const; /** * Get the finished frame. * @return A collection of segments that form a frame */ - DEFLECT_API Segments popFrame(); + DEFLECT_API Segments popFrame( View view = View::mono ); /** Allow this buffer to be used by the next FrameDispatcher::sendLatestFrame */ - DEFLECT_API void setAllowedToSend( bool enable ); + DEFLECT_API void setAllowedToSend( bool enable, View view ); /** @return true if this buffer can be sent by FrameDispatcher */ - DEFLECT_API bool isAllowedToSend() const; + DEFLECT_API bool isAllowedToSend( View view ) const; private: - FrameIndex _lastFrameComplete; - SourceBufferMap _sourceBuffers; - bool _allowedToSend; + std::map _sourceBuffers; + + /** The current indices of the mono/left/right frame for this source. */ + std::array _lastFrameComplete = { { 0u, 0u, 0u } }; + + /** Is the mono/left/right channel allowed to send. */ + std::array _allowedToSend = { { false, false, false } }; + + FrameIndex _getLastCompleteFrameIndex( View view ) const; + void _incrementLastFrameComplete( View view ); }; } diff --git a/deflect/SegmentParameters.h b/deflect/SegmentParameters.h index e8d7ec6..9fa9255 100644 --- a/deflect/SegmentParameters.h +++ b/deflect/SegmentParameters.h @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2016, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -46,6 +46,8 @@ #include #endif +#include + namespace deflect { @@ -56,27 +58,18 @@ struct SegmentParameters { /** @name Coordinates */ //@{ - uint32_t x; /**< The x position in pixels. */ - uint32_t y; /**< The y position in pixels. */ + uint32_t x = 0u; /**< The x position in pixels. */ + uint32_t y = 0u; /**< The y position in pixels. */ //@} /** @name Dimensions */ //@{ - uint32_t width; /**< The width in pixels. */ - uint32_t height; /**< The height in pixels. */ + uint32_t width = 0u; /**< The width in pixels. */ + uint32_t height = 0u; /**< The height in pixels. */ //@} /** Is the image raw pixel data or compressed in jpeg format */ - bool compressed; - - /** Default constructor */ - SegmentParameters() - : x( 0 ) - , y( 0 ) - , width( 0 ) - , height( 0 ) - , compressed( true ) - {} + bool compressed = true; }; } diff --git a/deflect/Server.cpp b/deflect/Server.cpp index 6dca945..6cb7a48 100644 --- a/deflect/Server.cpp +++ b/deflect/Server.cpp @@ -54,7 +54,7 @@ const int Server::defaultPortNumber = DEFAULT_PORT_NUMBER; class Server::Impl { public: - FrameDispatcher pixelStreamDispatcher; + FrameDispatcher frameDispatcher; }; Server::Server( const int port ) @@ -65,6 +65,16 @@ Server::Server( const int port ) const auto err = QString( "could not listen on port: %1" ).arg( port ); throw std::runtime_error( err.toStdString( )); } + + // Forward FrameDispatcher signals + connect( &_impl->frameDispatcher, &FrameDispatcher::pixelStreamOpened, + this, &Server::pixelStreamOpened ); + connect( &_impl->frameDispatcher, &FrameDispatcher::pixelStreamClosed, + this, &Server::pixelStreamClosed ); + connect( &_impl->frameDispatcher, &FrameDispatcher::sendFrame, + this, &Server::receivedFrame ); + connect( &_impl->frameDispatcher, &FrameDispatcher::bufferSizeExceeded, + this, &Server::closePixelStream ); } Server::~Server() @@ -77,21 +87,20 @@ Server::~Server() workerThread->wait(); } } - - delete _impl; } -FrameDispatcher& Server::getPixelStreamDispatcher() +void Server::requestFrame( const QString uri ) { - return _impl->pixelStreamDispatcher; + _impl->frameDispatcher.requestFrame( uri ); } -void Server::onPixelStreamerClosed( const QString uri ) +void Server::closePixelStream( const QString uri ) { - emit _pixelStreamerClosed( uri ); + emit _closePixelStream( uri ); + _impl->frameDispatcher.deleteStream( uri ); } -void Server::onEventRegistrationReply( const QString uri, const bool success ) +void Server::replyToEventRegistration( const QString uri, const bool success ) { emit _eventRegistrationReply( uri, success ); } @@ -121,26 +130,20 @@ void Server::incomingConnection( const qintptr socketHandle ) this, &Server::receivedSizeHints ); connect( worker, &ServerWorker::receivedData, this, &Server::receivedData ); - connect( this, &Server::_pixelStreamerClosed, + connect( this, &Server::_closePixelStream, worker, &ServerWorker::closeConnection ); connect( this, &Server::_eventRegistrationReply, worker, &ServerWorker::replyToEventRegistration ); - // PixelStreamDispatcher + // FrameDispatcher connect( worker, &ServerWorker::addStreamSource, - &_impl->pixelStreamDispatcher, &FrameDispatcher::addSource ); - connect( worker, - &ServerWorker::receivedSegment, - &_impl->pixelStreamDispatcher, - &FrameDispatcher::processSegment ); - connect( worker, - &ServerWorker::receivedFrameFinished, - &_impl->pixelStreamDispatcher, - &FrameDispatcher::processFrameFinished ); - connect( worker, - &ServerWorker::removeStreamSource, - &_impl->pixelStreamDispatcher, - &FrameDispatcher::removeSource ); + &_impl->frameDispatcher, &FrameDispatcher::addSource ); + connect( worker, &ServerWorker::receivedSegment, + &_impl->frameDispatcher, &FrameDispatcher::processSegment ); + connect( worker, &ServerWorker::receivedFrameFinished, + &_impl->frameDispatcher, &FrameDispatcher::processFrameFinished ); + connect( worker, &ServerWorker::removeStreamSource, + &_impl->frameDispatcher, &FrameDispatcher::removeSource ); workerThread->start(); } diff --git a/deflect/Server.h b/deflect/Server.h index 6b901bc..2f596ea 100644 --- a/deflect/Server.h +++ b/deflect/Server.h @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2016, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* Daniel.Nachbaur@epfl.ch */ /* All rights reserved. */ @@ -51,7 +51,12 @@ namespace deflect { /** - * Listen to incoming PixelStream connections from Stream clients. + * Listen to incoming connections from multiple Stream clients. + * + * Both mono and frame-sequential stereo 3D streams are supported. + * + * The server integrates a flow-control mechanism to ensure that new frames are + * dispatched only as fast as the application is capable of processing them. */ class DEFLECT_API Server : public QTcpServer { @@ -63,38 +68,104 @@ class DEFLECT_API Server : public QTcpServer /** * Create a new server listening for Stream connections. + * * @param port The port to listen on. Must be available. * @throw std::runtime_error if the server could not be started. */ explicit Server( int port = defaultPortNumber ); - /** Destructor */ + /** Stop the server and close all open pixel stream connections. */ ~Server(); - /** Get the PixelStreamDispatcher. */ - FrameDispatcher& getPixelStreamDispatcher(); +public slots: + /** + * Request the dispatching of the next frame for a given pixel stream. + * + * A receivedFrame() signal will subsequently be emitted for each of the + * view(s) (mono or stereo) for which a frame is or becomes available. + * + * To ensure that the two eye channels remain synchronized, stereo + * left/right frames are dispatched together only when both are available. + * + * @param uri Identifier for the stream + */ + void requestFrame( QString uri ); + + /** + * Reply to an event registration request after a registerToEvents() signal. + * + * @param uri Identifier for the stream + * @param success Result of the registration operation + */ + void replyToEventRegistration( QString uri, bool success ); + + /** + * Close a pixel stream, disconnecting the remote client. + * + * @param uri Identifier for the stream + */ + void closePixelStream( QString uri ); signals: + /** + * Notify that a pixel stream has been opened. + * + * @param uri Identifier for the stream + */ + void pixelStreamOpened( QString uri ); + + /** + * Notify that a pixel stream has been closed. + * + * @param uri Identifier for the stream + */ + void pixelStreamClosed( QString uri ); + + /** + * Emitted when a full frame has been received from a pixel stream. + * + * This signal is only emitted after the application signals that it is + * ready to handle a new frame by calling requestFrame(). + * + * @param frame The latest frame that was received for a stream. + */ + void receivedFrame( deflect::FramePtr frame ); + + /** + * Emitted when a remote client wants to register for receiving events. + * + * @param uri Identifier for the stream + * @param exclusive true if the receiver should receive events exclusively + * @param receiver the event receiver instance + */ void registerToEvents( QString uri, bool exclusive, deflect::EventReceiver* receiver ); + /** + * Emitted when a remote client sends size hints for displaying the stream. + * + * @param uri Identifier for the stream + * @param hints The size hints to apply + */ void receivedSizeHints( QString uri, deflect::SizeHints hints ); + /** + * Emitted when a remote client sends generic data. + * + * @param uri Identifier for the stream + * @param data A streamer-specific message + */ void receivedData( QString uri, QByteArray data ); -public slots: - void onPixelStreamerClosed( QString uri ); - void onEventRegistrationReply( QString uri, bool success ); - private: class Impl; - Impl* _impl; + std::unique_ptr _impl; /** Re-implemented handling of connections from QTCPSocket. */ void incomingConnection( qintptr socketHandle ) final; signals: - void _pixelStreamerClosed( QString uri ); + void _closePixelStream( QString uri ); void _eventRegistrationReply( QString uri, bool success ); }; diff --git a/deflect/ServerWorker.cpp b/deflect/ServerWorker.cpp index fd759d4..a7c86e4 100644 --- a/deflect/ServerWorker.cpp +++ b/deflect/ServerWorker.cpp @@ -61,6 +61,7 @@ ServerWorker::ServerWorker( const int socketDescriptor ) , _sourceId( socketDescriptor ) , _clientProtocolVersion( NETWORK_PROTOCOL_VERSION ) , _registeredToEvents( false ) + , _activeView( View::mono ) { if( !_tcpSocket->setSocketDescriptor( socketDescriptor )) { @@ -227,7 +228,7 @@ void ServerWorker::_handleMessage( const MessageHeader& messageHeader, return; } _streamId = uri; - // The version is only sent by deflect clients since v. 0.13.0 + // The version is only sent by deflect clients since v. 0.12.1 if( !byteArray.isEmpty( )) _parseClientProtocolVersion( byteArray ); emit addStreamSource( _streamId, _sourceId ); @@ -253,6 +254,14 @@ void ServerWorker::_handleMessage( const MessageHeader& messageHeader, emit receivedData( _streamId, byteArray ); break; + case MESSAGE_TYPE_IMAGE_VIEW: + { + const auto view = reinterpret_cast( byteArray.data( )); + if( *view >= View::mono && *view <= View::right_eye ) + _activeView = *view; + break; + } + case MESSAGE_TYPE_BIND_EVENTS: case MESSAGE_TYPE_BIND_EVENTS_EX: if( _registeredToEvents ) @@ -287,7 +296,7 @@ void ServerWorker::_handlePixelStreamMessage( const QByteArray& message ) segment.imageData = message.right( message.size() - sizeof( SegmentParameters )); - emit( receivedSegment( _streamId, _sourceId, segment )); + emit( receivedSegment( _streamId, _sourceId, segment, _activeView )); } void ServerWorker::_sendProtocolVersion() diff --git a/deflect/ServerWorker.h b/deflect/ServerWorker.h index 4717cbe..809e8ca 100644 --- a/deflect/ServerWorker.h +++ b/deflect/ServerWorker.h @@ -73,11 +73,11 @@ public slots: void removeStreamSource( QString uri, size_t sourceIndex ); void receivedSegment( QString uri, size_t sourceIndex, - deflect::Segment segment ); + deflect::Segment segment, deflect::View view ); void receivedFrameFinished( QString uri, size_t sourceIndex ); void registerToEvents( QString uri, bool exclusive, - deflect::EventReceiver* receiver); + deflect::EventReceiver* receiver ); void receivedSizeHints( QString uri, deflect::SizeHints hints ); @@ -101,6 +101,8 @@ private slots: bool _registeredToEvents; QQueue _events; + View _activeView; + void _receiveMessage(); MessageHeader _receiveMessageHeader(); QByteArray _receiveMessageBody( int size ); diff --git a/deflect/SourceBuffer.cpp b/deflect/SourceBuffer.cpp new file mode 100644 index 0000000..99717b8 --- /dev/null +++ b/deflect/SourceBuffer.cpp @@ -0,0 +1,101 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of Ecole polytechnique federale de Lausanne. */ +/*********************************************************************/ + +#include "SourceBuffer.h" + +#include + +namespace deflect +{ + +SourceBuffer::SourceBuffer() +{ + _getQueue( View::mono ).push( Segments( )); + _getQueue( View::left_eye ).push( Segments( )); + _getQueue( View::right_eye ).push( Segments( )); +} + +const Segments& SourceBuffer::getSegments( const View view ) const +{ + return _getQueue( view ).front(); +} + +FrameIndex SourceBuffer::getBackFrameIndex( const View view ) const +{ + return _backFrameIndex[as_underlying_type(view)]; +} + +bool SourceBuffer::isBackFrameEmpty( const View view ) const +{ + return _getQueue( view ).back().empty(); +} + +void SourceBuffer::pop( const View view ) +{ + _getQueue( view ).pop(); +} + +void SourceBuffer::push( const View view ) +{ + _getQueue( view ).push( Segments( )); + ++_backFrameIndex[as_underlying_type(view)]; +} + +void SourceBuffer::insert( const Segment& segment, const View view ) +{ + _getQueue( view ).back().push_back( segment ); +} + +size_t SourceBuffer::getQueueSize( const View view ) const +{ + return _getQueue( view ).size(); +} + +std::queue& SourceBuffer::_getQueue( const View view ) +{ + return _segments[as_underlying_type(view)]; +} + +const std::queue& +SourceBuffer::_getQueue( const View view ) const +{ + return _segments[as_underlying_type(view)]; +} + +} diff --git a/deflect/SourceBuffer.h b/deflect/SourceBuffer.h new file mode 100644 index 0000000..63c1f69 --- /dev/null +++ b/deflect/SourceBuffer.h @@ -0,0 +1,98 @@ +/*********************************************************************/ +/* Copyright (c) 2017, EPFL/Blue Brain Project */ +/* Raphael Dumusc */ +/* All rights reserved. */ +/* */ +/* Redistribution and use in source and binary forms, with or */ +/* without modification, are permitted provided that the following */ +/* conditions are met: */ +/* */ +/* 1. Redistributions of source code must retain the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer. */ +/* */ +/* 2. Redistributions in binary form must reproduce the above */ +/* copyright notice, this list of conditions and the following */ +/* disclaimer in the documentation and/or other materials */ +/* provided with the distribution. */ +/* */ +/* THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, */ +/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */ +/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */ +/* DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF TEXAS AT */ +/* AUSTIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, */ +/* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES */ +/* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE */ +/* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR */ +/* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF */ +/* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT */ +/* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT */ +/* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */ +/* POSSIBILITY OF SUCH DAMAGE. */ +/* */ +/* The views and conclusions contained in the software and */ +/* documentation are those of the authors and should not be */ +/* interpreted as representing official policies, either expressed */ +/* or implied, of Ecole polytechnique federale de Lausanne. */ +/*********************************************************************/ + +#ifndef DEFLECT_SOURCEBUFFER_H +#define DEFLECT_SOURCEBUFFER_H + +#include +#include +#include + +#include +#include + +namespace deflect +{ + +using FrameIndex = unsigned int; + +/** + * Buffer for a single source of segments. + */ +class SourceBuffer +{ +public: + /** Construct an empty buffer. */ + SourceBuffer(); + + /** @return the segments at the front of the queue for a given view. */ + const Segments& getSegments( View view ) const; + + /** @return the frame index of the back of the buffer for a given view. */ + FrameIndex getBackFrameIndex( View view ) const; + + /** @return true if the back frame of the given view has no segments. */ + bool isBackFrameEmpty( View view ) const; + + /** Insert a segment into the back frame of the appropriate queue. */ + void insert( const Segment& segment, View view ); + + /** Push a new frame to the back of given view. */ + void push( View view ); + + /** Pop the front frame of the buffer for the given view. */ + void pop( View view ); + + /** @return the size of the queue for the given view. */ + size_t getQueueSize( View view ) const; + +private: + /** The collections of segments for each mono/left/right view. */ + std::queue _segments[3]; + + /** The current indices of the mono/left/right frame for this source. */ + std::array _backFrameIndex = { { 0u, 0u, 0u } }; + + std::queue& _getQueue( View view ); + const std::queue& _getQueue( View view ) const; +}; + +} + +#endif diff --git a/deflect/StreamPrivate.cpp b/deflect/StreamPrivate.cpp index 49b1113..85472ea 100644 --- a/deflect/StreamPrivate.cpp +++ b/deflect/StreamPrivate.cpp @@ -141,6 +141,9 @@ bool StreamPrivate::send( const ImageWrapper& image ) return false; } + if( !sendImageView( image.view )) + return false; + const auto sendFunc = std::bind( &StreamPrivate::sendPixelStreamSegment, this, std::placeholders::_1 ); return imageSegmenter.generate( image, sendFunc ); @@ -161,6 +164,15 @@ bool StreamPrivate::finishFrame() return socket.send( mh, QByteArray( )); } +bool StreamPrivate::sendImageView( const View view ) +{ + QByteArray message; + message.append( (const char*)( &view ), sizeof(View) ); + + const MessageHeader mh( MESSAGE_TYPE_IMAGE_VIEW, message.size(), id ); + return socket.send( mh, message ); +} + bool StreamPrivate::sendPixelStreamSegment( const Segment& segment ) { // Create message header diff --git a/deflect/StreamPrivate.h b/deflect/StreamPrivate.h index 03ed288..2f03721 100644 --- a/deflect/StreamPrivate.h +++ b/deflect/StreamPrivate.h @@ -99,6 +99,9 @@ class StreamPrivate /** @sa Stream::finishFrame */ bool finishFrame(); + /** Send the view for the image to be sent with sendPixelStreamSegment. */ + bool sendImageView( View view ); + /** * Send a Segment through the Stream. * @param segment An image segment with valid parameters and data diff --git a/deflect/types.h b/deflect/types.h index d51131d..31bd6d6 100644 --- a/deflect/types.h +++ b/deflect/types.h @@ -49,6 +49,16 @@ namespace deflect { +/** The different types of view. */ +enum class View : std::uint8_t { mono, left_eye, right_eye }; + +/** Cast an enum class value to its underlying type. */ +template +constexpr typename std::underlying_type::type as_underlying_type( E e ) +{ + return static_cast::type>( e ); +} + class EventReceiver; class Frame; class FrameDispatcher; diff --git a/doc/Changelog.md b/doc/Changelog.md index c8b830b..df01493 100644 --- a/doc/Changelog.md +++ b/doc/Changelog.md @@ -1,6 +1,11 @@ Changelog {#Changelog} ============ +## Deflect 0.13 (git master) + +* [148](https://github.com/BlueBrain/Deflect/pull/148): + Support for streaming stereo 3D content in a frame-sequential manner. + ## Deflect 0.12 ### 0.12.1 (01-02-2017) diff --git a/doc/StereoStreaming.md b/doc/StereoStreaming.md new file mode 100644 index 0000000..a2240d2 --- /dev/null +++ b/doc/StereoStreaming.md @@ -0,0 +1,104 @@ +Stereo Streaming +============ + +This document describes the stereo streaming support introduced in Deflect 0.13. + +## Requirements + +* Simple extension of the monoscopic Stream API +* No network protocol changes that break current Deflect clients or servers +* Support both screen space decompostion (sort-first) and left-right stereo + decomposition modes for distributed rendering. + +![Stereo streaming overview](stereo.png) + +## API + +New view enum in deflect/types.h: + + enum class View : std::int8_t { mono, left_eye, right_eye }; + +On the client side, no changes to the Stream API. The ImageWrapper takes an +additional View parameter. + +On the server side, no changes to the Server API (except some cleanups). Each +Frame dispatched now contains the View information. + +## Protocol + +The Stream send an additional View information message before each image +payload. This message is silently ignored by older Servers. + +## Examples + +Example of a stereo 3D client application using the blocking Stream API: + + deflect::Stream stream( ... ); + + /** ...synchronize start with other render clients (network barrier)... */ + + renderLoop() + { + /** ...render left image... */ + + deflect::ImageWrapper leftImage( data, width, height, deflect::RGBA ); + leftImage.view = deflect::View::left_eye; + deflectStream->send( leftImage ); + + /** ...render right image... */ + + deflect::ImageWrapper rightImage( data, width, height, deflect::RGBA ); + rightImage.view = deflect::View::right_eye; + deflectStream->send( rightImage ); + + deflectStream->finishFrame(); + + /** ...synchronize with other render clients (network barrier)... */ + } + +Example of a stereo 3D client application using the asynchronous Stream API: + + deflect::Stream stream( ... ); + + /** ...synchronize start with other render clients (network barrier)... */ + + delfect::Stream::Future leftFuture, rightFuture; + leftFuture = deflect::qt::make_ready_future( true ); + rightFuture = deflect::qt::make_ready_future( true ); + + ImageData leftData, rightData; // must remain valid until sending completes + + renderLoop() + { + if( !leftFuture.valid() || !leftFuture.get( )) + return; + + /** ...render left image... */ + + deflect::ImageWrapper leftImage( leftData, width, height, deflect::RGBA ); + leftImage.view = deflect::View::left_eye; + leftFuture = deflectStream->asyncSend( leftImage ); + + if( !rightFuture.valid() || !rightFuture.get( )) + return; + + /** ...render right image... */ + + deflect::ImageWrapper rightImage( rightData, width, height, deflect::RGBA ); + rightImage.view = deflect::View::right_eye; + rightFuture = deflectStream->send( rightImage ); + + /** ...synchronize with other render clients (network barrier)... */ + } + +For a complete code example, please refer to the SimpleStreamer application. + +## Issues + +### 1: Should clients be notified if they attempt to stream stereo content to a server which can only handle monoscopic content? + +_Resolution: Not yet:_ +For legacy implementation reasons, it is not possible to +send information from the Server to the Stream about the capabilities of the +server (such as stereo support) without rejecting all clients based on Deflect +< 0.12.1. A transition period is required before implementing this feature. diff --git a/doc/stereo.png b/doc/stereo.png new file mode 100644 index 0000000..0d93b2a Binary files /dev/null and b/doc/stereo.png differ diff --git a/doc/stereo.svg b/doc/stereo.svg new file mode 100644 index 0000000..f01a8bf --- /dev/null +++ b/doc/stereo.svg @@ -0,0 +1,1260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 2 + 3 + 4 + + + + + SERVER + STREAM CLIENTS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LEFT + RIGHT + + diff --git a/tests/cpp/ReceiveBufferTests.cpp b/tests/cpp/ReceiveBufferTests.cpp index 47c5aab..59234da 100644 --- a/tests/cpp/ReceiveBufferTests.cpp +++ b/tests/cpp/ReceiveBufferTests.cpp @@ -1,5 +1,5 @@ /*********************************************************************/ -/* Copyright (c) 2013-2015, EPFL/Blue Brain Project */ +/* Copyright (c) 2013-2017, EPFL/Blue Brain Project */ /* Raphael Dumusc */ /* All rights reserved. */ /* */ @@ -45,6 +45,12 @@ namespace ut = boost::unit_test; #include #include +inline std::ostream& operator << ( std::ostream& str, const QSize& s ) +{ + str << s.width() << 'x' << s.height(); + return str; +} + BOOST_AUTO_TEST_CASE( TestAddAndRemoveSources ) { deflect::ReceiveBuffer buffer; @@ -64,6 +70,33 @@ BOOST_AUTO_TEST_CASE( TestAddAndRemoveSources ) buffer.removeSource( 888 ); buffer.removeSource( 11981 ); BOOST_CHECK_EQUAL( buffer.getSourceCount(), 0 ); + + buffer.removeSource( 7777 ); + BOOST_CHECK_EQUAL( buffer.getSourceCount(), 0 ); +} + +BOOST_AUTO_TEST_CASE( TestAllowedToSend ) +{ + deflect::ReceiveBuffer buffer; + + BOOST_CHECK( !buffer.isAllowedToSend( deflect::View::mono )); + BOOST_CHECK( !buffer.isAllowedToSend( deflect::View::left_eye )); + BOOST_CHECK( !buffer.isAllowedToSend( deflect::View::right_eye )); + + buffer.setAllowedToSend( true, deflect::View::mono ); + BOOST_CHECK( buffer.isAllowedToSend( deflect::View::mono )); + buffer.setAllowedToSend( false, deflect::View::mono ); + BOOST_CHECK( !buffer.isAllowedToSend( deflect::View::mono )); + + buffer.setAllowedToSend( true, deflect::View::left_eye ); + BOOST_CHECK( buffer.isAllowedToSend( deflect::View::left_eye )); + buffer.setAllowedToSend( false, deflect::View::left_eye ); + BOOST_CHECK( !buffer.isAllowedToSend( deflect::View::left_eye )); + + buffer.setAllowedToSend( true, deflect::View::right_eye ); + BOOST_CHECK( buffer.isAllowedToSend( deflect::View::right_eye )); + buffer.setAllowedToSend( false, deflect::View::right_eye ); + BOOST_CHECK( !buffer.isAllowedToSend( deflect::View::right_eye )); } BOOST_AUTO_TEST_CASE( TestCompleteAFrame ) @@ -80,15 +113,18 @@ BOOST_AUTO_TEST_CASE( TestCompleteAFrame ) segment.parameters.height = 256; buffer.insert( segment, sourceIndex ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); buffer.finishFrameForSource( sourceIndex ); - BOOST_CHECK( buffer.hasCompleteFrame( )); + BOOST_CHECK( buffer.hasCompleteMonoFrame( )); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); deflect::Segments segments = buffer.popFrame(); BOOST_CHECK_EQUAL( segments.size(), 1 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); deflect::Frame frame; frame.segments = segments; @@ -146,20 +182,18 @@ BOOST_AUTO_TEST_CASE( TestCompleteACompositeFrameSingleSource ) buffer.insert( testSegments[1], sourceIndex ); buffer.insert( testSegments[2], sourceIndex ); buffer.insert( testSegments[3], sourceIndex ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.finishFrameForSource( sourceIndex ); - BOOST_CHECK( buffer.hasCompleteFrame( )); + BOOST_CHECK( buffer.hasCompleteMonoFrame( )); deflect::Segments segments = buffer.popFrame(); BOOST_CHECK_EQUAL( segments.size(), 4 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); deflect::Frame frame; frame.segments = segments; - const QSize frameSize = frame.computeDimensions(); - BOOST_CHECK_EQUAL( frameSize.width(), 192 ); - BOOST_CHECK_EQUAL( frameSize.height(), 768 ); + BOOST_CHECK_EQUAL( frame.computeDimensions(), QSize( 192, 768 )); } BOOST_AUTO_TEST_CASE( TestCompleteACompositeFrameMultipleSources ) @@ -178,27 +212,25 @@ BOOST_AUTO_TEST_CASE( TestCompleteACompositeFrameMultipleSources ) buffer.insert( testSegments[0], sourceIndex1 ); buffer.insert( testSegments[1], sourceIndex2 ); buffer.insert( testSegments[2], sourceIndex3 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.finishFrameForSource( sourceIndex1 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.finishFrameForSource( sourceIndex2 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.insert( testSegments[3], sourceIndex3 ); buffer.finishFrameForSource( sourceIndex3 ); - BOOST_CHECK( buffer.hasCompleteFrame( )); + BOOST_CHECK( buffer.hasCompleteMonoFrame( )); deflect::Segments segments = buffer.popFrame(); BOOST_CHECK_EQUAL( segments.size(), 4 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); deflect::Frame frame; frame.segments = segments; - const QSize frameSize = frame.computeDimensions(); - BOOST_CHECK_EQUAL( frameSize.width(), 192 ); - BOOST_CHECK_EQUAL( frameSize.height(), 768 ); + BOOST_CHECK_EQUAL( frame.computeDimensions(), QSize( 192, 768 )); } BOOST_AUTO_TEST_CASE( TestRemoveSourceWhileStreaming ) @@ -217,33 +249,233 @@ BOOST_AUTO_TEST_CASE( TestRemoveSourceWhileStreaming ) buffer.insert( testSegments[1], sourceIndex1 ); buffer.insert( testSegments[2], sourceIndex2 ); buffer.insert( testSegments[3], sourceIndex2 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.finishFrameForSource(sourceIndex1); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.finishFrameForSource( sourceIndex2 ); - BOOST_CHECK( buffer.hasCompleteFrame( )); + BOOST_CHECK( buffer.hasCompleteMonoFrame( )); deflect::Segments segments = buffer.popFrame(); BOOST_CHECK_EQUAL( segments.size(), 4 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); // Second frame - 1 source buffer.removeSource( sourceIndex2 ); buffer.insert( testSegments[0], sourceIndex1 ); buffer.insert( testSegments[1], sourceIndex1 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); buffer.finishFrameForSource( sourceIndex1 ); - BOOST_CHECK( buffer.hasCompleteFrame( )); + BOOST_CHECK( buffer.hasCompleteMonoFrame( )); segments = buffer.popFrame(); BOOST_CHECK_EQUAL( segments.size(), 2 ); - BOOST_CHECK( !buffer.hasCompleteFrame( )); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); deflect::Frame frame; frame.segments = segments; - const QSize frameSize = frame.computeDimensions(); - BOOST_CHECK_EQUAL( frameSize.width(), 192 ); - BOOST_CHECK_EQUAL( frameSize.height(), 256 ); + BOOST_CHECK_EQUAL( frame.computeDimensions(), QSize( 192, 256 )); +} + +BOOST_AUTO_TEST_CASE( TestOneOfTwoSourceStopsSendingSegments ) +{ + const size_t sourceIndex1 = 46; + const size_t sourceIndex2 = 819; + + deflect::ReceiveBuffer buffer; + buffer.addSource( sourceIndex1 ); + buffer.addSource( sourceIndex2 ); + + deflect::Segments testSegments = generateTestSegments(); + + // First Frame - 2 sources + buffer.insert( testSegments[0], sourceIndex1 ); + buffer.insert( testSegments[1], sourceIndex1 ); + buffer.insert( testSegments[2], sourceIndex2 ); + buffer.insert( testSegments[3], sourceIndex2 ); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); + buffer.finishFrameForSource(sourceIndex1); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); + buffer.finishFrameForSource( sourceIndex2 ); + BOOST_CHECK( buffer.hasCompleteMonoFrame( )); + + deflect::Segments segments = buffer.popFrame(); + + BOOST_CHECK_EQUAL( segments.size(), 4 ); + BOOST_CHECK( !buffer.hasCompleteMonoFrame( )); + + // Next frames - one source stops sending segments + for( int i = 0; i < 150; ++i ) + { + buffer.insert( testSegments[0], sourceIndex1 ); + buffer.insert( testSegments[1], sourceIndex1 ); + BOOST_REQUIRE_NO_THROW( buffer.finishFrameForSource( sourceIndex1 )); + BOOST_REQUIRE( !buffer.hasCompleteMonoFrame( )); + } + // Test buffer exceeds maximum allowed size + buffer.insert( testSegments[0], sourceIndex1 ); + buffer.insert( testSegments[1], sourceIndex1 ); + BOOST_CHECK_THROW( buffer.finishFrameForSource( sourceIndex1 ), + std::runtime_error ); +} + +void _insert( deflect::ReceiveBuffer& buffer, const size_t sourceIndex, + const deflect::Segments& frame, const deflect::View view ) +{ + for( const auto& segment : frame ) + buffer.insert( segment, sourceIndex, view ); + buffer.finishFrameForSource( sourceIndex ); +} + +void _testStereoBuffer( deflect::ReceiveBuffer& buffer ) +{ + const auto leftSegments = buffer.popFrame( deflect::View::left_eye ); + BOOST_CHECK_EQUAL( leftSegments.size(), 4 ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + + const auto rightSegments = buffer.popFrame( deflect::View::right_eye ); + BOOST_CHECK_EQUAL( rightSegments.size(), 4 ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + + deflect::Frame frame; + frame.segments = leftSegments; + BOOST_CHECK_EQUAL( frame.computeDimensions(), QSize( 192, 768 )); + frame.segments = rightSegments; + BOOST_CHECK_EQUAL( frame.computeDimensions(), QSize( 192, 768 )); +} + +BOOST_AUTO_TEST_CASE( TestStereoOneSource ) +{ + const size_t sourceIndex = 46; + + deflect::ReceiveBuffer buffer; + buffer.addSource( sourceIndex ); + + deflect::Segments testSegments = generateTestSegments(); + + _insert( buffer, sourceIndex, testSegments, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + + _insert( buffer, sourceIndex, testSegments, deflect::View::right_eye ); + BOOST_CHECK( buffer.hasCompleteStereoFrame( )); + + _testStereoBuffer( buffer ); +} + +BOOST_AUTO_TEST_CASE( TestStereoSingleFinishFrame ) +{ + const size_t sourceIndex = 46; + + deflect::ReceiveBuffer buffer; + buffer.addSource( sourceIndex ); + + const deflect::Segments testSegments = generateTestSegments(); + + for( const auto& segment : testSegments ) + buffer.insert( segment, sourceIndex, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + + for( const auto& segment : testSegments ) + buffer.insert( segment, sourceIndex, deflect::View::right_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + + buffer.finishFrameForSource( sourceIndex ); + BOOST_CHECK( buffer.hasCompleteStereoFrame( )); + + _testStereoBuffer( buffer ); +} + +BOOST_AUTO_TEST_CASE( TestStereoTwoSourcesScreenSpaceSplit ) +{ + const size_t sourceIndex1 = 46; + const size_t sourceIndex2 = 819; + + deflect::ReceiveBuffer buffer; + buffer.addSource( sourceIndex1 ); + buffer.addSource( sourceIndex2 ); + + const auto testSegments = generateTestSegments(); + const auto segmentsScreen1 = deflect::Segments{ testSegments[0], + testSegments[1] }; + const auto segmentsScreen2 = deflect::Segments{ testSegments[2], + testSegments[3] }; + + _insert( buffer, sourceIndex1, segmentsScreen1, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex1, segmentsScreen1, deflect::View::right_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + + _insert( buffer, sourceIndex2, segmentsScreen2, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex2, segmentsScreen2, deflect::View::right_eye ); + BOOST_CHECK( buffer.hasCompleteStereoFrame( )); + + _testStereoBuffer( buffer ); +} + +BOOST_AUTO_TEST_CASE( TestStereoTwoSourcesStereoSplit ) +{ + const size_t sourceIndex1 = 46; + const size_t sourceIndex2 = 819; + + deflect::ReceiveBuffer buffer; + buffer.addSource( sourceIndex1 ); + buffer.addSource( sourceIndex2 ); + + const auto testSegments = generateTestSegments(); + + _insert( buffer, sourceIndex1, testSegments, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex2, testSegments, deflect::View::right_eye ); + BOOST_CHECK( buffer.hasCompleteStereoFrame( )); + + _testStereoBuffer( buffer ); +} + +BOOST_AUTO_TEST_CASE( TestStereoFourSourcesScreenSpaceAndStereoSplit ) +{ + const size_t sourceIndex1 = 46; + const size_t sourceIndex2 = 819; + const size_t sourceIndex3 = 489; + const size_t sourceIndex4 = 113; + + deflect::ReceiveBuffer buffer; + buffer.addSource( sourceIndex1 ); + buffer.addSource( sourceIndex2 ); + buffer.addSource( sourceIndex3 ); + buffer.addSource( sourceIndex4 ); + + const auto testSegments = generateTestSegments(); + const auto segmentsScreen1 = deflect::Segments{ testSegments[0], + testSegments[1] }; + const auto segmentsScreen2 = deflect::Segments{ testSegments[2], + testSegments[3] }; + + _insert( buffer, sourceIndex1, segmentsScreen1, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex2, segmentsScreen1, deflect::View::right_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex3, segmentsScreen2, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex4, segmentsScreen2, deflect::View::right_eye ); + BOOST_CHECK( buffer.hasCompleteStereoFrame( )); + + _testStereoBuffer( buffer ); + + // Random insertion order + _insert( buffer, sourceIndex1, segmentsScreen1, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex3, segmentsScreen2, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex2, segmentsScreen1, deflect::View::right_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex1, segmentsScreen1, deflect::View::left_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex2, segmentsScreen1, deflect::View::right_eye ); + BOOST_CHECK( !buffer.hasCompleteStereoFrame( )); + _insert( buffer, sourceIndex4, segmentsScreen2, deflect::View::right_eye ); + BOOST_CHECK( buffer.hasCompleteStereoFrame( )); + + _testStereoBuffer( buffer ); } diff --git a/tests/cpp/ServerTests.cpp b/tests/cpp/ServerTests.cpp index 73fd020..abfb617 100644 --- a/tests/cpp/ServerTests.cpp +++ b/tests/cpp/ServerTests.cpp @@ -142,7 +142,7 @@ BOOST_AUTO_TEST_CASE( testRegisterForEventReceivedByServer ) streamId = id; exclusiveBind = exclusive; eventReceiver = receiver; - server->onEventRegistrationReply( id, true ); // send reply to Stream + server->replyToEventRegistration( id, true ); // send reply to Stream mutex.lock(); receivedState = true; received.wakeAll();