This source file includes following definitions.
- gf_sm_new
- gf_sm_stream_new
- gf_sm_stream_find
- gf_sm_get_mux_info
- gf_sm_au_del
- gf_sm_reset_stream
- gf_sm_delete_stream
- gf_sm_stream_del
- gf_sm_del
- gf_sm_reset
- gf_sm_stream_au_new
- node_in_commands_subtree
- store_or_aggregate
- gf_sm_get_stream
- gf_sm_aggregate
- gf_sm_load_string
- gf_sm_load_init
- gf_sm_load_done
- gf_sm_load_run
- gf_sm_load_suspend
- gf_sm_update_bitwrapper_buffer
#include <gpac/scene_manager.h>
#include <gpac/constants.h>
#include <gpac/media_tools.h>
#include <gpac/bifs.h>
#include <gpac/xml.h>
#include <gpac/internal/scenegraph_dev.h>
#include <gpac/network.h>
GF_EXPORT
GF_SceneManager *gf_sm_new(GF_SceneGraph *graph)
{
GF_SceneManager *tmp;
if (!graph) return NULL;
GF_SAFEALLOC(tmp, GF_SceneManager);
if (!tmp) return NULL;
tmp->streams = gf_list_new();
tmp->scene_graph = graph;
return tmp;
}
GF_EXPORT
GF_StreamContext *gf_sm_stream_new(GF_SceneManager *ctx, u16 ES_ID, u8 streamType, u8 objectType)
{
u32 i;
GF_StreamContext *tmp;
i=0;
while ((tmp = (GF_StreamContext*)gf_list_enum(ctx->streams, &i))) {
if (tmp->streamType!=streamType) continue;
if ( tmp->ESID==ES_ID ) {
return tmp;
}
}
GF_SAFEALLOC(tmp, GF_StreamContext);
if (!tmp) return NULL;
tmp->AUs = gf_list_new();
tmp->ESID = ES_ID;
tmp->streamType = streamType;
tmp->objectType = objectType ? objectType : 1;
tmp->timeScale = 1000;
gf_list_add(ctx->streams, tmp);
return tmp;
}
GF_StreamContext *gf_sm_stream_find(GF_SceneManager *ctx, u16 ES_ID)
{
u32 i, count;
if (!ES_ID) return NULL;
count = gf_list_count(ctx->streams);
for (i=0; i<count; i++) {
GF_StreamContext *tmp = (GF_StreamContext *)gf_list_get(ctx->streams, i);
if (tmp->ESID==ES_ID) return tmp;
}
return NULL;
}
GF_EXPORT
GF_MuxInfo *gf_sm_get_mux_info(GF_ESD *src)
{
u32 i;
GF_MuxInfo *mux;
i=0;
while ((mux = (GF_MuxInfo *)gf_list_enum(src->extensionDescriptors, &i))) {
if (mux->tag == GF_ODF_MUXINFO_TAG) return mux;
}
return NULL;
}
static void gf_sm_au_del(GF_StreamContext *sc, GF_AUContext *au)
{
while (gf_list_count(au->commands)) {
void *comptr = gf_list_last(au->commands);
gf_list_rem_last(au->commands);
switch (sc->streamType) {
case GF_STREAM_OD:
gf_odf_com_del((GF_ODCom**) & comptr);
break;
case GF_STREAM_SCENE:
gf_sg_command_del((GF_Command *)comptr);
break;
}
}
gf_list_del(au->commands);
gf_free(au);
}
static void gf_sm_reset_stream(GF_StreamContext *sc)
{
while (gf_list_count(sc->AUs)) {
GF_AUContext *au = (GF_AUContext *)gf_list_last(sc->AUs);
gf_list_rem_last(sc->AUs);
gf_sm_au_del(sc, au);
}
}
static void gf_sm_delete_stream(GF_StreamContext *sc)
{
gf_sm_reset_stream(sc);
gf_list_del(sc->AUs);
if (sc->name) gf_free(sc->name);
if (sc->dec_cfg) gf_free(sc->dec_cfg);
gf_free(sc);
}
GF_EXPORT
void gf_sm_stream_del(GF_SceneManager *ctx, GF_StreamContext *sc)
{
if (gf_list_del_item(ctx->streams, sc)>=0) {
gf_sm_delete_stream(sc);
}
}
GF_EXPORT
void gf_sm_del(GF_SceneManager *ctx)
{
u32 count;
while ( (count = gf_list_count(ctx->streams)) ) {
GF_StreamContext *sc = (GF_StreamContext *)gf_list_get(ctx->streams, count-1);
gf_list_rem(ctx->streams, count-1);
gf_sm_delete_stream(sc);
}
gf_list_del(ctx->streams);
if (ctx->root_od) gf_odf_desc_del((GF_Descriptor *) ctx->root_od);
gf_free(ctx);
}
GF_EXPORT
void gf_sm_reset(GF_SceneManager *ctx)
{
GF_StreamContext *sc;
u32 i=0;
while ( (sc = gf_list_enum(ctx->streams, &i)) ) {
gf_sm_reset_stream(sc);
}
if (ctx->root_od) gf_odf_desc_del((GF_Descriptor *) ctx->root_od);
ctx->root_od = NULL;
}
GF_EXPORT
GF_AUContext *gf_sm_stream_au_new(GF_StreamContext *stream, u64 timing, Double time_sec, Bool isRap)
{
u32 i;
GF_AUContext *tmp;
u64 tmp_timing;
tmp_timing = timing ? timing : (u64) (time_sec*1000);
if (stream->imp_exp_time >= tmp_timing) {
i=0;
while ((tmp = (GF_AUContext *)gf_list_enum(stream->AUs, &i))) {
if (timing && (tmp->timing==timing)) return tmp;
else if (time_sec && (tmp->timing_sec == time_sec)) return tmp;
else if (!time_sec && !timing && !tmp->timing && !tmp->timing_sec) return tmp;
else if ((time_sec && time_sec<tmp->timing_sec) || (timing && timing<tmp->timing)) {
GF_SAFEALLOC(tmp, GF_AUContext);
if (!tmp) return NULL;
tmp->commands = gf_list_new();
if (isRap) tmp->flags = GF_SM_AU_RAP;
tmp->timing = timing;
tmp->timing_sec = time_sec;
tmp->owner = stream;
gf_list_insert(stream->AUs, tmp, i-1);
return tmp;
}
}
}
GF_SAFEALLOC(tmp, GF_AUContext);
if (!tmp) return NULL;
tmp->commands = gf_list_new();
if (isRap) tmp->flags = GF_SM_AU_RAP;
tmp->timing = timing;
tmp->timing_sec = time_sec;
tmp->owner = stream;
if (stream->disable_aggregation) tmp->flags |= GF_SM_AU_NOT_AGGREGATED;
gf_list_add(stream->AUs, tmp);
stream->imp_exp_time = tmp_timing;
return tmp;
}
static Bool node_in_commands_subtree(GF_Node *node, GF_List *commands)
{
#ifndef GPAC_DISABLE_VRML
u32 i, j, count, nb_fields;
count = gf_list_count(commands);
for (i=0; i<count; i++) {
GF_Command *com = gf_list_get(commands, i);
if (com->tag>=GF_SG_LAST_BIFS_COMMAND) {
GF_LOG(GF_LOG_ERROR, GF_LOG_SCENE, ("[Scene Manager] Command check for LASeR/DIMS not supported\n"));
return 0;
}
if (com->tag==GF_SG_SCENE_REPLACE) {
if (gf_node_parent_of(com->node, node)) return 1;
continue;
}
nb_fields = gf_list_count(com->command_fields);
for (j=0; j<nb_fields; j++) {
GF_CommandField *field = gf_list_get(com->command_fields, j);
switch (field->fieldType) {
case GF_SG_VRML_SFNODE:
if (field->new_node) {
if (gf_node_parent_of(field->new_node, node)) return 1;
}
break;
case GF_SG_VRML_MFNODE:
if (field->field_ptr) {
GF_ChildNodeItem *child;
child = field->node_list;
while (child) {
if (gf_node_parent_of(child->node, node)) return 1;
child = child->next;
}
}
break;
}
}
}
#endif
return 0;
}
static u32 store_or_aggregate(GF_StreamContext *sc, GF_Command *com, GF_List *commands, Bool *has_modif)
{
#ifndef GPAC_DISABLE_VRML
u32 i, count, j, nb_fields;
GF_CommandField *field, *check_field;
if (node_in_commands_subtree(com->node, commands)) return 0;
count = gf_list_count(commands);
for (i=0; i<count; i++) {
GF_Command *check = gf_list_get(commands, i);
if (sc->streamType == GF_STREAM_SCENE) {
Bool check_index=0;
Bool original_is_index = 0;
Bool apply;
switch (com->tag) {
case GF_SG_INDEXED_REPLACE:
check_index=1;
case GF_SG_MULTIPLE_INDEXED_REPLACE:
case GF_SG_FIELD_REPLACE:
case GF_SG_MULTIPLE_REPLACE:
if (check->node != com->node) break;
if (check_index) {
if (check->tag == GF_SG_INDEXED_REPLACE) {}
else if (check->tag == GF_SG_INDEXED_INSERT) {
original_is_index = 1;
}
else {
break;
}
} else {
if (check->tag != com->tag) break;
}
nb_fields = gf_list_count(com->command_fields);
if (gf_list_count(check->command_fields) != nb_fields) break;
apply=1;
for (j=0; j<nb_fields; j++) {
field = gf_list_get(com->command_fields, j);
check_field = gf_list_get(check->command_fields, j);
if ((field->pos != check_field->pos) || (field->fieldIndex != check_field->fieldIndex)) {
apply=0;
break;
}
}
if (apply) {
if (original_is_index) com->tag = GF_SG_INDEXED_INSERT;
gf_sg_command_del((GF_Command *)check);
gf_list_rem(commands, i);
if (has_modif) *has_modif = 1;
return 1;
}
break;
case GF_SG_NODE_REPLACE:
if (check->tag != GF_SG_NODE_REPLACE) {
break;
}
if (gf_node_get_id(check->node) != gf_node_get_id(com->node) ) {
break;
}
gf_sg_command_del((GF_Command *)check);
gf_list_rem(commands, i);
if (has_modif) *has_modif = 1;
return 1;
case GF_SG_INDEXED_DELETE:
if (check->tag != GF_SG_INDEXED_INSERT) break;
if (com->node != check->node) break;
field = gf_list_get(com->command_fields, 0);
check_field = gf_list_get(check->command_fields, 0);
if (!field || !check_field) break;
if (field->pos != check_field->pos) break;
if (field->fieldIndex != check_field->fieldIndex) break;
gf_sg_command_del((GF_Command *)check);
gf_list_rem(commands, i);
if (has_modif) *has_modif = 1;
return 2;
default:
GF_LOG(GF_LOG_ERROR, GF_LOG_SCENE, ("[Scene Manager] Stream Aggregation not implemented for command - aggregating on main scene\n"));
break;
}
}
}
if (has_modif) *has_modif=1;
#endif
return 1;
}
static GF_StreamContext *gf_sm_get_stream(GF_SceneManager *ctx, u16 ESID)
{
u32 i, count;
count = gf_list_count(ctx->streams);
for (i=0; i<count; i++) {
GF_StreamContext *sc = gf_list_get(ctx->streams, i);
if (sc->ESID==ESID) return sc;
}
return NULL;
}
GF_EXPORT
GF_Err gf_sm_aggregate(GF_SceneManager *ctx, u16 ESID)
{
GF_Err e;
u32 i, stream_count;
#ifndef GPAC_DISABLE_VRML
u32 j;
GF_AUContext *au;
GF_Command *com;
#endif
e = GF_OK;
#if DEBUG_RAP
com_count = 0;
stream_count = gf_list_count(ctx->streams);
for (i=0; i<stream_count; i++) {
GF_StreamContext *sc = (GF_StreamContext *)gf_list_get(ctx->streams, i);
if (sc->streamType == GF_STREAM_SCENE) {
au_count = gf_list_count(sc->AUs);
for (j=0; j<au_count; j++) {
au = (GF_AUContext *)gf_list_get(sc->AUs, j);
com_count += gf_list_count(au->commands);
}
}
}
GF_LOG(GF_LOG_INFO, GF_LOG_SCENE, ("[SceneManager] Making RAP with %d commands\n", com_count));
#endif
stream_count = gf_list_count(ctx->streams);
for (i=0; i<stream_count; i++) {
GF_AUContext *carousel_au;
GF_List *carousel_commands;
GF_StreamContext *aggregate_on_stream;
GF_StreamContext *sc = (GF_StreamContext *)gf_list_get(ctx->streams, i);
if (ESID && (sc->ESID!=ESID)) continue;
carousel_au = NULL;
carousel_commands = NULL;
aggregate_on_stream = sc->aggregate_on_esid ? gf_sm_get_stream(ctx, sc->aggregate_on_esid) : NULL;
if (aggregate_on_stream==sc) {
carousel_commands = gf_list_new();
} else if (aggregate_on_stream) {
if (!gf_list_count(aggregate_on_stream->AUs)) {
carousel_au = gf_sm_stream_au_new(aggregate_on_stream, 0, 0, 1);
} else {
assert(gf_list_count(aggregate_on_stream->AUs)==1);
carousel_au = gf_list_get(aggregate_on_stream->AUs, 0);
}
carousel_commands = carousel_au->commands;
}
#ifndef GPAC_DISABLE_VRML
if (sc->streamType == GF_STREAM_SCENE) {
Bool has_modif = 0;
Bool base_stream_found = 0;
if (sc->objectType == GPAC_OTI_SCENE_DIMS) base_stream_found = 1;
while (gf_list_count(sc->AUs)) {
u32 count;
au = (GF_AUContext *) gf_list_get(sc->AUs, 0);
gf_list_rem(sc->AUs, 0);
if (au->flags & GF_SM_AU_NOT_AGGREGATED) {
gf_sm_au_del(sc, au);
continue;
}
count = gf_list_count(au->commands);
for (j=0; j<count; j++) {
u32 store=0;
com = gf_list_get(au->commands, j);
if (!base_stream_found) {
switch (com->tag) {
case GF_SG_SCENE_REPLACE:
case GF_SG_LSR_NEW_SCENE:
case GF_SG_LSR_REFRESH_SCENE:
base_stream_found = 1;
break;
}
}
if (base_stream_found || !sc->aggregate_on_esid) {
store = 0;
}
else {
switch (com->tag) {
case GF_SG_ROUTE_REPLACE:
case GF_SG_ROUTE_DELETE:
case GF_SG_ROUTE_INSERT:
case GF_SG_PROTO_INSERT:
case GF_SG_PROTO_DELETE:
case GF_SG_PROTO_DELETE_ALL:
case GF_SG_GLOBAL_QUANTIZER:
case GF_SG_LSR_RESTORE:
case GF_SG_LSR_SAVE:
case GF_SG_LSR_SEND_EVENT:
case GF_SG_LSR_CLEAN:
store = 1;
break;
default:
assert(carousel_commands);
store = store_or_aggregate(sc, com, carousel_commands, &has_modif);
break;
}
}
switch (store) {
case 2:
gf_list_rem(au->commands, j);
j--;
count--;
gf_sg_command_del((GF_Command *)com);
break;
case 1:
gf_list_insert(carousel_commands, com, 0);
gf_list_rem(au->commands, j);
j--;
count--;
break;
default:
e = gf_sg_command_apply(ctx->scene_graph, com, 0);
break;
}
}
gf_sm_au_del(sc, au);
}
if (base_stream_found) {
au = gf_sm_stream_au_new(sc, 0, 0, 1);
switch (sc->objectType) {
case GPAC_OTI_SCENE_BIFS:
case GPAC_OTI_SCENE_BIFS_V2:
com = gf_sg_command_new(ctx->scene_graph, GF_SG_SCENE_REPLACE);
break;
case GPAC_OTI_SCENE_LASER:
com = gf_sg_command_new(ctx->scene_graph, GF_SG_LSR_NEW_SCENE);
break;
case GPAC_OTI_SCENE_DIMS:
default:
com = NULL;
break;
}
if (com) {
com->node = ctx->scene_graph->RootNode;
ctx->scene_graph->RootNode = NULL;
gf_list_del(com->new_proto_list);
com->new_proto_list = ctx->scene_graph->protos;
ctx->scene_graph->protos = NULL;
com->aggregated = 1;
gf_list_add(au->commands, com);
}
}
else if (carousel_commands) {
if (!carousel_au) {
carousel_au = gf_sm_stream_au_new(sc, 0, 0, 1);
gf_list_del(carousel_au->commands);
carousel_au->commands = carousel_commands;
}
carousel_au->flags |= GF_SM_AU_RAP | GF_SM_AU_CAROUSEL;
if (has_modif) carousel_au->flags |= GF_SM_AU_MODIFIED;
}
}
#endif
}
return e;
}
#ifndef GPAC_DISABLE_LOADER_BT
GF_Err gf_sm_load_init_bt(GF_SceneLoader *load);
#endif
#ifndef GPAC_DISABLE_LOADER_XMT
GF_Err gf_sm_load_init_xmt(GF_SceneLoader *load);
#endif
#ifndef GPAC_DISABLE_LOADER_ISOM
GF_Err gf_sm_load_init_isom(GF_SceneLoader *load);
#endif
#ifndef GPAC_DISABLE_SVG
GF_Err gf_sm_load_init_svg(GF_SceneLoader *load);
GF_Err gf_sm_load_init_xbl(GF_SceneLoader *load);
GF_Err gf_sm_load_run_xbl(GF_SceneLoader *load);
void gf_sm_load_done_xbl(GF_SceneLoader *load);
#endif
#ifndef GPAC_DISABLE_SWF_IMPORT
GF_Err gf_sm_load_init_swf(GF_SceneLoader *load);
#endif
#ifndef GPAC_DISABLE_QTVR
GF_Err gf_sm_load_init_qt(GF_SceneLoader *load);
#endif
GF_EXPORT
GF_Err gf_sm_load_string(GF_SceneLoader *load, const char *str, Bool do_clean)
{
GF_Err e;
if (!load->type) e = GF_BAD_PARAM;
else if (load->parse_string) e = load->parse_string(load, str);
else e = GF_NOT_SUPPORTED;
return e;
}
GF_EXPORT
GF_Err gf_sm_load_init(GF_SceneLoader *load)
{
GF_Err e = GF_NOT_SUPPORTED;
char *ext, szExt[50];
if (!load || (!load->ctx && !load->scene_graph)
#ifndef GPAC_DISABLE_ISOM
|| (!load->fileName && !load->isom && !(load->flags & GF_SM_LOAD_FOR_PLAYBACK) )
#endif
) return GF_BAD_PARAM;
if (!load->type) {
#ifndef GPAC_DISABLE_ISOM
if (load->isom) {
load->type = GF_SM_LOAD_MP4;
} else
#endif
{
ext = (char *)strrchr(load->fileName, '.');
if (!ext) return GF_NOT_SUPPORTED;
if (!stricmp(ext, ".gz")) {
char *anext;
ext[0] = 0;
anext = (char *)strrchr(load->fileName, '.');
ext[0] = '.';
ext = anext;
}
strcpy(szExt, &ext[1]);
strlwr(szExt);
if (strstr(szExt, "bt")) load->type = GF_SM_LOAD_BT;
else if (strstr(szExt, "wrl")) load->type = GF_SM_LOAD_VRML;
else if (strstr(szExt, "x3dv")) load->type = GF_SM_LOAD_X3DV;
#ifndef GPAC_DISABLE_LOADER_XMT
else if (strstr(szExt, "xmt") || strstr(szExt, "xmta")) load->type = GF_SM_LOAD_XMTA;
else if (strstr(szExt, "x3d")) load->type = GF_SM_LOAD_X3D;
#endif
else if (strstr(szExt, "swf")) load->type = GF_SM_LOAD_SWF;
else if (strstr(szExt, "mov")) load->type = GF_SM_LOAD_QT;
else if (strstr(szExt, "svg")) load->type = GF_SM_LOAD_SVG;
else if (strstr(szExt, "xsr")) load->type = GF_SM_LOAD_XSR;
else if (strstr(szExt, "xbl")) load->type = GF_SM_LOAD_XBL;
else if (strstr(szExt, "xml")) {
char *rtype = gf_xml_get_root_type(load->fileName, &e);
if (rtype) {
if (!strcmp(rtype, "SAFSession")) load->type = GF_SM_LOAD_XSR;
else if (!strcmp(rtype, "XMT-A")) load->type = GF_SM_LOAD_XMTA;
else if (!strcmp(rtype, "X3D")) load->type = GF_SM_LOAD_X3D;
else if (!strcmp(rtype, "bindings")) load->type = GF_SM_LOAD_XBL;
gf_free(rtype);
}
}
}
}
if (!load->type) return e;
if (!load->scene_graph) load->scene_graph = load->ctx->scene_graph;
switch (load->type) {
#ifndef GPAC_DISABLE_LOADER_BT
case GF_SM_LOAD_BT:
case GF_SM_LOAD_VRML:
case GF_SM_LOAD_X3DV:
return gf_sm_load_init_bt(load);
#endif
#ifndef GPAC_DISABLE_LOADER_XMT
case GF_SM_LOAD_XMTA:
case GF_SM_LOAD_X3D:
return gf_sm_load_init_xmt(load);
#endif
#ifndef GPAC_DISABLE_SVG
case GF_SM_LOAD_SVG:
case GF_SM_LOAD_XSR:
case GF_SM_LOAD_DIMS:
return gf_sm_load_init_svg(load);
case GF_SM_LOAD_XBL:
e = gf_sm_load_init_xbl(load);
load->process = gf_sm_load_run_xbl;;
load->done = gf_sm_load_done_xbl;
return e;
#endif
#ifndef GPAC_DISABLE_SWF_IMPORT
case GF_SM_LOAD_SWF:
return gf_sm_load_init_swf(load);
#endif
#ifndef GPAC_DISABLE_LOADER_ISOM
case GF_SM_LOAD_MP4:
return gf_sm_load_init_isom(load);
#endif
#ifndef GPAC_DISABLE_QTVR
case GF_SM_LOAD_QT:
return gf_sm_load_init_qt(load);
#endif
default:
return GF_NOT_SUPPORTED;
}
return GF_NOT_SUPPORTED;
}
GF_EXPORT
void gf_sm_load_done(GF_SceneLoader *load)
{
if (load->done) load->done(load);
}
GF_EXPORT
GF_Err gf_sm_load_run(GF_SceneLoader *load)
{
if (load->process) return load->process(load);
return GF_OK;
}
GF_EXPORT
GF_Err gf_sm_load_suspend(GF_SceneLoader *load, Bool suspend)
{
if (load->suspend) return load->suspend(load, suspend);
return GF_OK;
}
#if !defined(GPAC_DISABLE_LOADER_BT) || !defined(GPAC_DISABLE_LOADER_XMT)
#include <gpac/base_coding.h>
void gf_sm_update_bitwrapper_buffer(GF_Node *node, const char *fileName)
{
u32 data_size = 0;
char *data = NULL;
char *buffer;
M_BitWrapper *bw = (M_BitWrapper *)node;
if (!bw->buffer.buffer) return;
buffer = bw->buffer.buffer;
if (!strnicmp(buffer, "file://", 7)) {
char *url = gf_url_concatenate(fileName, buffer+7);
if (url) {
FILE *f = gf_fopen(url, "rb");
if (f) {
fseek(f, 0, SEEK_END);
data_size = (u32) ftell(f);
fseek(f, 0, SEEK_SET);
data = gf_malloc(sizeof(char)*data_size);
if (data) {
if (fread(data, 1, data_size, f) != data_size) {
GF_LOG(GF_LOG_ERROR, GF_LOG_SCENE, ("[Scene Manager] error reading bitwrapper file %s\n", url));
}
}
gf_fclose(f);
}
gf_free(url);
}
} else {
Bool base_64 = 0;
if (!strnicmp(buffer, "data:application/octet-string", 29)) {
char *sep = strchr(bw->buffer.buffer, ',');
base_64 = strstr(bw->buffer.buffer, ";base64") ? 1 : 0;
if (sep) buffer = sep+1;
}
if (base_64) {
data_size = 2 * (u32) strlen(buffer);
data = (char*)gf_malloc(sizeof(char)*data_size);
if (data)
data_size = gf_base64_decode(buffer, (u32) strlen(buffer), data, data_size);
} else {
u32 i, c;
char s[3];
data_size = (u32) strlen(buffer) / 3;
data = (char*)gf_malloc(sizeof(char) * data_size);
if (data) {
s[2] = 0;
for (i=0; i<data_size; i++) {
s[0] = buffer[3*i+1];
s[1] = buffer[3*i+2];
sscanf(s, "%02X", &c);
data[i] = (unsigned char) c;
}
}
}
}
gf_free(bw->buffer.buffer);
bw->buffer.buffer = NULL;
bw->buffer_len = 0;
if (data) {
bw->buffer.buffer = data;
bw->buffer_len = data_size;
}
}
#endif