mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-28 05:00:20 +00:00
Write dive data as video subtitles
This commit adds an entry to the dive media context menu which offers to write a subtitle file. This creates an .ass file for the selected videos. In an attempt to to clutter the screen too much, don't show irrelevant entries (zero temperature or NDL and show TTS only for dives with stops). VLC is able to show these subtitles directly, they can be integrated into the video file with ffmpeg. Signed-off-by: Robert C. Helling <helling@atdotde.de>
This commit is contained in:
parent
0573b19b65
commit
52105e5217
8 changed files with 125 additions and 2 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
- Desktop: For videos, add save data export as subtitle file
|
||||||
- Desktop: make dive sites 1st class citizens with their own dive site table
|
- Desktop: make dive sites 1st class citizens with their own dive site table
|
||||||
- Desktop: only show dives at the dive sites selected in dive site table
|
- Desktop: only show dives at the dive sites selected in dive site table
|
||||||
- Desktop: add undo functionality to edit operations and remove 'edited' state;
|
- Desktop: add undo functionality to edit operations and remove 'edited' state;
|
||||||
|
|
|
@ -1638,6 +1638,17 @@ or play the video, overlaying the _Subsurface_ window. Delete media from the _Me
|
||||||
it (single-click) and then by pressing the _Del_ key on the keyboard. This removes it BOTH
|
it (single-click) and then by pressing the _Del_ key on the keyboard. This removes it BOTH
|
||||||
from the _Media_ tab as well as the dive profile.
|
from the _Media_ tab as well as the dive profile.
|
||||||
|
|
||||||
|
By right-clicking on a video and selecting the "Save dive data as subtitles" option, a subtitles
|
||||||
|
file with the same name as the video but with an ".ass" extension is created that contains
|
||||||
|
time dependent dive data (runtime, depth, temperature, NDL, TTS, surface GF) to be overlayed
|
||||||
|
with the video. The VLC video player automatically finds this file upon playing the video
|
||||||
|
and overlays the dive data. Alternatively, the ffmpeg video encoder can be used to create a
|
||||||
|
new video file with the dive data encoded in the video stream. To do so run
|
||||||
|
|
||||||
|
ffmpeg -v video.mp4 -vf "ass=video.ass" video_with_data.mp4
|
||||||
|
|
||||||
|
from the command line. You need to have the libass library installed.
|
||||||
|
|
||||||
==== Media on an external hard disk
|
==== Media on an external hard disk
|
||||||
Most underwater photographers store media on an external drive. If such a drive can be mapped by the operating system
|
Most underwater photographers store media on an external drive. If such a drive can be mapped by the operating system
|
||||||
(almost always the case) the media can be directly accessed by _Subsurface_. This eases the interaction
|
(almost always the case) the media can be directly accessed by _Subsurface_. This eases the interaction
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "core/membuffer.h"
|
#include "core/membuffer.h"
|
||||||
#include "core/subsurface-string.h"
|
#include "core/subsurface-string.h"
|
||||||
#include "core/save-profiledata.h"
|
#include "core/save-profiledata.h"
|
||||||
|
#include "core/version.h"
|
||||||
|
|
||||||
static void put_int(struct membuffer *b, int val)
|
static void put_int(struct membuffer *b, int val)
|
||||||
{
|
{
|
||||||
|
@ -21,6 +22,15 @@ static void put_double(struct membuffer *b, double val)
|
||||||
put_format(b, "\"%f\" ", val);
|
put_format(b, "\"%f\" ", val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void put_video_time(struct membuffer *b, int secs)
|
||||||
|
{
|
||||||
|
int hours = secs / 3600;
|
||||||
|
secs -= hours * 3600;
|
||||||
|
int mins = secs / 60;
|
||||||
|
secs -= mins * 60;
|
||||||
|
put_format(b, "%d:%02d:%02d.000,", hours, mins, secs);
|
||||||
|
}
|
||||||
|
|
||||||
static void put_pd(struct membuffer *b, struct plot_data *entry)
|
static void put_pd(struct membuffer *b, struct plot_data *entry)
|
||||||
{
|
{
|
||||||
if (!entry)
|
if (!entry)
|
||||||
|
@ -148,6 +158,39 @@ static void put_headers(struct membuffer *b)
|
||||||
put_csv_string(b, "icd_warning");
|
put_csv_string(b, "icd_warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void put_st_event(struct membuffer *b, struct plot_data *entry, int offset, int length)
|
||||||
|
{
|
||||||
|
double value;
|
||||||
|
int decimals;
|
||||||
|
const char *unit;
|
||||||
|
|
||||||
|
if (entry->sec < offset || entry->sec > offset + length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
put_format(b, "Dialogue: 0,");
|
||||||
|
put_video_time(b, entry->sec - offset);
|
||||||
|
put_video_time(b, (entry+1)->sec - offset < length ? (entry+1)->sec - offset : length);
|
||||||
|
put_format(b, "Default,,0,0,0,,");
|
||||||
|
put_format(b, "%d:%02d ", FRACTION(entry->sec, 60));
|
||||||
|
value = get_depth_units(entry->depth, &decimals, &unit);
|
||||||
|
put_format(b, "D=%02.2f %s ", value, unit);
|
||||||
|
if (entry->temperature) {
|
||||||
|
value = get_temp_units(entry->temperature, &unit);
|
||||||
|
put_format(b, "T=%.1f%s ", value, unit);
|
||||||
|
}
|
||||||
|
// Only show NDL if it is not essentially infinite, show TTS for mandatory stops.
|
||||||
|
if (entry->ndl_calc < 3600) {
|
||||||
|
if (entry->ndl_calc > 0)
|
||||||
|
put_format(b, "NDL=%d:%02d ", FRACTION(entry->ndl_calc, 60));
|
||||||
|
else
|
||||||
|
if (entry->tts_calc > 0)
|
||||||
|
put_format(b, "TTS=%d:%02d ", FRACTION(entry->tts_calc, 60));
|
||||||
|
}
|
||||||
|
if (entry->surface_gf > 0.0) {
|
||||||
|
put_format(b, "sGF=%.1f%% ", entry->surface_gf);
|
||||||
|
}
|
||||||
|
put_format(b, "\n");
|
||||||
|
}
|
||||||
static void save_profiles_buffer(struct membuffer *b, bool select_only)
|
static void save_profiles_buffer(struct membuffer *b, bool select_only)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
|
@ -171,6 +214,27 @@ static void save_profiles_buffer(struct membuffer *b, bool select_only)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void save_subtitles_buffer(struct membuffer *b, struct dive *dive, int offset, int length)
|
||||||
|
{
|
||||||
|
struct plot_info pi;
|
||||||
|
struct deco_state *planner_deco_state = NULL;
|
||||||
|
|
||||||
|
pi = calculate_max_limits_new(dive, &dive->dc);
|
||||||
|
create_plot_info_new(dive, &dive->dc, &pi, false, planner_deco_state);
|
||||||
|
|
||||||
|
put_format(b, "[Script Info]\n");
|
||||||
|
put_format(b, "; Script generated by Subsurface %s\n", subsurface_canonical_version());
|
||||||
|
put_format(b, "ScriptType: v4.00+\nPlayResX: 384\nPlayResY: 288\n\n");
|
||||||
|
put_format(b, "[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n");
|
||||||
|
put_format(b, "Style: Default,Arial,12,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,7,10,10,10,0\n\n");
|
||||||
|
put_format(b, "[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n");
|
||||||
|
|
||||||
|
for (int i = 0; i < pi.nr; i++) {
|
||||||
|
put_st_event(b, &pi.entry[i], offset, length);
|
||||||
|
}
|
||||||
|
put_format(b, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
int save_profiledata(const char *filename, const bool select_only)
|
int save_profiledata(const char *filename, const bool select_only)
|
||||||
{
|
{
|
||||||
struct membuffer buf = { 0 };
|
struct membuffer buf = { 0 };
|
||||||
|
|
|
@ -9,6 +9,7 @@ extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int save_profiledata(const char *filename, bool selected_only);
|
int save_profiledata(const char *filename, bool selected_only);
|
||||||
|
void save_subtitles_buffer(struct membuffer *b, struct dive *dive, int offset, int length);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include "core/save-profiledata.h"
|
||||||
|
#include "core/membuffer.h"
|
||||||
|
|
||||||
//TODO: Remove those in the future.
|
//TODO: Remove those in the future.
|
||||||
#include "../mainwindow.h"
|
#include "../mainwindow.h"
|
||||||
|
@ -57,6 +59,7 @@ void TabDivePhotos::contextMenuEvent(QContextMenuEvent *event)
|
||||||
popup.addAction(tr("Delete all media files"), this, SLOT(removeAllPhotos()));
|
popup.addAction(tr("Delete all media files"), this, SLOT(removeAllPhotos()));
|
||||||
popup.addAction(tr("Open folder of selected media files"), this, SLOT(openFolderOfSelectedFiles()));
|
popup.addAction(tr("Open folder of selected media files"), this, SLOT(openFolderOfSelectedFiles()));
|
||||||
popup.addAction(tr("Recalculate selected thumbnails"), this, SLOT(recalculateSelectedThumbnails()));
|
popup.addAction(tr("Recalculate selected thumbnails"), this, SLOT(recalculateSelectedThumbnails()));
|
||||||
|
popup.addAction(tr("Save dive data as subtitles"), this, SLOT(saveSubtitles()));
|
||||||
popup.exec(event->globalPos());
|
popup.exec(event->globalPos());
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
@ -106,6 +109,40 @@ void TabDivePhotos::recalculateSelectedThumbnails()
|
||||||
Thumbnailer::instance()->calculateThumbnails(getSelectedFilenames());
|
Thumbnailer::instance()->calculateThumbnails(getSelectedFilenames());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabDivePhotos::saveSubtitles()
|
||||||
|
{
|
||||||
|
QVector<QString> selectedPhotos;
|
||||||
|
if (!ui->photosView->selectionModel()->hasSelection())
|
||||||
|
return;
|
||||||
|
QModelIndexList indexes = ui->photosView->selectionModel()->selectedRows();
|
||||||
|
if (indexes.count() == 0)
|
||||||
|
indexes = ui->photosView->selectionModel()->selectedIndexes();
|
||||||
|
selectedPhotos.reserve(indexes.count());
|
||||||
|
for (const auto &photo: indexes) {
|
||||||
|
if (photo.isValid()) {
|
||||||
|
QString fileUrl = photo.data(Qt::DisplayPropertyRole).toString();
|
||||||
|
if (!fileUrl.isEmpty()) {
|
||||||
|
QFileInfo fi = QFileInfo(fileUrl);
|
||||||
|
QFile subtitlefile;
|
||||||
|
subtitlefile.setFileName(QString(fi.path()) + "/" + fi.completeBaseName() + ".ass");
|
||||||
|
int offset = photo.data(Qt::UserRole + 1).toInt();
|
||||||
|
int duration = photo.data(Qt::UserRole + 2).toInt();
|
||||||
|
// Only videos have non-zero duration
|
||||||
|
if (!duration)
|
||||||
|
continue;
|
||||||
|
struct membuffer b = { 0 };
|
||||||
|
save_subtitles_buffer(&b, &displayed_dive, offset, duration);
|
||||||
|
char *data = detach_buffer(&b);
|
||||||
|
subtitlefile.open(QIODevice::WriteOnly);
|
||||||
|
subtitlefile.write(data, strlen(data));
|
||||||
|
subtitlefile.close();
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: This looks overly wrong. We shouldn't call MainWindow to retrieve the DiveList to add Images.
|
//TODO: This looks overly wrong. We shouldn't call MainWindow to retrieve the DiveList to add Images.
|
||||||
void TabDivePhotos::addPhotosFromFile()
|
void TabDivePhotos::addPhotosFromFile()
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,6 +29,7 @@ private slots:
|
||||||
void recalculateSelectedThumbnails();
|
void recalculateSelectedThumbnails();
|
||||||
void openFolderOfSelectedFiles();
|
void openFolderOfSelectedFiles();
|
||||||
void changeZoomLevel(int delta);
|
void changeZoomLevel(int delta);
|
||||||
|
void saveSubtitles();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ui::TabDivePhotos *ui;
|
Ui::TabDivePhotos *ui;
|
||||||
|
|
|
@ -59,7 +59,7 @@ void DivePictureModel::updateDivePictures()
|
||||||
if (dive->selected) {
|
if (dive->selected) {
|
||||||
int first = pictures.count();
|
int first = pictures.count();
|
||||||
FOR_EACH_PICTURE(dive)
|
FOR_EACH_PICTURE(dive)
|
||||||
pictures.push_back({ dive->id, picture, picture->filename, {}, picture->offset.seconds });
|
pictures.push_back({ dive->id, picture, picture->filename, {}, picture->offset.seconds, {.seconds = 0}});
|
||||||
|
|
||||||
// Sort pictures of this dive by offset.
|
// Sort pictures of this dive by offset.
|
||||||
// Thus, the list will be sorted by (diveId, offset).
|
// Thus, the list will be sorted by (diveId, offset).
|
||||||
|
@ -101,6 +101,11 @@ QVariant DivePictureModel::data(const QModelIndex &index, int role) const
|
||||||
case Qt::UserRole:
|
case Qt::UserRole:
|
||||||
ret = entry.diveId;
|
ret = entry.diveId;
|
||||||
break;
|
break;
|
||||||
|
case Qt::UserRole + 1:
|
||||||
|
ret = entry.offsetSeconds;
|
||||||
|
break;
|
||||||
|
case Qt::UserRole + 2:
|
||||||
|
ret = entry.length.seconds;
|
||||||
}
|
}
|
||||||
} else if (index.column() == 1) {
|
} else if (index.column() == 1) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
|
@ -197,8 +202,10 @@ void DivePictureModel::updateThumbnail(QString filename, QImage thumbnail, durat
|
||||||
{
|
{
|
||||||
int i = findPictureId(filename);
|
int i = findPictureId(filename);
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
if (duration.seconds > 0)
|
if (duration.seconds > 0) {
|
||||||
addDurationToThumbnail(thumbnail, duration); // If we know the duration paint it on top of the thumbnail
|
addDurationToThumbnail(thumbnail, duration); // If we know the duration paint it on top of the thumbnail
|
||||||
|
pictures[i].length = duration;
|
||||||
|
}
|
||||||
pictures[i].image = thumbnail;
|
pictures[i].image = thumbnail;
|
||||||
emit dataChanged(createIndex(i, 0), createIndex(i, 1));
|
emit dataChanged(createIndex(i, 0), createIndex(i, 1));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ struct PictureEntry {
|
||||||
QString filename;
|
QString filename;
|
||||||
QImage image;
|
QImage image;
|
||||||
int offsetSeconds;
|
int offsetSeconds;
|
||||||
|
duration_t length;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DivePictureModel : public QAbstractTableModel {
|
class DivePictureModel : public QAbstractTableModel {
|
||||||
|
|
Loading…
Reference in a new issue