This page will focus on explaining the tutorial application and how the Genvid API is integrated into it.
Content
The tutorial application displays 3 cubes named Athos, Porthos
and Aramis. These cubes are displayed rotating and moving in various
fixed directions. Upon reaching the extremity of the world, a cube
is moved to the opposite side of the world to continue its movement
without interruption. The player is in control of one cube at a
time. Here’s a list of the controls available to the player:
- The player is able to use the arrows and WASD keys to modify the
trajectory of the selected cube. Modifying the direction also directly
affects the rotation of the object.
- The player is able to change the selection cube by pressing 1,
2 or 3.
- The player can change the selected cube’s color by pressing a
number on the numeric keypad.
- The player can reset the orientation of the selected cube by
pressing the O key.
- The player can reset the position of the selected cube by pressing
the P key.
- The player can stop the cube from moving by pressing the Backspace
key.
How to run the tutorial
This guide assumes that your system is already properly configured, i.e., that
your config file is properly set up with a working YouTube account
(see Genvid Configuration) and that you can successfully run the
Genvid services (see Local Environment Usage).
Before explaining about the tutorial code used, let us explain how to
run the tutorial sample.
- Clean and load the project configuration:
genvid-sdk clean-config
genvid-sdk load-config-sdk
py tutorial.py load
- Build the project and the website:
- Launch the Cluster-UI:
On the Cluster-UI page, you can start the services, application, web and
even open the website.
Genvid integration
The Genvid SDK was integrated at various locations in the Tutorial
application. We will show at which location they were integrated
with code samples and line numbers to help you navigate through the
tutorial application.
We start by including the Genvid Native SDK header file, and declare
a few static variables:
#include <genvid.h>
static const std::string sStream_Audio = "Audio";
static const std::string sStream_Video = "Video";
static const std::string sStream_GameData = "GameData";
static const std::string sStream_Popularity = "Popularity";
static const std::string sStream_ColorChanged = "ColorChanged";
static const std::string sEvent_changeColor = "changeColor";
static const std::string sEvent_reset = "reset";
static const std::string sEvent_cheer = "cheer";
static const std::string sCommand_speed = "speed";
static const std::string sCommand_direction = "direction";
static const std::string sCommand_reset = "reset";
static std::string oldPopularityJSON;
static void GenvidSubscriptionCallback(const GenvidEventSummary* summary, void* userData);
static void GenvidSubscriptionCommandCallback(const GenvidCommandResult* summary, void* userData);
system_clock::time_point gStartTime = system_clock::now();
double gWorldTime = 0.0;
bool gSilent = true; // Global flag to kill all sounds.
XMVECTORF32 gBackgroundColor = Colors::DodgerBlue;
XMVECTORF32 gBackgroundColorArray[] = { Colors::DodgerBlue, Colors::Green, Colors::Red, Colors::Orange, Colors::Bisque, Colors::CadetBlue, Colors::Indigo, Colors::Yellow };
XMFLOAT4 gColors[] = {
{ 0.6f, 0.1f, 0.4f, 1.0f },
{ 0.2f, 0.9f, 0.1f, 1.0f },
{ 0.7f, 0.7f, 0.7f, 1.0f },
{ 0.9f, 0.9f, 0.1f, 1.0f },
{ 0.1f, 0.2f, 0.3f, 1.0f },
{ 0.3f, 0.3f, 0.3f, 1.0f },
{ 0.2f, 0.4f, 0.6f, 1.0f },
{ 0.5f, 0.3f, 0.2f, 1.0f },
{ 0.1f, 0.5f, 0.9f, 1.0f },
{ 0.4f, 0.2f, 0.4f, 1.0f },
};
std::map<std::string, XMFLOAT4> sNameToColor = {
{"rose" , gColors[0]},
{"green" , gColors[1]},
{"white" , gColors[2]},
{"yellow" , gColors[3]},
{"dark blue" , gColors[4]},
{"gray" , gColors[5]},
{"light blue", gColors[6]},
{"orange" , gColors[7]},
{"blue" , gColors[8]},
{"purple" , gColors[9]},
};
After setting up DirectX in InitDevice()
, we call InitGenvid()
which takes care of initializing the Genvid library, defines all of the
streams that we will need, and configures all the event and command
subscriptions:
HRESULT InitGenvid()
{
GenvidStatus gs;
// Initialize the Genvid Native SDK.
gs = Genvid_Initialize();
if (GENVID_FAILED(gs))
return E_FAIL;
// Create the audio stream.
gs = Genvid_CreateStream(sStream_Audio.c_str());
if(GENVID_FAILED(gs))
return E_FAIL;
// Specify auto-capture video source.
gs = Genvid_SetParameterInt(sStream_Audio.c_str(), "Audio.Source.WASAPI", 1);
if(GENVID_FAILED(gs))
return E_FAIL;
// Create the video stream.
gs = Genvid_CreateStream(sStream_Video.c_str());
if (GENVID_FAILED(gs))
return E_FAIL;
#if GENVID_USE_DXGISWAPCHAIN
// Specify auto-capture video source.
gs = Genvid_SetParameterPointer(sStream_Video.c_str(), "Video.Source.IDXGISwapChain", g_pSwapChain);
#endif
if (GENVID_FAILED(gs))
return E_FAIL;
// Create stream for game data.
gs = Genvid_CreateStream(sStream_GameData.c_str());
if (GENVID_FAILED(gs))
return E_FAIL;
gs = Genvid_CreateStream(sStream_Popularity.c_str());
if (GENVID_FAILED(gs))
return E_FAIL;
gs = Genvid_CreateStream(sStream_ColorChanged.c_str());
if (GENVID_FAILED(gs))
return E_FAIL;
// Subscribe to events.
gs = Genvid_Subscribe(sEvent_changeColor.c_str(), &GenvidSubscriptionCallback, nullptr);
if (GENVID_FAILED(gs))
return E_FAIL;
gs = Genvid_Subscribe(sEvent_reset.c_str(), &GenvidSubscriptionCallback, nullptr);
if (GENVID_FAILED(gs))
return E_FAIL;
gs = Genvid_Subscribe(sEvent_cheer.c_str(), &GenvidSubscriptionCallback, nullptr);
if (GENVID_FAILED(gs))
return E_FAIL;
// Subscribe to commands.
gs = Genvid_SubscribeCommand(sCommand_speed.c_str(), &GenvidSubscriptionCommandCallback, nullptr);
if(GENVID_FAILED(gs))
return E_FAIL;
gs = Genvid_SubscribeCommand(sCommand_direction.c_str(), &GenvidSubscriptionCommandCallback, nullptr);
if(GENVID_FAILED(gs))
return E_FAIL;
gs = Genvid_SubscribeCommand(sCommand_reset.c_str(), &GenvidSubscriptionCommandCallback, nullptr);
if(GENVID_FAILED(gs))
return E_FAIL;
return S_OK;
}
We currently have only one video stream and one audio stream. We send
the video IDXGISwapChain via
Genvid_SetParameterPointer()
. We create a stream to send the
game data to the servers, one for the popularity change and one for
the the color change. Afterwards, we create all the subscribes that we
need to catch the events and commands coming from the webpage. In our
case, we are catching 3 actions that can occur from the webpage: a
change of color, a position reset and a popularity change. The
function GenvidSubscriptionCallback()
is used to manage the event
catching portion which will be displayed later on. Considering we are
sending 3 types of events from the webpage, we need to call
Genvid_Subscribe()
3 times with the proper name (these
names must exactly match the ones used on the webpage and in the map
reduce rules). We are also catching 3 actions that can occur from the
webpage as a command: a speed change, a direction change or a reset
position.
Right before presenting the back buffer, we auto-capture the video frame
(using the previously specified IDXGISwapChain) and send it to Genvid.
We also gather game data (cube position, orientation, and camera matrix),
format it into JSON format for the website, and submit it with the same
timecode as the video data. This is done in the utility routine GetGameDataJSON()
.
We also gather the popularity data and verify if there was any changes to it. if
there was any change, we perform a Genvid_SubmitNotification()
with the up to
date popularity data formatted into JSON. We then call
Genvid_CheckForEvents()
to see if we have received any event from the website.
GenvidTimecode tc = Genvid_GetCurrentTimecode();
std::string gameData = GetGameDataJSON();
Genvid_SubmitGameData(tc, sStream_GameData.c_str(), gameData.data(), (int)gameData.size());
// Send the popularity as a notification
std::string popularityJSON = GetPopularityJSON();
if (popularityJSON != oldPopularityJSON)
{
Genvid_SubmitNotification(sStream_Popularity.c_str(), popularityJSON.data(), (int)popularityJSON.size());
}
oldPopularityJSON = popularityJSON;
Genvid_CheckForEvents();
Calling Genvid_CheckForEvents()
might invoke the function
GenvidSubscriptionCallback()
. This function is used to catch the
events sent from the webpage to the game. In this case, we are
evaluating the summary id value to know which event was sent.
Afterwards, we interpret the result key fields as parameters to the
event.
//--------------------------------------------------------------------------------------
// Callback invoked when Genvid Events are received.
//--------------------------------------------------------------------------------------
void GenvidSubscriptionCallback(const GenvidEventSummary* summary, void* /*userData*/)
{
if (summary->id == sEvent_changeColor)
{
// Change the color of the cube.
const char* cubeName = summary->results[0].key.fields[0];
const char* cubeColor = summary->results[0].key.fields[1];
for (int i = 0; i < g_cubeTotal; ++i)
{
if (g_cubeList[i].GetName() == cubeName)
{
std::map<std::string, XMFLOAT4>::iterator colorToFind = sNameToColor.find(cubeColor);
if (colorToFind != sNameToColor.end())
{
g_cubeList[i].SetColor(colorToFind->second);
}
}
}
OnUpdateColor();
}
else if (summary->id == sEvent_reset)
{
// Reset the cube position.
const char* cubeName = summary->results[0].key.fields[0];
for (int i = 0; i < g_cubeTotal; ++i)
{
if (g_cubeList[i].GetName() == cubeName)
{
g_cubeList[i].ResetPosition();
return;
}
}
}
else if (summary->id == sEvent_cheer)
{
// Handle cube cheering.
const char* cubeName = summary->results[0].key.fields[0];
for (int i = 0; i < g_cubeTotal; ++i)
{
if (g_cubeList[i].GetName() == cubeName)
{
g_cubeList[i].Cheer(float(summary->results[0].values[0].value));
return;
}
}
}
}
The structure of the summary is consistent with the map-reduce definition
that we provided alongside the application (in config/events.json
):
{
"version": "1.7.0",
"event": {
"game": {
"maps": [
{
"id": "changeColor",
"source": "userinput",
"where": {"key": ["changeColor", "<name>"], "name": "<color>", "type": "string"},
"key": ["changeColor", "<name>", "<color>"], "value": 1
},
{
"id": "reset",
"source": "userinput",
"where": {"key": ["reset"], "name": "<name>", "type": "string"},
"key": ["reset", "<name>"], "value": 1
},
{
"id": "cheer",
"source": "userinput",
"where": {"key": ["cheer"], "name": "<name>", "type": "string"},
"key": ["cheer", "<name>"], "value": 1
}
],
"reductions": [
{
"id": "changeColor",
"where": {"key": ["changeColor", "<name>", "<color>"]},
"key": ["<name>", "<color>"],
"value": ["$count"],
"period": 250
},
{
"id": "reset",
"where": {"key": ["reset", "<name>"]},
"key": ["<name>"],
"value": ["$count"],
"period": 250
},
{
"id": "cheer",
"where": {"key": ["cheer", "<name>"]},
"key": ["<name>"],
"value": ["$sum"],
"period": 250
}
]
}
}
}
Calling Genvid_CheckForEvents()
might also invoke the function
GenvidSubscriptionCommandCallback()
. This is where commands are
interpreted.
void GenvidSubscriptionCommandCallback(const GenvidCommandResult* result, void* /*userData*/)
{
std::string id(result->id);
std::string value(result->value);
if(id == "direction")
{
std::vector<std::string> elems;
split(value, ':', elems);
auto cubeName = elems[0];
for(int i = 0; i < g_cubeTotal; ++i)
{
if(g_cubeList[i].GetName() == cubeName)
{
float x = (float)std::atof(elems[1].c_str());
float y = (float)std::atof(elems[2].c_str());
float z = (float)std::atof(elems[3].c_str());
g_cubeList[i].ChangeDirection(x, y, z);
return;
}
}
}
else if(id == "speed")
{
std::vector<std::string> elems;
split(value, ':', elems);
auto cubeName = elems[0];
for(int i = 0; i < g_cubeTotal; ++i)
{
if(g_cubeList[i].GetName() == cubeName)
{
float speed = (float)std::atof(elems[1].c_str());
auto velocity = g_cubeList[i].GetLinearVelocity() * speed;
g_cubeList[i].SetLinearVelocity(velocity);
return;
}
}
}
else if(id == "reset")
{
auto cubeName = value;
for(int i = 0; i < g_cubeTotal; ++i)
{
if(g_cubeList[i].GetName() == cubeName)
{
auto velocity = g_cubeList[i].GetInitialLinearVelocity();
g_cubeList[i].ResetPosition();
g_cubeList[i].ResetOrientation();
g_cubeList[i].SetLinearVelocity(velocity);
return;
}
}
}
}
The command result value contains an id and a value which both are parsed
into their own strings to be use in the method.
The tutorial demonstrates two different approaches for video
streaming. The default use the auto-capture functionality of the SDK,
and you simply just have to call the
Genvid_SubmitVideoData()
function with null parameters to
activate the capture:
Genvid_SubmitVideoData(tc, sStream_Video.c_str(), nullptr, 0);
The other method does the capture manually. That could be because
you’re not using DirectX11 for example, or that you don’t want to use
the rendering thread to push the data to Genvid. In this case, you
must setup the video frame using the correct parameters once:
assert(description.Format == DXGI_FORMAT_R8G8B8A8_UNORM);
GenvidStatus gs = Genvid_SetParameterInt(sStream_Video.c_str(), "video.pixel_format", GenvidPixelFormat_R8G8B8A8);
assert(gs == GenvidStatus_Success);
gs = Genvid_SetParameterInt(sStream_Video.c_str(), "video.width", description.Width);
assert(gs == GenvidStatus_Success);
gs = Genvid_SetParameterInt(sStream_Video.c_str(), "video.height", description.Height);
assert(gs == GenvidStatus_Success);
And then, you can capture your buffer and submit it to Genvid:
Genvid_SubmitVideoData(tc, sStream_Video.c_str(), buffer.data(), static_cast<const int>(buffer.size()));
In the TermGenvid()
function, we perform every action necessary for
a proper cleanup of the Genvid SDK:
void TermGenvid()
{
// Cancel command subscriptions.
Genvid_UnsubscribeCommand(sCommand_reset.c_str(), &GenvidSubscriptionCommandCallback, nullptr);
Genvid_UnsubscribeCommand(sCommand_direction.c_str(), &GenvidSubscriptionCommandCallback, nullptr);
Genvid_UnsubscribeCommand(sCommand_speed.c_str(), &GenvidSubscriptionCommandCallback, nullptr);
// Cancel event subscriptions.
Genvid_Unsubscribe(sEvent_cheer.c_str(), &GenvidSubscriptionCallback, nullptr);
Genvid_Unsubscribe(sEvent_reset.c_str(), &GenvidSubscriptionCallback, nullptr);
Genvid_Unsubscribe(sEvent_changeColor.c_str(), &GenvidSubscriptionCallback, nullptr);
// Destroy the streams.
Genvid_DestroyStream(sStream_ColorChanged.c_str());
Genvid_DestroyStream(sStream_Popularity.c_str());
Genvid_DestroyStream(sStream_GameData.c_str());
Genvid_DestroyStream(sStream_Video.c_str());
Genvid_DestroyStream(sStream_Audio.c_str());
// Terminate the Genvid Native SDK.
Genvid_Terminate();
}
We need to unsubscribe every stream, event and command that we originally
subscribed to. Afterwards, we destroy all the streams and finish with a
Genvid_Terminate()
.