Browse Source

Render video subtitles

pull/8394/head
Yuri Pourre 3 months ago
parent
commit
6d04ba1aae
  1. 1
      Source/CMakeLists.txt
  2. 94
      Source/storm/storm_svid.cpp
  3. 95
      Source/utils/srt_parser.cpp
  4. 39
      Source/utils/srt_parser.hpp

1
Source/CMakeLists.txt

@ -164,6 +164,7 @@ set(libdevilutionx_SRCS
utils/language.cpp
utils/sdl_bilinear_scale.cpp
utils/sdl_thread.cpp
utils/srt_parser.cpp
utils/surface_to_clx.cpp
utils/timer.cpp)

94
Source/storm/storm_svid.cpp

@ -1,9 +1,12 @@
#include "storm/storm_svid.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string>
#include <vector>
#ifdef USE_SDL3
#include <SDL3/SDL_error.h>
@ -30,11 +33,13 @@
#include "engine/assets.hpp"
#include "engine/dx.h"
#include "engine/palette.h"
#include "engine/render/text_render.hpp"
#include "options.h"
#include "utils/display.h"
#include "utils/log.hpp"
#include "utils/sdl_compat.h"
#include "utils/sdl_wrap.h"
#include "utils/srt_parser.hpp"
namespace devilution {
namespace {
@ -77,6 +82,11 @@ SDLSurfaceUniquePtr SVidSurface;
uint64_t SVidFrameEnd;
// The length of a frame in SMK time units.
uint32_t SVidFrameLength;
// Video start time in SMK time units (when playback began).
uint64_t SVidStartTime;
// Subtitle entries for current video
std::vector<SubtitleEntry> SVidSubtitles;
bool IsLandscapeFit(unsigned long srcW, unsigned long srcH, unsigned long dstW, unsigned long dstH)
{
@ -294,6 +304,71 @@ bool BlitFrame()
}
}
// Render subtitles if available
if (!SVidSubtitles.empty()) {
const uint64_t currentTimeSmk = GetTicksSmk();
const uint64_t videoTimeMs = TimeSmkToMs(currentTimeSmk - SVidStartTime);
const std::string subtitleText = GetSubtitleAtTime(SVidSubtitles, videoTimeMs);
if (!subtitleText.empty()) {
SDL_Surface *outputSurface = GetOutputSurface();
SDL_Rect outputRect;
#ifndef USE_SDL1
if (renderer != nullptr) {
// When using renderer, video fills the entire output surface
outputRect.w = outputSurface->w;
outputRect.h = outputSurface->h;
outputRect.x = 0;
outputRect.y = 0;
} else
#endif
{
// Calculate video rect (same logic as above)
#ifdef USE_SDL1
const bool isIndexedOutputFormat = SDLBackport_IsPixelFormatIndexed(outputSurface->format);
#else
#ifdef USE_SDL3
const SDL_PixelFormat wndFormat = SDL_GetWindowPixelFormat(ghMainWnd);
#else
const Uint32 wndFormat = SDL_GetWindowPixelFormat(ghMainWnd);
#endif
const bool isIndexedOutputFormat = SDL_ISPIXELFORMAT_INDEXED(wndFormat);
#endif
if (isIndexedOutputFormat) {
outputRect.w = static_cast<int>(SVidWidth);
outputRect.h = static_cast<int>(SVidHeight);
} else if (IsLandscapeFit(SVidWidth, SVidHeight, outputSurface->w, outputSurface->h)) {
outputRect.w = outputSurface->w;
outputRect.h = SVidHeight * outputSurface->w / SVidWidth;
} else {
outputRect.w = SVidWidth * outputSurface->h / SVidHeight;
outputRect.h = outputSurface->h;
}
outputRect.x = (outputSurface->w - outputRect.w) / 2;
outputRect.y = (outputSurface->h - outputRect.h) / 2;
}
// Calculate subtitle position (bottom center, with some padding)
constexpr int SubtitlePadding = 20;
const int subtitleY = outputRect.y + outputRect.h - SubtitlePadding;
const int subtitleX = outputRect.x;
const int subtitleWidth = outputRect.w;
// Create a surface for rendering text
Surface outSurface(outputSurface);
// Allow enough height for multiple lines of text
constexpr int SubtitleMaxHeight = 120;
const int subtitleRectY = std::max(0, subtitleY - SubtitleMaxHeight);
Rectangle subtitleRect { { subtitleX, subtitleRectY }, { subtitleWidth, SubtitleMaxHeight } };
// Render subtitle with white text, centered, and outlined for visibility
TextRenderOptions opts;
opts.flags = UiFlags::AlignCenter | UiFlags::ColorWhite | UiFlags::Outlined;
opts.spacing = 1;
DrawString(outSurface, subtitleText, subtitleRect, opts);
}
}
RenderPresent();
return true;
}
@ -327,6 +402,20 @@ void SVidInitAudioStream(const SmackerAudioInfo &audioInfo)
}
#endif
// Load subtitles for the given video filename
void LoadSubtitles(const char *videoFilename)
{
SVidSubtitles.clear();
// Generate subtitle filename by replacing .smk with .srt
std::string subtitlePath(videoFilename);
std::replace(subtitlePath.begin(), subtitlePath.end(), '\\', '/');
const size_t extPos = subtitlePath.rfind('.');
subtitlePath = (extPos != std::string::npos ? subtitlePath.substr(0, extPos) : subtitlePath) + ".srt";
SVidSubtitles = LoadSrtFile(subtitlePath);
}
} // namespace
bool SVidPlayBegin(const char *filename, int flags)
@ -345,6 +434,9 @@ bool SVidPlayBegin(const char *filename, int flags)
// 0x800000 // Edge detection
// 0x200800 // Clear FB
// Load subtitles if available
LoadSubtitles(filename);
auto *videoStream = OpenAssetAsSdlRwOps(filename);
SVidHandle = Smacker_Open(videoStream);
if (!SVidHandle.isValid) {
@ -449,6 +541,7 @@ bool SVidPlayBegin(const char *filename, int flags)
UpdatePalette();
SVidFrameEnd = GetTicksSmk() + SVidFrameLength;
SVidStartTime = GetTicksSmk();
return true;
}
@ -523,6 +616,7 @@ void SVidPlayEnd()
SVidPalette = nullptr;
SVidSurface = nullptr;
SVidFrameBuffer = nullptr;
SVidSubtitles.clear();
#ifndef USE_SDL1
if (renderer != nullptr) {

95
Source/utils/srt_parser.cpp

@ -0,0 +1,95 @@
#include "utils/srt_parser.hpp"
#include <algorithm>
#include <cstdio>
#include <istream>
#include <sstream>
#include <string>
#include "engine/assets.hpp"
namespace devilution {
uint64_t ParseSrtTimestamp(std::string_view timestamp)
{
unsigned long long h = 0, m = 0, s = 0, ms = 0;
if (sscanf(timestamp.data(), "%llu:%llu:%llu,%llu", &h, &m, &s, &ms) == 4
|| sscanf(timestamp.data(), "%llu:%llu:%llu.%llu", &h, &m, &s, &ms) == 4) {
return static_cast<uint64_t>(h * 3600000ULL + m * 60000ULL + s * 1000ULL + ms);
}
return 0;
}
std::vector<SubtitleEntry> LoadSrtFile(std::string_view subtitlePath)
{
std::vector<SubtitleEntry> subtitles;
std::string pathStr(subtitlePath);
auto assetData = LoadAsset(pathStr);
if (!assetData.has_value())
return subtitles;
std::string content(assetData->data.get(), assetData->size);
std::istringstream stream(content);
std::string line, text;
uint64_t startTime = 0, endTime = 0;
while (std::getline(stream, line)) {
// Remove \r if present
if (!line.empty() && line.back() == '\r')
line.pop_back();
// Skip empty lines (end of subtitle block)
if (line.empty()) {
if (!text.empty() && startTime < endTime) {
// Remove trailing newline from text
if (!text.empty() && text.back() == '\n')
text.pop_back();
subtitles.push_back({ startTime, endTime, text });
text.clear();
}
continue;
}
// Check if line is a number (subtitle index) - skip it
if (std::all_of(line.begin(), line.end(), [](char c) { return std::isdigit(static_cast<unsigned char>(c)); }))
continue;
// Check if line contains --> (timestamp line)
const size_t arrowPos = line.find("-->");
if (arrowPos != std::string::npos) {
const std::string startStr = line.substr(0, arrowPos);
const std::string endStr = line.substr(arrowPos + 3);
startTime = ParseSrtTimestamp(startStr);
endTime = ParseSrtTimestamp(endStr);
continue;
}
// Otherwise it's subtitle text
if (!text.empty())
text += "\n";
text += line;
}
// Handle last subtitle if file doesn't end with blank line
if (!text.empty() && startTime < endTime) {
if (!text.empty() && text.back() == '\n')
text.pop_back();
subtitles.push_back({ startTime, endTime, text });
}
return subtitles;
}
std::string GetSubtitleAtTime(const std::vector<SubtitleEntry> &subtitles, uint64_t videoTimeMs)
{
for (const auto &entry : subtitles) {
if (videoTimeMs >= entry.startTimeMs && videoTimeMs < entry.endTimeMs) {
return entry.text;
}
}
return "";
}
} // namespace devilution

39
Source/utils/srt_parser.hpp

@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace devilution {
struct SubtitleEntry {
uint64_t startTimeMs; // Start time in milliseconds
uint64_t endTimeMs; // End time in milliseconds
std::string text; // Subtitle text (may contain multiple lines)
};
/**
* @brief Parse SRT timestamp (HH:MM:SS,mmm or HH:MM:SS.mmm) to milliseconds
* @param timestamp Timestamp string in SRT format
* @return Time in milliseconds, or 0 if parsing fails
*/
uint64_t ParseSrtTimestamp(std::string_view timestamp);
/**
* @brief Load and parse SRT subtitle file
* @param subtitlePath Path to the SRT file
* @return Vector of subtitle entries, empty if file not found or parsing fails
*/
std::vector<SubtitleEntry> LoadSrtFile(std::string_view subtitlePath);
/**
* @brief Get subtitle text for a given time from a list of subtitle entries
* @param subtitles Vector of subtitle entries
* @param videoTimeMs Current video time in milliseconds
* @return Subtitle text if a subtitle is active at this time, empty string otherwise
*/
std::string GetSubtitleAtTime(const std::vector<SubtitleEntry> &subtitles, uint64_t videoTimeMs);
} // namespace devilution
Loading…
Cancel
Save