media: implement importing SRT files
This commit is contained in:
@@ -46,6 +46,7 @@ extern "C" {
|
||||
#include "common/timecodefunctions.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/diskmanager.h"
|
||||
#include "render/subtitleparams.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
@@ -415,7 +416,51 @@ FootageDescription FFmpegDecoder::Probe(const QString &filename, const QAtomicIn
|
||||
|
||||
} else if (avstream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
|
||||
|
||||
qDebug() << "Subtitle probing: Stub";
|
||||
// Limit to SRT for now...
|
||||
if (avstream->codecpar->codec_id == AV_CODEC_ID_SUBRIP) {
|
||||
SubtitleParams sub;
|
||||
|
||||
AVPacket* pkt = av_packet_alloc();
|
||||
{
|
||||
Instance instance;
|
||||
instance.Open(filename_c, avstream->index);
|
||||
|
||||
//qDebug() << instance.GetSubtitleHeader();
|
||||
|
||||
AVSubtitle avsub;
|
||||
while (instance.GetSubtitle(pkt, &avsub) >= 0) {
|
||||
for (unsigned int j=0; j<avsub.num_rects; j++) {
|
||||
QString ass = avsub.rects[j]->ass;
|
||||
|
||||
int comma = 0;
|
||||
for (int k=0; k<ass.size(); k++) {
|
||||
if (ass.at(k) == ',') {
|
||||
comma++;
|
||||
}
|
||||
|
||||
// HARDCODED: I think FFmpeg always puts the text of SRTs in the 8th section
|
||||
if (comma == 8) {
|
||||
ass = ass.mid(k+1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ass.replace(QStringLiteral("\\n"), QStringLiteral("\n"), Qt::CaseInsensitive);
|
||||
|
||||
TimeRange time(Timecode::timestamp_to_time(pkt->pts, avstream->time_base),
|
||||
Timecode::timestamp_to_time(pkt->pts + pkt->duration, avstream->time_base));
|
||||
|
||||
sub.push_back(Subtitle(time, ass));
|
||||
}
|
||||
avsubtitle_free(&avsub);
|
||||
}
|
||||
|
||||
instance.Close();
|
||||
}
|
||||
av_packet_free(&pkt);
|
||||
|
||||
desc.AddSubtitleStream(sub);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1120,6 +1165,32 @@ int FFmpegDecoder::Instance::GetFrame(AVPacket *pkt, AVFrame *frame)
|
||||
return ret;
|
||||
}
|
||||
|
||||
const char *FFmpegDecoder::Instance::GetSubtitleHeader() const
|
||||
{
|
||||
return reinterpret_cast<const char*>(codec_ctx_->subtitle_header);
|
||||
}
|
||||
|
||||
int FFmpegDecoder::Instance::GetSubtitle(AVPacket *pkt, AVSubtitle *sub)
|
||||
{
|
||||
int ret;
|
||||
|
||||
do {
|
||||
av_packet_unref(pkt);
|
||||
|
||||
ret = av_read_frame(fmt_ctx_, pkt);
|
||||
} while (pkt->stream_index != avstream_->index && ret >= 0);
|
||||
|
||||
if (ret >= 0) {
|
||||
int got_sub;
|
||||
ret = avcodec_decode_subtitle2(codec_ctx_, sub, &got_sub, pkt);
|
||||
if (!got_sub) {
|
||||
ret = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void FFmpegDecoder::Instance::Seek(int64_t timestamp)
|
||||
{
|
||||
avcodec_flush_buffers(codec_ctx_);
|
||||
|
||||
@@ -91,6 +91,10 @@ private:
|
||||
*/
|
||||
int GetFrame(AVPacket* pkt, AVFrame* frame);
|
||||
|
||||
const char *GetSubtitleHeader() const;
|
||||
|
||||
int GetSubtitle(AVPacket* pkt, AVSubtitle* sub);
|
||||
|
||||
void Seek(int64_t timestamp);
|
||||
|
||||
AVFormatContext* fmt_ctx() const
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
|
||||
#include "timecodefunctions.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavutil/mathematics.h>
|
||||
}
|
||||
|
||||
#include <QtMath>
|
||||
|
||||
#include "config/config.h"
|
||||
@@ -254,7 +258,14 @@ rational Timecode::snap_time_to_timebase(const rational &time, const rational &t
|
||||
|
||||
rational Timecode::timestamp_to_time(const int64_t ×tamp, const rational &timebase)
|
||||
{
|
||||
return rational(timestamp) * timebase;
|
||||
int64_t num = int64_t(timebase.numerator()) * timestamp;
|
||||
int64_t den = timebase.denominator();
|
||||
|
||||
int num_r, den_r;
|
||||
|
||||
av_reduce(&num_r, &den_r, num, den, INT_MAX);
|
||||
|
||||
return rational(num_r, den_r);
|
||||
}
|
||||
|
||||
QString Timecode::time_to_timecode(const rational &time, const rational &timebase, const Timecode::Display &display, bool show_plus_if_positive)
|
||||
@@ -310,7 +321,7 @@ int64_t Timecode::rescale_timestamp(const int64_t &ts, const rational &source, c
|
||||
return ts;
|
||||
}
|
||||
|
||||
return qRound64(static_cast<double>(ts) * source.toDouble() / dest.toDouble());
|
||||
return av_rescale_q(ts, source.toAVRational(), dest.toAVRational());
|
||||
}
|
||||
|
||||
int64_t Timecode::rescale_timestamp_ceil(const int64_t &ts, const rational &source, const rational &dest)
|
||||
@@ -319,7 +330,7 @@ int64_t Timecode::rescale_timestamp_ceil(const int64_t &ts, const rational &sour
|
||||
return ts;
|
||||
}
|
||||
|
||||
return qCeil(static_cast<double>(ts) * source.toDouble() / dest.toDouble());
|
||||
return av_rescale_q_rnd(ts, source.toAVRational(), dest.toAVRational(), AV_ROUND_UP);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -93,6 +93,15 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota
|
||||
description = tr("%1 Hz %2 channels").arg(QString::number(ap.sample_rate()), QString::number(ap.channel_count()));
|
||||
break;
|
||||
}
|
||||
case Track::kSubtitle:
|
||||
{
|
||||
SubtitleParams sp = footage_->GetSubtitleParams(reference.index());
|
||||
is_enabled = sp.enabled();
|
||||
|
||||
// FIXME: Language?
|
||||
description = tr("Subtitles");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
stacked_widget_->addWidget(new StreamProperties());
|
||||
description = tr("Unknown");
|
||||
@@ -106,7 +115,8 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *foota
|
||||
|
||||
if (first_usable_stream == -1
|
||||
&& (reference.type() == Track::kVideo
|
||||
|| reference.type() == Track::kAudio)) {
|
||||
|| reference.type() == Track::kAudio
|
||||
|| reference.type() == Track::kSubtitle)) {
|
||||
first_usable_stream = i;
|
||||
}
|
||||
}
|
||||
@@ -163,6 +173,8 @@ void FootagePropertiesDialog::accept()
|
||||
old_stream_enabled = footage_->GetAudioParams(reference.index()).enabled();
|
||||
break;
|
||||
case Track::kSubtitle:
|
||||
old_stream_enabled = footage_->GetSubtitleParams(reference.index()).enabled();
|
||||
break;
|
||||
case Track::kNone:
|
||||
case Track::kCount:
|
||||
break;
|
||||
@@ -218,6 +230,13 @@ void FootagePropertiesDialog::StreamEnableChangeCommand::redo()
|
||||
break;
|
||||
}
|
||||
case Track::kSubtitle:
|
||||
{
|
||||
SubtitleParams sp = footage_->GetSubtitleParams(index_);
|
||||
old_enabled_ = sp.enabled();
|
||||
sp.set_enabled(new_enabled_);
|
||||
footage_->SetSubtitleParams(sp, index_);
|
||||
break;
|
||||
}
|
||||
case Track::kNone:
|
||||
case Track::kCount:
|
||||
break;
|
||||
@@ -242,6 +261,12 @@ void FootagePropertiesDialog::StreamEnableChangeCommand::undo()
|
||||
break;
|
||||
}
|
||||
case Track::kSubtitle:
|
||||
{
|
||||
SubtitleParams sp = footage_->GetSubtitleParams(index_);
|
||||
sp.set_enabled(old_enabled_);
|
||||
footage_->SetSubtitleParams(sp, index_);
|
||||
break;
|
||||
}
|
||||
case Track::kNone:
|
||||
case Track::kCount:
|
||||
break;
|
||||
|
||||
@@ -43,7 +43,11 @@ SubtitleBlock::SubtitleBlock()
|
||||
|
||||
QString SubtitleBlock::Name() const
|
||||
{
|
||||
return tr("Subtitle");
|
||||
if (GetText().isEmpty()) {
|
||||
return tr("Subtitle");
|
||||
} else {
|
||||
return GetText();
|
||||
}
|
||||
}
|
||||
|
||||
QString SubtitleBlock::id() const
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace olive {
|
||||
|
||||
const QString ViewerOutput::kVideoParamsInput = QStringLiteral("video_param_in");
|
||||
const QString ViewerOutput::kAudioParamsInput = QStringLiteral("audio_param_in");
|
||||
const QString ViewerOutput::kSubtitleParamsInput = QStringLiteral("subtitle_param_in");
|
||||
const QString ViewerOutput::kTextureInput = QStringLiteral("tex_in");
|
||||
const QString ViewerOutput::kSamplesInput = QStringLiteral("samples_in");
|
||||
const QString ViewerOutput::kVideoAutoCacheInput = QStringLiteral("video_autocache_in");
|
||||
@@ -48,6 +49,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream
|
||||
|
||||
AddInput(kAudioParamsInput, NodeValue::kAudioParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden));
|
||||
|
||||
AddInput(kSubtitleParamsInput, NodeValue::kSubtitleParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden));
|
||||
|
||||
if (create_buffer_inputs) {
|
||||
AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable));
|
||||
AddInput(kSamplesInput, NodeValue::kSamples, InputFlags(kInputFlagNotKeyframable));
|
||||
@@ -100,6 +103,7 @@ QString ViewerOutput::duration() const
|
||||
// Get first enabled streams
|
||||
VideoParams video = GetFirstEnabledVideoStream();
|
||||
AudioParams audio = GetFirstEnabledAudioStream();
|
||||
SubtitleParams sub = GetFirstEnabledSubtitleStream();
|
||||
|
||||
if (video.is_valid() && video.video_type() != VideoParams::kVideoTypeStill) {
|
||||
// Prioritize video
|
||||
@@ -113,6 +117,8 @@ QString ViewerOutput::duration() const
|
||||
}
|
||||
|
||||
using_timebase = audio.sample_rate_as_time_base();
|
||||
} else if (sub.is_valid()) {
|
||||
using_timebase = OLIVE_CONFIG("DefaultSequenceFrameRate").value<rational>();
|
||||
}
|
||||
|
||||
if (using_timebase.isNull()) {
|
||||
@@ -151,6 +157,11 @@ bool ViewerOutput::HasEnabledAudioStreams() const
|
||||
return GetFirstEnabledAudioStream().is_valid();
|
||||
}
|
||||
|
||||
bool ViewerOutput::HasEnabledSubtitleStreams() const
|
||||
{
|
||||
return GetFirstEnabledSubtitleStream().is_valid();
|
||||
}
|
||||
|
||||
VideoParams ViewerOutput::GetFirstEnabledVideoStream() const
|
||||
{
|
||||
int sz = GetVideoStreamCount();
|
||||
@@ -181,6 +192,21 @@ AudioParams ViewerOutput::GetFirstEnabledAudioStream() const
|
||||
return AudioParams();
|
||||
}
|
||||
|
||||
SubtitleParams ViewerOutput::GetFirstEnabledSubtitleStream() const
|
||||
{
|
||||
int sz = GetSubtitleStreamCount();
|
||||
|
||||
for (int i=0; i<sz; i++) {
|
||||
SubtitleParams sp = GetSubtitleParams(i);
|
||||
|
||||
if (sp.enabled()) {
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
|
||||
return SubtitleParams();
|
||||
}
|
||||
|
||||
void ViewerOutput::set_default_parameters()
|
||||
{
|
||||
int width = OLIVE_CONFIG("DefaultSequenceWidth").toInt();
|
||||
@@ -276,6 +302,16 @@ QVector<Track::Reference> ViewerOutput::GetEnabledStreamsAsReferences() const
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
int sp_sz = GetSubtitleStreamCount();
|
||||
|
||||
for (int i=0; i<sp_sz; i++) {
|
||||
if (GetSubtitleParams(i).enabled()) {
|
||||
refs.append(Track::Reference(Track::kSubtitle, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
@@ -285,6 +321,7 @@ void ViewerOutput::Retranslate()
|
||||
|
||||
SetInputName(kVideoParamsInput, tr("Video Parameters"));
|
||||
SetInputName(kAudioParamsInput, tr("Audio Parameters"));
|
||||
SetInputName(kSubtitleParamsInput, tr("Subtitle Parameters"));
|
||||
|
||||
if (HasInputWithID(kTextureInput)) {
|
||||
SetInputName(kTextureInput, tr("Texture"));
|
||||
@@ -520,6 +557,8 @@ int ViewerOutput::AddStream(Track::Type type, const QVariant& value)
|
||||
id = kVideoParamsInput;
|
||||
} else if (type == Track::kAudio) {
|
||||
id = kAudioParamsInput;
|
||||
} else if (type == Track::kSubtitle) {
|
||||
id = kSubtitleParamsInput;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#include "render/audioparams.h"
|
||||
#include "render/audioplaybackcache.h"
|
||||
#include "render/framehashcache.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "render/subtitleparams.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "timeline/timelinepoints.h"
|
||||
|
||||
@@ -88,6 +88,17 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
SubtitleParams GetSubtitleParams(int index = 0) const
|
||||
{
|
||||
// This check isn't strictly necessary (GetStandardValue will return a null VideoParams anyway),
|
||||
// but it does suppress a warning message that we don't need
|
||||
if (index < InputArraySize(kSubtitleParamsInput)) {
|
||||
return GetStandardValue(kSubtitleParamsInput, index).value<SubtitleParams>();
|
||||
} else {
|
||||
return SubtitleParams();
|
||||
}
|
||||
}
|
||||
|
||||
void SetVideoParams(const VideoParams &video, int index = 0)
|
||||
{
|
||||
SetStandardValue(kVideoParamsInput, QVariant::fromValue(video), index);
|
||||
@@ -98,6 +109,11 @@ public:
|
||||
SetStandardValue(kAudioParamsInput, QVariant::fromValue(audio), index);
|
||||
}
|
||||
|
||||
void SetSubtitleParams(const SubtitleParams &subs, int index = 0)
|
||||
{
|
||||
SetStandardValue(kSubtitleParamsInput, QVariant::fromValue(subs), index);
|
||||
}
|
||||
|
||||
int GetVideoStreamCount() const
|
||||
{
|
||||
return InputArraySize(kVideoParamsInput);
|
||||
@@ -108,16 +124,23 @@ public:
|
||||
return InputArraySize(kAudioParamsInput);
|
||||
}
|
||||
|
||||
int GetSubtitleStreamCount() const
|
||||
{
|
||||
return InputArraySize(kSubtitleParamsInput);
|
||||
}
|
||||
|
||||
int GetTotalStreamCount() const
|
||||
{
|
||||
return GetVideoStreamCount() + GetAudioStreamCount();
|
||||
return GetVideoStreamCount() + GetAudioStreamCount() + GetSubtitleStreamCount();
|
||||
}
|
||||
|
||||
bool HasEnabledVideoStreams() const;
|
||||
bool HasEnabledAudioStreams() const;
|
||||
bool HasEnabledSubtitleStreams() const;
|
||||
|
||||
VideoParams GetFirstEnabledVideoStream() const;
|
||||
AudioParams GetFirstEnabledAudioStream() const;
|
||||
SubtitleParams GetFirstEnabledSubtitleStream() const;
|
||||
|
||||
const rational &GetLength() const { return last_length_; }
|
||||
const rational &GetVideoLength() const { return video_length_; }
|
||||
@@ -191,6 +214,7 @@ public:
|
||||
|
||||
static const QString kVideoParamsInput;
|
||||
static const QString kAudioParamsInput;
|
||||
static const QString kSubtitleParamsInput;
|
||||
|
||||
static const QString kTextureInput;
|
||||
static const QString kSamplesInput;
|
||||
|
||||
@@ -121,6 +121,10 @@ void Footage::InputValueChangedEvent(const QString &input, int element)
|
||||
AddStream(Track::kAudio, QVariant::fromValue(footage_info.GetAudioStreams().at(i)));
|
||||
}
|
||||
|
||||
for (int i=0; i<footage_info.GetSubtitleStreams().size(); i++) {
|
||||
AddStream(Track::kSubtitle, QVariant::fromValue(footage_info.GetSubtitleStreams().at(i)));
|
||||
}
|
||||
|
||||
SetValid();
|
||||
}
|
||||
|
||||
@@ -146,6 +150,12 @@ rational Footage::VerifyLengthInternal(Track::Type type) const
|
||||
if (first_stream.is_valid()) {
|
||||
return Timecode::timestamp_to_time(first_stream.duration(), first_stream.time_base());
|
||||
}
|
||||
} else if (type == Track::kSubtitle) {
|
||||
SubtitleParams first_stream = GetFirstEnabledSubtitleStream();
|
||||
|
||||
if (first_stream.is_valid()) {
|
||||
return first_stream.duration();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -165,6 +175,7 @@ void Footage::Clear()
|
||||
// Clear all dynamically created inputs
|
||||
InputArrayResize(kVideoParamsInput, 0);
|
||||
InputArrayResize(kAudioParamsInput, 0);
|
||||
InputArrayResize(kSubtitleParamsInput, 0);
|
||||
|
||||
// Clear decoder link
|
||||
decoder_.clear();
|
||||
@@ -205,13 +216,19 @@ void Footage::set_timestamp(const qint64 &t)
|
||||
|
||||
int Footage::GetStreamIndex(Track::Type type, int index) const
|
||||
{
|
||||
if (type == Track::kVideo) {
|
||||
switch (type) {
|
||||
case Track::kVideo:
|
||||
return GetVideoParams(index).stream_index();
|
||||
} else if (type == Track::kAudio) {
|
||||
case Track::kAudio:
|
||||
return GetAudioParams(index).stream_index();
|
||||
} else {
|
||||
return -1;
|
||||
case Track::kSubtitle:
|
||||
return GetSubtitleParams(index).stream_index();
|
||||
case Track::kNone:
|
||||
case Track::kCount:
|
||||
break;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
Track::Reference Footage::GetReferenceFromRealIndex(int real_index) const
|
||||
@@ -229,6 +246,12 @@ Track::Reference Footage::GetReferenceFromRealIndex(int real_index) const
|
||||
}
|
||||
}
|
||||
|
||||
for (int i=0; i<GetSubtitleStreamCount(); i++) {
|
||||
if (GetSubtitleParams(i).stream_index() == real_index) {
|
||||
return Track::Reference(Track::kSubtitle, i);
|
||||
}
|
||||
}
|
||||
|
||||
return Track::Reference();
|
||||
}
|
||||
|
||||
@@ -249,6 +272,8 @@ QIcon Footage::icon() const
|
||||
return icon::Audio;
|
||||
} else if (s.is_valid() && s.video_type() == VideoParams::kVideoTypeStill) {
|
||||
return icon::Image;
|
||||
} else if (HasEnabledSubtitleStreams()) {
|
||||
return icon::TextSmallCaps; // FIXME: Procure icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,6 +300,12 @@ QString Footage::DescribeAudioStream(const AudioParams ¶ms)
|
||||
QString::number(params.sample_rate()));
|
||||
}
|
||||
|
||||
QString Footage::DescribeSubtitleStream(const SubtitleParams ¶ms)
|
||||
{
|
||||
return tr("%1: Subtitle")
|
||||
.arg(QString::number(params.stream_index()));
|
||||
}
|
||||
|
||||
void Footage::Hash(QCryptographicHash &hash, const NodeGlobals &globals, const VideoParams &video_params) const
|
||||
{
|
||||
super::Hash(hash, globals, video_params);
|
||||
@@ -475,6 +506,16 @@ void Footage::UpdateTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
int sp_sz = GetSubtitleStreamCount();
|
||||
for (int i=0; i<sp_sz; i++) {
|
||||
SubtitleParams p = GetSubtitleParams(i);
|
||||
|
||||
if (p.enabled()) {
|
||||
tip.append("\n");
|
||||
tip.append(DescribeSubtitleStream(p));
|
||||
}
|
||||
}
|
||||
|
||||
SetToolTip(tip);
|
||||
} else {
|
||||
SetToolTip(tr("Invalid"));
|
||||
|
||||
@@ -173,6 +173,7 @@ public:
|
||||
|
||||
static QString DescribeVideoStream(const VideoParams& params);
|
||||
static QString DescribeAudioStream(const AudioParams& params);
|
||||
static QString DescribeSubtitleStream(const SubtitleParams& params);
|
||||
|
||||
virtual void Value(const NodeValueRow& value, const NodeGlobals &globals, NodeValueTable *table) const override;
|
||||
|
||||
|
||||
@@ -67,6 +67,10 @@ bool FootageDescription::Load(const QString &filename)
|
||||
AudioParams ap;
|
||||
ap.Load(&reader);
|
||||
AddAudioStream(ap);
|
||||
} else if (reader.name() == QStringLiteral("subtitle")) {
|
||||
SubtitleParams sp;
|
||||
sp.Load(&reader);
|
||||
AddSubtitleStream(sp);
|
||||
} else {
|
||||
reader.skipCurrentElement();
|
||||
}
|
||||
@@ -124,6 +128,12 @@ bool FootageDescription::Save(const QString &filename) const
|
||||
writer.writeEndElement(); // audio
|
||||
}
|
||||
|
||||
foreach (const SubtitleParams& sp, subtitle_streams_) {
|
||||
writer.writeStartElement(QStringLiteral("subtitle"));
|
||||
sp.Save(&writer);
|
||||
writer.writeEndElement(); // audio
|
||||
}
|
||||
|
||||
writer.writeEndElement(); // streams
|
||||
|
||||
writer.writeEndElement(); // streamcache
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "node/output/track/track.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/subtitleparams.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace olive {
|
||||
@@ -37,7 +38,7 @@ public:
|
||||
|
||||
bool IsValid() const
|
||||
{
|
||||
return !decoder_.isEmpty() && (!video_streams_.isEmpty() || !audio_streams_.isEmpty());
|
||||
return !decoder_.isEmpty() && (!video_streams_.isEmpty() || !audio_streams_.isEmpty() || !subtitle_streams_.isEmpty());
|
||||
}
|
||||
|
||||
const QString& decoder() const
|
||||
@@ -59,12 +60,21 @@ public:
|
||||
audio_streams_.append(audio_params);
|
||||
}
|
||||
|
||||
void AddSubtitleStream(const SubtitleParams& sub_params)
|
||||
{
|
||||
Q_ASSERT(!HasStreamIndex(sub_params.stream_index()));
|
||||
|
||||
subtitle_streams_.append(sub_params);
|
||||
}
|
||||
|
||||
Track::Type GetTypeOfStream(int index)
|
||||
{
|
||||
if (StreamIsVideo(index)) {
|
||||
return Track::kVideo;
|
||||
} else if (StreamIsAudio(index)) {
|
||||
return Track::kAudio;
|
||||
} else if (StreamIsSubtitle(index)) {
|
||||
return Track::kSubtitle;
|
||||
} else {
|
||||
return Track::kNone;
|
||||
}
|
||||
@@ -92,9 +102,20 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
bool StreamIsSubtitle(int index) const
|
||||
{
|
||||
foreach (const SubtitleParams& sp, subtitle_streams_) {
|
||||
if (sp.stream_index() == index) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasStreamIndex(int index) const
|
||||
{
|
||||
return StreamIsVideo(index) || StreamIsAudio(index);
|
||||
return StreamIsVideo(index) || StreamIsAudio(index) || StreamIsSubtitle(index);
|
||||
}
|
||||
|
||||
bool Load(const QString& filename);
|
||||
@@ -111,8 +132,13 @@ public:
|
||||
return audio_streams_;
|
||||
}
|
||||
|
||||
const QVector<SubtitleParams>& GetSubtitleStreams() const
|
||||
{
|
||||
return subtitle_streams_;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr unsigned kFootageMetaVersion = 1;
|
||||
static constexpr unsigned kFootageMetaVersion = 2;
|
||||
|
||||
QString decoder_;
|
||||
|
||||
@@ -120,6 +146,8 @@ private:
|
||||
|
||||
QVector<AudioParams> audio_streams_;
|
||||
|
||||
QVector<SubtitleParams> subtitle_streams_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "common/bezier.h"
|
||||
#include "common/tohex.h"
|
||||
#include "render/audioparams.h"
|
||||
#include "render/subtitleparams.h"
|
||||
#include "render/videoparams.h"
|
||||
#include "render/color.h"
|
||||
|
||||
@@ -130,6 +131,7 @@ QByteArray NodeValue::ValueToBytes(NodeValue::Type type, const QVariant &value)
|
||||
return value.value<AudioParams>().toBytes();
|
||||
|
||||
// These types have no persistent input
|
||||
case kSubtitleParams:
|
||||
case kNone:
|
||||
case kTexture:
|
||||
case kSamples:
|
||||
@@ -345,6 +347,8 @@ QString NodeValue::GetPrettyDataTypeName(Type type)
|
||||
return QCoreApplication::translate("NodeValue", "Video Parameters");
|
||||
case kAudioParams:
|
||||
return QCoreApplication::translate("NodeValue", "Audio Parameters");
|
||||
case kSubtitleParams:
|
||||
return QCoreApplication::translate("NodeValue", "Subtitle Parameters");
|
||||
|
||||
case kDataTypeCount:
|
||||
break;
|
||||
@@ -394,6 +398,8 @@ QString NodeValue::GetDataTypeName(Type type)
|
||||
return QStringLiteral("vparam");
|
||||
case kAudioParams:
|
||||
return QStringLiteral("aparam");
|
||||
case kSubtitleParams:
|
||||
return QStringLiteral("sparam");
|
||||
case kDataTypeCount:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -178,6 +178,13 @@ public:
|
||||
*/
|
||||
kAudioParams,
|
||||
|
||||
/**
|
||||
* Subtitle Parameters type
|
||||
*
|
||||
* Resolves to `SubtitleParams`
|
||||
*/
|
||||
kSubtitleParams,
|
||||
|
||||
/**
|
||||
* End of list
|
||||
*/
|
||||
|
||||
@@ -518,6 +518,7 @@ void OpenGLRenderer::Blit(QVariant s, ShaderJob job, Texture *destination, Video
|
||||
case NodeValue::kFile:
|
||||
case NodeValue::kVideoParams:
|
||||
case NodeValue::kAudioParams:
|
||||
case NodeValue::kSubtitleParams:
|
||||
case NodeValue::kBezier:
|
||||
case NodeValue::kNone:
|
||||
case NodeValue::kDataTypeCount:
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include "common/xmlutils.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
QString SubtitleParams::GenerateASSHeader()
|
||||
@@ -106,4 +108,56 @@ QString SubtitleParams::GenerateASSHeader()
|
||||
return ass_code;
|
||||
}
|
||||
|
||||
void SubtitleParams::Load(QXmlStreamReader *reader)
|
||||
{
|
||||
this->clear();
|
||||
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("streamindex")) {
|
||||
set_stream_index(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("enabled")) {
|
||||
set_enabled(reader->readElementText().toInt());
|
||||
} else if (reader->name() == QStringLiteral("subtitles")) {
|
||||
while (XMLReadNextStartElement(reader)) {
|
||||
if (reader->name() == QStringLiteral("subtitle")) {
|
||||
rational in, out;
|
||||
QString text;
|
||||
|
||||
XMLAttributeLoop(reader, attr) {
|
||||
if (attr.name() == QStringLiteral("in")) {
|
||||
in = rational::fromString(attr.value().toString());
|
||||
} else if (attr.name() == QStringLiteral("out")) {
|
||||
out = rational::fromString(attr.value().toString());
|
||||
}
|
||||
}
|
||||
|
||||
text = reader->readElementText();
|
||||
|
||||
this->push_back(Subtitle(TimeRange(in, out), text));
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reader->skipCurrentElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SubtitleParams::Save(QXmlStreamWriter *writer) const
|
||||
{
|
||||
writer->writeTextElement(QStringLiteral("streamindex"), QString::number(stream_index_));
|
||||
writer->writeTextElement(QStringLiteral("enabled"), QString::number(enabled_));
|
||||
|
||||
writer->writeStartElement(QStringLiteral("subtitles"));
|
||||
for (auto it=this->cbegin(); it!=this->cend(); it++) {
|
||||
writer->writeStartElement(QStringLiteral("subtitle"));
|
||||
writer->writeAttribute(QStringLiteral("in"), it->time().in().toString());
|
||||
writer->writeAttribute(QStringLiteral("out"), it->time().out().toString());
|
||||
writer->writeCharacters(it->text());
|
||||
writer->writeEndElement(); // subtitle
|
||||
}
|
||||
writer->writeEndElement(); // subtitles
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,16 +21,84 @@
|
||||
#ifndef SUBTITLEPARAMS_H
|
||||
#define SUBTITLEPARAMS_H
|
||||
|
||||
#include <QRect>
|
||||
#include <QString>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include "common/timerange.h"
|
||||
|
||||
namespace olive {
|
||||
|
||||
class SubtitleParams {
|
||||
class Subtitle
|
||||
{
|
||||
public:
|
||||
Subtitle() = default;
|
||||
|
||||
Subtitle(const TimeRange &time, const QString &text) :
|
||||
range_(time),
|
||||
text_(text)
|
||||
{
|
||||
}
|
||||
|
||||
const TimeRange &time() const { return range_; }
|
||||
void set_time(const TimeRange &t) { range_ = t; }
|
||||
|
||||
const QString &text() const { return text_; }
|
||||
void set_text(const QString &t) { text_ = t; }
|
||||
|
||||
private:
|
||||
TimeRange range_;
|
||||
|
||||
QString text_;
|
||||
|
||||
};
|
||||
|
||||
class SubtitleParams : public std::vector<Subtitle>
|
||||
{
|
||||
public:
|
||||
SubtitleParams()
|
||||
{
|
||||
stream_index_ = 0;
|
||||
enabled_ = true;
|
||||
}
|
||||
|
||||
static QString GenerateASSHeader();
|
||||
|
||||
void Load(QXmlStreamReader* reader);
|
||||
|
||||
void Save(QXmlStreamWriter* writer) const;
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return !this->empty();
|
||||
}
|
||||
|
||||
rational duration() const
|
||||
{
|
||||
if (this->empty()) {
|
||||
return 0;
|
||||
} else {
|
||||
return back().time().out();
|
||||
}
|
||||
}
|
||||
|
||||
int stream_index() const { return stream_index_; }
|
||||
void set_stream_index(int i) { stream_index_ = i; }
|
||||
|
||||
bool enabled() const { return enabled_; }
|
||||
void set_enabled(bool e) { enabled_ = e; }
|
||||
|
||||
private:
|
||||
int stream_index_;
|
||||
|
||||
bool enabled_;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(olive::Subtitle)
|
||||
Q_DECLARE_METATYPE(olive::SubtitleParams)
|
||||
|
||||
#endif // SUBTITLEPARAMS_H
|
||||
|
||||
@@ -89,6 +89,7 @@ void NodeParamViewWidgetBridge::CreateWidgets()
|
||||
case NodeValue::kSamples:
|
||||
case NodeValue::kVideoParams:
|
||||
case NodeValue::kAudioParams:
|
||||
case NodeValue::kSubtitleParams:
|
||||
case NodeValue::kDataTypeCount:
|
||||
break;
|
||||
case NodeValue::kInt:
|
||||
@@ -240,6 +241,7 @@ void NodeParamViewWidgetBridge::WidgetCallback()
|
||||
case NodeValue::kSamples:
|
||||
case NodeValue::kVideoParams:
|
||||
case NodeValue::kAudioParams:
|
||||
case NodeValue::kSubtitleParams:
|
||||
case NodeValue::kDataTypeCount:
|
||||
break;
|
||||
case NodeValue::kInt:
|
||||
@@ -417,6 +419,7 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues()
|
||||
case NodeValue::kSamples:
|
||||
case NodeValue::kVideoParams:
|
||||
case NodeValue::kAudioParams:
|
||||
case NodeValue::kSubtitleParams:
|
||||
case NodeValue::kDataTypeCount:
|
||||
break;
|
||||
case NodeValue::kInt:
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "core.h"
|
||||
#include "dialog/sequence/sequence.h"
|
||||
#include "node/audio/volume/volume.h"
|
||||
#include "node/block/subtitle/subtitle.h"
|
||||
#include "node/distort/transform/transformdistortnode.h"
|
||||
#include "node/generator/matrix/matrix.h"
|
||||
#include "node/math/math/math.h"
|
||||
@@ -154,6 +155,7 @@ void ImportTool::DragLeave(QDragLeaveEvent* event)
|
||||
{
|
||||
if (!dragged_footage_.isEmpty()) {
|
||||
parent()->ClearGhosts();
|
||||
parent()->ClearTentativeSubtitleTrack();
|
||||
dragged_footage_.clear();
|
||||
|
||||
event->accept();
|
||||
@@ -235,25 +237,27 @@ void ImportTool::FootageToGhosts(rational ghost_start, const DraggedFootageData
|
||||
// Create ghosts
|
||||
foreach (const Track::Reference& ref, it->second) {
|
||||
Track::Type track_type = ref.type();
|
||||
Track::Reference dest_track(track_type, track_offsets.at(track_type));
|
||||
|
||||
TimelineViewGhostItem* ghost = new TimelineViewGhostItem();
|
||||
if (track_type == Track::kVideo || track_type == Track::kAudio) {
|
||||
auto ghost = CreateGhost(TimeRange(ghost_start, ghost_start + footage_duration), ghost_in, dest_track);
|
||||
|
||||
ghost->SetIn(ghost_start);
|
||||
ghost->SetOut(ghost_start + footage_duration);
|
||||
ghost->SetMediaIn(ghost_in);
|
||||
ghost->SetTrack(Track::Reference(track_type, track_offsets.at(track_type)));
|
||||
// Increment track count for this track type
|
||||
track_offsets[track_type]++;
|
||||
|
||||
snap_points_.push_back(ghost->GetIn());
|
||||
snap_points_.push_back(ghost->GetOut());
|
||||
TimelineViewGhostItem::AttachedFootage af = {it->first, ref.ToString()};
|
||||
ghost->SetData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(af));
|
||||
} else if (track_type == Track::kSubtitle) {
|
||||
SubtitleParams sp = footage->GetSubtitleParams(ref.index());
|
||||
|
||||
// Increment track count for this track type
|
||||
track_offsets[track_type]++;
|
||||
for (const Subtitle &sub : sp) {
|
||||
auto ghost = CreateGhost(sub.time() + ghost_start, 0, dest_track);
|
||||
|
||||
TimelineViewGhostItem::AttachedFootage af = {it->first, ref.ToString()};
|
||||
ghost->SetData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(af));
|
||||
ghost->SetMode(Timeline::kMove);
|
||||
ghost->SetData(TimelineViewGhostItem::kAttachedFootage, QVariant::fromValue(sub));
|
||||
}
|
||||
|
||||
parent()->AddGhost(ghost);
|
||||
parent()->AddTentativeSubtitleTrack();
|
||||
}
|
||||
}
|
||||
|
||||
// Stack each ghost one after the other
|
||||
@@ -276,6 +280,10 @@ void ImportTool::DropGhosts(bool insert)
|
||||
{
|
||||
MultiUndoCommand* command = new MultiUndoCommand();
|
||||
|
||||
if (MultiUndoCommand *c = parent()->TakeSubtitleSectionCommand()) {
|
||||
command->add_child(c);
|
||||
}
|
||||
|
||||
NodeGraph* dst_graph = nullptr;
|
||||
Sequence* sequence = this->sequence();
|
||||
bool open_sequence = false;
|
||||
@@ -384,69 +392,83 @@ void ImportTool::DropGhosts(bool insert)
|
||||
|
||||
for (int i=0;i<parent()->GetGhostItems().size();i++) {
|
||||
TimelineViewGhostItem* ghost = parent()->GetGhostItems().at(i);
|
||||
Block* block = nullptr;
|
||||
|
||||
TimelineViewGhostItem::AttachedFootage footage_stream = ghost->GetData(TimelineViewGhostItem::kAttachedFootage).value<TimelineViewGhostItem::AttachedFootage>();
|
||||
Track::Type track_type = ghost->GetAdjustedTrack().type();
|
||||
if (track_type == Track::kVideo || track_type == Track::kAudio) {
|
||||
TimelineViewGhostItem::AttachedFootage footage_stream = ghost->GetData(TimelineViewGhostItem::kAttachedFootage).value<TimelineViewGhostItem::AttachedFootage>();
|
||||
|
||||
ClipBlock* clip = new ClipBlock();
|
||||
clip->set_media_in(ghost->GetMediaIn());
|
||||
clip->set_length_and_media_out(ghost->GetLength());
|
||||
clip->SetLabel(footage_stream.footage->GetLabel());
|
||||
command->add_child(new NodeAddCommand(dst_graph, clip));
|
||||
ClipBlock* clip = new ClipBlock();
|
||||
block = clip;
|
||||
clip->set_media_in(ghost->GetMediaIn());
|
||||
clip->SetLabel(footage_stream.footage->GetLabel());
|
||||
command->add_child(new NodeAddCommand(dst_graph, clip));
|
||||
|
||||
// Position clip in its own context
|
||||
command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0)));
|
||||
// Position clip in its own context
|
||||
command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0)));
|
||||
|
||||
int dep_pos = kDefaultDistanceFromOutput;
|
||||
int dep_pos = kDefaultDistanceFromOutput;
|
||||
|
||||
// Position footage in its context
|
||||
command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(dep_pos, 0)));
|
||||
// Position footage in its context
|
||||
command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(dep_pos, 0)));
|
||||
|
||||
dep_pos++;
|
||||
dep_pos++;
|
||||
|
||||
switch (Track::Reference::TypeFromString(footage_stream.output)) {
|
||||
case Track::kVideo:
|
||||
{
|
||||
TransformDistortNode* transform = new TransformDistortNode();
|
||||
command->add_child(new NodeAddCommand(dst_graph, transform));
|
||||
switch (Track::Reference::TypeFromString(footage_stream.output)) {
|
||||
case Track::kVideo:
|
||||
{
|
||||
TransformDistortNode* transform = new TransformDistortNode();
|
||||
command->add_child(new NodeAddCommand(dst_graph, transform));
|
||||
|
||||
command->add_child(new NodeSetValueHintCommand(transform, TransformDistortNode::kTextureInput, -1, Node::ValueHint({NodeValue::kTexture}, footage_stream.output)));
|
||||
command->add_child(new NodeSetValueHintCommand(transform, TransformDistortNode::kTextureInput, -1, Node::ValueHint({NodeValue::kTexture}, footage_stream.output)));
|
||||
|
||||
command->add_child(new NodeEdgeAddCommand(footage_stream.footage, NodeInput(transform, TransformDistortNode::kTextureInput)));
|
||||
command->add_child(new NodeEdgeAddCommand(transform, NodeInput(clip, ClipBlock::kBufferIn)));
|
||||
command->add_child(new NodeSetPositionCommand(transform, clip, QPointF(dep_pos, 0)));
|
||||
break;
|
||||
command->add_child(new NodeEdgeAddCommand(footage_stream.footage, NodeInput(transform, TransformDistortNode::kTextureInput)));
|
||||
command->add_child(new NodeEdgeAddCommand(transform, NodeInput(clip, ClipBlock::kBufferIn)));
|
||||
command->add_child(new NodeSetPositionCommand(transform, clip, QPointF(dep_pos, 0)));
|
||||
break;
|
||||
}
|
||||
case Track::kAudio:
|
||||
{
|
||||
VolumeNode* volume_node = new VolumeNode();
|
||||
command->add_child(new NodeAddCommand(dst_graph, volume_node));
|
||||
|
||||
command->add_child(new NodeSetValueHintCommand(volume_node, VolumeNode::kSamplesInput, -1, Node::ValueHint({NodeValue::kSamples}, footage_stream.output)));
|
||||
|
||||
command->add_child(new NodeEdgeAddCommand(footage_stream.footage, NodeInput(volume_node, VolumeNode::kSamplesInput)));
|
||||
command->add_child(new NodeEdgeAddCommand(volume_node, NodeInput(clip, ClipBlock::kBufferIn)));
|
||||
command->add_child(new NodeSetPositionCommand(volume_node, clip, QPointF(dep_pos, 0)));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Link any clips so far that share the same Footage with this one
|
||||
for (int j=0;j<i;j++) {
|
||||
TimelineViewGhostItem::AttachedFootage footage_compare = parent()->GetGhostItems().at(j)->GetData(TimelineViewGhostItem::kAttachedFootage).value<TimelineViewGhostItem::AttachedFootage>();
|
||||
|
||||
if (footage_compare.footage == footage_stream.footage) {
|
||||
Block::Link(block_items.at(j), clip);
|
||||
}
|
||||
}
|
||||
} else if (track_type == Track::kSubtitle) {
|
||||
Subtitle src = ghost->GetData(TimelineViewGhostItem::kAttachedFootage).value<Subtitle>();
|
||||
SubtitleBlock *sub = new SubtitleBlock();
|
||||
sub->SetText(src.text());
|
||||
block = sub;
|
||||
|
||||
command->add_child(new NodeAddCommand(dst_graph, sub));
|
||||
command->add_child(new NodeSetPositionCommand(sub, sub, QPointF(0, 0)));
|
||||
}
|
||||
case Track::kAudio:
|
||||
{
|
||||
VolumeNode* volume_node = new VolumeNode();
|
||||
command->add_child(new NodeAddCommand(dst_graph, volume_node));
|
||||
|
||||
command->add_child(new NodeSetValueHintCommand(volume_node, VolumeNode::kSamplesInput, -1, Node::ValueHint({NodeValue::kSamples}, footage_stream.output)));
|
||||
|
||||
command->add_child(new NodeEdgeAddCommand(footage_stream.footage, NodeInput(volume_node, VolumeNode::kSamplesInput)));
|
||||
command->add_child(new NodeEdgeAddCommand(volume_node, NodeInput(clip, ClipBlock::kBufferIn)));
|
||||
command->add_child(new NodeSetPositionCommand(volume_node, clip, QPointF(dep_pos, 0)));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
block->set_length_and_media_out(ghost->GetLength());
|
||||
|
||||
command->add_child(new TrackPlaceBlockCommand(sequence->track_list(ghost->GetAdjustedTrack().type()),
|
||||
ghost->GetAdjustedTrack().index(),
|
||||
clip,
|
||||
block,
|
||||
ghost->GetAdjustedIn()));
|
||||
|
||||
block_items.replace(i, clip);
|
||||
|
||||
// Link any clips so far that share the same Footage with this one
|
||||
for (int j=0;j<i;j++) {
|
||||
TimelineViewGhostItem::AttachedFootage footage_compare = parent()->GetGhostItems().at(j)->GetData(TimelineViewGhostItem::kAttachedFootage).value<TimelineViewGhostItem::AttachedFootage>();
|
||||
|
||||
if (footage_compare.footage == footage_stream.footage) {
|
||||
Block::Link(block_items.at(j), clip);
|
||||
}
|
||||
}
|
||||
block_items.replace(i, block);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,4 +482,24 @@ void ImportTool::DropGhosts(bool insert)
|
||||
dragged_footage_.clear();
|
||||
}
|
||||
|
||||
TimelineViewGhostItem* ImportTool::CreateGhost(const TimeRange &range, const rational &media_in, const Track::Reference &track)
|
||||
{
|
||||
TimelineViewGhostItem* ghost = new TimelineViewGhostItem();
|
||||
|
||||
ghost->SetIn(range.in());
|
||||
ghost->SetOut(range.out());
|
||||
ghost->SetMediaIn(media_in);
|
||||
ghost->SetTrack(track);
|
||||
|
||||
snap_points_.push_back(ghost->GetIn());
|
||||
snap_points_.push_back(ghost->GetOut());
|
||||
|
||||
|
||||
ghost->SetMode(Timeline::kMove);
|
||||
|
||||
parent()->AddGhost(ghost);
|
||||
|
||||
return ghost;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ private:
|
||||
|
||||
void DropGhosts(bool insert);
|
||||
|
||||
TimelineViewGhostItem* CreateGhost(const TimeRange &range, const rational &media_in, const Track::Reference &track);
|
||||
|
||||
DraggedFootageData dragged_footage_;
|
||||
|
||||
int import_pre_buffer_;
|
||||
|
||||
Reference in New Issue
Block a user