1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-22 22:13:35 +02:00

Merge pull request #4785 from Laserlicht/subtitles

Subtitles for videos / sync
This commit is contained in:
Ivan Savenko 2024-11-06 21:56:43 +02:00 committed by GitHub
commit 11d9ee310e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 125 additions and 7 deletions

View File

@ -240,6 +240,18 @@ void CSoundHandler::stopSound(int handler)
Mix_HaltChannel(handler); Mix_HaltChannel(handler);
} }
void CSoundHandler::pauseSound(int handler)
{
if(isInitialized() && handler != -1)
Mix_Pause(handler);
}
void CSoundHandler::resumeSound(int handler)
{
if(isInitialized() && handler != -1)
Mix_Resume(handler);
}
ui32 CSoundHandler::getVolume() const ui32 CSoundHandler::getVolume() const
{ {
return volume; return volume;

View File

@ -67,6 +67,8 @@ public:
int playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) final; int playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) final;
int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) final; int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) final;
void stopSound(int handler) final; void stopSound(int handler) final;
void pauseSound(int handler) final;
void resumeSound(int handler) final;
void setCallback(int channel, std::function<void()> function) final; void setCallback(int channel, std::function<void()> function) final;
void resetCallback(int channel) final; void resetCallback(int channel) final;

View File

@ -316,6 +316,12 @@ bool CVideoInstance::loadNextFrame()
return true; return true;
} }
double CVideoInstance::timeStamp()
{
return getCurrentFrameEndTime();
}
bool CVideoInstance::videoEnded() bool CVideoInstance::videoEnded()
{ {
return getCurrentFrame() == nullptr; return getCurrentFrame() == nullptr;
@ -385,12 +391,38 @@ void CVideoInstance::tick(uint32_t msPassed)
if(videoEnded()) if(videoEnded())
throw std::runtime_error("Video already ended!"); throw std::runtime_error("Video already ended!");
frameTime += msPassed / 1000.0; if(startTime == std::chrono::steady_clock::time_point())
startTime = std::chrono::steady_clock::now();
if(frameTime >= getCurrentFrameEndTime()) auto nowTime = std::chrono::steady_clock::now();
double difference = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - startTime).count() / 1000.0;
int frameskipCounter = 0;
while(!videoEnded() && difference >= getCurrentFrameEndTime() + getCurrentFrameDuration() && frameskipCounter < MAX_FRAMESKIP) // Frameskip
{
decodeNextFrame();
frameskipCounter++;
}
if(!videoEnded() && difference >= getCurrentFrameEndTime())
loadNextFrame(); loadNextFrame();
} }
void CVideoInstance::activate()
{
if(deactivationStartTime != std::chrono::steady_clock::time_point())
{
auto pauseDuration = std::chrono::steady_clock::now() - deactivationStartTime;
startTime += pauseDuration;
deactivationStartTime = std::chrono::steady_clock::time_point();
}
}
void CVideoInstance::deactivate()
{
deactivationStartTime = std::chrono::steady_clock::now();
}
struct FFMpegFormatDescription struct FFMpegFormatDescription
{ {
uint8_t sampleSizeBytes; uint8_t sampleSizeBytes;

View File

@ -77,10 +77,13 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream
SDL_Surface * surface = nullptr; SDL_Surface * surface = nullptr;
Point dimensions; Point dimensions;
/// video playback current progress, in seconds /// video playback start time point
double frameTime = 0.0; std::chrono::steady_clock::time_point startTime;
std::chrono::steady_clock::time_point deactivationStartTime;
void prepareOutput(float scaleFactor, bool useTextureOutput); void prepareOutput(float scaleFactor, bool useTextureOutput);
const int MAX_FRAMESKIP = 5;
public: public:
~CVideoInstance(); ~CVideoInstance();
@ -88,11 +91,14 @@ public:
void openVideo(); void openVideo();
bool loadNextFrame(); bool loadNextFrame();
double timeStamp() final;
bool videoEnded() final; bool videoEnded() final;
Point size() final; Point size() final;
void show(const Point & position, Canvas & canvas) final; void show(const Point & position, Canvas & canvas) final;
void tick(uint32_t msPassed) final; void tick(uint32_t msPassed) final;
void activate() final;
void deactivate() final;
}; };
class CVideoPlayer final : public IVideoPlayer class CVideoPlayer final : public IVideoPlayer

View File

@ -22,6 +22,8 @@ public:
virtual int playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) = 0; virtual int playSound(std::pair<std::unique_ptr<ui8[]>, si64> & data, int repeats = 0, bool cache = false) = 0;
virtual int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) = 0; virtual int playSoundFromSet(std::vector<soundBase::soundID> & sound_vec) = 0;
virtual void stopSound(int handler) = 0; virtual void stopSound(int handler) = 0;
virtual void pauseSound(int handler) = 0;
virtual void resumeSound(int handler) = 0;
virtual ui32 getVolume() const = 0; virtual ui32 getVolume() const = 0;
virtual void setVolume(ui32 percent) = 0; virtual void setVolume(ui32 percent) = 0;

View File

@ -20,6 +20,9 @@ VCMI_LIB_NAMESPACE_END
class IVideoInstance class IVideoInstance
{ {
public: public:
/// Returns current video timestamp
virtual double timeStamp() = 0;
/// Returns true if video playback is over /// Returns true if video playback is over
virtual bool videoEnded() = 0; virtual bool videoEnded() = 0;
@ -32,6 +35,10 @@ public:
/// Advances video playback by specified duration /// Advances video playback by specified duration
virtual void tick(uint32_t msPassed) = 0; virtual void tick(uint32_t msPassed) = 0;
/// activate or deactivate video
virtual void activate() = 0;
virtual void deactivate() = 0;
virtual ~IVideoInstance() = default; virtual ~IVideoInstance() = default;
}; };

View File

@ -9,6 +9,7 @@
*/ */
#include "StdInc.h" #include "StdInc.h"
#include "VideoWidget.h" #include "VideoWidget.h"
#include "TextControls.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
@ -16,6 +17,8 @@
#include "../media/IVideoPlayer.h" #include "../media/IVideoPlayer.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../../lib/filesystem/Filesystem.h"
VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio) VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio)
: VideoWidgetBase(position, video, playAudio, 1.0) : VideoWidgetBase(position, video, playAudio, 1.0)
{ {
@ -33,11 +36,22 @@ VideoWidgetBase::~VideoWidgetBase() = default;
void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) void VideoWidgetBase::playVideo(const VideoPath & fileToPlay)
{ {
OBJECT_CONSTRUCTION;
JsonPath subTitlePath = fileToPlay.toType<EResType::JSON>();
JsonPath subTitlePathVideoDir = subTitlePath.addPrefix("VIDEO/");
if(CResourceHandler::get()->existsResource(subTitlePath))
subTitleData = JsonNode(subTitlePath);
else if(CResourceHandler::get()->existsResource(subTitlePathVideoDir))
subTitleData = JsonNode(subTitlePathVideoDir);
videoInstance = CCS->videoh->open(fileToPlay, scaleFactor); videoInstance = CCS->videoh->open(fileToPlay, scaleFactor);
if (videoInstance) if (videoInstance)
{ {
pos.w = videoInstance->size().x; pos.w = videoInstance->size().x;
pos.h = videoInstance->size().y; pos.h = videoInstance->size().y;
if(!subTitleData.isNull())
subTitle = std::make_unique<CMultiLineLabel>(Rect(0, (pos.h / 5) * 4, pos.w, pos.h / 5), EFonts::FONT_HIGH_SCORE, ETextAlignment::CENTER, Colors::WHITE);
} }
if (playAudio) if (playAudio)
@ -52,6 +66,8 @@ void VideoWidgetBase::show(Canvas & to)
{ {
if(videoInstance) if(videoInstance)
videoInstance->show(pos.topLeft(), to); videoInstance->show(pos.topLeft(), to);
if(subTitle)
subTitle->showAll(to);
} }
void VideoWidgetBase::loadAudio(const VideoPath & fileToPlay) void VideoWidgetBase::loadAudio(const VideoPath & fileToPlay)
@ -77,7 +93,7 @@ void VideoWidgetBase::startAudio()
{ {
this->audioHandle = -1; this->audioHandle = -1;
} }
); );
} }
} }
@ -91,22 +107,43 @@ void VideoWidgetBase::stopAudio()
} }
} }
std::string VideoWidgetBase::getSubTitleLine(double timestamp)
{
if(subTitleData.isNull())
return {};
for(auto & segment : subTitleData.Vector())
if(timestamp > segment["timeStart"].Float() && timestamp < segment["timeEnd"].Float())
return segment["text"].String();
return {};
}
void VideoWidgetBase::activate() void VideoWidgetBase::activate()
{ {
CIntObject::activate(); CIntObject::activate();
startAudio(); if(audioHandle != -1)
CCS->soundh->resumeSound(audioHandle);
else
startAudio();
if(videoInstance)
videoInstance->activate();
} }
void VideoWidgetBase::deactivate() void VideoWidgetBase::deactivate()
{ {
CIntObject::deactivate(); CIntObject::deactivate();
stopAudio(); CCS->soundh->pauseSound(audioHandle);
if(videoInstance)
videoInstance->deactivate();
} }
void VideoWidgetBase::showAll(Canvas & to) void VideoWidgetBase::showAll(Canvas & to)
{ {
if(videoInstance) if(videoInstance)
videoInstance->show(pos.topLeft(), to); videoInstance->show(pos.topLeft(), to);
if(subTitle)
subTitle->showAll(to);
} }
void VideoWidgetBase::tick(uint32_t msPassed) void VideoWidgetBase::tick(uint32_t msPassed)
@ -122,6 +159,8 @@ void VideoWidgetBase::tick(uint32_t msPassed)
onPlaybackFinished(); onPlaybackFinished();
} }
} }
if(subTitle && videoInstance)
subTitle->setText(getSubTitleLine(videoInstance->timeStamp()));
} }
VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio)

View File

@ -12,21 +12,26 @@
#include "../gui/CIntObject.h" #include "../gui/CIntObject.h"
#include "../lib/filesystem/ResourcePath.h" #include "../lib/filesystem/ResourcePath.h"
#include "../lib/json/JsonNode.h"
class IVideoInstance; class IVideoInstance;
class CMultiLineLabel;
class VideoWidgetBase : public CIntObject class VideoWidgetBase : public CIntObject
{ {
std::unique_ptr<IVideoInstance> videoInstance; std::unique_ptr<IVideoInstance> videoInstance;
std::unique_ptr<CMultiLineLabel> subTitle;
std::pair<std::unique_ptr<ui8[]>, si64> audioData = {nullptr, 0}; std::pair<std::unique_ptr<ui8[]>, si64> audioData = {nullptr, 0};
int audioHandle = -1; int audioHandle = -1;
bool playAudio = false; bool playAudio = false;
float scaleFactor = 1.0; float scaleFactor = 1.0;
JsonNode subTitleData;
void loadAudio(const VideoPath & file); void loadAudio(const VideoPath & file);
void startAudio(); void startAudio();
void stopAudio(); void stopAudio();
std::string getSubTitleLine(double timestamp);
protected: protected:
VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio); VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio);

View File

@ -56,6 +56,19 @@ This will export all strings from game into `Documents/My Games/VCMI/extracted/t
To export maps and campaigns, use '/translate maps' command instead. To export maps and campaigns, use '/translate maps' command instead.
### Video subtitles
It's possible to add video subtitles. Create a JSON file in `video` folder of translation mod with the name of the video (e.g. `H3Intro.json`):
```
[
{
"timeStart" : 5.640, // start time, seconds
"timeEnd" : 8.120, // end time, seconds
"text" : " ... " // text to show during this period
},
...
]
```
## Translating VCMI data ## Translating VCMI data
VCMI contains several new strings, to cover functionality not existing in Heroes III. It can be roughly split into following parts: VCMI contains several new strings, to cover functionality not existing in Heroes III. It can be roughly split into following parts: