Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ options:
Hide one or more display elements from the list below:

bloom - bloom effect
captions - caption text
date - current date
dirnames - names of directories
files - file icons
Expand Down Expand Up @@ -327,6 +328,10 @@ options:
--caption-offset X
Caption horizontal offset (0 to centre captions).

--caption-export FILE
Export captions to subtitle file (SRT or WebVTT format).
File format is determined by extension (.srt or .vtt/.webvtt).

-o, --output-ppm-stream FILE
Output a PPM image stream to a file ('-' for STDOUT).

Expand Down
5 changes: 5 additions & 0 deletions data/gource.1
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ Disable keyboard and mouse input.
Hide one or more display elements from the list below:

bloom \- bloom effect
captions \- caption text
date \- current date
dirnames \- names of directories
files \- file icons
Expand Down Expand Up @@ -300,6 +301,10 @@ Caption duration.
\fB\-\-caption-offset X
Caption horizontal offset (0 to centre captions).
.TP
\fB\-\-caption-export FILE
Export captions to subtitle file (SRT or WebVTT format).
File format is determined by extension (.srt or .vtt/.webvtt).
.TP
\fB\-o, \-\-output\-ppm\-stream FILE\fR
Output a PPM image stream to a file ('\-' for STDOUT).

Expand Down
173 changes: 144 additions & 29 deletions src/gource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

#include "gource.h"
#include "core/png_writer.h"
#include <fstream>
#include <iomanip>
#include <algorithm>
#include <map>

bool gGourceDrawBackground = true;
bool gGourceQuadTreeDebug = false;
Expand All @@ -28,6 +32,8 @@ Gource::Gource(FrameExporter* exporter) {

this->logfile = gGourceSettings.path;
commitlog = 0;
subtitle_export_stream = 0;
subtitle_export_index = 1;

//disable OpenGL 2.0 functions if not supported
if(!GLEW_VERSION_2_0) gGourceSettings.ffp = true;
Expand Down Expand Up @@ -155,6 +161,7 @@ Gource::Gource(FrameExporter* exporter) {
//min physics rate 60fps (ie maximum allowed delta 1.0/60)
max_tick_rate = 1.0 / 60.0;
runtime = 0.0f;
video_time = 0.0f;
frameskip = 0;
framecount = 0;

Expand Down Expand Up @@ -216,6 +223,12 @@ void Gource::writeCustomLog(const std::string& logfile, const std::string& outpu
Gource::~Gource() {
reset();

if(subtitle_export_stream) {
subtitle_export_stream->close();
delete subtitle_export_stream;
subtitle_export_stream = 0;
}

if(logmill!=0) delete logmill;
if(root!=0) delete root;

Expand Down Expand Up @@ -256,7 +269,15 @@ void Gource::update(float t, float dt) {
scaled_dt *= gGourceSettings.time_scale;

//have to manage runtime internally as we're messing with dt
if(!paused) runtime += scaled_dt;
if(!paused) {
runtime += scaled_dt;
// Track unscaled video time for subtitle export
if(frameExporter != 0) {
video_time += max_tick_rate; // Advances by 1/framerate per frame (1/25, 1/30, or 1/60)
} else {
video_time = runtime; // When not exporting, use runtime
}
}

if(gGourceSettings.stop_at_time > 0.0 && runtime >= gGourceSettings.stop_at_time) stop_position_reached = true;

Expand Down Expand Up @@ -955,6 +976,7 @@ void Gource::reset() {
currtime=0;
lasttime=0;
subseconds=0.0;
video_time=0.0f;
tag_seq = 1;
commit_seq = 1;
}
Expand Down Expand Up @@ -1128,6 +1150,76 @@ void Gource::loadCaptions() {

}

void Gource::exportCaptions() {
if(gGourceSettings.caption_export_file.empty()) return;

subtitle_export_stream = new std::ofstream(gGourceSettings.caption_export_file.c_str());
if(!subtitle_export_stream->is_open()) {
fprintf(stderr, "Failed to open caption export file: %s\n", gGourceSettings.caption_export_file.c_str());
delete subtitle_export_stream;
subtitle_export_stream = 0;
return;
}

// Determine format from file extension and write header if needed
std::string ext = gGourceSettings.caption_export_file.substr(gGourceSettings.caption_export_file.find_last_of(".") + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);

if(ext == "vtt" || ext == "webvtt") {
*subtitle_export_stream << "WEBVTT\n\n";
}

fprintf(stderr, "Caption export file opened: %s (will record actual timings)\n", gGourceSettings.caption_export_file.c_str());
}

void Gource::writeSubtitleEntry(RCaption* caption, float start_time, float end_time) {
if(!subtitle_export_stream || !subtitle_export_stream->is_open()) return;

// Determine format from file extension
std::string ext = gGourceSettings.caption_export_file.substr(gGourceSettings.caption_export_file.find_last_of(".") + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
bool is_webvtt = (ext == "vtt" || ext == "webvtt");

// Format time for SRT/WebVTT
int start_hours = (int)(start_time / 3600);
int start_mins = (int)((start_time - start_hours * 3600) / 60);
int start_secs = (int)(start_time - start_hours * 3600 - start_mins * 60);
int start_ms = (int)((start_time - (int)start_time) * 1000);

int end_hours = (int)(end_time / 3600);
int end_mins = (int)((end_time - end_hours * 3600) / 60);
int end_secs = (int)(end_time - end_hours * 3600 - end_mins * 60);
int end_ms = (int)((end_time - (int)end_time) * 1000);

if(is_webvtt) {
// WebVTT format
*subtitle_export_stream << std::setfill('0') << std::setw(2) << start_hours << ":"
<< std::setfill('0') << std::setw(2) << start_mins << ":"
<< std::setfill('0') << std::setw(2) << start_secs << "."
<< std::setfill('0') << std::setw(3) << start_ms
<< " --> "
<< std::setfill('0') << std::setw(2) << end_hours << ":"
<< std::setfill('0') << std::setw(2) << end_mins << ":"
<< std::setfill('0') << std::setw(2) << end_secs << "."
<< std::setfill('0') << std::setw(3) << end_ms << "\n";
} else {
// SRT format
*subtitle_export_stream << subtitle_export_index++ << "\n";
*subtitle_export_stream << std::setfill('0') << std::setw(2) << start_hours << ":"
<< std::setfill('0') << std::setw(2) << start_mins << ":"
<< std::setfill('0') << std::setw(2) << start_secs << ","
<< std::setfill('0') << std::setw(3) << start_ms
<< " --> "
<< std::setfill('0') << std::setw(2) << end_hours << ":"
<< std::setfill('0') << std::setw(2) << end_mins << ":"
<< std::setfill('0') << std::setw(2) << end_secs << ","
<< std::setfill('0') << std::setw(3) << end_ms << "\n";
}

*subtitle_export_stream << caption->getCaption() << "\n\n";
subtitle_export_stream->flush();
}

void Gource::readLog() {
if(stop_position_reached) return;

Expand Down Expand Up @@ -1717,6 +1809,7 @@ void Gource::logic(float t, float dt) {
subseconds = 0.0;

loadCaptions();
exportCaptions();
}

//set current time
Expand Down Expand Up @@ -1806,51 +1899,71 @@ void Gource::logic(float t, float dt) {
reloaded = false;
}

while(captions.size() > 0) {
RCaption* caption = captions.front();
// Process captions if we need to display them OR export them
bool process_captions = !gGourceSettings.hide_captions || (subtitle_export_stream && subtitle_export_stream->is_open());

if(process_captions) {
while(captions.size() > 0) {
RCaption* caption = captions.front();

if(caption->timestamp > currtime) break;
if(caption->timestamp > currtime) break;

float y = caption_start_y;
// Record when this caption becomes active for subtitle export
if(subtitle_export_stream && subtitle_export_stream->is_open()) {
caption_start_times[caption] = video_time; // Use unscaled video time
}

while(1) {
// Only calculate position if we're showing captions
if(!gGourceSettings.hide_captions) {
float y = caption_start_y;

bool found = false;
while(1) {
bool found = false;

for(RCaption* cap : active_captions) {
for(RCaption* cap : active_captions) {
if(cap->getPos().y == y) {
found = true;
break;
}
}

if(cap->getPos().y == y) {
found = true;
break;
if(!found) break;
y -= caption_height;
}
}

if(!found) break;
int caption_offset_x = gGourceSettings.caption_offset;

y -= caption_height;
}
// centre
if(caption_offset_x == 0) {
caption_offset_x = (display.width / 2) - (fontcaption.getWidth(caption->getCaption()) / 2);
} else if(caption_offset_x < 0) {
caption_offset_x = display.width + caption_offset_x - fontcaption.getWidth(caption->getCaption());
}

int caption_offset_x = gGourceSettings.caption_offset;
caption->setPos(vec2(caption_offset_x, y));
}

// centre
if(caption_offset_x == 0) {
caption_offset_x = (display.width / 2) - (fontcaption.getWidth(caption->getCaption()) / 2);
} else if(caption_offset_x < 0) {
caption_offset_x = display.width + caption_offset_x - fontcaption.getWidth(caption->getCaption());
captions.pop_front();
active_captions.push_back(caption);
}

caption->setPos(vec2(caption_offset_x, y));

captions.pop_front();
active_captions.push_back(caption);
}

// Process all active captions (visible or hidden)
for(std::list<RCaption*>::iterator it = active_captions.begin(); it!=active_captions.end();) {
RCaption* caption = *it;

caption->logic(dt);

if(caption->isFinished()) {
// Export subtitle when it finishes
if(subtitle_export_stream && subtitle_export_stream->is_open()) {
auto start_it = caption_start_times.find(caption);
if(start_it != caption_start_times.end()) {
writeSubtitleEntry(caption, start_it->second, video_time);
caption_start_times.erase(start_it);
}
}

it = active_captions.erase(it);
delete caption;
continue;
Expand Down Expand Up @@ -2655,10 +2768,12 @@ void Gource::draw(float t, float dt) {
fontmedium.alignTop(true);
}

for(std::list<RCaption*>::iterator it = active_captions.begin(); it!=active_captions.end(); it++) {
RCaption* caption = *it;
if(!gGourceSettings.hide_captions) {
for(std::list<RCaption*>::iterator it = active_captions.begin(); it!=active_captions.end(); it++) {
RCaption* caption = *it;

caption->draw();
caption->draw();
}
}

//file key
Expand Down
8 changes: 8 additions & 0 deletions src/gource.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class Gource : public SDLApp {
time_t currtime;
time_t lasttime;
float runtime;
float video_time; // Unscaled time for subtitle export
float subseconds;

float splash;
Expand Down Expand Up @@ -185,6 +186,11 @@ class Gource : public SDLApp {

std::list<RCaption*> captions;
std::list<RCaption*> active_captions;

// Subtitle export tracking
std::ofstream* subtitle_export_stream;
int subtitle_export_index;
std::map<RCaption*, float> caption_start_times;

QuadTree* dirNodeTree;
QuadTree* userTree;
Expand All @@ -208,6 +214,8 @@ class Gource : public SDLApp {
void selectNextUser();

void loadCaptions();
void exportCaptions();
void writeSubtitleEntry(RCaption* caption, float start_time, float end_time);

void readLog();

Expand Down
13 changes: 13 additions & 0 deletions src/gource_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ if(extended_help) {
printf(" --filename-time SECONDS Duration to keep filenames on screen (default: 4.0)\n\n");

printf(" --caption-file FILE Caption file\n");
printf(" --caption-export FILE Export captions to file (SRT or WebVTT format)\n");
printf(" --caption-size SIZE Caption font size\n");
printf(" --caption-colour FFFFFF Caption colour in hex\n");
printf(" --caption-duration SECONDS Caption duration (default: 10.0)\n");
Expand Down Expand Up @@ -271,6 +272,7 @@ GourceSettings::GourceSettings() {
arg_types["hide-bloom"] = "bool";
arg_types["hide-mouse"] = "bool";
arg_types["hide-root"] = "bool";
arg_types["hide-captions"] = "bool";
arg_types["highlight-users"] = "bool";
arg_types["highlight-dirs"] = "bool";
arg_types["file-extensions"] = "bool";
Expand Down Expand Up @@ -354,6 +356,7 @@ GourceSettings::GourceSettings() {
arg_types["dir-colour"] = "string";

arg_types["caption-file"] = "string";
arg_types["caption-export"] = "string";
arg_types["caption-size"] = "int";
arg_types["caption-duration"] = "float";
arg_types["caption-colour"] = "string";
Expand Down Expand Up @@ -383,6 +386,7 @@ void GourceSettings::setGourceDefaults() {
hide_bloom = false;
hide_mouse = false;
hide_root = false;
hide_captions = false;

start_timestamp = 0;
start_date = "";
Expand Down Expand Up @@ -478,6 +482,7 @@ void GourceSettings::setGourceDefaults() {
highlight_dirs = false;

caption_file = "";
caption_export_file = "";
caption_duration = 10.0f;
caption_size = 16;
caption_offset = 0;
Expand Down Expand Up @@ -695,6 +700,7 @@ void GourceSettings::importGourceSettings(ConfFile& conffile, ConfSection* gourc
else if(hidestr == "bloom") hide_bloom = true;
else if(hidestr == "progress") hide_progress = true;
else if(hidestr == "root") hide_root = true;
else if(hidestr == "captions") hide_captions = true;
else if(hidestr == "mouse") {
hide_mouse = true;
hide_progress = true;
Expand Down Expand Up @@ -874,6 +880,13 @@ void GourceSettings::importGourceSettings(ConfFile& conffile, ConfSection* gourc
}
}

if((entry = gource_settings->getEntry("caption-export")) != 0) {

if(!entry->hasValue()) conffile.entryException(entry, "specify caption export file (filename)");

caption_export_file = entry->getString();
}

if((entry = gource_settings->getEntry("caption-duration")) != 0) {

if(!entry->hasValue()) conffile.entryException(entry, "specify caption duration (seconds)");
Expand Down
Loading