2013-09-12 17:52:10 -05:00
|
|
|
/**
|
|
|
|
|
* @file
|
|
|
|
|
* @brief Source file for Timeline class
|
|
|
|
|
* @author Jonathan Thomas <jonathan@openshot.org>
|
|
|
|
|
*
|
|
|
|
|
* @section LICENSE
|
|
|
|
|
*
|
2014-03-29 18:49:22 -05:00
|
|
|
* Copyright (c) 2008-2014 OpenShot Studios, LLC
|
|
|
|
|
* <http://www.openshotstudios.com/>. This file is part of
|
|
|
|
|
* OpenShot Library (libopenshot), an open-source project dedicated to
|
|
|
|
|
* delivering high quality video editing and animation solutions to the
|
|
|
|
|
* world. For more information visit <http://www.openshot.org/>.
|
2013-09-12 17:52:10 -05:00
|
|
|
*
|
2014-03-29 18:49:22 -05:00
|
|
|
* OpenShot Library (libopenshot) is free software: you can redistribute it
|
|
|
|
|
* and/or modify it under the terms of the GNU Affero General Public License
|
|
|
|
|
* as published by the Free Software Foundation, either version 3 of the
|
|
|
|
|
* License, or (at your option) any later version.
|
2013-09-12 17:52:10 -05:00
|
|
|
*
|
2014-03-29 18:49:22 -05:00
|
|
|
* OpenShot Library (libopenshot) 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 Affero General Public License for more details.
|
2013-09-12 17:52:10 -05:00
|
|
|
*
|
2014-03-29 18:49:22 -05:00
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
* along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*
|
|
|
|
|
* Also, if your software can interact with users remotely through a computer
|
|
|
|
|
* network, you should also make sure that it provides a way for users to
|
|
|
|
|
* get its source. For example, if your program is a web application, its
|
|
|
|
|
* interface could display a "Source" link that leads users to an archive
|
|
|
|
|
* of the code. There are many ways you could offer source, and different
|
|
|
|
|
* solutions will be better for different programs; see section 13 for the
|
|
|
|
|
* specific requirements.
|
|
|
|
|
*
|
|
|
|
|
* You should also get your employer (if you work as a programmer) or school,
|
|
|
|
|
* if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
|
|
|
* For more information on this, and how to apply and follow the GNU AGPL, see
|
|
|
|
|
* <http://www.gnu.org/licenses/>.
|
2013-09-12 17:52:10 -05:00
|
|
|
*/
|
|
|
|
|
|
2012-10-03 01:55:24 -05:00
|
|
|
#include "../include/Timeline.h"
|
|
|
|
|
|
|
|
|
|
using namespace openshot;
|
|
|
|
|
|
|
|
|
|
// Default Constructor for the timeline (which sets the canvas width and height)
|
2014-01-05 22:37:11 -06:00
|
|
|
Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int channels) : is_open(false)
|
2012-10-03 01:55:24 -05:00
|
|
|
{
|
|
|
|
|
// Init viewport size (curve based, because it can be animated)
|
|
|
|
|
viewport_scale = Keyframe(100.0);
|
|
|
|
|
viewport_x = Keyframe(0.0);
|
|
|
|
|
viewport_y = Keyframe(0.0);
|
2012-11-12 17:21:21 -06:00
|
|
|
|
2012-11-29 17:28:22 -06:00
|
|
|
// Init background color
|
|
|
|
|
color.red = Keyframe(0.0);
|
|
|
|
|
color.green = Keyframe(0.0);
|
|
|
|
|
color.blue = Keyframe(0.0);
|
|
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
// Init cache
|
|
|
|
|
int64 bytes = height * width * 4 + (44100 * 2 * 4);
|
2013-02-15 00:23:55 -06:00
|
|
|
final_cache = Cache(2 * bytes); // 20 frames, 4 colors of chars, 2 audio channels of 4 byte floats
|
2012-11-20 16:22:50 -06:00
|
|
|
|
|
|
|
|
// Init FileInfo struct (clear all values)
|
|
|
|
|
InitFileInfo();
|
|
|
|
|
info.width = width;
|
|
|
|
|
info.height = height;
|
2014-01-05 22:37:11 -06:00
|
|
|
info.fps = fps;
|
2012-11-20 16:22:50 -06:00
|
|
|
info.sample_rate = sample_rate;
|
|
|
|
|
info.channels = channels;
|
2014-01-05 22:37:11 -06:00
|
|
|
info.video_timebase = fps.Reciprocal();
|
2014-01-27 23:31:38 -06:00
|
|
|
info.duration = 60 * 30; // 30 minute default duration
|
2012-10-05 01:58:27 -05:00
|
|
|
}
|
|
|
|
|
|
2012-10-05 17:05:33 -05:00
|
|
|
// Add an openshot::Clip to the timeline
|
2013-10-01 17:19:53 -05:00
|
|
|
void Timeline::AddClip(Clip* clip) throw(ReaderClosed)
|
2012-10-05 17:05:33 -05:00
|
|
|
{
|
2012-10-14 02:36:05 -05:00
|
|
|
// All clips must be converted to the frame rate of this timeline,
|
|
|
|
|
// so assign the same frame rate to each clip.
|
2014-01-05 22:37:11 -06:00
|
|
|
clip->Reader()->info.fps = info.fps;
|
2012-10-14 02:36:05 -05:00
|
|
|
|
2012-10-05 17:05:33 -05:00
|
|
|
// Add clip to list
|
|
|
|
|
clips.push_back(clip);
|
|
|
|
|
|
|
|
|
|
// Sort clips
|
|
|
|
|
SortClips();
|
|
|
|
|
}
|
|
|
|
|
|
2013-09-28 22:00:52 -05:00
|
|
|
// Add an effect to the timeline
|
|
|
|
|
void Timeline::AddEffect(EffectBase* effect)
|
|
|
|
|
{
|
|
|
|
|
// Add effect to list
|
|
|
|
|
effects.push_back(effect);
|
2013-10-01 17:19:53 -05:00
|
|
|
|
|
|
|
|
// Sort effects
|
|
|
|
|
SortEffects();
|
2013-09-28 22:00:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove an effect from the timeline
|
|
|
|
|
void Timeline::RemoveEffect(EffectBase* effect)
|
|
|
|
|
{
|
|
|
|
|
effects.remove(effect);
|
|
|
|
|
}
|
|
|
|
|
|
2013-02-13 02:46:55 -06:00
|
|
|
// Remove an openshot::Clip to the timeline
|
|
|
|
|
void Timeline::RemoveClip(Clip* clip)
|
|
|
|
|
{
|
|
|
|
|
clips.remove(clip);
|
|
|
|
|
}
|
|
|
|
|
|
2012-10-05 17:05:33 -05:00
|
|
|
// Calculate time of a frame number, based on a framerate
|
2014-01-05 22:37:11 -06:00
|
|
|
float Timeline::calculate_time(int number, Fraction rate)
|
2012-10-05 17:05:33 -05:00
|
|
|
{
|
|
|
|
|
// Get float version of fps fraction
|
2014-01-05 22:37:11 -06:00
|
|
|
float raw_fps = rate.ToFloat();
|
2012-10-05 17:05:33 -05:00
|
|
|
|
|
|
|
|
// Return the time (in seconds) of this frame
|
|
|
|
|
return float(number - 1) / raw_fps;
|
|
|
|
|
}
|
|
|
|
|
|
2013-10-06 18:11:33 -05:00
|
|
|
// Apply effects to the source frame (if any)
|
|
|
|
|
tr1::shared_ptr<Frame> Timeline::apply_effects(tr1::shared_ptr<Frame> frame, int timeline_frame_number, int layer)
|
|
|
|
|
{
|
|
|
|
|
// Calculate time of frame
|
2014-01-05 22:37:11 -06:00
|
|
|
float requested_time = calculate_time(timeline_frame_number, info.fps);
|
2013-10-06 18:11:33 -05:00
|
|
|
|
|
|
|
|
// Find Effects at this position and layer
|
|
|
|
|
list<EffectBase*>::iterator effect_itr;
|
|
|
|
|
for (effect_itr=effects.begin(); effect_itr != effects.end(); ++effect_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
EffectBase *effect = (*effect_itr);
|
|
|
|
|
|
|
|
|
|
// Have we gone past the requested time?
|
|
|
|
|
if (effect->Position() > requested_time)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// Does clip intersect the current requested time
|
|
|
|
|
float effect_duration = effect->End() - effect->Start();
|
|
|
|
|
bool does_effect_intersect = (effect->Position() <= requested_time && effect->Position() + effect_duration >= requested_time && effect->Layer() == layer);
|
|
|
|
|
|
|
|
|
|
// Clip is visible
|
|
|
|
|
if (does_effect_intersect)
|
|
|
|
|
{
|
|
|
|
|
// Determine the frame needed for this clip (based on the position on the timeline)
|
|
|
|
|
float time_diff = (requested_time - effect->Position()) + effect->Start();
|
2014-01-05 22:37:11 -06:00
|
|
|
int effect_frame_number = round(time_diff * info.fps.ToFloat()) + 1;
|
2013-10-06 18:11:33 -05:00
|
|
|
|
|
|
|
|
// Apply the effect to this frame
|
|
|
|
|
frame = effect->GetFrame(frame, effect_frame_number);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // end effect loop
|
|
|
|
|
|
|
|
|
|
// Return modified frame
|
|
|
|
|
return frame;
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-07 17:45:13 -06:00
|
|
|
// Process a new layer of video or audio
|
2012-11-29 17:28:22 -06:00
|
|
|
void Timeline::add_layer(tr1::shared_ptr<Frame> new_frame, Clip* source_clip, int clip_frame_number, int timeline_frame_number)
|
2012-11-07 17:45:13 -06:00
|
|
|
{
|
|
|
|
|
// Get the clip's frame & image
|
2012-11-12 17:21:21 -06:00
|
|
|
tr1::shared_ptr<Frame> source_frame;
|
2013-02-12 01:28:48 -06:00
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
#pragma omp critical (reader_lock)
|
2013-02-19 00:51:07 -06:00
|
|
|
source_frame = tr1::shared_ptr<Frame>(source_clip->GetFrame(clip_frame_number));
|
2013-02-12 01:28:48 -06:00
|
|
|
|
|
|
|
|
// No frame found... so bail
|
|
|
|
|
if (!source_frame)
|
|
|
|
|
return;
|
|
|
|
|
|
2013-10-06 18:11:33 -05:00
|
|
|
/* Apply effects to the source frame (if any) */
|
|
|
|
|
source_frame = apply_effects(source_frame, timeline_frame_number, source_clip->Layer());
|
|
|
|
|
|
2012-11-29 16:32:48 -06:00
|
|
|
tr1::shared_ptr<Magick::Image> source_image;
|
2012-11-12 01:25:35 -06:00
|
|
|
|
2012-11-29 16:32:48 -06:00
|
|
|
/* COPY AUDIO - with correct volume */
|
2013-02-12 01:28:48 -06:00
|
|
|
if (source_clip->Reader()->info.has_audio)
|
|
|
|
|
for (int channel = 0; channel < source_frame->GetAudioChannelsCount(); channel++)
|
|
|
|
|
{
|
|
|
|
|
float initial_volume = 1.0f;
|
|
|
|
|
float previous_volume = source_clip->volume.GetValue(clip_frame_number - 1); // previous frame's percentage of volume (0 to 1)
|
|
|
|
|
float volume = source_clip->volume.GetValue(clip_frame_number); // percentage of volume (0 to 1)
|
2012-11-29 16:32:48 -06:00
|
|
|
|
2013-02-12 01:28:48 -06:00
|
|
|
// If no ramp needed, set initial volume = clip's volume
|
|
|
|
|
if (isEqual(previous_volume, volume))
|
|
|
|
|
initial_volume = volume;
|
2012-11-29 16:32:48 -06:00
|
|
|
|
2013-02-12 01:28:48 -06:00
|
|
|
// Apply ramp to source frame (if needed)
|
|
|
|
|
if (!isEqual(previous_volume, volume))
|
|
|
|
|
source_frame->ApplyGainRamp(channel, 0, source_frame->GetAudioSamplesCount(), previous_volume, volume);
|
2012-11-29 16:32:48 -06:00
|
|
|
|
2013-02-12 01:28:48 -06:00
|
|
|
// Copy audio samples (and set initial volume). Mix samples with existing audio samples. The gains are added together, to
|
|
|
|
|
// be sure to set the gain's correctly, so the sum does not exceed 1.0 (of audio distortion will happen).
|
|
|
|
|
new_frame->AddAudio(false, channel, 0, source_frame->GetAudioSamples(channel), source_frame->GetAudioSamplesCount(), initial_volume);
|
|
|
|
|
}
|
2012-11-29 16:32:48 -06:00
|
|
|
|
|
|
|
|
/* GET IMAGE DATA - OR GENERATE IT */
|
|
|
|
|
if (!source_clip->Waveform())
|
|
|
|
|
// Get actual frame image data
|
|
|
|
|
source_image = source_frame->GetImage();
|
|
|
|
|
else
|
2012-11-29 23:11:50 -06:00
|
|
|
{
|
|
|
|
|
// Get the color of the waveform
|
|
|
|
|
int red = source_clip->wave_color.red.GetInt(timeline_frame_number);
|
|
|
|
|
int green = source_clip->wave_color.green.GetInt(timeline_frame_number);
|
|
|
|
|
int blue = source_clip->wave_color.blue.GetInt(timeline_frame_number);
|
|
|
|
|
|
2012-11-29 16:32:48 -06:00
|
|
|
// Generate Waveform Dynamically (the size of the timeline)
|
2014-01-05 22:37:11 -06:00
|
|
|
source_image = source_frame->GetWaveform(info.width, info.height, red, green, blue);
|
2012-11-29 23:11:50 -06:00
|
|
|
}
|
2012-11-29 16:32:48 -06:00
|
|
|
|
|
|
|
|
// Get some basic image properties
|
|
|
|
|
int source_width = source_image->columns();
|
|
|
|
|
int source_height = source_image->rows();
|
2012-11-12 01:25:35 -06:00
|
|
|
|
|
|
|
|
/* ALPHA & OPACITY */
|
2012-11-08 04:35:21 -06:00
|
|
|
if (source_clip->alpha.GetValue(clip_frame_number) != 0)
|
|
|
|
|
{
|
2012-11-13 00:11:20 -06:00
|
|
|
float alpha = 1.0 - source_clip->alpha.GetValue(clip_frame_number);
|
|
|
|
|
source_image->quantumOperator(Magick::OpacityChannel, Magick::MultiplyEvaluateOperator, alpha);
|
2012-11-08 04:35:21 -06:00
|
|
|
}
|
2012-11-07 17:45:13 -06:00
|
|
|
|
2012-11-12 01:25:35 -06:00
|
|
|
/* RESIZE SOURCE IMAGE - based on scale type */
|
2014-01-05 22:37:11 -06:00
|
|
|
Magick::Geometry new_size(info.width, info.height);
|
2012-11-12 01:25:35 -06:00
|
|
|
switch (source_clip->scale)
|
|
|
|
|
{
|
|
|
|
|
case (SCALE_FIT):
|
|
|
|
|
new_size.aspect(false); // respect aspect ratio
|
|
|
|
|
source_image->resize(new_size);
|
|
|
|
|
source_width = source_image->size().width();
|
|
|
|
|
source_height = source_image->size().height();
|
|
|
|
|
break;
|
|
|
|
|
case (SCALE_STRETCH):
|
|
|
|
|
new_size.aspect(true); // ignore aspect ratio
|
|
|
|
|
source_image->resize(new_size);
|
|
|
|
|
source_width = source_image->size().width();
|
|
|
|
|
source_height = source_image->size().height();
|
|
|
|
|
break;
|
|
|
|
|
case (SCALE_CROP):
|
2014-01-05 22:37:11 -06:00
|
|
|
Magick::Geometry width_size(info.width, round(info.width / (float(source_width) / float(source_height))));
|
|
|
|
|
Magick::Geometry height_size(round(info.height / (float(source_height) / float(source_width))), info.height);
|
2012-11-12 01:25:35 -06:00
|
|
|
new_size.aspect(false); // respect aspect ratio
|
2014-01-05 22:37:11 -06:00
|
|
|
if (width_size.width() >= info.width && width_size.height() >= info.height)
|
2012-11-12 01:25:35 -06:00
|
|
|
source_image->resize(width_size); // width is larger, so resize to it
|
|
|
|
|
else
|
|
|
|
|
source_image->resize(height_size); // height is larger, so resize to it
|
|
|
|
|
source_width = source_image->size().width();
|
|
|
|
|
source_height = source_image->size().height();
|
|
|
|
|
break;
|
|
|
|
|
}
|
2012-11-07 17:45:13 -06:00
|
|
|
|
2012-11-12 01:25:35 -06:00
|
|
|
/* GRAVITY LOCATION - Initialize X & Y to the correct values (before applying location curves) */
|
|
|
|
|
float x = 0.0; // left
|
|
|
|
|
float y = 0.0; // top
|
|
|
|
|
switch (source_clip->gravity)
|
|
|
|
|
{
|
|
|
|
|
case (GRAVITY_TOP):
|
2014-01-05 22:37:11 -06:00
|
|
|
x = (info.width - source_width) / 2.0; // center
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_TOP_RIGHT):
|
2014-01-05 22:37:11 -06:00
|
|
|
x = info.width - source_width; // right
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_LEFT):
|
2014-01-05 22:37:11 -06:00
|
|
|
y = (info.height - source_height) / 2.0; // center
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_CENTER):
|
2014-01-05 22:37:11 -06:00
|
|
|
x = (info.width - source_width) / 2.0; // center
|
|
|
|
|
y = (info.height - source_height) / 2.0; // center
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_RIGHT):
|
2014-01-05 22:37:11 -06:00
|
|
|
x = info.width - source_width; // right
|
|
|
|
|
y = (info.height - source_height) / 2.0; // center
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_BOTTOM_LEFT):
|
2014-01-05 22:37:11 -06:00
|
|
|
y = (info.height - source_height); // bottom
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_BOTTOM):
|
2014-01-05 22:37:11 -06:00
|
|
|
x = (info.width - source_width) / 2.0; // center
|
|
|
|
|
y = (info.height - source_height); // bottom
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
case (GRAVITY_BOTTOM_RIGHT):
|
2014-01-05 22:37:11 -06:00
|
|
|
x = info.width - source_width; // right
|
|
|
|
|
y = (info.height - source_height); // bottom
|
2012-11-12 01:25:35 -06:00
|
|
|
break;
|
|
|
|
|
}
|
2012-11-08 18:02:20 -06:00
|
|
|
|
2012-11-12 01:25:35 -06:00
|
|
|
/* LOCATION, ROTATION, AND SCALE */
|
|
|
|
|
float r = source_clip->rotation.GetValue(clip_frame_number); // rotate in degrees
|
2014-01-05 22:37:11 -06:00
|
|
|
x += info.width * source_clip->location_x.GetValue(clip_frame_number); // move in percentage of final width
|
|
|
|
|
y += info.height * source_clip->location_y.GetValue(clip_frame_number); // move in percentage of final height
|
2012-11-12 01:25:35 -06:00
|
|
|
float sx = source_clip->scale_x.GetValue(clip_frame_number); // percentage X scale
|
|
|
|
|
float sy = source_clip->scale_y.GetValue(clip_frame_number); // percentage Y scale
|
2012-11-13 22:07:49 -06:00
|
|
|
bool is_x_animated = source_clip->location_x.Points.size() > 2;
|
|
|
|
|
bool is_y_animated = source_clip->location_y.Points.size() > 2;
|
2012-11-12 01:25:35 -06:00
|
|
|
|
2012-12-06 17:58:51 -06:00
|
|
|
int offset_x = -1;
|
|
|
|
|
int offset_y = -1;
|
2013-02-10 21:16:46 -06:00
|
|
|
bool transformed = false;
|
2012-11-16 17:29:12 -06:00
|
|
|
if ((!isEqual(x, 0) || !isEqual(y, 0)) && (isEqual(r, 0) && isEqual(sx, 1) && isEqual(sy, 1) && !is_x_animated && !is_y_animated))
|
2012-11-12 17:21:21 -06:00
|
|
|
{
|
2013-02-12 01:28:48 -06:00
|
|
|
//cout << "SIMPLE" << endl;
|
2012-11-13 22:07:49 -06:00
|
|
|
// If only X and Y are different, and no animation is being used (just set the offset for speed)
|
|
|
|
|
offset_x = round(x);
|
|
|
|
|
offset_y = round(y);
|
2013-02-10 21:16:46 -06:00
|
|
|
transformed = true;
|
2012-11-13 22:07:49 -06:00
|
|
|
|
2012-11-16 17:29:12 -06:00
|
|
|
} else if (!isEqual(r, 0) || !isEqual(x, 0) || !isEqual(y, 0) || !isEqual(sx, 1) || !isEqual(sy, 1))
|
2012-11-13 22:07:49 -06:00
|
|
|
{
|
2013-02-12 01:28:48 -06:00
|
|
|
//cout << "COMPLEX" << endl;
|
2013-02-12 02:42:18 -06:00
|
|
|
|
|
|
|
|
/* RESIZE SOURCE CANVAS - to the same size as timeline canvas */
|
2014-01-05 22:37:11 -06:00
|
|
|
if (source_width != info.width || source_height != info.height)
|
2013-02-12 02:42:18 -06:00
|
|
|
{
|
|
|
|
|
source_image->borderColor(Magick::Color("none"));
|
|
|
|
|
source_image->border(Magick::Geometry(1, 1, 0, 0, false, false)); // prevent stretching of edge pixels (during the canvas resize)
|
2014-01-05 22:37:11 -06:00
|
|
|
source_image->size(Magick::Geometry(info.width, info.height, 0, 0, false, false)); // resize the canvas (to prevent clipping)
|
2013-02-12 02:42:18 -06:00
|
|
|
}
|
|
|
|
|
|
2012-11-13 22:07:49 -06:00
|
|
|
// Use the distort operator, which is very CPU intensive
|
|
|
|
|
// origin X,Y Scale Angle NewX,NewY
|
2012-12-04 02:21:01 -06:00
|
|
|
double distort_args[7] = {0,0, sx,sy, r, x,y };
|
2012-11-12 17:21:21 -06:00
|
|
|
source_image->distort(Magick::ScaleRotateTranslateDistortion, 7, distort_args, false);
|
2013-02-10 21:16:46 -06:00
|
|
|
transformed = true;
|
|
|
|
|
}
|
|
|
|
|
|
2013-02-12 01:28:48 -06:00
|
|
|
/* Is this the 1st layer? And the same size as this image? */
|
2013-02-12 02:42:18 -06:00
|
|
|
if (new_frame->GetImage()->columns() == 1 && !transformed && source_frame->GetHeight() == new_frame->GetHeight() && source_frame->GetWidth() == new_frame->GetWidth())
|
2013-02-10 21:16:46 -06:00
|
|
|
{
|
|
|
|
|
// Just use this image as the background
|
|
|
|
|
new_frame->AddImage(source_image);
|
|
|
|
|
}
|
|
|
|
|
else if (new_frame->GetImage()->columns() == 1)
|
|
|
|
|
{
|
2013-02-12 01:28:48 -06:00
|
|
|
/* CREATE BACKGROUND COLOR - needed if this is the 1st layer */
|
2013-02-10 21:16:46 -06:00
|
|
|
int red = color.red.GetInt(timeline_frame_number);
|
|
|
|
|
int green = color.green.GetInt(timeline_frame_number);
|
|
|
|
|
int blue = color.blue.GetInt(timeline_frame_number);
|
2014-01-05 22:37:11 -06:00
|
|
|
new_frame->AddColor(info.width, info.height, Magick::Color(red, green, blue, 0));
|
2013-02-10 21:16:46 -06:00
|
|
|
|
|
|
|
|
/* COMPOSITE SOURCE IMAGE (LAYER) ONTO FINAL IMAGE */
|
|
|
|
|
tr1::shared_ptr<Magick::Image> new_image = new_frame->GetImage();
|
|
|
|
|
new_image->composite(*source_image.get(), offset_x, offset_y, Magick::OverCompositeOp);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
/* COMPOSITE SOURCE IMAGE (LAYER) ONTO FINAL IMAGE */
|
|
|
|
|
tr1::shared_ptr<Magick::Image> new_image = new_frame->GetImage();
|
|
|
|
|
new_image->composite(*source_image.get(), offset_x, offset_y, Magick::OverCompositeOp);
|
2012-11-12 17:21:21 -06:00
|
|
|
}
|
2012-11-08 18:02:20 -06:00
|
|
|
|
2012-11-07 17:45:13 -06:00
|
|
|
}
|
|
|
|
|
|
2012-10-09 02:09:44 -05:00
|
|
|
// Update the list of 'opened' clips
|
|
|
|
|
void Timeline::update_open_clips(Clip *clip, bool is_open)
|
|
|
|
|
{
|
|
|
|
|
// is clip already in list?
|
|
|
|
|
bool clip_found = open_clips.count(clip);
|
|
|
|
|
|
|
|
|
|
if (clip_found && !is_open)
|
|
|
|
|
{
|
2012-12-03 22:55:46 -06:00
|
|
|
// Mark clip "to be removed"
|
|
|
|
|
closing_clips.push_back(clip);
|
2012-10-09 02:09:44 -05:00
|
|
|
}
|
|
|
|
|
else if (!clip_found && is_open)
|
|
|
|
|
{
|
|
|
|
|
// Add clip to 'opened' list, because it's missing
|
|
|
|
|
open_clips[clip] = clip;
|
|
|
|
|
|
|
|
|
|
// Open the clip's reader
|
|
|
|
|
clip->Open();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-12-03 22:55:46 -06:00
|
|
|
// Update the list of 'closed' clips
|
|
|
|
|
void Timeline::update_closed_clips()
|
|
|
|
|
{
|
|
|
|
|
// Close all "to be closed" clips
|
|
|
|
|
list<Clip*>::iterator clip_itr;
|
|
|
|
|
for (clip_itr=closing_clips.begin(); clip_itr != closing_clips.end(); ++clip_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
Clip *clip = (*clip_itr);
|
|
|
|
|
|
|
|
|
|
// Close the clip's reader
|
|
|
|
|
clip->Close();
|
|
|
|
|
|
|
|
|
|
// Remove clip from 'opened' list, because it's closed now
|
|
|
|
|
open_clips.erase(clip);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear list
|
|
|
|
|
closing_clips.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2012-10-05 17:05:33 -05:00
|
|
|
// Sort clips by position on the timeline
|
|
|
|
|
void Timeline::SortClips()
|
|
|
|
|
{
|
|
|
|
|
// sort clips
|
2013-09-10 12:59:06 -05:00
|
|
|
clips.sort(CompareClips());
|
2012-10-05 17:05:33 -05:00
|
|
|
}
|
|
|
|
|
|
2013-10-01 17:19:53 -05:00
|
|
|
// Sort effects by position on the timeline
|
|
|
|
|
void Timeline::SortEffects()
|
|
|
|
|
{
|
|
|
|
|
// sort clips
|
|
|
|
|
effects.sort(CompareEffects());
|
|
|
|
|
}
|
|
|
|
|
|
2012-10-08 16:22:18 -05:00
|
|
|
// Close the reader (and any resources it was consuming)
|
|
|
|
|
void Timeline::Close()
|
|
|
|
|
{
|
2012-10-10 00:52:47 -05:00
|
|
|
// Close all open clips
|
|
|
|
|
list<Clip*>::iterator clip_itr;
|
|
|
|
|
for (clip_itr=clips.begin(); clip_itr != clips.end(); ++clip_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
Clip *clip = (*clip_itr);
|
2012-10-08 16:22:18 -05:00
|
|
|
|
2012-10-10 00:52:47 -05:00
|
|
|
// Open or Close this clip, based on if it's intersecting or not
|
|
|
|
|
update_open_clips(clip, false);
|
|
|
|
|
}
|
2012-12-03 22:55:46 -06:00
|
|
|
|
|
|
|
|
// Actually close the clips
|
|
|
|
|
update_closed_clips();
|
2013-12-18 21:55:43 -06:00
|
|
|
is_open = false;
|
2014-01-05 22:37:11 -06:00
|
|
|
|
|
|
|
|
// Clear cache
|
|
|
|
|
final_cache.Clear();
|
2012-10-08 16:22:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open the reader (and start consuming resources)
|
|
|
|
|
void Timeline::Open()
|
|
|
|
|
{
|
2013-12-18 21:55:43 -06:00
|
|
|
is_open = true;
|
2012-10-08 16:22:18 -05:00
|
|
|
}
|
|
|
|
|
|
2012-11-16 17:29:12 -06:00
|
|
|
// Compare 2 floating point numbers for equality
|
|
|
|
|
bool Timeline::isEqual(double a, double b)
|
|
|
|
|
{
|
|
|
|
|
return fabs(a - b) < 0.000001;
|
|
|
|
|
}
|
2012-11-07 17:45:13 -06:00
|
|
|
|
2012-10-05 01:58:27 -05:00
|
|
|
// Get an openshot::Frame object for a specific frame number of this reader.
|
2012-10-14 03:43:52 -05:00
|
|
|
tr1::shared_ptr<Frame> Timeline::GetFrame(int requested_frame) throw(ReaderClosed)
|
2012-10-05 01:58:27 -05:00
|
|
|
{
|
2012-10-05 17:05:33 -05:00
|
|
|
// Adjust out of bounds frame number
|
|
|
|
|
if (requested_frame < 1)
|
|
|
|
|
requested_frame = 1;
|
|
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
// Check cache
|
|
|
|
|
if (final_cache.Exists(requested_frame))
|
|
|
|
|
return final_cache.GetFrame(requested_frame);
|
|
|
|
|
else
|
2012-10-05 17:05:33 -05:00
|
|
|
{
|
2014-05-14 23:04:35 -05:00
|
|
|
// Re-Sort Clips (since the likely changed)
|
|
|
|
|
SortClips();
|
|
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
// Minimum number of packets to process (for performance reasons)
|
2014-04-02 16:48:27 -05:00
|
|
|
//int minimum_frames = OPEN_MP_NUM_PROCESSORS;
|
2013-10-06 18:11:33 -05:00
|
|
|
int minimum_frames = 1;
|
2012-11-12 17:21:21 -06:00
|
|
|
//omp_set_num_threads(1);
|
|
|
|
|
omp_set_nested(true);
|
2014-04-02 16:48:27 -05:00
|
|
|
|
2013-10-06 18:11:33 -05:00
|
|
|
#pragma xx omp parallel
|
2012-10-10 02:36:53 -05:00
|
|
|
{
|
2013-10-06 18:11:33 -05:00
|
|
|
#pragma xx omp single
|
2012-11-12 17:21:21 -06:00
|
|
|
{
|
|
|
|
|
// Loop through all requested frames
|
|
|
|
|
for (int frame_number = requested_frame; frame_number < requested_frame + minimum_frames; frame_number++)
|
|
|
|
|
{
|
2013-10-06 18:11:33 -05:00
|
|
|
#pragma xx omp task firstprivate(frame_number)
|
2012-11-12 17:21:21 -06:00
|
|
|
{
|
|
|
|
|
// Create blank frame (which will become the requested frame)
|
2014-01-28 17:17:38 -06:00
|
|
|
tr1::shared_ptr<Frame> new_frame(tr1::shared_ptr<Frame>(new Frame(frame_number, info.width, info.height, "#000000", 0, info.channels)));
|
2012-11-07 17:45:13 -06:00
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
// Calculate time of frame
|
2014-01-05 22:37:11 -06:00
|
|
|
float requested_time = calculate_time(frame_number, info.fps);
|
2012-11-07 17:45:13 -06:00
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
// Find Clips at this time
|
|
|
|
|
list<Clip*>::iterator clip_itr;
|
|
|
|
|
for (clip_itr=clips.begin(); clip_itr != clips.end(); ++clip_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
Clip *clip = (*clip_itr);
|
|
|
|
|
|
2013-10-06 18:11:33 -05:00
|
|
|
// Have we gone past the requested time?
|
|
|
|
|
if (clip->Position() > requested_time)
|
|
|
|
|
break;
|
|
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
// Does clip intersect the current requested time
|
|
|
|
|
float clip_duration = clip->End() - clip->Start();
|
|
|
|
|
bool does_clip_intersect = (clip->Position() <= requested_time && clip->Position() + clip_duration >= requested_time);
|
|
|
|
|
|
2012-12-03 22:55:46 -06:00
|
|
|
// Open (or schedule for closing) this clip, based on if it's intersecting or not
|
2012-11-12 17:21:21 -06:00
|
|
|
#pragma omp critical (reader_lock)
|
|
|
|
|
update_open_clips(clip, does_clip_intersect);
|
|
|
|
|
|
|
|
|
|
// Clip is visible
|
|
|
|
|
if (does_clip_intersect)
|
|
|
|
|
{
|
|
|
|
|
// Determine the frame needed for this clip (based on the position on the timeline)
|
|
|
|
|
float time_diff = (requested_time - clip->Position()) + clip->Start();
|
2014-01-05 22:37:11 -06:00
|
|
|
int clip_frame_number = round(time_diff * info.fps.ToFloat()) + 1;
|
2012-11-12 17:21:21 -06:00
|
|
|
|
|
|
|
|
// Add clip's frame as layer
|
2012-11-29 17:28:22 -06:00
|
|
|
add_layer(new_frame, clip, clip_frame_number, frame_number);
|
2012-11-12 17:21:21 -06:00
|
|
|
|
|
|
|
|
} else
|
2013-02-15 00:23:55 -06:00
|
|
|
#pragma omp critical (timeline_output)
|
|
|
|
|
cout << "FRAME NOT IN CLIP DURATION: frame: " << frame_number << ", clip->Position(): " << clip->Position() << ", requested_time: " << requested_time << ", clip_duration: " << clip_duration << endl;
|
2012-11-12 17:21:21 -06:00
|
|
|
|
|
|
|
|
} // end clip loop
|
|
|
|
|
|
2014-05-14 23:04:35 -05:00
|
|
|
// Check for empty frame image (and fill with color)
|
|
|
|
|
if (new_frame->GetImage()->columns() == 1)
|
|
|
|
|
{
|
|
|
|
|
int red = color.red.GetInt(frame_number);
|
|
|
|
|
int green = color.green.GetInt(frame_number);
|
|
|
|
|
int blue = color.blue.GetInt(frame_number);
|
|
|
|
|
new_frame->AddColor(info.width, info.height, Magick::Color(red, green, blue));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add final frame to cache
|
|
|
|
|
#pragma omp critical (timeline_cache)
|
|
|
|
|
final_cache.Add(frame_number, new_frame);
|
|
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
} // end omp task
|
|
|
|
|
} // end frame loop
|
|
|
|
|
|
2012-12-03 22:55:46 -06:00
|
|
|
// Actually close all clips no longer needed
|
|
|
|
|
#pragma omp critical (reader_lock)
|
|
|
|
|
update_closed_clips();
|
|
|
|
|
|
2012-11-12 17:21:21 -06:00
|
|
|
} // end omp single
|
|
|
|
|
} // end omp parallel
|
|
|
|
|
|
|
|
|
|
// Return frame (or blank frame)
|
|
|
|
|
return final_cache.GetFrame(requested_frame);
|
2012-10-05 17:05:33 -05:00
|
|
|
}
|
2012-10-03 01:55:24 -05:00
|
|
|
}
|
2013-12-07 21:09:55 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// Generate JSON string of this object
|
|
|
|
|
string Timeline::Json() {
|
|
|
|
|
|
|
|
|
|
// Return formatted string
|
|
|
|
|
return JsonValue().toStyledString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate Json::JsonValue for this object
|
|
|
|
|
Json::Value Timeline::JsonValue() {
|
|
|
|
|
|
|
|
|
|
// Create root json object
|
|
|
|
|
Json::Value root = ReaderBase::JsonValue(); // get parent properties
|
|
|
|
|
root["type"] = "Timeline";
|
2014-01-05 22:37:11 -06:00
|
|
|
root["viewport_scale"] = viewport_scale.JsonValue();
|
|
|
|
|
root["viewport_x"] = viewport_x.JsonValue();
|
|
|
|
|
root["viewport_y"] = viewport_y.JsonValue();
|
|
|
|
|
root["color"] = color.JsonValue();
|
|
|
|
|
|
|
|
|
|
// Add array of clips
|
2014-01-27 23:31:38 -06:00
|
|
|
root["clips"] = Json::Value(Json::arrayValue);
|
2014-01-05 22:37:11 -06:00
|
|
|
|
|
|
|
|
// Find Clips at this time
|
|
|
|
|
list<Clip*>::iterator clip_itr;
|
|
|
|
|
for (clip_itr=clips.begin(); clip_itr != clips.end(); ++clip_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
Clip *existing_clip = (*clip_itr);
|
2014-01-27 23:31:38 -06:00
|
|
|
root["clips"].append(existing_clip->JsonValue());
|
2014-01-05 22:37:11 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add array of effects
|
2014-01-27 23:31:38 -06:00
|
|
|
root["effects"] = Json::Value(Json::arrayValue);
|
2014-01-05 22:37:11 -06:00
|
|
|
|
|
|
|
|
// loop through effects
|
|
|
|
|
list<EffectBase*>::iterator effect_itr;
|
|
|
|
|
for (effect_itr=effects.begin(); effect_itr != effects.end(); ++effect_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
EffectBase *existing_effect = (*effect_itr);
|
2014-01-27 23:31:38 -06:00
|
|
|
root["effects"].append(existing_effect->JsonValue());
|
2014-01-05 22:37:11 -06:00
|
|
|
}
|
2013-12-07 21:09:55 -06:00
|
|
|
|
|
|
|
|
// return JsonValue
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load JSON string into this object
|
|
|
|
|
void Timeline::SetJson(string value) throw(InvalidJSON) {
|
|
|
|
|
|
|
|
|
|
// Parse JSON string into JSON objects
|
|
|
|
|
Json::Value root;
|
|
|
|
|
Json::Reader reader;
|
|
|
|
|
bool success = reader.parse( value, root );
|
|
|
|
|
if (!success)
|
|
|
|
|
// Raise exception
|
|
|
|
|
throw InvalidJSON("JSON could not be parsed (or is invalid)", "");
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Set all values that match
|
|
|
|
|
SetJsonValue(root);
|
|
|
|
|
}
|
|
|
|
|
catch (exception e)
|
|
|
|
|
{
|
|
|
|
|
// Error parsing JSON (or missing keys)
|
|
|
|
|
throw InvalidJSON("JSON is invalid (missing keys or invalid data types)", "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load Json::JsonValue into this object
|
2014-01-05 22:37:11 -06:00
|
|
|
void Timeline::SetJsonValue(Json::Value root) throw(InvalidFile, ReaderClosed) {
|
|
|
|
|
|
|
|
|
|
// Close timeline before we do anything (this also removes all open and closing clips)
|
|
|
|
|
Close();
|
2013-12-07 21:09:55 -06:00
|
|
|
|
|
|
|
|
// Set parent data
|
|
|
|
|
ReaderBase::SetJsonValue(root);
|
|
|
|
|
|
2014-01-05 22:37:11 -06:00
|
|
|
// Clear existing clips
|
|
|
|
|
clips.clear();
|
|
|
|
|
|
2014-01-27 23:31:38 -06:00
|
|
|
if (!root["clips"].isNull())
|
2014-01-05 22:37:11 -06:00
|
|
|
// loop through clips
|
2014-01-27 23:31:38 -06:00
|
|
|
for (int x = 0; x < root["clips"].size(); x++) {
|
2014-01-05 22:37:11 -06:00
|
|
|
// Get each clip
|
2014-01-27 23:31:38 -06:00
|
|
|
Json::Value existing_clip = root["clips"][x];
|
2014-01-05 22:37:11 -06:00
|
|
|
|
|
|
|
|
// Create Clip
|
|
|
|
|
Clip *c = new Clip();
|
|
|
|
|
|
|
|
|
|
// Load Json into Clip
|
|
|
|
|
c->SetJsonValue(existing_clip);
|
|
|
|
|
|
|
|
|
|
// Add Clip to Timeline
|
|
|
|
|
AddClip(c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear existing effects
|
|
|
|
|
effects.clear();
|
|
|
|
|
|
2014-01-27 23:31:38 -06:00
|
|
|
if (!root["effects"].isNull())
|
2014-01-05 22:37:11 -06:00
|
|
|
// loop through effects
|
2014-01-27 23:31:38 -06:00
|
|
|
for (int x = 0; x < root["effects"].size(); x++) {
|
2014-01-05 22:37:11 -06:00
|
|
|
// Get each effect
|
2014-01-27 23:31:38 -06:00
|
|
|
Json::Value existing_effect = root["effects"][x];
|
2014-01-05 22:37:11 -06:00
|
|
|
|
|
|
|
|
// Create Effect
|
|
|
|
|
EffectBase *e = NULL;
|
|
|
|
|
|
2014-01-08 01:43:58 -06:00
|
|
|
if (!existing_effect["type"].isNull())
|
2014-01-05 22:37:11 -06:00
|
|
|
// Init the matching effect object
|
|
|
|
|
if (existing_effect["type"].asString() == "ChromaKey")
|
|
|
|
|
e = new ChromaKey();
|
|
|
|
|
|
|
|
|
|
else if (existing_effect["type"].asString() == "Deinterlace")
|
|
|
|
|
e = new Deinterlace();
|
|
|
|
|
|
|
|
|
|
else if (existing_effect["type"].asString() == "Mask")
|
|
|
|
|
e = new Mask();
|
|
|
|
|
|
|
|
|
|
else if (existing_effect["type"].asString() == "Negate")
|
|
|
|
|
e = new Negate();
|
|
|
|
|
|
|
|
|
|
// Load Json into Effect
|
|
|
|
|
e->SetJsonValue(existing_effect);
|
|
|
|
|
|
|
|
|
|
// Add Effect to Timeline
|
|
|
|
|
AddEffect(e);
|
|
|
|
|
}
|
2013-12-07 21:09:55 -06:00
|
|
|
}
|
2014-01-08 01:43:58 -06:00
|
|
|
|
|
|
|
|
// Apply a special formatted JSON object, which represents a change to the timeline (insert, update, delete)
|
|
|
|
|
void Timeline::ApplyJsonDiff(string value) throw(InvalidJSON, InvalidJSONKey) {
|
|
|
|
|
|
2014-05-14 23:04:35 -05:00
|
|
|
// Clear internal cache (since things are about to change)
|
|
|
|
|
final_cache.Clear();
|
|
|
|
|
|
2014-01-08 01:43:58 -06:00
|
|
|
// Parse JSON string into JSON objects
|
|
|
|
|
Json::Value root;
|
|
|
|
|
Json::Reader reader;
|
|
|
|
|
bool success = reader.parse( value, root );
|
|
|
|
|
if (!success || !root.isArray())
|
|
|
|
|
// Raise exception
|
|
|
|
|
throw InvalidJSON("JSON could not be parsed (or is invalid).", "");
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Process the JSON change array, loop through each item
|
|
|
|
|
for (int x = 0; x < root.size(); x++) {
|
|
|
|
|
// Get each change
|
|
|
|
|
Json::Value change = root[x];
|
|
|
|
|
string root_key = change["key"][(uint)0].asString();
|
|
|
|
|
|
|
|
|
|
// Process each type of change
|
|
|
|
|
if (root_key == "clips")
|
|
|
|
|
// Apply to CLIPS
|
|
|
|
|
apply_json_to_clips(change);
|
|
|
|
|
|
|
|
|
|
else if (root_key == "effects")
|
|
|
|
|
// Apply to EFFECTS
|
|
|
|
|
apply_json_to_effects(change);
|
|
|
|
|
|
|
|
|
|
else
|
|
|
|
|
// Apply to TIMELINE
|
|
|
|
|
apply_json_to_timeline(change);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (exception e)
|
|
|
|
|
{
|
|
|
|
|
// Error parsing JSON (or missing keys)
|
|
|
|
|
throw InvalidJSON("JSON is invalid (missing keys or invalid data types)", "");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply JSON diff to clips
|
|
|
|
|
void Timeline::apply_json_to_clips(Json::Value change) throw(InvalidJSONKey) {
|
|
|
|
|
|
|
|
|
|
// Get key and type of change
|
|
|
|
|
string change_type = change["type"].asString();
|
|
|
|
|
string clip_id = "";
|
|
|
|
|
Clip *existing_clip = NULL;
|
|
|
|
|
|
|
|
|
|
// Find id of clip (if any)
|
|
|
|
|
for (int x = 0; x < change["key"].size(); x++) {
|
|
|
|
|
// Get each change
|
|
|
|
|
Json::Value key_part = change["key"][x];
|
|
|
|
|
|
|
|
|
|
if (key_part.isObject()) {
|
|
|
|
|
// Check for id
|
|
|
|
|
if (!key_part["id"].isNull()) {
|
|
|
|
|
// Set the id
|
|
|
|
|
clip_id = key_part["id"].asString();
|
|
|
|
|
|
|
|
|
|
// Find matching clip in timeline (if any)
|
|
|
|
|
list<Clip*>::iterator clip_itr;
|
|
|
|
|
for (clip_itr=clips.begin(); clip_itr != clips.end(); ++clip_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get clip object from the iterator
|
|
|
|
|
Clip *c = (*clip_itr);
|
|
|
|
|
if (c->Id() == clip_id) {
|
|
|
|
|
existing_clip = c;
|
|
|
|
|
break; // clip found, exit loop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break; // id found, exit loop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine type of change operation
|
|
|
|
|
if (change_type == "insert") {
|
|
|
|
|
|
|
|
|
|
// Create new clip
|
|
|
|
|
Clip *clip = new Clip();
|
|
|
|
|
clip->SetJsonValue(change["value"]); // Set properties of new clip from JSON
|
|
|
|
|
AddClip(clip); // Add clip to timeline
|
|
|
|
|
|
|
|
|
|
} else if (change_type == "update") {
|
|
|
|
|
|
|
|
|
|
// Update existing clip
|
|
|
|
|
if (existing_clip)
|
|
|
|
|
existing_clip->SetJsonValue(change["value"]); // Update clip properties from JSON
|
|
|
|
|
|
|
|
|
|
} else if (change_type == "delete") {
|
|
|
|
|
|
|
|
|
|
// Remove existing clip
|
|
|
|
|
if (existing_clip)
|
|
|
|
|
RemoveClip(existing_clip); // Remove clip from timeline
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply JSON diff to effects
|
|
|
|
|
void Timeline::apply_json_to_effects(Json::Value change) throw(InvalidJSONKey) {
|
|
|
|
|
|
|
|
|
|
// Get key and type of change
|
|
|
|
|
string change_type = change["type"].asString();
|
|
|
|
|
string effect_id = "";
|
|
|
|
|
EffectBase *existing_effect = NULL;
|
|
|
|
|
|
|
|
|
|
// Find id of an effect (if any)
|
|
|
|
|
for (int x = 0; x < change["key"].size(); x++) {
|
|
|
|
|
// Get each change
|
|
|
|
|
Json::Value key_part = change["key"][x];
|
|
|
|
|
|
|
|
|
|
if (key_part.isObject()) {
|
|
|
|
|
// Check for id
|
|
|
|
|
if (!key_part["id"].isNull())
|
|
|
|
|
{
|
|
|
|
|
// Set the id
|
|
|
|
|
effect_id = key_part["id"].asString();
|
|
|
|
|
|
|
|
|
|
// Find matching effect in timeline (if any)
|
|
|
|
|
list<EffectBase*>::iterator effect_itr;
|
|
|
|
|
for (effect_itr=effects.begin(); effect_itr != effects.end(); ++effect_itr)
|
|
|
|
|
{
|
|
|
|
|
// Get effect object from the iterator
|
|
|
|
|
EffectBase *e = (*effect_itr);
|
|
|
|
|
if (e->Id() == effect_id) {
|
|
|
|
|
existing_effect = e;
|
|
|
|
|
break; // effect found, exit loop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break; // id found, exit loop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine type of change operation
|
|
|
|
|
if (change_type == "insert") {
|
|
|
|
|
|
|
|
|
|
// Determine type of effect
|
|
|
|
|
string effect_type = change["value"]["type"].asString();
|
|
|
|
|
|
|
|
|
|
// Create Effect
|
|
|
|
|
EffectBase *e = NULL;
|
|
|
|
|
|
|
|
|
|
// Init the matching effect object
|
|
|
|
|
if (effect_type == "ChromaKey")
|
|
|
|
|
e = new ChromaKey();
|
|
|
|
|
|
|
|
|
|
else if (effect_type == "Deinterlace")
|
|
|
|
|
e = new Deinterlace();
|
|
|
|
|
|
|
|
|
|
else if (effect_type == "Mask")
|
|
|
|
|
e = new Mask();
|
|
|
|
|
|
|
|
|
|
else if (effect_type == "Negate")
|
|
|
|
|
e = new Negate();
|
|
|
|
|
|
|
|
|
|
// Load Json into Effect
|
|
|
|
|
e->SetJsonValue(change["value"]);
|
|
|
|
|
|
|
|
|
|
// Add Effect to Timeline
|
|
|
|
|
AddEffect(e);
|
|
|
|
|
|
|
|
|
|
} else if (change_type == "update") {
|
|
|
|
|
|
|
|
|
|
// Update existing effect
|
|
|
|
|
if (existing_effect)
|
|
|
|
|
existing_effect->SetJsonValue(change["value"]); // Update effect properties from JSON
|
|
|
|
|
|
|
|
|
|
} else if (change_type == "delete") {
|
|
|
|
|
|
|
|
|
|
// Remove existing effect
|
|
|
|
|
if (existing_effect)
|
|
|
|
|
RemoveEffect(existing_effect); // Remove effect from timeline
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply JSON diff to timeline properties
|
|
|
|
|
void Timeline::apply_json_to_timeline(Json::Value change) throw(InvalidJSONKey) {
|
|
|
|
|
|
|
|
|
|
// Get key and type of change
|
|
|
|
|
string change_type = change["type"].asString();
|
|
|
|
|
string root_key = change["key"][(uint)0].asString();
|
|
|
|
|
|
|
|
|
|
// Determine type of change operation
|
|
|
|
|
if (change_type == "insert" || change_type == "update") {
|
|
|
|
|
|
|
|
|
|
// INSERT / UPDATE
|
|
|
|
|
// Check for valid property
|
|
|
|
|
if (root_key == "color")
|
|
|
|
|
// Set color
|
|
|
|
|
color.SetJsonValue(change["value"]);
|
|
|
|
|
else if (root_key == "viewport_scale")
|
|
|
|
|
// Set viewport scale
|
|
|
|
|
viewport_scale.SetJsonValue(change["value"]);
|
|
|
|
|
else if (root_key == "viewport_x")
|
|
|
|
|
// Set viewport x offset
|
|
|
|
|
viewport_x.SetJsonValue(change["value"]);
|
|
|
|
|
else if (root_key == "viewport_y")
|
|
|
|
|
// Set viewport y offset
|
|
|
|
|
viewport_y.SetJsonValue(change["value"]);
|
|
|
|
|
else
|
|
|
|
|
// Error parsing JSON (or missing keys)
|
|
|
|
|
throw InvalidJSONKey("JSON change key is invalid", change.toStyledString());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} else if (change["type"].asString() == "delete") {
|
|
|
|
|
|
|
|
|
|
// DELETE / RESET
|
|
|
|
|
// Reset the following properties (since we can't delete them)
|
|
|
|
|
if (root_key == "color") {
|
|
|
|
|
color = Color();
|
|
|
|
|
color.red = Keyframe(0.0);
|
|
|
|
|
color.green = Keyframe(0.0);
|
|
|
|
|
color.blue = Keyframe(0.0);
|
|
|
|
|
}
|
|
|
|
|
else if (root_key == "viewport_scale")
|
|
|
|
|
viewport_scale = Keyframe(1.0);
|
|
|
|
|
else if (root_key == "viewport_x")
|
|
|
|
|
viewport_x = Keyframe(0.0);
|
|
|
|
|
else if (root_key == "viewport_y")
|
|
|
|
|
viewport_y = Keyframe(0.0);
|
|
|
|
|
else
|
|
|
|
|
// Error parsing JSON (or missing keys)
|
|
|
|
|
throw InvalidJSONKey("JSON change key is invalid", change.toStyledString());
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|