From 4d9808a6a48bd7ad4617709a2a1dbb6891deb963 Mon Sep 17 00:00:00 2001 From: Liong Sauw Ming Date: Wed, 24 Sep 2014 04:01:34 +0000 Subject: Re #1790: Initial basic implementation of Android OpenGL renderer To use it, app has to specify PJMEDIA_HAS_VIDEO to 1 git-svn-id: http://svn.pjsip.org/repos/pjproject/trunk@4928 74dad513-b988-da41-8d7b-12977e46ad98 --- pjmedia/build/Makefile | 3 +- pjmedia/build/os-auto.mak.in | 5 +- pjmedia/include/pjmedia-videodev/config.h | 16 + pjmedia/src/pjmedia-videodev/android_opengl.c | 612 ++++++++++++++++++++++++++ 4 files changed, 634 insertions(+), 2 deletions(-) create mode 100644 pjmedia/src/pjmedia-videodev/android_opengl.c (limited to 'pjmedia') diff --git a/pjmedia/build/Makefile b/pjmedia/build/Makefile index 54395c38..ba2b8187 100644 --- a/pjmedia/build/Makefile +++ b/pjmedia/build/Makefile @@ -106,7 +106,8 @@ export PJMEDIA_AUDIODEV_LDFLAGS += $(PJLIB_LDLIB) \ # export PJMEDIA_VIDEODEV_SRCDIR = ../src/pjmedia-videodev export PJMEDIA_VIDEODEV_OBJS += errno.o videodev.o avi_dev.o ffmpeg_dev.o \ - colorbar_dev.o v4l2_dev.o opengl_dev.o + colorbar_dev.o v4l2_dev.o opengl_dev.o \ + android_opengl.o export PJMEDIA_VIDEODEV_CFLAGS += $(_CFLAGS) export PJMEDIA_VIDEODEV_CXXFLAGS += $(_CXXFLAGS) export PJMEDIA_VIDEODEV_LDFLAGS += $(PJLIB_LDLIB) \ diff --git a/pjmedia/build/os-auto.mak.in b/pjmedia/build/os-auto.mak.in index 91b14aa4..2e8d24d7 100644 --- a/pjmedia/build/os-auto.mak.in +++ b/pjmedia/build/os-auto.mak.in @@ -25,6 +25,9 @@ QT_CFLAGS = @ac_qt_cflags@ # iOS IOS_CFLAGS = @ac_ios_cflags@ +# Android +ANDROID_CFLAGS = @ac_android_cflags@ + # libyuv LIBYUV_CFLAGS = @ac_libyuv_cflags@ LIBYUV_LDFLAGS = @ac_libyuv_ldflags@ @@ -33,7 +36,7 @@ LIBYUV_LDFLAGS = @ac_libyuv_ldflags@ # PJMEDIA features exclusion export CFLAGS += @ac_no_small_filter@ @ac_no_large_filter@ @ac_no_speex_aec@ \ $(SDL_CFLAGS) $(FFMPEG_CFLAGS) $(V4L2_CFLAGS) $(QT_CFLAGS) \ - $(IOS_CFLAGS) $(LIBYUV_CFLAGS) + $(IOS_CFLAGS) $(ANDROID_CFLAGS) $(LIBYUV_CFLAGS) export LDFLAGS += $(SDL_LDFLAGS) $(FFMPEG_LDFLAGS) $(V4L2_LDFLAGS) \ $(LIBYUV_LDFLAGS) diff --git a/pjmedia/include/pjmedia-videodev/config.h b/pjmedia/include/pjmedia-videodev/config.h index f11485f0..85c7415e 100644 --- a/pjmedia/include/pjmedia-videodev/config.h +++ b/pjmedia/include/pjmedia-videodev/config.h @@ -70,6 +70,22 @@ PJ_BEGIN_DECL #endif +/** + * This setting controls whether OpenGL for Android should be included. + * + * Default: 0 (or detected by configure) + */ +#ifndef PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL +# define PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL 0 +#else +# if defined(PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL) && \ + PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL != 0 +# undef PJMEDIA_VIDEO_DEV_HAS_OPENGL_ES +# define PJMEDIA_VIDEO_DEV_HAS_OPENGL_ES 1 +# endif +#endif + + /** * This setting controls whether OpenGL ES support should be included. * diff --git a/pjmedia/src/pjmedia-videodev/android_opengl.c b/pjmedia/src/pjmedia-videodev/android_opengl.c new file mode 100644 index 00000000..f494e004 --- /dev/null +++ b/pjmedia/src/pjmedia-videodev/android_opengl.c @@ -0,0 +1,612 @@ +/* $Id$ */ +/* + * Copyright (C) 2013-2014 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +#include +#include +#include +#include + +#if defined(PJMEDIA_HAS_VIDEO) && PJMEDIA_HAS_VIDEO != 0 && \ + defined(PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL) && \ + PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL != 0 + +#include +#include +#include +#include +#include +#include + +#define THIS_FILE "android_opengl.cpp" + +#define MAX_JOBS 1 + +extern JavaVM *pj_jni_jvm; + +typedef struct andgl_fmt_info +{ + pjmedia_format_id pjmedia_format; +} andgl_fmt_info; + +/* Supported formats */ +static andgl_fmt_info andgl_fmts[] = +{ + {PJMEDIA_FORMAT_BGRA} +}; + +typedef pj_status_t (*job_func_ptr)(void *data); + +typedef struct job { + job_func_ptr func; + void *data; + unsigned flags; + pj_status_t retval; +} job; + +typedef struct job_queue { + job *jobs[MAX_JOBS]; + pj_sem_t *job_sem[MAX_JOBS]; + pj_mutex_t *mutex; + pj_thread_t *thread; + pj_sem_t *sem; + + unsigned size; + unsigned head, tail; + pj_bool_t is_quitting; +} job_queue; + +/* Video stream. */ +struct andgl_stream +{ + pjmedia_vid_dev_stream base; /**< Base stream */ + pjmedia_vid_dev_param param; /**< Settings */ + pj_pool_t *pool; /**< Memory pool */ + + pjmedia_vid_dev_cb vid_cb; /**< Stream callback */ + void *user_data; /**< Application data */ + + pj_timestamp frame_ts; + unsigned ts_inc; + pjmedia_rect_size vid_size; + + job_queue *jq; + pj_bool_t is_running; + const pjmedia_frame *frame; + + gl_buffers *gl_buf; + EGLDisplay display; + EGLSurface surface; + EGLContext context; + ANativeWindow *window; +}; + + +/* Prototypes */ +static pj_status_t andgl_stream_get_param(pjmedia_vid_dev_stream *strm, + pjmedia_vid_dev_param *param); +static pj_status_t andgl_stream_get_cap(pjmedia_vid_dev_stream *strm, + pjmedia_vid_dev_cap cap, + void *value); +static pj_status_t andgl_stream_set_cap(pjmedia_vid_dev_stream *strm, + pjmedia_vid_dev_cap cap, + const void *value); +static pj_status_t andgl_stream_start(pjmedia_vid_dev_stream *strm); +static pj_status_t andgl_stream_put_frame(pjmedia_vid_dev_stream *strm, + const pjmedia_frame *frame); +static pj_status_t andgl_stream_stop(pjmedia_vid_dev_stream *strm); +static pj_status_t andgl_stream_destroy(pjmedia_vid_dev_stream *strm); + +/* Job queue prototypes */ +static pj_status_t job_queue_create(pj_pool_t *pool, job_queue **pjq); +static pj_status_t job_queue_post_job(job_queue *jq, job_func_ptr func, + void *data, unsigned flags, + pj_status_t *retval); +static pj_status_t job_queue_destroy(job_queue *jq); + +/* Operations */ +static pjmedia_vid_dev_stream_op stream_op = +{ + &andgl_stream_get_param, + &andgl_stream_get_cap, + &andgl_stream_set_cap, + &andgl_stream_start, + NULL, + &andgl_stream_put_frame, + &andgl_stream_stop, + &andgl_stream_destroy +}; + +ANativeWindow *def_native_win = NULL; + +static void get_jvm(JNIEnv **jni_env) +{ + (*pj_jni_jvm)->GetEnv(pj_jni_jvm, (void **)jni_env, JNI_VERSION_1_4); +} + +void android_opengl_set_surface(jobject surface) +{ + JNIEnv *env = 0; + + PJ_LOG(4, (THIS_FILE, "Setting default OpenGL surface")); + + if (!surface) { + def_native_win = NULL; + } else { + get_jvm(&env); + if (env) def_native_win = ANativeWindow_fromSurface(env, surface); + } +} + +int pjmedia_vid_dev_opengl_imp_get_cap(void) +{ + return PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW | + PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE; +} + +static andgl_fmt_info* get_andgl_format_info(pjmedia_format_id id) +{ + unsigned i; + + for (i = 0; i < PJ_ARRAY_SIZE(andgl_fmts); i++) { + if (andgl_fmts[i].pjmedia_format == id) + return &andgl_fmts[i]; + } + + return NULL; +} + +#define EGL_ERR(str) \ + { \ + PJ_LOG(3, (THIS_FILE, "Unable to %s %d", str, eglGetError())); \ + return PJMEDIA_EVID_SYSERR; \ + } + +static pj_status_t init_opengl(void * data) +{ + struct andgl_stream *strm = (struct andgl_stream *)data; + const EGLint attr[] = + { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, 8, EGL_NONE + }; + EGLint context_attr[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; + EGLConfig config; + EGLint numConfigs; + EGLint format; + EGLint width; + EGLint height; + + strm->display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (strm->display == EGL_NO_DISPLAY || + !eglInitialize(strm->display, NULL, NULL)) + { + EGL_ERR("initialize OpenGL display"); + } + + if (!eglChooseConfig(strm->display, attr, &config, 1, &numConfigs) || + (!eglGetConfigAttrib(strm->display, config, EGL_NATIVE_VISUAL_ID, + &format))) + { + EGL_ERR("configure OpenGL display"); + } + + if (ANativeWindow_setBuffersGeometry(strm->window, strm->param.disp_size.w, + strm->param.disp_size.h, format) != 0) + { + EGL_ERR("set window geometry"); + } + + strm->surface = eglCreateWindowSurface(strm->display, config, + strm->window, 0); + if (strm->surface == EGL_NO_SURFACE) + EGL_ERR("create window surface"); + + strm->context = eglCreateContext(strm->display, config, EGL_NO_CONTEXT, + context_attr); + if (strm->context == EGL_NO_CONTEXT) + EGL_ERR("create OpenGL context"); + + if (!eglMakeCurrent(strm->display, strm->surface, strm->surface, + strm->context)) + { + EGL_ERR("make OpenGL as current context"); + } + + if (!eglQuerySurface(strm->display, strm->surface, EGL_WIDTH, &width) || + !eglQuerySurface(strm->display, strm->surface, EGL_HEIGHT, &height)) + { + EGL_ERR("query surface"); + } + + /* Create GL buffers */ + pjmedia_vid_dev_opengl_create_buffers(strm->pool, PJ_TRUE, &strm->gl_buf); + + /* Init GL buffers */ + return pjmedia_vid_dev_opengl_init_buffers(strm->gl_buf); +} + +static pj_status_t render(void * data) +{ + struct andgl_stream *stream = (struct andgl_stream *)data; + + pjmedia_vid_dev_opengl_draw(stream->gl_buf, stream->vid_size.w, + stream->vid_size.h, stream->frame->buf); + + return eglSwapBuffers(stream->display, stream->surface); +} + +static pj_status_t deinit_opengl(void * data) +{ + struct andgl_stream *stream = (struct andgl_stream *)data; + + pjmedia_vid_dev_opengl_destroy_buffers(stream->gl_buf); + + eglMakeCurrent(stream->display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT); + eglDestroyContext(stream->display, stream->context); + eglDestroySurface(stream->display, stream->surface); + eglTerminate(stream->display); + + stream->display = EGL_NO_DISPLAY; + stream->surface = EGL_NO_SURFACE; + stream->context = EGL_NO_CONTEXT; + + return PJ_SUCCESS; +} + +/* API: create stream */ +pj_status_t +pjmedia_vid_dev_opengl_imp_create_stream(pj_pool_t *pool, + pjmedia_vid_dev_param *param, + const pjmedia_vid_dev_cb *cb, + void *user_data, + pjmedia_vid_dev_stream **p_vid_strm) +{ + struct andgl_stream *strm; + const pjmedia_video_format_detail *vfd; + pj_status_t status = PJ_SUCCESS; + + strm = PJ_POOL_ZALLOC_T(pool, struct andgl_stream); + pj_memcpy(&strm->param, param, sizeof(*param)); + strm->pool = pool; + pj_memcpy(&strm->vid_cb, cb, sizeof(*cb)); + strm->user_data = user_data; + + vfd = pjmedia_format_get_video_format_detail(&strm->param.fmt, PJ_TRUE); + strm->ts_inc = PJMEDIA_SPF2(param->clock_rate, &vfd->fps, 1); + + /* If OUTPUT_RESIZE flag is not used, set display size to default */ + if (!(param->flags & PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE)) { + pj_bzero(&strm->param.disp_size, sizeof(strm->param.disp_size)); + } + + if (param->flags & PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW) { + andgl_stream_set_cap(&strm->base, PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW, + param->window.info.android.window); + } else { + strm->window = def_native_win; + } + + if (!strm->window) { + PJ_LOG(3, (THIS_FILE, "Unable to find output window nor surface")); + status = PJ_EINVAL; + goto on_error; + } + + /* Set video format */ + status = andgl_stream_set_cap(&strm->base, PJMEDIA_VID_DEV_CAP_FORMAT, + ¶m->fmt); + if (status != PJ_SUCCESS) + goto on_error; + + status = job_queue_create(pool, &strm->jq); + if (status != PJ_SUCCESS) + goto on_error; + + job_queue_post_job(strm->jq, init_opengl, strm, 0, &status); + if (status != PJ_SUCCESS) + goto on_error; + + /* Apply the remaining settings */ +/* if (param->flags & PJMEDIA_VID_DEV_CAP_ORIENTATION) { + andgl_stream_set_cap(&strm->base, PJMEDIA_VID_DEV_CAP_ORIENTATION, + ¶m->orient); + } +*/ + PJ_LOG(4, (THIS_FILE, "Android OpenGL ES renderer successfully created")); + + /* Done */ + strm->base.op = &stream_op; + *p_vid_strm = &strm->base; + + return PJ_SUCCESS; + +on_error: + andgl_stream_destroy((pjmedia_vid_dev_stream *)strm); + + return status; +} + +/* API: Get stream info. */ +static pj_status_t andgl_stream_get_param(pjmedia_vid_dev_stream *s, + pjmedia_vid_dev_param *pi) +{ + struct andgl_stream *strm = (struct andgl_stream*)s; + + PJ_ASSERT_RETURN(strm && pi, PJ_EINVAL); + + pj_memcpy(pi, &strm->param, sizeof(*pi)); + + if (andgl_stream_get_cap(s, PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW, + &pi->window) == PJ_SUCCESS) + { + pi->flags |= PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW; + } + + return PJ_SUCCESS; +} + +/* API: get capability */ +static pj_status_t andgl_stream_get_cap(pjmedia_vid_dev_stream *s, + pjmedia_vid_dev_cap cap, + void *pval) +{ + struct andgl_stream *strm = (struct andgl_stream*)s; + + PJ_UNUSED_ARG(strm); + + PJ_ASSERT_RETURN(s && pval, PJ_EINVAL); + + if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW) { + pjmedia_vid_dev_hwnd *wnd = (pjmedia_vid_dev_hwnd *)pval; + wnd->info.android.window = strm->window; + return PJ_SUCCESS; + } else { + return PJMEDIA_EVID_INVCAP; + } +} + +/* API: set capability */ +static pj_status_t andgl_stream_set_cap(pjmedia_vid_dev_stream *s, + pjmedia_vid_dev_cap cap, + const void *pval) +{ + struct andgl_stream *strm = (struct andgl_stream*)s; + + PJ_UNUSED_ARG(strm); + + PJ_ASSERT_RETURN(s && pval, PJ_EINVAL); + + if (cap==PJMEDIA_VID_DEV_CAP_FORMAT) { + const pjmedia_video_format_info *vfi; + pjmedia_video_format_detail *vfd; + pjmedia_format *fmt = (pjmedia_format *)pval; + andgl_fmt_info *ifi; + + if (!(ifi = get_andgl_format_info(fmt->id))) + return PJMEDIA_EVID_BADFORMAT; + + vfi = pjmedia_get_video_format_info(pjmedia_video_format_mgr_instance(), + fmt->id); + if (!vfi) + return PJMEDIA_EVID_BADFORMAT; + + pjmedia_format_copy(&strm->param.fmt, fmt); + + vfd = pjmedia_format_get_video_format_detail(fmt, PJ_TRUE); + pj_memcpy(&strm->vid_size, &vfd->size, sizeof(vfd->size)); + if (strm->param.disp_size.w == 0 || strm->param.disp_size.h == 0) + pj_memcpy(&strm->param.disp_size, &vfd->size, sizeof(vfd->size)); + + return PJ_SUCCESS; + } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_WINDOW) { + strm->window = (ANativeWindow *)pval; + strm->param.window.info.android.window = (void *)pval; + return PJ_SUCCESS; + } else if (cap == PJMEDIA_VID_DEV_CAP_OUTPUT_RESIZE) { + pj_memcpy(&strm->param.disp_size, pval, sizeof(strm->param.disp_size)); + return PJ_SUCCESS; + } else if (cap == PJMEDIA_VID_DEV_CAP_ORIENTATION) { + pj_memcpy(&strm->param.orient, pval, sizeof(strm->param.orient)); + if (strm->param.orient == PJMEDIA_ORIENT_UNKNOWN) + return PJ_SUCCESS; + return PJ_SUCCESS; + } + + return PJMEDIA_EVID_INVCAP; +} + +/* API: Start stream. */ +static pj_status_t andgl_stream_start(pjmedia_vid_dev_stream *strm) +{ + struct andgl_stream *stream = (struct andgl_stream*)strm; + + stream->is_running = PJ_TRUE; + PJ_LOG(4, (THIS_FILE, "Starting Android opengl stream")); + + return PJ_SUCCESS; +} + +/* API: Put frame from stream */ +static pj_status_t andgl_stream_put_frame(pjmedia_vid_dev_stream *strm, + const pjmedia_frame *frame) +{ + struct andgl_stream *stream = (struct andgl_stream*)strm; + pj_status_t status; + + if (!stream->is_running) + return PJ_EINVALIDOP; + + stream->frame = frame; + job_queue_post_job(stream->jq, render, strm, 0, &status); + + return status; +} + +/* API: Stop stream. */ +static pj_status_t andgl_stream_stop(pjmedia_vid_dev_stream *strm) +{ + struct andgl_stream *stream = (struct andgl_stream*)strm; + + stream->is_running = PJ_FALSE; + PJ_LOG(4, (THIS_FILE, "Stopping Android opengl stream")); + + return PJ_SUCCESS; +} + + +/* API: Destroy stream. */ +static pj_status_t andgl_stream_destroy(pjmedia_vid_dev_stream *strm) +{ + struct andgl_stream *stream = (struct andgl_stream*)strm; + + PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL); + + andgl_stream_stop(strm); + + job_queue_post_job(stream->jq, deinit_opengl, strm, 0, NULL); + + if (stream->jq) { + job_queue_destroy(stream->jq); + stream->jq = NULL; + } + + pj_pool_release(stream->pool); + + return PJ_SUCCESS; +} + +static int job_thread(void * data) +{ + job_queue *jq = (job_queue *)data; + + while (1) { + job *jb; + + /* Wait until there is a job. */ + pj_sem_wait(jq->sem); + + /* Make sure there is no pending jobs before we quit. */ + if (jq->is_quitting && jq->head == jq->tail) + break; + + jb = jq->jobs[jq->head]; + jb->retval = (*jb->func)(jb->data); + pj_sem_post(jq->job_sem[jq->head]); + jq->head = (jq->head + 1) % jq->size; + } + + return 0; +} + +static pj_status_t job_queue_create(pj_pool_t *pool, job_queue **pjq) +{ + unsigned i; + pj_status_t status; + + job_queue *jq = PJ_POOL_ZALLOC_T(pool, job_queue); + jq->size = MAX_JOBS; + status = pj_sem_create(pool, "thread_sem", 0, jq->size + 1, &jq->sem); + if (status != PJ_SUCCESS) + goto on_error; + + for (i = 0; i < jq->size; i++) { + status = pj_sem_create(pool, "job_sem", 0, 1, &jq->job_sem[i]); + if (status != PJ_SUCCESS) + goto on_error; + } + + status = pj_mutex_create_recursive(pool, "job_mutex", &jq->mutex); + if (status != PJ_SUCCESS) + goto on_error; + + status = pj_thread_create(pool, "job_th", job_thread, jq, 0, 0, + &jq->thread); + if (status != PJ_SUCCESS) + goto on_error; + + *pjq = jq; + return PJ_SUCCESS; + +on_error: + job_queue_destroy(jq); + return status; +} + +static pj_status_t job_queue_post_job(job_queue *jq, job_func_ptr func, + void *data, unsigned flags, + pj_status_t *retval) +{ + job jb; + int tail; + + if (jq->is_quitting) + return PJ_EBUSY; + + jb.func = func; + jb.data = data; + jb.flags = flags; + + pj_mutex_lock(jq->mutex); + jq->jobs[jq->tail] = &jb; + tail = jq->tail; + jq->tail = (jq->tail + 1) % jq->size; + + pj_sem_post(jq->sem); + /* Wait until our posted job is completed. */ + pj_sem_wait(jq->job_sem[tail]); + pj_mutex_unlock(jq->mutex); + + if (retval) *retval = jb.retval; + + return PJ_SUCCESS; +} + +static pj_status_t job_queue_destroy(job_queue *jq) +{ + unsigned i; + + jq->is_quitting = PJ_TRUE; + + if (jq->thread) { + pj_sem_post(jq->sem); + pj_thread_join(jq->thread); + pj_thread_destroy(jq->thread); + } + + if (jq->sem) { + pj_sem_destroy(jq->sem); + jq->sem = NULL; + } + for (i = 0; i < jq->size; i++) { + if (jq->job_sem[i]) { + pj_sem_destroy(jq->job_sem[i]); + jq->job_sem[i] = NULL; + } + } + + if (jq->mutex) { + pj_mutex_destroy(jq->mutex); + jq->mutex = NULL; + } + + return PJ_SUCCESS; +} + +#endif /* PJMEDIA_VIDEO_DEV_HAS_ANDROID_OPENGL */ -- cgit v1.2.3