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:
commit
11d9ee310e
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -77,22 +77,28 @@ 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();
|
||||||
|
|
||||||
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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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)
|
||||||
@ -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();
|
||||||
|
if(audioHandle != -1)
|
||||||
|
CCS->soundh->resumeSound(audioHandle);
|
||||||
|
else
|
||||||
startAudio();
|
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)
|
||||||
|
@ -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);
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user