Browse Source

Merge pull request #3998 from IvanSavenko/refactor_sdl_media

[1.6] Reorganize SDL sound/music/video handling
Ivan Savenko 1 year ago
parent
commit
adc3441bda
65 changed files with 2373 additions and 2130 deletions
  1. 6 6
      client/CGameInfo.h
  2. 11 21
      client/CMT.cpp
  3. 16 4
      client/CMakeLists.txt
  4. 0 753
      client/CMusicHandler.cpp
  5. 0 170
      client/CMusicHandler.h
  6. 3 1
      client/CPlayerInterface.cpp
  7. 0 713
      client/CVideoHandler.cpp
  8. 0 131
      client/CVideoHandler.h
  9. 2 1
      client/HeroMovementController.cpp
  10. 1 1
      client/adventureMap/CInGameConsole.cpp
  11. 1 1
      client/adventureMap/CInfoBar.cpp
  12. 2 1
      client/adventureMap/MapAudioPlayer.cpp
  13. 1 1
      client/adventureMap/TurnTimerWidget.cpp
  14. 1 1
      client/battle/BattleAnimationClasses.cpp
  15. 1 1
      client/battle/BattleEffectsController.cpp
  16. 10 2
      client/battle/BattleInterface.cpp
  17. 62 108
      client/battle/BattleInterfaceClasses.cpp
  18. 12 18
      client/battle/BattleInterfaceClasses.h
  19. 1 1
      client/battle/BattleObstacleController.cpp
  20. 1 1
      client/battle/BattleSiegeController.cpp
  21. 1 1
      client/battle/BattleStacksController.cpp
  22. 0 1
      client/battle/BattleWindow.cpp
  23. 2 1
      client/eventsSDL/InputHandler.cpp
  24. 1 1
      client/globalLobby/GlobalLobbyClient.cpp
  25. 1 1
      client/globalLobby/GlobalLobbyWidget.cpp
  26. 3 4
      client/lobby/CBonusSelection.cpp
  27. 1 2
      client/lobby/CSelectionBase.cpp
  28. 2 1
      client/lobby/OptionsTab.cpp
  29. 9 21
      client/mainmenu/CCampaignScreen.cpp
  30. 3 2
      client/mainmenu/CCampaignScreen.h
  31. 11 42
      client/mainmenu/CHighScoreScreen.cpp
  32. 4 6
      client/mainmenu/CHighScoreScreen.h
  33. 11 21
      client/mainmenu/CMainMenu.cpp
  34. 3 3
      client/mainmenu/CMainMenu.h
  35. 23 10
      client/mainmenu/CPrologEpilogVideo.cpp
  36. 2 0
      client/mainmenu/CPrologEpilogVideo.h
  37. 43 0
      client/media/CAudioBase.cpp
  38. 22 0
      client/media/CAudioBase.h
  39. 38 0
      client/media/CEmptyVideoPlayer.h
  40. 349 0
      client/media/CMusicHandler.cpp
  41. 95 0
      client/media/CMusicHandler.h
  42. 385 0
      client/media/CSoundHandler.cpp
  43. 78 0
      client/media/CSoundHandler.h
  44. 658 0
      client/media/CVideoHandler.cpp
  45. 110 0
      client/media/CVideoHandler.h
  46. 33 0
      client/media/IMusicPlayer.h
  47. 35 0
      client/media/ISoundPlayer.h
  48. 54 0
      client/media/IVideoPlayer.h
  49. 1 1
      client/widgets/Buttons.cpp
  50. 0 1
      client/widgets/Images.cpp
  51. 148 0
      client/widgets/VideoWidget.cpp
  52. 65 0
      client/widgets/VideoWidget.h
  53. 3 1
      client/windows/CCastleInterface.cpp
  54. 1 1
      client/windows/CPuzzleWindow.cpp
  55. 3 3
      client/windows/CSpellWindow.cpp
  56. 5 25
      client/windows/CTutorialWindow.cpp
  57. 2 5
      client/windows/CTutorialWindow.h
  58. 0 1
      client/windows/CWindowObject.cpp
  59. 5 12
      client/windows/GUIClasses.cpp
  60. 2 1
      client/windows/GUIClasses.h
  61. 2 1
      client/windows/settings/GeneralOptionsTab.cpp
  62. 24 21
      lib/Languages.h
  63. 2 4
      lib/filesystem/ResourcePath.cpp
  64. 1 0
      lib/filesystem/ResourcePath.h
  65. 1 0
      lib/json/JsonValidator.cpp

+ 6 - 6
client/CGameInfo.h

@@ -36,21 +36,21 @@ class CMap;
 VCMI_LIB_NAMESPACE_END
 
 class CMapHandler;
-class CSoundHandler;
-class CMusicHandler;
+class ISoundPlayer;
+class IMusicPlayer;
 class CursorHandler;
-class IMainVideoPlayer;
+class IVideoPlayer;
 class CServerHandler;
 
 //a class for non-mechanical client GUI classes
 class CClientState
 {
 public:
-	CSoundHandler * soundh;
-	CMusicHandler * musich;
+	ISoundPlayer * soundh;
+	IMusicPlayer * musich;
 	CConsoleHandler * consoleh;
 	CursorHandler * curh;
-	IMainVideoPlayer * videoh;
+	IVideoPlayer * videoh;
 };
 extern CClientState * CCS;
 

+ 11 - 21
client/CMT.cpp

@@ -14,11 +14,13 @@
 
 #include "CGameInfo.h"
 #include "mainmenu/CMainMenu.h"
+#include "media/CEmptyVideoPlayer.h"
+#include "media/CMusicHandler.h"
+#include "media/CSoundHandler.h"
+#include "media/CVideoHandler.h"
 #include "gui/CursorHandler.h"
 #include "eventsSDL/InputHandler.h"
 #include "CPlayerInterface.h"
-#include "CVideoHandler.h"
-#include "CMusicHandler.h"
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 #include "CServerHandler.h"
@@ -292,10 +294,8 @@ int main(int argc, char * argv[])
 	{
 		//initializing audio
 		CCS->soundh = new CSoundHandler();
-		CCS->soundh->init();
 		CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float());
 		CCS->musich = new CMusicHandler();
-		CCS->musich->init();
 		CCS->musich->setVolume((ui32)settings["general"]["music"].Float());
 		logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff());
 	}
@@ -396,20 +396,13 @@ int main(int argc, char * argv[])
 //plays intro, ends when intro is over or button has been pressed (handles events)
 void playIntro()
 {
-	auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK"));
-	int sound = CCS->soundh->playSound(audioData);
-	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO))
-	{
-		audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK"));
-		sound = CCS->soundh->playSound(audioData);
-		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO))
-		{
-			audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK"));
-			sound = CCS->soundh->playSound(audioData);
-			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, EVideoType::INTRO);
-		}
-	}
-	CCS->soundh->stopSound(sound);
+	if(!CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK")))
+		return;
+
+	if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK")))
+		return;
+
+	CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK"));
 }
 
 static void mainLoop()
@@ -457,9 +450,6 @@ static void mainLoop()
 		// cleanup, mostly to remove false leaks from analyzer
 		if(CCS)
 		{
-			CCS->musich->release();
-			CCS->soundh->release();
-
 			delete CCS->consoleh;
 			delete CCS->curh;
 			delete CCS->videoh;

+ 16 - 4
client/CMakeLists.txt

@@ -75,6 +75,11 @@ set(client_SRCS
 	mapView/MapViewModel.cpp
 	mapView/mapHandler.cpp
 
+	media/CAudioBase.cpp
+	media/CMusicHandler.cpp
+	media/CSoundHandler.cpp
+	media/CVideoHandler.cpp
+
 	render/CAnimation.cpp
 	render/CBitmapHandler.cpp
 	render/CDefFile.cpp
@@ -126,6 +131,7 @@ set(client_SRCS
 	widgets/CArtifactsOfHeroMarket.cpp
 	widgets/CArtifactsOfHeroBackpack.cpp
 	widgets/RadialMenu.cpp
+	widgets/VideoWidget.cpp
 	widgets/markets/CAltarArtifacts.cpp
 	widgets/markets/CAltarCreatures.cpp
 	widgets/markets/CArtifactsBuying.cpp
@@ -163,11 +169,9 @@ set(client_SRCS
 
 	CGameInfo.cpp
 	CMT.cpp
-	CMusicHandler.cpp
 	CPlayerInterface.cpp
 	PlayerLocalState.cpp
 	CServerHandler.cpp
-	CVideoHandler.cpp
 	Client.cpp
 	ClientCommandManager.cpp
 	GameChatHandler.cpp
@@ -260,6 +264,15 @@ set(client_HEADERS
 	mapView/MapViewModel.h
 	mapView/mapHandler.h
 
+	media/CAudioBase.h
+	media/CEmptyVideoPlayer.h
+	media/CMusicHandler.h
+	media/CSoundHandler.h
+	media/CVideoHandler.h
+	media/IMusicPlayer.h
+	media/ISoundPlayer.h
+	media/IVideoPlayer.h
+
 	render/CAnimation.h
 	render/CBitmapHandler.h
 	render/CDefFile.h
@@ -320,6 +333,7 @@ set(client_HEADERS
 	widgets/CArtifactsOfHeroMarket.h
 	widgets/CArtifactsOfHeroBackpack.h
 	widgets/RadialMenu.h
+	widgets/VideoWidget.h
 	widgets/markets/CAltarArtifacts.h
 	widgets/markets/CAltarCreatures.h
 	widgets/markets/CArtifactsBuying.h
@@ -357,11 +371,9 @@ set(client_HEADERS
 
 	CGameInfo.h
 	CMT.h
-	CMusicHandler.h
 	CPlayerInterface.h
 	PlayerLocalState.h
 	CServerHandler.h
-	CVideoHandler.h
 	Client.h
 	ClientCommandManager.h
 	ClientNetPackVisitors.h

+ 0 - 753
client/CMusicHandler.cpp

@@ -1,753 +0,0 @@
-/*
- * CMusicHandler.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include <SDL_mixer.h>
-#include <SDL_timer.h>
-
-#include "CMusicHandler.h"
-#include "CGameInfo.h"
-#include "renderSDL/SDLRWwrapper.h"
-#include "eventsSDL/InputHandler.h"
-#include "gui/CGuiHandler.h"
-
-#include "../lib/GameConstants.h"
-#include "../lib/filesystem/Filesystem.h"
-#include "../lib/constants/StringConstants.h"
-#include "../lib/CRandomGenerator.h"
-#include "../lib/VCMIDirs.h"
-#include "../lib/TerrainHandler.h"
-
-
-#define VCMI_SOUND_NAME(x)
-#define VCMI_SOUND_FILE(y) #y,
-
-// sounds mapped to soundBase enum
-static const std::string sounds[] = {
-	"", // invalid
-	"", // todo
-	VCMI_SOUND_LIST
-};
-#undef VCMI_SOUND_NAME
-#undef VCMI_SOUND_FILE
-
-void CAudioBase::init()
-{
-	if (initialized)
-		return;
-
-	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1)
-	{
-		logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError());
-		return;
-	}
-
-	initialized = true;
-}
-
-void CAudioBase::release()
-{
-	if(!(CCS->soundh->initialized && CCS->musich->initialized))
-		Mix_CloseAudio();
-
-	initialized = false;
-}
-
-void CAudioBase::setVolume(ui32 percent)
-{
-	if (percent > 100)
-		percent = 100;
-
-	volume = percent;
-}
-
-void CSoundHandler::onVolumeChange(const JsonNode &volumeNode)
-{
-	setVolume((ui32)volumeNode.Float());
-}
-
-CSoundHandler::CSoundHandler():
-	listener(settings.listen["general"]["sound"]),
-	ambientConfig(JsonPath::builtin("config/ambientSounds.json"))
-{
-	listener(std::bind(&CSoundHandler::onVolumeChange, this, _1));
-
-	battleIntroSounds =
-	{
-		soundBase::battle00, soundBase::battle01,
-		soundBase::battle02, soundBase::battle03, soundBase::battle04,
-		soundBase::battle05, soundBase::battle06, soundBase::battle07
-	};
-}
-
-void CSoundHandler::init()
-{
-	CAudioBase::init();
-	if(ambientConfig["allocateChannels"].isNumber())
-		Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer());
-
-	if (initialized)
-	{
-		Mix_ChannelFinished([](int channel)
-		{
-			CCS->soundh->soundFinishedCallback(channel);
-		});
-	}
-}
-
-void CSoundHandler::release()
-{
-	if (initialized)
-	{
-		Mix_HaltChannel(-1);
-
-		for (auto &chunk : soundChunks)
-		{
-			if (chunk.second.first)
-				Mix_FreeChunk(chunk.second.first);
-		}
-	}
-
-	CAudioBase::release();
-}
-
-// Allocate an SDL chunk and cache it.
-Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache)
-{
-	try
-	{
-		if (cache && soundChunks.find(sound) != soundChunks.end())
-			return soundChunks[sound].first;
-
-		auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll();
-		SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
-		Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1);	// will free ops
-
-		if (cache)
-			soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))});
-
-		return chunk;
-	}
-	catch(std::exception &e)
-	{
-		logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what());
-		return nullptr;
-	}
-}
-
-Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache)
-{
-	try
-	{
-		std::vector<ui8> startBytes = std::vector<ui8>(data.first.get(), data.first.get() + std::min((si64)100, data.second));
-
-		if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end())
-			return soundChunksRaw[startBytes].first;
-
-		SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
-		Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1);	// will free ops
-
-		if (cache)
-			soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))});
-
-		return chunk;
-	}
-	catch(std::exception &e)
-	{
-		logGlobal->warn("Cannot get sound chunk: %s", e.what());
-		return nullptr;
-	}
-}
-
-int CSoundHandler::ambientDistToVolume(int distance) const
-{
-	const auto & distancesVector = ambientConfig["distances"].Vector();
-
-	if(distance >= distancesVector.size())
-		return 0;
-
-	int volume = static_cast<int>(distancesVector[distance].Integer());
-	return volume * (int)ambientConfig["volume"].Integer() / 100;
-}
-
-void CSoundHandler::ambientStopSound(const AudioPath & soundId)
-{
-	stopSound(ambientChannels[soundId]);
-	setChannelVolume(ambientChannels[soundId], volume);
-}
-
-uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound)
-{
-	if (!initialized || sound.empty())
-		return 0;
-
-	auto resourcePath = sound.addPrefix("SOUNDS/");
-
-	if (!CResourceHandler::get()->existsResource(resourcePath))
-		return 0;
-
-	auto data = CResourceHandler::get()->load(resourcePath)->readAll();
-
-	SDL_AudioSpec spec;
-	uint32_t audioLen;
-	uint8_t *audioBuf;
-	uint32_t miliseconds = 0;
-
-	if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr)
-	{
-		SDL_FreeWAV(audioBuf);
-		uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8;
-		uint32_t sampleCount = audioLen / sampleSize;
-		uint32_t sampleLen = sampleCount / spec.channels;
-		miliseconds = 1000 * sampleLen / spec.freq;
-	}
-
-	return miliseconds ;
-}
-
-// Plays a sound, and return its channel so we can fade it out later
-int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
-{
-	assert(soundID < soundBase::sound_after_last);
-	auto sound = AudioPath::builtin(sounds[soundID]);
-	logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName());
-
-	return playSound(sound, repeats, true);
-}
-
-int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache)
-{
-	if (!initialized || sound.empty())
-		return -1;
-
-	int channel;
-	Mix_Chunk *chunk = GetSoundChunk(sound, cache);
-
-	if (chunk)
-	{
-		channel = Mix_PlayChannel(-1, chunk, repeats);
-		if (channel == -1)
-		{
-			logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError());
-			if (!cache)
-				Mix_FreeChunk(chunk);
-		}
-		else if (cache)
-			initCallback(channel);
-		else
-			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
-	}
-	else
-		channel = -1;
-
-	return channel;
-}
-
-int CSoundHandler::playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats, bool cache)
-{
-	int channel = -1;
-	if (Mix_Chunk *chunk = GetSoundChunk(data, cache))
-	{
-		channel = Mix_PlayChannel(-1, chunk, repeats);
-		if (channel == -1)
-		{
-			logGlobal->error("Unable to play sound, error %s", Mix_GetError());
-			if (!cache)
-				Mix_FreeChunk(chunk);
-		}
-		else if (cache)
-			initCallback(channel);
-		else
-			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
-	}
-	return channel;
-}
-
-// Helper. Randomly select a sound from an array and play it
-int CSoundHandler::playSoundFromSet(std::vector<soundBase::soundID> &sound_vec)
-{
-	return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault()));
-}
-
-void CSoundHandler::stopSound(int handler)
-{
-	if (initialized && handler != -1)
-		Mix_HaltChannel(handler);
-}
-
-// Sets the sound volume, from 0 (mute) to 100
-void CSoundHandler::setVolume(ui32 percent)
-{
-	CAudioBase::setVolume(percent);
-
-	if (initialized)
-	{
-		setChannelVolume(-1, volume);
-
-		for (auto const & channel : channelVolumes)
-			updateChannelVolume(channel.first);
-	}
-}
-
-void CSoundHandler::updateChannelVolume(int channel)
-{
-	if (channelVolumes.count(channel))
-		setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100);
-	else
-		setChannelVolume(channel, getVolume());
-}
-
-// Sets the sound volume, from 0 (mute) to 100
-void CSoundHandler::setChannelVolume(int channel, ui32 percent)
-{
-	Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100);
-}
-
-void CSoundHandler::setCallback(int channel, std::function<void()> function)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-
-	auto iter = callbacks.find(channel);
-
-	//channel not found. It may have finished so fire callback now
-	if(iter == callbacks.end())
-		function();
-	else
-		iter->second.push_back(function);
-}
-
-void CSoundHandler::resetCallback(int channel)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-
-	callbacks.erase(channel);
-}
-
-void CSoundHandler::soundFinishedCallback(int channel)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-
-	if (callbacks.count(channel) == 0)
-		return;
-
-	// store callbacks from container locally - SDL might reuse this channel for another sound
-	// but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own
-	auto callback = callbacks.at(channel);
-	callbacks.erase(channel);
-
-	if (!callback.empty())
-	{
-		GH.dispatchMainThread([callback](){
-			for (auto entry : callback)
-				entry();
-		});
-	}
-}
-
-void CSoundHandler::initCallback(int channel)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-	assert(callbacks.count(channel) == 0);
-	callbacks[channel] = {};
-}
-
-void CSoundHandler::initCallback(int channel, const std::function<void()> & function)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-	assert(callbacks.count(channel) == 0);
-	callbacks[channel].push_back(function);
-}
-
-int CSoundHandler::ambientGetRange() const
-{
-	return static_cast<int>(ambientConfig["range"].Integer());
-}
-
-void CSoundHandler::ambientUpdateChannels(std::map<AudioPath, int> soundsArg)
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	std::vector<AudioPath> stoppedSounds;
-	for(auto & pair : ambientChannels)
-	{
-		const auto & soundId = pair.first;
-		const int channel = pair.second;
-
-		if(!vstd::contains(soundsArg, soundId))
-		{
-			ambientStopSound(soundId);
-			stoppedSounds.push_back(soundId);
-		}
-		else
-		{
-			int volume = ambientDistToVolume(soundsArg[soundId]);
-			channelVolumes[channel] = volume;
-			updateChannelVolume(channel);
-		}
-	}
-	for(auto soundId : stoppedSounds)
-	{
-		channelVolumes.erase(ambientChannels[soundId]);
-		ambientChannels.erase(soundId);
-	}
-
-	for(auto & pair : soundsArg)
-	{
-		const auto & soundId = pair.first;
-		const int distance = pair.second;
-
-		if(!vstd::contains(ambientChannels, soundId))
-		{
-			int channel = playSound(soundId, -1);
-			int volume = ambientDistToVolume(distance);
-			channelVolumes[channel] = volume;
-
-			updateChannelVolume(channel);
-			ambientChannels[soundId] = channel;
-		}
-	}
-}
-
-void CSoundHandler::ambientStopAllChannels()
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	for(auto ch : ambientChannels)
-	{
-		ambientStopSound(ch.first);
-	}
-	channelVolumes.clear();
-	ambientChannels.clear();
-}
-
-void CMusicHandler::onVolumeChange(const JsonNode &volumeNode)
-{
-	setVolume((ui32)volumeNode.Float());
-}
-
-CMusicHandler::CMusicHandler():
-	listener(settings.listen["general"]["music"])
-{
-	listener(std::bind(&CMusicHandler::onVolumeChange, this, _1));
-
-	auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) ->  bool
-	{
-		if(id.getType() != EResType::SOUND)
-			return false;
-
-		if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
-			return false;
-
-		logGlobal->trace("Found music file %s", id.getName());
-		return true;
-	});
-
-	for(const ResourcePath & file : mp3files)
-	{
-		if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat"))
-			addEntryToSet("battle", AudioPath::fromResource(file));
-		else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme"))
-			addEntryToSet("enemy-turn", AudioPath::fromResource(file));
-	}
-
-}
-
-void CMusicHandler::loadTerrainMusicThemes()
-{
-	for (const auto & terrain : CGI->terrainTypeHandler->objects)
-	{
-		addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename);
-	}
-}
-
-void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI)
-{
-	musicsSet[set].push_back(musicURI);
-}
-
-void CMusicHandler::init()
-{
-	CAudioBase::init();
-
-	if (initialized)
-	{
-		Mix_HookMusicFinished([]()
-		{
-			CCS->musich->musicFinishedCallback();
-		});
-	}
-}
-
-void CMusicHandler::release()
-{
-	if (initialized)
-	{
-		boost::mutex::scoped_lock guard(mutex);
-
-		Mix_HookMusicFinished(nullptr);
-		current->stop();
-
-		current.reset();
-		next.reset();
-	}
-
-	CAudioBase::release();
-}
-
-void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart)
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	if (current && current->isPlaying() && current->isTrack(musicURI))
-		return;
-
-	queueNext(this, "", musicURI, loop, fromStart);
-}
-
-void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart)
-{
-	playMusicFromSet(musicSet + "_" + entryID, loop, fromStart);
-}
-
-void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart)
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	auto selectedSet = musicsSet.find(whichSet);
-	if (selectedSet == musicsSet.end())
-	{
-		logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
-		return;
-	}
-
-	if (current && current->isPlaying() && current->isSet(whichSet))
-		return;
-
-	// in this mode - play random track from set
-	queueNext(this, whichSet, AudioPath(), loop, fromStart);
-}
-
-void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
-{
-	if (!initialized)
-		return;
-
-	next = std::move(queued);
-
-	if (current.get() == nullptr || !current->stop(1000))
-	{
-		current.reset(next.release());
-		current->play();
-	}
-}
-
-void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart)
-{
-	queueNext(std::make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
-}
-
-void CMusicHandler::stopMusic(int fade_ms)
-{
-	if (!initialized)
-		return;
-
-	boost::mutex::scoped_lock guard(mutex);
-
-	if (current.get() != nullptr)
-		current->stop(fade_ms);
-	next.reset();
-}
-
-void CMusicHandler::setVolume(ui32 percent)
-{
-	CAudioBase::setVolume(percent);
-
-	if (initialized)
-		Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100);
-}
-
-void CMusicHandler::musicFinishedCallback()
-{
-	// call music restart in separate thread to avoid deadlock in some cases
-	// It is possible for:
-	// 1) SDL thread to call this method on end of playback
-	// 2) VCMI code to call queueNext() method to queue new file
-	// this leads to:
-	// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
-	// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
-
-	GH.dispatchMainThread([this]()
-	{
-		boost::unique_lock lockGuard(mutex);
-		if (current.get() != nullptr)
-		{
-			// if music is looped, play it again
-			if (current->play())
-				return;
-			else
-				current.reset();
-		}
-
-		if (current.get() == nullptr && next.get() != nullptr)
-		{
-			current.reset(next.release());
-			current->play();
-		}
-	});
-}
-
-MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart):
-	owner(owner),
-	music(nullptr),
-	playing(false),
-	startTime(uint32_t(-1)),
-	startPosition(0),
-	loop(looped ? -1 : 1),
-	fromStart(fromStart),
-	setName(std::move(setName))
-{
-	if (!musicURI.empty())
-		load(std::move(musicURI));
-}
-MusicEntry::~MusicEntry()
-{
-	if (playing && loop > 0)
-	{
-		assert(0);
-		logGlobal->error("Attempt to delete music while playing!");
-		Mix_HaltMusic();
-	}
-
-	if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
-	{
-		assert(0);
-		logGlobal->error("Attempt to delete music while fading out!");
-		Mix_HaltMusic();
-	}
-
-	logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
-	if (music)
-		Mix_FreeMusic(music);
-}
-
-void MusicEntry::load(const AudioPath & musicURI)
-{
-	if (music)
-	{
-		logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
-		Mix_FreeMusic(music);
-		music = nullptr;
-	}
-
-	if (CResourceHandler::get()->existsResource(musicURI))
-		currentName = musicURI;
-	else
-		currentName = musicURI.addPrefix("MUSIC/");
-
-	music = nullptr;
-
-	logGlobal->trace("Loading music file %s", currentName.getOriginalName());
-
-	try
-	{
-		auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName));
-		music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
-	}
-	catch(std::exception &e)
-	{
-		logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName());
-		logGlobal->error("Exception: %s", e.what());
-	}
-
-	if(!music)
-	{
-		logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError());
-		return;
-	}
-}
-
-bool MusicEntry::play()
-{
-	if (!(loop--) && music) //already played once - return
-		return false;
-
-	if (!setName.empty())
-	{
-		const auto & set = owner->musicsSet[setName];
-		const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault());
-		load(*iter);
-	}
-
-	logGlobal->trace("Playing music file %s", currentName.getOriginalName());
-
-	if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0)
-	{
-		float timeToStart = owner->trackPositions[currentName];
-		startPosition = std::round(timeToStart * 1000);
-
-		// erase stored position:
-		// if music track will be interrupted again - new position will be written in stop() method
-		// if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should
-		owner->trackPositions.erase(owner->trackPositions.find(currentName));
-
-		if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1)
-		{
-			logGlobal->error("Unable to play music (%s)", Mix_GetError());
-			return false;
-		}
-	}
-	else
-	{
-		startPosition = 0;
-
-		if(Mix_PlayMusic(music, 1) == -1)
-		{
-			logGlobal->error("Unable to play music (%s)", Mix_GetError());
-			return false;
-		}
-	}
-
-	startTime = GH.input().getTicks();
-	
-	playing = true;
-	return true;
-}
-
-bool MusicEntry::stop(int fade_ms)
-{
-	if (Mix_PlayingMusic())
-	{
-		playing = false;
-		loop = 0;
-		uint32_t endTime = GH.input().getTicks();
-		assert(startTime != uint32_t(-1));
-		float playDuration = (endTime - startTime + startPosition) / 1000.f;
-		owner->trackPositions[currentName] = playDuration;
-		logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration);
-
-		Mix_FadeOutMusic(fade_ms);
-		return true;
-	}
-	return false;
-}
-
-bool MusicEntry::isPlaying()
-{
-	return playing;
-}
-
-bool MusicEntry::isSet(std::string set)
-{
-	return !setName.empty() && set == setName;
-}
-
-bool MusicEntry::isTrack(const AudioPath & track)
-{
-	return setName.empty() && track == currentName;
-}

+ 0 - 170
client/CMusicHandler.h

@@ -1,170 +0,0 @@
-/*
- * CMusicHandler.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../lib/CConfigHandler.h"
-#include "../lib/CSoundBase.h"
-
-struct _Mix_Music;
-struct SDL_RWops;
-using Mix_Music = struct _Mix_Music;
-struct Mix_Chunk;
-
-class CAudioBase {
-protected:
-	boost::mutex mutex;
-	bool initialized;
-	int volume;					// from 0 (mute) to 100
-
-	CAudioBase(): initialized(false), volume(0) {};
-	~CAudioBase() = default;
-public:
-	virtual void init() = 0;
-	virtual void release() = 0;
-
-	virtual void setVolume(ui32 percent);
-	ui32 getVolume() const { return volume; };
-};
-
-class CSoundHandler final : public CAudioBase
-{
-private:
-	//update volume on configuration change
-	SettingsListener listener;
-	void onVolumeChange(const JsonNode &volumeNode);
-
-	using CachedChunk = std::pair<Mix_Chunk *, std::unique_ptr<ui8[]>>;
-	std::map<AudioPath, CachedChunk> soundChunks;
-	std::map<std::vector<ui8>, CachedChunk> soundChunksRaw;
-
-	Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache);
-	Mix_Chunk *GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache);
-
-	/// have entry for every currently active channel
-	/// vector will be empty if callback was not set
-	std::map<int, std::vector<std::function<void()>> > callbacks;
-
-	/// Protects access to callbacks member to avoid data races:
-	/// SDL calls sound finished callbacks from audio thread
-	boost::mutex mutexCallbacks;
-
-	int ambientDistToVolume(int distance) const;
-	void ambientStopSound(const AudioPath & soundId);
-	void updateChannelVolume(int channel);
-
-	const JsonNode ambientConfig;
-
-	std::map<AudioPath, int> ambientChannels;
-	std::map<int, int> channelVolumes;
-
-	void initCallback(int channel, const std::function<void()> & function);
-	void initCallback(int channel);
-
-public:
-	CSoundHandler();
-
-	void init() override;
-	void release() override;
-
-	void setVolume(ui32 percent) override;
-	void setChannelVolume(int channel, ui32 percent);
-
-	// Sounds
-	uint32_t getSoundDurationMilliseconds(const AudioPath & sound);
-	int playSound(soundBase::soundID soundID, int repeats=0);
-	int playSound(const AudioPath & sound, int repeats=0, bool cache=false);
-	int playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats=0, bool cache=false);
-	int playSoundFromSet(std::vector<soundBase::soundID> &sound_vec);
-	void stopSound(int handler);
-
-	void setCallback(int channel, std::function<void()> function);
-	void resetCallback(int channel);
-	void soundFinishedCallback(int channel);
-
-	int ambientGetRange() const;
-	void ambientUpdateChannels(std::map<AudioPath, int> currentSounds);
-	void ambientStopAllChannels();
-
-	// Sets
-	std::vector<soundBase::soundID> battleIntroSounds;
-};
-
-class CMusicHandler;
-
-//Class for handling one music file
-class MusicEntry
-{
-	CMusicHandler *owner;
-	Mix_Music *music;
-
-	int loop; // -1 = indefinite
-	bool fromStart;
-	bool playing;
-	uint32_t startTime;
-	uint32_t startPosition;
-	//if not null - set from which music will be randomly selected
-	std::string setName;
-	AudioPath currentName;
-
-	void load(const AudioPath & musicURI);
-
-public:
-	MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart);
-	~MusicEntry();
-
-	bool isSet(std::string setName);
-	bool isTrack(const AudioPath & trackName);
-	bool isPlaying();
-
-	bool play();
-	bool stop(int fade_ms=0);
-};
-
-class CMusicHandler final: public CAudioBase
-{
-private:
-	//update volume on configuration change
-	SettingsListener listener;
-	void onVolumeChange(const JsonNode &volumeNode);
-
-	std::unique_ptr<MusicEntry> current;
-	std::unique_ptr<MusicEntry> next;
-
-	void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart);
-	void queueNext(std::unique_ptr<MusicEntry> queued);
-	void musicFinishedCallback();
-
-	/// map <set name> -> <list of URI's to tracks belonging to the said set>
-	std::map<std::string, std::vector<AudioPath>> musicsSet;
-	/// stored position, in seconds at which music player should resume playing this track
-	std::map<AudioPath, float> trackPositions;
-
-public:
-	CMusicHandler();
-
-	/// add entry with URI musicURI in set. Track will have ID musicID
-	void addEntryToSet(const std::string & set, const AudioPath & musicURI);
-
-	void init() override;
-	void loadTerrainMusicThemes();
-	void release() override;
-	void setVolume(ui32 percent) override;
-
-	/// play track by URI, if loop = true music will be looped
-	void playMusic(const AudioPath & musicURI, bool loop, bool fromStart);
-	/// play random track from this set
-	void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart);
-	/// play random track from set (musicSet, entryID)
-	void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart);
-	/// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any
-	void stopMusic(int fade_ms=1000);
-
-	friend class MusicEntry;
-};

+ 3 - 1
client/CPlayerInterface.cpp

@@ -14,7 +14,6 @@
 
 #include "CGameInfo.h"
 #include "CMT.h"
-#include "CMusicHandler.h"
 #include "CServerHandler.h"
 #include "HeroMovementController.h"
 #include "PlayerLocalState.h"
@@ -41,6 +40,9 @@
 
 #include "mapView/mapHandler.h"
 
+#include "media/IMusicPlayer.h"
+#include "media/ISoundPlayer.h"
+
 #include "render/CAnimation.h"
 #include "render/IImage.h"
 #include "render/IRenderHandler.h"

+ 0 - 713
client/CVideoHandler.cpp

@@ -1,713 +0,0 @@
-/*
- * CVideoHandler.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "CVideoHandler.h"
-
-#include "CMT.h"
-#include "gui/CGuiHandler.h"
-#include "eventsSDL/InputHandler.h"
-#include "gui/FramerateManager.h"
-#include "renderSDL/SDL_Extensions.h"
-#include "CPlayerInterface.h"
-#include "../lib/filesystem/Filesystem.h"
-#include "../lib/filesystem/CInputStream.h"
-
-#include <SDL_render.h>
-
-#ifndef DISABLE_VIDEO
-
-extern "C" {
-#include <libavformat/avformat.h>
-#include <libavcodec/avcodec.h>
-#include <libavutil/imgutils.h>
-#include <libswscale/swscale.h>
-}
-
-#ifdef _MSC_VER
-#pragma comment(lib, "avcodec.lib")
-#pragma comment(lib, "avutil.lib")
-#pragma comment(lib, "avformat.lib")
-#pragma comment(lib, "swscale.lib")
-#endif // _MSC_VER
-
-// Define a set of functions to read data
-static int lodRead(void* opaque, uint8_t* buf, int size)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-	int bytes = static_cast<int>(video->data->read(buf, size));
-	if(bytes == 0)
-    	return AVERROR_EOF;
-
-	return bytes;
-}
-
-static si64 lodSeek(void * opaque, si64 pos, int whence)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-
-	if (whence & AVSEEK_SIZE)
-		return video->data->getSize();
-
-	return video->data->seek(pos);
-}
-
-// Define a set of functions to read data
-static int lodReadAudio(void* opaque, uint8_t* buf, int size)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-	int bytes = static_cast<int>(video->dataAudio->read(buf, size));
-	if(bytes == 0)
-    	return AVERROR_EOF;
-
-	return bytes;
-}
-
-static si64 lodSeekAudio(void * opaque, si64 pos, int whence)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-
-	if (whence & AVSEEK_SIZE)
-		return video->dataAudio->getSize();
-
-	return video->dataAudio->seek(pos);
-}
-
-CVideoPlayer::CVideoPlayer()
-	: stream(-1)
-	, format (nullptr)
-	, codecContext(nullptr)
-	, codec(nullptr)
-	, frame(nullptr)
-	, sws(nullptr)
-	, context(nullptr)
-	, texture(nullptr)
-	, dest(nullptr)
-	, destRect(0,0,0,0)
-	, pos(0,0,0,0)
-	, frameTime(0)
-	, doLoop(false)
-{}
-
-bool CVideoPlayer::open(const VideoPath & fname, bool scale)
-{
-	return open(fname, true, false);
-}
-
-// loop = to loop through the video
-// overlay = directly write to the screen.
-bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool overlay, bool scale)
-{
-	close();
-
-	doLoop = loop;
-	frameTime = 0;
-
-	if (CResourceHandler::get()->existsResource(videoToOpen))
-		fname = videoToOpen;
-	else
-		fname = videoToOpen.addPrefix("VIDEO/");
-
-	if (!CResourceHandler::get()->existsResource(fname))
-	{
-		logGlobal->error("Error: video %s was not found", fname.getName());
-		return false;
-	}
-
-	data = CResourceHandler::get()->load(fname);
-
-	static const int BUFFER_SIZE = 4096;
-
-	unsigned char * buffer  = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
-	context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek);
-
-	format = avformat_alloc_context();
-	format->pb = context;
-	// filename is not needed - file was already open and stored in this->data;
-	int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr);
-
-	if (avfopen != 0)
-	{
-		return false;
-	}
-	// Retrieve stream information
-	if (avformat_find_stream_info(format, nullptr) < 0)
-		return false;
-
-	// Find the first video stream
-	stream = -1;
-	for(ui32 i=0; i<format->nb_streams; i++)
-	{
-		if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
-		{
-			stream = i;
-			break;
-		}
-	}
-
-	if (stream < 0)
-		// No video stream in that file
-		return false;
-
-	// Find the decoder for the video stream
-	codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
-
-	if (codec == nullptr)
-	{
-		// Unsupported codec
-		return false;
-	}
-
-	codecContext = avcodec_alloc_context3(codec);
-	if(!codecContext)
-		return false;
-	// Get a pointer to the codec context for the video stream
-	int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar);
-	if (ret < 0)
-	{
-		//We cannot get codec from parameters
-		avcodec_free_context(&codecContext);
-		return false;
-	}
-
-	// Open codec
-	if ( avcodec_open2(codecContext, codec, nullptr) < 0 )
-	{
-		// Could not open codec
-		codec = nullptr;
-		return false;
-	}
-	// Allocate video frame
-	frame = av_frame_alloc();
-
-	//setup scaling
-	if(scale)
-	{
-		pos.w = screen->w;
-		pos.h = screen->h;
-	}
-	else
-	{
-		pos.w  = codecContext->width;
-		pos.h = codecContext->height;
-	}
-
-	// Allocate a place to put our YUV image on that screen
-	if (overlay)
-	{
-		texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
-	}
-	else
-	{
-		dest = CSDL_Ext::newSurface(pos.w, pos.h);
-		destRect.x = destRect.y = 0;
-		destRect.w = pos.w;
-		destRect.h = pos.h;
-	}
-
-	if (texture == nullptr && dest == nullptr)
-		return false;
-
-	if (texture)
-	{ // Convert the image into YUV format that SDL uses
-		sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
-							 pos.w, pos.h,
-							 AV_PIX_FMT_YUV420P,
-							 SWS_BICUBIC, nullptr, nullptr, nullptr);
-	}
-	else
-	{
-		AVPixelFormat screenFormat = AV_PIX_FMT_NONE;
-		if (screen->format->Bshift > screen->format->Rshift)
-		{
-			// this a BGR surface
-			switch (screen->format->BytesPerPixel)
-			{
-				case 2: screenFormat = AV_PIX_FMT_BGR565; break;
-				case 3: screenFormat = AV_PIX_FMT_BGR24; break;
-				case 4: screenFormat = AV_PIX_FMT_BGR32; break;
-				default: return false;
-			}
-		}
-		else
-		{
-			// this a RGB surface
-			switch (screen->format->BytesPerPixel)
-			{
-				case 2: screenFormat = AV_PIX_FMT_RGB565; break;
-				case 3: screenFormat = AV_PIX_FMT_RGB24; break;
-				case 4: screenFormat = AV_PIX_FMT_RGB32; break;
-				default: return false;
-			}
-		}
-
-		sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
-							 pos.w, pos.h, screenFormat,
-							 SWS_BICUBIC, nullptr, nullptr, nullptr);
-	}
-
-	if (sws == nullptr)
-		return false;
-
-	return true;
-}
-
-// Read the next frame. Return false on error/end of file.
-bool CVideoPlayer::nextFrame()
-{
-	AVPacket packet;
-	int frameFinished = 0;
-	bool gotError = false;
-
-	if (sws == nullptr)
-		return false;
-
-	while(!frameFinished)
-	{
-		int ret = av_read_frame(format, &packet);
-		if (ret < 0)
-		{
-			// Error. It's probably an end of file.
-			if (doLoop && !gotError)
-			{
-				// Rewind
-				frameTime = 0;
-				if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0)
-					break;
-				gotError = true;
-			}
-			else
-			{
-				break;
-			}
-		}
-		else
-		{
-			// Is this a packet from the video stream?
-			if (packet.stream_index == stream)
-			{
-				// Decode video frame
-				int rc = avcodec_send_packet(codecContext, &packet);
-				if (rc >=0)
-					packet.size = 0;
-				rc = avcodec_receive_frame(codecContext, frame);
-				if (rc >= 0)
-					frameFinished = 1;
-				// Did we get a video frame?
-				if (frameFinished)
-				{
-					uint8_t *data[4];
-					int linesize[4];
-
-					if (texture) {
-						av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1);
-
-						sws_scale(sws, frame->data, frame->linesize,
-								  0, codecContext->height, data, linesize);
-
-						SDL_UpdateYUVTexture(texture, nullptr, data[0], linesize[0],
-								data[1], linesize[1],
-								data[2], linesize[2]);
-						av_freep(&data[0]);
-					}
-					else
-					{
-						/* Avoid buffer overflow caused by sws_scale():
-						 *     http://trac.ffmpeg.org/ticket/9254
-						 * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale()
-						 * has a few requirements for target data buffers on rescaling:
-						 * 1. buffer has to be aligned to be usable for SIMD instructions
-						 * 2. buffer has to be padded to allow small overflow by SIMD instructions
-						 * Unfortunately SDL_Surface does not provide these guarantees.
-						 * This means that atempt to rescale directly into SDL surface causes
-						 * memory corruption. Usually it happens on campaign selection screen
-						 * where short video moves start spinning on mouse hover.
-						 *
-						 * To fix [1.] we use av_malloc() for memory allocation.
-						 * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space.
-						 * We have to use intermdiate buffer and then use memcpy() to land it
-						 * to SDL_Surface.
-						 */
-						size_t pic_bytes = dest->pitch * dest->h;
-						size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
-						void * for_sws = av_malloc (pic_bytes + ffmped_pad);
-						data[0] = (ui8 *)for_sws;
-						linesize[0] = dest->pitch;
-
-						sws_scale(sws, frame->data, frame->linesize,
-								  0, codecContext->height, data, linesize);
-						memcpy(dest->pixels, for_sws, pic_bytes);
-						av_free(for_sws);
-					}
-				}
-			}
-
-			av_packet_unref(&packet);
-		}
-	}
-
-	return frameFinished != 0;
-}
-
-void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update )
-{
-	if (sws == nullptr)
-		return;
-
-	pos.x = x;
-	pos.y = y;
-	CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft());
-
-	if (update)
-		CSDL_Ext::updateRect(dst, pos);
-}
-
-void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update )
-{
-	show(x, y, dst, update);
-}
-
-void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function<void()> onVideoRestart)
-{
-	if (sws == nullptr)
-		return;
-
-#if (LIBAVUTIL_VERSION_MAJOR < 58)   
-	auto packet_duration = frame->pkt_duration;
-#else
-	auto packet_duration = frame->duration;
-#endif
-	double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base);
-	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0;
-
-	if (frameTime >= frameEndTime )
-	{
-		if (nextFrame())
-			show(x,y,dst,update);
-		else
-		{
-			if(onVideoRestart)
-				onVideoRestart();
-			VideoPath filenameToReopen = fname; // create copy to backup this->fname
-			open(filenameToReopen);
-			nextFrame();
-
-			// The y position is wrong at the first frame.
-			// Note: either the windows player or the linux player is
-			// broken. Compensate here until the bug is found.
-			show(x, y--, dst, update);
-		}
-	}
-	else
-	{
-		redraw(x, y, dst, update);
-	}
-}
-
-void CVideoPlayer::close()
-{
-	fname = VideoPath();
-
-	if (sws)
-	{
-		sws_freeContext(sws);
-		sws = nullptr;
-	}
-
-	if (texture)
-	{
-		SDL_DestroyTexture(texture);
-		texture = nullptr;
-	}
-
-	if (dest)
-	{
-		SDL_FreeSurface(dest);
-		dest = nullptr;
-	}
-
-	if (frame)
-	{
-		av_frame_free(&frame);//will be set to null
-	}
-
-	if (codec)
-	{
-		avcodec_close(codecContext);
-		codec = nullptr;
-	}
-	if (codecContext)
-	{
-		avcodec_free_context(&codecContext);
-	}
-
-	if (format)
-	{
-		avformat_close_input(&format);
-	}
-
-	if (context)
-	{
-		av_free(context);
-		context = nullptr;
-	}
-}
-
-std::pair<std::unique_ptr<ui8 []>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
-{
-	std::pair<std::unique_ptr<ui8 []>, si64> dat(std::make_pair(nullptr, 0));
-
-	VideoPath fnameAudio;
-
-	if (CResourceHandler::get()->existsResource(videoToOpen))
-		fnameAudio = videoToOpen;
-	else
-		fnameAudio = videoToOpen.addPrefix("VIDEO/");
-
-	if (!CResourceHandler::get()->existsResource(fnameAudio))
-	{
-		logGlobal->error("Error: video %s was not found", fnameAudio.getName());
-		return dat;
-	}
-
-	dataAudio = CResourceHandler::get()->load(fnameAudio);
-
-	static const int BUFFER_SIZE = 4096;
-
-	unsigned char * bufferAudio  = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
-	AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio);
-
-	AVFormatContext * formatAudio = avformat_alloc_context();
-	formatAudio->pb = contextAudio;
-	// filename is not needed - file was already open and stored in this->data;
-	int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr);
-
-	if (avfopen != 0)
-	{
-		return dat;
-	}
-	// Retrieve stream information
-	if (avformat_find_stream_info(formatAudio, nullptr) < 0)
-		return dat;
-
-	// Find the first audio stream
-	int streamAudio = -1;
-	for(ui32 i = 0; i < formatAudio->nb_streams; i++)
-	{
-		if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
-		{
-			streamAudio = i;
-			break;
-		}
-	}
-
-	if(streamAudio < 0)
-		return dat;
-
-	const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id);
-		
-	AVCodecContext *codecContextAudio;
-	if (codecAudio != nullptr)
-		codecContextAudio = avcodec_alloc_context3(codecAudio);
-
-	// Get a pointer to the codec context for the audio stream
-	if (streamAudio > -1)
-	{
-		int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar);
-		if (ret < 0)
-		{
-			//We cannot get codec from parameters
-			avcodec_free_context(&codecContextAudio);
-		}
-	}
-	
-	// Open codec
-	AVFrame *frameAudio;
-	if (codecAudio != nullptr)
-	{
-		if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 )
-		{
-			// Could not open codec
-			codecAudio = nullptr;
-		}
-		// Allocate audio frame
-		frameAudio = av_frame_alloc();
-	}
-		
-	AVPacket packet;
-
-	std::vector<ui8> samples;
-
-	while (av_read_frame(formatAudio, &packet) >= 0)
-	{
-		if(packet.stream_index == streamAudio)
-		{
-			int rc = avcodec_send_packet(codecContextAudio, &packet);
-			if (rc >= 0)
-				packet.size = 0;
-			rc = avcodec_receive_frame(codecContextAudio, frameAudio);
-			int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8));
-			if (rc >= 0)
-				for (int s = 0; s < bytesToRead; s += sizeof(ui8))
-				{
-					ui8 value;
-					memcpy(&value, &frameAudio->data[0][s], sizeof(ui8));
-					samples.push_back(value);
-				}
-		}
-
-		av_packet_unref(&packet);
-	}
-
-	typedef struct WAV_HEADER {
-		ui8 RIFF[4] = {'R', 'I', 'F', 'F'};
-		ui32 ChunkSize;
-		ui8 WAVE[4] = {'W', 'A', 'V', 'E'};
-		ui8 fmt[4] = {'f', 'm', 't', ' '};
-		ui32 Subchunk1Size = 16;
-		ui16 AudioFormat = 1;
-		ui16 NumOfChan = 2;
-		ui32 SamplesPerSec = 22050;
-		ui32 bytesPerSec = 22050 * 2;
-		ui16 blockAlign = 2;
-		ui16 bitsPerSample = 16;
-		ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'};
-		ui32 Subchunk2Size;
-	} wav_hdr;
-
-	wav_hdr wav;
-	wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8;
-  	wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44;
-	wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate;
-	wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample;
-	auto wavPtr = reinterpret_cast<ui8*>(&wav);
-
-	dat = std::make_pair(std::make_unique<ui8[]>(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr));
-	std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get());
-	std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr));
-
-	if (frameAudio)
-		av_frame_free(&frameAudio);
-
-	if (codecAudio)
-	{
-		avcodec_close(codecContextAudio);
-		codecAudio = nullptr;
-	}
-	if (codecContextAudio)
-		avcodec_free_context(&codecContextAudio);
-
-	if (formatAudio)
-		avformat_close_input(&formatAudio);
-
-	if (contextAudio)
-	{
-		av_free(contextAudio);
-		contextAudio = nullptr;
-	}
-
-	return dat;
-}
-
-Point CVideoPlayer::size()
-{
-	if(frame)
-		return Point(frame->width, frame->height);
-	else
-		return Point(0, 0);
-}
-
-// Plays a video. Only works for overlays.
-bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay)
-{
-	// Note: either the windows player or the linux player is
-	// broken. Compensate here until the bug is found.
-	y--;
-
-	pos.x = x;
-	pos.y = y;
-	frameTime = 0.0;
-
-	auto lastTimePoint = boost::chrono::steady_clock::now();
-
-	while(nextFrame())
-	{
-		if(stopOnKey)
-		{
-			GH.input().fetchEvents();
-			if(GH.input().ignoreEventsUntilInput())
-				return false;
-		}
-
-		SDL_Rect rect = CSDL_Ext::toSDL(pos);
-
-		if(overlay)
-		{
-			SDL_RenderFillRect(mainRenderer, &rect);
-		}
-		else
-		{
-			SDL_RenderClear(mainRenderer);
-		}
-		SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
-		SDL_RenderPresent(mainRenderer);
-
-#if (LIBAVUTIL_VERSION_MAJOR < 58)
-		auto packet_duration = frame->pkt_duration;
-#else
-		auto packet_duration = frame->duration;
-#endif
-		// Framerate delay
-		double targetFrameTimeSeconds = packet_duration * av_q2d(format->streams[stream]->time_base);
-		auto targetFrameTime = boost::chrono::milliseconds(static_cast<int>(1000 * (targetFrameTimeSeconds)));
-
-		auto timePointAfterPresent = boost::chrono::steady_clock::now();
-		auto timeSpentBusy = boost::chrono::duration_cast<boost::chrono::milliseconds>(timePointAfterPresent - lastTimePoint);
-
-		if (targetFrameTime > timeSpentBusy)
-			boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy);
-
-		lastTimePoint = boost::chrono::steady_clock::now();
-	}
-
-	return true;
-}
-
-bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
-{
-	bool scale;
-	bool stopOnKey;
-	bool overlay;
-
-	switch(videoType)
-	{
-		case EVideoType::INTRO:
-			stopOnKey = true;
-			scale = true;
-			overlay = false;
-			break;
-		case EVideoType::SPELLBOOK:
-		default:
-			stopOnKey = false;
-			scale = false;
-			overlay = true;
-	}
-	open(name, false, true, scale);
-	bool ret = playVideo(x, y,  stopOnKey, overlay);
-	close();
-	return ret;
-}
-
-CVideoPlayer::~CVideoPlayer()
-{
-	close();
-}
-
-#endif
-

+ 0 - 131
client/CVideoHandler.h

@@ -1,131 +0,0 @@
-/*
- * CVideoHandler.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../lib/Rect.h"
-#include "../lib/filesystem/ResourcePath.h"
-
-struct SDL_Surface;
-struct SDL_Texture;
-
-enum class EVideoType : ui8
-{
-	INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false
-	SPELLBOOK  // overlay video: stopOnKey = false, scale = false, overlay = true
-};
-
-class IVideoPlayer : boost::noncopyable
-{
-public:
-	virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes
-	virtual void close()=0;
-	virtual bool nextFrame()=0;
-	virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0;
-	virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer
-	virtual bool wait()=0;
-	virtual int curFrame() const =0;
-	virtual int frameCount() const =0;
-};
-
-class IMainVideoPlayer : public IVideoPlayer
-{
-public:
-	virtual ~IMainVideoPlayer() = default;
-	virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> restart = nullptr){}
-	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType)
-	{
-		return false;
-	}
-	virtual std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); };
-	virtual Point size() { return Point(0, 0); };
-};
-
-class CEmptyVideoPlayer final : public IMainVideoPlayer
-{
-public:
-	int curFrame() const override {return -1;};
-	int frameCount() const override {return -1;};
-	void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {};
-	void show( int x, int y, SDL_Surface *dst, bool update = true ) override {};
-	bool nextFrame() override {return false;};
-	void close() override {};
-	bool wait() override {return false;};
-	bool open(const VideoPath & name, bool scale = false) override {return false;};
-};
-
-#ifndef DISABLE_VIDEO
-
-struct AVFormatContext;
-struct AVCodecContext;
-struct AVCodec;
-struct AVFrame;
-struct AVIOContext;
-
-VCMI_LIB_NAMESPACE_BEGIN
-class CInputStream;
-VCMI_LIB_NAMESPACE_END
-
-class CVideoPlayer final : public IMainVideoPlayer
-{
-	int stream;					// stream index in video
-	AVFormatContext *format;
-	AVCodecContext *codecContext; // codec context for stream
-	const AVCodec *codec;
-	AVFrame *frame;
-	struct SwsContext *sws;
-
-	AVIOContext * context;
-
-	VideoPath fname;  //name of current video file (empty if idle)
-
-	// Destination. Either overlay or dest.
-
-	SDL_Texture *texture;
-	SDL_Surface *dest;
-	Rect destRect;			// valid when dest is used
-	Rect pos;				// destination on screen
-
-	/// video playback currnet progress, in seconds
-	double frameTime;
-	bool doLoop;				// loop through video
-
-	bool playVideo(int x, int y, bool stopOnKey, bool overlay);
-	bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false);
-public:
-	CVideoPlayer();
-	~CVideoPlayer();
-
-	bool init();
-	bool open(const VideoPath & fname, bool scale = false) override;
-	void close() override;
-	bool nextFrame() override;			// display next frame
-
-	void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame
-	void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer
-	void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true
-
-	// Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played)
-	bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override;
-
-	std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) override;
-
-	Point size() override;
-
-	//TODO:
-	bool wait() override {return false;};
-	int curFrame() const override {return -1;};
-	int frameCount() const override {return -1;};
-
-	// public to allow access from ffmpeg IO functions
-	std::unique_ptr<CInputStream> data;
-	std::unique_ptr<CInputStream> dataAudio;
-};
-
-#endif

+ 2 - 1
client/HeroMovementController.cpp

@@ -11,7 +11,6 @@
 #include "HeroMovementController.h"
 
 #include "CGameInfo.h"
-#include "CMusicHandler.h"
 #include "CPlayerInterface.h"
 #include "PlayerLocalState.h"
 #include "adventureMap/AdventureMapInterface.h"
@@ -19,10 +18,12 @@
 #include "gui/CGuiHandler.h"
 #include "gui/CursorHandler.h"
 #include "mapView/mapHandler.h"
+#include "media/ISoundPlayer.h"
 
 #include "../CCallback.h"
 
 #include "../lib/CondSh.h"
+#include "../lib/CConfigHandler.h"
 #include "../lib/pathfinder/CGPathNode.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/networkPacks/PacksForClient.h"

+ 1 - 1
client/adventureMap/CInGameConsole.cpp

@@ -12,7 +12,6 @@
 #include "CInGameConsole.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../GameChatHandler.h"
@@ -21,6 +20,7 @@
 #include "../gui/WindowHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/TextAlignment.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/IScreenHandler.h"

+ 1 - 1
client/adventureMap/CInfoBar.cpp

@@ -20,11 +20,11 @@
 #include "../widgets/MiscWidgets.h"
 #include "../windows/InfoWindows.h"
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/IScreenHandler.h"
 
 #include "../../CCallback.h"

+ 2 - 1
client/adventureMap/MapAudioPlayer.cpp

@@ -12,9 +12,10 @@
 
 #include "../CCallback.h"
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../mapView/mapHandler.h"
+#include "../media/IMusicPlayer.h"
+#include "../media/ISoundPlayer.h"
 
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/mapObjects/CArmedInstance.h"

+ 1 - 1
client/adventureMap/TurnTimerWidget.cpp

@@ -11,11 +11,11 @@
 #include "TurnTimerWidget.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleStacksController.h"
 #include "../gui/CGuiHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Graphics.h"
 #include "../widgets/Images.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -20,10 +20,10 @@
 #include "CreatureAnimation.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/IRenderHandler.h"
 
 #include "../../CCallback.h"

+ 1 - 1
client/battle/BattleEffectsController.cpp

@@ -18,9 +18,9 @@
 #include "BattleStacksController.h"
 #include "BattleRenderer.h"
 
-#include "../CMusicHandler.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Canvas.h"
 #include "../render/CAnimation.h"
 #include "../render/Graphics.h"

+ 10 - 2
client/battle/BattleInterface.cpp

@@ -24,11 +24,12 @@
 #include "BattleRenderer.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
+#include "../media/IMusicPlayer.h"
+#include "../media/ISoundPlayer.h"
 #include "../windows/CTutorialWindow.h"
 #include "../render/Canvas.h"
 #include "../adventureMap/AdventureMapInterface.h"
@@ -113,7 +114,14 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
 			onIntroSoundPlayed();
 	};
 
-	int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
+	std::vector<soundBase::soundID> battleIntroSounds =
+	{
+		soundBase::battle00, soundBase::battle01,
+		soundBase::battle02, soundBase::battle03, soundBase::battle04,
+		soundBase::battle05, soundBase::battle06, soundBase::battle07
+	};
+
+	int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(battleIntroSounds);
 	if (battleIntroSoundChannel != -1)
 	{
 		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);

+ 62 - 108
client/battle/BattleInterfaceClasses.cpp

@@ -19,14 +19,13 @@
 #include "BattleWindow.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
-#include "../CVideoHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/MouseButton.h"
 #include "../gui/WindowHandler.h"
+#include "../media/IMusicPlayer.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../render/IFont.h"
@@ -35,6 +34,7 @@
 #include "../widgets/Images.h"
 #include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../windows/CMessage.h"
 #include "../windows/CCreatureWindow.h"
@@ -603,7 +603,7 @@ HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
 }
 
 BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay)
-	: owner(_owner), currentVideo(BattleResultVideo::NONE)
+	: owner(_owner)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
@@ -705,68 +705,98 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 			}
 		}
 	}
+
+	auto resources = getResources(br);
+
+	description = std::make_shared<CTextBox>(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	videoPlayer = std::make_shared<VideoWidget>(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false);
+
+	CCS->musich->playMusic(resources.musicName, false, true);
+}
+
+BattleResultResources BattleResultWindow::getResources(const BattleResult & br)
+{
 	//printing result description
 	bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide());
-	if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won
+	bool weAreDefender = !weAreAttacker;
+	bool weWon = (br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker);
+	bool isSiege = owner.cb->getBattle(br.battleID)->battleGetDefendedTown() != nullptr;
+
+	BattleResultResources resources;
+
+	if(weWon)
 	{
-		int text = 304;
-		currentVideo = BattleResultVideo::WIN;
+		if(isSiege && weAreDefender)
+		{
+			resources.musicName = AudioPath::builtin("Music/Defend Castle");
+			resources.prologueVideo = VideoPath::builtin("DEFENDALL.BIK");
+			resources.loopedVideo = VideoPath::builtin("defendloop.bik");
+		}
+		else
+		{
+			resources.musicName = AudioPath::builtin("Music/Win Battle");
+			resources.prologueVideo = VideoPath();
+			resources.loopedVideo = VideoPath::builtin("WIN3.BIK");
+		}
+
 		switch(br.result)
 		{
 		case EBattleResult::NORMAL:
-			if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker)
-				currentVideo = BattleResultVideo::WIN_SIEGE;
+			resources.resultText.appendTextID("core.genrltxt.304");
 			break;
 		case EBattleResult::ESCAPE:
-			text = 303;
+			resources.resultText.appendTextID("core.genrltxt.303");
 			break;
 		case EBattleResult::SURRENDER:
-			text = 302;
+			resources.resultText.appendTextID("core.genrltxt.302");
 			break;
 		default:
-			logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result));
-			break;
+			throw std::runtime_error("Invalid battle result!");
 		}
-		playVideo();
-
-		std::string str = CGI->generaltexth->allTexts[text];
 
 		const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
 		if (ourHero)
 		{
-			str += CGI->generaltexth->allTexts[305];
-			boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated());
-			boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1]));
+			resources.resultText.appendTextID("core.genrltxt.305");
+			resources.resultText.replaceTextID(ourHero->getNameTranslated());
+			resources.resultText.replaceNumber(br.exp[weAreAttacker ? 0 : 1]);
 		}
-
-		description = std::make_shared<CTextBox>(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 	}
 	else // we lose
 	{
-		int text = 311;
-		currentVideo = BattleResultVideo::DEFEAT;
 		switch(br.result)
 		{
 		case EBattleResult::NORMAL:
-			if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker)
-				currentVideo = BattleResultVideo::DEFEAT_SIEGE;
+			resources.resultText.appendTextID("core.genrltxt.311");
+			resources.musicName = AudioPath::builtin("Music/LoseCombat");
+			resources.prologueVideo = VideoPath::builtin("LBSTART.BIK");
+			resources.loopedVideo = VideoPath::builtin("LBLOOP.BIK");
 			break;
 		case EBattleResult::ESCAPE:
-			currentVideo = BattleResultVideo::RETREAT;
-			text = 310;
+			resources.resultText.appendTextID("core.genrltxt.310");
+			resources.musicName = AudioPath::builtin("Music/Retreat Battle");
+			resources.prologueVideo = VideoPath::builtin("RTSTART.BIK");
+			resources.loopedVideo = VideoPath::builtin("RTLOOP.BIK");
 			break;
 		case EBattleResult::SURRENDER:
-			currentVideo = BattleResultVideo::SURRENDER;
-			text = 309;
+			resources.resultText.appendTextID("core.genrltxt.309");
+			resources.musicName = AudioPath::builtin("Music/Surrender Battle");
+			resources.prologueVideo = VideoPath();
+			resources.loopedVideo = VideoPath::builtin("SURRENDER.BIK");
 			break;
 		default:
-			logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result));
-			break;
+				throw std::runtime_error("Invalid battle result!");
 		}
-		playVideo();
 
-		labels.push_back(std::make_shared<CLabel>(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text]));
+		if(isSiege && weAreDefender)
+		{
+			resources.musicName = AudioPath::builtin("Music/LoseCastle");
+			resources.prologueVideo = VideoPath::builtin("LOSECSTL.BIK");
+			resources.loopedVideo = VideoPath::builtin("LOSECSLP.BIK");
+		}
 	}
+
+	return resources;
 }
 
 void BattleResultWindow::activate()
@@ -775,81 +805,6 @@ void BattleResultWindow::activate()
 	CIntObject::activate();
 }
 
-void BattleResultWindow::show(Canvas & to)
-{
-	CIntObject::show(to);
-	CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false,
-	[&]()
-	{
-		playVideo(true);
-	});
-}
-
-void BattleResultWindow::playVideo(bool startLoop)
-{
-	AudioPath musicName = AudioPath();
-	VideoPath videoName = VideoPath();
-
-	if(!startLoop)
-	{
-		switch(currentVideo)
-		{
-			case BattleResultVideo::WIN:
-				musicName = AudioPath::builtin("Music/Win Battle");
-				videoName = VideoPath::builtin("WIN3.BIK");
-				break;
-			case BattleResultVideo::SURRENDER:
-				musicName = AudioPath::builtin("Music/Surrender Battle");
-				videoName = VideoPath::builtin("SURRENDER.BIK");
-				break;
-			case BattleResultVideo::RETREAT:
-				musicName = AudioPath::builtin("Music/Retreat Battle");
-				videoName = VideoPath::builtin("RTSTART.BIK");
-				break;
-			case BattleResultVideo::DEFEAT:
-				musicName = AudioPath::builtin("Music/LoseCombat");
-				videoName = VideoPath::builtin("LBSTART.BIK");
-				break;
-			case BattleResultVideo::DEFEAT_SIEGE:
-				musicName = AudioPath::builtin("Music/LoseCastle");
-				videoName = VideoPath::builtin("LOSECSTL.BIK");	
-				break;
-			case BattleResultVideo::WIN_SIEGE:
-				musicName = AudioPath::builtin("Music/Defend Castle");
-				videoName = VideoPath::builtin("DEFENDALL.BIK");	
-				break;
-		}
-	}
-	else
-	{
-		switch(currentVideo)
-		{
-			case BattleResultVideo::RETREAT:
-				currentVideo = BattleResultVideo::RETREAT_LOOP;
-				videoName = VideoPath::builtin("RTLOOP.BIK");
-				break;
-			case BattleResultVideo::DEFEAT:
-				currentVideo = BattleResultVideo::DEFEAT_LOOP;
-				videoName = VideoPath::builtin("LBLOOP.BIK");
-				break;
-			case BattleResultVideo::DEFEAT_SIEGE:
-				currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP;
-				videoName = VideoPath::builtin("LOSECSLP.BIK");	
-				break;
-			case BattleResultVideo::WIN_SIEGE:
-				currentVideo = BattleResultVideo::WIN_SIEGE_LOOP;
-				videoName = VideoPath::builtin("DEFENDLOOP.BIK");	
-				break;
-		}	
-	}
-
-	if(musicName != AudioPath())
-		CCS->musich->playMusic(musicName, false, true);
-	
-	if(videoName != VideoPath())
-		CCS->videoh->open(videoName);
-}
-
 void BattleResultWindow::buttonPressed(int button)
 {
 	if (resultCallback)
@@ -865,7 +820,6 @@ void BattleResultWindow::buttonPressed(int button)
 	//Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle,
 	//so we can be sure that there is no dialogs left on GUI stack.
 	intTmp.showingDialog->setn(false);
-	CCS->videoh->close();
 }
 
 void BattleResultWindow::bExitf()

+ 12 - 18
client/battle/BattleInterfaceClasses.h

@@ -14,6 +14,7 @@
 #include "../../lib/FunctionList.h"
 #include "../../lib/battle/BattleHex.h"
 #include "../windows/CWindowObject.h"
+#include "../../lib/MetaString.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -42,6 +43,7 @@ class CAnimImage;
 class TransparentFilledRectangle;
 class CPlayerInterface;
 class BattleRenderer;
+class VideoWidget;
 
 /// Class which shows the console at the bottom of the battle screen and manages the text of the console
 class BattleConsole : public CIntObject, public IStatusBar
@@ -185,6 +187,14 @@ public:
 	HeroInfoWindow(const InfoAboutHero & hero, Point * position);
 };
 
+struct BattleResultResources
+{
+	VideoPath prologueVideo;
+	VideoPath loopedVideo;
+	AudioPath musicName;
+	MetaString resultText;
+};
+
 /// Class which is responsible for showing the battle result window
 class BattleResultWindow : public WindowBase
 {
@@ -195,25 +205,10 @@ private:
 	std::shared_ptr<CButton> repeat;
 	std::vector<std::shared_ptr<CAnimImage>> icons;
 	std::shared_ptr<CTextBox> description;
+	std::shared_ptr<VideoWidget> videoPlayer;
 	CPlayerInterface & owner;
 
-	enum BattleResultVideo
-	{
-		NONE,
-		WIN,
-		SURRENDER,
-		RETREAT,
-		RETREAT_LOOP,
-		DEFEAT,
-		DEFEAT_LOOP,
-		DEFEAT_SIEGE,
-		DEFEAT_SIEGE_LOOP,
-		WIN_SIEGE,
-		WIN_SIEGE_LOOP,
-	};
-	BattleResultVideo currentVideo;
-
-	void playVideo(bool startLoop = false);
+	BattleResultResources getResources(const BattleResult & br);
 	
 	void buttonPressed(int button); //internal function for button callbacks
 public:
@@ -224,7 +219,6 @@ public:
 	std::function<void(int result)> resultCallback; //callback receiving which button was pressed
 
 	void activate() override;
-	void show(Canvas & to) override;
 };
 
 /// Shows the stack queue

+ 1 - 1
client/battle/BattleObstacleController.cpp

@@ -17,10 +17,10 @@
 #include "BattleRenderer.h"
 #include "CreatureAnimation.h"
 
-#include "../CMusicHandler.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
 

+ 1 - 1
client/battle/BattleSiegeController.cpp

@@ -17,10 +17,10 @@
 #include "BattleFieldController.h"
 #include "BattleRenderer.h"
 
-#include "../CMusicHandler.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"

+ 1 - 1
client/battle/BattleStacksController.cpp

@@ -23,10 +23,10 @@
 #include "CreatureAnimation.h"
 
 #include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
 #include "../CGameInfo.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"

+ 0 - 1
client/battle/BattleWindow.cpp

@@ -18,7 +18,6 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"

+ 2 - 1
client/eventsSDL/InputHandler.cpp

@@ -22,10 +22,11 @@
 #include "../gui/CursorHandler.h"
 #include "../gui/EventDispatcher.h"
 #include "../gui/MouseButton.h"
+#include "../media/IMusicPlayer.h"
+#include "../media/ISoundPlayer.h"
 #include "../CMT.h"
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 
 #include "../../lib/CConfigHandler.h"
 

+ 1 - 1
client/globalLobby/GlobalLobbyClient.cpp

@@ -17,11 +17,11 @@
 #include "GlobalLobbyWindow.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../mainmenu/CMainMenu.h"
+#include "../media/ISoundPlayer.h"
 #include "../windows/InfoWindows.h"
 
 #include "../../lib/CConfigHandler.h"

+ 1 - 1
client/globalLobby/GlobalLobbyWidget.cpp

@@ -16,10 +16,10 @@
 #include "GlobalLobbyRoomWindow.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
+#include "../media/ISoundPlayer.h"
 #include "../render/Colors.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CTextInput.h"

+ 3 - 4
client/lobby/CBonusSelection.cpp

@@ -18,12 +18,11 @@
 #include "ExtraOptionsTab.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CVideoHandler.h"
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../mainmenu/CPrologEpilogVideo.h"
+#include "../media/IMusicPlayer.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
@@ -41,9 +40,9 @@
 
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/CGeneralTextHandler.h"
-
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/CBuildingHandler.h"
-
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/CSkillHandler.h"
 #include "../../lib/CTownHandler.h"
 #include "../../lib/CHeroHandler.h"

+ 1 - 2
client/lobby/CSelectionBase.cpp

@@ -20,14 +20,13 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
-#include "../CVideoHandler.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../globalLobby/GlobalLobbyClient.h"
 #include "../mainmenu/CMainMenu.h"
+#include "../media/ISoundPlayer.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CTextInput.h"

+ 2 - 1
client/lobby/OptionsTab.cpp

@@ -14,12 +14,12 @@
 
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
-#include "../CMusicHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../render/Graphics.h"
 #include "../render/IFont.h"
+#include "../media/ISoundPlayer.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/ComboBox.h"
 #include "../widgets/CTextInput.h"
@@ -38,6 +38,7 @@
 #include "../../lib/networkPacks/PacksForLobby.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CArtHandler.h"
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/CTownHandler.h"
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/mapping/CMapInfo.h"

+ 9 - 21
client/mainmenu/CCampaignScreen.cpp

@@ -14,18 +14,18 @@
 #include "CMainMenu.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CVideoHandler.h"
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
+#include "../media/IMusicPlayer.h"
 #include "../render/Canvas.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
 #include "../windows/CWindowObject.h"
@@ -36,7 +36,7 @@
 #include "../../lib/CArtHandler.h"
 #include "../../lib/CBuildingHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
-
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/CSkillHandler.h"
 #include "../../lib/CTownHandler.h"
 #include "../../lib/CHeroHandler.h"
@@ -100,7 +100,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
 	pos.h = 116;
 
 	campFile = config["file"].String();
-	video = VideoPath::fromJson(config["video"]);
+	videoPath = VideoPath::fromJson(config["video"]);
 
 	status = CCampaignScreen::ENABLED;
 
@@ -127,7 +127,6 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
 	{
 		addUsedEvents(LCLICK | HOVER);
 		graphicsImage = std::make_shared<CPicture>(ImagePath::fromJson(config["image"]));
-
 		hoverLabel = std::make_shared<CLabel>(pos.w / 2, pos.h + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, "");
 		parent->addChild(hoverLabel.get());
 	}
@@ -136,30 +135,19 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const
 		graphicsCompleted = std::make_shared<CPicture>(ImagePath::builtin("CAMPCHK"));
 }
 
-void CCampaignScreen::CCampaignButton::show(Canvas & to)
-{
-	if(status == CCampaignScreen::DISABLED)
-		return;
-
-	CIntObject::show(to);
-
-	// Play the campaign button video when the mouse cursor is placed over the button
-	if(isHovered())
-		CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); // plays sequentially frame by frame, starts at the beginning when the video is over
-}
-
 void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition)
 {
-	CCS->videoh->close();
 	CMainMenu::openCampaignLobby(campFile, campaignSet);
 }
 
 void CCampaignScreen::CCampaignButton::hover(bool on)
 {
-	if (on)
-		CCS->videoh->open(video);
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	if (on && !videoPath.empty())
+		videoPlayer = std::make_shared<VideoWidget>(Point(), videoPath, false);
 	else
-		CCS->videoh->close();
+		videoPlayer.reset();
 
 	if(hoverLabel)
 	{

+ 3 - 2
client/mainmenu/CCampaignScreen.h

@@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_END
 class CLabel;
 class CPicture;
 class CButton;
+class VideoWidget;
 
 class CCampaignScreen : public CWindowObject
 {
@@ -34,10 +35,11 @@ private:
 		std::shared_ptr<CLabel> hoverLabel;
 		std::shared_ptr<CPicture> graphicsImage;
 		std::shared_ptr<CPicture> graphicsCompleted;
+		std::shared_ptr<VideoWidget> videoPlayer;
 		CampaignStatus status;
+		VideoPath videoPath;
 
 		std::string campFile; // the filename/resourcename of the campaign
-		VideoPath video; // the resource name of the video
 		std::string hoverText;
 
 		std::string campaignSet;
@@ -47,7 +49,6 @@ private:
 
 	public:
 		CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet);
-		void show(Canvas & to) override;
 	};
 
 	std::string campaignSet;

+ 11 - 42
client/mainmenu/CHighScoreScreen.cpp

@@ -14,17 +14,18 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/Shortcut.h"
+#include "../media/IMusicPlayer.h"
+#include "../media/ISoundPlayer.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CTextInput.h"
 #include "../widgets/Images.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/VideoWidget.h"
 #include "../windows/InfoWindows.h"
 #include "../widgets/TextControls.h"
 #include "../render/Canvas.h"
 
 #include "../CGameInfo.h"
-#include "../CVideoHandler.h"
-#include "../CMusicHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CCreatureHandler.h"
@@ -216,7 +217,7 @@ void CHighScoreScreen::buttonExitClick()
 }
 
 CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc)
-	: CWindowObject(BORDERED), won(won), calc(calc), videoSoundHandle(-1)
+	: CWindowObject(BORDERED), won(won), calc(calc)
 {
 	addUsedEvents(LCLICK | KEYBOARD);
 
@@ -228,6 +229,9 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
 
 	if(won)
 	{
+
+		videoPlayer = std::make_shared<VideoWidget>(Point(0, 0), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK"), true);
+
 		int border = 100;
 		int textareaW = ((pos.w - 2 * border) / 4);
 		std::vector<std::string> t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank
@@ -242,9 +246,10 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
 		CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true);
 	}
 	else
+	{
+		videoPlayer = std::make_shared<VideoWidgetOnce>(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();});
 		CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true);
-
-	video = won ? "HSANIM.SMK" : "LOSEGAME.SMK";
+	}
 }
 
 int CHighScoreInputScreen::addEntry(std::string text) {
@@ -289,43 +294,7 @@ int CHighScoreInputScreen::addEntry(std::string text) {
 
 void CHighScoreInputScreen::show(Canvas & to)
 {
-	CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false,
-	[&]()
-	{
-		if(won)
-		{
-			CCS->videoh->close();
-			video = "HSLOOP.SMK";
-			auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video));
-			videoSoundHandle = CCS->soundh->playSound(audioData);
-			CCS->videoh->open(VideoPath::builtin(video));
-		}
-		else
-			close();
-	});
-	redraw();
-
-	CIntObject::show(to);
-}
-
-void CHighScoreInputScreen::activate()
-{
-	auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video));
-	videoSoundHandle = CCS->soundh->playSound(audioData);
-	if(!CCS->videoh->open(VideoPath::builtin(video)))
-	{
-		if(!won)
-			close();
-	}
-	else
-		background = nullptr;
-	CIntObject::activate();
-}
-
-void CHighScoreInputScreen::deactivate()
-{
-	CCS->videoh->close();
-	CCS->soundh->stopSound(videoSoundHandle);
+	CWindowObject::showAll(to);
 }
 
 void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)

+ 4 - 6
client/mainmenu/CHighScoreScreen.h

@@ -15,6 +15,7 @@ class CLabel;
 class CMultiLineLabel;
 class CAnimImage;
 class CTextInput;
+class VideoWidgetBase;
 
 class TransparentFilledRectangle;
 
@@ -93,9 +94,8 @@ class CHighScoreInputScreen : public CWindowObject
 	std::vector<std::shared_ptr<CMultiLineLabel>> texts;
 	std::shared_ptr<CHighScoreInput> input;
 	std::shared_ptr<TransparentFilledRectangle> background;
+	std::shared_ptr<VideoWidgetBase> videoPlayer;
 
-	std::string video;
-	int videoSoundHandle;
 	bool won;
 	HighScoreCalculation calc;
 public:
@@ -103,9 +103,7 @@ public:
 
 	int addEntry(std::string text);
 
-	void show(Canvas & to) override;
-	void activate() override;
-	void deactivate() override;
 	void clickPressed(const Point & cursorPosition) override;
 	void keyPressed(EShortcut key) override;
-};
+	void show(Canvas & to) override;
+};

+ 11 - 21
client/mainmenu/CMainMenu.cpp

@@ -17,6 +17,7 @@
 #include "../lobby/CBonusSelection.h"
 #include "../lobby/CSelectionBase.h"
 #include "../lobby/CLobbyScreen.h"
+#include "../media/IMusicPlayer.h"
 #include "../gui/CursorHandler.h"
 #include "../windows/GUIClasses.h"
 #include "../gui/CGuiHandler.h"
@@ -33,12 +34,11 @@
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../windows/InfoWindows.h"
 #include "../CServerHandler.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CVideoHandler.h"
 #include "../CPlayerInterface.h"
 #include "../Client.h"
 #include "../CMT.h"
@@ -92,8 +92,14 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
 	menuNameToEntry.push_back("credits");
 
 	tabs = std::make_shared<CTabbedInt>(std::bind(&CMenuScreen::createTab, this, _1));
-	if(config["video"].isNull())
+	if(!config["video"].isNull())
+	{
+		Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer());
+		videoPlayer = std::make_shared<VideoWidget>(videoPosition, VideoPath::fromJson(config["video"]["name"]), false);
+	}
+	else
 		tabs->setRedrawParent(true);
+
 }
 
 std::shared_ptr<CIntObject> CMenuScreen::createTab(size_t index)
@@ -106,32 +112,16 @@ std::shared_ptr<CIntObject> CMenuScreen::createTab(size_t index)
 
 void CMenuScreen::show(Canvas & to)
 {
-	if(!config["video"].isNull())
-	{
-		// redraw order: background -> video -> buttons and pictures
-		background->redraw();
-		CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false);
-		tabs->redraw();
-	}
-	CIntObject::show(to);
+	// TODO: avoid excessive redraws
+	CIntObject::showAll(to);
 }
 
 void CMenuScreen::activate()
 {
 	CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true);
-	if(!config["video"].isNull())
-		CCS->videoh->open(VideoPath::fromJson(config["video"]["name"]));
 	CIntObject::activate();
 }
 
-void CMenuScreen::deactivate()
-{
-	if(!config["video"].isNull())
-		CCS->videoh->close();
-
-	CIntObject::deactivate();
-}
-
 void CMenuScreen::switchToTab(size_t index)
 {
 	tabs->setActive(index);

+ 3 - 3
client/mainmenu/CMainMenu.h

@@ -28,7 +28,7 @@ class CAnimation;
 class CButton;
 class CFilledTexture;
 class CLabel;
-
+class VideoWidget;
 
 // TODO: Find new location for these enums
 enum class ESelectionScreen : ui8 {
@@ -48,6 +48,7 @@ class CMenuScreen : public CWindowObject
 	std::shared_ptr<CTabbedInt> tabs;
 
 	std::shared_ptr<CPicture> background;
+	std::shared_ptr<VideoWidget> videoPlayer;
 	std::vector<std::shared_ptr<CPicture>> images;
 
 	std::shared_ptr<CIntObject> createTab(size_t index);
@@ -57,9 +58,8 @@ public:
 
 	CMenuScreen(const JsonNode & configNode);
 
-	void show(Canvas & to) override;
 	void activate() override;
-	void deactivate() override;
+	void show(Canvas & to) override;
 
 	void switchToTab(size_t index);
 	void switchToTab(std::string name);

+ 23 - 10
client/mainmenu/CPrologEpilogVideo.cpp

@@ -12,15 +12,15 @@
 
 #include "CPrologEpilogVideo.h"
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CVideoHandler.h"
-#include "../gui/WindowHandler.h"
+#include "../media/IMusicPlayer.h"
+#include "../media/ISoundPlayer.h"
+//#include "../gui/WindowHandler.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/FramerateManager.h"
+//#include "../gui/FramerateManager.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../render/Canvas.h"
 
-
 CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback)
 	: CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0)
 {
@@ -29,9 +29,23 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f
 	pos = center(Rect(0, 0, 800, 600));
 	updateShadow();
 
-	auto audioData = CCS->videoh->getAudio(spe.prologVideo);
-	videoSoundHandle = CCS->soundh->playSound(audioData, -1);
-	CCS->videoh->open(spe.prologVideo);
+	//TODO: remove hardcoded paths. Some of campaigns video actually consist from 2 parts
+	// however, currently our campaigns format expects only	a single video file
+	static const std::map<VideoPath, VideoPath> pairedVideoFiles = {
+		{ VideoPath::builtin("EVIL2AP1"),  VideoPath::builtin("EVIL2AP2") },
+		{ VideoPath::builtin("H3ABdb4"),   VideoPath::builtin("H3ABdb4b") },
+		{ VideoPath::builtin("H3x2_RNe1"), VideoPath::builtin("H3x2_RNe2") },
+	};
+
+	if (pairedVideoFiles.count(spe.prologVideo))
+		videoPlayer = std::make_shared<VideoWidget>(Point(0, 0), spe.prologVideo, pairedVideoFiles.at(spe.prologVideo), true);
+	else
+		videoPlayer = std::make_shared<VideoWidget>(Point(0, 0), spe.prologVideo, true);
+
+	//some videos are 800x600 in size while some are 800x400
+	if (videoPlayer->pos.h == 400)
+		videoPlayer->moveBy(Point(0, 100));
+
 	CCS->musich->playMusic(spe.prologMusic, true, true);
 	voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice);
 	voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice);
@@ -65,9 +79,8 @@ void CPrologEpilogVideo::tick(uint32_t msPassed)
 void CPrologEpilogVideo::show(Canvas & to)
 {
 	to.drawColor(pos, Colors::BLACK);
-	//some videos are 800x600 in size while some are 800x400
-	CCS->videoh->update(pos.x, pos.y + (CCS->videoh->size().y == 400 ? 100 : 0), to.getInternalSurface(), true, false);
 
+	videoPlayer->show(to);
 	text->showAll(to); // blit text over video, if needed
 }
 

+ 2 - 0
client/mainmenu/CPrologEpilogVideo.h

@@ -13,6 +13,7 @@
 #include "../../lib/campaign/CampaignScenarioPrologEpilog.h"
 
 class CMultiLineLabel;
+class VideoWidget;
 
 class CPrologEpilogVideo : public CWindowObject
 {
@@ -25,6 +26,7 @@ class CPrologEpilogVideo : public CWindowObject
 	std::function<void()> exitCb;
 
 	std::shared_ptr<CMultiLineLabel> text;
+	std::shared_ptr<VideoWidget> videoPlayer;
 
 	bool voiceStopped = false;
 

+ 43 - 0
client/media/CAudioBase.cpp

@@ -0,0 +1,43 @@
+/*
+ * CAudioBase.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CAudioBase.h"
+
+#include <SDL_mixer.h>
+
+int CAudioBase::initializationCounter = 0;
+bool CAudioBase::initializeSuccess = false;
+
+CAudioBase::CAudioBase()
+{
+	if(initializationCounter == 0)
+	{
+		if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1)
+			logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError());
+		else
+			initializeSuccess = true;
+	}
+	++initializationCounter;
+}
+
+bool CAudioBase::isInitialized() const
+{
+	return initializeSuccess;
+}
+
+CAudioBase::~CAudioBase()
+{
+	--initializationCounter;
+
+	if(initializationCounter == 0 && initializeSuccess)
+		Mix_CloseAudio();
+
+	initializeSuccess = false;
+}

+ 22 - 0
client/media/CAudioBase.h

@@ -0,0 +1,22 @@
+/*
+ * CAudioBase.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+class CAudioBase : boost::noncopyable
+{
+	static int initializationCounter;
+	static bool initializeSuccess;
+
+protected:
+	bool isInitialized() const;
+
+	CAudioBase();
+	~CAudioBase();
+};

+ 38 - 0
client/media/CEmptyVideoPlayer.h

@@ -0,0 +1,38 @@
+/*
+ * CEmptyVideoPlayer.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "IVideoPlayer.h"
+
+class CEmptyVideoPlayer final : public IVideoPlayer
+{
+public:
+	/// Plays video on top of the screen, returns only after playback is over
+	bool playIntroVideo(const VideoPath & name) override
+	{
+		return false;
+	};
+
+	void playSpellbookAnimation(const VideoPath & name, const Point & position) override
+	{
+	}
+
+	/// Load video from specified path
+	std::unique_ptr<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) override
+	{
+		return nullptr;
+	};
+
+	/// Extracts audio data from provided video in wav format
+	std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) override
+	{
+		return {nullptr, 0};
+	};
+};

+ 349 - 0
client/media/CMusicHandler.cpp

@@ -0,0 +1,349 @@
+/*
+ * CMusicHandler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CMusicHandler.h"
+
+#include "../CGameInfo.h"
+#include "../eventsSDL/InputHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../renderSDL/SDLRWwrapper.h"
+
+#include "../../lib/CRandomGenerator.h"
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/filesystem/Filesystem.h"
+
+#include <SDL_mixer.h>
+
+void CMusicHandler::onVolumeChange(const JsonNode & volumeNode)
+{
+	setVolume(volumeNode.Integer());
+}
+
+CMusicHandler::CMusicHandler():
+	listener(settings.listen["general"]["music"])
+{
+	listener(std::bind(&CMusicHandler::onVolumeChange, this, _1));
+
+	auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) ->  bool
+	{
+		if(id.getType() != EResType::SOUND)
+			return false;
+
+		if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
+			return false;
+
+		logGlobal->trace("Found music file %s", id.getName());
+		return true;
+	});
+
+	for(const ResourcePath & file : mp3files)
+	{
+		if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat"))
+			addEntryToSet("battle", AudioPath::fromResource(file));
+		else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme"))
+			addEntryToSet("enemy-turn", AudioPath::fromResource(file));
+	}
+
+	if (isInitialized())
+	{
+		Mix_HookMusicFinished([]()
+		{
+			CCS->musich->musicFinishedCallback();
+		});
+	}
+}
+
+void CMusicHandler::loadTerrainMusicThemes()
+{
+	for(const auto & terrain : CGI->terrainTypeHandler->objects)
+	{
+		addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename);
+	}
+}
+
+void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI)
+{
+	musicsSet[set].push_back(musicURI);
+}
+
+CMusicHandler::~CMusicHandler()
+{
+	if(isInitialized())
+	{
+		boost::mutex::scoped_lock guard(mutex);
+
+		Mix_HookMusicFinished(nullptr);
+		current->stop();
+
+		current.reset();
+		next.reset();
+	}
+}
+
+void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart)
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	if(current && current->isPlaying() && current->isTrack(musicURI))
+		return;
+
+	queueNext(this, "", musicURI, loop, fromStart);
+}
+
+void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart)
+{
+	playMusicFromSet(musicSet + "_" + entryID, loop, fromStart);
+}
+
+void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart)
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	auto selectedSet = musicsSet.find(whichSet);
+	if(selectedSet == musicsSet.end())
+	{
+		logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
+		return;
+	}
+
+	if(current && current->isPlaying() && current->isSet(whichSet))
+		return;
+
+	// in this mode - play random track from set
+	queueNext(this, whichSet, AudioPath(), loop, fromStart);
+}
+
+void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
+{
+	if(!isInitialized())
+		return;
+
+	next = std::move(queued);
+
+	if(current == nullptr || !current->stop(1000))
+	{
+		current.reset(next.release());
+		current->play();
+	}
+}
+
+void CMusicHandler::queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart)
+{
+	queueNext(std::make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
+}
+
+void CMusicHandler::stopMusic(int fade_ms)
+{
+	if(!isInitialized())
+		return;
+
+	boost::mutex::scoped_lock guard(mutex);
+
+	if(current != nullptr)
+		current->stop(fade_ms);
+	next.reset();
+}
+
+ui32 CMusicHandler::getVolume() const
+{
+	return volume;
+}
+
+void CMusicHandler::setVolume(ui32 percent)
+{
+	volume = std::min(100u, percent);
+
+	if(isInitialized())
+		Mix_VolumeMusic((MIX_MAX_VOLUME * volume) / 100);
+}
+
+void CMusicHandler::musicFinishedCallback()
+{
+	// call music restart in separate thread to avoid deadlock in some cases
+	// It is possible for:
+	// 1) SDL thread to call this method on end of playback
+	// 2) VCMI code to call queueNext() method to queue new file
+	// this leads to:
+	// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
+	// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
+
+	GH.dispatchMainThread(
+		[this]()
+		{
+			boost::unique_lock lockGuard(mutex);
+			if(current != nullptr)
+			{
+				// if music is looped, play it again
+				if(current->play())
+					return;
+				else
+					current.reset();
+			}
+
+			if(current == nullptr && next != nullptr)
+			{
+				current.reset(next.release());
+				current->play();
+			}
+		}
+		);
+}
+
+MusicEntry::MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart)
+	: owner(owner)
+	, music(nullptr)
+	, setName(std::move(setName))
+	, startTime(static_cast<uint32_t>(-1))
+	, startPosition(0)
+	, loop(looped ? -1 : 1)
+	, fromStart(fromStart)
+	, playing(false)
+
+{
+	if(!musicURI.empty())
+		load(musicURI);
+}
+
+MusicEntry::~MusicEntry()
+{
+	if(playing && loop > 0)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while playing!");
+		Mix_HaltMusic();
+	}
+
+	if(loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while fading out!");
+		Mix_HaltMusic();
+	}
+
+	logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
+	if(music)
+		Mix_FreeMusic(music);
+}
+
+void MusicEntry::load(const AudioPath & musicURI)
+{
+	if(music)
+	{
+		logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
+		Mix_FreeMusic(music);
+		music = nullptr;
+	}
+
+	if(CResourceHandler::get()->existsResource(musicURI))
+		currentName = musicURI;
+	else
+		currentName = musicURI.addPrefix("MUSIC/");
+
+	music = nullptr;
+
+	logGlobal->trace("Loading music file %s", currentName.getOriginalName());
+
+	try
+	{
+		auto * musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName));
+		music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
+	}
+	catch(std::exception & e)
+	{
+		logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName());
+		logGlobal->error("Exception: %s", e.what());
+	}
+
+	if(!music)
+	{
+		logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError());
+		return;
+	}
+}
+
+bool MusicEntry::play()
+{
+	if(!(loop--) && music) //already played once - return
+		return false;
+
+	if(!setName.empty())
+	{
+		const auto & set = owner->musicsSet[setName];
+		const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault());
+		load(*iter);
+	}
+
+	logGlobal->trace("Playing music file %s", currentName.getOriginalName());
+
+	if(!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0)
+	{
+		float timeToStart = owner->trackPositions[currentName];
+		startPosition = std::round(timeToStart * 1000);
+
+		// erase stored position:
+		// if music track will be interrupted again - new position will be written in stop() method
+		// if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should
+		owner->trackPositions.erase(owner->trackPositions.find(currentName));
+
+		if(Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1)
+		{
+			logGlobal->error("Unable to play music (%s)", Mix_GetError());
+			return false;
+		}
+	}
+	else
+	{
+		startPosition = 0;
+
+		if(Mix_PlayMusic(music, 1) == -1)
+		{
+			logGlobal->error("Unable to play music (%s)", Mix_GetError());
+			return false;
+		}
+	}
+
+	startTime = GH.input().getTicks();
+
+	playing = true;
+	return true;
+}
+
+bool MusicEntry::stop(int fade_ms)
+{
+	if(Mix_PlayingMusic())
+	{
+		playing = false;
+		loop = 0;
+		uint32_t endTime = GH.input().getTicks();
+		assert(startTime != uint32_t(-1));
+		float playDuration = (endTime - startTime + startPosition) / 1000.f;
+		owner->trackPositions[currentName] = playDuration;
+		logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration);
+
+		Mix_FadeOutMusic(fade_ms);
+		return true;
+	}
+	return false;
+}
+
+bool MusicEntry::isPlaying() const
+{
+	return playing;
+}
+
+bool MusicEntry::isSet(const std::string & set)
+{
+	return !setName.empty() && set == setName;
+}
+
+bool MusicEntry::isTrack(const AudioPath & track)
+{
+	return setName.empty() && track == currentName;
+}

+ 95 - 0
client/media/CMusicHandler.h

@@ -0,0 +1,95 @@
+/*
+ * CMusicHandler.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CAudioBase.h"
+#include "IMusicPlayer.h"
+
+#include "../lib/CConfigHandler.h"
+
+struct _Mix_Music;
+using Mix_Music = struct _Mix_Music;
+
+class CMusicHandler;
+
+//Class for handling one music file
+class MusicEntry : boost::noncopyable
+{
+	CMusicHandler * owner;
+	Mix_Music * music;
+
+	//if not null - set from which music will be randomly selected
+	std::string setName;
+	AudioPath currentName;
+
+	uint32_t startTime;
+	uint32_t startPosition;
+	int loop; // -1 = indefinite
+	bool fromStart;
+	bool playing;
+
+	void load(const AudioPath & musicURI);
+
+public:
+	MusicEntry(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart);
+	~MusicEntry();
+
+	bool isSet(const std::string & setName);
+	bool isTrack(const AudioPath & trackName);
+	bool isPlaying() const;
+
+	bool play();
+	bool stop(int fade_ms = 0);
+};
+
+class CMusicHandler final : public CAudioBase, public IMusicPlayer
+{
+private:
+	//update volume on configuration change
+	SettingsListener listener;
+	void onVolumeChange(const JsonNode & volumeNode);
+
+	std::unique_ptr<MusicEntry> current;
+	std::unique_ptr<MusicEntry> next;
+
+	boost::mutex mutex;
+	int volume = 0; // from 0 (mute) to 100
+
+	void queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart);
+	void queueNext(std::unique_ptr<MusicEntry> queued);
+	void musicFinishedCallback() final;
+
+	/// map <set name> -> <list of URI's to tracks belonging to the said set>
+	std::map<std::string, std::vector<AudioPath>> musicsSet;
+	/// stored position, in seconds at which music player should resume playing this track
+	std::map<AudioPath, float> trackPositions;
+
+public:
+	CMusicHandler();
+	~CMusicHandler();
+
+	/// add entry with URI musicURI in set. Track will have ID musicID
+	void addEntryToSet(const std::string & set, const AudioPath & musicURI);
+
+	void loadTerrainMusicThemes() final;
+	void setVolume(ui32 percent) final;
+	ui32 getVolume() const final;
+
+	/// play track by URI, if loop = true music will be looped
+	void playMusic(const AudioPath & musicURI, bool loop, bool fromStart) final;
+	/// play random track from this set
+	void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart) final;
+	/// play random track from set (musicSet, entryID)
+	void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) final;
+	/// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any
+	void stopMusic(int fade_ms) final;
+
+	friend class MusicEntry;
+};

+ 385 - 0
client/media/CSoundHandler.cpp

@@ -0,0 +1,385 @@
+/*
+ * CMusicHandler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CSoundHandler.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../CGameInfo.h"
+
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/CRandomGenerator.h"
+
+#include <SDL_mixer.h>
+
+#define VCMI_SOUND_NAME(x)
+#define VCMI_SOUND_FILE(y) #y,
+
+// sounds mapped to soundBase enum
+static const std::string soundsList[] = {
+	"", // invalid
+	"", // todo
+	VCMI_SOUND_LIST
+};
+#undef VCMI_SOUND_NAME
+#undef VCMI_SOUND_FILE
+
+void CSoundHandler::onVolumeChange(const JsonNode & volumeNode)
+{
+	setVolume(volumeNode.Integer());
+}
+
+CSoundHandler::CSoundHandler():
+	listener(settings.listen["general"]["sound"]),
+	ambientConfig(JsonPath::builtin("config/ambientSounds.json"))
+{
+	listener(std::bind(&CSoundHandler::onVolumeChange, this, _1));
+
+	if(ambientConfig["allocateChannels"].isNumber())
+		Mix_AllocateChannels(ambientConfig["allocateChannels"].Integer());
+
+	if(isInitialized())
+	{
+		Mix_ChannelFinished([](int channel)
+		{
+			CCS->soundh->soundFinishedCallback(channel);
+		});
+	}
+}
+
+CSoundHandler::~CSoundHandler()
+{
+	if(isInitialized())
+	{
+		Mix_HaltChannel(-1);
+
+		for(auto & chunk : soundChunks)
+		{
+			if(chunk.second.first)
+				Mix_FreeChunk(chunk.second.first);
+		}
+	}
+}
+
+// Allocate an SDL chunk and cache it.
+Mix_Chunk * CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache)
+{
+	try
+	{
+		if(cache && soundChunks.find(sound) != soundChunks.end())
+			return soundChunks[sound].first;
+
+		auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll();
+		SDL_RWops * ops = SDL_RWFromMem(data.first.get(), data.second);
+		Mix_Chunk * chunk = Mix_LoadWAV_RW(ops, 1); // will free ops
+
+		if(cache)
+			soundChunks.insert({sound, std::make_pair(chunk, std::move(data.first))});
+
+		return chunk;
+	}
+	catch(std::exception & e)
+	{
+		logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what());
+		return nullptr;
+	}
+}
+
+Mix_Chunk * CSoundHandler::GetSoundChunk(std::pair<std::unique_ptr<ui8[]>, si64> & data, bool cache)
+{
+	try
+	{
+		std::vector<ui8> startBytes = std::vector<ui8>(data.first.get(), data.first.get() + std::min(static_cast<si64>(100), data.second));
+
+		if(cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end())
+			return soundChunksRaw[startBytes].first;
+
+		SDL_RWops * ops = SDL_RWFromMem(data.first.get(), data.second);
+		Mix_Chunk * chunk = Mix_LoadWAV_RW(ops, 1); // will free ops
+
+		if(cache)
+			soundChunksRaw.insert({startBytes, std::make_pair(chunk, std::move(data.first))});
+
+		return chunk;
+	}
+	catch(std::exception & e)
+	{
+		logGlobal->warn("Cannot get sound chunk: %s", e.what());
+		return nullptr;
+	}
+}
+
+int CSoundHandler::ambientDistToVolume(int distance) const
+{
+	const auto & distancesVector = ambientConfig["distances"].Vector();
+
+	if(distance >= distancesVector.size())
+		return 0;
+
+	int volumeByDistance = static_cast<int>(distancesVector[distance].Integer());
+	return volumeByDistance * ambientConfig["volume"].Integer() / 100;
+}
+
+void CSoundHandler::ambientStopSound(const AudioPath & soundId)
+{
+	stopSound(ambientChannels[soundId]);
+	setChannelVolume(ambientChannels[soundId], volume);
+}
+
+uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound)
+{
+	if(!isInitialized() || sound.empty())
+		return 0;
+
+	auto resourcePath = sound.addPrefix("SOUNDS/");
+
+	if(!CResourceHandler::get()->existsResource(resourcePath))
+		return 0;
+
+	auto data = CResourceHandler::get()->load(resourcePath)->readAll();
+
+	SDL_AudioSpec spec;
+	uint32_t audioLen;
+	uint8_t * audioBuf;
+	uint32_t miliseconds = 0;
+
+	if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), data.second), 1, &spec, &audioBuf, &audioLen) != nullptr)
+	{
+		SDL_FreeWAV(audioBuf);
+		uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8;
+		uint32_t sampleCount = audioLen / sampleSize;
+		uint32_t sampleLen = sampleCount / spec.channels;
+		miliseconds = 1000 * sampleLen / spec.freq;
+	}
+
+	return miliseconds;
+}
+
+// Plays a sound, and return its channel so we can fade it out later
+int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
+{
+	assert(soundID < soundBase::sound_after_last);
+	auto sound = AudioPath::builtin(soundsList[soundID]);
+	logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName());
+
+	return playSound(sound, repeats, true);
+}
+
+int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache)
+{
+	if(!isInitialized() || sound.empty())
+		return -1;
+
+	int channel;
+	Mix_Chunk * chunk = GetSoundChunk(sound, cache);
+
+	if(chunk)
+	{
+		channel = Mix_PlayChannel(-1, chunk, repeats);
+		if(channel == -1)
+		{
+			logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError());
+			if(!cache)
+				Mix_FreeChunk(chunk);
+		}
+		else if(cache)
+			initCallback(channel);
+		else
+			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
+	}
+	else
+		channel = -1;
+
+	return channel;
+}
+
+int CSoundHandler::playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats, bool cache)
+{
+	int channel = -1;
+	if(Mix_Chunk * chunk = GetSoundChunk(data, cache))
+	{
+		channel = Mix_PlayChannel(-1, chunk, repeats);
+		if(channel == -1)
+		{
+			logGlobal->error("Unable to play sound, error %s", Mix_GetError());
+			if(!cache)
+				Mix_FreeChunk(chunk);
+		}
+		else if(cache)
+			initCallback(channel);
+		else
+			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
+	}
+	return channel;
+}
+
+// Helper. Randomly select a sound from an array and play it
+int CSoundHandler::playSoundFromSet(std::vector<soundBase::soundID> & sound_vec)
+{
+	return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault()));
+}
+
+void CSoundHandler::stopSound(int handler)
+{
+	if(isInitialized() && handler != -1)
+		Mix_HaltChannel(handler);
+}
+
+ui32 CSoundHandler::getVolume() const
+{
+	return volume;
+}
+
+// Sets the sound volume, from 0 (mute) to 100
+void CSoundHandler::setVolume(ui32 percent)
+{
+	volume = std::min(100u, percent);
+
+	if(isInitialized())
+	{
+		setChannelVolume(-1, volume);
+
+		for(const auto & channel : channelVolumes)
+			updateChannelVolume(channel.first);
+	}
+}
+
+void CSoundHandler::updateChannelVolume(int channel)
+{
+	if(channelVolumes.count(channel))
+		setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100);
+	else
+		setChannelVolume(channel, getVolume());
+}
+
+// Sets the sound volume, from 0 (mute) to 100
+void CSoundHandler::setChannelVolume(int channel, ui32 percent)
+{
+	Mix_Volume(channel, (MIX_MAX_VOLUME * percent) / 100);
+}
+
+void CSoundHandler::setCallback(int channel, std::function<void()> function)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+
+	auto iter = callbacks.find(channel);
+
+	//channel not found. It may have finished so fire callback now
+	if(iter == callbacks.end())
+		function();
+	else
+		iter->second.push_back(function);
+}
+
+void CSoundHandler::resetCallback(int channel)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+
+	callbacks.erase(channel);
+}
+
+void CSoundHandler::soundFinishedCallback(int channel)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+
+	if(callbacks.count(channel) == 0)
+		return;
+
+	// store callbacks from container locally - SDL might reuse this channel for another sound
+	// but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own
+	auto callback = callbacks.at(channel);
+	callbacks.erase(channel);
+
+	if(!callback.empty())
+	{
+		GH.dispatchMainThread(
+			[callback]()
+			{
+				for(const auto & entry : callback)
+					entry();
+			}
+			);
+	}
+}
+
+void CSoundHandler::initCallback(int channel)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+	assert(callbacks.count(channel) == 0);
+	callbacks[channel] = {};
+}
+
+void CSoundHandler::initCallback(int channel, const std::function<void()> & function)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+	assert(callbacks.count(channel) == 0);
+	callbacks[channel].push_back(function);
+}
+
+int CSoundHandler::ambientGetRange() const
+{
+	return ambientConfig["range"].Integer();
+}
+
+void CSoundHandler::ambientUpdateChannels(std::map<AudioPath, int> soundsArg)
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	std::vector<AudioPath> stoppedSounds;
+	for(const auto & pair : ambientChannels)
+	{
+		const auto & soundId = pair.first;
+		const int channel = pair.second;
+
+		if(!vstd::contains(soundsArg, soundId))
+		{
+			ambientStopSound(soundId);
+			stoppedSounds.push_back(soundId);
+		}
+		else
+		{
+			int channelVolume = ambientDistToVolume(soundsArg[soundId]);
+			channelVolumes[channel] = channelVolume;
+			updateChannelVolume(channel);
+		}
+	}
+	for(const auto & soundId : stoppedSounds)
+	{
+		channelVolumes.erase(ambientChannels[soundId]);
+		ambientChannels.erase(soundId);
+	}
+
+	for(const auto & pair : soundsArg)
+	{
+		const auto & soundId = pair.first;
+		const int distance = pair.second;
+
+		if(!vstd::contains(ambientChannels, soundId))
+		{
+			int channel = playSound(soundId, -1);
+			int channelVolume = ambientDistToVolume(distance);
+			channelVolumes[channel] = channelVolume;
+
+			updateChannelVolume(channel);
+			ambientChannels[soundId] = channel;
+		}
+	}
+}
+
+void CSoundHandler::ambientStopAllChannels()
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	for(const auto & ch : ambientChannels)
+	{
+		ambientStopSound(ch.first);
+	}
+	channelVolumes.clear();
+	ambientChannels.clear();
+}

+ 78 - 0
client/media/CSoundHandler.h

@@ -0,0 +1,78 @@
+/*
+ * CSoundHandler.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CAudioBase.h"
+#include "ISoundPlayer.h"
+
+#include "../lib/CConfigHandler.h"
+
+struct Mix_Chunk;
+
+class CSoundHandler final : public CAudioBase, public ISoundPlayer
+{
+private:
+	//update volume on configuration change
+	SettingsListener listener;
+	void onVolumeChange(const JsonNode & volumeNode);
+
+	using CachedChunk = std::pair<Mix_Chunk *, std::unique_ptr<ui8[]>>;
+	std::map<AudioPath, CachedChunk> soundChunks;
+	std::map<std::vector<ui8>, CachedChunk> soundChunksRaw;
+
+	Mix_Chunk * GetSoundChunk(const AudioPath & sound, bool cache);
+	Mix_Chunk * GetSoundChunk(std::pair<std::unique_ptr<ui8[]>, si64> & data, bool cache);
+
+	/// have entry for every currently active channel
+	/// vector will be empty if callback was not set
+	std::map<int, std::vector<std::function<void()>>> callbacks;
+
+	/// Protects access to callbacks member to avoid data races:
+	/// SDL calls sound finished callbacks from audio thread
+	boost::mutex mutexCallbacks;
+
+	int ambientDistToVolume(int distance) const;
+	void ambientStopSound(const AudioPath & soundId);
+	void updateChannelVolume(int channel);
+
+	const JsonNode ambientConfig;
+
+	boost::mutex mutex;
+	std::map<AudioPath, int> ambientChannels;
+	std::map<int, int> channelVolumes;
+	int volume = 0;
+
+	void initCallback(int channel, const std::function<void()> & function);
+	void initCallback(int channel);
+
+public:
+	CSoundHandler();
+	~CSoundHandler();
+
+	ui32 getVolume() const final;
+	void setVolume(ui32 percent) final;
+	void setChannelVolume(int channel, ui32 percent);
+
+	// Sounds
+	uint32_t getSoundDurationMilliseconds(const AudioPath & sound) final;
+	int playSound(soundBase::soundID soundID, int repeats = 0) final;
+	int playSound(const AudioPath & sound, int repeats = 0, bool cache = false) final;
+	int playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) final;
+	int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) final;
+	void stopSound(int handler) final;
+
+	void setCallback(int channel, std::function<void()> function) final;
+	void resetCallback(int channel) final;
+	void soundFinishedCallback(int channel) final;
+
+	int ambientGetRange() const final;
+	void ambientUpdateChannels(std::map<AudioPath, int> currentSounds) final;
+	void ambientStopAllChannels() final;
+};

+ 658 - 0
client/media/CVideoHandler.cpp

@@ -0,0 +1,658 @@
+/*
+ * CVideoHandler.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CVideoHandler.h"
+
+#ifndef DISABLE_VIDEO
+
+#include "ISoundPlayer.h"
+
+#include "../CGameInfo.h"
+#include "../CMT.h"
+#include "../eventsSDL/InputHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/Canvas.h"
+#include "../renderSDL/SDL_Extensions.h"
+
+#include "../../lib/filesystem/CInputStream.h"
+#include "../../lib/filesystem/Filesystem.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/Languages.h"
+
+#include <SDL_render.h>
+
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/imgutils.h>
+#include <libswscale/swscale.h>
+}
+
+// Define a set of functions to read data
+static int lodRead(void * opaque, uint8_t * buf, int size)
+{
+	auto * data = static_cast<CInputStream *>(opaque);
+	auto bytesRead = data->read(buf, size);
+	if(bytesRead == 0)
+		return AVERROR_EOF;
+
+	return bytesRead;
+}
+
+static si64 lodSeek(void * opaque, si64 pos, int whence)
+{
+	auto * data = static_cast<CInputStream *>(opaque);
+
+	if(whence & AVSEEK_SIZE)
+		return data->getSize();
+
+	return data->seek(pos);
+}
+
+[[noreturn]] static void throwFFmpegError(int errorCode)
+{
+	std::array<char, AV_ERROR_MAX_STRING_SIZE> errorMessage{};
+	av_strerror(errorCode, errorMessage.data(), errorMessage.size());
+
+	throw std::runtime_error(errorMessage.data());
+}
+
+static std::unique_ptr<CInputStream> findVideoData(const VideoPath & videoToOpen)
+{
+	if(CResourceHandler::get()->existsResource(videoToOpen))
+		return CResourceHandler::get()->load(videoToOpen);
+
+	auto highQualityVideoToOpenWithDir = videoToOpen.addPrefix("VIDEO/");
+	auto lowQualityVideo = videoToOpen.toType<EResType::VIDEO_LOW_QUALITY>();
+	auto lowQualityVideoWithDir = highQualityVideoToOpenWithDir.toType<EResType::VIDEO_LOW_QUALITY>();
+
+	if(CResourceHandler::get()->existsResource(highQualityVideoToOpenWithDir))
+		return CResourceHandler::get()->load(highQualityVideoToOpenWithDir);
+
+	if(CResourceHandler::get()->existsResource(lowQualityVideo))
+		return CResourceHandler::get()->load(lowQualityVideo);
+
+	if(CResourceHandler::get()->existsResource(lowQualityVideoWithDir))
+		return CResourceHandler::get()->load(lowQualityVideoWithDir);
+
+	return nullptr;
+}
+
+bool FFMpegStream::openInput(const VideoPath & videoToOpen)
+{
+	input = findVideoData(videoToOpen);
+
+	return input != nullptr;
+}
+
+void FFMpegStream::openContext()
+{
+	static const int BUFFER_SIZE = 4096;
+	input->seek(0);
+
+	auto * buffer = static_cast<unsigned char *>(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg
+	context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek);
+
+	formatContext = avformat_alloc_context();
+	formatContext->pb = context;
+	// filename is not needed - file was already open and stored in this->data;
+	int avfopen = avformat_open_input(&formatContext, "dummyFilename", nullptr, nullptr);
+
+	if(avfopen != 0)
+		throwFFmpegError(avfopen);
+
+	// Retrieve stream information
+	int findStreamInfo = avformat_find_stream_info(formatContext, nullptr);
+
+	if(avfopen < 0)
+		throwFFmpegError(findStreamInfo);
+}
+
+void FFMpegStream::openCodec(int desiredStreamIndex)
+{
+	streamIndex = desiredStreamIndex;
+
+	// Find the decoder for the stream
+	codec = avcodec_find_decoder(formatContext->streams[streamIndex]->codecpar->codec_id);
+
+	if(codec == nullptr)
+		throw std::runtime_error("Unsupported codec");
+
+	codecContext = avcodec_alloc_context3(codec);
+	if(codecContext == nullptr)
+		throw std::runtime_error("Failed to create codec context");
+
+	// Get a pointer to the codec context for the video stream
+	int ret = avcodec_parameters_to_context(codecContext, formatContext->streams[streamIndex]->codecpar);
+	if(ret < 0)
+	{
+		//We cannot get codec from parameters
+		avcodec_free_context(&codecContext);
+		throwFFmpegError(ret);
+	}
+
+	// Open codec
+	ret = avcodec_open2(codecContext, codec, nullptr);
+	if(ret < 0)
+	{
+		// Could not open codec
+		codec = nullptr;
+		throwFFmpegError(ret);
+	}
+
+	// Allocate video frame
+	frame = av_frame_alloc();
+}
+
+const AVCodecParameters * FFMpegStream::getCodecParameters() const
+{
+	return formatContext->streams[streamIndex]->codecpar;
+}
+
+const AVCodecContext * FFMpegStream::getCodecContext() const
+{
+	return codecContext;
+}
+
+const AVFrame * FFMpegStream::getCurrentFrame() const
+{
+	return frame;
+}
+
+void CVideoInstance::openVideo()
+{
+	openContext();
+	openCodec(findVideoStream());
+}
+
+void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput)
+{
+	//setup scaling
+	if(scaleToScreenSize)
+	{
+		dimensions.x = screen->w;
+		dimensions.y = screen->h;
+	}
+	else
+	{
+		dimensions.x  = getCodecContext()->width;
+		dimensions.y = getCodecContext()->height;
+	}
+
+	// Allocate a place to put our YUV image on that screen
+	if (useTextureOutput)
+	{
+		std::array potentialFormats = {
+			AV_PIX_FMT_YUV420P, // -> SDL_PIXELFORMAT_IYUV - most of H3 videos use YUV format, so it is preferred to save some space & conversion time
+			AV_PIX_FMT_RGB32,   // -> SDL_PIXELFORMAT_ARGB8888 - some .smk videos actually use palette, so RGB > YUV. This is also our screen texture format
+			AV_PIX_FMT_NONE
+		};
+
+		auto preferredFormat = avcodec_find_best_pix_fmt_of_list(potentialFormats.data(), getCodecContext()->pix_fmt, false, nullptr);
+
+		if (preferredFormat == AV_PIX_FMT_YUV420P)
+			textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y);
+		else
+			textureRGB = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y);
+		sws = sws_getContext(getCodecContext()->width, getCodecContext()->height, getCodecContext()->pix_fmt,
+							dimensions.x, dimensions.y, preferredFormat,
+							 SWS_BICUBIC, nullptr, nullptr, nullptr);
+	}
+	else
+	{
+		surface = CSDL_Ext::newSurface(dimensions.x, dimensions.y);
+		sws = sws_getContext(getCodecContext()->width, getCodecContext()->height, getCodecContext()->pix_fmt,
+							 dimensions.x, dimensions.y, AV_PIX_FMT_RGB32,
+							 SWS_BICUBIC, nullptr, nullptr, nullptr);
+	}
+
+	if (sws == nullptr)
+		throw std::runtime_error("Failed to create sws");
+}
+
+void FFMpegStream::decodeNextFrame()
+{
+	AVPacket packet;
+
+	for(;;)
+	{
+		int rc = avcodec_receive_frame(codecContext, frame);
+		if(rc == AVERROR(EAGAIN))
+			break;
+
+		if(rc < 0)
+			throwFFmpegError(rc);
+
+		return;
+	}
+
+	for(;;)
+	{
+		int ret = av_read_frame(formatContext, &packet);
+		if(ret < 0)
+		{
+			if(ret == AVERROR_EOF)
+			{
+				av_packet_unref(&packet);
+				av_frame_free(&frame);
+				frame = nullptr;
+				return;
+			}
+			throwFFmpegError(ret);
+		}
+
+		// Is this a packet from the video stream?
+		if(packet.stream_index == streamIndex)
+		{
+			// Decode video frame
+			int rc = avcodec_send_packet(codecContext, &packet);
+			if(rc < 0 && rc != AVERROR(EAGAIN))
+				throwFFmpegError(rc);
+
+			rc = avcodec_receive_frame(codecContext, frame);
+			if(rc == AVERROR(EAGAIN))
+			{
+				av_packet_unref(&packet);
+				continue;
+			}
+			if(rc < 0)
+				throwFFmpegError(rc);
+
+			av_packet_unref(&packet);
+			return;
+		}
+		av_packet_unref(&packet);
+	}
+}
+
+bool CVideoInstance::loadNextFrame()
+{
+	decodeNextFrame();
+	const AVFrame * frame = getCurrentFrame();
+
+	if(!frame)
+		return false;
+
+	uint8_t * data[4] = {};
+	int linesize[4] = {};
+
+	if(textureYUV)
+	{
+		av_image_alloc(data, linesize, dimensions.x, dimensions.y, AV_PIX_FMT_YUV420P, 1);
+		sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize);
+		SDL_UpdateYUVTexture(textureYUV, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]);
+		av_freep(&data[0]);
+	}
+	if(textureRGB)
+	{
+		av_image_alloc(data, linesize, dimensions.x, dimensions.y, AV_PIX_FMT_RGB32, 1);
+		sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize);
+		SDL_UpdateTexture(textureRGB, nullptr, data[0], linesize[0]);
+		av_freep(&data[0]);
+	}
+	if(surface)
+	{
+		// Avoid buffer overflow caused by sws_scale():
+		// http://trac.ffmpeg.org/ticket/9254
+
+		size_t pic_bytes = surface->pitch * surface->h;
+		size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
+		void * for_sws = av_malloc(pic_bytes + ffmped_pad);
+		data[0] = (ui8 *)for_sws;
+		linesize[0] = surface->pitch;
+
+		sws_scale(sws, frame->data, frame->linesize, 0, getCodecContext()->height, data, linesize);
+		memcpy(surface->pixels, for_sws, pic_bytes);
+		av_free(for_sws);
+	}
+	return true;
+}
+
+bool CVideoInstance::videoEnded()
+{
+	return getCurrentFrame() == nullptr;
+}
+
+CVideoInstance::~CVideoInstance()
+{
+	sws_freeContext(sws);
+	SDL_DestroyTexture(textureYUV);
+	SDL_DestroyTexture(textureRGB);
+	SDL_FreeSurface(surface);
+}
+
+FFMpegStream::~FFMpegStream()
+{
+	av_frame_free(&frame);
+
+	avcodec_close(codecContext);
+	avcodec_free_context(&codecContext);
+
+	avcodec_close(codecContext);
+	avcodec_free_context(&codecContext);
+
+	avformat_close_input(&formatContext);
+	av_free(context);
+}
+
+Point CVideoInstance::size()
+{
+	if(!getCurrentFrame())
+		throw std::runtime_error("Invalid video frame!");
+
+	return Point(getCurrentFrame()->width, getCurrentFrame()->height);
+}
+
+void CVideoInstance::show(const Point & position, Canvas & canvas)
+{
+	if(sws == nullptr)
+		throw std::runtime_error("No video to show!");
+
+	CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position);
+}
+
+double FFMpegStream::getCurrentFrameEndTime() const
+{
+#if(LIBAVUTIL_VERSION_MAJOR < 58)
+	auto packet_duration = frame->pkt_duration;
+#else
+	auto packet_duration = frame->duration;
+#endif
+	return (frame->pts + packet_duration) * av_q2d(formatContext->streams[streamIndex]->time_base);
+}
+
+double FFMpegStream::getCurrentFrameDuration() const
+{
+#if(LIBAVUTIL_VERSION_MAJOR < 58)
+	auto packet_duration = frame->pkt_duration;
+#else
+	auto packet_duration = frame->duration;
+#endif
+	return packet_duration * av_q2d(formatContext->streams[streamIndex]->time_base);
+}
+
+void CVideoInstance::tick(uint32_t msPassed)
+{
+	if(sws == nullptr)
+		throw std::runtime_error("No video to show!");
+
+	if(videoEnded())
+		throw std::runtime_error("Video already ended!");
+
+	frameTime += msPassed / 1000.0;
+
+	if(frameTime >= getCurrentFrameEndTime())
+		loadNextFrame();
+}
+
+struct FFMpegFormatDescription
+{
+	uint8_t sampleSizeBytes;
+	uint8_t wavFormatID;
+	bool isPlanar;
+};
+
+static FFMpegFormatDescription getAudioFormatProperties(int audioFormat)
+{
+	switch (audioFormat)
+	{
+		case AV_SAMPLE_FMT_U8:   return { 1, 1, false};
+		case AV_SAMPLE_FMT_U8P:  return { 1, 1, true};
+		case AV_SAMPLE_FMT_S16:  return { 2, 1, false};
+		case AV_SAMPLE_FMT_S16P: return { 2, 1, true};
+		case AV_SAMPLE_FMT_S32:  return { 4, 1, false};
+		case AV_SAMPLE_FMT_S32P: return { 4, 1, true};
+		case AV_SAMPLE_FMT_S64:  return { 8, 1, false};
+		case AV_SAMPLE_FMT_S64P: return { 8, 1, true};
+		case AV_SAMPLE_FMT_FLT:  return { 4, 3, false};
+		case AV_SAMPLE_FMT_FLTP: return { 4, 3, true};
+		case AV_SAMPLE_FMT_DBL:  return { 8, 3, false};
+		case AV_SAMPLE_FMT_DBLP: return { 8, 3, true};
+	}
+	throw std::runtime_error("Invalid audio format");
+}
+
+int FFMpegStream::findAudioStream() const
+{
+	std::vector<int> audioStreamIndices;
+
+	for(int i = 0; i < formatContext->nb_streams; i++)
+		if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
+			audioStreamIndices.push_back(i);
+
+	if (audioStreamIndices.empty())
+		return -1;
+
+	if (audioStreamIndices.size() == 1)
+		return audioStreamIndices.front();
+
+	// multiple audio streams - try to pick best one based on language settings
+	std::map<int, std::string> streamToLanguage;
+
+	// Approach 1 - check if stream has language set in metadata
+	for (auto const & index : audioStreamIndices)
+	{
+		const AVDictionaryEntry *e = av_dict_get(formatContext->streams[index]->metadata, "language", nullptr, 0);
+		if (e)
+			streamToLanguage[index]	= e->value;
+	}
+
+	// Approach 2 - no metadata found. This may be video from Chronicles which have predefined (presumably hardcoded) list of languages
+	if (streamToLanguage.empty())
+	{
+		if (audioStreamIndices.size() == 2)
+		{
+			streamToLanguage[audioStreamIndices[0]] = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).tagISO2;
+			streamToLanguage[audioStreamIndices[1]] = Languages::getLanguageOptions(Languages::ELanguages::GERMAN).tagISO2;
+		}
+
+		if (audioStreamIndices.size() == 5)
+		{
+			streamToLanguage[audioStreamIndices[0]] = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).tagISO2;
+			streamToLanguage[audioStreamIndices[1]] = Languages::getLanguageOptions(Languages::ELanguages::FRENCH).tagISO2;
+			streamToLanguage[audioStreamIndices[2]] = Languages::getLanguageOptions(Languages::ELanguages::GERMAN).tagISO2;
+			streamToLanguage[audioStreamIndices[3]] = Languages::getLanguageOptions(Languages::ELanguages::ITALIAN).tagISO2;
+			streamToLanguage[audioStreamIndices[4]] = Languages::getLanguageOptions(Languages::ELanguages::SPANISH).tagISO2;
+		}
+	}
+
+	std::string preferredLanguageName = CGI->generaltexth->getPreferredLanguage();
+	std::string preferredTag = Languages::getLanguageOptions(preferredLanguageName).tagISO2;
+
+	for (auto const & entry : streamToLanguage)
+		if (entry.second == preferredTag)
+			return entry.first;
+
+	return audioStreamIndices.front();
+}
+
+int FFMpegStream::findVideoStream() const
+{
+	for(int i = 0; i < formatContext->nb_streams; i++)
+		if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+			return i;
+
+	return -1;
+}
+
+std::pair<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const VideoPath & videoToOpen)
+{
+	if (!openInput(videoToOpen))
+		return { nullptr, 0};
+	openContext();
+	openCodec(findAudioStream());
+
+	const auto * codecpar = getCodecParameters();
+
+	std::vector<ui8> samples;
+
+	auto formatProperties = getAudioFormatProperties(codecpar->format);
+#if(LIBAVUTIL_VERSION_MAJOR < 58)
+	int numChannels = codecpar->channels;
+#else
+	int numChannels = codecpar->ch_layout.nb_channels;
+#endif
+
+	samples.reserve(44100 * 5); // arbitrary 5-second buffer
+
+	for (;;)
+	{
+		decodeNextFrame();
+		const AVFrame * frame = getCurrentFrame();
+
+		if (!frame)
+			break;
+
+		int samplesToRead = frame->nb_samples * numChannels;
+		int bytesToRead = samplesToRead * formatProperties.sampleSizeBytes;
+
+		if (formatProperties.isPlanar && numChannels > 1)
+		{
+			// Workaround for lack of resampler
+			// Currently, ffmpeg on conan systems is built without sws resampler
+			// Because of that, and because wav format does not supports 'planar' formats from ffmpeg
+			// we need to de-planarize it and convert to "normal" (non-planar / interleaved) stream
+			samples.reserve(samples.size() + bytesToRead);
+			for (int sm = 0; sm < frame->nb_samples; ++sm)
+				for (int ch = 0; ch < numChannels; ++ch)
+					samples.insert(samples.end(), frame->data[ch] + sm * formatProperties.sampleSizeBytes, frame->data[ch] + (sm+1) * formatProperties.sampleSizeBytes );
+		}
+		else
+		{
+			samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead);
+		}
+	}
+
+	struct WavHeader {
+		ui8 RIFF[4] = {'R', 'I', 'F', 'F'};
+		ui32 ChunkSize;
+		ui8 WAVE[4] = {'W', 'A', 'V', 'E'};
+		ui8 fmt[4] = {'f', 'm', 't', ' '};
+		ui32 Subchunk1Size = 16;
+		ui16 AudioFormat = 1;
+		ui16 NumOfChan = 2;
+		ui32 SamplesPerSec = 22050;
+		ui32 bytesPerSec = 22050 * 2;
+		ui16 blockAlign = 2;
+		ui16 bitsPerSample = 32;
+		ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'};
+		ui32 Subchunk2Size;
+	};
+
+	WavHeader wav;
+	wav.ChunkSize = samples.size() + sizeof(WavHeader) - 8;
+	wav.AudioFormat = formatProperties.wavFormatID; // 1 = PCM, 3 = IEEE float
+	wav.NumOfChan = numChannels;
+	wav.SamplesPerSec = codecpar->sample_rate;
+	wav.bytesPerSec = codecpar->sample_rate * formatProperties.sampleSizeBytes;
+	wav.bitsPerSample = formatProperties.sampleSizeBytes * 8;
+	wav.Subchunk2Size = samples.size() + sizeof(WavHeader) - 44;
+	auto * wavPtr = reinterpret_cast<ui8*>(&wav);
+
+	auto dat = std::make_pair(std::make_unique<ui8[]>(samples.size() + sizeof(WavHeader)), samples.size() + sizeof(WavHeader));
+	std::copy(wavPtr, wavPtr + sizeof(WavHeader), dat.first.get());
+	std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(WavHeader));
+
+	return dat;
+}
+
+bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey)
+{
+	CVideoInstance instance;
+	CAudioInstance audio;
+
+	auto extractedAudio = audio.extractAudio(name);
+	int audioHandle = CCS->soundh->playSound(extractedAudio);
+
+	if (!instance.openInput(name))
+		return true;
+
+	instance.openVideo();
+	instance.prepareOutput(scale, useOverlay);
+
+	auto lastTimePoint = boost::chrono::steady_clock::now();
+
+	while(instance.loadNextFrame())
+	{
+		if(stopOnKey)
+		{
+			GH.input().fetchEvents();
+			if(GH.input().ignoreEventsUntilInput())
+			{
+				CCS->soundh->stopSound(audioHandle);
+				return false;
+			}
+		}
+
+		SDL_Rect rect;
+		rect.x = position.x;
+		rect.y = position.y;
+		rect.w = instance.dimensions.x;
+		rect.h = instance.dimensions.y;
+
+		if(useOverlay)
+			SDL_RenderFillRect(mainRenderer, &rect);
+		else
+			SDL_RenderClear(mainRenderer);
+
+		if(instance.textureYUV)
+			SDL_RenderCopy(mainRenderer, instance.textureYUV, nullptr, &rect);
+		else
+			SDL_RenderCopy(mainRenderer, instance.textureRGB, nullptr, &rect);
+
+		SDL_RenderPresent(mainRenderer);
+
+		// Framerate delay
+		double targetFrameTimeSeconds = instance.getCurrentFrameDuration();
+		auto targetFrameTime = boost::chrono::milliseconds(static_cast<int>(1000 * targetFrameTimeSeconds));
+
+		auto timePointAfterPresent = boost::chrono::steady_clock::now();
+		auto timeSpentBusy = boost::chrono::duration_cast<boost::chrono::milliseconds>(timePointAfterPresent - lastTimePoint);
+
+		logGlobal->info("Sleeping for %d", (targetFrameTime - timeSpentBusy).count());
+		if(targetFrameTime > timeSpentBusy)
+			boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy);
+
+		lastTimePoint = boost::chrono::steady_clock::now();
+	}
+	return true;
+}
+
+bool CVideoPlayer::playIntroVideo(const VideoPath & name)
+{
+	return openAndPlayVideoImpl(name, Point(0, 0), true, true, true);
+}
+
+void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position)
+{
+	openAndPlayVideoImpl(name, position, false, false, false);
+}
+
+std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, bool scaleToScreen)
+{
+	auto result = std::make_unique<CVideoInstance>();
+
+	if (!result->openInput(name))
+		return nullptr;
+
+	result->openVideo();
+	result->prepareOutput(scaleToScreen, false);
+	result->loadNextFrame(); // prepare 1st frame
+
+	return result;
+}
+
+std::pair<std::unique_ptr<ui8[]>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
+{
+	CAudioInstance audio;
+	return audio.extractAudio(videoToOpen);
+}
+
+#endif

+ 110 - 0
client/media/CVideoHandler.h

@@ -0,0 +1,110 @@
+/*
+ * CVideoHandler.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#ifndef DISABLE_VIDEO
+
+#include "../lib/Point.h"
+#include "IVideoPlayer.h"
+
+struct SDL_Surface;
+struct SDL_Texture;
+struct AVFormatContext;
+struct AVCodecContext;
+struct AVCodecParameters;
+struct AVCodec;
+struct AVFrame;
+struct AVIOContext;
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CInputStream;
+class Point;
+VCMI_LIB_NAMESPACE_END
+
+class FFMpegStream : boost::noncopyable
+{
+	std::unique_ptr<CInputStream> input;
+
+	AVIOContext * context = nullptr;
+	AVFormatContext * formatContext = nullptr;
+
+	const AVCodec * codec = nullptr;
+	AVCodecContext * codecContext = nullptr;
+	int streamIndex = -1;
+
+	AVFrame * frame = nullptr;
+
+protected:
+	void openContext();
+	void openCodec(int streamIndex);
+
+	int findVideoStream() const;
+	int findAudioStream() const;
+
+	const AVCodecParameters * getCodecParameters() const;
+	const AVCodecContext * getCodecContext() const;
+	void decodeNextFrame();
+	const AVFrame * getCurrentFrame() const;
+	double getCurrentFrameEndTime() const;
+	double getCurrentFrameDuration() const;
+
+public:
+	virtual ~FFMpegStream();
+
+	bool openInput(const VideoPath & fname);
+};
+
+class CAudioInstance final : public FFMpegStream
+{
+public:
+	std::pair<std::unique_ptr<ui8[]>, si64> extractAudio(const VideoPath & videoToOpen);
+};
+
+class CVideoInstance final : public IVideoInstance, public FFMpegStream
+{
+	friend class CVideoPlayer;
+
+	struct SwsContext * sws = nullptr;
+	SDL_Texture * textureRGB = nullptr;
+	SDL_Texture * textureYUV = nullptr;
+	SDL_Surface * surface = nullptr;
+	Point dimensions;
+
+	/// video playback current progress, in seconds
+	double frameTime = 0.0;
+
+	void prepareOutput(bool scaleToScreenSize, bool useTextureOutput);
+
+public:
+	~CVideoInstance();
+
+	void openVideo();
+	bool loadNextFrame();
+
+	bool videoEnded() final;
+	Point size() final;
+
+	void show(const Point & position, Canvas & canvas) final;
+	void tick(uint32_t msPassed) final;
+};
+
+class CVideoPlayer final : public IVideoPlayer
+{
+	bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey);
+	void openVideoFile(CVideoInstance & state, const VideoPath & fname);
+
+public:
+	bool playIntroVideo(const VideoPath & name) final;
+	void playSpellbookAnimation(const VideoPath & name, const Point & position) final;
+	std::unique_ptr<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) final;
+	std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) final;
+};
+
+#endif

+ 33 - 0
client/media/IMusicPlayer.h

@@ -0,0 +1,33 @@
+/*
+ * IMusicPlayer.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../lib/filesystem/ResourcePath.h"
+
+class IMusicPlayer
+{
+public:
+	virtual ~IMusicPlayer() = default;
+
+	virtual void loadTerrainMusicThemes() = 0;
+	virtual void setVolume(ui32 percent) = 0;
+	virtual ui32 getVolume() const = 0;
+
+	virtual void musicFinishedCallback() = 0;
+
+	/// play track by URI, if loop = true music will be looped
+	virtual void playMusic(const AudioPath & musicURI, bool loop, bool fromStart) = 0;
+	/// play random track from this set
+	virtual void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart) = 0;
+	/// play random track from set (musicSet, entryID)
+	virtual void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) = 0;
+	/// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any
+	virtual void stopMusic(int fade_ms = 1000) = 0;
+};

+ 35 - 0
client/media/ISoundPlayer.h

@@ -0,0 +1,35 @@
+/*
+ * ISoundPlayer.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../lib/CSoundBase.h"
+#include "../lib/filesystem/ResourcePath.h"
+
+class ISoundPlayer
+{
+public:
+	virtual ~ISoundPlayer() = default;
+
+	virtual int playSound(soundBase::soundID soundID, int repeats = 0) = 0;
+	virtual int playSound(const AudioPath & sound, int repeats = 0, bool cache = false) = 0;
+	virtual int playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) = 0;
+	virtual int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) = 0;
+	virtual void stopSound(int handler) = 0;
+
+	virtual ui32 getVolume() const = 0;
+	virtual void setVolume(ui32 percent) = 0;
+	virtual uint32_t getSoundDurationMilliseconds(const AudioPath & sound) = 0;
+	virtual void setCallback(int channel, std::function<void()> function) = 0;
+	virtual void resetCallback(int channel) = 0;
+	virtual void soundFinishedCallback(int channel) = 0;
+	virtual void ambientUpdateChannels(std::map<AudioPath, int> currentSounds) = 0;
+	virtual void ambientStopAllChannels() = 0;
+	virtual int ambientGetRange() const = 0;
+};

+ 54 - 0
client/media/IVideoPlayer.h

@@ -0,0 +1,54 @@
+/*
+ * IVideoPlayer.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../lib/filesystem/ResourcePath.h"
+
+class Canvas;
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+VCMI_LIB_NAMESPACE_END
+
+class IVideoInstance
+{
+public:
+	/// Returns true if video playback is over
+	virtual bool videoEnded() = 0;
+
+	/// Returns dimensions of the video
+	virtual Point size() = 0;
+
+	/// Displays current frame at specified position
+	virtual void show(const Point & position, Canvas & canvas) = 0;
+
+	/// Advances video playback by specified duration
+	virtual void tick(uint32_t msPassed) = 0;
+
+	virtual ~IVideoInstance() = default;
+};
+
+class IVideoPlayer : boost::noncopyable
+{
+public:
+	/// Plays video on top of the screen, returns only after playback is over, aborts on input event
+	virtual bool playIntroVideo(const VideoPath & name) = 0;
+
+	/// Plays video on top of the screen, returns only after playback is over
+	virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0;
+
+	/// Load video from specified path. Returns nullptr on failure
+	virtual std::unique_ptr<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) = 0;
+
+	/// Extracts audio data from provided video in wav format
+	virtual std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) = 0;
+
+	virtual ~IVideoPlayer() = default;
+};

+ 1 - 1
client/widgets/Buttons.cpp

@@ -13,7 +13,6 @@
 #include "Images.h"
 #include "TextControls.h"
 
-#include "../CMusicHandler.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
@@ -23,6 +22,7 @@
 #include "../gui/MouseButton.h"
 #include "../gui/Shortcut.h"
 #include "../gui/InterfaceObjectConfigurable.h"
+#include "../media/ISoundPlayer.h"
 #include "../windows/InfoWindows.h"
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"

+ 0 - 1
client/widgets/Images.cpp

@@ -26,7 +26,6 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
 
 #include "../../CCallback.h"
 

+ 148 - 0
client/widgets/VideoWidget.cpp

@@ -0,0 +1,148 @@
+/*
+ * TextControls.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "VideoWidget.h"
+
+#include "../CGameInfo.h"
+#include "../gui/CGuiHandler.h"
+#include "../media/ISoundPlayer.h"
+#include "../media/IVideoPlayer.h"
+#include "../render/Canvas.h"
+
+VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio)
+	: playAudio(playAudio)
+{
+	addUsedEvents(TIME);
+	pos += position;
+	playVideo(video);
+}
+
+VideoWidgetBase::~VideoWidgetBase() = default;
+
+void VideoWidgetBase::playVideo(const VideoPath & fileToPlay)
+{
+	videoInstance = CCS->videoh->open(fileToPlay, false);
+	if (videoInstance)
+	{
+		pos.w = videoInstance->size().x;
+		pos.h = videoInstance->size().y;
+	}
+
+	if (playAudio)
+	{
+		loadAudio(fileToPlay);
+		if (isActive())
+			startAudio();
+	}
+}
+
+void VideoWidgetBase::show(Canvas & to)
+{
+	if(videoInstance)
+		videoInstance->show(pos.topLeft(), to);
+}
+
+void VideoWidgetBase::loadAudio(const VideoPath & fileToPlay)
+{
+	if (!playAudio)
+		return;
+
+	audioData = CCS->videoh->getAudio(fileToPlay);
+}
+
+void VideoWidgetBase::startAudio()
+{
+	if(audioData.first == nullptr)
+		return;
+
+	audioHandle = CCS->soundh->playSound(audioData);
+
+	if(audioHandle != -1)
+	{
+		CCS->soundh->setCallback(
+			audioHandle,
+			[this]()
+			{
+				this->audioHandle = -1;
+			}
+			);
+	}
+}
+
+void VideoWidgetBase::stopAudio()
+{
+	if(audioHandle != -1)
+	{
+		CCS->soundh->resetCallback(audioHandle);
+		CCS->soundh->stopSound(audioHandle);
+		audioHandle = -1;
+	}
+}
+
+void VideoWidgetBase::activate()
+{
+	CIntObject::activate();
+	startAudio();
+}
+
+void VideoWidgetBase::deactivate()
+{
+	CIntObject::deactivate();
+	stopAudio();
+}
+
+void VideoWidgetBase::showAll(Canvas & to)
+{
+	if(videoInstance)
+		videoInstance->show(pos.topLeft(), to);
+}
+
+void VideoWidgetBase::tick(uint32_t msPassed)
+{
+	if(videoInstance)
+	{
+		videoInstance->tick(msPassed);
+
+		if(videoInstance->videoEnded())
+		{
+			videoInstance.reset();
+			stopAudio();
+			onPlaybackFinished();
+		}
+	}
+}
+
+VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio)
+	: VideoWidgetBase(position, prologue, playAudio)
+	, loopedVideo(looped)
+{
+}
+
+VideoWidget::VideoWidget(const Point & position, const VideoPath & looped, bool playAudio)
+	: VideoWidgetBase(position, looped, playAudio)
+	, loopedVideo(looped)
+{
+}
+
+void VideoWidget::onPlaybackFinished()
+{
+	playVideo(loopedVideo);
+}
+
+VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function<void()> & callback)
+	: VideoWidgetBase(position, video, playAudio)
+	, callback(callback)
+{
+}
+
+void VideoWidgetOnce::onPlaybackFinished()
+{
+	callback();
+}

+ 65 - 0
client/widgets/VideoWidget.h

@@ -0,0 +1,65 @@
+/*
+ * VideoWidget.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../gui/CIntObject.h"
+
+#include "../lib/filesystem/ResourcePath.h"
+
+class IVideoInstance;
+
+class VideoWidgetBase : public CIntObject
+{
+	std::unique_ptr<IVideoInstance> videoInstance;
+
+	std::pair<std::unique_ptr<ui8[]>, si64> audioData = {nullptr, 0};
+	int audioHandle = -1;
+	bool playAudio = false;
+
+	void loadAudio(const VideoPath & file);
+	void startAudio();
+	void stopAudio();
+
+protected:
+	VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio);
+
+	virtual void onPlaybackFinished() = 0;
+	void playVideo(const VideoPath & video);
+
+public:
+	~VideoWidgetBase();
+
+	void activate() override;
+	void deactivate() override;
+	void show(Canvas & to) override;
+	void showAll(Canvas & to) override;
+	void tick(uint32_t msPassed) override;
+
+	void setPlaybackFinishedCallback(std::function<void()>);
+};
+
+class VideoWidget final: public VideoWidgetBase
+{
+	VideoPath loopedVideo;
+
+	void onPlaybackFinished() final;
+public:
+	VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio);
+	VideoWidget(const Point & position, const VideoPath & looped, bool playAudio);
+};
+
+class VideoWidgetOnce final: public VideoWidgetBase
+{
+	std::function<void()> callback;
+
+	void onPlaybackFinished() final;
+public:
+	VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function<void()> & callback);
+};

+ 3 - 1
client/windows/CCastleInterface.cpp

@@ -18,12 +18,12 @@
 #include "CCreatureWindow.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
+#include "../media/IMusicPlayer.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CGarrisonInt.h"
@@ -43,6 +43,8 @@
 #include "../../CCallback.h"
 #include "../../lib/CArtHandler.h"
 #include "../../lib/CBuildingHandler.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CSoundBase.h"
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/GameSettings.h"

+ 1 - 1
client/windows/CPuzzleWindow.cpp

@@ -11,13 +11,13 @@
 #include "CPuzzleWindow.h"
 
 #include "../CGameInfo.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../adventureMap/CResDataBar.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/TextAlignment.h"
 #include "../gui/Shortcut.h"
 #include "../mapView/MapView.h"
+#include "../media/ISoundPlayer.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
 #include "../widgets/TextControls.h"

+ 3 - 3
client/windows/CSpellWindow.cpp

@@ -19,12 +19,12 @@
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
-#include "../CVideoHandler.h"
 
 #include "../battle/BattleInterface.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
+#include "../media/IVideoPlayer.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CTextInput.h"
@@ -524,13 +524,13 @@ void CSpellWindow::setCurrentPage(int value)
 void CSpellWindow::turnPageLeft()
 {
 	if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook)
-		CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK);
+		CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNLFT.SMK"), pos.topLeft() + Point(13, 15));
 }
 
 void CSpellWindow::turnPageRight()
 {
 	if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook)
-		CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK);
+		CCS->videoh->playSpellbookAnimation(VideoPath::builtin("PGTRNRGH.SMK"), pos.topLeft() + Point(13, 15));
 }
 
 void CSpellWindow::keyPressed(EShortcut key)

+ 5 - 25
client/windows/CTutorialWindow.cpp

@@ -16,7 +16,6 @@
 #include "../../lib/CGeneralTextHandler.h"
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
-#include "../CVideoHandler.h"
 
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
@@ -24,6 +23,7 @@
 #include "../widgets/Images.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
 #include "../render/Canvas.h"
 
 CTutorialWindow::CTutorialWindow(const TutorialMode & m)
@@ -54,7 +54,10 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m)
 
 void CTutorialWindow::setContent()
 {
-	video = "tutorial/" + videos[page];
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	auto video = VideoPath::builtin("tutorial/" + videos[page]);
+
+	videoPlayer = std::make_shared<VideoWidget>(Point(30, 120), video, false);
 
 	buttonLeft->block(page<1);
 	buttonRight->block(page>videos.size() - 2);
@@ -98,26 +101,3 @@ void CTutorialWindow::previous()
 	deactivate();
 	activate();
 }
-
-void CTutorialWindow::show(Canvas & to)
-{
-	CCS->videoh->update(pos.x + 30, pos.y + 120, to.getInternalSurface(), true, false,
-	[&]()
-	{
-		CCS->videoh->close();
-		CCS->videoh->open(VideoPath::builtin(video));
-	});
-
-	CIntObject::show(to);
-}
-
-void CTutorialWindow::activate()
-{
-	CCS->videoh->open(VideoPath::builtin(video));
-	CIntObject::activate();
-}
-
-void CTutorialWindow::deactivate()
-{
-	CCS->videoh->close();
-}

+ 2 - 5
client/windows/CTutorialWindow.h

@@ -15,6 +15,7 @@ class CFilledTexture;
 class CButton;
 class CLabel;
 class CMultiLineLabel;
+class VideoWidget;
 
 enum TutorialMode
 {
@@ -33,8 +34,8 @@ class CTutorialWindow : public CWindowObject
 
 	std::shared_ptr<CLabel> labelTitle;
 	std::shared_ptr<CMultiLineLabel> labelInformation;
+	std::shared_ptr<VideoWidget> videoPlayer;
 
-	std::string video;
 	std::vector<std::string> videos;
 
 	int page;
@@ -47,8 +48,4 @@ class CTutorialWindow : public CWindowObject
 public:
 	CTutorialWindow(const TutorialMode & m);
 	static void openWindowFirstTime(const TutorialMode & m);	
-
-	void show(Canvas & to) override;
-	void activate() override;
-	void deactivate() override;
 };

+ 0 - 1
client/windows/CWindowObject.cpp

@@ -25,7 +25,6 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
 
 #include "../../CCallback.h"
 

+ 5 - 12
client/windows/GUIClasses.cpp

@@ -19,9 +19,7 @@
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../Client.h"
-#include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
-#include "../CVideoHandler.h"
 
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
@@ -36,6 +34,7 @@
 #include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/VideoWidget.h"
 
 #include "../render/Canvas.h"
 #include "../render/CAnimation.h"
@@ -58,6 +57,7 @@
 #include "../lib/GameSettings.h"
 #include "../lib/CondSh.h"
 #include "../lib/CSkillHandler.h"
+#include "../lib/CSoundBase.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/TextOperations.h"
 
@@ -515,11 +515,11 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func
 			recruit->block(true);
 	}
 	if(LOCPLINT->castleInt)
-		CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo);
+		videoPlayer = std::make_shared<VideoWidget>(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo, false);
 	else if(const auto * townObj = dynamic_cast<const CGTownInstance *>(TavernObj))
-		CCS->videoh->open(townObj->town->clientInfo.tavernVideo);
+		videoPlayer = std::make_shared<VideoWidget>(Point(70, 56), townObj->town->clientInfo.tavernVideo, false);
 	else
-		CCS->videoh->open(VideoPath::builtin("TAVERN.BIK"));
+		videoPlayer = std::make_shared<VideoWidget>(Point(70, 56), VideoPath::builtin("TAVERN.BIK"), false);
 
 	addInvite();
 }
@@ -572,11 +572,6 @@ void CTavernWindow::close()
 	CStatusbarWindow::close();
 }
 
-CTavernWindow::~CTavernWindow()
-{
-	CCS->videoh->close();
-}
-
 void CTavernWindow::show(Canvas & to)
 {
 	CWindowObject::show(to);
@@ -600,8 +595,6 @@ void CTavernWindow::show(Canvas & to)
 
 		to.drawBorder(Rect::createAround(sel->pos, 2), Colors::BRIGHT_YELLOW, 2);
 	}
-
-	CCS->videoh->update(pos.x+70, pos.y+56, to.getInternalSurface(), true, false);
 }
 
 void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition)

+ 2 - 1
client/windows/GUIClasses.h

@@ -39,6 +39,7 @@ class CHeroArea;
 class CAnimImage;
 class CFilledTexture;
 class IImage;
+class VideoWidget;
 
 enum class EUserEvent;
 
@@ -261,6 +262,7 @@ public:
 	std::shared_ptr<CLabel> cost;
 	std::shared_ptr<CLabel> heroesForHire;
 	std::shared_ptr<CTextBox> heroDescription;
+	std::shared_ptr<VideoWidget> videoPlayer;
 
 	std::shared_ptr<CTextBox> rumor;
 	
@@ -272,7 +274,6 @@ public:
 	void addInvite();
 
 	CTavernWindow(const CGObjectInstance * TavernObj, const std::function<void()> & onWindowClosed);
-	~CTavernWindow();
 
 	void close() override;
 	void recruitb();

+ 2 - 1
client/windows/settings/GeneralOptionsTab.cpp

@@ -11,9 +11,10 @@
 #include "GeneralOptionsTab.h"
 
 #include "CGameInfo.h"
-#include "CMusicHandler.h"
 #include "CPlayerInterface.h"
 #include "CServerHandler.h"
+#include "media/IMusicPlayer.h"
+#include "media/ISoundPlayer.h"
 #include "render/IScreenHandler.h"
 #include "windows/GUIClasses.h"
 

+ 24 - 21
lib/Languages.h

@@ -68,6 +68,9 @@ struct Options
 	/// primary IETF language tag
 	std::string tagIETF;
 
+	/// ISO 639-2 (B) language code
+	std::string tagISO2;
+
 	/// DateTime format
 	std::string dateTimeFormat;
 
@@ -82,27 +85,27 @@ inline const auto & getLanguageList()
 {
 	static const std::array<Options, 20> languages
 	{ {
-		{ "czech",       "Czech",       "Čeština",    "CP1250", "cs", "%d.%m.%Y %T", EPluralForms::CZ_3, true },
-		{ "chinese",     "Chinese",     "简体中文",       "GBK",    "zh", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
-		{ "english",     "English",     "English",    "CP1252", "en", "%F %T", EPluralForms::EN_2, true }, // English uses international date/time format here
-		{ "finnish",     "Finnish",     "Suomi",      "CP1252", "fi", "%d.%m.%Y %T", EPluralForms::EN_2, true },
-		{ "french",      "French",      "Français",   "CP1252", "fr", "%d/%m/%Y %T", EPluralForms::FR_2, true },
-		{ "german",      "German",      "Deutsch",    "CP1252", "de", "%d.%m.%Y %T", EPluralForms::EN_2, true },
-		{ "hungarian",   "Hungarian",   "Magyar",     "CP1250", "hu", "%Y. %m. %d. %T", EPluralForms::EN_2, true },
-		{ "italian",     "Italian",     "Italiano",   "CP1250", "it", "%d/%m/%Y %T", EPluralForms::EN_2, true },
-		{ "korean",      "Korean",      "한국어",        "CP949",  "ko", "%F %T", EPluralForms::VI_1, true },
-		{ "polish",      "Polish",      "Polski",     "CP1250", "pl", "%d.%m.%Y %T", EPluralForms::PL_3, true },
-		{ "portuguese",  "Portuguese",  "Português",  "CP1252", "pt", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
-		{ "russian",     "Russian",     "Русский",    "CP1251", "ru", "%d.%m.%Y %T", EPluralForms::UK_3, true },
-		{ "spanish",     "Spanish",     "Español",    "CP1252", "es", "%d/%m/%Y %T", EPluralForms::EN_2, true },
-		{ "swedish",     "Swedish",     "Svenska",    "CP1252", "sv", "%F %T", EPluralForms::EN_2, true },
-		{ "turkish",     "Turkish",     "Türkçe",     "CP1254", "tr", "%d.%m.%Y %T", EPluralForms::EN_2, true },
-		{ "ukrainian",   "Ukrainian",   "Українська", "CP1251", "uk", "%d.%m.%Y %T", EPluralForms::UK_3, true },
-		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8",  "vi", "%d/%m/%Y %T", EPluralForms::VI_1, true }, // Fan translation uses special encoding
-
-		{ "other_cp1250", "Other (East European)",   "", "CP1250", "", "", EPluralForms::NONE, false },
-		{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", EPluralForms::NONE, false },
-		{ "other_cp1252", "Other (West European)",   "", "CP1252", "", "", EPluralForms::NONE, false }
+		{ "czech",       "Czech",       "Čeština",    "CP1250", "cs", "cze", "%d.%m.%Y %T", EPluralForms::CZ_3, true },
+		{ "chinese",     "Chinese",     "简体中文",       "GBK",    "zh", "chi", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
+		{ "english",     "English",     "English",    "CP1252", "en", "eng", "%F %T", EPluralForms::EN_2, true }, // English uses international date/time format here
+		{ "finnish",     "Finnish",     "Suomi",      "CP1252", "fi", "fin", "%d.%m.%Y %T", EPluralForms::EN_2, true },
+		{ "french",      "French",      "Français",   "CP1252", "fr", "fre", "%d/%m/%Y %T", EPluralForms::FR_2, true },
+		{ "german",      "German",      "Deutsch",    "CP1252", "de", "ger", "%d.%m.%Y %T", EPluralForms::EN_2, true },
+		{ "hungarian",   "Hungarian",   "Magyar",     "CP1250", "hu", "hun", "%Y. %m. %d. %T", EPluralForms::EN_2, true },
+		{ "italian",     "Italian",     "Italiano",   "CP1250", "it", "ita", "%d/%m/%Y %T", EPluralForms::EN_2, true },
+		{ "korean",      "Korean",      "한국어",        "CP949",  "ko", "kor", "%F %T", EPluralForms::VI_1, true },
+		{ "polish",      "Polish",      "Polski",     "CP1250", "pl", "pol", "%d.%m.%Y %T", EPluralForms::PL_3, true },
+		{ "portuguese",  "Portuguese",  "Português",  "CP1252", "pt", "por", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
+		{ "russian",     "Russian",     "Русский",    "CP1251", "ru", "rus", "%d.%m.%Y %T", EPluralForms::UK_3, true },
+		{ "spanish",     "Spanish",     "Español",    "CP1252", "es", "spa", "%d/%m/%Y %T", EPluralForms::EN_2, true },
+		{ "swedish",     "Swedish",     "Svenska",    "CP1252", "sv", "swe", "%F %T", EPluralForms::EN_2, true },
+		{ "turkish",     "Turkish",     "Türkçe",     "CP1254", "tr", "tur", "%d.%m.%Y %T", EPluralForms::EN_2, true },
+		{ "ukrainian",   "Ukrainian",   "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %T", EPluralForms::UK_3, true },
+		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8",  "vi", "vie", "%d/%m/%Y %T", EPluralForms::VI_1, true }, // Fan translation uses special encoding
+
+		{ "other_cp1250", "Other (East European)",   "", "CP1250", "", "", "", EPluralForms::NONE, false },
+		{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", "", EPluralForms::NONE, false },
+		{ "other_cp1252", "Other (West European)",   "", "CP1252", "", "", "", EPluralForms::NONE, false }
 	} };
 	static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");
 

+ 2 - 4
lib/filesystem/ResourcePath.cpp

@@ -113,11 +113,8 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension)
 		{".MP3",   EResType::SOUND},
 		{".OGG",   EResType::SOUND},
 		{".FLAC",  EResType::SOUND},
-		{".SMK",   EResType::VIDEO},
+		{".SMK",   EResType::VIDEO_LOW_QUALITY},
 		{".BIK",   EResType::VIDEO},
-		{".MJPG",  EResType::VIDEO},
-		{".MPG",   EResType::VIDEO},
-		{".AVI",   EResType::VIDEO},
 		{".WEBM",  EResType::VIDEO},
 		{".ZIP",   EResType::ARCHIVE_ZIP},
 		{".LOD",   EResType::ARCHIVE_LOD},
@@ -157,6 +154,7 @@ std::string EResTypeHelper::getEResTypeAsString(EResType type)
 		MAP_ENUM(TTF_FONT)
 		MAP_ENUM(IMAGE)
 		MAP_ENUM(VIDEO)
+		MAP_ENUM(VIDEO_LOW_QUALITY)
 		MAP_ENUM(SOUND)
 		MAP_ENUM(ARCHIVE_ZIP)
 		MAP_ENUM(ARCHIVE_LOD)

+ 1 - 0
lib/filesystem/ResourcePath.h

@@ -46,6 +46,7 @@ enum class EResType
 	TTF_FONT,
 	IMAGE,
 	VIDEO,
+	VIDEO_LOW_QUALITY,
 	SOUND,
 	ARCHIVE_VID,
 	ARCHIVE_ZIP,

+ 1 - 0
lib/json/JsonValidator.cpp

@@ -493,6 +493,7 @@ static std::string imageFile(const JsonNode & node)
 static std::string videoFile(const JsonNode & node)
 {
 	TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO);
+	TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO_LOW_QUALITY);
 	return "Video file \"" + node.String() + "\" was not found";
 }
 #undef TEST_FILE