Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate lists for video and audio filters #1126

Open
wants to merge 3 commits into
base: staging
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion js/module.d.ts
Original file line number Diff line number Diff line change
@@ -458,8 +458,12 @@ export interface IInput extends ISource {
sendFocus(focus: boolean): void;
sendKeyClick(eventData: IKeyEvent, keyUp: boolean): void;
setFilterOrder(filter: IFilter, movement: EOrderMovement): void;
setFilterOrder(filter: IFilter, movement: EOrderMovement): void;
setFilterPosition(filter: IFilter, position: number): void;
setVideoFilterPosition(filter: IFilter, position: number): void;
setAudioFilterPosition(filter: IFilter, position: number): void;
readonly filters: IFilter[];
readonly videoFilters: IFilter[];
readonly audioFilters: IFilter[];
readonly width: number;
readonly height: number;
getDuration(): number;
18 changes: 14 additions & 4 deletions js/module.ts
Original file line number Diff line number Diff line change
@@ -819,17 +819,27 @@ export interface IInput extends ISource {
setFilterOrder(filter: IFilter, movement: EOrderMovement): void;

/**
* Move a filter up, down, top, or bottom in the filter list.
* Set a filter position in the filter list.
* @param filter - The filter to move within the input source.
* @param movement - The movement to make within the list.
* @param position - The position to make within the list.
*/
setFilterOrder(filter: IFilter, movement: EOrderMovement): void;
setFilterPosition(filter: IFilter, position: number): void;
setVideoFilterPosition(filter: IFilter, position: number): void;
setAudioFilterPosition(filter: IFilter, position: number): void;


/**
* Obtain a list of all filters associated with the input source
*/
readonly filters: IFilter[];
readonly filters: IFilter[];
/**
* Obtain a list of video filters associated with the input source
*/
readonly videoFilters: IFilter[];
/**
* Obtain a list of audio filters associated with the input source
*/
readonly audioFilters: IFilter[];

/**
* Width of the underlying source
2 changes: 1 addition & 1 deletion obs-studio-client/source/cache-manager.hpp
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ struct SourceDataInfo
uint32_t audioMixers = UINT32_MAX;
bool audioMixersChanged = true;

std::vector<uint64_t>* filters = new std::vector<uint64_t>();
std::vector<std::pair<uint64_t, int>>* filters = new std::vector<std::pair<uint64_t, int>>();
bool filtersOrderChanged = true;
};

146 changes: 121 additions & 25 deletions obs-studio-client/source/input.cpp
Original file line number Diff line number Diff line change
@@ -47,6 +47,9 @@ Napi::Object osn::Input::Init(Napi::Env env, Napi::Object exports) {
InstanceMethod("addFilter", &osn::Input::AddFilter),
InstanceMethod("removeFilter", &osn::Input::RemoveFilter),
InstanceMethod("setFilterOrder", &osn::Input::SetFilterOrder),
InstanceMethod("setFilterPosition", &osn::Input::SetFilterPosition),
InstanceMethod("setVideoFilterPosition", &osn::Input::SetVideoFilterPosition),
InstanceMethod("setAudioFilterPosition", &osn::Input::SetAudioFilterPosition),
InstanceMethod("findFilter", &osn::Input::FindFilter),
InstanceMethod("copyFilters", &osn::Input::CopyFilters),

@@ -61,6 +64,8 @@ Napi::Object osn::Input::Init(Napi::Env env, Napi::Object exports) {
InstanceAccessor("deinterlaceFieldOrder", &osn::Input::GetDeinterlaceFieldOrder, &osn::Input::SetDeinterlaceFieldOrder),
InstanceAccessor("deinterlaceMode", &osn::Input::GetDeinterlaceMode, &osn::Input::SetDeinterlaceMode),
InstanceAccessor("filters", &osn::Input::Filters, nullptr),
InstanceAccessor("videoFilters", &osn::Input::VideoFilters, nullptr),
InstanceAccessor("audioFilters", &osn::Input::AudioFilters, nullptr),
InstanceAccessor("seek", &osn::Input::GetTime, &osn::Input::SetTime),

InstanceAccessor("configurable", &osn::Input::CallIsConfigurable, nullptr),
@@ -572,20 +577,44 @@ void osn::Input::SetDeinterlaceMode(const Napi::CallbackInfo& info, const Napi::
}

Napi::Value osn::Input::Filters(const Napi::CallbackInfo& info)
{
return GetFilters(info, osn::FilterSubset::All);
}

Napi::Value osn::Input::AudioFilters(const Napi::CallbackInfo& info)
{
return GetFilters(info, osn::FilterSubset::Audio);
}

Napi::Value osn::Input::VideoFilters(const Napi::CallbackInfo& info)
{
return GetFilters(info, osn::FilterSubset::Video);
}

Napi::Value osn::Input::FiltersFromCache(const Napi::CallbackInfo& info, osn::FilterSubset subset, std::vector<std::pair<uint64_t, int>> * filters)
{
Napi::Array array = Napi::Array::New(info.Env());
int index = 0;
for (uint32_t i = 0; i < filters->size(); i++) {
if ((filters->at(i).second == static_cast<int>(osn::FilterSubset::Video) && subset == osn::FilterSubset::Audio)
|| (filters->at(i).second == static_cast<int>(osn::FilterSubset::Audio) && subset == osn::FilterSubset::Video))
continue;
auto instance =
osn::Filter::constructor.New({
Napi::Number::New(info.Env(), filters->at(i).first)
});
array.Set(index, instance);
index++;
}
return array;
}

Napi::Value osn::Input::GetFilters(const Napi::CallbackInfo& info, osn::FilterSubset subset)
{
SourceDataInfo* sdi = CacheManager<SourceDataInfo*>::getInstance().Retrieve(this->sourceId);

if (sdi && !sdi->filtersOrderChanged) {
std::vector<uint64_t>* filters = sdi->filters;
Napi::Array array = Napi::Array::New(info.Env(), int(filters->size()));
for (uint32_t i = 0; i < filters->size(); i++) {
auto instance =
osn::Filter::constructor.New({
Napi::Number::New(info.Env(), filters->at(i))
});
array.Set(i, instance);
}
return array;
return FiltersFromCache(info, subset, sdi->filters);
}

auto conn = GetConnection(info);
@@ -598,28 +627,35 @@ Napi::Value osn::Input::Filters(const Napi::CallbackInfo& info)
if (!ValidateResponse(info, response))
return info.Env().Undefined();

std::vector<uint64_t>* filters;
Napi::Array array = Napi::Array::New(info.Env());
std::vector<std::pair<uint64_t, int>>* filters;
if (sdi) {
filters = sdi->filters;
filters->clear();
}

Napi::Array array = Napi::Array::New(info.Env(), response.size() - 1);
for (size_t idx = 1; idx < response.size(); idx++) {
auto instance =
osn::Filter::constructor.New({
Napi::Number::New(info.Env(), response[idx].value_union.ui64)
});
array.Set(uint32_t(idx) - 1, instance);

if (sdi)
filters->push_back(response[idx].value_union.ui64);
}
for (size_t idx = 1; idx < response.size(); idx += 2) {
filters->push_back(std::make_pair(response[idx].value_union.ui64, response[idx+1].value_union.i32));
}

if (sdi)
sdi->filtersOrderChanged = false;

return array;
return FiltersFromCache(info, subset, filters);
} else {
int index = 0;
for (size_t idx = 1; idx < response.size(); idx += 2) {
if ((response[idx+1].value_union.i32 == static_cast<int>(osn::FilterSubset::Video) && subset == osn::FilterSubset::Audio)
|| (response[idx+1].value_union.i32 == static_cast<int>(osn::FilterSubset::Audio) && subset == osn::FilterSubset::Video))
continue;

auto instance =
osn::Filter::constructor.New({
Napi::Number::New(info.Env(), response[idx].value_union.ui64)
});
array.Set(index, instance);
index++;
}
return array;
}
}

Napi::Value osn::Input::AddFilter(const Napi::CallbackInfo& info)
@@ -675,6 +711,66 @@ Napi::Value osn::Input::SetFilterOrder(const Napi::CallbackInfo& info)
return info.Env().Undefined();
}

Napi::Value osn::Input::SetFilterPosition(const Napi::CallbackInfo& info)
{
osn::Filter* objfilter = Napi::ObjectWrap<osn::Filter>::Unwrap(info[0].ToObject());
uint32_t position = info[1].ToNumber().Uint32Value();

auto conn = GetConnection(info);
if (!conn)
return info.Env().Undefined();

conn->call(
"Input", "PositionFilter", {ipc::value(this->sourceId), ipc::value(objfilter->sourceId), ipc::value(position), ipc::value(0)});

SourceDataInfo* sdi = CacheManager<SourceDataInfo*>::getInstance().Retrieve(this->sourceId);
if (sdi) {
sdi->filtersOrderChanged = true;
}

return info.Env().Undefined();
}

Napi::Value osn::Input::SetVideoFilterPosition(const Napi::CallbackInfo& info)
{
osn::Filter* objfilter = Napi::ObjectWrap<osn::Filter>::Unwrap(info[0].ToObject());
uint32_t position = info[1].ToNumber().Uint32Value();

auto conn = GetConnection(info);
if (!conn)
return info.Env().Undefined();

conn->call(
"Input", "PositionFilter", {ipc::value(this->sourceId), ipc::value(objfilter->sourceId), ipc::value(position), ipc::value(static_cast<int>(osn::FilterSubset::Video))});

SourceDataInfo* sdi = CacheManager<SourceDataInfo*>::getInstance().Retrieve(this->sourceId);
if (sdi) {
sdi->filtersOrderChanged = true;
}

return info.Env().Undefined();
}

Napi::Value osn::Input::SetAudioFilterPosition(const Napi::CallbackInfo& info)
{
osn::Filter* objfilter = Napi::ObjectWrap<osn::Filter>::Unwrap(info[0].ToObject());
uint32_t position = info[1].ToNumber().Uint32Value();

auto conn = GetConnection(info);
if (!conn)
return info.Env().Undefined();

conn->call(
"Input", "PositionFilter", {ipc::value(this->sourceId), ipc::value(objfilter->sourceId), ipc::value(position), ipc::value(static_cast<int>(osn::FilterSubset::Video))});

SourceDataInfo* sdi = CacheManager<SourceDataInfo*>::getInstance().Retrieve(this->sourceId);
if (sdi) {
sdi->filtersOrderChanged = true;
}

return info.Env().Undefined();
}

Napi::Value osn::Input::FindFilter(const Napi::CallbackInfo& info)
{
std::string name = info[0].ToString().Utf8Value();
13 changes: 13 additions & 0 deletions obs-studio-client/source/input.hpp
Original file line number Diff line number Diff line change
@@ -23,6 +23,12 @@

namespace osn
{
enum class FilterSubset : std::int16_t
{
All =0,
Audio = 2,
Video = 1
};
class Input : public Napi::ObjectWrap<osn::Input>
{
public:
@@ -43,6 +49,9 @@ namespace osn
Napi::Value AddFilter(const Napi::CallbackInfo& info);
Napi::Value RemoveFilter(const Napi::CallbackInfo& info);
Napi::Value SetFilterOrder(const Napi::CallbackInfo& info);
Napi::Value SetFilterPosition(const Napi::CallbackInfo& info);
Napi::Value SetVideoFilterPosition(const Napi::CallbackInfo& info);
Napi::Value SetAudioFilterPosition(const Napi::CallbackInfo& info);
Napi::Value FindFilter(const Napi::CallbackInfo& info);
Napi::Value CopyFilters(const Napi::CallbackInfo& info);

@@ -63,6 +72,10 @@ namespace osn
Napi::Value GetDeinterlaceMode(const Napi::CallbackInfo& info);
void SetDeinterlaceMode(const Napi::CallbackInfo& info, const Napi::Value &value);
Napi::Value Filters(const Napi::CallbackInfo& info);
Napi::Value VideoFilters(const Napi::CallbackInfo& info);
Napi::Value AudioFilters(const Napi::CallbackInfo& info);
Napi::Value GetFilters(const Napi::CallbackInfo& info, osn::FilterSubset subset);
Napi::Value FiltersFromCache(const Napi::CallbackInfo& info, osn::FilterSubset subset, std::vector<std::pair<uint64_t, int>> * filters);

Napi::Value CallIsConfigurable(const Napi::CallbackInfo& info);
Napi::Value CallGetProperties(const Napi::CallbackInfo& info);
68 changes: 66 additions & 2 deletions obs-studio-server/source/osn-input.cpp
Original file line number Diff line number Diff line change
@@ -90,13 +90,15 @@ void osn::Input::Register(ipc::server& srv)
"SetDeInterlaceMode", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::Int32}, GetDeInterlaceMode));

cls->register_function(
std::make_shared<ipc::function>("GetFilters", std::vector<ipc::type>{ipc::type::UInt64}, GetFilters));
std::make_shared<ipc::function>("GetFilters", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::Int32}, GetFilters));
cls->register_function(std::make_shared<ipc::function>(
"AddFilter", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::UInt64}, AddFilter));
cls->register_function(std::make_shared<ipc::function>(
"RemoveFilter", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::UInt64}, RemoveFilter));
cls->register_function(std::make_shared<ipc::function>(
"MoveFilter", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::UInt64, ipc::type::UInt32}, MoveFilter));
cls->register_function(std::make_shared<ipc::function>(
"PositionFilter", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::UInt64, ipc::type::UInt32, ipc::type::UInt32}, PositionFilter));
cls->register_function(std::make_shared<ipc::function>(
"FindFilter", std::vector<ipc::type>{ipc::type::UInt64, ipc::type::String}, FindFilter));
cls->register_function(std::make_shared<ipc::function>(
@@ -632,6 +634,66 @@ void osn::Input::MoveFilter(
AUTO_DEBUG;
}

size_t FilterPositionInFullList(obs_source_t* input, size_t position, int subset)
{
struct filter_position_info {
size_t count;
size_t position;
size_t subset_count;
size_t full_position;
int subset;
} info = {0, position, 0, position, subset};

auto enum_cb = [](obs_source_t* parent, obs_source_t* filter, void* data) {
filter_position_info* info = reinterpret_cast<filter_position_info*>(data);
uint32_t output_flags = obs_source_get_output_flags(filter);
info->count++;
if(info->subset == OBS_SOURCE_VIDEO && (output_flags & OBS_SOURCE_VIDEO)) {
info->subset_count++;
if( info->subset_count == info->position) {
info->full_position = info->count;
}
}
if(info->subset == OBS_SOURCE_AUDIO && (output_flags & OBS_SOURCE_AUDIO)) {
info->subset_count++;
if( info->subset_count == info->position) {
info->full_position = info->count;
}
}
};

obs_source_enum_filters(input, enum_cb, &info);
return info.full_position;
}

void osn::Input::PositionFilter(
void* data,
const int64_t id,
const std::vector<ipc::value>& args,
std::vector<ipc::value>& rval)
{
obs_source_t* input = osn::Source::Manager::GetInstance().find(args[0].value_union.ui64);
if (!input) {
PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Input reference is not valid.");
}

obs_source_t* filter = osn::Source::Manager::GetInstance().find(args[1].value_union.ui64);
if (!filter) {
PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Filter reference is not valid.");
}

size_t position = (size_t)args[2].value_union.ui32;
int subset = args[3].value_union.ui32;

if (subset != 0)
position = FilterPositionInFullList(input, position, subset);

obs_source_filter_set_position(input, filter, position);

rval.push_back(ipc::value((uint64_t)ErrorCode::Ok));
AUTO_DEBUG;
}

void osn::Input::FindFilter(
void* data,
const int64_t id,
@@ -674,10 +736,12 @@ void osn::Input::GetFilters(

auto enum_cb = [](obs_source_t* parent, obs_source_t* filter, void* data) {
std::vector<ipc::value>* rval = reinterpret_cast<std::vector<ipc::value>*>(data);

uint32_t output_flags = obs_source_get_output_flags(filter);
uint64_t id = osn::Source::Manager::GetInstance().find(filter);

if (id != UINT64_MAX) {
rval->push_back(id);
rval->push_back(output_flags & (OBS_SOURCE_AUDIO|OBS_SOURCE_VIDEO));
}
};

5 changes: 5 additions & 0 deletions obs-studio-server/source/osn-input.hpp
Original file line number Diff line number Diff line change
@@ -128,6 +128,11 @@ namespace osn
const int64_t id,
const std::vector<ipc::value>& args,
std::vector<ipc::value>& rval);
static void PositionFilter(
void* data,
const int64_t id,
const std::vector<ipc::value>& args,
std::vector<ipc::value>& rval);
static void FindFilter(
void* data,
const int64_t id,
103 changes: 102 additions & 1 deletion tests/osn-tests/src/test_osn_input.ts
Original file line number Diff line number Diff line change
@@ -563,7 +563,7 @@ describe(testName, () => {
});
});

it('Change the order of filters in the list', () => {
it('Change the order of filters in the list by moving', () => {
// Creating source
const input = osn.InputFactory.create(EOBSInputTypes.ImageSource, 'test_source');

@@ -620,6 +620,107 @@ describe(testName, () => {
input.release();
});

it('Change the order of filters in the list by positioning', () => {
// Creating source
const input = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'ffmpeg_source');

// Checking if source was created correctly
expect(input).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateInput, EOBSInputTypes.FFMPEGSource));
expect(input.id).to.equal(EOBSInputTypes.FFMPEGSource, GetErrorMessage(ETestErrorMsg.InputId, EOBSInputTypes.FFMPEGSource));
expect(input.name).to.equal('ffmpeg_source', GetErrorMessage(ETestErrorMsg.InputName, EOBSInputTypes.FFMPEGSource));

// Creating filters
const filter1 = osn.FilterFactory.create(EOBSFilterTypes.Color, 'filter1');
const filter2 = osn.FilterFactory.create(EOBSFilterTypes.Crop, 'filter2');
const filter3 = osn.FilterFactory.create(EOBSFilterTypes.Gain, 'filter3');
const filter4 = osn.FilterFactory.create(EOBSFilterTypes.GPUDelay, 'filter4');

// Checking if filters were created correctly
expect(filter1).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Color));
expect(filter2).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Crop));
expect(filter3).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Gain));
expect(filter4).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.GPUDelay));

// Adding filters to source
input.addFilter(filter1);
input.addFilter(filter2);
input.addFilter(filter3);
input.addFilter(filter4);

// Checking if filters are in the right position
expect(input.filters[0].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Color));
expect(input.filters[1].name).to.equal('filter2', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Crop));
expect(input.filters[2].name).to.equal('filter3', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Gain));
expect(input.filters[3].name).to.equal('filter4', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.GPUDelay));

// Changing filter order down
input.setFilterOrder(filter1, osn.EOrderMovement.Down);
// Checking if filter is in the right position
expect(input.filters[1].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.MoveFilterDown, EOBSFilterTypes.Color));

// Change filter position
input.setFilterPosition(filter1, 2);

// Checking if filter is in the right position
expect(input.filters[2].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.PositionFilter, EOBSFilterTypes.Color));

// Removing all filters
input.filters.forEach(function(filter) {
input.removeFilter(filter);
filter.release();
});

input.release();
});

it('Use separate lists for audio and video filters', () => {
// Creating source
const input = osn.InputFactory.create(EOBSInputTypes.FFMPEGSource, 'ffmpeg_source');

// Checking if source was created correctly
expect(input).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateInput, EOBSInputTypes.FFMPEGSource));
expect(input.id).to.equal(EOBSInputTypes.FFMPEGSource, GetErrorMessage(ETestErrorMsg.InputId, EOBSInputTypes.FFMPEGSource));
expect(input.name).to.equal('ffmpeg_source', GetErrorMessage(ETestErrorMsg.InputName, EOBSInputTypes.FFMPEGSource));

// Creating filters
const filter1 = osn.FilterFactory.create(EOBSFilterTypes.Color, 'filter1');
const filter2 = osn.FilterFactory.create(EOBSFilterTypes.Crop, 'filter2');
const filter3 = osn.FilterFactory.create(EOBSFilterTypes.Gain, 'filter3');
const filter4 = osn.FilterFactory.create(EOBSFilterTypes.GPUDelay, 'filter4');
const filter5 = osn.FilterFactory.create(EOBSFilterTypes.Compressor, 'filter5');

// Checking if filters were created correctly
expect(filter1).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Color));
expect(filter2).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Crop));
expect(filter3).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Gain));
expect(filter4).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.GPUDelay));
expect(filter5).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.CreateFilter, EOBSFilterTypes.Compressor));

// Adding filters to source
input.addFilter(filter1);
input.addFilter(filter2);
input.addFilter(filter3);
input.addFilter(filter4);

// Checking if filters are in the right position
expect(input.videoFilters[0].name).to.equal('filter1', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Color));
expect(input.videoFilters[1].name).to.equal('filter2', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Crop));
expect(input.videoFilters[2].name).to.equal('filter4', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.GPUDelay));
expect(input.audioFilters[0].name).to.equal('filter3', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Gain));

// Adding more filters
input.addFilter(filter5);
expect(input.audioFilters[1].name).to.equal('filter5', GetErrorMessage(ETestErrorMsg.FilterInsert, EOBSFilterTypes.Compressor));

// Removing all filters
input.filters.forEach(function(filter) {
input.removeFilter(filter);
filter.release();
});

input.release();
});

it('Fail test - Try to find an input that does not exist', () => {
let inputFromName: IInput;

2 changes: 2 additions & 0 deletions tests/osn-tests/util/error_messages.ts
Original file line number Diff line number Diff line change
@@ -86,6 +86,8 @@ export const enum ETestErrorMsg {
MonitoringType = 'Failed to update monitoring type of input %VALUE1%',
FindFilter = 'Did not found filter %VALUE1% in input %VALUE2%',
RemoveFilter = 'Not all filters were removed',
PositionFilter = 'Failed to move filter %VALUE1% into position',
FilterInsert = 'Failed to insert a filter %VALUE1% into source',
MoveFilterDown = 'Failed to move filter %VALUE1% down',
MoveFilterUp = 'Failed to move filter %VALUE1% up',
MoveFilterBottom = 'Failed to move filter %VALUE1% to bottom',