CryVideo
Project Status On hold
Project Duration 1 year
Software Used CryEngine 3
Languages Used C++, C

CryVideo is a system to publish your own video files into the free version of the CryEngine 3, which was significant at the time due there only being support for the proprietary Scaleform GFx codec that wasn't available to the public at large. 

Initially I wasn't entirely sure how to incorporate a proper video decoder into the engine, so I chose a very basic approach by playing a series of compressed dds frames instead that were extracted from a video and using an accompanying separate audio file.

The result was the first version of CryVideo which I officially announced with the following trailer:


While I managed to make the video and audio mostly sync, the performance impact and memory overhead of loading so many dds files was dramatic and thus the result wasn't all that great. The cumbersome workflow only added to it, so I decided to go ahead and figure out how to include a proper decoder into the engine. I ended up choosing the VP8/WebM codec.

The next version of CryVideo was already a lot better:

The consequences of implementing a decoder were smoother framerates, drastically reduced file sizes and a much, much easier workflow.

Ultimately the project was shelved due a lack of interest on my part to see it to the end, but a Beta version got released to a few testers and received positive feedback.


CryVideo Library

The CryVideo library solution in Visual Studio


The CryVideo.dll library acted as the high level link between the engine and the low level decoder libraries. It hosted the CryVideo initialization code, managed video playback, registered Flow Graph Nodes, communicated with the decoder libraries and handled synchronization of video and audio.

The initialization function:

bool CCryVideo::Init(ISystem *pSystem, IGameFramework *pGameFramework, IGameToEditorInterface *pGameToEditorInterface)
{
	//0.9.8: Dlls moved to CryVideo directory
	if (m_bIs64Bit)
		SetDllDirectory("Bin64\\CryVideo");
	else
		SetDllDirectory("Bin32\\CryVideo");


	started = false;
	m_bMenuRendered = false;

	gEnv = pSystem->GetGlobalEnvironment();
	m_pFramework = pGameFramework;

	gEnv->pRenderer->RegisterCaptureFrame(this);
	pGameToEditorIface = pGameToEditorInterface;

	//Timer Manager
	if (!m_pTimerManager)
		m_pTimerManager = new CTimerManager();

	//Initialize flow nodes (CryVideoPlayer)
	if (IFlowSystem *pFlow = m_pFramework->GetIFlowSystem())
	{
		CG2AutoRegFlowNodeBase *pFactory = CG2AutoRegFlowNodeBase::m_pFirst;

		while (pFactory)
		{
			pFlow->RegisterType(pFactory->m_sClassName, pFactory);
			pFactory = pFactory->m_pNext;
		}
	}

	//Init OpenCV plugin
	if (m_hCryVideoOpenCVHandle)
		CryFreeLibrary(m_hCryVideoOpenCVHandle);

	m_hCryVideoOpenCVHandle = CryLoadLibrary("CryVideo\\CryVideoOpenCV.dll");
	if (!m_hCryVideoOpenCVHandle)
		GameWarning("CryVideoOpenCV.DLL Loading Failed");

	//Init WebM plugin
	if (m_hCryVideoWebMHandle)
		CryFreeLibrary(m_hCryVideoWebMHandle);

	m_hCryVideoWebMHandle = CryLoadLibrary("CryVideo\\CryVideoWebM.dll");
	if (!m_hCryVideoWebMHandle)
		GameWarning("CryVideoWebM.dll Loading Failed");

	//Test if RGB8 is available
	int testTextureId = m_pFramework->GetIUIDraw()->CreateTexture("Textures\\defaults\\black.tif", false);
	ITexture *pTestTexture = gEnv->pRenderer->EF_GetTextureByID(testTextureId);

	unsigned char* textureData = pTestTexture->GetData32(0, 0, (uint8*)0, eTF_A8R8G8B8);

	int finalTestTextureId = gEnv->pRenderer->DownLoadToVideoMemory(textureData, pTestTexture->GetWidth(), pTestTexture->GetHeight(), eTF_A8R8G8B8, eTF_R8G8B8, 1, false);
	ITexture *pFinalTestTexture = gEnv->pRenderer->EF_GetTextureByID(finalTestTextureId);

	if (pFinalTestTexture->IsTextureLoaded())
        {
	        isRGB8Usable = true;
                CryLogAlways("CryVideo: RGB8 is available!");
        }
	else
        {
                isRGB8Usable = false;
                CryLogAlways("CryVideo: RGB8 is NOT available!");
        }

        //Cleanup test textures
	m_pFramework->GetIUIDraw()->DeleteTexture(testTextureId);
	m_pFramework->GetIUIDraw()->DeleteTexture(finalTestTextureId);

	//Register CVars
	gEnv->pConsole->Register("cv_useLevelAsMenuBg", &cv_useLevelAsMenuBg, 0, VF_NULL, "use level as menu background");
	cv_levelMenuBgMapName = gEnv->pConsole->RegisterString("cv_levelMenuBgMapName", "", VF_NULL, "level menu map name");

	gEnv->pConsole->Register("cv_useVideoAsMenuBg", &cv_useVideoAsMenuBg, 0, VF_NULL, "use video as menu background");
	cv_videoMenuBgVideoName = gEnv->pConsole->RegisterString("cv_videoMenuBgVideoName", "", VF_NULL, "video menu video name");

	cv_startingLevel = gEnv->pConsole->RegisterString("cv_startingLevel", "", VF_CHEAT | VF_DUMPTODISK | VF_READONLY, "default starting level when running the launcher");

	gEnv->pConsole->Register("cv_renderToSecondWindow", &cv_renderToSecondWindow, 0, VF_NULL, "render all videos to a separate window for debugging");
	gEnv->pConsole->Register("cv_forceManualConversion", &cv_forceManualConversion, 0, VF_NULL, "force manual color conversion for debugging");
	gEnv->pConsole->Register("cv_swapBGRA", &cv_swapBGRA, -1, VF_CHEAT | VF_DUMPTODISK | VF_READONLY, "-1=Autodetect(Default); 0=BGRA; 1=RGBA");

	//Load CryVideo.cfg config file
	gEnv->pConsole->ExecuteString("exec CryVideo.cfg");

	//Swap BGRA Autodetect (CryEngine's DX9 renderer expects textures to be in BGRA ; the DX11 one however requires RGBA)
	ICVar* swapChannels = gEnv->pConsole->GetCVar("cv_swapBGRA");
	if (swapChannels)
	{
		if (swapChannels->GetIVal() == -1)
		{
			if (gEnv->pRenderer->GetRenderType() == eRT_DX11)
                        {
				swapChannels->ForceSet("1");
                                CryLogAlways("CryVideo: DX11 --> RGBA color channel order!");
                        }
			else
                        {
				swapChannels->ForceSet("0");
                                CryLogAlways("CryVideo: DX9 --> BGRA color channel order!");
                        }
		}
	}

	//Create global video list on startup
	CreateGlobalVideoList();

	//Start audio timer
	m_LastSoundUpdate = gEnv->pTimer->GetAsyncCurTime();

	started = true;
	return true;
}


Excerpt from the VideoManager class which handles playback control:

//////////////////////////////////////////////////////////////////////////
bool CVideoManager::PlaySingleVideo(string videoName, bool ingame, string ingameAudio, float ingameAudioVolume, IEntity* ingameVideoEntity, int slot, int subMtlId, int texSlot, bool loop, bool keepLastFrame)
{
	CryLogAlways("VideoManager: Play Single Video!");
	if (!IsEnabled())
	{
		isPlaying = false;
		isPaused = false;
		return false;
	}

	assert(m_pGameFramework != NULL);
	if (m_pGameFramework == NULL)
	{
		return false;
	}

	assert(m_pUiDraw != NULL);
	if (m_pUiDraw == NULL)
	{
		return false;
	}

	IRenderer* pRenderer = gEnv->pRenderer;
	assert(pRenderer != NULL);
	if (pRenderer == NULL)
	{
		return false;
	}

	//Find video info
	bool bFound = false;
	int iFoundIndex = 0;

	if (!g_pCryVideo->m_globalVideoList.empty())
	{
		for (int i = 0; i < g_pCryVideo->m_globalVideoList.size(); i++)
		{
			SVideoInfo info = g_pCryVideo->m_globalVideoList[i];
			if (info.videoName.MakeLower() == videoName.MakeLower())
			{
				//Detect right game folder
				const char* gameFolder;
				ICVar *pVar = gEnv->pConsole->GetCVar("sys_game_folder");
				if (pVar)
					gameFolder = pVar->GetString();
				else
					gameFolder = "Game";

				//Init video file
				char fileName[255];
				sprintf(fileName, "%s/Videos/Library/%s", gameFolder, videoName.MakeLower());

				string fileNameString = fileName;
				bool success = false;

				if (fileNameString.substr(fileNameString.size() - 5, 5) == ".webm")
				{
					videoFormat = 2;
					success = m_pCryVideoOpenCV->LoadVideoFile(fileName, videoFormat);
					if (!success)
					{
						GameWarning("CryVideo: WebM file '%s' couldn't be opened. Retrying!", videoName);
						success = m_pCryVideoOpenCV->LoadVideoFile(fileName, videoFormat);
					}
				}
				else
				{
					videoFormat = 1;
					success = m_pCryVideoOpenCV->LoadVideoFile(fileName, videoFormat);
					if (!success)
					{
						GameWarning("CryVideo: Video file '%s' couldn't be opened. Retrying!", videoName);
						success = m_pCryVideoOpenCV->LoadVideoFile(fileName, videoFormat);
					}
				}

				if (!success)
				{
					GameWarning("CryVideo: Video file '%s' couldn't be opened. Check file?", videoName);
				}
				else
				{
					bFound = true;
					iFoundIndex = i;
				}

				break;
			}
		}
	}

	if (bFound)
	{
		//Set global values
		pIngameVideoTargetEntity = ingameVideoEntity;

		ingameVideoMaterialSlotId = slot;
		ingameVideoSubMtlId = subMtlId;
		ingameVideoTexSlot = texSlot;

		ingameAudioFile = ingameAudio;
		ingameAudioVol = ingameAudioVolume;
		isIngameVideo = ingame;
		isLoopedVideo = loop;
		freezeOnLastFrame = keepLastFrame;

		StartVideo(iFoundIndex);

		if (isPlaying)
			return true;
	}
	else
	{
		GameWarning("CryVideo: Video '%s' couldn't be played! Skipping...", videoName);
	}

	return false;
}
//////////////////////////////////////////////////////////////////////////
void CVideoManager::StartVideo(int videoIndex)
{
	IRenderer* pRenderer = gEnv->pRenderer;
	assert(pRenderer != NULL);
	if (pRenderer == NULL)
	{
		return;
	}

	//Set values
	SVideoInfo info = g_pCryVideo->m_globalVideoList[videoIndex];

	pIngameVideoTargetTex = NULL;
	frameCounter = 0;
	videoName = info.videoName;

	//1 second black screen time
	blackFramesCounter = int_round(gEnv->pTimer->GetFrameRate());

	videoWidth = info.videoWidth;
	videoHeight = info.videoHeight;
	xPos = info.posX;
	yPos = info.posY;
	pSoundId = NULL;

	//Get video properties from file
	frameEndIndex = m_pCryVideoOpenCV->GetFrameCount();
	frameRate = m_pCryVideoOpenCV->GetVideoFrameRate();
	frameWidth = m_pCryVideoOpenCV->GetVideoWidth();
	frameHeight = m_pCryVideoOpenCV->GetVideoHeight();

	desiredFrameUpdateTime = 1.0f / frameRate;
	CryLogAlways("VideoManager: Desired Frame Update Time: %f", desiredFrameUpdateTime);

	//Check sound type
	string szSoundFilePath;

	if (info.soundFile == "Default")
		szSoundFilePath = "videos/library/TempAudio/CryVideo_" + info.videoName + ".mp2";
	else
		szSoundFilePath = "videos/library/" + info.soundFile;

	if (info.allowSkip == 1)
	{
		isSkippable = true;
	}
	else
	{
		isSkippable = false;
	}

	//Don't allow skip on first start
	if (firstStart && !g_pCryVideo->m_bMenuRendered)
	{
		isSkippable = false;
		firstStartCounter = int_round(frameRate);
	}

	//Create master texture
	CryLogAlways("VideoManager: CreateMasterTexture!");
	char* imageData = m_pCryVideoOpenCV->StartPlayback();

	if (imageData != NULL)
	{
		bool success = false;
		if (g_pCryVideo->IsRGB8Usable())
		{
			master_texture_id = gEnv->pRenderer->DownLoadToVideoMemory(reinterpret_cast<unsigned char*> (imageData), frameWidth, frameHeight, eTF_R8G8B8, eTF_R8G8B8, 1, false);
		}
		else
		{
			master_texture_id = gEnv->pRenderer->DownLoadToVideoMemory(reinterpret_cast<unsigned char*> (imageData), frameWidth, frameHeight, eTF_A8R8G8B8, eTF_A8R8G8B8, 1, false);
		}

		ITexture *pTexture = gEnv->pRenderer->EF_GetTextureByID(master_texture_id);
		if (pTexture)
			if (pTexture->IsTextureLoaded())
				success = true;

		if (!success)
		{
			GameWarning("CryVideo: Critical Error. Texture format not recognized. Check video file '%s' !", videoName);
			StopVideo();
			return;
		}
	}
	else
	{
		GameWarning("CryVideo: Critical Error. Frame couldn't be retrieved. Check video file '%s' !", videoName);
		StopVideo();
		return;
	}

	//Start audio
	if (!isIngameVideo) //Playback in main menu
	{
		if (szSoundFilePath != "" && szSoundFilePath != "videos/library/")
		{
			ISound *pSound = gEnv->pSoundSystem->CreateSound(szSoundFilePath.c_str(), FLAG_SOUND_2D | FLAG_SOUND_MOVIE | FLAG_SOUND_PRECACHE_LOAD_SOUND);
			if (pSound)
			{
				pSoundId = pSound->GetId();

				if (!isLoopedVideo)
					pSound->GetInterfaceDeprecated()->SetLoopMode(false);

				if (firstStart && !g_pCryVideo->m_bMenuRendered)
				{
					pSound->Play(0.0f); //Play silent audio if first start
				}
				else
				{
					pSound->Play();
				}
			}
		}
	}
	else //Playback ingame (fullscreen or on entities)
	{
		if (pIngameVideoTargetEntity && szSoundFilePath != "" && szSoundFilePath != "videos/library/")
		{
			IEntitySoundProxy *pSoundProxy = (IEntitySoundProxy *)pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND);
			if (!pSoundProxy)
				pSoundProxy = (IEntitySoundProxy *)pIngameVideoTargetEntity->CreateProxy(ENTITY_PROXY_SOUND);

			pSoundProxy->StopAllSounds();

			ISound *pSound;

			if (pIngameVideoTargetEntity == g_pCryVideo->m_pFramework->GetClientActor()->GetEntity()) //If the selected entity is the player do fullscreen audio
			{
				pSound = gEnv->pSoundSystem->CreateSound(szSoundFilePath.c_str(), FLAG_SOUND_DEFAULT_3D | FLAG_SOUND_VOICE);
				pSound->SetSemantic(eSoundSemantic_Dialog);
			}
			else //3D audio for playback on world entities
			{
				pSound = gEnv->pSoundSystem->CreateSound(szSoundFilePath.c_str(), FLAG_SOUND_DEFAULT_3D);
				pSound->SetSemantic(eSoundSemantic_Living_Entity);
			}

			if (pSound)
			{
				pSoundId = pSound->GetId();

				if (!isLoopedVideo)
					pSound->GetInterfaceDeprecated()->SetLoopMode(false);

				pSoundProxy->PlaySound(pSound, Vec3(ZERO), FORWARD_DIRECTION, ingameAudioVol, false);
			}

		}

		if (pIngameVideoTargetEntity)
		{
			if (pIngameVideoTargetEntity != g_pCryVideo->m_pFramework->GetClientActor()->GetEntity()) //Playback on an entity, get material
			{
				IEntityRenderProxy* pRenderProxy((IEntityRenderProxy*)pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_RENDER));
				if (pRenderProxy)
				{
					IMaterial* pMtl(pRenderProxy->GetRenderMaterial(ingameVideoMaterialSlotId));
					if (pMtl)
					{
						pMtl = pMtl->GetSafeSubMtl(ingameVideoSubMtlId);
						if (pMtl)
						{
							const SShaderItem& shaderItem(pMtl->GetShaderItem());
							if (shaderItem.m_pShaderResources)
							{
								SEfResTexture* pTex = shaderItem.m_pShaderResources->GetTexture(ingameVideoTexSlot);
								if (pTex)
								{
									pIngameVideoTargetTex = pTex;
									pDefaultTexture = pTex->m_Sampler.m_pITex;

									ITexture *pNewTexture = pRenderer->EF_GetTextureByID(master_texture_id);
									if (pNewTexture)
									{
										pIngameVideoTargetTex->m_Sampler.m_pITex = pNewTexture; //Overwrite texture in the selected material slot with first video frame
									}
								}
							}
						}
					}
				}
			}
		}
	}

	lastVideoFinishedReason = "None";
	isPlaying = true;
	isPaused = false;
	scheduleUnpause = false;

	//Schedule timer
	if (m_TimerID)
		g_pCryVideo->GetTimerManager()->KillTimer(m_TimerID);

	m_TimerID = g_pCryVideo->GetTimerManager()->CreateTimer(this, desiredFrameUpdateTime, true, true);
}
/////////////////////////////////////////////////////////////////////////
void CVideoManager::StopVideo()
{
	if (gEnv->IsDedicated())
		return;

	if (!IsEnabled() || !isPlaying)
		return;

	stopVideo = true;
	isLoopedVideo = false;

	OnVideoTimer(m_TimerID); //Stop video immediately
}
/////////////////////////////////////////////////////////////////////////
void CVideoManager::PauseVideo()
{
	if (gEnv->IsDedicated())
		return;

	if (!IsEnabled() || !isPlaying || isPaused || !isIngameVideo)
		return;

	isPaused = true;

	if (pIngameVideoTargetEntity && pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND) && pSoundId)
	{
		IEntitySoundProxy *pSoundProxy = (IEntitySoundProxy *)pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND);
		if (pSoundProxy)
		{
			ISound *pSound = pSoundProxy->GetSound(pSoundId);
			if (pSound)
			{
				pSound->SetPaused(true);
			}
		}
	}
}
/////////////////////////////////////////////////////////////////////////
void CVideoManager::ResumeVideo()
{
	if (gEnv->IsDedicated())
		return;

	if (!IsEnabled() || !isPlaying || !isPaused || !isIngameVideo)
		return;

	isPaused = false;

	if (pIngameVideoTargetEntity && pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND) && pSoundId)
	{
		IEntitySoundProxy *pSoundProxy = (IEntitySoundProxy *)pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND);
		if (pSoundProxy)
		{
			ISound *pSound = pSoundProxy->GetSound(pSoundId);
			if (pSound)
			{
				pSound->SetPaused(false);
			}
		}
	}

	OnVideoTimer(m_TimerID);
}
//////////////////////////////////////////////////////////////////////////


These functions could also get invoked directly by the Flow Graph Node.

The Flow Graph Node

The Flow Graph Node which allows you to play movies both fullscreen and on ingame surfaces such as a monitor


The Video:CryVideoPlayer Node was a core feature of CryVideo to control movie playback inside the game. Many exposed options gave the level scripter suitable control over the movie playback such as adjusting the audio volume in real time and pausing/resuming the video whenever needed.


FlowCryVideoPlayer.
cpp defines the class CFlowCryVideoPlayer as shown:

#include "StdAfx.h"
#include "Nodes/G2FlowBaseNode.h"
#include "UIDraw/UIDraw.h"
#include "HUD/CryVideo/VideoManager.h"
#include "CryVideo.h"
#include "Game.h"
#include "IRenderer.h"

class CFlowCryVideoPlayer : public CFlowBaseNode<eNCT_Instanced>, public IGameFrameworkListener
{
private:
	SActivationInfo m_ActInfo;
	IRenderer* m_pRenderer;
	CVideoManager* m_pVideoManager;
	bool m_bEnabled;
	bool m_bPaused;
	bool m_bLooped;

public:
	CFlowCryVideoPlayer(SActivationInfo * pActInfo)
	{
		m_pRenderer = gEnv->pRenderer;
		m_pVideoManager = NULL;
		m_bEnabled = false;
		m_bPaused = false;
		m_bLooped = false;
	}

	~CFlowCryVideoPlayer()
	{
		if (g_pCryVideo && g_pCryVideo->m_pFramework)
			g_pCryVideo->m_pFramework->UnregisterListener(this);

		SAFE_DELETE(m_pVideoManager);
	}

	void Serialize(SActivationInfo* pActInfo, TSerialize ser)
	{
		ser.Value("m_bEnabled", m_bEnabled);
		ser.Value("m_bPaused", m_bPaused);
		ser.Value("m_bLooped", m_bLooped);

		if (ser.IsReading())
		{
			m_ActInfo = *pActInfo;
			//Listener
			if (g_pCryVideo && g_pCryVideo->m_pFramework && m_bEnabled && !m_bPaused && m_pVideoManager->IsPlaying() && !m_pVideoManager->IsPaused())
				g_pCryVideo->m_pFramework->RegisterListener(this, "FlowNode_CryVideoPlayer", eFLPriority_Default);
		}
	}

	enum EInputPorts
	{
		EIP_Play = 0,
		EIP_Stop,
		EIP_Pause,
		EIP_Resume,
		EIP_Slot,
		EIP_SubMtlId,
		EIP_TexSlot,
		EIP_VideoName,
		EIP_AudioVolume,
		EIP_Looped,
		EIP_KeepLastFrame,
	};

	enum EOutputPorts
	{
		EOP_OnPlay = 0,
		EOP_OnStop,
		EOP_OnPause,
		EOP_OnResume,
		EOP_OnVideoNotFound,
	};

	virtual void GetConfiguration(SFlowNodeConfig& config)
	{
		static const SInputPortConfig inputs[] = {
			InputPortConfig_Void("Play", _HELP("Start playback")),
			InputPortConfig_Void("Stop", _HELP("Stop playback")),
			InputPortConfig_Void("Pause", _HELP("Pause playback")),
			InputPortConfig_Void("Resume", _HELP("Resume playback")),
			InputPortConfig<int>("Slot", 0,  _HELP("Material Slot (e.g. to use CryVideoPlayer on dynamic textures)")),
			InputPortConfig<int>("SubMtlId", 0,  _HELP("Sub Material Id (e.g. to use CryVideoPlayer on dynamic textures)")),
			InputPortConfig<int>("TexSlot", 0,  _HELP("Texture Slot (e.g. to use CryVideoPlayer on dynamic textures)")),
			InputPortConfig<string>("VideoName", _HELP("Name of video to play"), 0, _UICONFIG("enum_global:video")), //Patch 0.9.8: Changed
			InputPortConfig<float>("Volume", 1.0f,  _HELP("Volume (dynamic)")),
			InputPortConfig<bool>("Loop", false,  _HELP("Video is looped")),
			InputPortConfig<bool>("KeepLastFrame", false,  _HELP("The video stops on the last frame. Can be used together with loop to create a seamless looping video")),
			{ 0 }
		};

		static const SOutputPortConfig outputs[] =
		{
			OutputPortConfig_Void("OnPlay",	_HELP("Triggered once the video started")),
			OutputPortConfig<bool>("OnStop",	_HELP("Triggered once the video stopped. True if the video was finished, false if skipped")),
			OutputPortConfig_Void("OnPause",	_HELP("Triggered once the video is paused")),
			OutputPortConfig_Void("OnResume",	_HELP("Triggered once the video resumes")),
			OutputPortConfig_Void("OnVideoNotFound",	_HELP("Triggered when video file was not found")),
			{ 0 }
		};

		config.pInputPorts = inputs;
		config.pOutputPorts = outputs;
		config.sDescription = _HELP("CryVideo player node");
		config.nFlags |= EFLN_TARGET_ENTITY;
		config.SetCategory(EFLN_APPROVED);
	}

	virtual void ProcessEvent(EFlowEvent event, SActivationInfo *pActInfo)
	{
		switch (event)
		{
		case eFE_Initialize:
		{
			m_ActInfo = *pActInfo;
			m_pRenderer = gEnv->pRenderer;

			if (m_pVideoManager == NULL)
				m_pVideoManager = new CVideoManager();
		}
		break;

		case eFE_Activate:
		{
			if (IsPortActive(pActInfo, EIP_Play) && !m_bEnabled && !m_bPaused)
			{
				IUIDraw *pDraw;
				if (g_pCryVideo)
					if (IGameFramework *pFW = g_pCryVideo->m_pFramework)
						pDraw = pFW->GetIUIDraw();

				if (!pDraw) return;

				string videoName = GetPortString(pActInfo, EIP_VideoName);
				float volume = GetPortFloat(pActInfo, EIP_AudioVolume);

				//0.9.6: Loop and KeepLastFrame option added
				m_bLooped = GetPortBool(pActInfo, EIP_Looped);
				bool keepLastFrame = GetPortBool(pActInfo, EIP_KeepLastFrame);

				int slot = GetPortInt(pActInfo, EIP_Slot);
				int subMtlId = GetPortInt(pActInfo, EIP_SubMtlId);
				int texSlot = GetPortInt(pActInfo, EIP_TexSlot);

				if (!videoName || videoName == "")
				{
					ActivateOutput(pActInfo, EOP_OnVideoNotFound, true);
					return;
				}

				IEntity* pEntity = pActInfo->pEntity;
				if (pEntity)
				{
					bool success = m_pVideoManager->PlaySingleVideo(videoName, true, "", 1.0f, pEntity, slot, subMtlId, texSlot, false, keepLastFrame);
					if (success)
					{
						m_bEnabled = true;
						if (g_pCryVideo && g_pCryVideo->m_pFramework)
							g_pCryVideo->m_pFramework->RegisterListener(this, "FlowNode_CryVideoPlayer", eFLPriority_Default);

						ActivateOutput(pActInfo, EOP_OnPlay, true);
						pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, true);
					}
					else
					{
						ActivateOutput(pActInfo, EOP_OnVideoNotFound, true);
					}
				}
			}
			if (IsPortActive(pActInfo, EIP_Stop) && m_bEnabled)
			{
				m_bEnabled = false;
				m_bPaused = false;

				if (g_pCryVideo && g_pCryVideo->m_pFramework)
					g_pCryVideo->m_pFramework->UnregisterListener(this);

				pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, false);

				if (!m_pVideoManager->IsPlaying())
				{
					ActivateOutput(pActInfo, EOP_OnStop, false);
				}
				else
				{
					m_pVideoManager->StopVideo();
					ActivateOutput(pActInfo, EOP_OnStop, false);
				}
			}

			if (IsPortActive(pActInfo, EIP_Pause) && m_bEnabled && !m_bPaused)
			{
				m_bPaused = true;

				if (g_pCryVideo && g_pCryVideo->m_pFramework)
					g_pCryVideo->m_pFramework->UnregisterListener(this);

				pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, false);

				if (m_pVideoManager->IsPlaying() && !m_pVideoManager->IsPaused())
				{
					m_pVideoManager->PauseVideo();
					ActivateOutput(pActInfo, EOP_OnPause, true);
				}
			}

			if (IsPortActive(pActInfo, EIP_Resume) && m_bEnabled && m_bPaused)
			{
				m_bPaused = false;

				if (m_pVideoManager->IsPlaying() && m_pVideoManager->IsPaused())
				{
					if (g_pCryVideo && g_pCryVideo->m_pFramework)
						g_pCryVideo->m_pFramework->RegisterListener(this, "FlowNode_CryVideoPlayer", eFLPriority_Default);

					pActInfo->pGraph->SetRegularlyUpdated(pActInfo->myID, true);

					m_pVideoManager->ResumeVideo();
					ActivateOutput(pActInfo, EOP_OnResume, true);
				}
			}
		}
		break;

		case eFE_Update:
		{
			if (m_bEnabled && !m_bPaused)
			{
				if (m_pVideoManager->IsPlaying() && !m_pVideoManager->IsPaused())
				{
					if (m_pVideoManager->pIngameVideoTargetEntity && m_pVideoManager->pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND) && m_pVideoManager->pSoundId)
					{
						IEntitySoundProxy *pSoundProxy = (IEntitySoundProxy *)m_pVideoManager->pIngameVideoTargetEntity->GetProxy(ENTITY_PROXY_SOUND);
						if (pSoundProxy)
						{
							ISound *pSound = pSoundProxy->GetSound(m_pVideoManager->pSoundId);
							if (pSound)
							{
								float newVolume = GetPortFloat(pActInfo, EIP_AudioVolume);
								pSound->GetInterfaceExtended()->SetVolume(newVolume);
							}
						}
					}
				}
			}
		}
		break;
		}
	}

	IFlowNodePtr Clone(SActivationInfo * pActInfo)
	{
		return new CFlowCryVideoPlayer(pActInfo);
	}

	virtual void GetMemoryUsage(ICrySizer * s) const
	{
		s->Add(*this);
	}

	////////////////////////////////////////////////////
	// ~IGameFrameworkListener
	virtual void OnSaveGame(ISaveGame* pSaveGame) {}
	virtual void OnLoadGame(ILoadGame* pLoadGame) {}
	virtual void OnLevelEnd(const char* nextLevel) {}
	virtual void OnActionEvent(const SActionEvent& event) {}
	virtual void OnPostUpdate(float fDeltaTime)
	{
		if (!m_bEnabled || m_bPaused) return;

		IUIDraw *pDraw;
		if (g_pCryVideo)
			if (IGameFramework *pFW = g_pCryVideo->m_pFramework)
				pDraw = pFW->GetIUIDraw();

		if (!pDraw) return;
		if (!m_pVideoManager) return;

		if (!m_pVideoManager->IsPlaying() && !m_pVideoManager->IsPaused())
		{
			if (m_pVideoManager->lastVideoFinishedReason == "Finished")
			{
				ActivateOutput(&m_ActInfo, EOP_OnStop, true);
			}
			else if (m_pVideoManager->lastVideoFinishedReason == "Skipped")
			{
				ActivateOutput(&m_ActInfo, EOP_OnStop, false);
			}

			m_bEnabled = false;
			m_bPaused = false;

			if (g_pCryVideo && g_pCryVideo->m_pFramework)
				g_pCryVideo->m_pFramework->UnregisterListener(this);

			m_ActInfo.pGraph->SetRegularlyUpdated(m_ActInfo.myID, false);

			//0.9.6: Loop and KeepLastFrame option added
			if (m_pVideoManager->lastVideoFinishedReason == "Finished" && m_bLooped)
			{
				string videoName = GetPortString(&m_ActInfo, EIP_VideoName);
				float volume = GetPortFloat(&m_ActInfo, EIP_AudioVolume);

				m_bLooped = GetPortBool(&m_ActInfo, EIP_Looped);
				bool keepLastFrame = GetPortBool(&m_ActInfo, EIP_KeepLastFrame);

				int slot = GetPortInt(&m_ActInfo, EIP_Slot);
				int subMtlId = GetPortInt(&m_ActInfo, EIP_SubMtlId);
				int texSlot = GetPortInt(&m_ActInfo, EIP_TexSlot);

				if (!videoName || videoName == "")
				{
					ActivateOutput(&m_ActInfo, EOP_OnVideoNotFound, true);
					return;
				}

				IEntity* pEntity = m_ActInfo.pEntity;
				if (pEntity)
				{
					bool success = m_pVideoManager->PlaySingleVideo(videoName, true, "", 1.0f, pEntity, slot, subMtlId, texSlot, false, keepLastFrame);
					if (success)
					{
						m_bEnabled = true;
						if (g_pCryVideo && g_pCryVideo->m_pFramework)
							g_pCryVideo->m_pFramework->RegisterListener(this, "FlowNode_CryVideoPlayer", eFLPriority_Default);

						ActivateOutput(&m_ActInfo, EOP_OnPlay, true);
						m_ActInfo.pGraph->SetRegularlyUpdated(m_ActInfo.myID, true);
					}
					else
					{
						ActivateOutput(&m_ActInfo, EOP_OnVideoNotFound, true);
					}
				}
			}
		}
	}
};


////////////////////////////////////////////////////
////////////////////////////////////////////////////
REGISTER_FLOW_NODE("Video:CryVideoPlayer", CFlowCryVideoPlayer);
Bonus Features

CryVideo shipped with a host of bonus features such as the ability to play a series of movies at boot time (think company logos) and also to use a video as the background of your main game menu:


Rather unrelated to video playback, but still nice to have:

CryVideo also gave you the option to use an actual game level as the background of your main menu. A default cutscene would play upon a successful load locking the camera in a static position and the menu would be overlayed after. It was possible to breathe life into the scenery with Flow Graphs to make your main menu background both dynamic and nice to look at.

In this example I setup a dynamic and constant Time of Day progression and NPCs who would do random activities such as driving and walking around at certain hours of the day: