From 5198acefd13b91a7953f1db1370ce7d434132472 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Thu, 13 Dec 2018 00:57:53 +0100 Subject: [PATCH] Add a multitrack export action. --- futatabi/export.cpp | 196 ++++++++++++++++++++++++++++++++++++++++ futatabi/export.h | 8 ++ futatabi/mainwindow.cpp | 27 ++++++ futatabi/mainwindow.h | 1 + futatabi/mainwindow.ui | 12 +++ meson.build | 1 + 6 files changed, 245 insertions(+) create mode 100644 futatabi/export.cpp create mode 100644 futatabi/export.h diff --git a/futatabi/export.cpp b/futatabi/export.cpp new file mode 100644 index 0000000..bed2643 --- /dev/null +++ b/futatabi/export.cpp @@ -0,0 +1,196 @@ +#include "clip_list.h" +#include "defs.h" +#include "export.h" +#include "flags.h" +#include "frame_on_disk.h" +#include "shared/ffmpeg_raii.h" +#include "shared/timebase.h" + +#include +#include + +#include + +#include + +extern "C" { +#include +} + +using namespace std; + +namespace { + +// Only used in export_cliplist_clip_multitrack_triggered. +struct BufferedJPEG { + int64_t pts; + unsigned stream_idx; + string jpeg; +}; + +bool write_buffered_jpegs(AVFormatContext *avctx, const vector &buffered_jpegs) +{ + for (const BufferedJPEG &jpeg : buffered_jpegs) { + AVPacket pkt; + av_init_packet(&pkt); + pkt.stream_index = jpeg.stream_idx; + pkt.data = (uint8_t *)jpeg.jpeg.data(); + pkt.size = jpeg.jpeg.size(); + pkt.pts = jpeg.pts; + pkt.dts = jpeg.pts; + pkt.flags = AV_PKT_FLAG_KEY; + + if (av_write_frame(avctx, &pkt) < 0) { + return false; + } + } + return true; +} + +} // namespace + +void export_multitrack_clip(const string &filename, const Clip &clip) +{ + AVFormatContext *avctx = nullptr; + avformat_alloc_output_context2(&avctx, NULL, NULL, filename.c_str()); + if (avctx == nullptr) { + QMessageBox msgbox; + msgbox.setText("Could not allocate FFmpeg context"); + msgbox.exec(); + return; + } + AVFormatContextWithCloser closer(avctx); + + int ret = avio_open(&avctx->pb, filename.c_str(), AVIO_FLAG_WRITE); + if (ret < 0) { + QMessageBox msgbox; + msgbox.setText(QString::fromStdString("Could not open output file '" + filename + "'")); + msgbox.exec(); + return; + } + + // Find the first frame for each stream. + size_t num_frames = 0; + size_t num_streams_with_frames_left = 0; + size_t last_stream_idx = 0; + FrameReader readers[MAX_STREAMS]; + bool has_frames[MAX_STREAMS]; + size_t first_frame_idx[MAX_STREAMS], last_frame_idx[MAX_STREAMS]; // Inclusive, exclusive. + { + lock_guard lock(frame_mu); + for (size_t stream_idx = 0; stream_idx < MAX_STREAMS; ++stream_idx) { + // Find the first frame such that frame.pts <= pts_in. + auto it = find_first_frame_at_or_after(frames[stream_idx], clip.pts_in); + first_frame_idx[stream_idx] = distance(frames[stream_idx].begin(), it); + has_frames[stream_idx] = (it != frames[stream_idx].end()); + + // Find the first frame such that frame.pts >= pts_out. + it = find_first_frame_at_or_after(frames[stream_idx], clip.pts_out); + last_frame_idx[stream_idx] = distance(frames[stream_idx].begin(), it); + num_frames += last_frame_idx[stream_idx] - first_frame_idx[stream_idx]; + + if (has_frames[stream_idx]) { + ++num_streams_with_frames_left; + last_stream_idx = stream_idx; + } + } + } + + // Create the streams. Note that some of them could be without frames + // (we try to maintain the stream indexes in the export). + vector video_streams; + for (unsigned stream_idx = 0; stream_idx <= last_stream_idx; ++stream_idx) { + AVStream *avstream_video = avformat_new_stream(avctx, nullptr); + if (avstream_video == nullptr) { + fprintf(stderr, "avformat_new_stream() failed\n"); + exit(1); + } + avstream_video->time_base = AVRational{1, TIMEBASE}; + avstream_video->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + avstream_video->codecpar->codec_id = AV_CODEC_ID_MJPEG; + avstream_video->codecpar->width = global_flags.width; // Might be wrong, but doesn't matter all that much. + avstream_video->codecpar->height = global_flags.height; + + // TODO: Deduplicate this against Mux. + avstream_video->codecpar->color_primaries = AVCOL_PRI_BT709; // RGB colorspace (inout_format.color_space). + avstream_video->codecpar->color_trc = AVCOL_TRC_IEC61966_2_1; // Gamma curve (inout_format.gamma_curve). + // YUV colorspace (output_ycbcr_format.luma_coefficients). + avstream_video->codecpar->color_space = AVCOL_SPC_BT709; + avstream_video->codecpar->color_range = AVCOL_RANGE_MPEG; // Full vs. limited range (output_ycbcr_format.full_range). + avstream_video->codecpar->chroma_location = AVCHROMA_LOC_LEFT; // Chroma sample location. See chroma_offset_0[] in Mixer::subsample_chroma(). + avstream_video->codecpar->field_order = AV_FIELD_PROGRESSIVE; + video_streams.push_back(avstream_video); + } + + if (avformat_write_header(avctx, nullptr) < 0) { + QMessageBox msgbox; + msgbox.setText("Writing header failed"); + msgbox.exec(); + unlink(filename.c_str()); + return; + } + + QProgressDialog progress(QString::fromStdString("Exporting to " + filename + "..."), "Abort", 0, 1); + progress.setWindowTitle("Futatabi"); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(1000); + progress.setMaximum(num_frames); + progress.setValue(0); + + // We buffer up to 1000 frames at a time, in a hope that we can reduce + // the amount of seeking needed on rotational media. + vector buffered_jpegs; + size_t frames_written = 0; + while (num_streams_with_frames_left > 0) { + // Find the stream with the lowest frame. Lower stream indexes win. + FrameOnDisk first_frame; + unsigned first_frame_stream_idx = 0; + { + lock_guard lock(frame_mu); + for (size_t stream_idx = 0; stream_idx < MAX_STREAMS; ++stream_idx) { + if (!has_frames[stream_idx]) { + continue; + } + if (first_frame.pts == -1 || frames[stream_idx][first_frame_idx[stream_idx]].pts < first_frame.pts) { + first_frame = frames[stream_idx][first_frame_idx[stream_idx]]; + first_frame_stream_idx = stream_idx; + } + } + ++first_frame_idx[first_frame_stream_idx]; + if (first_frame_idx[first_frame_stream_idx] >= last_frame_idx[first_frame_stream_idx]) { + has_frames[first_frame_stream_idx] = false; + --num_streams_with_frames_left; + } + } + string jpeg = readers[first_frame_stream_idx].read_frame(first_frame); + int64_t scaled_pts = av_rescale_q(first_frame.pts, AVRational{1, TIMEBASE}, + video_streams[first_frame_stream_idx]->time_base); + buffered_jpegs.emplace_back(BufferedJPEG{ scaled_pts, first_frame_stream_idx, std::move(jpeg) }); + if (buffered_jpegs.size() >= 1000) { + if (!write_buffered_jpegs(avctx, buffered_jpegs)) { + QMessageBox msgbox; + msgbox.setText("Writing frames failed"); + msgbox.exec(); + unlink(filename.c_str()); + return; + } + frames_written += buffered_jpegs.size(); + progress.setValue(frames_written); + buffered_jpegs.clear(); + } + if (progress.wasCanceled()) { + unlink(filename.c_str()); + return; + } + } + + if (!write_buffered_jpegs(avctx, buffered_jpegs)) { + QMessageBox msgbox; + msgbox.setText("Writing frames failed"); + msgbox.exec(); + unlink(filename.c_str()); + return; + } + frames_written += buffered_jpegs.size(); + progress.setValue(frames_written); +} diff --git a/futatabi/export.h b/futatabi/export.h new file mode 100644 index 0000000..2349db7 --- /dev/null +++ b/futatabi/export.h @@ -0,0 +1,8 @@ +#ifndef _EXPORT_H +#define _EXPORT_H 1 + +#include + +void export_multitrack_clip(const std::string &filename, const Clip &clip); + +#endif diff --git a/futatabi/mainwindow.cpp b/futatabi/mainwindow.cpp index 57693ef..6d0f7c9 100644 --- a/futatabi/mainwindow.cpp +++ b/futatabi/mainwindow.cpp @@ -2,6 +2,7 @@ #include "shared/aboutdialog.h" #include "clip_list.h" +#include "export.h" #include "shared/disk_space_estimator.h" #include "flags.h" #include "frame_on_disk.h" @@ -11,6 +12,7 @@ #include "ui_mainwindow.h" #include +#include #include #include #include @@ -39,6 +41,7 @@ MainWindow::MainWindow() // The menus. connect(ui->exit_action, &QAction::triggered, this, &MainWindow::exit_triggered); + connect(ui->export_cliplist_clip_multitrack_action, &QAction::triggered, this, &MainWindow::export_cliplist_clip_multitrack_triggered); connect(ui->manual_action, &QAction::triggered, this, &MainWindow::manual_triggered); connect(ui->about_action, &QAction::triggered, this, &MainWindow::about_triggered); @@ -754,6 +757,30 @@ void MainWindow::exit_triggered() close(); } +void MainWindow::export_cliplist_clip_multitrack_triggered() +{ + QItemSelectionModel *selected = ui->clip_list->selectionModel(); + if (!selected->hasSelection()) { + QMessageBox msgbox; + msgbox.setText("No clip selected in the clip list. Select one and try exporting again."); + msgbox.exec(); + return; + } + + QModelIndex index = selected->currentIndex(); + Clip clip = *cliplist_clips->clip(index.row()); + QString filename = QFileDialog::getSaveFileName(this, + "Export multitrack clip", QString(), tr("Matroska video files (*.mkv)")); + if (filename.isNull()) { + // Cancel. + return; + } + if (!filename.endsWith(".mkv")) { + filename += ".mkv"; + } + export_multitrack_clip(filename.toStdString(), clip); +} + void MainWindow::manual_triggered() { if (!QDesktopServices::openUrl(QUrl("https://nageru.sesse.net/doc/"))) { diff --git a/futatabi/mainwindow.h b/futatabi/mainwindow.h index 57814b3..a0c938d 100644 --- a/futatabi/mainwindow.h +++ b/futatabi/mainwindow.h @@ -102,6 +102,7 @@ private: void report_disk_space(off_t free_bytes, double estimated_seconds_left); void exit_triggered(); + void export_cliplist_clip_multitrack_triggered(); void manual_triggered(); void about_triggered(); diff --git a/futatabi/mainwindow.ui b/futatabi/mainwindow.ui index 880a451..0af53fe 100644 --- a/futatabi/mainwindow.ui +++ b/futatabi/mainwindow.ui @@ -450,6 +450,13 @@ &File + + + &Export + + + + @@ -477,6 +484,11 @@ &About Futatabi… + + + Selected clip list clip as raw &multitrack… + + diff --git a/meson.build b/meson.build index fca2df9..9ce748c 100644 --- a/meson.build +++ b/meson.build @@ -297,6 +297,7 @@ futatabi_srcs = ['futatabi/flow.cpp', 'futatabi/gpu_timers.cpp'] futatabi_srcs += ['futatabi/main.cpp', 'futatabi/player.cpp', 'futatabi/video_stream.cpp', 'futatabi/chroma_subsampler.cpp'] futatabi_srcs += ['futatabi/vaapi_jpeg_decoder.cpp', 'futatabi/db.cpp', 'futatabi/ycbcr_converter.cpp', 'futatabi/flags.cpp'] futatabi_srcs += ['futatabi/mainwindow.cpp', 'futatabi/jpeg_frame_view.cpp', 'futatabi/clip_list.cpp', 'futatabi/frame_on_disk.cpp'] +futatabi_srcs += ['futatabi/export.cpp'] futatabi_srcs += moc_files futatabi_srcs += proto_generated -- 2.39.2