Skip to content

Commit

Permalink
Add videostreaming as an alternative to JPEG streaming to RocketsPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
tribal-tec committed Sep 9, 2019
1 parent 54bf8e1 commit 6079731
Show file tree
Hide file tree
Showing 7 changed files with 455 additions and 4 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ endif()
# HTTP messaging
common_find_package(LibJpegTurbo)
common_find_package(Rockets)
common_find_package(FFMPEG 3.4 SYSTEM)
if(ROCKETS_FOUND AND ROCKETS_USE_LIBWEBSOCKETS)
option(BRAYNS_NETWORKING_ENABLED "Activate networking interfaces" ON)
else()
Expand Down
6 changes: 5 additions & 1 deletion brayns/parameters/ApplicationParameters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const std::string PARAM_PLUGIN = "plugin";
const std::string PARAM_STEREO = "stereo";
const std::string PARAM_WINDOW_SIZE = "window-size";
const std::string PARAM_ENV_MAP = "env-map";
const std::string PARAM_VIDEOSTREAMING = "videostreaming";

const size_t DEFAULT_WINDOW_WIDTH = 800;
const size_t DEFAULT_WINDOW_HEIGHT = 600;
Expand Down Expand Up @@ -80,7 +81,10 @@ ApplicationParameters::ApplicationParameters()
(PARAM_MAX_RENDER_FPS.c_str(), po::value<size_t>(&_maxRenderFPS),
"Max. render FPS") //
(PARAM_ENV_MAP.c_str(), po::value<std::string>(&_envMap),
"Path to environment map");
"Path to environment map")(
PARAM_VIDEOSTREAMING.c_str(),
po::bool_switch(&_useVideoStreaming)->default_value(false),
"Use videostreaming over websockets instead of JPEG");

_positionalArgs.add(PARAM_INPUT_PATHS.c_str(), -1);
}
Expand Down
5 changes: 3 additions & 2 deletions brayns/parameters/ApplicationParameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class ApplicationParameters : public AbstractParameters
_updateValue(_imageStreamFPS, fps);
}

bool useVideoStreaming() const { return _useVideoStreaming; }
/** Max render FPS to limit */
size_t getMaxRenderFPS() const { return _maxRenderFPS; }
bool isStereo() const { return _stereo; }
Expand All @@ -88,8 +89,7 @@ class ApplicationParameters : public AbstractParameters
_updateValue(_httpServerURI, httpServerURI);
}

const std::string& getEnvMap() const {return _envMap;}

const std::string& getEnvMap() const { return _envMap; }
const strings& getInputPaths() const { return _inputPaths; }
po::positional_options_description& posArgs() { return _positionalArgs; }
protected:
Expand All @@ -106,6 +106,7 @@ class ApplicationParameters : public AbstractParameters
std::string _httpServerURI;
bool _parallelRendering{false};
bool _dynamicLoadBalancer{false};
bool _useVideoStreaming{false};
std::string _envMap;

strings _inputPaths;
Expand Down
9 changes: 9 additions & 0 deletions plugins/Rockets/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ set(BRAYNSROCKETS_LINK_LIBRARIES PRIVATE Rockets braynsParameters braynsTasks
if(LibJpegTurbo_FOUND)
list(APPEND BRAYNSROCKETS_LINK_LIBRARIES PRIVATE ${LibJpegTurbo_LIBRARIES})
endif()
if(FFMPEG_FOUND)
list(APPEND BRAYNSROCKETS_HEADERS encoder.h)
list(APPEND BRAYNSROCKETS_SOURCES encoder.cpp)
list(APPEND BRAYNSROCKETS_LINK_LIBRARIES PRIVATE ${FFMPEG_LIBRARIES})
endif()

if(libuv_FOUND)
list(APPEND BRAYNSROCKETS_LINK_LIBRARIES PRIVATE ${libuv_LIBRARIES})
Expand All @@ -44,6 +49,10 @@ set(BRAYNSROCKETS_INCLUDE_NAME rocketsplugin)
common_library(braynsRockets)
target_include_directories(braynsRockets SYSTEM PRIVATE ${FREEIMAGE_INCLUDE_DIRS})

if(FFMPEG_FOUND)
target_include_directories(braynsRockets SYSTEM PRIVATE ${FFMPEG_INCLUDE_DIR})
endif()

# needed for staticjson and rapidjson
target_include_directories(braynsRockets SYSTEM PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
Expand Down
65 changes: 64 additions & 1 deletion plugins/Rockets/RocketsPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
#include "ImageGenerator.h"
#include "Throttle.h"

#ifdef BRAYNS_USE_FFMPEG
#include "encoder.h"
#endif

namespace
{
constexpr int64_t INTERACTIVE_THROTTLE = 1;
Expand Down Expand Up @@ -266,7 +270,12 @@ class RocketsPlugin::Impl : public ActionInterface
if (!_rocketsServer || _rocketsServer->getConnectionCount() == 0)
return;

_broadcastImageJpeg();
if (!_parametersManager.getApplicationParameters().useVideoStreaming())
_broadcastImageJpeg();
#ifdef BRAYNS_USE_FFMPEG
else
_broadcastVideo();
#endif
}

void registerNotification(const RpcParameterDescription& desc,
Expand Down Expand Up @@ -417,6 +426,12 @@ class RocketsPlugin::Impl : public ActionInterface

void _setupWebsocket()
{
#ifdef BRAYNS_USE_FFMPEG
_rocketsServer->handleOpen([this](auto) {
_resetEncoder = true;
return std::vector<rockets::ws::Response>{};
});
#endif
_rocketsServer->handleClose([this](const uintptr_t clientID) {
_binaryRequests.removeRequest(clientID);
return std::vector<rockets::ws::Response>{};
Expand Down Expand Up @@ -1041,6 +1056,49 @@ class RocketsPlugin::Impl : public ActionInterface
image.size);
}

#ifdef BRAYNS_USE_FFMPEG
void _broadcastVideo()
{
auto& frameBuffer = _engine.getFrameBuffer();
if (frameBuffer.getFrameBufferFormat() == FrameBufferFormat::none ||
!frameBuffer.isModified())
{
return;
}

const auto& params = _parametersManager.getApplicationParameters();
const auto fps = params.getImageStreamFPS();
if (fps == 0)
return;

int width = frameBuffer.getFrameSize().x;
if (width % 2 != 0)
width += 1;
int height = frameBuffer.getFrameSize().y;
if (height % 2 != 0)
height += 1;
if (_encoder)
{
if (_encoder->width != width || _encoder->height != height ||
_resetEncoder)
_encoder.reset();
}
if (!_encoder)
{
const int kbps = 5000;

_encoder =
std::make_unique<Encoder>(width, height, fps, kbps,
[& rs = _rocketsServer](auto a,
auto b) {
rs->broadcastBinary(a, b);
});
}
_resetEncoder = false;
_encoder->encode(frameBuffer);
}
#endif

void _handleVersion()
{
static brayns::Version version;
Expand Down Expand Up @@ -1834,6 +1892,11 @@ class RocketsPlugin::Impl : public ActionInterface
bool _endpointsRegistered{false};

std::vector<BaseObject*> _objects;

#ifdef BRAYNS_USE_FFMPEG
std::unique_ptr<Encoder> _encoder;
bool _resetEncoder{false};
#endif
};

RocketsPlugin::~RocketsPlugin()
Expand Down
219 changes: 219 additions & 0 deletions plugins/Rockets/encoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/* Copyright (c) 2019 EPFL/Blue Brain Project
* All rights reserved. Do not distribute without permission.
*
* This file is part of Brayns <https://github.com/BlueBrain/Brayns>
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License version 3.0 as published
* by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include "encoder.h"

#include <brayns/common/log.h>
#include <brayns/engine/FrameBuffer.h>

int custom_io_write(void *opaque, uint8_t *buffer, int32_t buffer_size)
{
auto encoder = (brayns::Encoder *)opaque;
encoder->_dataFunc((const char *)buffer, buffer_size);
return buffer_size;
}

namespace brayns
{
Encoder::Encoder(const int width_, const int height_, const int fps,
const int64_t kbps, const DataFunc &dataFunc)
: _dataFunc(dataFunc)
, width(width_)
, height(height_)
, _fps(fps)
{
#ifndef FF_API_NEXT
av_register_all();
#endif
formatContext = avformat_alloc_context();
formatContext->oformat = av_guess_format("mp4", nullptr, nullptr);
formatContext->flags = AVFMT_FLAG_CUSTOM_IO;

const AVCodecID codecID = AV_CODEC_ID_H264;
codec = avcodec_find_encoder(codecID);
if (!codec)
BRAYNS_THROW(
std::runtime_error(std::string("Could not find encoder for ") +
avcodec_get_name(codecID)));

if (!(stream = avformat_new_stream(formatContext, codec)))
BRAYNS_THROW(std::runtime_error("Could not create stream"));

const AVRational avFPS = {fps, 1};
stream->avg_frame_rate = avFPS;
stream->time_base = av_inv_q(avFPS);

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
codecContext = stream->codec;
#pragma GCC diagnostic pop
codecContext->codec_tag = 0;
codecContext->codec_id = codecID;
codecContext->codec_type = AVMEDIA_TYPE_VIDEO;
codecContext->width = width;
codecContext->height = height;
codecContext->gop_size = 0;
codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
codecContext->framerate = avFPS;
codecContext->time_base = av_inv_q(avFPS);
codecContext->bit_rate = kbps * 1000;
codecContext->max_b_frames = 0;
codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

codecContext->profile = 100;
codecContext->level = 31;

av_opt_set(codecContext->priv_data, "crf", "12", 0);
av_opt_set(codecContext->priv_data, "preset", "ultrafast", 0);
// av_opt_set(codecContext->priv_data, "profile", "main", 0);
av_opt_set(codecContext->priv_data, "tune", "zerolatency", 0);

if (avcodec_open2(codecContext, codec, NULL) < 0)
BRAYNS_THROW(std::runtime_error("Could not open video encoder!"));

int avio_buffer_size = 1 * 1024 * 1024;
void *avio_buffer = av_malloc(avio_buffer_size);

AVIOContext *custom_io =
avio_alloc_context((unsigned char *)avio_buffer, avio_buffer_size, 1,
(void *)this, NULL, &custom_io_write, NULL);

formatContext->pb = custom_io;

AVDictionary *fmt_opts = NULL;
av_dict_set(&fmt_opts, "brand", "mp42", 0);
av_dict_set(&fmt_opts, "movflags", "faststart+frag_keyframe+empty_moov", 0);
av_dict_set(&fmt_opts, "live", "1", 0);
if (avformat_write_header(formatContext, &fmt_opts) < 0)
BRAYNS_THROW(std::runtime_error("Could not write header!"));

picture.init(codecContext->pix_fmt, width, height);

if (_async)
_thread = std::thread(std::bind(&Encoder::_runAsync, this));

_timer.start();
}

Encoder::~Encoder()
{
if (_async)
{
_running = false;
_queue.push(-1);
_thread.join();
}

if (formatContext)
{
av_write_trailer(formatContext);
av_free(formatContext->pb);
avcodec_close(codecContext);
avformat_free_context(formatContext);
}
}

void Encoder::encode(FrameBuffer &fb)
{
if (_async && _queue.size() == 2)
return;

fb.map();
auto cdata = reinterpret_cast<const uint8_t *const>(fb.getColorBuffer());
if (_async)
{
auto &image = _image[_currentImage];
image.width = fb.getSize().x;
image.height = fb.getSize().y;
const auto bufferSize = image.width * image.height * fb.getColorDepth();

if (image.data.size() < bufferSize)
image.data.resize(bufferSize);
memcpy(image.data.data(), cdata, bufferSize);
_queue.push(_currentImage);
_currentImage = _currentImage == 0 ? 1 : 0;
fb.unmap();
return;
}

_toPicture(cdata, fb.getSize().x, fb.getSize().y);
fb.unmap();

_encode();
}

void Encoder::_encode()
{
const auto elapsed = _timer.elapsed() + _leftover;
const auto duration = 1.0 / _fps;
if (elapsed < duration)
return;

_leftover = elapsed - duration;
for (; _leftover > duration;)
_leftover -= duration;

picture.frame->pts = _frameNumber++;

if (avcodec_send_frame(codecContext, picture.frame) < 0)
return;

AVPacket pkt;
av_init_packet(&pkt);
const auto ret = avcodec_receive_packet(codecContext, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
av_packet_unref(&pkt);
return;
}

av_packet_rescale_ts(&pkt, codecContext->time_base, stream->time_base);
pkt.stream_index = stream->index;
av_interleaved_write_frame(formatContext, &pkt);

_timer.start();
}

void Encoder::_runAsync()
{
while (_running)
{
auto idx = _queue.pop();
if (idx < 0)
break;

auto &image = _image[idx];

_toPicture(image.data.data(), image.width, image.height);
_encode();
}
}

void Encoder::_toPicture(const uint8_t *const data, const int width_,
const int height_)
{
sws_context =
sws_getCachedContext(sws_context, width_, height_, AV_PIX_FMT_RGBA,
width, height, AV_PIX_FMT_YUV420P,
SWS_FAST_BILINEAR, 0, 0, 0);
const int stride[] = {4 * (int)width_};
sws_scale(sws_context, &data, stride, 0, height_, picture.frame->data,
picture.frame->linesize);
}
}
Loading

0 comments on commit 6079731

Please sign in to comment.