Merge branch 'master' into nodeview-redux
This commit is contained in:
@@ -44,6 +44,8 @@ namespace olive {
|
||||
class Decoder;
|
||||
using DecoderPtr = std::shared_ptr<Decoder>;
|
||||
|
||||
#define DECODER_DEFAULT_DESTRUCTOR(x) virtual ~x() override {CloseInternal();}
|
||||
|
||||
/**
|
||||
* @brief A decoder's is the main class for bringing external media into Olive
|
||||
*
|
||||
|
||||
+19
-1
@@ -91,7 +91,8 @@ EncodingParams::EncodingParams() :
|
||||
video_threads_(0),
|
||||
video_is_image_sequence_(false),
|
||||
audio_enabled_(false),
|
||||
audio_bit_rate_(0)
|
||||
audio_bit_rate_(0),
|
||||
subtitles_enabled_(false)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -114,6 +115,12 @@ void EncodingParams::EnableAudio(const AudioParams &audio_params, const ExportCo
|
||||
audio_codec_ = acodec;
|
||||
}
|
||||
|
||||
void EncodingParams::EnableSubtitles(const ExportCodec::Codec &scodec)
|
||||
{
|
||||
subtitles_enabled_ = true;
|
||||
subtitles_codec_ = scodec;
|
||||
}
|
||||
|
||||
void EncodingParams::set_video_option(const QString &key, const QString &value)
|
||||
{
|
||||
video_opts_.insert(key, value);
|
||||
@@ -219,6 +226,16 @@ const AudioParams &EncodingParams::audio_params() const
|
||||
return audio_params_;
|
||||
}
|
||||
|
||||
bool EncodingParams::subtitles_enabled() const
|
||||
{
|
||||
return subtitles_enabled_;
|
||||
}
|
||||
|
||||
ExportCodec::Codec EncodingParams::subtitles_codec() const
|
||||
{
|
||||
return subtitles_codec_;
|
||||
}
|
||||
|
||||
const rational &EncodingParams::GetExportLength() const
|
||||
{
|
||||
return export_length_;
|
||||
@@ -310,6 +327,7 @@ Encoder::Type Encoder::GetTypeFromFormat(ExportFormat::Format f)
|
||||
case ExportFormat::kFormatFLAC:
|
||||
case ExportFormat::kFormatOgg:
|
||||
case ExportFormat::kFormatWebM:
|
||||
case ExportFormat::kFormatSRT:
|
||||
return kEncoderTypeFFmpeg;
|
||||
case ExportFormat::kFormatOpenEXR:
|
||||
case ExportFormat::kFormatPNG:
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
#include "codec/frame.h"
|
||||
#include "codec/samplebuffer.h"
|
||||
#include "common/timerange.h"
|
||||
#include "node/block/subtitle/subtitle.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/subtitleparams.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -47,6 +49,7 @@ public:
|
||||
|
||||
void EnableVideo(const VideoParams& video_params, const ExportCodec::Codec& vcodec);
|
||||
void EnableAudio(const AudioParams& audio_params, const ExportCodec::Codec &acodec);
|
||||
void EnableSubtitles(const ExportCodec::Codec &scodec);
|
||||
|
||||
void set_video_option(const QString& key, const QString& value);
|
||||
void set_video_bit_rate(const int64_t& rate);
|
||||
@@ -91,6 +94,9 @@ public:
|
||||
audio_bit_rate_ = b;
|
||||
}
|
||||
|
||||
bool subtitles_enabled() const;
|
||||
ExportCodec::Codec subtitles_codec() const;
|
||||
|
||||
const rational& GetExportLength() const;
|
||||
void SetExportLength(const rational& GetExportLength);
|
||||
|
||||
@@ -116,6 +122,9 @@ private:
|
||||
AudioParams audio_params_;
|
||||
int64_t audio_bit_rate_;
|
||||
|
||||
bool subtitles_enabled_;
|
||||
ExportCodec::Codec subtitles_codec_;
|
||||
|
||||
rational export_length_;
|
||||
|
||||
};
|
||||
@@ -174,6 +183,7 @@ public slots:
|
||||
|
||||
virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) = 0;
|
||||
virtual bool WriteAudio(olive::SampleBufferPtr audio) = 0;
|
||||
virtual bool WriteSubtitle(const SubtitleBlock *sub_block) = 0;
|
||||
|
||||
virtual void Close() = 0;
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ QString ExportCodec::GetCodecName(ExportCodec::Codec c)
|
||||
return tr("Vorbis");
|
||||
case kCodecVP9:
|
||||
return tr("VP9");
|
||||
case kCodecSRT:
|
||||
return tr("SubRip SRT");
|
||||
case kCodecCount:
|
||||
break;
|
||||
}
|
||||
@@ -82,6 +84,7 @@ bool ExportCodec::IsCodecAStillImage(ExportCodec::Codec c)
|
||||
case kCodecOpus:
|
||||
case kCodecFLAC:
|
||||
case kCodecVP9:
|
||||
case kCodecSRT:
|
||||
return false;
|
||||
case kCodecOpenEXR:
|
||||
case kCodecPNG:
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <QString>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "render/subtitleparams.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
@@ -52,6 +53,9 @@ public:
|
||||
kCodecVorbis,
|
||||
kCodecFLAC,
|
||||
|
||||
// Subtitle codecs
|
||||
kCodecSRT,
|
||||
|
||||
kCodecCount
|
||||
};
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ QString ExportFormat::GetName(olive::ExportFormat::Format f)
|
||||
return tr("Ogg");
|
||||
case kFormatWebM:
|
||||
return tr("WebM");
|
||||
case kFormatSRT:
|
||||
return tr("SubRip SRT");
|
||||
|
||||
case kFormatCount:
|
||||
break;
|
||||
@@ -90,6 +92,8 @@ QString ExportFormat::GetExtension(ExportFormat::Format f)
|
||||
return QStringLiteral("ogg");
|
||||
case kFormatWebM:
|
||||
return QStringLiteral("webm");
|
||||
case kFormatSRT:
|
||||
return QStringLiteral("srt");
|
||||
case kFormatCount:
|
||||
break;
|
||||
}
|
||||
@@ -121,7 +125,7 @@ QList<ExportCodec::Codec> ExportFormat::GetVideoCodecs(ExportFormat::Format f)
|
||||
case kFormatAIFF:
|
||||
case kFormatMP3:
|
||||
case kFormatFLAC:
|
||||
return {};
|
||||
case kFormatSRT:
|
||||
case kFormatCount:
|
||||
break;
|
||||
}
|
||||
@@ -132,22 +136,19 @@ QList<ExportCodec::Codec> ExportFormat::GetVideoCodecs(ExportFormat::Format f)
|
||||
QList<ExportCodec::Codec> ExportFormat::GetAudioCodecs(ExportFormat::Format f)
|
||||
{
|
||||
switch (f) {
|
||||
// Video/audio formats
|
||||
case kFormatDNxHD:
|
||||
return {ExportCodec::kCodecPCM};
|
||||
case kFormatMatroska:
|
||||
return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus};
|
||||
case kFormatMPEG4:
|
||||
return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM};
|
||||
return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3};
|
||||
case kFormatQuickTime:
|
||||
return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM};
|
||||
case kFormatWebM:
|
||||
return {ExportCodec::kCodecAAC, ExportCodec::kCodecMP2, ExportCodec::kCodecMP3, ExportCodec::kCodecPCM, ExportCodec::kCodecVorbis, ExportCodec::kCodecOpus};
|
||||
case kFormatOpenEXR:
|
||||
case kFormatPNG:
|
||||
case kFormatTIFF:
|
||||
return {};
|
||||
|
||||
|
||||
// Audio only formats
|
||||
case kFormatWAV:
|
||||
return {ExportCodec::kCodecPCM};
|
||||
case kFormatAIFF:
|
||||
@@ -159,9 +160,39 @@ QList<ExportCodec::Codec> ExportFormat::GetAudioCodecs(ExportFormat::Format f)
|
||||
case kFormatOgg:
|
||||
return {ExportCodec::kCodecOpus, ExportCodec::kCodecVorbis, ExportCodec::kCodecPCM};
|
||||
|
||||
|
||||
// Video only formats
|
||||
case kFormatOpenEXR:
|
||||
case kFormatPNG:
|
||||
case kFormatTIFF:
|
||||
case kFormatSRT:
|
||||
case kFormatCount:
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QList<ExportCodec::Codec> ExportFormat::GetSubtitleCodecs(Format f)
|
||||
{
|
||||
switch (f) {
|
||||
case kFormatDNxHD:
|
||||
case kFormatMPEG4:
|
||||
case kFormatOpenEXR:
|
||||
case kFormatQuickTime:
|
||||
case kFormatPNG:
|
||||
case kFormatTIFF:
|
||||
case kFormatWAV:
|
||||
case kFormatAIFF:
|
||||
case kFormatMP3:
|
||||
case kFormatFLAC:
|
||||
case kFormatOgg:
|
||||
case kFormatWebM:
|
||||
case kFormatCount:
|
||||
break;
|
||||
case kFormatMatroska:
|
||||
case kFormatSRT:
|
||||
return {ExportCodec::kCodecSRT};
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
kFormatFLAC,
|
||||
kFormatOgg,
|
||||
kFormatWebM,
|
||||
kFormatSRT,
|
||||
|
||||
kFormatCount
|
||||
};
|
||||
@@ -55,6 +56,7 @@ public:
|
||||
static QString GetExtension(Format f);
|
||||
static QList<ExportCodec::Codec> GetVideoCodecs(ExportFormat::Format f);
|
||||
static QList<ExportCodec::Codec> GetAudioCodecs(ExportFormat::Format f);
|
||||
static QList<ExportCodec::Codec> GetSubtitleCodecs(ExportFormat::Format f);
|
||||
|
||||
static QStringList GetPixelFormatsForCodec(Format f, ExportCodec::Codec c);
|
||||
|
||||
|
||||
@@ -60,11 +60,6 @@ FFmpegDecoder::FFmpegDecoder() :
|
||||
{
|
||||
}
|
||||
|
||||
FFmpegDecoder::~FFmpegDecoder()
|
||||
{
|
||||
CloseInternal();
|
||||
}
|
||||
|
||||
bool FFmpegDecoder::OpenInternal()
|
||||
{
|
||||
if (instance_.Open(stream().filename().toUtf8(), stream().stream())) {
|
||||
@@ -277,7 +272,8 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn
|
||||
|
||||
if (decoder
|
||||
&& (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO
|
||||
|| avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)) {
|
||||
|| avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO
|
||||
|| avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)) {
|
||||
|
||||
if (avstream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
|
||||
@@ -372,7 +368,7 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn
|
||||
|
||||
desc.AddVideoStream(stream);
|
||||
|
||||
} else {
|
||||
} else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
|
||||
// Create an audio stream object
|
||||
uint64_t channel_layout = avstream->codecpar->channel_layout;
|
||||
@@ -417,6 +413,10 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn
|
||||
stream.set_duration(avstream->duration);
|
||||
desc.AddAudioStream(stream);
|
||||
|
||||
} else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
|
||||
|
||||
qDebug() << "Subtitle probing: Stub";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -654,15 +654,16 @@ FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, c
|
||||
{
|
||||
int64_t target_ts = GetTimeInTimebaseUnits(time, instance_.avstream()->time_base, instance_.avstream()->start_time);
|
||||
|
||||
if (params.dst_interlacing == VideoParams::kInterlaceNone && params.src_interlacing != VideoParams::kInterlaceNone) {
|
||||
const int64_t min_seek = -instance_.avstream()->start_time;
|
||||
int64_t seek_ts = target_ts;
|
||||
bool still_seeking = false;
|
||||
|
||||
if (params.src_interlacing != VideoParams::kInterlaceNone) {
|
||||
// If we are de-interlacing, the timebase is doubled because we get one frame per field, so we
|
||||
// double the target timestamp too
|
||||
target_ts *= 2;
|
||||
}
|
||||
|
||||
int64_t seek_ts = target_ts;
|
||||
bool still_seeking = false;
|
||||
|
||||
if (time != kAnyTimecode) {
|
||||
// If the frame wasn't in the frame cache, see if this frame cache is too old to use
|
||||
if (cached_frames_.isEmpty()
|
||||
@@ -670,7 +671,7 @@ FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, c
|
||||
ClearFrameCache();
|
||||
|
||||
instance_.Seek(seek_ts);
|
||||
if (seek_ts == 0) {
|
||||
if (seek_ts == min_seek) {
|
||||
cache_at_zero_ = true;
|
||||
}
|
||||
|
||||
@@ -708,9 +709,9 @@ FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, c
|
||||
// We'll only be here if the frame cache was emptied earlier
|
||||
if (!cache_at_zero_ && (ret == AVERROR_EOF || working_frame->pts > target_ts)) {
|
||||
|
||||
seek_ts = qMax(static_cast<int64_t>(0), seek_ts - second_ts_);
|
||||
seek_ts = qMax(min_seek, seek_ts - second_ts_);
|
||||
instance_.Seek(seek_ts);
|
||||
if (seek_ts == 0) {
|
||||
if (seek_ts == min_seek) {
|
||||
cache_at_zero_ = true;
|
||||
}
|
||||
continue;
|
||||
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
FFmpegDecoder();
|
||||
|
||||
// Destructor
|
||||
virtual ~FFmpegDecoder() override;
|
||||
DECODER_DEFAULT_DESTRUCTOR(FFmpegDecoder)
|
||||
|
||||
virtual QString id() const override;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ extern "C" {
|
||||
#include <QFile>
|
||||
|
||||
#include "common/ffmpegutils.h"
|
||||
#include "common/timecodefunctions.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
@@ -83,6 +84,7 @@ QStringList FFmpegEncoder::GetPixelFormatsForCodec(ExportCodec::Codec c) const
|
||||
case ExportCodec::kCodecFLAC:
|
||||
case ExportCodec::kCodecOpus:
|
||||
case ExportCodec::kCodecVorbis:
|
||||
case ExportCodec::kCodecSRT:
|
||||
case ExportCodec::kCodecCount:
|
||||
// These are audio or invalid codecs and therefore have no pixel formats
|
||||
break;
|
||||
@@ -178,6 +180,13 @@ bool FFmpegEncoder::Open()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize a subtitle stream if it's enabled
|
||||
if (params().subtitles_enabled()) {
|
||||
if (!InitializeStream(AVMEDIA_TYPE_SUBTITLE, &subtitle_stream_, &subtitle_codec_ctx_, params().subtitles_codec())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
av_dump_format(fmt_ctx_, 0, filename_c_str, 1);
|
||||
|
||||
// Open output file for writing
|
||||
@@ -299,29 +308,26 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio)
|
||||
if (converted > 0) {
|
||||
// Split sample buffer into frames
|
||||
for (int i=0; i<converted; ) {
|
||||
int copy_offset = audio_frame_offset_;
|
||||
int frame_remaining_samples = audio_frame_->nb_samples - copy_offset;
|
||||
int frame_remaining_samples = audio_max_samples_ - audio_frame_offset_;
|
||||
int converted_remaining_samples = converted - i;
|
||||
|
||||
int copy_length = qMin(frame_remaining_samples, converted_remaining_samples);
|
||||
|
||||
av_samples_copy(audio_frame_->data, output_data, copy_offset, i,
|
||||
av_samples_copy(audio_frame_->data, output_data, audio_frame_offset_, i,
|
||||
copy_length,
|
||||
audio_frame_->channels, static_cast<AVSampleFormat>(audio_frame_->format));
|
||||
|
||||
if (copy_length != frame_remaining_samples && input_data) {
|
||||
// Frame didn't get all the samples it needed, save them for later
|
||||
audio_frame_offset_ += copy_length;
|
||||
} else {
|
||||
audio_frame_offset_ += copy_length;
|
||||
i += copy_length;
|
||||
|
||||
if (audio_frame_offset_ == audio_max_samples_ || (i == converted && !input_data)) {
|
||||
// Got all the samples we needed, write the frame
|
||||
audio_frame_->pts = audio_write_count_;
|
||||
|
||||
WriteAVFrame(audio_frame_, audio_codec_ctx_, audio_stream_);
|
||||
audio_write_count_ += audio_frame_->nb_samples;
|
||||
audio_write_count_ += audio_frame_offset_;
|
||||
audio_frame_offset_ = 0;
|
||||
}
|
||||
|
||||
i += copy_length;
|
||||
}
|
||||
} else if (converted < 0) {
|
||||
FFmpegError(tr("Failed to resample audio"), converted);
|
||||
@@ -347,6 +353,82 @@ bool FFmpegEncoder::WriteAudio(SampleBufferPtr audio)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString GetAssTime(const rational &time)
|
||||
{
|
||||
int64_t total_centiseconds = qRound64(time.toDouble() * 100);
|
||||
|
||||
int64_t cs = total_centiseconds % 100;
|
||||
int64_t ss = (total_centiseconds / 100) % 60;
|
||||
int64_t mm = (total_centiseconds / 6000) % 60;
|
||||
int64_t hh = total_centiseconds / 360000;
|
||||
|
||||
return QStringLiteral("%1:%2:%3.%4").arg(
|
||||
QString::number(hh),
|
||||
QStringLiteral("%1").arg(mm, 2, 10, QLatin1Char('0')),
|
||||
QStringLiteral("%1").arg(ss, 2, 10, QLatin1Char('0')),
|
||||
QStringLiteral("%1").arg(cs, 2, 10, QLatin1Char('0'))
|
||||
);
|
||||
}
|
||||
|
||||
bool FFmpegEncoder::WriteSubtitle(const SubtitleBlock *sub_block)
|
||||
{
|
||||
AVSubtitle subtitle;
|
||||
memset(&subtitle, 0, sizeof(subtitle));
|
||||
|
||||
AVSubtitleRect rect;
|
||||
memset(&rect, 0, sizeof(rect));
|
||||
|
||||
QString ass_line = QStringLiteral("Dialogue: 0,%1,%2,Default,,0,0,0,,%3").arg(
|
||||
GetAssTime(sub_block->in()),
|
||||
GetAssTime(sub_block->out()),
|
||||
sub_block->GetText()
|
||||
);
|
||||
|
||||
QByteArray utf8_sub = sub_block->GetText().toUtf8();
|
||||
QByteArray utf8_ass = ass_line.toUtf8();
|
||||
|
||||
rect.type = SUBTITLE_ASS;
|
||||
rect.text = utf8_sub.data();
|
||||
rect.ass = utf8_ass.data();
|
||||
|
||||
AVSubtitleRect *rect_array = ▭
|
||||
subtitle.num_rects = 1;
|
||||
subtitle.rects = &rect_array;
|
||||
|
||||
subtitle.pts = Timecode::time_to_timestamp(sub_block->in(), av_get_time_base_q(), true);
|
||||
subtitle.end_display_time = qRound64(sub_block->length().toDouble() * 1000);
|
||||
|
||||
QVector<uint8_t> out_buf(1024 * 1024);
|
||||
|
||||
int sub_sz = avcodec_encode_subtitle(subtitle_codec_ctx_, out_buf.data(), out_buf.size(), &subtitle);
|
||||
if (sub_sz < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AVPacket *pkt = av_packet_alloc();
|
||||
|
||||
pkt->stream_index = subtitle_stream_->index;
|
||||
pkt->data = out_buf.data();
|
||||
pkt->size = sub_sz;
|
||||
pkt->pts = subtitle.pts;
|
||||
pkt->duration = subtitle.end_display_time;
|
||||
pkt->dts = pkt->pts;
|
||||
av_packet_rescale_ts(pkt, av_get_time_base_q(), subtitle_stream_->time_base);
|
||||
|
||||
int err = av_interleaved_write_frame(fmt_ctx_, pkt);
|
||||
bool ret = true;
|
||||
|
||||
if (err < 0) {
|
||||
FFmpegError(tr("Failed to write interleaved packet"), err);
|
||||
ret = false;
|
||||
}
|
||||
|
||||
av_packet_free(&pkt);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
void FFmpegEncoder::WriteAudio(AudioParams pcm_info, QIODevice* file)
|
||||
{
|
||||
@@ -464,7 +546,7 @@ void FFmpegEncoder::FFmpegError(const QString& context, int error_code)
|
||||
char err[1024];
|
||||
av_strerror(error_code, err, 1024);
|
||||
|
||||
QString formatted_err = tr("%1: %2 %3").arg(context, formatted_err, QString::number(error_code));
|
||||
QString formatted_err = tr("%1: %2 %3").arg(context, err, QString::number(error_code));
|
||||
qDebug() << formatted_err;
|
||||
SetError(formatted_err);
|
||||
}
|
||||
@@ -500,7 +582,11 @@ bool FFmpegEncoder::WriteAVFrame(AVFrame *frame, AVCodecContext* codec_ctx, AVSt
|
||||
av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
|
||||
|
||||
// Write packet to file
|
||||
av_interleaved_write_frame(fmt_ctx_, pkt);
|
||||
error_code = av_interleaved_write_frame(fmt_ctx_, pkt);
|
||||
if (error_code < 0) {
|
||||
FFmpegError(tr("Failed to write interleaved packet"), error_code);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
// Unref packet in case we're getting another
|
||||
av_packet_unref(pkt);
|
||||
@@ -516,8 +602,8 @@ fail:
|
||||
|
||||
bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AVCodecContext** codec_ctx_ptr, const ExportCodec::Codec& codec)
|
||||
{
|
||||
if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO) {
|
||||
SetError(tr("Cannot initialize a stream that is not a video or audio type"));
|
||||
if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO && type != AVMEDIA_TYPE_SUBTITLE) {
|
||||
SetError(tr("Cannot initialize a stream that is not a video, audio, or subtitle type"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -570,6 +656,9 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV
|
||||
case ExportCodec::kCodecFLAC:
|
||||
codec_id = AV_CODEC_ID_FLAC;
|
||||
break;
|
||||
case ExportCodec::kCodecSRT:
|
||||
codec_id = AV_CODEC_ID_SUBRIP;
|
||||
break;
|
||||
case ExportCodec::kCodecCount:
|
||||
break;
|
||||
}
|
||||
@@ -648,7 +737,7 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
} else if (type == AVMEDIA_TYPE_AUDIO) {
|
||||
|
||||
// Assume audio stream
|
||||
codec_ctx->sample_rate = params().audio_params().sample_rate();
|
||||
@@ -661,6 +750,15 @@ bool FFmpegEncoder::InitializeStream(AVMediaType type, AVStream** stream_ptr, AV
|
||||
codec_ctx->bit_rate = params().audio_bit_rate();
|
||||
}
|
||||
|
||||
} else if (type == AVMEDIA_TYPE_SUBTITLE) {
|
||||
|
||||
codec_ctx->time_base = av_get_time_base_q();
|
||||
|
||||
QByteArray ass_header = SubtitleParams::GenerateASSHeader().toUtf8();
|
||||
codec_ctx->subtitle_header = new uint8_t[ass_header.size()];
|
||||
memcpy(codec_ctx->subtitle_header, ass_header.constData(), ass_header.size());
|
||||
codec_ctx->subtitle_header_size = ass_header.size();
|
||||
|
||||
}
|
||||
|
||||
if (!SetupCodecContext(stream, codec_ctx, encoder)) {
|
||||
@@ -734,6 +832,15 @@ void FFmpegEncoder::FlushEncoders()
|
||||
|
||||
FlushCodecCtx(audio_codec_ctx_, audio_stream_);
|
||||
}
|
||||
|
||||
if (fmt_ctx_) {
|
||||
if (fmt_ctx_->oformat->flags & AVFMT_ALLOW_FLUSH) {
|
||||
int r = av_interleaved_write_frame(fmt_ctx_, nullptr);
|
||||
if (r < 0) {
|
||||
FFmpegError(tr("Failed to write interleaved packet"), r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FFmpegEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream* stream)
|
||||
@@ -751,7 +858,11 @@ void FFmpegEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream* stream)
|
||||
|
||||
pkt->stream_index = stream->index;
|
||||
av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
|
||||
av_interleaved_write_frame(fmt_ctx_, pkt);
|
||||
int r = av_interleaved_write_frame(fmt_ctx_, pkt);
|
||||
if (r < 0) {
|
||||
FFmpegError(tr("Failed to write interleaved packet"), r);
|
||||
break;
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
} while (error_code >= 0);
|
||||
|
||||
@@ -784,15 +895,15 @@ bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio)
|
||||
return false;
|
||||
}
|
||||
|
||||
int max_frame_samples = audio_codec_ctx_->frame_size;
|
||||
if (!max_frame_samples) {
|
||||
audio_max_samples_ = audio_codec_ctx_->frame_size;
|
||||
if (!audio_max_samples_) {
|
||||
// If not, use another frame size
|
||||
if (params().video_enabled()) {
|
||||
// If we're encoding video, use enough samples to cover roughly one frame of video
|
||||
max_frame_samples = params().audio_params().time_to_samples(params().video_params().frame_rate_as_time_base());
|
||||
audio_max_samples_ = params().audio_params().time_to_samples(params().video_params().frame_rate_as_time_base());
|
||||
} else {
|
||||
// If no video, just use an arbitrary number
|
||||
max_frame_samples = 256;
|
||||
audio_max_samples_ = 256;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +914,7 @@ bool FFmpegEncoder::InitializeResampleContext(SampleBufferPtr audio)
|
||||
|
||||
audio_frame_->channel_layout = audio_codec_ctx_->channel_layout;
|
||||
audio_frame_->format = audio_codec_ctx_->sample_fmt;
|
||||
audio_frame_->nb_samples = max_frame_samples;
|
||||
audio_frame_->nb_samples = audio_max_samples_;
|
||||
|
||||
err = av_frame_get_buffer(audio_frame_, 0);
|
||||
if (err < 0) {
|
||||
|
||||
@@ -46,6 +46,8 @@ public:
|
||||
|
||||
virtual bool WriteAudio(olive::SampleBufferPtr audio) override;
|
||||
|
||||
virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override;
|
||||
|
||||
virtual void Close() override;
|
||||
|
||||
virtual VideoParams::Format GetDesiredPixelFormat() const override
|
||||
@@ -87,9 +89,13 @@ private:
|
||||
AVCodecContext* audio_codec_ctx_;
|
||||
SwrContext* audio_resample_ctx_;
|
||||
AVFrame* audio_frame_;
|
||||
int audio_max_samples_;
|
||||
int audio_frame_offset_;
|
||||
int audio_write_count_;
|
||||
|
||||
AVStream* subtitle_stream_;
|
||||
AVCodecContext* subtitle_codec_ctx_;
|
||||
|
||||
bool open_;
|
||||
|
||||
};
|
||||
|
||||
@@ -41,11 +41,6 @@ OIIODecoder::OIIODecoder() :
|
||||
{
|
||||
}
|
||||
|
||||
OIIODecoder::~OIIODecoder()
|
||||
{
|
||||
CloseInternal();
|
||||
}
|
||||
|
||||
QString OIIODecoder::id() const
|
||||
{
|
||||
return QStringLiteral("oiio");
|
||||
|
||||
@@ -34,7 +34,7 @@ class OIIODecoder : public Decoder
|
||||
public:
|
||||
OIIODecoder();
|
||||
|
||||
virtual ~OIIODecoder() override;
|
||||
DECODER_DEFAULT_DESTRUCTOR(OIIODecoder)
|
||||
|
||||
virtual QString id() const override;
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ bool OIIOEncoder::WriteAudio(SampleBufferPtr audio)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OIIOEncoder::WriteSubtitle(const SubtitleBlock *sub_block)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void OIIOEncoder::Close()
|
||||
{
|
||||
// Do nothing
|
||||
|
||||
@@ -36,6 +36,7 @@ public slots:
|
||||
|
||||
virtual bool WriteFrame(olive::FramePtr frame, olive::rational time) override;
|
||||
virtual bool WriteAudio(SampleBufferPtr audio) override;
|
||||
virtual bool WriteSubtitle(const SubtitleBlock *sub_block) override;
|
||||
|
||||
virtual void Close() override;
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ add_subdirectory(keyframeproperties)
|
||||
add_subdirectory(preferences)
|
||||
add_subdirectory(progress)
|
||||
add_subdirectory(rendercancel)
|
||||
add_subdirectory(richtext)
|
||||
add_subdirectory(sequence)
|
||||
add_subdirectory(task)
|
||||
add_subdirectory(text)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
|
||||
@@ -18,13 +18,15 @@ add_subdirectory(codec)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
dialog/export/export.h
|
||||
dialog/export/export.cpp
|
||||
dialog/export/exportadvancedvideodialog.h
|
||||
dialog/export/export.h
|
||||
dialog/export/exportadvancedvideodialog.cpp
|
||||
dialog/export/exportaudiotab.h
|
||||
dialog/export/exportadvancedvideodialog.h
|
||||
dialog/export/exportaudiotab.cpp
|
||||
dialog/export/exportvideotab.h
|
||||
dialog/export/exportaudiotab.h
|
||||
dialog/export/exportsubtitlestab.cpp
|
||||
dialog/export/exportsubtitlestab.h
|
||||
dialog/export/exportvideotab.cpp
|
||||
dialog/export/exportvideotab.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
@@ -124,29 +124,30 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, QWidget *parent) :
|
||||
QHBoxLayout* av_enabled_layout = new QHBoxLayout();
|
||||
|
||||
video_enabled_ = new QCheckBox(tr("Export Video"));
|
||||
video_enabled_->setChecked(true);
|
||||
av_enabled_layout->addWidget(video_enabled_);
|
||||
|
||||
audio_enabled_ = new QCheckBox(tr("Export Audio"));
|
||||
audio_enabled_->setChecked(true);
|
||||
av_enabled_layout->addWidget(audio_enabled_);
|
||||
|
||||
subtitles_enabled_ = new QCheckBox(tr("Export Subtitle"));
|
||||
av_enabled_layout->addWidget(subtitles_enabled_);
|
||||
|
||||
preferences_layout->addLayout(av_enabled_layout, row, 0, 1, 4);
|
||||
|
||||
row++;
|
||||
|
||||
preferences_tabs_ = new QTabWidget();
|
||||
QScrollArea* video_area = new QScrollArea();
|
||||
|
||||
color_manager_ = viewer_node_->project()->color_manager();
|
||||
video_tab_ = new ExportVideoTab(color_manager_);
|
||||
video_area->setWidgetResizable(true);
|
||||
video_area->setWidget(video_tab_);
|
||||
preferences_tabs_->addTab(video_area, tr("Video"));
|
||||
QScrollArea* audio_area = new QScrollArea();
|
||||
AddPreferencesTab(video_tab_, tr("Video"));
|
||||
|
||||
audio_tab_ = new ExportAudioTab();
|
||||
audio_area->setWidgetResizable(true);
|
||||
audio_area->setWidget(audio_tab_);
|
||||
preferences_tabs_->addTab(audio_area, tr("Audio"));
|
||||
AddPreferencesTab(audio_tab_, tr("Audio"));
|
||||
|
||||
subtitle_tab_ = new ExportSubtitlesTab();
|
||||
AddPreferencesTab(subtitle_tab_, tr("Subtitles"));
|
||||
|
||||
preferences_layout->addWidget(preferences_tabs_, row, 0, 1, 4);
|
||||
|
||||
row++;
|
||||
@@ -268,9 +269,9 @@ rational ExportDialog::GetSelectedTimebase() const
|
||||
|
||||
void ExportDialog::StartExport()
|
||||
{
|
||||
if (!video_enabled_->isChecked() && !audio_enabled_->isChecked()) {
|
||||
if (!video_enabled_->isChecked() && !audio_enabled_->isChecked() && !subtitles_enabled_->isChecked()) {
|
||||
QtUtils::MessageBox(this, QMessageBox::Critical, tr("Invalid parameters"),
|
||||
tr("Both video and audio are disabled. There's nothing to export."));
|
||||
tr("Video, audio, and subtitles are disabled. There's nothing to export."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -392,6 +393,14 @@ void ExportDialog::closeEvent(QCloseEvent *e)
|
||||
QDialog::closeEvent(e);
|
||||
}
|
||||
|
||||
void ExportDialog::AddPreferencesTab(QWidget *inner_widget, const QString &title)
|
||||
{
|
||||
QScrollArea* scroll_area = new QScrollArea();
|
||||
scroll_area->setWidgetResizable(true);
|
||||
scroll_area->setWidget(inner_widget);
|
||||
preferences_tabs_->addTab(scroll_area, title);
|
||||
}
|
||||
|
||||
void ExportDialog::BrowseFilename()
|
||||
{
|
||||
ExportFormat::Format f = GetSelectedFormat();
|
||||
@@ -437,6 +446,10 @@ void ExportDialog::FormatChanged(int index)
|
||||
bool has_audio_codecs = audio_tab_->SetFormat(current_format);
|
||||
audio_enabled_->setChecked(has_audio_codecs);
|
||||
audio_enabled_->setEnabled(has_audio_codecs);
|
||||
|
||||
bool has_subtitle_codecs = subtitle_tab_->SetFormat(current_format);
|
||||
subtitles_enabled_->setChecked(has_subtitle_codecs);
|
||||
subtitles_enabled_->setEnabled(has_subtitle_codecs);
|
||||
}
|
||||
|
||||
void ExportDialog::ResolutionChanged()
|
||||
@@ -549,6 +562,10 @@ ExportParams ExportDialog::GenerateParams() const
|
||||
params.set_audio_bit_rate(audio_tab_->bit_rate_slider()->GetValue() * 1000);
|
||||
}
|
||||
|
||||
if (subtitles_enabled_->isChecked()) {
|
||||
params.EnableSubtitles(subtitle_tab_->GetSubtitleCodec());
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "codec/exportcodec.h"
|
||||
#include "codec/exportformat.h"
|
||||
#include "exportaudiotab.h"
|
||||
#include "exportsubtitlestab.h"
|
||||
#include "exportvideotab.h"
|
||||
#include "task/export/export.h"
|
||||
#include "widget/viewer/viewer.h"
|
||||
@@ -50,6 +51,8 @@ protected:
|
||||
virtual void closeEvent(QCloseEvent *e) override;
|
||||
|
||||
private:
|
||||
void AddPreferencesTab(QWidget *inner_widget, const QString &title);
|
||||
|
||||
void LoadPresets();
|
||||
void SetDefaultFilename();
|
||||
|
||||
@@ -75,6 +78,7 @@ private:
|
||||
|
||||
QCheckBox* video_enabled_;
|
||||
QCheckBox* audio_enabled_;
|
||||
QCheckBox* subtitles_enabled_;
|
||||
|
||||
ViewerWidget* preview_viewer_;
|
||||
QLineEdit* filename_edit_;
|
||||
@@ -82,6 +86,7 @@ private:
|
||||
|
||||
ExportVideoTab* video_tab_;
|
||||
ExportAudioTab* audio_tab_;
|
||||
ExportSubtitlesTab* subtitle_tab_;
|
||||
|
||||
double video_aspect_ratio_;
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "exportsubtitlestab.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
|
||||
namespace olive {
|
||||
|
||||
ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent) :
|
||||
QWidget(parent)
|
||||
{
|
||||
QVBoxLayout* outer_layout = new QVBoxLayout(this);
|
||||
|
||||
QGridLayout* layout = new QGridLayout();
|
||||
outer_layout->addLayout(layout);
|
||||
|
||||
int row = 0;
|
||||
|
||||
layout->addWidget(new QLabel(tr("Codec:")), row, 0);
|
||||
|
||||
codec_combobox_ = new QComboBox();
|
||||
layout->addWidget(codec_combobox_, row, 1);
|
||||
|
||||
outer_layout->addStretch();
|
||||
}
|
||||
|
||||
int ExportSubtitlesTab::SetFormat(ExportFormat::Format format)
|
||||
{
|
||||
auto scodecs = ExportFormat::GetSubtitleCodecs(format);
|
||||
setEnabled(!scodecs.isEmpty());
|
||||
codec_combobox_->clear();
|
||||
foreach (ExportCodec::Codec scodec, scodecs) {
|
||||
codec_combobox_->addItem(ExportCodec::GetCodecName(scodec), scodec);
|
||||
}
|
||||
return scodecs.size();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef EXPORTSUBTITLESTAB_H
|
||||
#define EXPORTSUBTITLESTAB_H
|
||||
|
||||
#include <QComboBox>
|
||||
|
||||
#include "codec/exportformat.h"
|
||||
#include "render/subtitleparams.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class ExportSubtitlesTab : public QWidget
|
||||
{
|
||||
public:
|
||||
ExportSubtitlesTab(QWidget *parent = nullptr);
|
||||
|
||||
int SetFormat(ExportFormat::Format format);
|
||||
|
||||
ExportCodec::Codec GetSubtitleCodec()
|
||||
{
|
||||
return static_cast<ExportCodec::Codec>(codec_combobox_->currentData().toInt());
|
||||
}
|
||||
|
||||
private:
|
||||
QComboBox *codec_combobox_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // EXPORTSUBTITLESTAB_H
|
||||
@@ -74,7 +74,7 @@ QWidget* ExportVideoTab::SetupResolutionSection()
|
||||
int row = 0;
|
||||
|
||||
QGroupBox* resolution_group = new QGroupBox();
|
||||
resolution_group->setTitle(tr("Basic"));
|
||||
resolution_group->setTitle(tr("General"));
|
||||
|
||||
QGridLayout* layout = new QGridLayout(resolution_group);
|
||||
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "richtext.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ui/icons/icons.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
RichTextDialog::RichTextDialog(QString start, QWidget* parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
|
||||
// Create toolbar
|
||||
QHBoxLayout* toolbar_layout = new QHBoxLayout();
|
||||
|
||||
bold_btn_ = CreateToolbarButton(tr("B"), tr("Bold"), {QStringLiteral("b"), QStringLiteral("strong")});
|
||||
toolbar_layout->addWidget(bold_btn_);
|
||||
italic_btn_ = CreateToolbarButton(tr("I"), tr("Italic"), {QStringLiteral("i"), QStringLiteral("em")});
|
||||
toolbar_layout->addWidget(italic_btn_);
|
||||
underline_btn_ = CreateToolbarButton(tr("U"), tr("Underline"), {QStringLiteral("u")});
|
||||
toolbar_layout->addWidget(underline_btn_);
|
||||
strikeout_btn_ = CreateToolbarButton(tr("S"), tr("Strikethrough"), {QStringLiteral("strike")});
|
||||
toolbar_layout->addWidget(strikeout_btn_);
|
||||
font_combo_ = new QFontComboBox();
|
||||
font_combo_->setToolTip(tr("Font Family"));
|
||||
toolbar_layout->addWidget(font_combo_);
|
||||
size_slider_ = new FloatSlider();
|
||||
size_slider_->SetMinimum(0.1);
|
||||
size_slider_->SetLadderElementCount(1);
|
||||
size_slider_->setToolTip(tr("Font Size"));
|
||||
toolbar_layout->addWidget(size_slider_);
|
||||
|
||||
toolbar_layout->addStretch();
|
||||
|
||||
left_align_btn_ = CreateToolbarButton(tr("L"), tr("Left Align"), {});
|
||||
toolbar_layout->addWidget(left_align_btn_);
|
||||
center_align_btn_ = CreateToolbarButton(tr("C"), tr("Center Align"), {});
|
||||
toolbar_layout->addWidget(center_align_btn_);
|
||||
right_align_btn_ = CreateToolbarButton(tr("R"), tr("Right Align"), {});
|
||||
toolbar_layout->addWidget(right_align_btn_);
|
||||
justify_align_btn_ = CreateToolbarButton(tr("J"), tr("Justify Align"), {});
|
||||
toolbar_layout->addWidget(justify_align_btn_);
|
||||
|
||||
layout->addLayout(toolbar_layout);
|
||||
|
||||
// Create text edit widget
|
||||
text_edit_ = new QTextEdit();
|
||||
text_edit_->setWordWrapMode(QTextOption::NoWrap);
|
||||
connect(text_edit_, &QTextEdit::cursorPositionChanged, this, &RichTextDialog::UpdateButtons);
|
||||
start.replace(QStringLiteral("<br>"), QStringLiteral("\n"));
|
||||
text_edit_->document()->setPlainText(start);
|
||||
layout->addWidget(text_edit_);
|
||||
|
||||
// Create buttons
|
||||
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &RichTextDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &RichTextDialog::reject);
|
||||
|
||||
// Connect font buttons
|
||||
/*
|
||||
connect(size_slider_, &FloatSlider::ValueChanged, text_edit_, &QTextEdit::setFontPointSize);
|
||||
connect(left_align_btn_, &QPushButton::clicked, this, [this](){
|
||||
text_edit_->setAlignment(Qt::AlignLeft);
|
||||
UpdateButtons();
|
||||
});
|
||||
connect(center_align_btn_, &QPushButton::clicked, this, [this](){
|
||||
text_edit_->setAlignment(Qt::AlignCenter);
|
||||
UpdateButtons();
|
||||
});
|
||||
connect(right_align_btn_, &QPushButton::clicked, this, [this](){
|
||||
text_edit_->setAlignment(Qt::AlignRight);
|
||||
UpdateButtons();
|
||||
});
|
||||
connect(justify_align_btn_, &QPushButton::clicked, this, [this](){
|
||||
text_edit_->setAlignment(Qt::AlignJustify);
|
||||
UpdateButtons();
|
||||
});
|
||||
|
||||
|
||||
connect(font_combo_, &QFontComboBox::currentTextChanged, this, [this](const QString& s){
|
||||
text_edit_->setFontFamily(s);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
QPushButton *RichTextDialog::CreateToolbarButton(const QString& label, const QString& tooltip, const QStringList &tags)
|
||||
{
|
||||
QPushButton* btn = new QPushButton(label);
|
||||
btn->setCheckable(true);
|
||||
btn->setToolTip(tooltip);
|
||||
btn->setFixedWidth(btn->sizeHint().height());
|
||||
|
||||
if (!tags.isEmpty()) {
|
||||
btn->setProperty("tag", tags);
|
||||
connect(btn, &QPushButton::clicked, this, &RichTextDialog::TagButtonToggled);
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
int SnapPositionOutsideTags(const QString& text, int pos)
|
||||
{
|
||||
// Look for closest opening bracket before position
|
||||
int opening_bracket_pos = text.lastIndexOf('<', pos - text.size() -1);
|
||||
|
||||
// Look for closest closing bracket before position
|
||||
int closing_bracket_pos = text.indexOf('>', opening_bracket_pos);
|
||||
|
||||
if (opening_bracket_pos > -1 && closing_bracket_pos >= pos) {
|
||||
// Must be inside an angle bracket, snap to closest position outside of bracket
|
||||
closing_bracket_pos++;
|
||||
|
||||
if (pos - opening_bracket_pos < closing_bracket_pos - pos) {
|
||||
// Closer to opening bracket pos
|
||||
return opening_bracket_pos;
|
||||
} else {
|
||||
return closing_bracket_pos;
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
void RichTextDialog::SetTags(const QStringList &t, bool enabled)
|
||||
{
|
||||
QString s = text_edit_->toPlainText();
|
||||
|
||||
int selection_start, selection_end;
|
||||
|
||||
{
|
||||
QTextCursor c = text_edit_->textCursor();
|
||||
|
||||
if (c.hasSelection()) {
|
||||
selection_start = SnapPositionOutsideTags(s, c.selectionStart());
|
||||
selection_end = SnapPositionOutsideTags(s, c.selectionEnd());
|
||||
|
||||
c.clearSelection();
|
||||
c.setPosition(selection_start, QTextCursor::MoveAnchor);
|
||||
c.setPosition(selection_end, QTextCursor::KeepAnchor);
|
||||
} else {
|
||||
selection_start = SnapPositionOutsideTags(s, c.position());
|
||||
selection_end = selection_start;
|
||||
c.setPosition(selection_start, QTextCursor::MoveAnchor);
|
||||
}
|
||||
|
||||
text_edit_->setTextCursor(c);
|
||||
}
|
||||
|
||||
QString open_tag = CreateOpeningTag(t.first());
|
||||
QString close_tag = CreateClosingTag(t.first());
|
||||
|
||||
// Insert tags
|
||||
QString new_text;
|
||||
|
||||
if (!enabled) {
|
||||
std::swap(open_tag, close_tag);
|
||||
}
|
||||
|
||||
bool open_tag_cancels_out = !QString::compare(s.mid(selection_start - close_tag.size(), close_tag.size()), close_tag, Qt::CaseInsensitive);
|
||||
bool close_tag_cancels_out = !QString::compare(s.mid(selection_end, open_tag.size()), open_tag, Qt::CaseInsensitive);
|
||||
|
||||
QString selected_text = text_edit_->textCursor().selectedText();
|
||||
|
||||
if (open_tag_cancels_out && close_tag_cancels_out) {
|
||||
|
||||
// Both tags cancel each other out, simply remove
|
||||
selection_start -= close_tag.size();
|
||||
|
||||
QTextCursor c = text_edit_->textCursor();
|
||||
c.clearSelection();
|
||||
c.setPosition(selection_start, QTextCursor::MoveAnchor);
|
||||
c.setPosition(selection_end + open_tag.size(), QTextCursor::KeepAnchor);
|
||||
text_edit_->setTextCursor(c);
|
||||
|
||||
selection_end -= close_tag.size();
|
||||
|
||||
new_text = selected_text;
|
||||
|
||||
} else if (open_tag_cancels_out) {
|
||||
|
||||
// Open tag cancels out, shift close tag rather than inserting new tags
|
||||
selection_start -= close_tag.size();
|
||||
|
||||
QTextCursor c = text_edit_->textCursor();
|
||||
c.clearSelection();
|
||||
c.setPosition(selection_start, QTextCursor::MoveAnchor);
|
||||
c.setPosition(selection_end, QTextCursor::KeepAnchor);
|
||||
text_edit_->setTextCursor(c);
|
||||
|
||||
selection_end -= close_tag.size();
|
||||
|
||||
new_text = selected_text;
|
||||
new_text.append(close_tag);
|
||||
|
||||
} else if (close_tag_cancels_out) {
|
||||
|
||||
// Close tag cancels out, shift open tag rather than inserting new tags
|
||||
selection_end += open_tag.size();
|
||||
|
||||
QTextCursor c = text_edit_->textCursor();
|
||||
c.clearSelection();
|
||||
c.setPosition(selection_start, QTextCursor::MoveAnchor);
|
||||
c.setPosition(selection_end, QTextCursor::KeepAnchor);
|
||||
text_edit_->setTextCursor(c);
|
||||
|
||||
selection_start += open_tag.size();
|
||||
|
||||
new_text = open_tag;
|
||||
new_text.append(selected_text);
|
||||
|
||||
} else {
|
||||
// Nothing is cancelled out, simply insert tags
|
||||
new_text = QStringLiteral("%1%2%3").arg(open_tag,
|
||||
selected_text,
|
||||
close_tag);
|
||||
|
||||
selection_start += open_tag.size();
|
||||
selection_end += open_tag.size();
|
||||
}
|
||||
|
||||
text_edit_->insertPlainText(new_text);
|
||||
|
||||
text_edit_->setFocus();
|
||||
|
||||
{
|
||||
// Re-select text
|
||||
QTextCursor c = text_edit_->textCursor();
|
||||
|
||||
c.clearSelection();
|
||||
c.setPosition(selection_start, QTextCursor::MoveAnchor);
|
||||
c.setPosition(selection_end, QTextCursor::KeepAnchor);
|
||||
text_edit_->setTextCursor(c);
|
||||
}
|
||||
}
|
||||
|
||||
QString RichTextDialog::CreateOpeningTag(const QString &s)
|
||||
{
|
||||
return QStringLiteral("<%1>").arg(s);
|
||||
}
|
||||
|
||||
QString RichTextDialog::CreateClosingTag(const QString &s)
|
||||
{
|
||||
return QStringLiteral("</%1>").arg(s);
|
||||
}
|
||||
|
||||
void RichTextDialog::UpdateTagButton(QPushButton *btn,
|
||||
const QString &text,
|
||||
int cursor_pos)
|
||||
{
|
||||
QStringList tags = btn->property("tag").toStringList();
|
||||
foreach (const QString& t, tags) {
|
||||
QString opening = CreateOpeningTag(t);
|
||||
QString closing = CreateClosingTag(t);
|
||||
|
||||
int opening_index = text.lastIndexOf(opening,
|
||||
cursor_pos - text.size() - 1,
|
||||
Qt::CaseInsensitive);
|
||||
int closing_index = text.indexOf(closing,
|
||||
opening_index,
|
||||
Qt::CaseInsensitive);
|
||||
|
||||
if (opening_index > -1 && closing_index + closing.size() > cursor_pos) {
|
||||
btn->setChecked(true);
|
||||
btn->setProperty("foundtag", t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
btn->setChecked(false);
|
||||
btn->setProperty("foundtag", QVariant());
|
||||
}
|
||||
|
||||
void RichTextDialog::TagButtonToggled(bool checked)
|
||||
{
|
||||
QPushButton* src = static_cast<QPushButton*>(sender());
|
||||
QStringList tags;
|
||||
|
||||
if (src->property("foundtag").isNull()) {
|
||||
tags = src->property("tag").toStringList();
|
||||
} else {
|
||||
tags = QStringList({src->property("foundtag").toString()});
|
||||
}
|
||||
|
||||
SetTags(tags, checked);
|
||||
}
|
||||
|
||||
void RichTextDialog::UpdateButtons()
|
||||
{
|
||||
QString text = text_edit_->toPlainText();
|
||||
int cursor_pos = text_edit_->textCursor().position();
|
||||
|
||||
UpdateTagButton(bold_btn_, text, cursor_pos);
|
||||
UpdateTagButton(italic_btn_, text, cursor_pos);
|
||||
UpdateTagButton(underline_btn_, text, cursor_pos);
|
||||
UpdateTagButton(strikeout_btn_, text, cursor_pos);
|
||||
|
||||
/*
|
||||
|
||||
// Update font family
|
||||
font_combo_->blockSignals(true);
|
||||
font_combo_->setCurrentFont(text_edit_->currentFont().family());
|
||||
font_combo_->blockSignals(false);
|
||||
|
||||
size_slider_->SetValue(text_edit_->fontPointSize());
|
||||
|
||||
left_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignLeft);
|
||||
center_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignCenter);
|
||||
right_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignRight);
|
||||
justify_align_btn_->setChecked(text_edit_->alignment() == Qt::AlignJustify);
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RICHTEXTDIALOG_H
|
||||
#define RICHTEXTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFontComboBox>
|
||||
#include <QTextEdit>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "widget/slider/floatslider.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class RichTextDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RichTextDialog(QString start, QWidget* parent = nullptr);
|
||||
|
||||
QString text() const
|
||||
{
|
||||
QString s = text_edit_->document()->toPlainText();
|
||||
|
||||
// Convert linebreaks
|
||||
s.replace('\n', QStringLiteral("<br>"));
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
QPushButton* CreateToolbarButton(const QString &label,
|
||||
const QString &tooltip,
|
||||
const QStringList& tags);
|
||||
|
||||
void SetTags(const QStringList& t, bool enabled);
|
||||
|
||||
static QString CreateOpeningTag(const QString& s);
|
||||
static QString CreateClosingTag(const QString& s);
|
||||
|
||||
static void UpdateTagButton(QPushButton* btn,
|
||||
const QString &text,
|
||||
int cursor_pos);
|
||||
|
||||
QFontDatabase font_db_;
|
||||
|
||||
QTextEdit* text_edit_;
|
||||
|
||||
QPushButton* bold_btn_;
|
||||
QPushButton* italic_btn_;
|
||||
QPushButton* underline_btn_;
|
||||
QPushButton* strikeout_btn_;
|
||||
QFontComboBox* font_combo_;
|
||||
FloatSlider* size_slider_;
|
||||
QPushButton* left_align_btn_;
|
||||
QPushButton* center_align_btn_;
|
||||
QPushButton* right_align_btn_;
|
||||
QPushButton* justify_align_btn_;
|
||||
|
||||
private slots:
|
||||
void TagButtonToggled(bool checked);
|
||||
|
||||
void UpdateButtons();
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // RICHTEXTDIALOG_H
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
dialog/richtext/richtext.h
|
||||
dialog/richtext/richtext.cpp
|
||||
dialog/text/text.h
|
||||
dialog/text/text.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "text.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ui/icons/icons.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
TextDialog::TextDialog(const QString &start, QWidget* parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
|
||||
// Create text edit widget
|
||||
text_edit_ = new QPlainTextEdit();
|
||||
text_edit_->document()->setPlainText(start);
|
||||
layout->addWidget(text_edit_);
|
||||
|
||||
// Create buttons
|
||||
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &TextDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &TextDialog::reject);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef RICHTEXTDIALOG_H
|
||||
#define RICHTEXTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFontComboBox>
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "widget/slider/floatslider.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class TextDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TextDialog(const QString &start, QWidget* parent = nullptr);
|
||||
|
||||
QString text() const
|
||||
{
|
||||
return text_edit_->toPlainText();
|
||||
}
|
||||
|
||||
private:
|
||||
QPlainTextEdit* text_edit_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // RICHTEXTDIALOG_H
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
add_subdirectory(clip)
|
||||
add_subdirectory(gap)
|
||||
add_subdirectory(subtitle)
|
||||
add_subdirectory(transition)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
|
||||
@@ -26,9 +26,11 @@ namespace olive {
|
||||
|
||||
const QString ClipBlock::kBufferIn = QStringLiteral("buffer_in");
|
||||
|
||||
ClipBlock::ClipBlock()
|
||||
ClipBlock::ClipBlock(bool create_buffer_in)
|
||||
{
|
||||
AddInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable));
|
||||
if (create_buffer_in) {
|
||||
AddInput(kBufferIn, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable));
|
||||
}
|
||||
}
|
||||
|
||||
Node *ClipBlock::copy() const
|
||||
@@ -108,7 +110,9 @@ void ClipBlock::Retranslate()
|
||||
{
|
||||
super::Retranslate();
|
||||
|
||||
SetInputName(kBufferIn, tr("Buffer"));
|
||||
if (HasInputWithID(kBufferIn)) {
|
||||
SetInputName(kBufferIn, tr("Buffer"));
|
||||
}
|
||||
}
|
||||
|
||||
void ClipBlock::Hash(const QString &out, QCryptographicHash &hash, const rational &time, const VideoParams &video_params) const
|
||||
|
||||
@@ -32,7 +32,7 @@ class ClipBlock : public Block
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ClipBlock();
|
||||
ClipBlock(bool create_buffer_in = true);
|
||||
|
||||
NODE_DEFAULT_DESTRUCTOR(ClipBlock)
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Olive - Non-Linear Video Editor
|
||||
# Copyright (C) 2021 Olive Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
node/block/subtitle/subtitle.cpp
|
||||
node/block/subtitle/subtitle.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "subtitle.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
#define super ClipBlock
|
||||
|
||||
const QString SubtitleBlock::kTextIn = QStringLiteral("text_in");
|
||||
|
||||
SubtitleBlock::SubtitleBlock() :
|
||||
super(false)
|
||||
{
|
||||
AddInput(kTextIn, NodeValue::kText, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable));
|
||||
}
|
||||
|
||||
Node *SubtitleBlock::copy() const
|
||||
{
|
||||
return new SubtitleBlock();
|
||||
}
|
||||
|
||||
QString SubtitleBlock::Name() const
|
||||
{
|
||||
return tr("Subtitle");
|
||||
}
|
||||
|
||||
QString SubtitleBlock::id() const
|
||||
{
|
||||
return QStringLiteral("org.olivevideoeditor.Olive.subtitle");
|
||||
}
|
||||
|
||||
QString SubtitleBlock::Description() const
|
||||
{
|
||||
return tr("A time-based node representing a single subtitle element for a certain period of time.");
|
||||
}
|
||||
|
||||
void SubtitleBlock::Retranslate()
|
||||
{
|
||||
super::Retranslate();
|
||||
|
||||
SetInputName(kTextIn, tr("Text"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef SUBTITLEBLOCK_H
|
||||
#define SUBTITLEBLOCK_H
|
||||
|
||||
#include "node/block/clip/clip.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class SubtitleBlock : public ClipBlock
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
SubtitleBlock();
|
||||
|
||||
NODE_DEFAULT_DESTRUCTOR(SubtitleBlock)
|
||||
|
||||
virtual Node* copy() const override;
|
||||
|
||||
virtual QString Name() const override;
|
||||
virtual QString id() const override;
|
||||
virtual QString Description() const override;
|
||||
|
||||
virtual void Retranslate() override;
|
||||
|
||||
static const QString kTextIn;
|
||||
|
||||
QString GetText() const
|
||||
{
|
||||
return GetStandardValue(kTextIn).toString();
|
||||
}
|
||||
|
||||
void SetText(const QString &text)
|
||||
{
|
||||
SetStandardValue(kTextIn, text);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // SUBTITLEBLOCK_H
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "audio/volume/volume.h"
|
||||
#include "block/clip/clip.h"
|
||||
#include "block/gap/gap.h"
|
||||
#include "block/subtitle/subtitle.h"
|
||||
#include "block/transition/crossdissolve/crossdissolvetransition.h"
|
||||
#include "block/transition/diptocolor/diptocolortransition.h"
|
||||
#include "distort/crop/cropdistortnode.h"
|
||||
@@ -236,6 +237,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id)
|
||||
return new ValueNode();
|
||||
case kTimeRemapNode:
|
||||
return new TimeRemapNode();
|
||||
case kSubtitleBlock:
|
||||
return new SubtitleBlock();
|
||||
|
||||
case kInternalNodeCount:
|
||||
break;
|
||||
|
||||
@@ -58,6 +58,7 @@ public:
|
||||
kProjectSequence,
|
||||
kValueNode,
|
||||
kTimeRemapNode,
|
||||
kSubtitleBlock,
|
||||
|
||||
// Count value
|
||||
kInternalNodeCount
|
||||
|
||||
@@ -32,6 +32,7 @@ enum TextVerticalAlign {
|
||||
};
|
||||
|
||||
const QString TextGenerator::kTextInput = QStringLiteral("text_in");
|
||||
const QString TextGenerator::kHtmlInput = QStringLiteral("html_in");
|
||||
const QString TextGenerator::kColorInput = QStringLiteral("color_in");
|
||||
const QString TextGenerator::kVAlignInput = QStringLiteral("valign_in");
|
||||
const QString TextGenerator::kFontInput = QStringLiteral("font_in");
|
||||
@@ -41,6 +42,8 @@ TextGenerator::TextGenerator()
|
||||
{
|
||||
AddInput(kTextInput, NodeValue::kText, tr("Sample Text"));
|
||||
|
||||
AddInput(kHtmlInput, NodeValue::kBoolean, false);
|
||||
|
||||
AddInput(kColorInput, NodeValue::kColor, QVariant::fromValue(Color(1.0f, 1.0f, 1.0)));
|
||||
|
||||
AddInput(kVAlignInput, NodeValue::kCombo, 1);
|
||||
@@ -78,6 +81,7 @@ QString TextGenerator::Description() const
|
||||
void TextGenerator::Retranslate()
|
||||
{
|
||||
SetInputName(kTextInput, tr("Text"));
|
||||
SetInputName(kHtmlInput, tr("Enable HTML"));
|
||||
SetInputName(kFontInput, tr("Font"));
|
||||
SetInputName(kFontSizeInput, tr("Font Size"));
|
||||
SetInputName(kColorInput, tr("Color"));
|
||||
@@ -91,6 +95,7 @@ NodeValueTable TextGenerator::Value(const QString &output, NodeValueDatabase &va
|
||||
|
||||
GenerateJob job;
|
||||
job.InsertValue(this, kTextInput, value);
|
||||
job.InsertValue(this, kHtmlInput, value);
|
||||
job.InsertValue(this, kColorInput, value);
|
||||
job.InsertValue(this, kVAlignInput, value);
|
||||
job.InsertValue(this, kFontInput, value);
|
||||
@@ -126,7 +131,13 @@ void TextGenerator::GenerateFrame(FramePtr frame, const GenerateJob& job) const
|
||||
// Center by default
|
||||
text_doc.setDefaultTextOption(QTextOption(Qt::AlignCenter));
|
||||
|
||||
text_doc.setHtml(job.GetValue(kTextInput).data().toString());
|
||||
QString html = job.GetValue(kTextInput).data().toString();
|
||||
if (job.GetValue(kHtmlInput).data().toBool()) {
|
||||
html.replace('\n', QStringLiteral("<br>"));
|
||||
text_doc.setHtml(html);
|
||||
} else {
|
||||
text_doc.setPlainText(html);
|
||||
}
|
||||
|
||||
// Align to 80% width because that's considered the "title safe" area
|
||||
int tenth_of_width = frame->video_params().width() / 10;
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
virtual void GenerateFrame(FramePtr frame, const GenerateJob &job) const override;
|
||||
|
||||
static const QString kTextInput;
|
||||
static const QString kHtmlInput;
|
||||
static const QString kColorInput;
|
||||
static const QString kVAlignInput;
|
||||
static const QString kFontInput;
|
||||
|
||||
+1
-2
@@ -591,8 +591,7 @@ QVariant Node::GetSplitValueAtTimeOnTrack(const QString &input, const rational &
|
||||
NodeKeyframe* after = key_track.at(i+1);
|
||||
|
||||
if (before->time() == time
|
||||
|| !NodeValue::type_can_be_interpolated(type)
|
||||
|| (before->type() == NodeKeyframe::kHold && after->time() > time)) {
|
||||
|| ((!NodeValue::type_can_be_interpolated(type) || before->type() == NodeKeyframe::kHold) && after->time() > time)) {
|
||||
|
||||
// Time == keyframe time, so value is precise
|
||||
return before->value();
|
||||
|
||||
@@ -155,6 +155,8 @@ bool Track::LoadCustom(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uin
|
||||
|
||||
void Track::SaveCustom(QXmlStreamWriter *writer) const
|
||||
{
|
||||
super::SaveCustom(writer);
|
||||
|
||||
writer->writeTextElement(QStringLiteral("height"), QString::number(GetTrackHeight()));
|
||||
}
|
||||
|
||||
|
||||
@@ -497,12 +497,14 @@ bool ViewerOutput::LoadCustom(QXmlStreamReader *reader, XMLNodeData &xml_node_da
|
||||
timeline_points_.Load(reader);
|
||||
return true;
|
||||
} else {
|
||||
return LoadCustom(reader, xml_node_data, version, cancelled);
|
||||
return super::LoadCustom(reader, xml_node_data, version, cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
void ViewerOutput::SaveCustom(QXmlStreamWriter *writer) const
|
||||
{
|
||||
super::SaveCustom(writer);
|
||||
|
||||
// Write TimelinePoints
|
||||
writer->writeStartElement(QStringLiteral("points"));
|
||||
timeline_points_.Save(writer);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#include "footage.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
@@ -56,6 +56,11 @@ Footage::Footage(const QString &filename) :
|
||||
Clear();
|
||||
|
||||
set_filename(filename);
|
||||
|
||||
QTimer *check_timer = new QTimer(this);
|
||||
check_timer->setInterval(5000);
|
||||
connect(check_timer, &QTimer::timeout, this, &Footage::CheckFootage);
|
||||
check_timer->start();
|
||||
}
|
||||
|
||||
void Footage::Retranslate()
|
||||
@@ -511,17 +516,20 @@ void Footage::UpdateTooltip()
|
||||
|
||||
void Footage::CheckFootage()
|
||||
{
|
||||
QString fn = filename();
|
||||
// Don't check files if not the active window
|
||||
if (qApp->activeWindow()) {
|
||||
QString fn = filename();
|
||||
|
||||
if (!fn.isEmpty()) {
|
||||
QFileInfo info(fn);
|
||||
if (!fn.isEmpty()) {
|
||||
QFileInfo info(fn);
|
||||
|
||||
qint64 current_file_timestamp = info.lastModified().toMSecsSinceEpoch();
|
||||
qint64 current_file_timestamp = info.lastModified().toMSecsSinceEpoch();
|
||||
|
||||
if (current_file_timestamp != timestamp()) {
|
||||
// File has changed!
|
||||
set_timestamp(current_file_timestamp);
|
||||
InvalidateAll(kFilenameInput);
|
||||
if (current_file_timestamp != timestamp()) {
|
||||
// File has changed!
|
||||
set_timestamp(current_file_timestamp);
|
||||
InvalidateAll(kFilenameInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ set(OLIVE_SOURCES
|
||||
render/renderprocessor.h
|
||||
render/shadercode.h
|
||||
render/stillimagecache.h
|
||||
render/subtitleparams.cpp
|
||||
render/subtitleparams.h
|
||||
render/texture.cpp
|
||||
render/texture.h
|
||||
render/videoparams.cpp
|
||||
|
||||
@@ -39,7 +39,18 @@ private:
|
||||
|
||||
};
|
||||
|
||||
using DecoderCache = RenderCache<Decoder::CodecStream, DecoderPtr>;
|
||||
struct DecoderPair {
|
||||
DecoderPair()
|
||||
{
|
||||
decoder = nullptr;
|
||||
last_modified = 0;
|
||||
}
|
||||
|
||||
DecoderPtr decoder;
|
||||
qint64 last_modified;
|
||||
};
|
||||
|
||||
using DecoderCache = RenderCache<Decoder::CodecStream, DecoderPair>;
|
||||
using ShaderCache = RenderCache<QString, QVariant>;
|
||||
|
||||
}
|
||||
|
||||
@@ -91,10 +91,10 @@ void RenderManager::ClearOldDecoders()
|
||||
qint64 min_age = QDateTime::currentMSecsSinceEpoch() - kDecoderMaximumInactivity;
|
||||
|
||||
for (auto it=decoder_cache_->begin(); it!=decoder_cache_->end(); ) {
|
||||
DecoderPtr decoder = it.value();
|
||||
DecoderPair decoder = it.value();
|
||||
|
||||
if (decoder->GetLastAccessedTime() < min_age) {
|
||||
decoder->Close();
|
||||
if (decoder.decoder->GetLastAccessedTime() < min_age) {
|
||||
decoder.decoder->Close();
|
||||
it = decoder_cache_->erase(it);
|
||||
} else {
|
||||
it++;
|
||||
|
||||
@@ -187,13 +187,16 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(const QString& decoder_id, c
|
||||
|
||||
QMutexLocker locker(decoder_cache_->mutex());
|
||||
|
||||
DecoderPtr decoder = decoder_cache_->value(stream);
|
||||
DecoderPair decoder = decoder_cache_->value(stream);
|
||||
|
||||
if (!decoder) {
|
||||
qint64 file_last_modified = QFileInfo(stream.filename()).lastModified().toMSecsSinceEpoch();
|
||||
|
||||
if (!decoder.decoder || decoder.last_modified != file_last_modified) {
|
||||
// No decoder
|
||||
decoder = Decoder::CreateFromID(decoder_id);
|
||||
decoder.decoder = Decoder::CreateFromID(decoder_id);
|
||||
decoder.last_modified = file_last_modified;
|
||||
|
||||
if (decoder->Open(stream)) {
|
||||
if (decoder.decoder->Open(stream)) {
|
||||
decoder_cache_->insert(stream, decoder);
|
||||
} else {
|
||||
qWarning() << "Failed to open decoder for" << stream.filename()
|
||||
@@ -202,7 +205,7 @@ DecoderPtr RenderProcessor::ResolveDecoderFromInput(const QString& decoder_id, c
|
||||
}
|
||||
}
|
||||
|
||||
return decoder;
|
||||
return decoder.decoder;
|
||||
}
|
||||
|
||||
void RenderProcessor::Process(RenderTicketPtr ticket, Renderer *render_ctx, StillImageCache *still_image_cache, DecoderCache *decoder_cache, ShaderCache *shader_cache, QVariant default_shader)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "subtitleparams.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
namespace olive {
|
||||
|
||||
QString SubtitleParams::GenerateASSHeader()
|
||||
{
|
||||
// NOTE: We'll probably implement more customization as we support ASS better. Right now, we only
|
||||
// natively support SRT and only make this header because FFmpeg requires it.
|
||||
static const int kAssDefaultPlayResX = 384;
|
||||
static const int kAssDefaultPlayResY = 288;
|
||||
static const QString kAssDefaultFont = QStringLiteral("Arial");
|
||||
static const int kAssDefaultFontSize = 16;
|
||||
static const int kAssDefaultPrimaryColor = 0xFFFFFF; // White
|
||||
static const int kAssDefaultSecondaryColor = 0xFFFFFF; // White
|
||||
static const int kAssDefaultOutlineColor = 0x000000; // Black
|
||||
static const int kAssDefaultBackColor = 0x000000; // Black
|
||||
static const int kAssBold = 0;
|
||||
static const int kAssItalic = 0;
|
||||
static const int kAssUnderline = 0;
|
||||
static const int kAssStrike = 0;
|
||||
static const int kAssBorderStyle = 1;
|
||||
static const int kAssAlignment = 2;
|
||||
|
||||
static const QString kFormatHeader = QStringLiteral(
|
||||
"[Script Info]\r\n"
|
||||
"; Script generated by %1 %2\r\n"
|
||||
"ScriptType: v4.00+\r\n"
|
||||
"PlayResX: %3\r\n"
|
||||
"PlayResY: %4\r\n"
|
||||
"ScaledBorderAndShadow: yes\r\n"
|
||||
"\r\n"
|
||||
|
||||
/* ASSv4 header */
|
||||
"[V4+ Styles]\r\n"
|
||||
"Format: Name, "
|
||||
"Fontname, Fontsize, "
|
||||
"PrimaryColour, SecondaryColour, OutlineColour, BackColour, "
|
||||
"Bold, Italic, Underline, StrikeOut, "
|
||||
"ScaleX, ScaleY, "
|
||||
"Spacing, Angle, "
|
||||
"BorderStyle, Outline, Shadow, "
|
||||
"Alignment, MarginL, MarginR, MarginV, "
|
||||
"Encoding\r\n"
|
||||
|
||||
"Style: "
|
||||
"Default," /* Name */
|
||||
"%5,%6," /* Font{name,size} */
|
||||
"&H%7,&H%8,&H%9,&H%10," /* {Primary,Secondary,Outline,Back}Colour */
|
||||
"%11,%12,%13,%14," /* Bold, Italic, Underline, StrikeOut */
|
||||
"100,100," /* Scale{X,Y} */
|
||||
"0,0," /* Spacing, Angle */
|
||||
"%15,1,0," /* BorderStyle, Outline, Shadow */
|
||||
"%16,10,10,10," /* Alignment, Margin[LRV] */
|
||||
"0\r\n" /* Encoding */
|
||||
|
||||
"\r\n"
|
||||
"[Events]\r\n"
|
||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\r\n"
|
||||
);
|
||||
|
||||
return kFormatHeader.arg(QCoreApplication::applicationName(),
|
||||
QCoreApplication::applicationVersion(),
|
||||
QString::number(kAssDefaultPlayResX),
|
||||
QString::number(kAssDefaultPlayResY),
|
||||
kAssDefaultFont,
|
||||
QString::number(kAssDefaultFontSize),
|
||||
QString::number(kAssDefaultPrimaryColor, 16),
|
||||
QString::number(kAssDefaultSecondaryColor, 16),
|
||||
QString::number(kAssDefaultOutlineColor, 16),
|
||||
QString::number(kAssDefaultBackColor, 16),
|
||||
QString::number(kAssBold),
|
||||
QString::number(kAssItalic),
|
||||
QString::number(kAssUnderline),
|
||||
QString::number(kAssStrike),
|
||||
QString::number(kAssBorderStyle),
|
||||
QString::number(kAssAlignment)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef SUBTITLEPARAMS_H
|
||||
#define SUBTITLEPARAMS_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace olive {
|
||||
|
||||
class SubtitleParams {
|
||||
public:
|
||||
static QString GenerateASSHeader();
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // SUBTITLEPARAMS_H
|
||||
@@ -56,7 +56,7 @@ bool ExportTask::Run()
|
||||
}
|
||||
|
||||
if (!encoder_->Open()) {
|
||||
SetError(tr("Failed to open file"));
|
||||
SetError(tr("Failed to open file: %1").arg(encoder_->GetError()));
|
||||
encoder_->deleteLater();
|
||||
return false;
|
||||
}
|
||||
@@ -102,6 +102,7 @@ bool ExportTask::Run()
|
||||
|
||||
// Start render process
|
||||
TimeRangeList video_range, audio_range;
|
||||
TimeRange subtitle_range;
|
||||
|
||||
if (params_.video_enabled()) {
|
||||
video_range = {range};
|
||||
@@ -111,7 +112,11 @@ bool ExportTask::Run()
|
||||
audio_range = {range};
|
||||
}
|
||||
|
||||
Render(color_manager_, video_range, audio_range, RenderMode::kOnline, nullptr,
|
||||
if (params_.subtitles_enabled()) {
|
||||
subtitle_range = range;
|
||||
}
|
||||
|
||||
Render(color_manager_, video_range, audio_range, subtitle_range, RenderMode::kOnline, nullptr,
|
||||
video_force_size, video_force_matrix, encoder_->GetDesiredPixelFormat(),
|
||||
color_processor_);
|
||||
|
||||
@@ -191,6 +196,11 @@ void ExportTask::AudioDownloaded(const TimeRange &range, SampleBufferPtr samples
|
||||
}
|
||||
}
|
||||
|
||||
void ExportTask::EncodeSubtitle(const SubtitleBlock *sub)
|
||||
{
|
||||
encoder_->WriteSubtitle(sub);
|
||||
}
|
||||
|
||||
void ExportTask::WriteAudioLoop(const TimeRange& time, SampleBufferPtr samples)
|
||||
{
|
||||
encoder_->WriteAudio(samples);
|
||||
|
||||
@@ -42,6 +42,8 @@ protected:
|
||||
|
||||
virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) override;
|
||||
|
||||
virtual void EncodeSubtitle(const SubtitleBlock *sub) override;
|
||||
|
||||
virtual bool TwoStepFrameRendering() const override
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -67,6 +67,7 @@ bool PreCacheTask::Run()
|
||||
Render(project_->color_manager(),
|
||||
video_range,
|
||||
TimeRangeList(),
|
||||
TimeRange(),
|
||||
RenderMode::kOnline,
|
||||
viewer()->video_frame_cache());
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "render.h"
|
||||
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -40,7 +41,7 @@ RenderTask::~RenderTask()
|
||||
|
||||
bool RenderTask::Render(ColorManager* manager,
|
||||
const TimeRangeList& video_range,
|
||||
const TimeRangeList &audio_range,
|
||||
const TimeRangeList &audio_range, const TimeRange &subtitle_range,
|
||||
RenderMode::Mode mode,
|
||||
FrameHashCache* cache, const QSize &force_size,
|
||||
const QMatrix4x4 &force_matrix, VideoParams::Format force_format,
|
||||
@@ -129,6 +130,50 @@ bool RenderTask::Render(ColorManager* manager,
|
||||
mode, cache, force_size, force_matrix, force_format, force_color_output);
|
||||
}
|
||||
|
||||
// Subtitle loop, loops over all blocks in sequence on all tracks
|
||||
if (!subtitle_range.length().isNull()) {
|
||||
Sequence *sequence = dynamic_cast<Sequence*>(viewer_);
|
||||
if (sequence) {
|
||||
TrackList *list = sequence->track_list(Track::kSubtitle);
|
||||
QVector<int> block_indexes(list->GetTrackCount(), 0);
|
||||
|
||||
QVector<int> tracks_to_push;
|
||||
do {
|
||||
tracks_to_push.clear();
|
||||
|
||||
for (int i=0; i<block_indexes.size(); i++) {
|
||||
Track *this_track = list->GetTrackAt(i);
|
||||
int &this_block_index = block_indexes[i];
|
||||
if (this_block_index >= this_track->Blocks().size()) {
|
||||
continue;
|
||||
}
|
||||
Block *this_block = this_track->Blocks().at(this_block_index);
|
||||
|
||||
Track *compare_track = tracks_to_push.isEmpty() ? nullptr : list->GetTrackAt(tracks_to_push.first());
|
||||
const int &compare_block_index = tracks_to_push.isEmpty() ? -1 : block_indexes.at(tracks_to_push.first());
|
||||
Block *compare_block = compare_track ? compare_track->Blocks().at(compare_block_index) : nullptr;
|
||||
if (!compare_track || compare_block->in() >= this_block->in()) {
|
||||
if (compare_track && compare_block->in() != this_block->in()) {
|
||||
tracks_to_push.clear();
|
||||
}
|
||||
tracks_to_push.append(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i=0; i<tracks_to_push.size(); i++) {
|
||||
Track *this_track = list->GetTrackAt(tracks_to_push.at(i));
|
||||
Block *this_block = this_track->Blocks().at(block_indexes.at(tracks_to_push.at(i)));
|
||||
|
||||
if (const SubtitleBlock *sub = dynamic_cast<const SubtitleBlock*>(this_block)) {
|
||||
EncodeSubtitle(sub);
|
||||
}
|
||||
|
||||
block_indexes[tracks_to_push.at(i)]++;
|
||||
}
|
||||
} while (!tracks_to_push.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
finished_watcher_mutex_.lock();
|
||||
|
||||
while (!IsCancelled()) {
|
||||
@@ -238,6 +283,11 @@ void RenderTask::DownloadFrame(QThread *thread, FramePtr frame, const QByteArray
|
||||
hash));
|
||||
}
|
||||
|
||||
void RenderTask::EncodeSubtitle(const SubtitleBlock *subtitle)
|
||||
{
|
||||
Q_UNUSED(subtitle)
|
||||
}
|
||||
|
||||
void RenderTask::PrepareWatcher(RenderTicketWatcher *watcher, QThread *thread)
|
||||
{
|
||||
watcher->moveToThread(thread);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
|
||||
#include "node/block/subtitle/subtitle.h"
|
||||
#include "node/color/colormanager/colormanager.h"
|
||||
#include "node/output/viewer/viewer.h"
|
||||
#include "task/task.h"
|
||||
@@ -41,7 +42,8 @@ public:
|
||||
|
||||
protected:
|
||||
bool Render(ColorManager *manager, const TimeRangeList &video_range,
|
||||
const TimeRangeList &audio_range, RenderMode::Mode mode,
|
||||
const TimeRangeList &audio_range, const TimeRange &subtitle_range,
|
||||
RenderMode::Mode mode,
|
||||
FrameHashCache *cache, const QSize& force_size = QSize(0, 0),
|
||||
const QMatrix4x4& force_matrix = QMatrix4x4(),
|
||||
VideoParams::Format force_format = VideoParams::kFormatInvalid,
|
||||
@@ -53,6 +55,8 @@ protected:
|
||||
|
||||
virtual void AudioDownloaded(const TimeRange& range, SampleBufferPtr samples, qint64 job_time) = 0;
|
||||
|
||||
virtual void EncodeSubtitle(const SubtitleBlock *subtitle);
|
||||
|
||||
ViewerOutput* viewer() const
|
||||
{
|
||||
return viewer_;
|
||||
|
||||
@@ -104,17 +104,19 @@ void TimelineMarkerList::Load(QXmlStreamReader *reader)
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("marker")) {
|
||||
QString name;
|
||||
TimeRange range;
|
||||
rational in, out;
|
||||
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (attr.name() == QStringLiteral("name")) {
|
||||
name = attr.value().toString();
|
||||
} else if (attr.name() == QStringLiteral("in")) {
|
||||
range.set_in(rational::fromString(attr.value().toString()));
|
||||
in = rational::fromString(attr.value().toString());
|
||||
} else if (attr.name() == QStringLiteral("out")) {
|
||||
range.set_out(rational::fromString(attr.value().toString()));
|
||||
out = rational::fromString(attr.value().toString());
|
||||
}
|
||||
}
|
||||
|
||||
AddMarker(TimeRange(in, out), name);
|
||||
}
|
||||
|
||||
reader->skipCurrentElement();
|
||||
|
||||
@@ -96,6 +96,9 @@ public:
|
||||
/// An audio clip with a sine connected to it
|
||||
kAddableTone,
|
||||
|
||||
/// A subtitle clip
|
||||
kAddableSubtitle,
|
||||
|
||||
kAddableCount
|
||||
};
|
||||
|
||||
@@ -112,6 +115,8 @@ public:
|
||||
return QCoreApplication::translate("Tool", "Title");
|
||||
case kAddableTone:
|
||||
return QCoreApplication::translate("Tool", "Tone");
|
||||
case kAddableSubtitle:
|
||||
return QCoreApplication::translate("Tool", "Subtitle");
|
||||
case kAddableCount:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,14 @@ set(OLIVE_SOURCES
|
||||
widget/nodeparamview/nodeparamviewarraywidget.cpp
|
||||
widget/nodeparamview/nodeparamviewconnectedlabel.h
|
||||
widget/nodeparamview/nodeparamviewconnectedlabel.cpp
|
||||
widget/nodeparamview/nodeparamviewdockarea.h
|
||||
widget/nodeparamview/nodeparamviewdockarea.cpp
|
||||
widget/nodeparamview/nodeparamviewitem.h
|
||||
widget/nodeparamview/nodeparamviewitem.cpp
|
||||
widget/nodeparamview/nodeparamviewkeyframecontrol.h
|
||||
widget/nodeparamview/nodeparamviewkeyframecontrol.cpp
|
||||
widget/nodeparamview/nodeparamviewrichtext.h
|
||||
widget/nodeparamview/nodeparamviewrichtext.cpp
|
||||
widget/nodeparamview/nodeparamviewtextedit.h
|
||||
widget/nodeparamview/nodeparamviewtextedit.cpp
|
||||
widget/nodeparamview/nodeparamviewundo.h
|
||||
widget/nodeparamview/nodeparamviewundo.cpp
|
||||
widget/nodeparamview/nodeparamviewwidgetbridge.h
|
||||
|
||||
@@ -55,7 +55,7 @@ NodeParamView::NodeParamView(QWidget *parent) :
|
||||
connect(param_widget_container_, &NodeParamViewParamContainer::Resized, this, &NodeParamView::UpdateGlobalScrollBar);
|
||||
scroll_area->setWidget(param_widget_container_);
|
||||
|
||||
param_widget_area_ = new QMainWindow();
|
||||
param_widget_area_ = new NodeParamViewDockArea();
|
||||
|
||||
// Disable dock widgets from tabbing and disable glitchy animations
|
||||
param_widget_area_->setDockOptions(static_cast<QMainWindow::DockOption>(0));
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
#ifndef NODEPARAMVIEW_H
|
||||
#define NODEPARAMVIEW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "node/node.h"
|
||||
#include "nodeparamviewdockarea.h"
|
||||
#include "nodeparamviewitem.h"
|
||||
#include "widget/keyframeview/keyframeview.h"
|
||||
#include "widget/timebased/timebasedwidget.h"
|
||||
@@ -121,7 +121,7 @@ private:
|
||||
|
||||
// This may look weird, but QMainWindow is just a QWidget with a fancy layout that allows
|
||||
// docking windows
|
||||
QMainWindow* param_widget_area_;
|
||||
NodeParamViewDockArea* param_widget_area_;
|
||||
|
||||
QVector<Node*> pinned_nodes_;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "nodeparamviewdockarea.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
NodeParamViewDockArea::NodeParamViewDockArea(QWidget *parent) :
|
||||
QMainWindow(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QMenu *NodeParamViewDockArea::createPopupMenu()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2021 Olive Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef NODEPARAMVIEWDOCKAREA_H
|
||||
#define NODEPARAMVIEWDOCKAREA_H
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
namespace olive {
|
||||
|
||||
class NodeParamViewDockArea : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit NodeParamViewDockArea(QWidget *parent = nullptr);
|
||||
|
||||
virtual QMenu *createPopupMenu() override;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // NODEPARAMVIEWDOCKAREA_H
|
||||
+10
-10
@@ -18,46 +18,46 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "nodeparamviewrichtext.h"
|
||||
#include "nodeparamviewtextedit.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "dialog/richtext/richtext.h"
|
||||
#include "dialog/text/text.h"
|
||||
#include "ui/icons/icons.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
NodeParamViewRichText::NodeParamViewRichText(QWidget *parent) :
|
||||
NodeParamViewTextEdit::NodeParamViewTextEdit(QWidget *parent) :
|
||||
QWidget(parent)
|
||||
{
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
layout->setMargin(0);
|
||||
|
||||
line_edit_ = new QTextEdit();
|
||||
line_edit_ = new QPlainTextEdit();
|
||||
line_edit_->setUndoRedoEnabled(true);
|
||||
connect(line_edit_, &QTextEdit::textChanged, this, &NodeParamViewRichText::InnerWidgetTextChanged);
|
||||
connect(line_edit_, &QPlainTextEdit::textChanged, this, &NodeParamViewTextEdit::InnerWidgetTextChanged);
|
||||
layout->addWidget(line_edit_);
|
||||
|
||||
QPushButton* edit_btn = new QPushButton();
|
||||
edit_btn->setIcon(icon::ToolEdit);
|
||||
edit_btn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
|
||||
layout->addWidget(edit_btn);
|
||||
connect(edit_btn, &QPushButton::clicked, this, &NodeParamViewRichText::ShowRichTextDialog);
|
||||
connect(edit_btn, &QPushButton::clicked, this, &NodeParamViewTextEdit::ShowTextDialog);
|
||||
}
|
||||
|
||||
void NodeParamViewRichText::ShowRichTextDialog()
|
||||
void NodeParamViewTextEdit::ShowTextDialog()
|
||||
{
|
||||
RichTextDialog d(this->text(), this);
|
||||
TextDialog d(this->text(), this);
|
||||
if (d.exec() == QDialog::Accepted) {
|
||||
QString s = d.text();
|
||||
|
||||
line_edit_->setText(s);
|
||||
line_edit_->setPlainText(s);
|
||||
emit textEdited(s);
|
||||
}
|
||||
}
|
||||
|
||||
void NodeParamViewRichText::InnerWidgetTextChanged()
|
||||
void NodeParamViewTextEdit::InnerWidgetTextChanged()
|
||||
{
|
||||
emit textEdited(this->text());
|
||||
}
|
||||
+11
-11
@@ -18,32 +18,32 @@
|
||||
|
||||
***/
|
||||
|
||||
#ifndef NODEPARAMVIEWRICHTEXT_H
|
||||
#define NODEPARAMVIEWRICHTEXT_H
|
||||
#ifndef NODEPARAMVIEWTEXTEDIT_H
|
||||
#define NODEPARAMVIEWTEXTEDIT_H
|
||||
|
||||
#include <QTextEdit>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QWidget>
|
||||
|
||||
#include "common/define.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class NodeParamViewRichText : public QWidget
|
||||
class NodeParamViewTextEdit : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
NodeParamViewRichText(QWidget* parent = nullptr);
|
||||
NodeParamViewTextEdit(QWidget* parent = nullptr);
|
||||
|
||||
QString text() const
|
||||
{
|
||||
return line_edit_->toPlainText().replace('\n', QStringLiteral("<br>"));
|
||||
return line_edit_->toPlainText();
|
||||
}
|
||||
|
||||
public slots:
|
||||
void setText(QString s)
|
||||
void setText(const QString &s)
|
||||
{
|
||||
line_edit_->blockSignals(true);
|
||||
line_edit_->setPlainText(s.replace(QStringLiteral("<br>"), QStringLiteral("\n")));
|
||||
line_edit_->setPlainText(s);
|
||||
line_edit_->blockSignals(false);
|
||||
}
|
||||
|
||||
@@ -65,10 +65,10 @@ signals:
|
||||
void textEdited(const QString &);
|
||||
|
||||
private:
|
||||
QTextEdit* line_edit_;
|
||||
QPlainTextEdit* line_edit_;
|
||||
|
||||
private slots:
|
||||
void ShowRichTextDialog();
|
||||
void ShowTextDialog();
|
||||
|
||||
void InnerWidgetTextChanged();
|
||||
|
||||
@@ -76,4 +76,4 @@ private slots:
|
||||
|
||||
}
|
||||
|
||||
#endif // NODEPARAMVIEWRICHTEXT_H
|
||||
#endif // NODEPARAMVIEWTEXTEDIT_H
|
||||
@@ -30,7 +30,7 @@
|
||||
#include "node/node.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "nodeparamviewarraywidget.h"
|
||||
#include "nodeparamviewrichtext.h"
|
||||
#include "nodeparamviewtextedit.h"
|
||||
#include "nodeparamviewundo.h"
|
||||
#include "undo/undostack.h"
|
||||
#include "widget/colorbutton/colorbutton.h"
|
||||
@@ -143,9 +143,9 @@ void NodeParamViewWidgetBridge::CreateWidgets()
|
||||
}
|
||||
case NodeValue::kText:
|
||||
{
|
||||
NodeParamViewRichText* line_edit = new NodeParamViewRichText();
|
||||
NodeParamViewTextEdit* line_edit = new NodeParamViewTextEdit();
|
||||
widgets_.append(line_edit);
|
||||
connect(line_edit, &NodeParamViewRichText::textEdited, this, &NodeParamViewWidgetBridge::WidgetCallback);
|
||||
connect(line_edit, &NodeParamViewTextEdit::textEdited, this, &NodeParamViewWidgetBridge::WidgetCallback);
|
||||
break;
|
||||
}
|
||||
case NodeValue::kBoolean:
|
||||
@@ -358,7 +358,7 @@ void NodeParamViewWidgetBridge::WidgetCallback()
|
||||
case NodeValue::kText:
|
||||
{
|
||||
// Sender is a NodeParamViewRichText
|
||||
SetInputValue(static_cast<NodeParamViewRichText*>(sender())->text(), 0);
|
||||
SetInputValue(static_cast<NodeParamViewTextEdit*>(sender())->text(), 0);
|
||||
break;
|
||||
}
|
||||
case NodeValue::kBoolean:
|
||||
@@ -498,7 +498,7 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues()
|
||||
}
|
||||
case NodeValue::kText:
|
||||
{
|
||||
NodeParamViewRichText* e = static_cast<NodeParamViewRichText*>(widgets_.first());
|
||||
NodeParamViewTextEdit* e = static_cast<NodeParamViewTextEdit*>(widgets_.first());
|
||||
e->setTextPreservingCursor(input_.GetValueAtTime(node_time).toString());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1230,11 +1230,11 @@ public:
|
||||
|
||||
if (timeline_->type() == Track::kVideo) {
|
||||
relevant_input = ViewerOutput::kTextureInput;
|
||||
} else {
|
||||
} else if (timeline_->type() == Track::kAudio) {
|
||||
relevant_input = ViewerOutput::kSamplesInput;
|
||||
}
|
||||
|
||||
if (!timeline_->parent()->IsInputConnected(relevant_input)) {
|
||||
if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) {
|
||||
direct_ = NodeInput(timeline_->parent(), relevant_input);
|
||||
|
||||
Node::ConnectEdge(track_, direct_);
|
||||
@@ -1289,15 +1289,20 @@ private:
|
||||
merge_ = new MergeNode();
|
||||
base_ = NodeInput(merge_, MergeNode::kBaseIn);
|
||||
blend_ = NodeInput(merge_, MergeNode::kBlendIn);
|
||||
} else {
|
||||
} else if (timeline_->type() == Track::kAudio) {
|
||||
merge_ = new MathNode();
|
||||
base_ = NodeInput(merge_, MathNode::kParamAIn);
|
||||
blend_ = NodeInput(merge_, MathNode::kParamBIn);
|
||||
} else {
|
||||
merge_ = nullptr;
|
||||
}
|
||||
merge_->setParent(&memory_manager_);
|
||||
} else {
|
||||
merge_ = nullptr;
|
||||
}
|
||||
|
||||
if (merge_) {
|
||||
merge_->setParent(&memory_manager_);
|
||||
}
|
||||
}
|
||||
|
||||
TrackList* timeline_;
|
||||
|
||||
@@ -77,7 +77,6 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
|
||||
// Create list of TimelineViews - these MUST correspond to the ViewType enum
|
||||
|
||||
view_splitter_ = new QSplitter(Qt::Vertical);
|
||||
view_splitter_->setChildrenCollapsible(false);
|
||||
vert_layout->addWidget(view_splitter_);
|
||||
|
||||
// Video view
|
||||
@@ -86,6 +85,9 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
|
||||
// Audio view
|
||||
views_.append(new TimelineAndTrackView(Qt::AlignTop));
|
||||
|
||||
// Subtitle view
|
||||
views_.append(new TimelineAndTrackView(Qt::AlignTop));
|
||||
|
||||
// Create tools
|
||||
tools_.resize(olive::Tool::kCount);
|
||||
tools_.fill(nullptr);
|
||||
@@ -155,7 +157,17 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
|
||||
}
|
||||
|
||||
// Split viewer 50/50
|
||||
view_splitter_->setSizes({INT_MAX, INT_MAX});
|
||||
QList<int> view_sizes;
|
||||
view_sizes.reserve(views_.size());
|
||||
view_sizes.append(height()/2); // Video
|
||||
view_sizes.append(height()/2); // Audio
|
||||
view_sizes.append(0); // Subtitle (hidden by default)
|
||||
view_splitter_->setSizes(view_sizes);
|
||||
|
||||
// Video and audio are not collapsible, subtitle is
|
||||
view_splitter_->setCollapsible(Track::kVideo, false);
|
||||
view_splitter_->setCollapsible(Track::kAudio, false);
|
||||
view_splitter_->setCollapsible(Track::kSubtitle, true);
|
||||
|
||||
// FIXME: Magic number
|
||||
SetScale(90.0);
|
||||
@@ -786,11 +798,23 @@ void TimelineWidget::ViewMouseMoved(TimelineViewMouseEvent *event)
|
||||
|
||||
if (hover_tool) {
|
||||
hover_tool->HoverMove(event);
|
||||
|
||||
// Special cast for subtitle adding - ensure section is visible
|
||||
if (dynamic_cast<AddTool*>(hover_tool)
|
||||
&& Core::instance()->GetSelectedAddableObject() == Tool::kAddableSubtitle) {
|
||||
QList<int> sz = view_splitter_->sizes();
|
||||
int &subtitle_section_height = sz[Track::kSubtitle];
|
||||
if (subtitle_section_height == 0) {
|
||||
subtitle_section_height = height() / Track::kCount;
|
||||
view_splitter_->setSizes(sz);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void TimelineWidget::ViewMouseReleased(TimelineViewMouseEvent *event)
|
||||
{
|
||||
if (active_tool_) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "add.h"
|
||||
#include "core.h"
|
||||
#include "node/block/subtitle/subtitle.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
#include "node/generator/text/text.h"
|
||||
@@ -54,6 +55,9 @@ void AddTool::MousePress(TimelineViewMouseEvent *event)
|
||||
case olive::Tool::kAddableTone:
|
||||
add_type = Track::kAudio;
|
||||
break;
|
||||
case olive::Tool::kAddableSubtitle:
|
||||
add_type = Track::kSubtitle;
|
||||
break;
|
||||
case olive::Tool::kAddableEmpty:
|
||||
// Leave as "none", which means this block can be placed on any track
|
||||
break;
|
||||
@@ -93,7 +97,12 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event)
|
||||
if (!ghost_->GetAdjustedLength().isNull()) {
|
||||
MultiUndoCommand* command = new MultiUndoCommand();
|
||||
|
||||
ClipBlock* clip = new ClipBlock();
|
||||
ClipBlock* clip;
|
||||
if (Core::instance()->GetSelectedAddableObject() == olive::Tool::kAddableSubtitle) {
|
||||
clip = new SubtitleBlock();
|
||||
} else {
|
||||
clip = new ClipBlock();
|
||||
}
|
||||
clip->set_length_and_media_out(ghost_->GetAdjustedLength());
|
||||
clip->SetLabel(olive::Tool::GetAddableObjectName(Core::instance()->GetSelectedAddableObject()));
|
||||
|
||||
@@ -140,6 +149,9 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event)
|
||||
// Not implemented yet
|
||||
qWarning() << "Unimplemented add object:" << Core::instance()->GetSelectedAddableObject();
|
||||
break;
|
||||
case olive::Tool::kAddableSubtitle:
|
||||
// The block itself is the node we want
|
||||
break;
|
||||
case olive::Tool::kAddableCount:
|
||||
// Invalid value, do nothing
|
||||
break;
|
||||
|
||||
@@ -60,7 +60,7 @@ void TimelineView::mousePressEvent(QMouseEvent *event)
|
||||
TimelineViewMouseEvent timeline_event = CreateMouseEvent(event);
|
||||
|
||||
if (HandPress(event)
|
||||
|| (!GetItemAtScenePos(timeline_event.GetFrame(), timeline_event.GetTrack().index()) && PlayheadPress(event))) {
|
||||
|| (!GetItemAtScenePos(timeline_event.GetFrame(), timeline_event.GetTrack().index()) && Core::instance()->tool() != Tool::kAdd && PlayheadPress(event))) {
|
||||
// Let the parent handle this
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user