X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=futatabi%2Fexport.cpp;h=1b7c59c90ae75f853b1d9eaf6b7f27a190d06996;hb=e0cb348ca42ae7057f8f5acee92a23e7eb26075f;hp=bed2643547a03335662bbda0f60dec36cd5fa981;hpb=5198acefd13b91a7953f1db1370ce7d434132472;p=nageru diff --git a/futatabi/export.cpp b/futatabi/export.cpp index bed2643..1b7c59c 100644 --- a/futatabi/export.cpp +++ b/futatabi/export.cpp @@ -1,17 +1,19 @@ +#include "export.h" + #include "clip_list.h" #include "defs.h" -#include "export.h" #include "flags.h" #include "frame_on_disk.h" +#include "player.h" #include "shared/ffmpeg_raii.h" +#include "shared/shared_defs.h" #include "shared/timebase.h" #include #include - -#include - +#include #include +#include extern "C" { #include @@ -22,22 +24,22 @@ using namespace std; namespace { // Only used in export_cliplist_clip_multitrack_triggered. -struct BufferedJPEG { +struct BufferedFrame { int64_t pts; - unsigned stream_idx; - string jpeg; + unsigned video_stream_idx; + string data; }; -bool write_buffered_jpegs(AVFormatContext *avctx, const vector &buffered_jpegs) +bool write_buffered_frames(AVFormatContext *avctx, const vector &buffered_frames) { - for (const BufferedJPEG &jpeg : buffered_jpegs) { + for (const BufferedFrame &frame : buffered_frames) { 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.stream_index = frame.video_stream_idx; + pkt.data = (uint8_t *)frame.data.data(); + pkt.size = frame.data.size(); + pkt.pts = frame.pts; + pkt.dts = frame.pts; pkt.flags = AV_PKT_FLAG_KEY; if (av_write_frame(avctx, &pkt) < 0) { @@ -98,14 +100,14 @@ void export_multitrack_clip(const string &filename, const Clip &clip) // 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; + 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); + abort(); } - avstream_video->time_base = AVRational{1, TIMEBASE}; + 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. @@ -122,6 +124,23 @@ void export_multitrack_clip(const string &filename, const Clip &clip) video_streams.push_back(avstream_video); } + // Similar, for audio streams. + vector audio_streams; + for (unsigned stream_idx = 0; stream_idx <= last_stream_idx; ++stream_idx) { + AVStream *avstream_audio = avformat_new_stream(avctx, nullptr); + if (avstream_audio == nullptr) { + fprintf(stderr, "avformat_new_stream() failed\n"); + abort(); + } + avstream_audio->time_base = AVRational{ 1, TIMEBASE }; + avstream_audio->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; + avstream_audio->codecpar->codec_id = AV_CODEC_ID_PCM_S32LE; + avstream_audio->codecpar->channel_layout = AV_CH_LAYOUT_STEREO; + avstream_audio->codecpar->channels = 2; + avstream_audio->codecpar->sample_rate = OUTPUT_FREQUENCY; + audio_streams.push_back(avstream_audio); + } + if (avformat_write_header(avctx, nullptr) < 0) { QMessageBox msgbox; msgbox.setText("Writing header failed"); @@ -139,7 +158,7 @@ void export_multitrack_clip(const string &filename, const Clip &clip) // 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; + vector buffered_frames; size_t frames_written = 0; while (num_streams_with_frames_left > 0) { // Find the stream with the lowest frame. Lower stream indexes win. @@ -162,35 +181,103 @@ void export_multitrack_clip(const string &filename, const Clip &clip) --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)) { + + FrameReader::Frame frame = readers[first_frame_stream_idx].read_frame(first_frame, /*read_audio=*/true); + + // Write audio. (Before video, since that's what we expect on input.) + if (!frame.audio.empty()) { + unsigned audio_stream_idx = first_frame_stream_idx + video_streams.size(); + int64_t scaled_audio_pts = av_rescale_q(first_frame.pts, AVRational{ 1, TIMEBASE }, + audio_streams[first_frame_stream_idx]->time_base); + buffered_frames.emplace_back(BufferedFrame{ scaled_audio_pts, audio_stream_idx, std::move(frame.audio) }); + } + + // Write video. + unsigned video_stream_idx = first_frame_stream_idx; + int64_t scaled_video_pts = av_rescale_q(first_frame.pts, AVRational{ 1, TIMEBASE }, + video_streams[first_frame_stream_idx]->time_base); + buffered_frames.emplace_back(BufferedFrame{ scaled_video_pts, video_stream_idx, std::move(frame.video) }); + + // Flush to disk if required. + if (buffered_frames.size() >= 1000) { + if (!write_buffered_frames(avctx, buffered_frames)) { QMessageBox msgbox; msgbox.setText("Writing frames failed"); msgbox.exec(); unlink(filename.c_str()); return; } - frames_written += buffered_jpegs.size(); + frames_written += buffered_frames.size(); progress.setValue(frames_written); - buffered_jpegs.clear(); + buffered_frames.clear(); } if (progress.wasCanceled()) { unlink(filename.c_str()); return; - } + } } - if (!write_buffered_jpegs(avctx, buffered_jpegs)) { + if (!write_buffered_frames(avctx, buffered_frames)) { QMessageBox msgbox; msgbox.setText("Writing frames failed"); msgbox.exec(); unlink(filename.c_str()); return; } - frames_written += buffered_jpegs.size(); + frames_written += buffered_frames.size(); progress.setValue(frames_written); } + +void export_interpolated_clip(const string &filename, const vector &clips) +{ + 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; + } + + QProgressDialog progress(QString::fromStdString("Exporting to " + filename + "..."), "Abort", 0, 1); + progress.setWindowTitle("Futatabi"); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(1000); + progress.setMaximum(100000); + progress.setValue(0); + + vector clips_with_id; + for (const Clip &clip : clips) { + clips_with_id.emplace_back(ClipWithID{ clip, 0 }); + } + TimeRemaining total_length = compute_total_time(clips_with_id); + + promise done_promise; + future done = done_promise.get_future(); + std::atomic current_value{ 0.0 }; + + Player player(/*destination=*/nullptr, Player::FILE_STREAM_OUTPUT, closer.release()); + player.set_done_callback([&done_promise] { + done_promise.set_value(); + }); + player.set_progress_callback([¤t_value, total_length](const std::map &player_progress, TimeRemaining time_remaining) { + current_value = 1.0 - time_remaining.t / total_length.t; // Nothing to do about the infinite clips. + }); + player.play(clips_with_id); + while (done.wait_for(std::chrono::milliseconds(100)) != future_status::ready && !progress.wasCanceled()) { + progress.setValue(lrint(100000.0 * current_value)); + } + if (progress.wasCanceled()) { + unlink(filename.c_str()); + // Destroying player on scope exit will abort the render job. + } +}