summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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 {