diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | Documentation/user-manual.txt | 11 | ||||
-rw-r--r-- | core/save-profiledata.c | 64 | ||||
-rw-r--r-- | core/save-profiledata.h | 1 | ||||
-rw-r--r-- | desktop-widgets/tab-widgets/TabDivePhotos.cpp | 37 | ||||
-rw-r--r-- | desktop-widgets/tab-widgets/TabDivePhotos.h | 1 | ||||
-rw-r--r-- | qt-models/divepicturemodel.cpp | 11 | ||||
-rw-r--r-- | qt-models/divepicturemodel.h | 1 |
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 { |