From d27b854cb12995c2f42236b022b93bf676e8db6b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 May 2024 15:55:20 +0300 Subject: [PATCH 01/15] Reorganized music, video and sound players: - All XXXplayers are now in client/media directory - Reogranized code on one class per file basis - Extracted interfaces from handlers. Handlers now implement corresponding interface. - CCS now only stores pointer to an interface --- client/CGameInfo.h | 12 +- client/CMT.cpp | 11 +- client/CMakeLists.txt | 18 +- client/CMusicHandler.cpp | 753 ------------------ client/CMusicHandler.h | 170 ---- client/CPlayerInterface.cpp | 4 +- client/CVideoHandler.h | 131 --- client/HeroMovementController.cpp | 3 +- client/adventureMap/CInGameConsole.cpp | 2 +- client/adventureMap/CInfoBar.cpp | 2 +- client/adventureMap/MapAudioPlayer.cpp | 3 +- client/adventureMap/TurnTimerWidget.cpp | 2 +- client/battle/BattleAnimationClasses.cpp | 2 +- client/battle/BattleEffectsController.cpp | 2 +- client/battle/BattleInterface.cpp | 12 +- client/battle/BattleInterfaceClasses.cpp | 4 +- client/battle/BattleObstacleController.cpp | 2 +- client/battle/BattleSiegeController.cpp | 2 +- client/battle/BattleStacksController.cpp | 2 +- client/battle/BattleWindow.cpp | 1 - client/eventsSDL/InputHandler.cpp | 3 +- client/globalLobby/GlobalLobbyClient.cpp | 2 +- client/globalLobby/GlobalLobbyWidget.cpp | 2 +- client/lobby/CBonusSelection.cpp | 7 +- client/lobby/CSelectionBase.cpp | 3 +- client/lobby/OptionsTab.cpp | 3 +- client/mainmenu/CCampaignScreen.cpp | 6 +- client/mainmenu/CHighScoreScreen.cpp | 5 +- client/mainmenu/CMainMenu.cpp | 4 +- client/mainmenu/CPrologEpilogVideo.cpp | 5 +- client/media/CAudioBase.cpp | 45 ++ client/media/CAudioBase.h | 21 + client/media/CEmptyVideoPlayer.h | 30 + client/media/CMusicHandler.cpp | 346 ++++++++ client/media/CMusicHandler.h | 94 +++ client/media/CSoundHandler.cpp | 383 +++++++++ client/media/CSoundHandler.h | 78 ++ client/{ => media}/CVideoHandler.cpp | 12 +- client/media/CVideoHandler.h | 86 ++ client/media/IMusicPlayer.h | 33 + client/media/ISoundPlayer.h | 35 + client/media/IVideoPlayer.h | 43 + client/widgets/Buttons.cpp | 2 +- client/widgets/Images.cpp | 1 - client/windows/CCastleInterface.cpp | 4 +- client/windows/CPuzzleWindow.cpp | 2 +- client/windows/CSpellWindow.cpp | 2 +- client/windows/CTutorialWindow.cpp | 2 +- client/windows/CWindowObject.cpp | 1 - client/windows/GUIClasses.cpp | 5 +- client/windows/settings/GeneralOptionsTab.cpp | 3 +- 51 files changed, 1284 insertions(+), 1122 deletions(-) delete mode 100644 client/CMusicHandler.cpp delete mode 100644 client/CMusicHandler.h delete mode 100644 client/CVideoHandler.h create mode 100644 client/media/CAudioBase.cpp create mode 100644 client/media/CAudioBase.h create mode 100644 client/media/CEmptyVideoPlayer.h create mode 100644 client/media/CMusicHandler.cpp create mode 100644 client/media/CMusicHandler.h create mode 100644 client/media/CSoundHandler.cpp create mode 100644 client/media/CSoundHandler.h rename client/{ => media}/CVideoHandler.cpp (99%) create mode 100644 client/media/CVideoHandler.h create mode 100644 client/media/IMusicPlayer.h create mode 100644 client/media/ISoundPlayer.h create mode 100644 client/media/IVideoPlayer.h diff --git a/client/CGameInfo.h b/client/CGameInfo.h index c07bb3c7d..1de8025e7 100644 --- a/client/CGameInfo.h +++ b/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; diff --git a/client/CMT.cpp b/client/CMT.cpp index be8e262d5..e8ceb035a 100644 --- a/client/CMT.cpp +++ b/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()); } @@ -457,9 +457,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; diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0e8b0d562..ebc0d4e5e 100644 --- a/client/CMakeLists.txt +++ b/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 @@ -163,11 +168,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 +263,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 @@ -357,11 +369,9 @@ set(client_HEADERS CGameInfo.h CMT.h - CMusicHandler.h CPlayerInterface.h PlayerLocalState.h CServerHandler.h - CVideoHandler.h Client.h ClientCommandManager.h ClientNetPackVisitors.h diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp deleted file mode 100644 index 00edf149c..000000000 --- a/client/CMusicHandler.cpp +++ /dev/null @@ -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 -#include - -#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, si64> & data, bool cache) -{ - try - { - std::vector startBytes = std::vector(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(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, 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 &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 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 & function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel].push_back(function); -} - -int CSoundHandler::ambientGetRange() const -{ - return static_cast(ambientConfig["range"].Integer()); -} - -void CSoundHandler::ambientUpdateChannels(std::map soundsArg) -{ - boost::mutex::scoped_lock guard(mutex); - - std::vector 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 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(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; -} diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h deleted file mode 100644 index 4a024c450..000000000 --- a/client/CMusicHandler.h +++ /dev/null @@ -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>; - std::map soundChunks; - std::map, CachedChunk> soundChunksRaw; - - Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); - Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); - - /// have entry for every currently active channel - /// vector will be empty if callback was not set - std::map> > 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 ambientChannels; - std::map channelVolumes; - - void initCallback(int channel, const std::function & 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, si64> & data, int repeats=0, bool cache=false); - int playSoundFromSet(std::vector &sound_vec); - void stopSound(int handler); - - void setCallback(int channel, std::function function); - void resetCallback(int channel); - void soundFinishedCallback(int channel); - - int ambientGetRange() const; - void ambientUpdateChannels(std::map currentSounds); - void ambientStopAllChannels(); - - // Sets - std::vector 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 current; - std::unique_ptr next; - - void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); - void queueNext(std::unique_ptr queued); - void musicFinishedCallback(); - - /// map -> - std::map> musicsSet; - /// stored position, in seconds at which music player should resume playing this track - std::map 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; -}; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 858024fd7..7a4d2ef20 100644 --- a/client/CPlayerInterface.cpp +++ b/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" diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h deleted file mode 100644 index 89f8016a2..000000000 --- a/client/CVideoHandler.h +++ /dev/null @@ -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 restart = nullptr){} - virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) - { - return false; - } - virtual std::pair, 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 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, 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 data; - std::unique_ptr dataAudio; -}; - -#endif diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index d69075661..4c415704a 100644 --- a/client/HeroMovementController.cpp +++ b/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" diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 5a8a05cc0..54b07b7e9 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/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" diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 68809752a..70b6484bb 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/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" diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 81a7cf002..bca944448 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/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" diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index c97d67f94..ac7cb7602 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/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" diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 2bff6e8ce..78bfcadfa 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/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" diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index f1db77c16..7a2d936b7 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/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" diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index faa8f9bf1..cb9b9c985 100644 --- a/client/battle/BattleInterface.cpp +++ b/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 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); diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 1891f3f08..e72d20a83 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -19,14 +19,14 @@ #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 "../media/IVideoPlayer.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IFont.h" diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 0c6e7e809..2d7fbdbb7 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/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" diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 660440f1e..9feaa7504 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/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" diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 5528b0bb4..bbc14298d 100644 --- a/client/battle/BattleStacksController.cpp +++ b/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" diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index c750b620d..648e3c4ff 100644 --- a/client/battle/BattleWindow.cpp +++ b/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" diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 9a91d8fde..83fbea3e1 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/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" diff --git a/client/globalLobby/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp index 6b7be1513..9b58d782f 100644 --- a/client/globalLobby/GlobalLobbyClient.cpp +++ b/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" diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index a6af670db..647ccf2c2 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/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" diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index f105b6efb..3406f30a5 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/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" diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 0c2fb781c..709395f37 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/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" diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 0ee53b900..5865dfc48 100644 --- a/client/lobby/OptionsTab.cpp +++ b/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" diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 9db6cf79c..41d5c44ba 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -14,12 +14,12 @@ #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 "../media/IVideoPlayer.h" #include "../render/Canvas.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.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" diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index c099b5335..67979982d 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -14,6 +14,9 @@ #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" +#include "../media/IVideoPlayer.h" #include "../widgets/Buttons.h" #include "../widgets/CTextInput.h" #include "../widgets/Images.h" @@ -23,8 +26,6 @@ #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" diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 12b44298f..c322e6b53 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -17,6 +17,8 @@ #include "../lobby/CBonusSelection.h" #include "../lobby/CSelectionBase.h" #include "../lobby/CLobbyScreen.h" +#include "../media/IMusicPlayer.h" +#include "../media/IVideoPlayer.h" #include "../gui/CursorHandler.h" #include "../windows/GUIClasses.h" #include "../gui/CGuiHandler.h" @@ -37,8 +39,6 @@ #include "../CServerHandler.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" #include "../CPlayerInterface.h" #include "../Client.h" #include "../CMT.h" diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index af1d77a72..5f1d55c9c 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -12,8 +12,9 @@ #include "CPrologEpilogVideo.h" #include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CVideoHandler.h" +#include "../media/IMusicPlayer.h" +#include "../media/ISoundPlayer.h" +#include "../media/IVideoPlayer.h" #include "../gui/WindowHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/FramerateManager.h" diff --git a/client/media/CAudioBase.cpp b/client/media/CAudioBase.cpp new file mode 100644 index 000000000..aa487878e --- /dev/null +++ b/client/media/CAudioBase.cpp @@ -0,0 +1,45 @@ +/* + * 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 "../CGameInfo.h" + +#include + +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; +} diff --git a/client/media/CAudioBase.h b/client/media/CAudioBase.h new file mode 100644 index 000000000..96f8f1fcd --- /dev/null +++ b/client/media/CAudioBase.h @@ -0,0 +1,21 @@ +/* + * 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 +{ + static int initializationCounter; + static bool initializeSuccess; +protected: + bool isInitialized() const; + + CAudioBase(); + ~CAudioBase(); +}; diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h new file mode 100644 index 000000000..60c084642 --- /dev/null +++ b/client/media/CEmptyVideoPlayer.h @@ -0,0 +1,30 @@ +/* + * 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" +#include "../lib/Point.h" + +class CEmptyVideoPlayer final : public IVideoPlayer +{ +public: + int curFrame() const override {return -1;}; + int frameCount() const override {return -1;}; + void redraw( int x, int y, SDL_Surface *dst, bool update) override {}; + void show( int x, int y, SDL_Surface *dst, bool update) override {}; + bool nextFrame() override {return false;}; + void close() override {}; + bool wait() override {return false;}; + bool open(const VideoPath & name, bool scale) override {return false;}; + void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function restart) override {} + bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override { return false; } + std::pair, si64> getAudio(const VideoPath & videoToOpen) override { return std::make_pair(nullptr, 0); }; + Point size() override { return Point(0, 0); }; +}; diff --git a/client/media/CMusicHandler.cpp b/client/media/CMusicHandler.cpp new file mode 100644 index 000000000..67d64458b --- /dev/null +++ b/client/media/CMusicHandler.cpp @@ -0,0 +1,346 @@ +/* + * 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 "../renderSDL/SDLRWwrapper.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/CGuiHandler.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/CRandomGenerator.h" +#include "../../lib/TerrainHandler.h" + +#include + +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)); + } + + 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 queued) +{ + if (!isInitialized()) + 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(owner, setName, musicURI, looped, fromStart)); +} + +void CMusicHandler::stopMusic(int fade_ms) +{ + if (!isInitialized()) + return; + + boost::mutex::scoped_lock guard(mutex); + + if (current.get() != 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.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; +} diff --git a/client/media/CMusicHandler.h b/client/media/CMusicHandler.h new file mode 100644 index 000000000..fda2e814e --- /dev/null +++ b/client/media/CMusicHandler.h @@ -0,0 +1,94 @@ +/* + * 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 +{ + 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, public IMusicPlayer +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode &volumeNode); + + std::unique_ptr current; + std::unique_ptr 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 queued); + void musicFinishedCallback() final; + + /// map -> + std::map> musicsSet; + /// stored position, in seconds at which music player should resume playing this track + std::map 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=1000) final; + + friend class MusicEntry; +}; diff --git a/client/media/CSoundHandler.cpp b/client/media/CSoundHandler.cpp new file mode 100644 index 000000000..f13bfc094 --- /dev/null +++ b/client/media/CSoundHandler.cpp @@ -0,0 +1,383 @@ +/* + * 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 + +#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 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)); + + if(ambientConfig["allocateChannels"].isNumber()) + Mix_AllocateChannels((int)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(), (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, si64> & data, bool cache) +{ + try + { + std::vector startBytes = std::vector(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(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 (!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(), (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 (!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, 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 &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 (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 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 & function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel].push_back(function); +} + +int CSoundHandler::ambientGetRange() const +{ + return static_cast(ambientConfig["range"].Integer()); +} + +void CSoundHandler::ambientUpdateChannels(std::map soundsArg) +{ + boost::mutex::scoped_lock guard(mutex); + + std::vector 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(); +} diff --git a/client/media/CSoundHandler.h b/client/media/CSoundHandler.h new file mode 100644 index 000000000..a20ac5a22 --- /dev/null +++ b/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>; + std::map soundChunks; + std::map, CachedChunk> soundChunksRaw; + + Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); + Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); + + /// have entry for every currently active channel + /// vector will be empty if callback was not set + std::map> > 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 ambientChannels; + std::map channelVolumes; + int volume = 0; + + void initCallback(int channel, const std::function & 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, si64> & data, int repeats=0, bool cache=false) final; + int playSoundFromSet(std::vector &sound_vec) final; + void stopSound(int handler) final; + + void setCallback(int channel, std::function function) final; + void resetCallback(int channel) final; + void soundFinishedCallback(int channel) final; + + int ambientGetRange() const final; + void ambientUpdateChannels(std::map currentSounds) final; + void ambientStopAllChannels() final; +}; diff --git a/client/CVideoHandler.cpp b/client/media/CVideoHandler.cpp similarity index 99% rename from client/CVideoHandler.cpp rename to client/media/CVideoHandler.cpp index 4595e5977..830c0491c 100644 --- a/client/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "CVideoHandler.h" +#ifndef DISABLE_VIDEO + #include "CMT.h" #include "gui/CGuiHandler.h" #include "eventsSDL/InputHandler.h" @@ -21,8 +23,6 @@ #include -#ifndef DISABLE_VIDEO - extern "C" { #include #include @@ -43,7 +43,7 @@ static int lodRead(void* opaque, uint8_t* buf, int size) auto video = reinterpret_cast(opaque); int bytes = static_cast(video->data->read(buf, size)); if(bytes == 0) - return AVERROR_EOF; + return AVERROR_EOF; return bytes; } @@ -64,7 +64,7 @@ static int lodReadAudio(void* opaque, uint8_t* buf, int size) auto video = reinterpret_cast(opaque); int bytes = static_cast(video->dataAudio->read(buf, size)); if(bytes == 0) - return AVERROR_EOF; + return AVERROR_EOF; return bytes; } @@ -97,7 +97,7 @@ CVideoPlayer::CVideoPlayer() bool CVideoPlayer::open(const VideoPath & fname, bool scale) { - return open(fname, true, false); + return open(fname, true, false, false); } // loop = to loop through the video @@ -395,7 +395,7 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo if(onVideoRestart) onVideoRestart(); VideoPath filenameToReopen = fname; // create copy to backup this->fname - open(filenameToReopen); + open(filenameToReopen, false); nextFrame(); // The y position is wrong at the first frame. diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h new file mode 100644 index 000000000..46139885b --- /dev/null +++ b/client/media/CVideoHandler.h @@ -0,0 +1,86 @@ +/* + * 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 "IVideoPlayer.h" + +#include "../lib/Rect.h" + +struct SDL_Surface; +struct SDL_Texture; +struct AVFormatContext; +struct AVCodecContext; +struct AVCodec; +struct AVFrame; +struct AVIOContext; + +VCMI_LIB_NAMESPACE_BEGIN +class CInputStream; +VCMI_LIB_NAMESPACE_END + +class CVideoPlayer final : public IVideoPlayer +{ + 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, bool scale); +public: + CVideoPlayer(); + ~CVideoPlayer(); + + bool init(); + bool open(const VideoPath & fname, bool scale) override; + void close() override; + bool nextFrame() override; // display next frame + + void show(int x, int y, SDL_Surface *dst, bool update) override; //blit current frame + void redraw(int x, int y, SDL_Surface *dst, bool update) override; //reblits buffer + void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) 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, 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 data; + std::unique_ptr dataAudio; +}; + +#endif diff --git a/client/media/IMusicPlayer.h b/client/media/IMusicPlayer.h new file mode 100644 index 000000000..d40f7ee8d --- /dev/null +++ b/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; +}; diff --git a/client/media/ISoundPlayer.h b/client/media/ISoundPlayer.h new file mode 100644 index 000000000..9b3d9d5e9 --- /dev/null +++ b/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, si64> & data, int repeats = 0, bool cache = false) = 0; + virtual int playSoundFromSet(std::vector & 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 function) = 0; + virtual void resetCallback(int channel) = 0; + virtual void soundFinishedCallback(int channel) = 0; + virtual void ambientUpdateChannels(std::map currentSounds) = 0; + virtual void ambientStopAllChannels() = 0; + virtual int ambientGetRange() const = 0; +}; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h new file mode 100644 index 000000000..bf8733147 --- /dev/null +++ b/client/media/IVideoPlayer.h @@ -0,0 +1,43 @@ +/* + * 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" + +struct SDL_Surface; + +VCMI_LIB_NAMESPACE_BEGIN +class Point; +VCMI_LIB_NAMESPACE_END + +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; + virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr) = 0; + virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) = 0; + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; + virtual Point size() = 0; + + virtual ~IVideoPlayer() = default; +}; diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index b69cc6199..620c08587 100644 --- a/client/widgets/Buttons.cpp +++ b/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" diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 6699476da..58f50dcda 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -26,7 +26,6 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../../CCallback.h" diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 2fbd723c2..7eb0b45c9 100644 --- a/client/windows/CCastleInterface.cpp +++ b/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" diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 8b5f70edb..d234e0e95 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/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" diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index d752c24af..0a4c05330 100644 --- a/client/windows/CSpellWindow.cpp +++ b/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" diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index 72f945547..c7364c3fb 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -16,11 +16,11 @@ #include "../../lib/CGeneralTextHandler.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" -#include "../CVideoHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../media/IVideoPlayer.h" #include "../widgets/Images.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 0403c8c91..30f70432b 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -25,7 +25,6 @@ #include "../CGameInfo.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" #include "../../CCallback.h" diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 38d6677b9..7513559b8 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -19,15 +19,15 @@ #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" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" +#include "../media/IVideoPlayer.h" + #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" #include "../widgets/CreatureCostBox.h" @@ -58,6 +58,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" diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 2058b664c..46f409e1c 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/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" From d08c7b7b8f551c053257ad6f123dd2bee863fc84 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 May 2024 22:14:50 +0300 Subject: [PATCH 02/15] Added VideoWidget to hide implementation details --- client/CMakeLists.txt | 2 + client/battle/BattleInterfaceClasses.cpp | 168 ++++++++--------------- client/battle/BattleInterfaceClasses.h | 30 ++-- client/mainmenu/CCampaignScreen.cpp | 26 +--- client/mainmenu/CCampaignScreen.h | 5 +- client/mainmenu/CHighScoreScreen.cpp | 52 +------ client/mainmenu/CHighScoreScreen.h | 9 +- client/mainmenu/CMainMenu.cpp | 32 ++--- client/mainmenu/CMainMenu.h | 5 +- client/mainmenu/CPrologEpilogVideo.cpp | 15 +- client/mainmenu/CPrologEpilogVideo.h | 2 + client/media/CEmptyVideoPlayer.h | 3 - client/media/CVideoHandler.h | 5 - client/media/IVideoPlayer.h | 3 - client/widgets/VideoWidget.cpp | 68 +++++++++ client/widgets/VideoWidget.h | 32 +++++ client/windows/CTutorialWindow.cpp | 30 +--- client/windows/CTutorialWindow.h | 7 +- client/windows/GUIClasses.cpp | 16 +-- client/windows/GUIClasses.h | 3 +- 20 files changed, 228 insertions(+), 285 deletions(-) create mode 100644 client/widgets/VideoWidget.cpp create mode 100644 client/widgets/VideoWidget.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ebc0d4e5e..9c3d4ef54 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -131,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 @@ -332,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 diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index e72d20a83..d305320cf 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -26,7 +26,6 @@ #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" #include "../media/IMusicPlayer.h" -#include "../media/IVideoPlayer.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(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + videoPlayer = std::make_shared(Point(107, 70), resources.prologueVideo, resources.loopedVideo); + + 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(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(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(br.result)); - break; + throw std::runtime_error("Invalid battle result!"); } - playVideo(); - labels.push_back(std::make_shared(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() diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index d50a9e41e..6b9762d62 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/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 repeat; std::vector> icons; std::shared_ptr description; + std::shared_ptr 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 resultCallback; //callback receiving which button was pressed void activate() override; - void show(Canvas & to) override; }; /// Shows the stack queue diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 41d5c44ba..dc161734f 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -19,13 +19,13 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../media/IMusicPlayer.h" -#include "../media/IVideoPlayer.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" @@ -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(ImagePath::fromJson(config["image"])); - hoverLabel = std::make_shared(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(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(Point(), videoPath); else - CCS->videoh->close(); + videoPlayer.reset(); if(hoverLabel) { diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index e19395974..87b69bc5e 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/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 hoverLabel; std::shared_ptr graphicsImage; std::shared_ptr graphicsCompleted; + std::shared_ptr 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; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 67979982d..7a4a42c4f 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -16,11 +16,11 @@ #include "../gui/Shortcut.h" #include "../media/IMusicPlayer.h" #include "../media/ISoundPlayer.h" -#include "../media/IVideoPlayer.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" @@ -217,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); @@ -229,6 +229,8 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc if(won) { + videoPlayer = std::make_shared(Point(30, 120), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK")); + int border = 100; int textareaW = ((pos.w - 2 * border) / 4); std::vector t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank @@ -243,9 +245,10 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); } else + { + videoPlayer = std::make_shared(Point(30, 120), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("LOSEGAME.SMK")); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); - - video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; + } } int CHighScoreInputScreen::addEntry(std::string text) { @@ -288,47 +291,6 @@ int CHighScoreInputScreen::addEntry(std::string text) { return pos; } -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); -} - void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 2261dbe60..a4f777422 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -15,6 +15,7 @@ class CLabel; class CMultiLineLabel; class CAnimImage; class CTextInput; +class VideoWidget; class TransparentFilledRectangle; @@ -93,9 +94,8 @@ class CHighScoreInputScreen : public CWindowObject std::vector> texts; std::shared_ptr input; std::shared_ptr background; + std::shared_ptr videoPlayer; - std::string video; - int videoSoundHandle; bool won; HighScoreCalculation calc; public: @@ -103,9 +103,6 @@ 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; -}; \ No newline at end of file +}; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index c322e6b53..431c29bae 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -18,7 +18,6 @@ #include "../lobby/CSelectionBase.h" #include "../lobby/CLobbyScreen.h" #include "../media/IMusicPlayer.h" -#include "../media/IVideoPlayer.h" #include "../gui/CursorHandler.h" #include "../windows/GUIClasses.h" #include "../gui/CGuiHandler.h" @@ -35,6 +34,7 @@ #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../windows/InfoWindows.h" #include "../CServerHandler.h" @@ -92,8 +92,14 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) menuNameToEntry.push_back("credits"); tabs = std::make_shared(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(videoPosition, VideoPath::fromJson(config["video"]["name"])); + } + else tabs->setRedrawParent(true); + } std::shared_ptr CMenuScreen::createTab(size_t index) @@ -104,34 +110,12 @@ std::shared_ptr CMenuScreen::createTab(size_t index) return std::make_shared(this, config["items"].Vector()[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); -} - 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); diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index e4d62aef1..31783120d 100644 --- a/client/mainmenu/CMainMenu.h +++ b/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 tabs; std::shared_ptr background; + std::shared_ptr videoPlayer; std::vector> images; std::shared_ptr createTab(size_t index); @@ -57,9 +58,7 @@ public: CMenuScreen(const JsonNode & configNode); - void show(Canvas & to) override; void activate() override; - void deactivate() override; void switchToTab(size_t index); void switchToTab(std::string name); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 5f1d55c9c..61e5317e3 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -14,14 +14,13 @@ #include "../CGameInfo.h" #include "../media/IMusicPlayer.h" #include "../media/ISoundPlayer.h" -#include "../media/IVideoPlayer.h" #include "../gui/WindowHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/FramerateManager.h" #include "../widgets/TextControls.h" +#include "../widgets/VideoWidget.h" #include "../render/Canvas.h" - CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback) : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0) { @@ -30,9 +29,12 @@ 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); + videoPlayer = std::make_shared(Point(30, 120), spe.prologVideo); + + //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); @@ -66,9 +68,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 } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index 5923791d5..b2c1fd9e3 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/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 exitCb; std::shared_ptr text; + std::shared_ptr videoPlayer; bool voiceStopped = false; diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index 60c084642..99b0aac1c 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -15,13 +15,10 @@ class CEmptyVideoPlayer final : public IVideoPlayer { public: - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; void redraw( int x, int y, SDL_Surface *dst, bool update) override {}; void show( int x, int y, SDL_Surface *dst, bool update) override {}; bool nextFrame() override {return false;}; void close() override {}; - bool wait() override {return false;}; bool open(const VideoPath & name, bool scale) override {return false;}; void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function restart) override {} bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override { return false; } diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 46139885b..a70177be9 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -73,11 +73,6 @@ public: 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 data; std::unique_ptr dataAudio; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index bf8733147..c7037fba9 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -31,9 +31,6 @@ public: 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; virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr) = 0; virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) = 0; virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp new file mode 100644 index 000000000..fe36725e3 --- /dev/null +++ b/client/widgets/VideoWidget.cpp @@ -0,0 +1,68 @@ +/* + * 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 "../media/IVideoPlayer.h" +#include "../media/ISoundPlayer.h" +#include "../render/Canvas.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" + +VideoWidget::VideoWidget(const Point & position, const VideoPath & looped) + : VideoWidget(position, VideoPath(), looped) +{ +} + +VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped) + :current(prologue) + ,next(looped) + ,videoSoundHandle(-1) +{ +} + +VideoWidget::~VideoWidget() = default; + +void VideoWidget::show(Canvas & to) +{ + CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false); +} + +void VideoWidget::activate() +{ + CCS->videoh->open(current); + auto audioData = CCS->videoh->getAudio(current); + videoSoundHandle = CCS->soundh->playSound(audioData, -1); + + if (videoSoundHandle != -1) + { + CCS->soundh->setCallback(videoSoundHandle, [this](){ + if (GH.windows().isTopWindow(this)) + this->videoSoundHandle = -1; + }); + } +} + +void VideoWidget::deactivate() +{ + CCS->videoh->close(); + CCS->soundh->stopSound(videoSoundHandle); +} + +void VideoWidget::showAll(Canvas & to) +{ + CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false); +} + +void VideoWidget::tick(uint32_t msPassed) +{ + +} diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h new file mode 100644 index 000000000..31616337e --- /dev/null +++ b/client/widgets/VideoWidget.h @@ -0,0 +1,32 @@ +/* + * 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 VideoWidget final : public CIntObject +{ + VideoPath current; + VideoPath next; + + int videoSoundHandle; +public: + VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped); + VideoWidget(const Point & position, const VideoPath & looped); + ~VideoWidget(); + + void activate() override; + void deactivate() override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + void tick(uint32_t msPassed) override; +}; diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index c7364c3fb..fbcd16b7b 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -20,10 +20,10 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" -#include "../media/IVideoPlayer.h" #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(Point(30, 120), video); 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(); -} diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index d002f9aed..c4fcf31f9 100644 --- a/client/windows/CTutorialWindow.h +++ b/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 labelTitle; std::shared_ptr labelInformation; + std::shared_ptr videoPlayer; - std::string video; std::vector 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; }; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 7513559b8..da292f7ed 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -26,8 +26,6 @@ #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" -#include "../media/IVideoPlayer.h" - #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" #include "../widgets/CreatureCostBox.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" @@ -516,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(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); else if(const auto * townObj = dynamic_cast(TavernObj)) - CCS->videoh->open(townObj->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), townObj->town->clientInfo.tavernVideo); else - CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); + videoPlayer = std::make_shared(Point(70, 56), VideoPath::builtin("TAVERN.BIK")); addInvite(); } @@ -573,11 +572,6 @@ void CTavernWindow::close() CStatusbarWindow::close(); } -CTavernWindow::~CTavernWindow() -{ - CCS->videoh->close(); -} - void CTavernWindow::show(Canvas & to) { CWindowObject::show(to); @@ -601,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) diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 6cce1a0ea..fa40fa895 100644 --- a/client/windows/GUIClasses.h +++ b/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 cost; std::shared_ptr heroesForHire; std::shared_ptr heroDescription; + std::shared_ptr videoPlayer; std::shared_ptr rumor; @@ -272,7 +274,6 @@ public: void addInvite(); CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); - ~CTavernWindow(); void close() override; void recruitb(); From 661a66121b4c30f5f000f3480b63b834e5ab2ae1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 May 2024 19:07:00 +0300 Subject: [PATCH 03/15] Cleaned up IVideoPlayed API to remove global state --- client/CMT.cpp | 6 +- client/media/CEmptyVideoPlayer.h | 31 +- client/media/CVideoHandler.cpp | 729 ++++++++++--------------------- client/media/CVideoHandler.h | 108 +++-- client/media/IVideoPlayer.h | 40 +- client/widgets/VideoWidget.cpp | 45 +- client/widgets/VideoWidget.h | 5 + client/windows/CSpellWindow.cpp | 4 +- 8 files changed, 390 insertions(+), 578 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index e8ceb035a..859da2dce 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -398,15 +398,15 @@ 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)) + if(CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK"))) { 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)) + if (CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) { 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->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); } } CCS->soundh->stopSound(sound); diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index 99b0aac1c..a9f825d93 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -10,18 +10,29 @@ #pragma once #include "IVideoPlayer.h" -#include "../lib/Point.h" class CEmptyVideoPlayer final : public IVideoPlayer { public: - void redraw( int x, int y, SDL_Surface *dst, bool update) override {}; - void show( int x, int y, SDL_Surface *dst, bool update) override {}; - bool nextFrame() override {return false;}; - void close() override {}; - bool open(const VideoPath & name, bool scale) override {return false;}; - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function restart) override {} - bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override { return false; } - std::pair, si64> getAudio(const VideoPath & videoToOpen) override { return std::make_pair(nullptr, 0); }; - Point size() override { return Point(0, 0); }; + /// Plays video on top of the screen, returns only after playback is over + virtual bool playIntroVideo(const VideoPath & name) + { + return false; + }; + + virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) + { + } + + /// Load video from specified path + virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) + { + return nullptr; + }; + + /// Extracts audio data from provided video in wav format + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) + { + return { nullptr, 0}; + }; }; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 830c0491c..8bc819c29 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -12,14 +12,16 @@ #ifndef DISABLE_VIDEO -#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 "../CMT.h" +#include "../CPlayerInterface.h" +#include "../eventsSDL/InputHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/FramerateManager.h" +#include "../render/Canvas.h" +#include "../renderSDL/SDL_Extensions.h" + +#include "../../lib/filesystem/CInputStream.h" +#include "../../lib/filesystem/Filesystem.h" #include @@ -30,18 +32,11 @@ extern "C" { #include } -#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) +static int lodRead(void * opaque, uint8_t * buf, int size) { - auto video = reinterpret_cast(opaque); - int bytes = static_cast(video->data->read(buf, size)); + auto * data = static_cast(opaque); + int bytes = static_cast(data->read(buf, size)); if(bytes == 0) return AVERROR_EOF; @@ -50,509 +45,298 @@ static int lodRead(void* opaque, uint8_t* buf, int size) static si64 lodSeek(void * opaque, si64 pos, int whence) { - auto video = reinterpret_cast(opaque); + auto * data = static_cast(opaque); - if (whence & AVSEEK_SIZE) - return video->data->getSize(); + if(whence & AVSEEK_SIZE) + return data->getSize(); - return video->data->seek(pos); + return data->seek(pos); } -// Define a set of functions to read data -static int lodReadAudio(void* opaque, uint8_t* buf, int size) +[[noreturn]] static void throwFFmpegError(int errorCode) { - auto video = reinterpret_cast(opaque); - int bytes = static_cast(video->dataAudio->read(buf, size)); - if(bytes == 0) - return AVERROR_EOF; + std::array errorMessage{}; + av_strerror(errorCode, errorMessage.data(), errorMessage.size()); - return bytes; + throw std::runtime_error(errorMessage.data()); } -static si64 lodSeekAudio(void * opaque, si64 pos, int whence) +void CVideoInstance::open(const VideoPath & videoToOpen) { - auto video = reinterpret_cast(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, 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; + if(CResourceHandler::get()->existsResource(videoToOpen)) + state.actualPath = videoToOpen; else - fname = videoToOpen.addPrefix("VIDEO/"); + state.actualPath = 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); + state.videoData = CResourceHandler::get()->load(state.actualPath); 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); + auto * buffer = static_cast(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg + state.context = avio_alloc_context(buffer, BUFFER_SIZE, 0, state.videoData.get(), lodRead, nullptr, lodSeek); - format = avformat_alloc_context(); - format->pb = context; + state.formatContext = avformat_alloc_context(); + state.formatContext->pb = state.context; // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr); + int avfopen = avformat_open_input(&state.formatContext, "dummyFilename", nullptr, nullptr); + + if(avfopen != 0) + throwFFmpegError(avfopen); - if (avfopen != 0) - { - return false; - } // Retrieve stream information - if (avformat_find_stream_info(format, nullptr) < 0) - return false; + int findStreamInfo = avformat_find_stream_info(state.formatContext, nullptr); - // Find the first video stream - stream = -1; - for(ui32 i=0; inb_streams; i++) + if(avfopen < 0) + throwFFmpegError(findStreamInfo); + + for(int i = 0; i < state.formatContext->nb_streams; i++) { - if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + if(state.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video.streamIndex == -1) { - stream = i; - break; + openStream(video, i); } + + if(state.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio.streamIndex == -1) + openStream(audio, i); } +} - if (stream < 0) - // No video stream in that file - return false; +void CVideoInstance::openStream(FFMpegStreamState & streamState, int streamIndex) +{ + streamState.streamIndex = streamIndex; - // Find the decoder for the video stream - codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); + // Find the decoder for the stream + streamState.codec = avcodec_find_decoder(state.formatContext->streams[streamIndex]->codecpar->codec_id); - if (codec == nullptr) - { - // Unsupported codec - return false; - } + if(streamState.codec == nullptr) + throw std::runtime_error("Unsupported codec"); + + streamState.codecContext = avcodec_alloc_context3(streamState.codec); + if(streamState.codecContext == nullptr) + throw std::runtime_error("Failed to create codec context"); - 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) + int ret = avcodec_parameters_to_context(streamState.codecContext, state.formatContext->streams[streamIndex]->codecpar); + if(ret < 0) { //We cannot get codec from parameters - avcodec_free_context(&codecContext); - return false; + avcodec_free_context(&streamState.codecContext); + throwFFmpegError(ret); } // Open codec - if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) + ret = avcodec_open2(streamState.codecContext, streamState.codec, nullptr); + if(ret < 0) { // Could not open codec - codec = nullptr; - return false; + streamState.codec = nullptr; + throwFFmpegError(ret); } +} + +void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput) +{ + if (video.streamIndex == -1) + throw std::runtime_error("Invalid file state! No video stream!"); + // Allocate video frame - frame = av_frame_alloc(); + output.frame = av_frame_alloc(); //setup scaling - if(scale) + if(scaleToScreenSize) { - pos.w = screen->w; - pos.h = screen->h; + output.dimensions.x = screen->w; + output.dimensions.y = screen->h; } else { - pos.w = codecContext->width; - pos.h = codecContext->height; + output.dimensions.x = video.codecContext->width; + output.dimensions.y = video.codecContext->height; } // Allocate a place to put our YUV image on that screen - if (overlay) + if (useTextureOutput) { - 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, + output.texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y); + output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt, + output.dimensions.x, output.dimensions.y, 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, + output.surface = CSDL_Ext::newSurface(output.dimensions.x, output.dimensions.y); + output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt, + output.dimensions.x, output.dimensions.y, AV_PIX_FMT_RGB32, SWS_BICUBIC, nullptr, nullptr, nullptr); } - if (sws == nullptr) - return false; - - return true; + if (output.sws == nullptr) + throw std::runtime_error("Failed to create sws"); } -// Read the next frame. Return false on error/end of file. -bool CVideoPlayer::nextFrame() +bool CVideoInstance::nextFrame() { AVPacket packet; - int frameFinished = 0; - bool gotError = false; - if (sws == nullptr) - return false; - - while(!frameFinished) + for(;;) { - int ret = av_read_frame(format, &packet); - if (ret < 0) + int ret = av_read_frame(state.formatContext, &packet); + if(ret < 0) { - // Error. It's probably an end of file. - if (doLoop && !gotError) + if(ret == AVERROR_EOF) + return false; + throwFFmpegError(ret); + } + + // Is this a packet from the video stream? + if(packet.stream_index == video.streamIndex) + { + // Decode video frame + int rc = avcodec_send_packet(video.codecContext, &packet); + if(rc < 0) + throwFFmpegError(ret); + + rc = avcodec_receive_frame(video.codecContext, output.frame); + if(rc < 0) + throwFFmpegError(ret); + + uint8_t * data[4]; + int linesize[4]; + + if(output.texture) { - // Rewind - frameTime = 0; - if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0) - break; - gotError = true; + av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_YUV420P, 1); + sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); + SDL_UpdateYUVTexture(output.texture, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); + av_freep(&data[0]); } else { - break; + // Avoid buffer overflow caused by sws_scale(): + // http://trac.ffmpeg.org/ticket/9254 + + size_t pic_bytes = output.surface->pitch * output.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] = output.surface->pitch; + + sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); + memcpy(output.surface->pixels, for_sws, pic_bytes); + av_free(for_sws); } - } - 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 true; } } - - return frameFinished != 0; } -void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update ) +bool CVideoInstance::videoEnded() { - 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); + return output.videoEnded; } -void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) +void CVideoInstance::close() { - show(x, y, dst, update); + sws_freeContext(output.sws); + av_frame_free(&output.frame); + + SDL_DestroyTexture(output.texture); + SDL_FreeSurface(output.surface); + + // state.videoStream.codec??? + // state.audioStream.codec??? + + avcodec_close(video.codecContext); + avcodec_free_context(&video.codecContext); + + avcodec_close(audio.codecContext); + avcodec_free_context(&audio.codecContext); + + avformat_close_input(&state.formatContext); + av_free(state.context); + + output = FFMpegVideoOutput(); + video = FFMpegStreamState(); + audio = FFMpegStreamState(); + state = FFMpegFileState(); } -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) +CVideoInstance::~CVideoInstance() { - if (sws == nullptr) - return; + close(); +} -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else +Point CVideoInstance::size() +{ + if(!output.frame) + throw std::runtime_error("Invalid video frame!"); + + return Point(output.frame->width, output.frame->height); +} + +void CVideoInstance::show(const Point & position, Canvas & canvas) +{ + if(output.sws == nullptr) + throw std::runtime_error("No video to show!"); + + CSDL_Ext::blitSurface(output.surface, canvas.getInternalSurface(), position); +} + +void CVideoInstance::tick(uint32_t msPassed) +{ + if(output.sws == nullptr) + throw std::runtime_error("No video to show!"); + + if(output.videoEnded) + throw std::runtime_error("Video already ended!"); + +# if(LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = output.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; +# endif + double frameEndTime = (output.frame->pts + packet_duration) * av_q2d(state.formatContext->streams[video.streamIndex]->time_base); + output.frameTime += msPassed / 1000.0; - if (frameTime >= frameEndTime ) + if(output.frameTime >= frameEndTime) { - if (nextFrame()) - show(x,y,dst,update); - else - { - if(onVideoRestart) - onVideoRestart(); - VideoPath filenameToReopen = fname; // create copy to backup this->fname - open(filenameToReopen, false); - 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); + if(!nextFrame()) + output.videoEnded = true; } } -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; - } -} +# if 0 std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) { std::pair, si64> dat(std::make_pair(nullptr, 0)); - VideoPath fnameAudio; + FFMpegFileState audio; + openVideoFile(audio, videoToOpen); - if (CResourceHandler::get()->existsResource(videoToOpen)) - fnameAudio = videoToOpen; - else - fnameAudio = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fnameAudio)) + if (audio.audioStream.streamIndex < 0) { - logGlobal->error("Error: video %s was not found", fnameAudio.getName()); - return dat; + closeVideoFile(audio); + return { nullptr, 0}; } - 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(); - } + AVFrame *frameAudio = av_frame_alloc(); AVPacket packet; std::vector samples; - while (av_read_frame(formatAudio, &packet) >= 0) + while (av_read_frame(audio.formatContext, &packet) >= 0) { - if(packet.stream_index == streamAudio) + if(packet.stream_index == audio.audioStream.streamIndex) { - int rc = avcodec_send_packet(codecContextAudio, &packet); + int rc = avcodec_send_packet(audio.audioStream.codecContext, &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)); + rc = avcodec_receive_frame(audio.audioStream.codecContext, frameAudio); + int bytesToRead = (frameAudio->nb_samples * 2 * (audio.formatContext->streams[audio.audioStream.streamIndex]->codecpar->bits_per_coded_sample / 8)); if (rc >= 0) for (int s = 0; s < bytesToRead; s += sizeof(ui8)) { @@ -561,7 +345,6 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath samples.push_back(value); } } - av_packet_unref(&packet); } @@ -584,8 +367,8 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath 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; + wav.SamplesPerSec = audio.formatContext->streams[audio.audioStream.streamIndex]->codecpar->sample_rate; + wav.bitsPerSample = audio.formatContext->streams[audio.audioStream.streamIndex]->codecpar->bits_per_coded_sample; auto wavPtr = reinterpret_cast(&wav); dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); @@ -595,48 +378,23 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath 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; - } + closeVideoFile(audio); return dat; } -Point CVideoPlayer::size() -{ - if(frame) - return Point(frame->width, frame->height); - else - return Point(0, 0); -} +# endif -// Plays a video. Only works for overlays. -bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay) +bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey) { - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - y--; + CVideoInstance instance; - pos.x = x; - pos.y = y; - frameTime = 0.0; + instance.open(name); + instance.prepareOutput(scale, useOverlay); auto lastTimePoint = boost::chrono::steady_clock::now(); - while(nextFrame()) + while(instance.nextFrame()) { if(stopOnKey) { @@ -645,26 +403,28 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay) return false; } - SDL_Rect rect = CSDL_Ext::toSDL(pos); + SDL_Rect rect; + rect.x = position.x; + rect.y = position.y; + rect.w = instance.output.dimensions.x; + rect.h = instance.output.dimensions.y; - if(overlay) - { + if(useOverlay) SDL_RenderFillRect(mainRenderer, &rect); - } else - { SDL_RenderClear(mainRenderer); - } - SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); + + SDL_RenderCopy(mainRenderer, instance.output.texture, nullptr, &rect); SDL_RenderPresent(mainRenderer); #if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; + auto packet_duration = instance.output.frame->pkt_duration; #else - auto packet_duration = frame->duration; + auto packet_duration = output.frame->duration; #endif + // Framerate delay - double targetFrameTimeSeconds = packet_duration * av_q2d(format->streams[stream]->time_base); + double targetFrameTimeSeconds = packet_duration * av_q2d(instance.state.formatContext->streams[instance.video.streamIndex]->time_base); auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); auto timePointAfterPresent = boost::chrono::steady_clock::now(); @@ -675,39 +435,32 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay) lastTimePoint = boost::chrono::steady_clock::now(); } - return true; } -bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) +bool CVideoPlayer::playIntroVideo(const VideoPath & name) { - 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; + return openAndPlayVideoImpl(name, Point(0,0), true, true, true); } -CVideoPlayer::~CVideoPlayer() +void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position) { - close(); + openAndPlayVideoImpl(name, position, false, false, false); +} + +std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool scaleToScreen) +{ + auto result = std::make_unique(); + + result->open(name); + result->prepareOutput(scaleToScreen, false); + + return result; +} + +std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) +{ + return {nullptr, 0}; } #endif - diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index a70177be9..06345d383 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -11,9 +11,9 @@ #ifndef DISABLE_VIDEO -#include "IVideoPlayer.h" +# include "IVideoPlayer.h" -#include "../lib/Rect.h" +# include "../lib/Rect.h" struct SDL_Surface; struct SDL_Texture; @@ -27,55 +27,69 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; VCMI_LIB_NAMESPACE_END +struct FFMpegStreamState +{ + int streamIndex = -1; + const AVCodec * codec = nullptr; + AVCodecContext * codecContext = nullptr; +}; + +struct FFMpegFileState +{ + VideoPath actualPath; + std::unique_ptr videoData; + AVIOContext * context = nullptr; + AVFormatContext * formatContext = nullptr; +}; + +struct FFMpegVideoOutput +{ + AVFrame * frame = nullptr; + struct SwsContext * sws = nullptr; + SDL_Texture * texture = nullptr; + SDL_Surface * surface = nullptr; + Point dimensions; + + /// video playback current progress, in seconds + double frameTime = 0.0; + bool videoEnded = false; +}; + +class CVideoInstance final : public IVideoInstance +{ + friend class CVideoPlayer; + + FFMpegFileState state; + FFMpegStreamState video; + FFMpegStreamState audio; + FFMpegVideoOutput output; + + void open(const VideoPath & fname); + void openStream(FFMpegStreamState & streamState, int streamIndex); + void prepareOutput(bool scaleToScreenSize, bool useTextureOutput); + bool nextFrame(); + void close(); + +public: + ~CVideoInstance(); + + 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 { - int stream; // stream index in video - AVFormatContext *format; - AVCodecContext *codecContext; // codec context for stream - const AVCodec *codec; - AVFrame *frame; - struct SwsContext *sws; + bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey); + void openVideoFile(CVideoInstance & state, const VideoPath & fname); - 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, bool scale); public: - CVideoPlayer(); - ~CVideoPlayer(); - - bool init(); - bool open(const VideoPath & fname, bool scale) override; - void close() override; - bool nextFrame() override; // display next frame - - void show(int x, int y, SDL_Surface *dst, bool update) override; //blit current frame - void redraw(int x, int y, SDL_Surface *dst, bool update) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) 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, si64> getAudio(const VideoPath & videoToOpen) override; - - Point size() override; - - // public to allow access from ffmpeg IO functions - std::unique_ptr data; - std::unique_ptr dataAudio; + bool playIntroVideo(const VideoPath & name) final; + void playSpellbookAnimation(const VideoPath & name, const Point & position) final; + std::unique_ptr open(const VideoPath & name, bool scaleToScreen) final; + std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; #endif diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index c7037fba9..4df144f4c 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -11,30 +11,44 @@ #include "../lib/filesystem/ResourcePath.h" -struct SDL_Surface; +class Canvas; VCMI_LIB_NAMESPACE_BEGIN class Point; VCMI_LIB_NAMESPACE_END -enum class EVideoType : ui8 +class IVideoInstance { - INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false - SPELLBOOK // overlay video: stopOnKey = false, scale = false, overlay = true +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: - 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 void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr) = 0; - virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) = 0; - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; - virtual Point size() = 0; + /// 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 open(const VideoPath & name, bool scaleToScreen) = 0; + + /// Extracts audio data from provided video in wav format. Return nullptr on failure + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; virtual ~IVideoPlayer() = default; }; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index fe36725e3..380c4f467 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -11,58 +11,73 @@ #include "VideoWidget.h" #include "../CGameInfo.h" -#include "../media/IVideoPlayer.h" -#include "../media/ISoundPlayer.h" -#include "../render/Canvas.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../media/ISoundPlayer.h" +#include "../media/IVideoPlayer.h" +#include "../render/Canvas.h" VideoWidget::VideoWidget(const Point & position, const VideoPath & looped) : VideoWidget(position, VideoPath(), looped) { + addUsedEvents(TIME); } VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped) - :current(prologue) - ,next(looped) - ,videoSoundHandle(-1) + : current(prologue) + , next(looped) + , videoSoundHandle(-1) { + if(current.empty()) + videoInstance = CCS->videoh->open(looped, false); + else + videoInstance = CCS->videoh->open(current, false); } VideoWidget::~VideoWidget() = default; void VideoWidget::show(Canvas & to) { - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false); + if(videoInstance) + videoInstance->show(pos.topLeft(), to); } void VideoWidget::activate() { - CCS->videoh->open(current); auto audioData = CCS->videoh->getAudio(current); videoSoundHandle = CCS->soundh->playSound(audioData, -1); - if (videoSoundHandle != -1) + if(videoSoundHandle != -1) { - CCS->soundh->setCallback(videoSoundHandle, [this](){ - if (GH.windows().isTopWindow(this)) - this->videoSoundHandle = -1; - }); + CCS->soundh->setCallback( + videoSoundHandle, + [this]() + { + if(GH.windows().isTopWindow(this)) + this->videoSoundHandle = -1; + } + ); } } void VideoWidget::deactivate() { - CCS->videoh->close(); CCS->soundh->stopSound(videoSoundHandle); } void VideoWidget::showAll(Canvas & to) { - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false); + if(videoInstance) + videoInstance->show(pos.topLeft(), to); } void VideoWidget::tick(uint32_t msPassed) { + if(videoInstance) + { + videoInstance->tick(msPassed); + if(videoInstance->videoEnded()) + videoInstance = CCS->videoh->open(next, false); + } } diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h index 31616337e..173087323 100644 --- a/client/widgets/VideoWidget.h +++ b/client/widgets/VideoWidget.h @@ -13,12 +13,17 @@ #include "../lib/filesystem/ResourcePath.h" +class IVideoInstance; + class VideoWidget final : public CIntObject { + std::unique_ptr videoInstance; + VideoPath current; VideoPath next; int videoSoundHandle; + public: VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped); VideoWidget(const Point & position, const VideoPath & looped); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 0a4c05330..82d969a77 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -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) From 7ad64207ed666bf3f1a7a6cb90e87f19926283cd Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 May 2024 20:08:29 +0300 Subject: [PATCH 04/15] Prefer high-quality bik's to smk's --- client/media/CVideoHandler.cpp | 23 ++++++++++++++++++----- lib/filesystem/ResourcePath.cpp | 5 +---- lib/filesystem/ResourcePath.h | 1 + lib/json/JsonValidator.cpp | 1 + 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 8bc819c29..1c7337360 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -61,14 +61,27 @@ static si64 lodSeek(void * opaque, si64 pos, int whence) throw std::runtime_error(errorMessage.data()); } -void CVideoInstance::open(const VideoPath & videoToOpen) +static std::unique_ptr findVideoData(const VideoPath & videoToOpen) { if(CResourceHandler::get()->existsResource(videoToOpen)) - state.actualPath = videoToOpen; - else - state.actualPath = videoToOpen.addPrefix("VIDEO/"); + return CResourceHandler::get()->load(videoToOpen); - state.videoData = CResourceHandler::get()->load(state.actualPath); + auto highQualityVideoToOpenWithDir = videoToOpen.addPrefix("VIDEO/"); + auto lowQualityVideo = videoToOpen.toType(); + auto lowQualityVideoWithDir = highQualityVideoToOpenWithDir.toType(); + + if(CResourceHandler::get()->existsResource(highQualityVideoToOpenWithDir)) + return CResourceHandler::get()->load(highQualityVideoToOpenWithDir); + + if(CResourceHandler::get()->existsResource(lowQualityVideo)) + return CResourceHandler::get()->load(lowQualityVideo); + + return CResourceHandler::get()->load(lowQualityVideoWithDir); +} + +void CVideoInstance::open(const VideoPath & videoToOpen) +{ + state.videoData = findVideoData(videoToOpen); static const int BUFFER_SIZE = 4096; diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index 15efb5cb7..d6bc81a2d 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/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}, diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 250e76bfc..fde6bec9f 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -46,6 +46,7 @@ enum class EResType TTF_FONT, IMAGE, VIDEO, + VIDEO_LOW_QUALITY, SOUND, ARCHIVE_VID, ARCHIVE_ZIP, diff --git a/lib/json/JsonValidator.cpp b/lib/json/JsonValidator.cpp index fd4b67d49..e2f6b1e06 100644 --- a/lib/json/JsonValidator.cpp +++ b/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 From 8e78e3bfb5961983d98f4c6aec34fc2befac353e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 May 2024 20:14:58 +0300 Subject: [PATCH 05/15] Use RGB texture for videos where this format is better than YUV --- client/media/CVideoHandler.cpp | 41 ++++++++++++++++++++++++++-------- client/media/CVideoHandler.h | 4 ++-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 1c7337360..7ea3c0a2c 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -170,9 +170,20 @@ void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput // Allocate a place to put our YUV image on that screen if (useTextureOutput) { - output.texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y); + 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(), video.codecContext->pix_fmt, false, nullptr); + + if (preferredFormat == AV_PIX_FMT_YUV420P) + output.textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y); + else + output.textureRGB = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y); output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt, - output.dimensions.x, output.dimensions.y, AV_PIX_FMT_YUV420P, + output.dimensions.x, output.dimensions.y, preferredFormat, SWS_BICUBIC, nullptr, nullptr, nullptr); } else @@ -213,17 +224,24 @@ bool CVideoInstance::nextFrame() if(rc < 0) throwFFmpegError(ret); - uint8_t * data[4]; - int linesize[4]; + uint8_t * data[4] = {}; + int linesize[4] = {}; - if(output.texture) + if(output.textureYUV) { av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_YUV420P, 1); sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); - SDL_UpdateYUVTexture(output.texture, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); + SDL_UpdateYUVTexture(output.textureYUV, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); av_freep(&data[0]); } - else + if(output.textureRGB) + { + av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_RGB32, 1); + sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); + SDL_UpdateTexture(output.textureRGB, nullptr, data[0], linesize[0]); + av_freep(&data[0]); + } + if (output.surface) { // Avoid buffer overflow caused by sws_scale(): // http://trac.ffmpeg.org/ticket/9254 @@ -254,7 +272,8 @@ void CVideoInstance::close() sws_freeContext(output.sws); av_frame_free(&output.frame); - SDL_DestroyTexture(output.texture); + SDL_DestroyTexture(output.textureYUV); + SDL_DestroyTexture(output.textureRGB); SDL_FreeSurface(output.surface); // state.videoStream.codec??? @@ -427,7 +446,11 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po else SDL_RenderClear(mainRenderer); - SDL_RenderCopy(mainRenderer, instance.output.texture, nullptr, &rect); + if (instance.output.textureYUV) + SDL_RenderCopy(mainRenderer, instance.output.textureYUV, nullptr, &rect); + else + SDL_RenderCopy(mainRenderer, instance.output.textureRGB, nullptr, &rect); + SDL_RenderPresent(mainRenderer); #if (LIBAVUTIL_VERSION_MAJOR < 58) diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 06345d383..9a578260c 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -36,7 +36,6 @@ struct FFMpegStreamState struct FFMpegFileState { - VideoPath actualPath; std::unique_ptr videoData; AVIOContext * context = nullptr; AVFormatContext * formatContext = nullptr; @@ -46,7 +45,8 @@ struct FFMpegVideoOutput { AVFrame * frame = nullptr; struct SwsContext * sws = nullptr; - SDL_Texture * texture = nullptr; + SDL_Texture * textureRGB = nullptr; + SDL_Texture * textureYUV = nullptr; SDL_Surface * surface = nullptr; Point dimensions; From 3f8781db69cb49475f59808b60e6f3e66ffbca39 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 May 2024 22:22:57 +0300 Subject: [PATCH 06/15] Fixed sound extraction, slightly better approach to ffmpeg --- client/CMT.cpp | 11 -- client/media/CVideoHandler.cpp | 233 ++++++++++++++++++++++----------- client/media/CVideoHandler.h | 24 ++-- client/media/IVideoPlayer.h | 6 +- client/widgets/VideoWidget.cpp | 4 +- 5 files changed, 176 insertions(+), 102 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 859da2dce..d3aeaed0a 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -396,20 +396,9 @@ 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->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK"))) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); - sound = CCS->soundh->playSound(audioData); if (CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); - sound = CCS->soundh->playSound(audioData); CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); - } - } - CCS->soundh->stopSound(sound); } static void mainLoop() diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 7ea3c0a2c..ec664ae5b 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -12,6 +12,9 @@ #ifndef DISABLE_VIDEO +#include "ISoundPlayer.h" + +#include "../CGameInfo.h" #include "../CMT.h" #include "../CPlayerInterface.h" #include "../eventsSDL/InputHandler.h" @@ -81,12 +84,16 @@ static std::unique_ptr findVideoData(const VideoPath & videoToOpen void CVideoInstance::open(const VideoPath & videoToOpen) { - state.videoData = findVideoData(videoToOpen); + input = findVideoData(videoToOpen); +} +void CVideoInstance::openContext(FFMpegStreamState & state) +{ static const int BUFFER_SIZE = 4096; + input->seek(0); auto * buffer = static_cast(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg - state.context = avio_alloc_context(buffer, BUFFER_SIZE, 0, state.videoData.get(), lodRead, nullptr, lodSeek); + state.context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek); state.formatContext = avformat_alloc_context(); state.formatContext->pb = state.context; @@ -101,52 +108,55 @@ void CVideoInstance::open(const VideoPath & videoToOpen) if(avfopen < 0) throwFFmpegError(findStreamInfo); - - for(int i = 0; i < state.formatContext->nb_streams; i++) - { - if(state.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video.streamIndex == -1) - { - openStream(video, i); - } - - if(state.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio.streamIndex == -1) - openStream(audio, i); - } } -void CVideoInstance::openStream(FFMpegStreamState & streamState, int streamIndex) +void CVideoInstance::openCodec(FFMpegStreamState & state, int streamIndex) { - streamState.streamIndex = streamIndex; + state.streamIndex = streamIndex; // Find the decoder for the stream - streamState.codec = avcodec_find_decoder(state.formatContext->streams[streamIndex]->codecpar->codec_id); + state.codec = avcodec_find_decoder(state.formatContext->streams[streamIndex]->codecpar->codec_id); - if(streamState.codec == nullptr) + if(state.codec == nullptr) throw std::runtime_error("Unsupported codec"); - streamState.codecContext = avcodec_alloc_context3(streamState.codec); - if(streamState.codecContext == nullptr) + state.codecContext = avcodec_alloc_context3(state.codec); + if(state.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(streamState.codecContext, state.formatContext->streams[streamIndex]->codecpar); + int ret = avcodec_parameters_to_context(state.codecContext, state.formatContext->streams[streamIndex]->codecpar); if(ret < 0) { //We cannot get codec from parameters - avcodec_free_context(&streamState.codecContext); + avcodec_free_context(&state.codecContext); throwFFmpegError(ret); } // Open codec - ret = avcodec_open2(streamState.codecContext, streamState.codec, nullptr); + ret = avcodec_open2(state.codecContext, state.codec, nullptr); if(ret < 0) { // Could not open codec - streamState.codec = nullptr; + state.codec = nullptr; throwFFmpegError(ret); } } +void CVideoInstance::openVideo() +{ + openContext(video); + + for(int i = 0; i < video.formatContext->nb_streams; i++) + { + if(video.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + { + openCodec(video, i); + return; + } + } +} + void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput) { if (video.streamIndex == -1) @@ -204,7 +214,7 @@ bool CVideoInstance::nextFrame() for(;;) { - int ret = av_read_frame(state.formatContext, &packet); + int ret = av_read_frame(video.formatContext, &packet); if(ret < 0) { if(ret == AVERROR_EOF) @@ -218,11 +228,11 @@ bool CVideoInstance::nextFrame() // Decode video frame int rc = avcodec_send_packet(video.codecContext, &packet); if(rc < 0) - throwFFmpegError(ret); + throwFFmpegError(rc); rc = avcodec_receive_frame(video.codecContext, output.frame); if(rc < 0) - throwFFmpegError(ret); + throwFFmpegError(rc); uint8_t * data[4] = {}; int linesize[4] = {}; @@ -276,22 +286,25 @@ void CVideoInstance::close() SDL_DestroyTexture(output.textureRGB); SDL_FreeSurface(output.surface); + closeState(video); +} + +void CVideoInstance::closeState(FFMpegStreamState & streamState) +{ // state.videoStream.codec??? // state.audioStream.codec??? avcodec_close(video.codecContext); avcodec_free_context(&video.codecContext); - avcodec_close(audio.codecContext); - avcodec_free_context(&audio.codecContext); + avcodec_close(video.codecContext); + avcodec_free_context(&video.codecContext); - avformat_close_input(&state.formatContext); - av_free(state.context); + avformat_close_input(&video.formatContext); + av_free(video.context); output = FFMpegVideoOutput(); video = FFMpegStreamState(); - audio = FFMpegStreamState(); - state = FFMpegFileState(); } CVideoInstance::~CVideoInstance() @@ -328,7 +341,7 @@ void CVideoInstance::tick(uint32_t msPassed) # else auto packet_duration = frame->duration; # endif - double frameEndTime = (output.frame->pts + packet_duration) * av_q2d(state.formatContext->streams[video.streamIndex]->time_base); + double frameEndTime = (output.frame->pts + packet_duration) * av_q2d(video.formatContext->streams[video.streamIndex]->time_base); output.frameTime += msPassed / 1000.0; if(output.frameTime >= frameEndTime) @@ -338,44 +351,118 @@ void CVideoInstance::tick(uint32_t msPassed) } } -# if 0 - -std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) +static int32_t sampleSizeBytes(int audioFormat) { - std::pair, si64> dat(std::make_pair(nullptr, 0)); - - FFMpegFileState audio; - openVideoFile(audio, videoToOpen); - - if (audio.audioStream.streamIndex < 0) + switch (audioFormat) { - closeVideoFile(audio); - return { nullptr, 0}; + case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits + case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar + return 1; + case AV_SAMPLE_FMT_S16: ///< signed 16 bits + case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar + return 2; + case AV_SAMPLE_FMT_S32: ///< signed 32 bits + case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar + case AV_SAMPLE_FMT_FLT: ///< float + case AV_SAMPLE_FMT_FLTP: ///< float, planar + return 4; + case AV_SAMPLE_FMT_DBL: ///< double + case AV_SAMPLE_FMT_DBLP: ///< double, planar + case AV_SAMPLE_FMT_S64: ///< signed 64 bits + case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar + return 8; + } + throw std::runtime_error("Invalid audio format"); +} + +static int32_t sampleWavType(int audioFormat) +{ + switch (audioFormat) + { + case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits + case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar + case AV_SAMPLE_FMT_S16: ///< signed 16 bits + case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar + case AV_SAMPLE_FMT_S32: ///< signed 32 bits + case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar + case AV_SAMPLE_FMT_S64: ///< signed 64 bits + case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar + return 1; // PCM + + case AV_SAMPLE_FMT_FLT: ///< float + case AV_SAMPLE_FMT_FLTP: ///< float, planar + case AV_SAMPLE_FMT_DBL: ///< double + case AV_SAMPLE_FMT_DBLP: ///< double, planar + return 3; // IEEE float + } + throw std::runtime_error("Invalid audio format"); +} + +void CVideoInstance::playAudio() +{ + FFMpegStreamState audio; + + openContext(audio); + + for(int i = 0; i < audio.formatContext->nb_streams; i++) + { + if(audio.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + { + openCodec(audio, i); + break; + } } - // Open codec + std::pair, si64> dat(std::make_pair(nullptr, 0)); + + if (audio.streamIndex < 0) + return; // nothing to play + + const auto * codecpar = audio.formatContext->streams[audio.streamIndex]->codecpar; AVFrame *frameAudio = av_frame_alloc(); - + AVFrame *frameVideo = av_frame_alloc(); AVPacket packet; std::vector samples; + int32_t sampleSize = sampleSizeBytes(codecpar->format); + + samples.reserve(44100 * 5); // arbitrary 5-second buffer + while (av_read_frame(audio.formatContext, &packet) >= 0) { - if(packet.stream_index == audio.audioStream.streamIndex) + if (packet.stream_index == video.streamIndex) { - int rc = avcodec_send_packet(audio.audioStream.codecContext, &packet); - if (rc >= 0) - packet.size = 0; - rc = avcodec_receive_frame(audio.audioStream.codecContext, frameAudio); - int bytesToRead = (frameAudio->nb_samples * 2 * (audio.formatContext->streams[audio.audioStream.streamIndex]->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); - } + // Decode video frame + int rc = avcodec_send_packet(video.codecContext, &packet); + if(rc < 0) + throwFFmpegError(rc); + + rc = avcodec_receive_frame(video.codecContext, frameVideo); + if(rc < 0) + throwFFmpegError(rc); + } + + if(packet.stream_index == audio.streamIndex) + { + int rc = avcodec_send_packet(audio.codecContext, &packet); + + if(rc < 0) + throwFFmpegError(rc); + + for (;;) + { + rc = avcodec_receive_frame(audio.codecContext, frameAudio); + if (rc == AVERROR(EAGAIN)) + break; + + if(rc < 0) + throwFFmpegError(rc); + + int bytesToRead = frameAudio->nb_samples * 2 * sampleSize; + + samples.insert(samples.end(), frameAudio->data[0], frameAudio->data[0] + bytesToRead); + } } av_packet_unref(&packet); } @@ -391,16 +478,19 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath ui32 SamplesPerSec = 22050; ui32 bytesPerSec = 22050 * 2; ui16 blockAlign = 2; - ui16 bitsPerSample = 16; + ui16 bitsPerSample = 32; 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 = audio.formatContext->streams[audio.audioStream.streamIndex]->codecpar->sample_rate; - wav.bitsPerSample = audio.formatContext->streams[audio.audioStream.streamIndex]->codecpar->bits_per_coded_sample; + wav.AudioFormat = sampleWavType(codecpar->format); + wav.NumOfChan = codecpar->channels; + wav.SamplesPerSec = codecpar->sample_rate; + wav.bytesPerSec = codecpar->sample_rate * sampleSize; + wav.bitsPerSample = sampleSize * 8; + wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; auto wavPtr = reinterpret_cast(&wav); dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); @@ -410,18 +500,17 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath if (frameAudio) av_frame_free(&frameAudio); - closeVideoFile(audio); - - return dat; + CCS->soundh->playSound(dat); + closeState(audio); } -# endif - bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey) { CVideoInstance instance; instance.open(name); + instance.playAudio(); + instance.openVideo(); instance.prepareOutput(scale, useOverlay); auto lastTimePoint = boost::chrono::steady_clock::now(); @@ -460,7 +549,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po #endif // Framerate delay - double targetFrameTimeSeconds = packet_duration * av_q2d(instance.state.formatContext->streams[instance.video.streamIndex]->time_base); + double targetFrameTimeSeconds = packet_duration * av_q2d(instance.video.formatContext->streams[instance.video.streamIndex]->time_base); auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); auto timePointAfterPresent = boost::chrono::steady_clock::now(); @@ -489,14 +578,10 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool auto result = std::make_unique(); result->open(name); + result->openVideo(); result->prepareOutput(scaleToScreen, false); return result; } -std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) -{ - return {nullptr, 0}; -} - #endif diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 9a578260c..bfb8e865b 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -29,16 +29,12 @@ VCMI_LIB_NAMESPACE_END struct FFMpegStreamState { - int streamIndex = -1; - const AVCodec * codec = nullptr; - AVCodecContext * codecContext = nullptr; -}; - -struct FFMpegFileState -{ - std::unique_ptr videoData; AVIOContext * context = nullptr; AVFormatContext * formatContext = nullptr; + + const AVCodec * codec = nullptr; + AVCodecContext * codecContext = nullptr; + int streamIndex = -1; }; struct FFMpegVideoOutput @@ -59,16 +55,20 @@ class CVideoInstance final : public IVideoInstance { friend class CVideoPlayer; - FFMpegFileState state; + std::unique_ptr input; + FFMpegStreamState video; - FFMpegStreamState audio; FFMpegVideoOutput output; void open(const VideoPath & fname); - void openStream(FFMpegStreamState & streamState, int streamIndex); + void openContext(FFMpegStreamState & streamState); + void openCodec(FFMpegStreamState & streamState, int streamIndex); + void openVideo(); void prepareOutput(bool scaleToScreenSize, bool useTextureOutput); + bool nextFrame(); void close(); + void closeState(FFMpegStreamState & streamState); public: ~CVideoInstance(); @@ -78,6 +78,7 @@ public: void show(const Point & position, Canvas & canvas) final; void tick(uint32_t msPassed) final; + void playAudio() final; }; class CVideoPlayer final : public IVideoPlayer @@ -89,7 +90,6 @@ public: bool playIntroVideo(const VideoPath & name) final; void playSpellbookAnimation(const VideoPath & name, const Point & position) final; std::unique_ptr open(const VideoPath & name, bool scaleToScreen) final; - std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; #endif diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index 4df144f4c..a0c1e6dc7 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -32,6 +32,9 @@ public: /// Advances video playback by specified duration virtual void tick(uint32_t msPassed) = 0; + /// Attempts to start audio playback from video, if any exists + virtual void playAudio() = 0; + virtual ~IVideoInstance() = default; }; @@ -47,8 +50,5 @@ public: /// Load video from specified path. Returns nullptr on failure virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) = 0; - /// Extracts audio data from provided video in wav format. Return nullptr on failure - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; - virtual ~IVideoPlayer() = default; }; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index 380c4f467..76627be5e 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -44,8 +44,8 @@ void VideoWidget::show(Canvas & to) void VideoWidget::activate() { - auto audioData = CCS->videoh->getAudio(current); - videoSoundHandle = CCS->soundh->playSound(audioData, -1); + if(videoInstance) + videoInstance->playAudio(); if(videoSoundHandle != -1) { From 75941a6869af283574667575b411a531c07ad35e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 May 2024 19:09:45 +0300 Subject: [PATCH 07/15] Unified audio and video handling of ffmpeg --- client/media/CVideoHandler.cpp | 477 ++++++++++++++++++--------------- client/media/CVideoHandler.h | 62 +++-- client/media/IVideoPlayer.h | 6 +- client/widgets/VideoWidget.cpp | 4 +- 4 files changed, 309 insertions(+), 240 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index ec664ae5b..c028dd99b 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -82,99 +82,103 @@ static std::unique_ptr findVideoData(const VideoPath & videoToOpen return CResourceHandler::get()->load(lowQualityVideoWithDir); } -void CVideoInstance::open(const VideoPath & videoToOpen) +void FFMpegStream::openInput(const VideoPath & videoToOpen) { input = findVideoData(videoToOpen); } -void CVideoInstance::openContext(FFMpegStreamState & state) +void FFMpegStream::openContext() { static const int BUFFER_SIZE = 4096; input->seek(0); auto * buffer = static_cast(av_malloc(BUFFER_SIZE)); // will be freed by ffmpeg - state.context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek); + context = avio_alloc_context(buffer, BUFFER_SIZE, 0, input.get(), lodRead, nullptr, lodSeek); - state.formatContext = avformat_alloc_context(); - state.formatContext->pb = state.context; + 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(&state.formatContext, "dummyFilename", nullptr, nullptr); + int avfopen = avformat_open_input(&formatContext, "dummyFilename", nullptr, nullptr); if(avfopen != 0) throwFFmpegError(avfopen); // Retrieve stream information - int findStreamInfo = avformat_find_stream_info(state.formatContext, nullptr); + int findStreamInfo = avformat_find_stream_info(formatContext, nullptr); if(avfopen < 0) throwFFmpegError(findStreamInfo); } -void CVideoInstance::openCodec(FFMpegStreamState & state, int streamIndex) +void FFMpegStream::openCodec(int desiredStreamIndex) { - state.streamIndex = streamIndex; + streamIndex = desiredStreamIndex; // Find the decoder for the stream - state.codec = avcodec_find_decoder(state.formatContext->streams[streamIndex]->codecpar->codec_id); + codec = avcodec_find_decoder(formatContext->streams[streamIndex]->codecpar->codec_id); - if(state.codec == nullptr) + if(codec == nullptr) throw std::runtime_error("Unsupported codec"); - state.codecContext = avcodec_alloc_context3(state.codec); - if(state.codecContext == nullptr) + 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(state.codecContext, state.formatContext->streams[streamIndex]->codecpar); + int ret = avcodec_parameters_to_context(codecContext, formatContext->streams[streamIndex]->codecpar); if(ret < 0) { //We cannot get codec from parameters - avcodec_free_context(&state.codecContext); + avcodec_free_context(&codecContext); throwFFmpegError(ret); } // Open codec - ret = avcodec_open2(state.codecContext, state.codec, nullptr); + ret = avcodec_open2(codecContext, codec, nullptr); if(ret < 0) { // Could not open codec - state.codec = nullptr; + codec = nullptr; throwFFmpegError(ret); } + + // Allocate video frame + frame = av_frame_alloc(); +} + +const AVCodecParameters * FFMpegStream::getCodecParameters() +{ + return formatContext->streams[streamIndex]->codecpar; +} + +const AVCodecContext * FFMpegStream::getCodecContext() +{ + return codecContext; +} + +const AVFrame * FFMpegStream::getCurrentFrame() +{ + return frame; } void CVideoInstance::openVideo() { - openContext(video); - - for(int i = 0; i < video.formatContext->nb_streams; i++) - { - if(video.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) - { - openCodec(video, i); - return; - } - } + openContext(); + openCodec(findVideoStream()); } void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput) { - if (video.streamIndex == -1) - throw std::runtime_error("Invalid file state! No video stream!"); - - // Allocate video frame - output.frame = av_frame_alloc(); - //setup scaling if(scaleToScreenSize) { - output.dimensions.x = screen->w; - output.dimensions.y = screen->h; + dimensions.x = screen->w; + dimensions.y = screen->h; } else { - output.dimensions.x = video.codecContext->width; - output.dimensions.y = video.codecContext->height; + dimensions.x = getCodecContext()->width; + dimensions.y = getCodecContext()->height; } // Allocate a place to put our YUV image on that screen @@ -186,171 +190,228 @@ void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput AV_PIX_FMT_NONE }; - auto preferredFormat = avcodec_find_best_pix_fmt_of_list(potentialFormats.data(), video.codecContext->pix_fmt, false, nullptr); + auto preferredFormat = avcodec_find_best_pix_fmt_of_list(potentialFormats.data(), getCodecContext()->pix_fmt, false, nullptr); if (preferredFormat == AV_PIX_FMT_YUV420P) - output.textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y); + textureYUV = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); else - output.textureRGB = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, output.dimensions.x, output.dimensions.y); - output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt, - output.dimensions.x, output.dimensions.y, preferredFormat, + 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 { - output.surface = CSDL_Ext::newSurface(output.dimensions.x, output.dimensions.y); - output.sws = sws_getContext(video.codecContext->width, video.codecContext->height, video.codecContext->pix_fmt, - output.dimensions.x, output.dimensions.y, AV_PIX_FMT_RGB32, + 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 (output.sws == nullptr) + if (sws == nullptr) throw std::runtime_error("Failed to create sws"); } -bool CVideoInstance::nextFrame() +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(video.formatContext, &packet); + int ret = av_read_frame(formatContext, &packet); if(ret < 0) { if(ret == AVERROR_EOF) - return false; + { + 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 == video.streamIndex) + if(packet.stream_index == streamIndex) { // Decode video frame - int rc = avcodec_send_packet(video.codecContext, &packet); + 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); - rc = avcodec_receive_frame(video.codecContext, output.frame); - if(rc < 0) - throwFFmpegError(rc); - - uint8_t * data[4] = {}; - int linesize[4] = {}; - - if(output.textureYUV) - { - av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_YUV420P, 1); - sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); - SDL_UpdateYUVTexture(output.textureYUV, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); - av_freep(&data[0]); - } - if(output.textureRGB) - { - av_image_alloc(data, linesize, output.dimensions.x, output.dimensions.y, AV_PIX_FMT_RGB32, 1); - sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); - SDL_UpdateTexture(output.textureRGB, nullptr, data[0], linesize[0]); - av_freep(&data[0]); - } - if (output.surface) - { - // Avoid buffer overflow caused by sws_scale(): - // http://trac.ffmpeg.org/ticket/9254 - - size_t pic_bytes = output.surface->pitch * output.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] = output.surface->pitch; - - sws_scale(output.sws, output.frame->data, output.frame->linesize, 0, video.codecContext->height, data, linesize); - memcpy(output.surface->pixels, for_sws, pic_bytes); - av_free(for_sws); - } av_packet_unref(&packet); - return true; + 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 output.videoEnded; -} - -void CVideoInstance::close() -{ - sws_freeContext(output.sws); - av_frame_free(&output.frame); - - SDL_DestroyTexture(output.textureYUV); - SDL_DestroyTexture(output.textureRGB); - SDL_FreeSurface(output.surface); - - closeState(video); -} - -void CVideoInstance::closeState(FFMpegStreamState & streamState) -{ - // state.videoStream.codec??? - // state.audioStream.codec??? - - avcodec_close(video.codecContext); - avcodec_free_context(&video.codecContext); - - avcodec_close(video.codecContext); - avcodec_free_context(&video.codecContext); - - avformat_close_input(&video.formatContext); - av_free(video.context); - - output = FFMpegVideoOutput(); - video = FFMpegStreamState(); + return getCurrentFrame() == nullptr; } CVideoInstance::~CVideoInstance() { - close(); + sws_freeContext(sws); + SDL_DestroyTexture(textureYUV); + SDL_DestroyTexture(textureRGB); + SDL_FreeSurface(surface); +} + +FFMpegStream::~FFMpegStream() +{ + // state.videoStream.codec??? + // state.audioStream.codec??? + 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(!output.frame) + if(!getCurrentFrame()) throw std::runtime_error("Invalid video frame!"); - return Point(output.frame->width, output.frame->height); + return Point(getCurrentFrame()->width, getCurrentFrame()->height); } void CVideoInstance::show(const Point & position, Canvas & canvas) { - if(output.sws == nullptr) + if(sws == nullptr) throw std::runtime_error("No video to show!"); - CSDL_Ext::blitSurface(output.surface, canvas.getInternalSurface(), position); + CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position); +} + +double FFMpegStream::getCurrentFrameEndTime() +{ +# 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() +{ +# 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(output.sws == nullptr) + if(sws == nullptr) throw std::runtime_error("No video to show!"); - if(output.videoEnded) + if(videoEnded()) throw std::runtime_error("Video already ended!"); -# if(LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = output.frame->pkt_duration; -# else - auto packet_duration = frame->duration; -# endif - double frameEndTime = (output.frame->pts + packet_duration) * av_q2d(video.formatContext->streams[video.streamIndex]->time_base); - output.frameTime += msPassed / 1000.0; + frameTime += msPassed / 1000.0; - if(output.frameTime >= frameEndTime) - { - if(!nextFrame()) - output.videoEnded = true; - } + if(frameTime >= getCurrentFrameEndTime()) + loadNextFrame(); } +static bool sampleIsPlanar(int audioFormat) +{ + switch (audioFormat) + { + case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits + case AV_SAMPLE_FMT_S16: ///< signed 16 bits + case AV_SAMPLE_FMT_S32: ///< signed 32 bits + case AV_SAMPLE_FMT_S64: ///< signed 64 bits + case AV_SAMPLE_FMT_FLT: ///< float + case AV_SAMPLE_FMT_DBL: ///< double + return false; + case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar + case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar + case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar + case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar + case AV_SAMPLE_FMT_FLTP: ///< float, planar + case AV_SAMPLE_FMT_DBLP: ///< double, planar + return true; + } + throw std::runtime_error("Invalid audio format"); +} + + static int32_t sampleSizeBytes(int audioFormat) { switch (audioFormat) @@ -398,73 +459,61 @@ static int32_t sampleWavType(int audioFormat) throw std::runtime_error("Invalid audio format"); } -void CVideoInstance::playAudio() +int FFMpegStream::findAudioStream() { - FFMpegStreamState audio; + for(int i = 0; i < formatContext->nb_streams; i++) + if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + return i; - openContext(audio); + return -1; +} - for(int i = 0; i < audio.formatContext->nb_streams; i++) - { - if(audio.formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - { - openCodec(audio, i); - break; - } - } +int FFMpegStream::findVideoStream() +{ + for(int i = 0; i < formatContext->nb_streams; i++) + if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + return i; - std::pair, si64> dat(std::make_pair(nullptr, 0)); + return -1; +} - if (audio.streamIndex < 0) - return; // nothing to play +std::pair, si64> CAudioInstance::extractAudio(const VideoPath & videoToOpen) +{ + openInput(videoToOpen); + openContext(); + openCodec(findAudioStream()); - const auto * codecpar = audio.formatContext->streams[audio.streamIndex]->codecpar; - AVFrame *frameAudio = av_frame_alloc(); - AVFrame *frameVideo = av_frame_alloc(); - AVPacket packet; + const auto * codecpar = getCodecParameters(); std::vector samples; int32_t sampleSize = sampleSizeBytes(codecpar->format); + int numChannels = codecpar->channels; + + // 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 load only one channel and play this sound as mono-channel audio + // correct approach would be to either use resampler or to do this conversion manually + // alternative approaches: + // - use SDL resampler, however planar formats are not suppported by it either + // - generate two audio streams and play them separately + // Good news is that it looks like none of H3 video files use this format (only in not supported HD Edition) + if (sampleIsPlanar(codecpar->format)) + numChannels = 1; samples.reserve(44100 * 5); // arbitrary 5-second buffer - while (av_read_frame(audio.formatContext, &packet) >= 0) + for (;;) { - if (packet.stream_index == video.streamIndex) - { - // Decode video frame - int rc = avcodec_send_packet(video.codecContext, &packet); - if(rc < 0) - throwFFmpegError(rc); + decodeNextFrame(); + const AVFrame * frame = getCurrentFrame(); - rc = avcodec_receive_frame(video.codecContext, frameVideo); - if(rc < 0) - throwFFmpegError(rc); - } + if (!frame) + break; - if(packet.stream_index == audio.streamIndex) - { - int rc = avcodec_send_packet(audio.codecContext, &packet); - - if(rc < 0) - throwFFmpegError(rc); - - for (;;) - { - rc = avcodec_receive_frame(audio.codecContext, frameAudio); - if (rc == AVERROR(EAGAIN)) - break; - - if(rc < 0) - throwFFmpegError(rc); - - int bytesToRead = frameAudio->nb_samples * 2 * sampleSize; - - samples.insert(samples.end(), frameAudio->data[0], frameAudio->data[0] + bytesToRead); - } - } - av_packet_unref(&packet); + int bytesToRead = frame->nb_samples * numChannels * sampleSize; + samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead); } typedef struct WAV_HEADER { @@ -486,75 +535,73 @@ void CVideoInstance::playAudio() wav_hdr wav; wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; wav.AudioFormat = sampleWavType(codecpar->format); - wav.NumOfChan = codecpar->channels; + wav.NumOfChan = numChannels; wav.SamplesPerSec = codecpar->sample_rate; wav.bytesPerSec = codecpar->sample_rate * sampleSize; wav.bitsPerSample = sampleSize * 8; wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; auto wavPtr = reinterpret_cast(&wav); - dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); + auto dat = std::make_pair(std::make_unique(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); - - CCS->soundh->playSound(dat); - closeState(audio); + return dat; + //CCS->soundh->playSound(dat); } bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey) { CVideoInstance instance; + CAudioInstance audio; - instance.open(name); - instance.playAudio(); + auto extractedAudio = audio.extractAudio(name); + int audioHandle = CCS->soundh->playSound(extractedAudio); + + instance.openInput(name); instance.openVideo(); instance.prepareOutput(scale, useOverlay); auto lastTimePoint = boost::chrono::steady_clock::now(); - while(instance.nextFrame()) + 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.output.dimensions.x; - rect.h = instance.output.dimensions.y; + rect.w = instance.dimensions.x; + rect.h = instance.dimensions.y; if(useOverlay) SDL_RenderFillRect(mainRenderer, &rect); else SDL_RenderClear(mainRenderer); - if (instance.output.textureYUV) - SDL_RenderCopy(mainRenderer, instance.output.textureYUV, nullptr, &rect); + if (instance.textureYUV) + SDL_RenderCopy(mainRenderer, instance.textureYUV, nullptr, &rect); else - SDL_RenderCopy(mainRenderer, instance.output.textureRGB, nullptr, &rect); + SDL_RenderCopy(mainRenderer, instance.textureRGB, nullptr, &rect); SDL_RenderPresent(mainRenderer); -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = instance.output.frame->pkt_duration; -#else - auto packet_duration = output.frame->duration; -#endif - // Framerate delay - double targetFrameTimeSeconds = packet_duration * av_q2d(instance.video.formatContext->streams[instance.video.streamIndex]->time_base); + double targetFrameTimeSeconds = instance.getCurrentFrameDuration(); auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); auto timePointAfterPresent = boost::chrono::steady_clock::now(); auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); + logGlobal->info("Sleeping for %d", (targetFrameTime - timeSpentBusy).count()); if (targetFrameTime > timeSpentBusy) boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); @@ -577,11 +624,17 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool { auto result = std::make_unique(); - result->open(name); + result->openInput(name); result->openVideo(); result->prepareOutput(scaleToScreen, false); return result; } +std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) +{ + CAudioInstance audio; + return audio.extractAudio(videoToOpen); +} + #endif diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index bfb8e865b..58f2369a3 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -19,6 +19,7 @@ struct SDL_Surface; struct SDL_Texture; struct AVFormatContext; struct AVCodecContext; +struct AVCodecParameters; struct AVCodec; struct AVFrame; struct AVIOContext; @@ -27,19 +28,50 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; VCMI_LIB_NAMESPACE_END -struct FFMpegStreamState +class FFMpegStream { + std::unique_ptr 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(); + int findAudioStream(); + + const AVCodecParameters * getCodecParameters(); + const AVCodecContext * getCodecContext(); + void decodeNextFrame(); + const AVFrame * getCurrentFrame(); + double getCurrentFrameEndTime(); + double getCurrentFrameDuration(); + +public: + virtual ~FFMpegStream(); + + void openInput(const VideoPath & fname); }; -struct FFMpegVideoOutput +class CAudioInstance final : public FFMpegStream { - AVFrame * frame = nullptr; +public: + std::pair, 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; @@ -48,37 +80,20 @@ struct FFMpegVideoOutput /// video playback current progress, in seconds double frameTime = 0.0; - bool videoEnded = false; -}; -class CVideoInstance final : public IVideoInstance -{ - friend class CVideoPlayer; - - std::unique_ptr input; - - FFMpegStreamState video; - FFMpegVideoOutput output; - - void open(const VideoPath & fname); - void openContext(FFMpegStreamState & streamState); - void openCodec(FFMpegStreamState & streamState, int streamIndex); - void openVideo(); void prepareOutput(bool scaleToScreenSize, bool useTextureOutput); - bool nextFrame(); - void close(); - void closeState(FFMpegStreamState & streamState); - 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; - void playAudio() final; }; class CVideoPlayer final : public IVideoPlayer @@ -90,6 +105,7 @@ public: bool playIntroVideo(const VideoPath & name) final; void playSpellbookAnimation(const VideoPath & name, const Point & position) final; std::unique_ptr open(const VideoPath & name, bool scaleToScreen) final; + std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; #endif diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index a0c1e6dc7..70c7db4a2 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -32,9 +32,6 @@ public: /// Advances video playback by specified duration virtual void tick(uint32_t msPassed) = 0; - /// Attempts to start audio playback from video, if any exists - virtual void playAudio() = 0; - virtual ~IVideoInstance() = default; }; @@ -50,5 +47,8 @@ public: /// Load video from specified path. Returns nullptr on failure virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) = 0; + /// Extracts audio data from provided video in wav format + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; + virtual ~IVideoPlayer() = default; }; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index 76627be5e..cdf8f0457 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -44,8 +44,8 @@ void VideoWidget::show(Canvas & to) void VideoWidget::activate() { - if(videoInstance) - videoInstance->playAudio(); +// if(videoInstance) +// videoInstance->playAudio(); if(videoSoundHandle != -1) { From 2ef3e5e2f14444019434214a40deaede0ef6c9ee Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 5 May 2024 11:56:55 +0300 Subject: [PATCH 08/15] Removed duplicated switches --- client/media/CVideoHandler.cpp | 95 ++++++++++------------------------ 1 file changed, 26 insertions(+), 69 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index c028dd99b..4c804720b 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -389,72 +389,29 @@ void CVideoInstance::tick(uint32_t msPassed) loadNextFrame(); } -static bool sampleIsPlanar(int audioFormat) +struct FFMpegFormatDescription +{ + uint8_t sampleSizeBytes; + uint8_t wavFormatID; + bool isPlanar; +}; + +static FFMpegFormatDescription getAudioFormatProperties(int audioFormat) { switch (audioFormat) { - case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits - case AV_SAMPLE_FMT_S16: ///< signed 16 bits - case AV_SAMPLE_FMT_S32: ///< signed 32 bits - case AV_SAMPLE_FMT_S64: ///< signed 64 bits - case AV_SAMPLE_FMT_FLT: ///< float - case AV_SAMPLE_FMT_DBL: ///< double - return false; - case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar - case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar - case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar - case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar - case AV_SAMPLE_FMT_FLTP: ///< float, planar - case AV_SAMPLE_FMT_DBLP: ///< double, planar - return true; - } - throw std::runtime_error("Invalid audio format"); -} - - -static int32_t sampleSizeBytes(int audioFormat) -{ - switch (audioFormat) - { - case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits - case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar - return 1; - case AV_SAMPLE_FMT_S16: ///< signed 16 bits - case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar - return 2; - case AV_SAMPLE_FMT_S32: ///< signed 32 bits - case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar - case AV_SAMPLE_FMT_FLT: ///< float - case AV_SAMPLE_FMT_FLTP: ///< float, planar - return 4; - case AV_SAMPLE_FMT_DBL: ///< double - case AV_SAMPLE_FMT_DBLP: ///< double, planar - case AV_SAMPLE_FMT_S64: ///< signed 64 bits - case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar - return 8; - } - throw std::runtime_error("Invalid audio format"); -} - -static int32_t sampleWavType(int audioFormat) -{ - switch (audioFormat) - { - case AV_SAMPLE_FMT_U8: ///< unsigned 8 bits - case AV_SAMPLE_FMT_U8P: ///< unsigned 8 bits, planar - case AV_SAMPLE_FMT_S16: ///< signed 16 bits - case AV_SAMPLE_FMT_S16P: ///< signed 16 bits, planar - case AV_SAMPLE_FMT_S32: ///< signed 32 bits - case AV_SAMPLE_FMT_S32P: ///< signed 32 bits, planar - case AV_SAMPLE_FMT_S64: ///< signed 64 bits - case AV_SAMPLE_FMT_S64P: ///< signed 64 bits, planar - return 1; // PCM - - case AV_SAMPLE_FMT_FLT: ///< float - case AV_SAMPLE_FMT_FLTP: ///< float, planar - case AV_SAMPLE_FMT_DBL: ///< double - case AV_SAMPLE_FMT_DBLP: ///< double, planar - return 3; // IEEE float + 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"); } @@ -487,7 +444,7 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide std::vector samples; - int32_t sampleSize = sampleSizeBytes(codecpar->format); + auto formatProperties = getAudioFormatProperties(codecpar->format); int numChannels = codecpar->channels; // Workaround for lack of resampler @@ -499,7 +456,7 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide // - use SDL resampler, however planar formats are not suppported by it either // - generate two audio streams and play them separately // Good news is that it looks like none of H3 video files use this format (only in not supported HD Edition) - if (sampleIsPlanar(codecpar->format)) + if (formatProperties.isPlanar) numChannels = 1; samples.reserve(44100 * 5); // arbitrary 5-second buffer @@ -512,7 +469,7 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide if (!frame) break; - int bytesToRead = frame->nb_samples * numChannels * sampleSize; + int bytesToRead = frame->nb_samples * numChannels * formatProperties.sampleSizeBytes; samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead); } @@ -534,11 +491,11 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide wav_hdr wav; wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; - wav.AudioFormat = sampleWavType(codecpar->format); + wav.AudioFormat = formatProperties.wavFormatID; // 1 = PCM, 3 = IEEE float wav.NumOfChan = numChannels; wav.SamplesPerSec = codecpar->sample_rate; - wav.bytesPerSec = codecpar->sample_rate * sampleSize; - wav.bitsPerSample = sampleSize * 8; + wav.bytesPerSec = codecpar->sample_rate * formatProperties.sampleSizeBytes; + wav.bitsPerSample = formatProperties.sampleSizeBytes * 8; wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; auto wavPtr = reinterpret_cast(&wav); From e39461165acd9922ee9cfa98ea224f425f621d57 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 15 May 2024 16:15:16 +0000 Subject: [PATCH 09/15] Added better workaround for planar audio format from ffmpeg --- client/media/CVideoHandler.cpp | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 4c804720b..78ae1f1af 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -445,19 +445,11 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide std::vector samples; auto formatProperties = getAudioFormatProperties(codecpar->format); +#if(LIBAVUTIL_VERSION_MAJOR < 58) int numChannels = codecpar->channels; - - // 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 load only one channel and play this sound as mono-channel audio - // correct approach would be to either use resampler or to do this conversion manually - // alternative approaches: - // - use SDL resampler, however planar formats are not suppported by it either - // - generate two audio streams and play them separately - // Good news is that it looks like none of H3 video files use this format (only in not supported HD Edition) - if (formatProperties.isPlanar) - numChannels = 1; +#else + int numChannels = codecpar->ch_layout.nb_channels; +#endif samples.reserve(44100 * 5); // arbitrary 5-second buffer @@ -469,8 +461,24 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide if (!frame) break; - int bytesToRead = frame->nb_samples * numChannels * formatProperties.sampleSizeBytes; - samples.insert(samples.end(), frame->data[0], frame->data[0] + bytesToRead); + 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) steram + 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); + } } typedef struct WAV_HEADER { From 02da800151db87c64df60997d7de8b6838f1010b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 15 May 2024 16:15:25 +0000 Subject: [PATCH 10/15] Formatting --- client/media/CVideoHandler.cpp | 30 +++++++++++++++--------------- client/media/CVideoHandler.h | 6 ++---- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 78ae1f1af..7863658f4 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -216,10 +216,10 @@ void FFMpegStream::decodeNextFrame() { AVPacket packet; - for (;;) + for(;;) { int rc = avcodec_receive_frame(codecContext, frame); - if (rc == AVERROR(EAGAIN)) + if(rc == AVERROR(EAGAIN)) break; if(rc < 0) @@ -252,7 +252,7 @@ void FFMpegStream::decodeNextFrame() throwFFmpegError(rc); rc = avcodec_receive_frame(codecContext, frame); - if (rc == AVERROR(EAGAIN)) + if(rc == AVERROR(EAGAIN)) { av_packet_unref(&packet); continue; @@ -272,7 +272,7 @@ bool CVideoInstance::loadNextFrame() decodeNextFrame(); const AVFrame * frame = getCurrentFrame(); - if (!frame) + if(!frame) return false; uint8_t * data[4] = {}; @@ -292,7 +292,7 @@ bool CVideoInstance::loadNextFrame() SDL_UpdateTexture(textureRGB, nullptr, data[0], linesize[0]); av_freep(&data[0]); } - if (surface) + if(surface) { // Avoid buffer overflow caused by sws_scale(): // http://trac.ffmpeg.org/ticket/9254 @@ -357,21 +357,21 @@ void CVideoInstance::show(const Point & position, Canvas & canvas) double FFMpegStream::getCurrentFrameEndTime() { -# if(LIBAVUTIL_VERSION_MAJOR < 58) +#if(LIBAVUTIL_VERSION_MAJOR < 58) auto packet_duration = frame->pkt_duration; -# else +#else auto packet_duration = frame->duration; -# endif +#endif return (frame->pts + packet_duration) * av_q2d(formatContext->streams[streamIndex]->time_base); } double FFMpegStream::getCurrentFrameDuration() { -# if(LIBAVUTIL_VERSION_MAJOR < 58) +#if(LIBAVUTIL_VERSION_MAJOR < 58) auto packet_duration = frame->pkt_duration; -# else +#else auto packet_duration = frame->duration; -# endif +#endif return (packet_duration) * av_q2d(formatContext->streams[streamIndex]->time_base); } @@ -552,7 +552,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po else SDL_RenderClear(mainRenderer); - if (instance.textureYUV) + if(instance.textureYUV) SDL_RenderCopy(mainRenderer, instance.textureYUV, nullptr, &rect); else SDL_RenderCopy(mainRenderer, instance.textureRGB, nullptr, &rect); @@ -567,7 +567,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); logGlobal->info("Sleeping for %d", (targetFrameTime - timeSpentBusy).count()); - if (targetFrameTime > timeSpentBusy) + if(targetFrameTime > timeSpentBusy) boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); lastTimePoint = boost::chrono::steady_clock::now(); @@ -577,7 +577,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po bool CVideoPlayer::playIntroVideo(const VideoPath & name) { - return openAndPlayVideoImpl(name, Point(0,0), true, true, true); + return openAndPlayVideoImpl(name, Point(0, 0), true, true, true); } void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position) @@ -596,7 +596,7 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool return result; } -std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) +std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) { CAudioInstance audio; return audio.extractAudio(videoToOpen); diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 58f2369a3..dd55ca6ad 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -12,7 +12,6 @@ #ifndef DISABLE_VIDEO # include "IVideoPlayer.h" - # include "../lib/Rect.h" struct SDL_Surface; @@ -64,14 +63,13 @@ public: class CAudioInstance final : public FFMpegStream { public: - std::pair, si64> extractAudio(const VideoPath & videoToOpen); + std::pair, 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; @@ -105,7 +103,7 @@ public: bool playIntroVideo(const VideoPath & name) final; void playSpellbookAnimation(const VideoPath & name, const Point & position) final; std::unique_ptr open(const VideoPath & name, bool scaleToScreen) final; - std::pair, si64> getAudio(const VideoPath & videoToOpen) final; + std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; #endif From 540bd16e7b8015d8bda039a409d81245dd7a6cee Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 15 May 2024 16:34:23 +0000 Subject: [PATCH 11/15] Fixed video widget playback --- client/battle/BattleInterfaceClasses.cpp | 2 +- client/mainmenu/CCampaignScreen.cpp | 2 +- client/mainmenu/CHighScoreScreen.cpp | 10 +- client/mainmenu/CHighScoreScreen.h | 5 +- client/mainmenu/CMainMenu.cpp | 8 +- client/mainmenu/CMainMenu.h | 1 + client/mainmenu/CPrologEpilogVideo.cpp | 2 +- client/media/CVideoHandler.cpp | 3 +- client/widgets/VideoWidget.cpp | 151 ++++++++++++++++------- client/widgets/VideoWidget.h | 42 +++++-- client/windows/CTutorialWindow.cpp | 2 +- client/windows/GUIClasses.cpp | 6 +- 12 files changed, 171 insertions(+), 63 deletions(-) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index d305320cf..b5416e7bf 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -709,7 +709,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface auto resources = getResources(br); description = std::make_shared(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - videoPlayer = std::make_shared(Point(107, 70), resources.prologueVideo, resources.loopedVideo); + videoPlayer = std::make_shared(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false); CCS->musich->playMusic(resources.musicName, false, true); } diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index dc161734f..16ae03a8a 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -145,7 +145,7 @@ void CCampaignScreen::CCampaignButton::hover(bool on) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; if (on && !videoPath.empty()) - videoPlayer = std::make_shared(Point(), videoPath); + videoPlayer = std::make_shared(Point(), videoPath, false); else videoPlayer.reset(); diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 7a4a42c4f..25c846f5d 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -229,7 +229,8 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc if(won) { - videoPlayer = std::make_shared(Point(30, 120), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK")); + + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("HSLOOP.SMK"), true); int border = 100; int textareaW = ((pos.w - 2 * border) / 4); @@ -246,7 +247,7 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc } else { - videoPlayer = std::make_shared(Point(30, 120), VideoPath::builtin("HSANIM.SMK"), VideoPath::builtin("LOSEGAME.SMK")); + videoPlayer = std::make_shared(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();}); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); } } @@ -291,6 +292,11 @@ int CHighScoreInputScreen::addEntry(std::string text) { return pos; } +void CHighScoreInputScreen::show(Canvas & to) +{ + CWindowObject::showAll(to); +} + void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index a4f777422..852ddc26f 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -15,7 +15,7 @@ class CLabel; class CMultiLineLabel; class CAnimImage; class CTextInput; -class VideoWidget; +class VideoWidgetBase; class TransparentFilledRectangle; @@ -94,7 +94,7 @@ class CHighScoreInputScreen : public CWindowObject std::vector> texts; std::shared_ptr input; std::shared_ptr background; - std::shared_ptr videoPlayer; + std::shared_ptr videoPlayer; bool won; HighScoreCalculation calc; @@ -105,4 +105,5 @@ public: void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; + void show(Canvas & to) override; }; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 431c29bae..5a0314aa1 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -95,7 +95,7 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) if(!config["video"].isNull()) { Point videoPosition(config["video"]["x"].Integer(), config["video"]["y"].Integer()); - videoPlayer = std::make_shared(videoPosition, VideoPath::fromJson(config["video"]["name"])); + videoPlayer = std::make_shared(videoPosition, VideoPath::fromJson(config["video"]["name"]), false); } else tabs->setRedrawParent(true); @@ -110,6 +110,12 @@ std::shared_ptr CMenuScreen::createTab(size_t index) return std::make_shared(this, config["items"].Vector()[index]); } +void CMenuScreen::show(Canvas & to) +{ + // TODO: avoid excessive redraws + CIntObject::showAll(to); +} + void CMenuScreen::activate() { CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 31783120d..116db260d 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -59,6 +59,7 @@ public: CMenuScreen(const JsonNode & configNode); void activate() override; + void show(Canvas & to); void switchToTab(size_t index); void switchToTab(std::string name); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 61e5317e3..5c1f5ac88 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -29,7 +29,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f pos = center(Rect(0, 0, 800, 600)); updateShadow(); - videoPlayer = std::make_shared(Point(30, 120), spe.prologVideo); + videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, true); //some videos are 800x600 in size while some are 800x400 if (videoPlayer->pos.h == 400) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 7863658f4..64c216189 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -469,7 +469,7 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide // 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) steram + // 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) @@ -592,6 +592,7 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool result->openInput(name); result->openVideo(); result->prepareOutput(scaleToScreen, false); + result->loadNextFrame(); // prepare 1st frame return result; } diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index cdf8f0457..2d68b82ca 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -12,72 +12,137 @@ #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" #include "../media/ISoundPlayer.h" #include "../media/IVideoPlayer.h" #include "../render/Canvas.h" -VideoWidget::VideoWidget(const Point & position, const VideoPath & looped) - : VideoWidget(position, VideoPath(), looped) +VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio) + : playAudio(playAudio) { addUsedEvents(TIME); + pos += position; + playVideo(video); } -VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped) - : current(prologue) - , next(looped) - , videoSoundHandle(-1) +VideoWidgetBase::~VideoWidgetBase() = default; + +void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) { - if(current.empty()) - videoInstance = CCS->videoh->open(looped, false); - else - videoInstance = CCS->videoh->open(current, false); -} - -VideoWidget::~VideoWidget() = default; - -void VideoWidget::show(Canvas & to) -{ - if(videoInstance) - videoInstance->show(pos.topLeft(), to); -} - -void VideoWidget::activate() -{ -// if(videoInstance) -// videoInstance->playAudio(); - - if(videoSoundHandle != -1) + videoInstance = CCS->videoh->open(fileToPlay, false); + if (videoInstance) { - CCS->soundh->setCallback( - videoSoundHandle, - [this]() - { - if(GH.windows().isTopWindow(this)) - this->videoSoundHandle = -1; - } - ); + pos.w = videoInstance->size().x; + pos.h = videoInstance->size().y; + } + + if (playAudio) + { + loadAudio(fileToPlay); + if (isActive()) + startAudio(); } } -void VideoWidget::deactivate() -{ - CCS->soundh->stopSound(videoSoundHandle); -} - -void VideoWidget::showAll(Canvas & to) +void VideoWidgetBase::show(Canvas & to) { if(videoInstance) videoInstance->show(pos.topLeft(), to); } -void VideoWidget::tick(uint32_t msPassed) +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 = CCS->videoh->open(next, false); + { + 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 & callback) + : VideoWidgetBase(position, video, playAudio) + , callback(callback) +{ +} + +void VideoWidgetOnce::onPlaybackFinished() +{ + callback(); +} diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h index 173087323..f7c963d51 100644 --- a/client/widgets/VideoWidget.h +++ b/client/widgets/VideoWidget.h @@ -15,23 +15,51 @@ class IVideoInstance; -class VideoWidget final : public CIntObject +class VideoWidgetBase : public CIntObject { std::unique_ptr videoInstance; - VideoPath current; - VideoPath next; + std::pair, si64> audioData = {nullptr, 0}; + int audioHandle = -1; + bool playAudio = false; - int videoSoundHandle; + 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: - VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped); - VideoWidget(const Point & position, const VideoPath & looped); - ~VideoWidget(); + ~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); +}; + +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 callback; + + void onPlaybackFinished() final; +public: + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function & callback); }; diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index fbcd16b7b..1655e9710 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -57,7 +57,7 @@ void CTutorialWindow::setContent() OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; auto video = VideoPath::builtin("tutorial/" + videos[page]); - videoPlayer = std::make_shared(Point(30, 120), video); + videoPlayer = std::make_shared(Point(30, 120), video, false); buttonLeft->block(page<1); buttonRight->block(page>videos.size() - 2); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index da292f7ed..48e768a84 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -515,11 +515,11 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->block(true); } if(LOCPLINT->castleInt) - videoPlayer = std::make_shared(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), LOCPLINT->castleInt->town->town->clientInfo.tavernVideo, false); else if(const auto * townObj = dynamic_cast(TavernObj)) - videoPlayer = std::make_shared(Point(70, 56), townObj->town->clientInfo.tavernVideo); + videoPlayer = std::make_shared(Point(70, 56), townObj->town->clientInfo.tavernVideo, false); else - videoPlayer = std::make_shared(Point(70, 56), VideoPath::builtin("TAVERN.BIK")); + videoPlayer = std::make_shared(Point(70, 56), VideoPath::builtin("TAVERN.BIK"), false); addInvite(); } From 31349f305200a36eb92cdcbbf70d2cc0aa31d629 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 May 2024 15:43:21 +0000 Subject: [PATCH 12/15] Fix handling of missing video files --- client/media/CVideoHandler.cpp | 20 +++++++++++++++----- client/media/CVideoHandler.h | 2 +- lib/filesystem/ResourcePath.cpp | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 64c216189..e82ad533e 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -79,12 +79,17 @@ static std::unique_ptr findVideoData(const VideoPath & videoToOpen if(CResourceHandler::get()->existsResource(lowQualityVideo)) return CResourceHandler::get()->load(lowQualityVideo); - return CResourceHandler::get()->load(lowQualityVideoWithDir); + if(CResourceHandler::get()->existsResource(lowQualityVideoWithDir)) + return CResourceHandler::get()->load(lowQualityVideoWithDir); + + return nullptr; } -void FFMpegStream::openInput(const VideoPath & videoToOpen) +bool FFMpegStream::openInput(const VideoPath & videoToOpen) { input = findVideoData(videoToOpen); + + return input != nullptr; } void FFMpegStream::openContext() @@ -436,7 +441,8 @@ int FFMpegStream::findVideoStream() std::pair, si64> CAudioInstance::extractAudio(const VideoPath & videoToOpen) { - openInput(videoToOpen); + if (!openInput(videoToOpen)) + return { nullptr, 0}; openContext(); openCodec(findAudioStream()); @@ -523,7 +529,9 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po auto extractedAudio = audio.extractAudio(name); int audioHandle = CCS->soundh->playSound(extractedAudio); - instance.openInput(name); + if (!instance.openInput(name)) + return true; + instance.openVideo(); instance.prepareOutput(scale, useOverlay); @@ -589,7 +597,9 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool { auto result = std::make_unique(); - result->openInput(name); + if (!result->openInput(name)) + return nullptr; + result->openVideo(); result->prepareOutput(scaleToScreen, false); result->loadNextFrame(); // prepare 1st frame diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index dd55ca6ad..26a58a1dc 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -57,7 +57,7 @@ protected: public: virtual ~FFMpegStream(); - void openInput(const VideoPath & fname); + bool openInput(const VideoPath & fname); }; class CAudioInstance final : public FFMpegStream diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index d6bc81a2d..4a94e34de 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -154,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) From dc0b90e75582628d1470213cf0cfa14e76594513 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 May 2024 16:01:08 +0000 Subject: [PATCH 13/15] Code style & formatting --- client/CMT.cpp | 10 ++- client/mainmenu/CMainMenu.h | 2 +- client/media/CAudioBase.cpp | 2 - client/media/CAudioBase.h | 3 +- client/media/CEmptyVideoPlayer.h | 10 +-- client/media/CMusicHandler.cpp | 119 ++++++++++++++-------------- client/media/CMusicHandler.h | 33 ++++---- client/media/CSoundHandler.cpp | 130 ++++++++++++++++--------------- client/media/CSoundHandler.h | 16 ++-- client/media/CVideoHandler.cpp | 47 +++++------ client/media/CVideoHandler.h | 21 ++--- client/media/IMusicPlayer.h | 2 +- client/media/IVideoPlayer.h | 2 +- 13 files changed, 201 insertions(+), 196 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index d3aeaed0a..adcf29da9 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -396,9 +396,13 @@ int main(int argc, char * argv[]) //plays intro, ends when intro is over or button has been pressed (handles events) void playIntro() { - if(CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK"))) - if (CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) - CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); + 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() diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 116db260d..442f92afb 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -59,7 +59,7 @@ public: CMenuScreen(const JsonNode & configNode); void activate() override; - void show(Canvas & to); + void show(Canvas & to) override; void switchToTab(size_t index); void switchToTab(std::string name); diff --git a/client/media/CAudioBase.cpp b/client/media/CAudioBase.cpp index aa487878e..24bbc1eff 100644 --- a/client/media/CAudioBase.cpp +++ b/client/media/CAudioBase.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "CAudioBase.h" -#include "../CGameInfo.h" - #include int CAudioBase::initializationCounter = 0; diff --git a/client/media/CAudioBase.h b/client/media/CAudioBase.h index 96f8f1fcd..94437db51 100644 --- a/client/media/CAudioBase.h +++ b/client/media/CAudioBase.h @@ -9,10 +9,11 @@ */ #pragma once -class CAudioBase +class CAudioBase : boost::noncopyable { static int initializationCounter; static bool initializeSuccess; + protected: bool isInitialized() const; diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index a9f825d93..6dc91abaf 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -15,24 +15,24 @@ class CEmptyVideoPlayer final : public IVideoPlayer { public: /// Plays video on top of the screen, returns only after playback is over - virtual bool playIntroVideo(const VideoPath & name) + bool playIntroVideo(const VideoPath & name) override { return false; }; - virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) + void playSpellbookAnimation(const VideoPath & name, const Point & position) override { } /// Load video from specified path - virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) + std::unique_ptr open(const VideoPath & name, bool scaleToScreen) override { return nullptr; }; /// Extracts audio data from provided video in wav format - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) + std::pair, si64> getAudio(const VideoPath & videoToOpen) override { - return { nullptr, 0}; + return {nullptr, 0}; }; }; diff --git a/client/media/CMusicHandler.cpp b/client/media/CMusicHandler.cpp index 67d64458b..b608bbab9 100644 --- a/client/media/CMusicHandler.cpp +++ b/client/media/CMusicHandler.cpp @@ -11,19 +11,19 @@ #include "CMusicHandler.h" #include "../CGameInfo.h" -#include "../renderSDL/SDLRWwrapper.h" #include "../eventsSDL/InputHandler.h" #include "../gui/CGuiHandler.h" +#include "../renderSDL/SDLRWwrapper.h" -#include "../../lib/filesystem/Filesystem.h" #include "../../lib/CRandomGenerator.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/filesystem/Filesystem.h" #include -void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) +void CMusicHandler::onVolumeChange(const JsonNode & volumeNode) { - setVolume((ui32)volumeNode.Float()); + setVolume(volumeNode.Integer()); } CMusicHandler::CMusicHandler(): @@ -58,12 +58,11 @@ CMusicHandler::CMusicHandler(): CCS->musich->musicFinishedCallback(); }); } - } void CMusicHandler::loadTerrainMusicThemes() { - for (const auto & terrain : CGI->terrainTypeHandler->objects) + for(const auto & terrain : CGI->terrainTypeHandler->objects) { addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); } @@ -76,7 +75,7 @@ void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & mus CMusicHandler::~CMusicHandler() { - if (isInitialized()) + if(isInitialized()) { boost::mutex::scoped_lock guard(mutex); @@ -92,7 +91,7 @@ void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromSt { boost::mutex::scoped_lock guard(mutex); - if (current && current->isPlaying() && current->isTrack(musicURI)) + if(current && current->isPlaying() && current->isTrack(musicURI)) return; queueNext(this, "", musicURI, loop, fromStart); @@ -108,13 +107,13 @@ void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bo boost::mutex::scoped_lock guard(mutex); auto selectedSet = musicsSet.find(whichSet); - if (selectedSet == musicsSet.end()) + if(selectedSet == musicsSet.end()) { logGlobal->error("Error: playing music from non-existing set: %s", whichSet); return; } - if (current && current->isPlaying() && current->isSet(whichSet)) + if(current && current->isPlaying() && current->isSet(whichSet)) return; // in this mode - play random track from set @@ -123,31 +122,31 @@ void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bo void CMusicHandler::queueNext(std::unique_ptr queued) { - if (!isInitialized()) + if(!isInitialized()) return; next = std::move(queued); - if (current.get() == nullptr || !current->stop(1000)) + 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) +void CMusicHandler::queueNext(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) { queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); } void CMusicHandler::stopMusic(int fade_ms) { - if (!isInitialized()) + if(!isInitialized()) return; boost::mutex::scoped_lock guard(mutex); - if (current.get() != nullptr) + if(current != nullptr) current->stop(fade_ms); next.reset(); } @@ -161,8 +160,8 @@ void CMusicHandler::setVolume(ui32 percent) { volume = std::min(100u, percent); - if (isInitialized()) - Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100); + if(isInitialized()) + Mix_VolumeMusic((MIX_MAX_VOLUME * volume) / 100); } void CMusicHandler::musicFinishedCallback() @@ -175,49 +174,53 @@ void CMusicHandler::musicFinishedCallback() // 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) + GH.dispatchMainThread( + [this]() { - // if music is looped, play it again - if (current->play()) - return; - else - current.reset(); - } + boost::unique_lock lockGuard(mutex); + if(current != 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(); + 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), - playing(false), - startTime(uint32_t(-1)), - startPosition(0), - loop(looped ? -1 : 1), - fromStart(fromStart), - setName(std::move(setName)) +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(-1)) + , startPosition(0) + , loop(looped ? -1 : 1) + , fromStart(fromStart) + , playing(false) + { - if (!musicURI.empty()) - load(std::move(musicURI)); + if(!musicURI.empty()) + load(musicURI); } + MusicEntry::~MusicEntry() { - if (playing && loop > 0) + if(playing && loop > 0) { assert(0); logGlobal->error("Attempt to delete music while playing!"); Mix_HaltMusic(); } - if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) + if(loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) { assert(0); logGlobal->error("Attempt to delete music while fading out!"); @@ -225,20 +228,20 @@ MusicEntry::~MusicEntry() } logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - if (music) + if(music) Mix_FreeMusic(music); } void MusicEntry::load(const AudioPath & musicURI) { - if (music) + if(music) { logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); Mix_FreeMusic(music); music = nullptr; } - if (CResourceHandler::get()->existsResource(musicURI)) + if(CResourceHandler::get()->existsResource(musicURI)) currentName = musicURI; else currentName = musicURI.addPrefix("MUSIC/"); @@ -249,10 +252,10 @@ void MusicEntry::load(const AudioPath & musicURI) try { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); + auto * musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); } - catch(std::exception &e) + catch(std::exception & e) { logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); logGlobal->error("Exception: %s", e.what()); @@ -267,10 +270,10 @@ void MusicEntry::load(const AudioPath & musicURI) bool MusicEntry::play() { - if (!(loop--) && music) //already played once - return + if(!(loop--) && music) //already played once - return return false; - if (!setName.empty()) + if(!setName.empty()) { const auto & set = owner->musicsSet[setName]; const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); @@ -279,7 +282,7 @@ bool MusicEntry::play() logGlobal->trace("Playing music file %s", currentName.getOriginalName()); - if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) + if(!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) { float timeToStart = owner->trackPositions[currentName]; startPosition = std::round(timeToStart * 1000); @@ -289,7 +292,7 @@ bool MusicEntry::play() // 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) + if(Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) { logGlobal->error("Unable to play music (%s)", Mix_GetError()); return false; @@ -307,14 +310,14 @@ bool MusicEntry::play() } startTime = GH.input().getTicks(); - + playing = true; return true; } bool MusicEntry::stop(int fade_ms) { - if (Mix_PlayingMusic()) + if(Mix_PlayingMusic()) { playing = false; loop = 0; @@ -330,12 +333,12 @@ bool MusicEntry::stop(int fade_ms) return false; } -bool MusicEntry::isPlaying() +bool MusicEntry::isPlaying() const { return playing; } -bool MusicEntry::isSet(std::string set) +bool MusicEntry::isSet(const std::string & set) { return !setName.empty() && set == setName; } diff --git a/client/media/CMusicHandler.h b/client/media/CMusicHandler.h index fda2e814e..b85463d4a 100644 --- a/client/media/CMusicHandler.h +++ b/client/media/CMusicHandler.h @@ -20,40 +20,41 @@ using Mix_Music = struct _Mix_Music; class CMusicHandler; //Class for handling one music file -class MusicEntry +class MusicEntry : boost::noncopyable { - CMusicHandler *owner; - Mix_Music *music; + 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; + 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(CMusicHandler * owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); ~MusicEntry(); - bool isSet(std::string setName); + bool isSet(const std::string & setName); bool isTrack(const AudioPath & trackName); - bool isPlaying(); + bool isPlaying() const; bool play(); - bool stop(int fade_ms=0); + bool stop(int fade_ms = 0); }; -class CMusicHandler final: public CAudioBase, public IMusicPlayer +class CMusicHandler final : public CAudioBase, public IMusicPlayer { private: //update volume on configuration change SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); + void onVolumeChange(const JsonNode & volumeNode); std::unique_ptr current; std::unique_ptr next; @@ -61,7 +62,7 @@ private: 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(CMusicHandler * owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); void queueNext(std::unique_ptr queued); void musicFinishedCallback() final; @@ -88,7 +89,7 @@ public: /// 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=1000) final; + void stopMusic(int fade_ms) final; friend class MusicEntry; }; diff --git a/client/media/CSoundHandler.cpp b/client/media/CSoundHandler.cpp index f13bfc094..532cdc617 100644 --- a/client/media/CSoundHandler.cpp +++ b/client/media/CSoundHandler.cpp @@ -22,7 +22,7 @@ #define VCMI_SOUND_FILE(y) #y, // sounds mapped to soundBase enum -static const std::string sounds[] = { +static const std::string soundsList[] = { "", // invalid "", // todo VCMI_SOUND_LIST @@ -30,10 +30,9 @@ static const std::string sounds[] = { #undef VCMI_SOUND_NAME #undef VCMI_SOUND_FILE - -void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) +void CSoundHandler::onVolumeChange(const JsonNode & volumeNode) { - setVolume((ui32)volumeNode.Float()); + setVolume(volumeNode.Integer()); } CSoundHandler::CSoundHandler(): @@ -43,9 +42,9 @@ CSoundHandler::CSoundHandler(): listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); if(ambientConfig["allocateChannels"].isNumber()) - Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer()); + Mix_AllocateChannels(ambientConfig["allocateChannels"].Integer()); - if (isInitialized()) + if(isInitialized()) { Mix_ChannelFinished([](int channel) { @@ -56,60 +55,60 @@ CSoundHandler::CSoundHandler(): CSoundHandler::~CSoundHandler() { - if (isInitialized()) + if(isInitialized()) { Mix_HaltChannel(-1); - for (auto &chunk : soundChunks) + for(auto & chunk : soundChunks) { - if (chunk.second.first) + 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) +Mix_Chunk * CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) { try { - if (cache && soundChunks.find(sound) != soundChunks.end()) + 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 + 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))}); + if(cache) + soundChunks.insert({sound, std::make_pair(chunk, std::move(data.first))}); return chunk; } - catch(std::exception &e) + catch(std::exception & e) { logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); return nullptr; } } -Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) +Mix_Chunk * CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) { try { - std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min((si64)100, data.second)); + std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min(static_cast(100), data.second)); - if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) + 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 + 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))}); + if(cache) + soundChunksRaw.insert({startBytes, std::make_pair(chunk, std::move(data.first))}); return chunk; } - catch(std::exception &e) + catch(std::exception & e) { logGlobal->warn("Cannot get sound chunk: %s", e.what()); return nullptr; @@ -123,8 +122,8 @@ int CSoundHandler::ambientDistToVolume(int distance) const if(distance >= distancesVector.size()) return 0; - int volume = static_cast(distancesVector[distance].Integer()); - return volume * (int)ambientConfig["volume"].Integer() / 100; + int volumeByDistance = static_cast(distancesVector[distance].Integer()); + return volumeByDistance * ambientConfig["volume"].Integer() / 100; } void CSoundHandler::ambientStopSound(const AudioPath & soundId) @@ -135,22 +134,22 @@ void CSoundHandler::ambientStopSound(const AudioPath & soundId) uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) { - if (!isInitialized() || sound.empty()) + if(!isInitialized() || sound.empty()) return 0; auto resourcePath = sound.addPrefix("SOUNDS/"); - if (!CResourceHandler::get()->existsResource(resourcePath)) + if(!CResourceHandler::get()->existsResource(resourcePath)) return 0; auto data = CResourceHandler::get()->load(resourcePath)->readAll(); SDL_AudioSpec spec; uint32_t audioLen; - uint8_t *audioBuf; + uint8_t * audioBuf; uint32_t miliseconds = 0; - if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr) + 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; @@ -159,14 +158,14 @@ uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) miliseconds = 1000 * sampleLen / spec.freq; } - return miliseconds ; + 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]); + 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); @@ -174,22 +173,22 @@ int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) { - if (!isInitialized() || sound.empty()) + if(!isInitialized() || sound.empty()) return -1; int channel; - Mix_Chunk *chunk = GetSoundChunk(sound, cache); + Mix_Chunk * chunk = GetSoundChunk(sound, cache); - if (chunk) + if(chunk) { channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) + if(channel == -1) { logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); - if (!cache) + if(!cache) Mix_FreeChunk(chunk); } - else if (cache) + else if(cache) initCallback(channel); else initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); @@ -200,19 +199,19 @@ int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) return channel; } -int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) +int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) { int channel = -1; - if (Mix_Chunk *chunk = GetSoundChunk(data, cache)) + if(Mix_Chunk * chunk = GetSoundChunk(data, cache)) { channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) + if(channel == -1) { logGlobal->error("Unable to play sound, error %s", Mix_GetError()); - if (!cache) + if(!cache) Mix_FreeChunk(chunk); } - else if (cache) + else if(cache) initCallback(channel); else initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); @@ -221,14 +220,14 @@ int CSoundHandler::playSound(std::pair, si64> & data, in } // Helper. Randomly select a sound from an array and play it -int CSoundHandler::playSoundFromSet(std::vector &sound_vec) +int CSoundHandler::playSoundFromSet(std::vector & sound_vec) { return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); } void CSoundHandler::stopSound(int handler) { - if (isInitialized() && handler != -1) + if(isInitialized() && handler != -1) Mix_HaltChannel(handler); } @@ -242,18 +241,18 @@ void CSoundHandler::setVolume(ui32 percent) { volume = std::min(100u, percent); - if (isInitialized()) + if(isInitialized()) { setChannelVolume(-1, volume); - for (auto const & channel : channelVolumes) + for(const auto & channel : channelVolumes) updateChannelVolume(channel.first); } } void CSoundHandler::updateChannelVolume(int channel) { - if (channelVolumes.count(channel)) + if(channelVolumes.count(channel)) setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); else setChannelVolume(channel, getVolume()); @@ -262,7 +261,7 @@ void CSoundHandler::updateChannelVolume(int channel) // Sets the sound volume, from 0 (mute) to 100 void CSoundHandler::setChannelVolume(int channel, ui32 percent) { - Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100); + Mix_Volume(channel, (MIX_MAX_VOLUME * percent) / 100); } void CSoundHandler::setCallback(int channel, std::function function) @@ -289,7 +288,7 @@ void CSoundHandler::soundFinishedCallback(int channel) { boost::mutex::scoped_lock lockGuard(mutexCallbacks); - if (callbacks.count(channel) == 0) + if(callbacks.count(channel) == 0) return; // store callbacks from container locally - SDL might reuse this channel for another sound @@ -297,12 +296,15 @@ void CSoundHandler::soundFinishedCallback(int channel) auto callback = callbacks.at(channel); callbacks.erase(channel); - if (!callback.empty()) + if(!callback.empty()) { - GH.dispatchMainThread([callback](){ - for (auto entry : callback) - entry(); - }); + GH.dispatchMainThread( + [callback]() + { + for(const auto & entry : callback) + entry(); + } + ); } } @@ -322,7 +324,7 @@ void CSoundHandler::initCallback(int channel, const std::function & func int CSoundHandler::ambientGetRange() const { - return static_cast(ambientConfig["range"].Integer()); + return ambientConfig["range"].Integer(); } void CSoundHandler::ambientUpdateChannels(std::map soundsArg) @@ -330,7 +332,7 @@ void CSoundHandler::ambientUpdateChannels(std::map soundsArg) boost::mutex::scoped_lock guard(mutex); std::vector stoppedSounds; - for(auto & pair : ambientChannels) + for(const auto & pair : ambientChannels) { const auto & soundId = pair.first; const int channel = pair.second; @@ -342,18 +344,18 @@ void CSoundHandler::ambientUpdateChannels(std::map soundsArg) } else { - int volume = ambientDistToVolume(soundsArg[soundId]); - channelVolumes[channel] = volume; + int channelVolume = ambientDistToVolume(soundsArg[soundId]); + channelVolumes[channel] = channelVolume; updateChannelVolume(channel); } } - for(auto soundId : stoppedSounds) + for(const auto & soundId : stoppedSounds) { channelVolumes.erase(ambientChannels[soundId]); ambientChannels.erase(soundId); } - for(auto & pair : soundsArg) + for(const auto & pair : soundsArg) { const auto & soundId = pair.first; const int distance = pair.second; @@ -361,8 +363,8 @@ void CSoundHandler::ambientUpdateChannels(std::map soundsArg) if(!vstd::contains(ambientChannels, soundId)) { int channel = playSound(soundId, -1); - int volume = ambientDistToVolume(distance); - channelVolumes[channel] = volume; + int channelVolume = ambientDistToVolume(distance); + channelVolumes[channel] = channelVolume; updateChannelVolume(channel); ambientChannels[soundId] = channel; @@ -374,7 +376,7 @@ void CSoundHandler::ambientStopAllChannels() { boost::mutex::scoped_lock guard(mutex); - for(auto ch : ambientChannels) + for(const auto & ch : ambientChannels) { ambientStopSound(ch.first); } diff --git a/client/media/CSoundHandler.h b/client/media/CSoundHandler.h index a20ac5a22..5a10a5493 100644 --- a/client/media/CSoundHandler.h +++ b/client/media/CSoundHandler.h @@ -21,18 +21,18 @@ class CSoundHandler final : public CAudioBase, public ISoundPlayer private: //update volume on configuration change SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); + void onVolumeChange(const JsonNode & volumeNode); using CachedChunk = std::pair>; std::map soundChunks; std::map, CachedChunk> soundChunksRaw; - Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); - Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); + Mix_Chunk * GetSoundChunk(const AudioPath & sound, bool cache); + Mix_Chunk * GetSoundChunk(std::pair, si64> & data, bool cache); /// have entry for every currently active channel /// vector will be empty if callback was not set - std::map> > callbacks; + std::map>> callbacks; /// Protects access to callbacks member to avoid data races: /// SDL calls sound finished callbacks from audio thread @@ -62,10 +62,10 @@ public: // 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, si64> & data, int repeats=0, bool cache=false) final; - int playSoundFromSet(std::vector &sound_vec) 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, si64> & data, int repeats = 0, bool cache = false) final; + int playSoundFromSet(std::vector & sound_vec) final; void stopSound(int handler) final; void setCallback(int channel, std::function function) final; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index e82ad533e..0a9ad789e 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -16,10 +16,8 @@ #include "../CGameInfo.h" #include "../CMT.h" -#include "../CPlayerInterface.h" #include "../eventsSDL/InputHandler.h" #include "../gui/CGuiHandler.h" -#include "../gui/FramerateManager.h" #include "../render/Canvas.h" #include "../renderSDL/SDL_Extensions.h" @@ -39,11 +37,11 @@ extern "C" { static int lodRead(void * opaque, uint8_t * buf, int size) { auto * data = static_cast(opaque); - int bytes = static_cast(data->read(buf, size)); - if(bytes == 0) + auto bytesRead = data->read(buf, size); + if(bytesRead == 0) return AVERROR_EOF; - return bytes; + return bytesRead; } static si64 lodSeek(void * opaque, si64 pos, int whence) @@ -151,17 +149,17 @@ void FFMpegStream::openCodec(int desiredStreamIndex) frame = av_frame_alloc(); } -const AVCodecParameters * FFMpegStream::getCodecParameters() +const AVCodecParameters * FFMpegStream::getCodecParameters() const { return formatContext->streams[streamIndex]->codecpar; } -const AVCodecContext * FFMpegStream::getCodecContext() +const AVCodecContext * FFMpegStream::getCodecContext() const { return codecContext; } -const AVFrame * FFMpegStream::getCurrentFrame() +const AVFrame * FFMpegStream::getCurrentFrame() const { return frame; } @@ -330,8 +328,6 @@ CVideoInstance::~CVideoInstance() FFMpegStream::~FFMpegStream() { - // state.videoStream.codec??? - // state.audioStream.codec??? av_frame_free(&frame); avcodec_close(codecContext); @@ -360,7 +356,7 @@ void CVideoInstance::show(const Point & position, Canvas & canvas) CSDL_Ext::blitSurface(surface, canvas.getInternalSurface(), position); } -double FFMpegStream::getCurrentFrameEndTime() +double FFMpegStream::getCurrentFrameEndTime() const { #if(LIBAVUTIL_VERSION_MAJOR < 58) auto packet_duration = frame->pkt_duration; @@ -370,14 +366,14 @@ double FFMpegStream::getCurrentFrameEndTime() return (frame->pts + packet_duration) * av_q2d(formatContext->streams[streamIndex]->time_base); } -double FFMpegStream::getCurrentFrameDuration() +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); + return packet_duration * av_q2d(formatContext->streams[streamIndex]->time_base); } void CVideoInstance::tick(uint32_t msPassed) @@ -421,7 +417,7 @@ static FFMpegFormatDescription getAudioFormatProperties(int audioFormat) throw std::runtime_error("Invalid audio format"); } -int FFMpegStream::findAudioStream() +int FFMpegStream::findAudioStream() const { for(int i = 0; i < formatContext->nb_streams; i++) if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) @@ -430,7 +426,7 @@ int FFMpegStream::findAudioStream() return -1; } -int FFMpegStream::findVideoStream() +int FFMpegStream::findVideoStream() const { for(int i = 0; i < formatContext->nb_streams; i++) if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) @@ -487,7 +483,7 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide } } - typedef struct WAV_HEADER { + struct WavHeader { ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; ui32 ChunkSize; ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; @@ -501,24 +497,23 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide ui16 bitsPerSample = 32; ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; ui32 Subchunk2Size; - } wav_hdr; + }; - wav_hdr wav; - wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; + 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(wav_hdr) - 44; - auto wavPtr = reinterpret_cast(&wav); + wav.Subchunk2Size = samples.size() + sizeof(WavHeader) - 44; + auto * wavPtr = reinterpret_cast(&wav); - auto dat = std::make_pair(std::make_unique(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)); + auto dat = std::make_pair(std::make_unique(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; - //CCS->soundh->playSound(dat); } bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey) @@ -569,7 +564,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po // Framerate delay double targetFrameTimeSeconds = instance.getCurrentFrameDuration(); - auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); + auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * targetFrameTimeSeconds)); auto timePointAfterPresent = boost::chrono::steady_clock::now(); auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 26a58a1dc..3fc6256cd 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -11,8 +11,8 @@ #ifndef DISABLE_VIDEO -# include "IVideoPlayer.h" -# include "../lib/Rect.h" +#include "../lib/Point.h" +#include "IVideoPlayer.h" struct SDL_Surface; struct SDL_Texture; @@ -25,9 +25,10 @@ struct AVIOContext; VCMI_LIB_NAMESPACE_BEGIN class CInputStream; +class Point; VCMI_LIB_NAMESPACE_END -class FFMpegStream +class FFMpegStream : boost::noncopyable { std::unique_ptr input; @@ -44,15 +45,15 @@ protected: void openContext(); void openCodec(int streamIndex); - int findVideoStream(); - int findAudioStream(); + int findVideoStream() const; + int findAudioStream() const; - const AVCodecParameters * getCodecParameters(); - const AVCodecContext * getCodecContext(); + const AVCodecParameters * getCodecParameters() const; + const AVCodecContext * getCodecContext() const; void decodeNextFrame(); - const AVFrame * getCurrentFrame(); - double getCurrentFrameEndTime(); - double getCurrentFrameDuration(); + const AVFrame * getCurrentFrame() const; + double getCurrentFrameEndTime() const; + double getCurrentFrameDuration() const; public: virtual ~FFMpegStream(); diff --git a/client/media/IMusicPlayer.h b/client/media/IMusicPlayer.h index d40f7ee8d..1dde50ed7 100644 --- a/client/media/IMusicPlayer.h +++ b/client/media/IMusicPlayer.h @@ -29,5 +29,5 @@ public: /// 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; + virtual void stopMusic(int fade_ms = 1000) = 0; }; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index 70c7db4a2..a867e9b24 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -48,7 +48,7 @@ public: virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) = 0; /// Extracts audio data from provided video in wav format - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; virtual ~IVideoPlayer() = default; }; From 3ed7c1d066b14dcbc14e283b6b330a56b609e817 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 30 May 2024 18:02:19 +0000 Subject: [PATCH 14/15] Implement playback of two-part campaign videos --- client/mainmenu/CPrologEpilogVideo.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 5c1f5ac88..8d4a6d93e 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -14,9 +14,9 @@ #include "../CGameInfo.h" #include "../media/IMusicPlayer.h" #include "../media/ISoundPlayer.h" -#include "../gui/WindowHandler.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" @@ -29,7 +29,18 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f pos = center(Rect(0, 0, 800, 600)); updateShadow(); - videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, true); + //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 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(Point(0, 0), spe.prologVideo, pairedVideoFiles.at(spe.prologVideo), true); + else + videoPlayer = std::make_shared(Point(0, 0), spe.prologVideo, true); //some videos are 800x600 in size while some are 800x400 if (videoPlayer->pos.h == 400) From 37e196c56551fabb71bdf54e8a2556753bb343e9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 30 May 2024 18:02:50 +0000 Subject: [PATCH 15/15] Automatically select preferred audio stream when playing video --- client/media/CVideoHandler.cpp | 51 ++++++++++++++++++++++++++++++++-- lib/Languages.h | 43 +++++++++++++++------------- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 0a9ad789e..3775942ad 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -23,6 +23,8 @@ #include "../../lib/filesystem/CInputStream.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/Languages.h" #include @@ -419,11 +421,56 @@ static FFMpegFormatDescription getAudioFormatProperties(int audioFormat) int FFMpegStream::findAudioStream() const { + std::vector audioStreamIndices; + for(int i = 0; i < formatContext->nb_streams; i++) if(formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - return i; + audioStreamIndices.push_back(i); - return -1; + 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 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 diff --git a/lib/Languages.h b/lib/Languages.h index f762d6e20..1ef49e559 100644 --- a/lib/Languages.h +++ b/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 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 + { "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 } + { "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(ELanguages::COUNT), "Languages array is missing a value!");