summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Robert C. Helling <helling@atdotde.de>2019-04-14 16:19:23 +0200
committerGravatar bstoeger <32835590+bstoeger@users.noreply.github.com>2019-04-16 20:38:19 +0200
commit52105e521720c87e64dbd2519be7bbe5dc243439 (patch)
treec8483c1d7163a9bf6b40b6f984467ccfd549c7d9
parent0573b19b653d0b4963fe11efdc2303289d2b7994 (diff)
downloadsubsurface-52105e521720c87e64dbd2519be7bbe5dc243439.tar.gz
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>
-rw-r--r--CHANGELOG.md1
-rw-r--r--Documentation/user-manual.txt11
-rw-r--r--core/save-profiledata.c64
-rw-r--r--core/save-profiledata.h1
-rw-r--r--desktop-widgets/tab-widgets/TabDivePhotos.cpp37
-rw-r--r--desktop-widgets/tab-widgets/TabDivePhotos.h1
-rw-r--r--qt-models/divepicturemodel.cpp11
-rw-r--r--qt-models/divepicturemodel.h1
8 files changed, 125 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f74783586..cce954eb1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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: only show dives at the dive sites selected in dive site table
- Desktop: add undo functionality to edit operations and remove 'edited' state;
diff --git a/Documentation/user-manual.txt b/Documentation/user-manual.txt
index 359c37b01..71e0b6487 100644
--- a/Documentation/user-manual.txt
+++ b/Documentation/user-manual.txt
@@ -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
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
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
diff --git a/core/save-profiledata.c b/core/save-profiledata.c
index 603045fe0..09ac76886 100644
--- a/core/save-profiledata.c
+++ b/core/save-profiledata.c
@@ -5,6 +5,7 @@
#include "core/membuffer.h"
#include "core/subsurface-string.h"
#include "core/save-profiledata.h"
+#include "core/version.h"
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);
}
+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)
{
if (!entry)
@@ -148,6 +158,39 @@ static void put_headers(struct membuffer *b)
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)
{
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)
{
struct membuffer buf = { 0 };
diff --git a/core/save-profiledata.h b/core/save-profiledata.h
index 23e833a27..fd38e48ca 100644
--- a/core/save-profiledata.h
+++ b/core/save-profiledata.h
@@ -9,6 +9,7 @@ extern "C" {
#endif
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
}
diff --git a/desktop-widgets/tab-widgets/TabDivePhotos.cpp b/desktop-widgets/tab-widgets/TabDivePhotos.cpp
index de65f2e89..fc84e1972 100644
--- a/desktop-widgets/tab-widgets/TabDivePhotos.cpp
+++ b/desktop-widgets/tab-widgets/TabDivePhotos.cpp
@@ -11,6 +11,8 @@
#include <QUrl>
#include <QMessageBox>
#include <QFileInfo>
+#include "core/save-profiledata.h"
+#include "core/membuffer.h"
//TODO: Remove those in the future.
#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("Open folder of selected media files"), this, SLOT(openFolderOfSelectedFiles()));
popup.addAction(tr("Recalculate selected thumbnails"), this, SLOT(recalculateSelectedThumbnails()));
+ popup.addAction(tr("Save dive data as subtitles"), this, SLOT(saveSubtitles()));
popup.exec(event->globalPos());
event->accept();
}
@@ -106,6 +109,40 @@ void TabDivePhotos::recalculateSelectedThumbnails()
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.
void TabDivePhotos::addPhotosFromFile()
{
diff --git a/desktop-widgets/tab-widgets/TabDivePhotos.h b/desktop-widgets/tab-widgets/TabDivePhotos.h
index f172df180..1078e1ca2 100644
--- a/desktop-widgets/tab-widgets/TabDivePhotos.h
+++ b/desktop-widgets/tab-widgets/TabDivePhotos.h
@@ -29,6 +29,7 @@ private slots:
void recalculateSelectedThumbnails();
void openFolderOfSelectedFiles();
void changeZoomLevel(int delta);
+ void saveSubtitles();
private:
Ui::TabDivePhotos *ui;
diff --git a/qt-models/divepicturemodel.cpp b/qt-models/divepicturemodel.cpp
index babd404f5..8156693ef 100644
--- a/qt-models/divepicturemodel.cpp
+++ b/qt-models/divepicturemodel.cpp
@@ -59,7 +59,7 @@ void DivePictureModel::updateDivePictures()
if (dive->selected) {
int first = pictures.count();
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.
// 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:
ret = entry.diveId;
break;
+ case Qt::UserRole + 1:
+ ret = entry.offsetSeconds;
+ break;
+ case Qt::UserRole + 2:
+ ret = entry.length.seconds;
}
} else if (index.column() == 1) {
switch (role) {
@@ -197,8 +202,10 @@ void DivePictureModel::updateThumbnail(QString filename, QImage thumbnail, durat
{
int i = findPictureId(filename);
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
+ pictures[i].length = duration;
+ }
pictures[i].image = thumbnail;
emit dataChanged(createIndex(i, 0), createIndex(i, 1));
}
diff --git a/qt-models/divepicturemodel.h b/qt-models/divepicturemodel.h
index 6c9384c62..4e25db687 100644
--- a/qt-models/divepicturemodel.h
+++ b/qt-models/divepicturemodel.h
@@ -14,6 +14,7 @@ struct PictureEntry {
QString filename;
QImage image;
int offsetSeconds;
+ duration_t length;
};
class DivePictureModel : public QAbstractTableModel {