Index: pjproject/branches/projects/2.0-dev/pjmedia/build/Makefile
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/build/Makefile	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/build/Makefile	(revision 3617)
@@ -58,5 +58,5 @@
 			delaybuf.o echo_common.o \
 			echo_port.o echo_suppress.o endpoint.o errno.o \
-			format.o ffmpeg_util.o \
+			event.o format.o ffmpeg_util.o \
 			g711.o jbuf.o master_port.o mem_capture.o mem_player.o \
 			null_port.o plc_common.o port.o splitcomb.o \
Index: pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia-videodev/videodev.h
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia-videodev/videodev.h	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia-videodev/videodev.h	(revision 3617)
@@ -26,4 +26,5 @@
 #include <pjmedia-videodev/config.h>
 #include <pjmedia-videodev/errno.h>
+#include <pjmedia/event.h>
 #include <pjmedia/frame.h>
 #include <pjmedia/format.h>
@@ -170,28 +171,4 @@
 /** Forward declaration for pjmedia_vid_dev_stream */
 typedef struct pjmedia_vid_dev_stream pjmedia_vid_dev_stream;
-
-typedef enum pjmedia_event_type
-{
-    PJMEDIA_EVENT_NONE,
-    PJMEDIA_EVENT_FMT_CHANGED,
-    PJMEDIA_EVENT_MOUSEBUTTONDOWN,
-    PJMEDIA_EVENT_WINDOW_RESIZE,
-    PJMEDIA_EVENT_WINDOW_FULLSCREEN,
-    PJMEDIA_EVENT_WINDOW_CLOSE,
-} pjmedia_event_type;
-
-typedef struct pjmedia_vid_event
-{
-    pjmedia_event_type event_type;
-    union {
-        struct resize_event {
-            pjmedia_rect_size new_size;
-        } resize;
-        struct fmt_changed_event {
-            pjmedia_format new_format;
-        } fmt_change;
-    } event_desc;
-} pjmedia_vid_event;
-
 
 typedef struct pjmedia_vid_cb
@@ -235,20 +212,4 @@
                              pjmedia_frame *frame);
 
-    /**
-    * This callback is called by the stream to report the occurence of an
-    * event to the application.
-    *
-    * @param stream	   The video stream.
-    * @param user_data     User data associated with the stream.
-    * @param event	   The event.
-    *
-    * @return              Return PJ_SUCCESS will invoke the video stream's
-    *                      default event-handler (if any), otherwise the
-    *                      video stream will ignore the particular event.
-    */
-    pj_status_t (*on_event_cb)(pjmedia_vid_dev_stream *stream,
-			       void *user_data,
-                               pjmedia_vid_event *event);
-
 } pjmedia_vid_cb;
 
@@ -598,4 +559,16 @@
 					    pjmedia_vid_dev_stream *strm);
 
+/**
+ * Get the event publisher object for the video stream. Caller typically use
+ * the returned object to subscribe or unsubscribe events from the video
+ * stream.
+ *
+ * @param strm      The video stream.
+ *
+ * @return          The event publisher object.
+ */
+PJ_DECL(pjmedia_event_publisher*)
+pjmedia_vid_dev_stream_get_event_publisher(pjmedia_vid_dev_stream *strm);
+
 /* Get/put frame API for passive stream */
 PJ_DECL(pj_status_t) pjmedia_vid_dev_stream_get_frame(
Index: pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia-videodev/videodev_imp.h
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia-videodev/videodev_imp.h	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia-videodev/videodev_imp.h	(revision 3617)
@@ -186,4 +186,7 @@
     /** Operations */
     pjmedia_vid_dev_stream_op *op;
+
+    /** Event producer */
+    pjmedia_event_publisher	epub;
 };
 
Index: pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia.h
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia.h	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia.h	(revision 3617)
@@ -38,4 +38,5 @@
 #include <pjmedia/endpoint.h>
 #include <pjmedia/errno.h>
+#include <pjmedia/event.h>
 #include <pjmedia/frame.h>
 #include <pjmedia/format.h>
Index: pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia/event.h
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia/event.h	(revision 3617)
+++ pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia/event.h	(revision 3617)
@@ -0,0 +1,380 @@
+/* $Id$ */
+/* 
+ * Copyright (C) 2011-2011 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 
+ */
+#ifndef __PJMEDIA_EVENT_H__
+#define __PJMEDIA_EVENT_H__
+
+/**
+ * @file pjmedia/event.h
+ * @brief Event framework
+ */
+#include <pjmedia/format.h>
+#include <pj/list.h>
+
+PJ_BEGIN_DECL
+
+/**
+ * @defgroup PJMEDIA_EVENT Event Framework
+ * @brief PJMEDIA event framework
+ * @{
+ */
+
+/**
+ * This enumeration describes list of media events.
+ */
+typedef enum pjmedia_event_type
+{
+    /**
+     * No event.
+     */
+    PJMEDIA_EVENT_NONE,
+
+    /**
+     * Media format has changed event.
+     */
+    PJMEDIA_EVENT_FMT_CHANGED,
+
+    /**
+     * Video window is being closed.
+     */
+    PJMEDIA_EVENT_WND_CLOSING,
+
+    /**
+     * Video window has been closed event.
+     */
+    PJMEDIA_EVENT_WND_CLOSED,
+
+    /**
+     * Video window has been resized event.
+     */
+    PJMEDIA_EVENT_WND_RESIZED,
+
+    /**
+     * Mouse button has been pressed event.
+     */
+    PJMEDIA_EVENT_MOUSE_BTN_DOWN,
+
+    /**
+     * Video key frame has just been decoded event.
+     */
+    PJMEDIA_EVENT_KEY_FRAME_FOUND,
+
+    /**
+     * Video decoding error due to missing key frame event.
+     */
+    PJMEDIA_EVENT_KEY_FRAME_MISSING,
+
+    /**
+     * Start of user event.
+     */
+    PJMEDIA_EVENT_USER = 100
+
+} pjmedia_event_type;
+
+/**
+ * Forward declaration for event subscription.
+ */
+typedef struct pjmedia_event_subscription pjmedia_event_subscription;
+
+/**
+ * Forward declaration for event publisher.
+ */
+typedef struct pjmedia_event_publisher pjmedia_event_publisher;
+
+/**
+ * Additional data/parameters for media format changed event
+ * (PJMEDIA_EVENT_FMT_CHANGED).
+ */
+typedef struct pjmedia_event_fmt_changed_data
+{
+    /**
+     * The new media format.
+     */
+    pjmedia_format	new_fmt;
+} pjmedia_event_fmt_changed_data;
+
+/**
+ * Additional data/parameters are not needed.
+ */
+typedef struct pjmedia_event_dummy_data
+{
+    /** Dummy data */
+    int			dummy;
+} pjmedia_event_dummy_data;
+
+/**
+ * Additional data/parameters for window resized event
+ * (PJMEDIA_EVENT_WND_RESIZED).
+ */
+typedef struct pjmedia_event_wnd_resized_data
+{
+    /**
+     * The new window size.
+     */
+    pjmedia_rect_size	new_size;
+} pjmedia_event_wnd_resized_data;
+
+/**
+ * Additional data/parameters for window closing event.
+ */
+typedef struct pjmedia_event_wnd_closing_data
+{
+    /** Consumer may set this field to PJ_TRUE to cancel the closing */
+    pj_bool_t		cancel;
+} pjmedia_event_wnd_closing_data;
+
+/** Additional parameters for window changed event. */
+typedef pjmedia_event_dummy_data pjmedia_event_wnd_closed_data;
+
+/** Additional parameters for mouse button down event */
+typedef pjmedia_event_dummy_data pjmedia_event_mouse_btn_down_data;
+
+/** Additional parameters for key frame found event */
+typedef pjmedia_event_dummy_data pjmedia_event_key_frame_found_data;
+
+/** Additional parameters for key frame missing event */
+typedef pjmedia_event_dummy_data pjmedia_event_key_frame_missing_data;
+
+/**
+ * Maximum size of additional parameters section in pjmedia_event structure
+ */
+#define PJMEDIA_EVENT_DATA_MAX_SIZE	sizeof(pjmedia_event_fmt_changed_data)
+
+/** Type of storage to hold user data in pjmedia_event structure */
+typedef char pjmedia_event_user_data[PJMEDIA_EVENT_DATA_MAX_SIZE];
+
+/**
+ * This structure describes a media event. It consists mainly of the event
+ * type and additional data/parameters for the event. Event publishers need
+ * to use #pjmedia_event_init() to initialize this event structure with
+ * basic information about the event.
+ */
+typedef struct pjmedia_event
+{
+    /**
+     * The event type.
+     */
+    pjmedia_event_type			 type;
+
+    /**
+     * The media timestamp when the event occurs.
+     */
+    pj_timestamp		 	 timestamp;
+
+    /**
+     * This keeps count on the number of subscribers that have
+     * processed this event.
+     */
+    unsigned				 proc_cnt;
+
+    /**
+     * Pointer information about the source of this event. This field
+     * is provided mainly so that the event subscribers can compare it
+     * against the publisher that it subscribed the events from initially,
+     * a publisher can republish events from other publisher. Event
+     * subscription must be careful when using this pointer other than for
+     * comparison purpose, since access to the publisher may require special
+     * care (e.g. mutex locking).
+     */
+    const pjmedia_event_publisher	*epub;
+
+    /**
+     * Additional data/parameters about the event. The type of data
+     * will be specific to the event type being reported.
+     */
+    union {
+	/** Media format changed event data. */
+	pjmedia_event_fmt_changed_data		fmt_changed;
+
+	/** Window resized event data */
+	pjmedia_event_wnd_resized_data		wnd_resized;
+
+	/** Window closing event data. */
+	pjmedia_event_wnd_closing_data		wnd_closing;
+
+	/** Window closed event data */
+	pjmedia_event_wnd_closed_data		wnd_closed;
+
+	/** Mouse button down event data */
+	pjmedia_event_mouse_btn_down_data	mouse_btn_down;
+
+	/** Key frame found event data */
+	pjmedia_event_key_frame_found_data	key_frm_found;
+
+	/** Key frame missing event data */
+	pjmedia_event_key_frame_missing_data	key_frm_missing;
+
+	/** Storage for user event data */
+	pjmedia_event_user_data			user;
+
+	/** Pointer to storage to user event data, if it's outside
+	 * this struct
+	 */
+	void					*ptr;
+    } data;
+} pjmedia_event;
+
+/**
+ * The callback to receive media events. The callback should increase
+ * \a proc_cnt field of the event if it processes the event.
+ *
+ * @param esub		The subscription that was made initially to receive
+ * 			this event.
+ * @param event		The media event itself.
+ *
+ * @return		If the callback returns non-PJ_SUCCESS, this return
+ * 			code may be propagated back to the producer.
+ */
+typedef pj_status_t pjmedia_event_cb(pjmedia_event_subscription *esub,
+				     pjmedia_event *event);
+
+/**
+ * This structure keeps the data needed to maintain an event subscription.
+ * This data is normally kept by event publishers.
+ */
+struct pjmedia_event_subscription
+{
+    /** Standard list members */
+    PJ_DECL_LIST_MEMBER(pjmedia_event_subscription);
+
+    /** Callback that will be called by publisher to report events. */
+    pjmedia_event_cb	*cb;
+
+    /** User data for this subscription */
+    void		*user_data;
+};
+
+/**
+ * This describes an event publisher. An event publisher is an object that
+ * maintains event subscriptions. When an event is published on behalf of
+ * a publisher with #pjmedia_event_publish(), that event will be propagated
+ * to all of the subscribers registered to the publisher.
+ */
+struct pjmedia_event_publisher
+{
+    /** List of subscriptions for this event publisher */
+    pjmedia_event_subscription	subscription_list;
+};
+
+/**
+ * Initialize event structure with basic data about the event.
+ *
+ * @param event		The event to be initialized.
+ * @param type		The event type to be set for this event.
+ * @param ts		Event timestamp. May be set to NULL to set the event
+ * 			timestamp to zero.
+ * @param epub		Event publisher.
+ */
+PJ_DECL(void) pjmedia_event_init(pjmedia_event *event,
+                                 pjmedia_event_type type,
+                                 const pj_timestamp *ts,
+                                 const pjmedia_event_publisher *epub);
+
+/**
+ * Initialize an event publisher structure.
+ *
+ * @param epub		The event publisher.
+ */
+PJ_DECL(void) pjmedia_event_publisher_init(pjmedia_event_publisher *epub);
+
+/**
+ * Initialize subscription data.
+ *
+ * @param esub		The event subscription.
+ * @param cb		The callback to receive events.
+ * @param user_data	Arbitrary user data to be associated with the
+ * 			subscription.
+ */
+PJ_DECL(void) pjmedia_event_subscription_init(pjmedia_event_subscription *esub,
+                                              pjmedia_event_cb *cb,
+                                              void *user_data);
+
+/**
+ * Subscribe to events published by the specified publisher using the
+ * specified subscription object. The callback and user data fields of
+ * the subscription object must have been initialized prior to calling
+ * this function, and the subscription object must be kept alive throughout
+ * the duration of the subscription (e.g. it must not be allocated from
+ * the stack).
+ *
+ * Note that the subscriber may receive not only events emitted by
+ * the specific publisher specified in the argument, but also from other
+ * publishers contained by the publisher, if the publisher is republishing
+ * events from other publishers.
+ *
+ * @param epub		The event publisher.
+ * @param esub 		The event subscription object.
+ *
+ * @return		PJ_SUCCESS on success or the appropriate error code.
+ */
+PJ_DECL(pj_status_t) pjmedia_event_subscribe(pjmedia_event_publisher *epub,
+                                             pjmedia_event_subscription *esub);
+
+/**
+ * Unsubscribe from the specified publisher.
+ *
+ * @param epub		The event publisher.
+ * @param esub		The event subscription object, which must be the same
+ * 			object that was given to #pjmedia_event_subscribe().
+ *
+ * @return		PJ_SUCCESS on success or the appropriate error code.
+ */
+PJ_DECL(pj_status_t) pjmedia_event_unsubscribe(pjmedia_event_publisher *epub,
+                                               pjmedia_event_subscription *esub);
+
+/**
+ * Publish the specified event to all subscribers of the specified event
+ * publisher.
+ *
+ * @param epub		The event publisher.
+ * @param event		The event to be published.
+ *
+ * @return		PJ_SUCCESS only if all subscription callbacks returned
+ * 			PJ_SUCCESS.
+ */
+PJ_DECL(pj_status_t) pjmedia_event_publish(pjmedia_event_publisher *epub,
+                                           pjmedia_event *event);
+
+/**
+ * Subscribe to events produced by the source publisher in \a esrc and
+ * republish the events to all subscribers in \a epub publisher.
+ *
+ * @param esrc		The event source from which events will be
+ * 			republished.
+ * @param epub		Events from the event source above will be
+ * 			republished to subscribers of this publisher.
+ * @param esub		The subscription object to be used to subscribe
+ * 			to \a esrc. This doesn't need to be initialized,
+ * 			but it must be kept alive throughout the lifetime
+ * 			of the subsciption.
+ *
+ * @return		PJ_SUCCESS only if all subscription callbacks returned
+ * 			PJ_SUCCESS.
+ */
+PJ_DECL(pj_status_t) pjmedia_event_republish(pjmedia_event_publisher *esrc,
+                                             pjmedia_event_publisher *epub,
+                                             pjmedia_event_subscription *esub);
+
+/**
+ * @}  PJMEDIA_EVENT
+ */
+
+
+PJ_END_DECL
+
+#endif	/* __PJMEDIA_EVENT_H__ */
Index: pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia/vid_port.h
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia/vid_port.h	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/include/pjmedia/vid_port.h	(revision 3617)
@@ -106,4 +106,14 @@
 
 /**
+ * Get the event publisher instance of the video port.
+ *
+ * @param vid_port	The video port.
+ *
+ * @return		The event publisher of the video port.
+ */
+PJ_DECL(pjmedia_event_publisher*)
+pjmedia_vid_port_get_event_publisher(pjmedia_vid_port *vid_port);
+
+/**
  * Return the underlying video stream of the video port.
  *
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/colorbar_dev.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/colorbar_dev.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/colorbar_dev.c	(revision 3617)
@@ -417,4 +417,5 @@
     pj_memcpy(&strm->vafp, &vafp, sizeof(vafp));
     strm->ts_inc = PJMEDIA_SPF2(param->clock_rate, &vfd->fps, 1);
+    pjmedia_event_publisher_init(&strm->base.epub);
 
     for (i = 0; i < vfi->plane_cnt; ++i) {
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/dshow_dev.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/dshow_dev.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/dshow_dev.c	(revision 3617)
@@ -779,4 +779,5 @@
     pj_memcpy(&strm->vid_cb, cb, sizeof(*cb));
     strm->user_data = user_data;
+    pjmedia_event_publisher_init(&strm->base.epub);
 
     if (param->dir & PJMEDIA_DIR_CAPTURE) {
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/ffmpeg_dev.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/ffmpeg_dev.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/ffmpeg_dev.c	(revision 3617)
@@ -385,4 +385,5 @@
     strm->pool = pool;
     pj_memcpy(&strm->param, param, sizeof(*param));
+    pjmedia_event_publisher_init(&strm->base.epub);
 
     /* Done */
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/ios_dev.m
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/ios_dev.m	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/ios_dev.m	(revision 3617)
@@ -427,4 +427,5 @@
     pj_memcpy(&strm->vid_cb, cb, sizeof(*cb));
     strm->user_data = user_data;
+    pjmedia_event_publisher_init(&strm->base.epub);
 
     vfd = pjmedia_format_get_video_format_detail(&strm->param.fmt, PJ_TRUE);
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/qt_dev.m
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/qt_dev.m	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/qt_dev.m	(revision 3617)
@@ -420,4 +420,5 @@
     strm->user_data = user_data;
     strm->apool = [[NSAutoreleasePool alloc]init];
+    pjmedia_event_publisher_init(&strm->base.epub);
 
     /* Create capture stream here */
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/sdl_dev.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/sdl_dev.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/sdl_dev.c	(revision 3617)
@@ -148,4 +148,5 @@
     pjmedia_format              *new_fmt;
     pjmedia_rect_size           *new_disp_size;
+    pj_timestamp		 last_ts;
 
 #if SDL_VERSION_ATLEAST(1,3,0)
@@ -684,4 +685,6 @@
 {
     struct sdl_stream *strm = (struct sdl_stream*)data;
+    pj_bool_t notify_wnd_closed_event = PJ_FALSE;
+    pj_status_t saved_stream_status;
 #endif
 
@@ -734,5 +737,5 @@
     {
         SDL_Event sevent;
-        pjmedia_vid_event pevent;
+        pjmedia_event pevent;
 
 #if PJMEDIA_VIDEO_DEV_SDL_HAS_OPENGL
@@ -752,9 +755,10 @@
          */
         while (SDL_PollEvent(&sevent)) {
-            pj_bzero(&pevent, sizeof(pevent));
+            pjmedia_event_init(&pevent, PJMEDIA_EVENT_NONE, &strm->last_ts,
+                               &strm->base.epub);
 
             switch(sevent.type) {
                 case SDL_MOUSEBUTTONDOWN:
-                    pevent.event_type = PJMEDIA_EVENT_MOUSEBUTTONDOWN;
+                    pevent.type = PJMEDIA_EVENT_MOUSE_BTN_DOWN;
                     break;
 #if SDL_VERSION_ATLEAST(1,3,0)
@@ -762,8 +766,8 @@
                     switch (sevent.window.event) {
                         case SDL_WINDOWEVENT_RESIZED:
-                            pevent.event_type = PJMEDIA_EVENT_WINDOW_RESIZE;
-                            pevent.event_desc.resize.new_size.w =
+                            pevent.type = PJMEDIA_EVENT_WND_RESIZED;
+                            pevent.data.wnd_resized.new_size.w =
                                 sevent.window.data1;
-                            pevent.event_desc.resize.new_size.h =
+                            pevent.data.wnd_resized.new_size.h =
                                 sevent.window.data2;
                             break;
@@ -772,78 +776,44 @@
 #else
                 case SDL_VIDEORESIZE:
-                    pevent.event_type = PJMEDIA_EVENT_WINDOW_RESIZE;
-                    pevent.event_desc.resize.new_size.w = sevent.resize.w;
-                    pevent.event_desc.resize.new_size.h = sevent.resize.h;
+                    pevent.type = PJMEDIA_EVENT_WND_RESIZED;
+                    pevent.data.wnd_resized.new_size.w = sevent.resize.w;
+                    pevent.data.wnd_resized.new_size.h = sevent.resize.h;
                     break;
                 case SDL_QUIT:
-                    pevent.event_type = PJMEDIA_EVENT_WINDOW_CLOSE;
+                    pevent.type = PJMEDIA_EVENT_WND_CLOSING;
+                    break;
 #endif
             }
 
-            switch (pevent.event_type) {
-                case PJMEDIA_EVENT_MOUSEBUTTONDOWN:
-                    if (strm->vid_cb.on_event_cb)
-                        if ((*strm->vid_cb.on_event_cb)(&strm->base,
-							strm->user_data,
-							&pevent) != PJ_SUCCESS)
-                        {
-                            /* Application wants us to ignore this event */
-                            break;
-                        }
-                    break;
-
-                case PJMEDIA_EVENT_WINDOW_RESIZE:
-                    if (strm->vid_cb.on_event_cb) {
-                        if ((*strm->vid_cb.on_event_cb)(&strm->base,
-                                                        strm->user_data,
-                                                        &pevent) != PJ_SUCCESS)
-                        {
-                            break;
-                        }
-                    }
-                    strm->new_disp_size = &pevent.event_desc.resize.new_size;
+            if (pevent.type != PJMEDIA_EVENT_NONE) {
+        	pj_status_t status;
+
+        	status = pjmedia_event_publish(&strm->base.epub, &pevent);
+
+		switch (pevent.type) {
+		case PJMEDIA_EVENT_WND_RESIZED:
+                    strm->new_disp_size = &pevent.data.wnd_resized.new_size;
                     detect_fmt_change(strm);
-                    break;
-
-                case PJMEDIA_EVENT_WINDOW_CLOSE:
-                    /**
-                     * To process PJMEDIA_EVENT_WINDOW_CLOSE event,
-                     * application should do this in the on_event_cb callback:
-                     * 1. stop further calls to 
-		     *    #pjmedia_vid_dev_stream_put_frame()
-                     * 2. return PJ_SUCCESS
-                     * Upon returning from the callback, SDL will destroy its
-                     * own stream.
-                     *
-                     * Returning non-PJ_SUCCESS will cause SDL to ignore
-                     * the event
-                     */
-                    if (strm->vid_cb.on_event_cb) {
-                        strm->is_quitting = PJ_TRUE;
-                        if ((*strm->vid_cb.on_event_cb)(&strm->base,
-							strm->user_data,
-							&pevent) != PJ_SUCCESS)
-                        {
-                            /* Application wants us to ignore this event */
-                            strm->is_quitting = PJ_FALSE;
-                            break;
-                        }
-
-                        /* Destroy the stream */
-                        sdl_stream_destroy(&strm->base);
-                        goto on_return;
-                    }
-
-                    /**
-                     * Default event-handler when there is no user-specified
-                     * callback: close the renderer window. We cannot destroy
-                     * the stream here since there is no callback to notify
-                     * the application.
-                     */
-                    sdl_stream_stop(&strm->base);
-                    goto on_return;
-
-                default:
-                    break;
+		    break;
+
+		case PJMEDIA_EVENT_WND_CLOSING:
+		    if (pevent.data.wnd_closing.cancel) {
+			/* Cancel the closing operation */
+			break;
+		    }
+
+		    /* Proceed to cleanup SDL. App must still call
+		     * pjmedia_dev_stream_destroy() when getting WND_CLOSED
+		     * event
+		     */
+		    strm->is_quitting = PJ_TRUE;
+		    notify_wnd_closed_event = PJ_TRUE;
+		    sdl_stream_stop(&strm->base);
+		    goto on_return;
+
+		default:
+		    /* Just to prevent gcc warning about unused enums */
+		    break;
+		}
             }
 	}
@@ -859,6 +829,24 @@
 #endif
     strm->screen = NULL;
-
-    return strm->status;
+    saved_stream_status = strm->status;
+
+    if (notify_wnd_closed_event) {
+	pjmedia_event pevent;
+
+	pjmedia_event_init(&pevent, PJMEDIA_EVENT_WND_CLOSED, &strm->last_ts,
+	                   &strm->base.epub);
+	pjmedia_event_publish(&strm->base.epub, &pevent);
+    }
+
+    /*
+     * Note: don't access the stream after this point, it  might have
+     * been destroyed
+     */
+
+    return saved_stream_status;
+}
+
+static void hello_world()
+{
 }
 
@@ -901,4 +889,6 @@
     struct sdl_stream *stream = (struct sdl_stream*)strm;
     pj_status_t status = PJ_SUCCESS;
+
+    stream->last_ts.u64 = frame->timestamp.u64;
 
     if (!stream->is_running) {
@@ -1024,4 +1014,5 @@
     pj_memcpy(&strm->vid_cb, cb, sizeof(*cb));
     strm->user_data = user_data;
+    pjmedia_event_publisher_init(&strm->base.epub);
 
     /* Create render stream here */
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/v4l2_dev.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/v4l2_dev.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/v4l2_dev.c	(revision 3617)
@@ -574,4 +574,5 @@
     stream->user_data = user_data;
     stream->fd = INVALID_FD;
+    pjmedia_event_publisher_init(&stream->base.epub);
 
     stream->fd = v4l2_open(vdi->dev_name, O_RDWR | O_NONBLOCK, 0);
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/videodev.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/videodev.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia-videodev/videodev.c	(revision 3617)
@@ -751,4 +751,10 @@
 }
 
+PJ_DEF(pjmedia_event_publisher*)
+pjmedia_vid_dev_stream_get_event_publisher(pjmedia_vid_dev_stream *strm)
+{
+    return &strm->epub;
+}
+
 /* API: Start the stream. */
 PJ_DEF(pj_status_t) pjmedia_vid_dev_stream_start(pjmedia_vid_dev_stream *strm)
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia/event.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia/event.c	(revision 3617)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia/event.c	(revision 3617)
@@ -0,0 +1,123 @@
+/* $Id$ */
+/* 
+ * Copyright (C) 2011-2011 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 <pjmedia/event.h>
+#include <pjmedia/errno.h>
+#include <pj/assert.h>
+#include <pj/log.h>
+#include <pj/string.h>
+
+#define THIS_FILE	"event.c"
+
+#if 1
+#   define TRACE_(x)	PJ_LOG(4,x)
+#else
+#   define TRACE_(x)
+#endif
+
+PJ_DEF(void) pjmedia_event_init( pjmedia_event *event,
+                                 pjmedia_event_type type,
+                                 const pj_timestamp *ts,
+                                 const pjmedia_event_publisher *epub)
+{
+    pj_bzero(event, sizeof(*event));
+    event->type = type;
+    if (ts)
+	event->timestamp.u64 = ts->u64;
+    event->epub = epub;
+}
+
+PJ_DEF(void) pjmedia_event_publisher_init(pjmedia_event_publisher *epub)
+{
+    pj_bzero(epub, sizeof(*epub));
+    pj_list_init(&epub->subscription_list);
+}
+
+PJ_DEF(void) pjmedia_event_subscription_init( pjmedia_event_subscription *esub,
+                                              pjmedia_event_cb *cb,
+                                              void *user_data)
+{
+    pj_bzero(esub, sizeof(*esub));
+    esub->cb = cb;
+    esub->user_data = user_data;
+}
+
+PJ_DEF(pj_status_t) pjmedia_event_subscribe( pjmedia_event_publisher *epub,
+                                             pjmedia_event_subscription *esub)
+{
+    PJ_ASSERT_RETURN(epub && esub && esub->cb, PJ_EINVAL);
+    pj_list_push_back(&epub->subscription_list, esub);
+    return PJ_SUCCESS;
+}
+
+PJ_DEF(pj_status_t) pjmedia_event_unsubscribe( pjmedia_event_publisher *epub,
+                                               pjmedia_event_subscription *esub)
+{
+    PJ_ASSERT_RETURN(epub && esub, PJ_EINVAL);
+    PJ_ASSERT_RETURN(pj_list_find_node(&epub->subscription_list, esub)==esub,
+                     PJ_ENOTFOUND);
+    pj_list_erase(esub);
+    return PJ_SUCCESS;
+}
+
+PJ_DEF(pj_status_t) pjmedia_event_publish( pjmedia_event_publisher *epub,
+                                           pjmedia_event *event)
+{
+    pjmedia_event_subscription *esub;
+    pj_status_t err = PJ_SUCCESS;
+
+    PJ_ASSERT_RETURN(epub && event, PJ_EINVAL);
+
+    TRACE_((THIS_FILE, "Event %d is published by publisher %x",
+		       event->type, epub));
+
+    esub = epub->subscription_list.next;
+    while (esub != &epub->subscription_list) {
+	pjmedia_event_subscription *next;
+	pj_status_t status;
+
+	/* just in case esub is destroyed in the callback */
+	next = esub->next;
+
+	status = (*esub->cb)(esub, event);
+	if (status != PJ_SUCCESS && err == PJ_SUCCESS)
+	    err = status;
+
+	esub = next;
+    }
+
+    return err;
+}
+
+static pj_status_t republisher_cb(pjmedia_event_subscription *esub,
+       				  pjmedia_event *event)
+{
+    return pjmedia_event_publish((pjmedia_event_publisher*)esub->user_data,
+                                 event);
+}
+
+PJ_DEF(pj_status_t) pjmedia_event_republish(pjmedia_event_publisher *esrc,
+                                             pjmedia_event_publisher *epub,
+                                             pjmedia_event_subscription *esub)
+{
+    PJ_ASSERT_RETURN(esrc && epub && esub, PJ_EINVAL);
+
+    pjmedia_event_subscription_init(esub, &republisher_cb, epub);
+    return pjmedia_event_subscribe(esrc, esub);
+}
+
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia/vid_port.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia/vid_port.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/pjmedia/vid_port.c	(revision 3617)
@@ -21,4 +21,5 @@
 #include <pjmedia/converter.h>
 #include <pjmedia/errno.h>
+#include <pjmedia/event.h>
 #include <pjmedia/vid_codec.h>
 #include <pj/log.h>
@@ -56,6 +57,8 @@
     pjmedia_conversion_param conv_param;
 
+    pjmedia_event_publisher  epub;
+    pjmedia_event_subscription esub_dev;
+
     pjmedia_clock           *clock;
-
     pjmedia_clock_src        clocksrc;
 
@@ -86,7 +89,6 @@
 				       void *user_data,
 				       pjmedia_frame *frame);
-static pj_status_t vidstream_event_cb(pjmedia_vid_dev_stream *stream,
-			              void *user_data,
-                                      pjmedia_vid_event *event);
+static pj_status_t vidstream_event_cb(pjmedia_event_subscription *esub,
+			              pjmedia_event *event);
 
 static void enc_clock_cb(const pj_timestamp *ts, void *user_data);
@@ -152,4 +154,5 @@
     vp->dir = prm->vidparam.dir;
 //    vp->cap_size = vfd->size;
+    pjmedia_event_publisher_init(&vp->epub);
 
     vparam = prm->vidparam;
@@ -203,5 +206,4 @@
     vid_cb.capture_cb = &vidstream_cap_cb;
     vid_cb.render_cb = &vidstream_render_cb;
-    vid_cb.on_event_cb = &vidstream_event_cb;
 
     status = pjmedia_vid_dev_stream_create(&vparam, &vid_cb, vp,
@@ -216,4 +218,10 @@
 	      vparam.fmt.det.vid.size.w, vparam.fmt.det.vid.size.h,
 	      vparam.fmt.det.vid.fps.num, vparam.fmt.det.vid.fps.denum));
+
+    /* Subscribe to device's events */
+    pjmedia_event_subscription_init(&vp->esub_dev, vidstream_event_cb, vp);
+    pjmedia_event_subscribe(
+	    pjmedia_vid_dev_stream_get_event_publisher(vp->strm),
+	    &vp->esub_dev);
 
     if (vp->dir & PJMEDIA_DIR_CAPTURE) {
@@ -339,4 +347,11 @@
 }
 
+PJ_DEF(pjmedia_event_publisher*)
+pjmedia_vid_port_get_event_publisher(pjmedia_vid_port *vid_port)
+{
+    PJ_ASSERT_RETURN(vid_port, NULL);
+    return &vid_port->epub;
+}
+
 PJ_DEF(pjmedia_vid_dev_stream*)
 pjmedia_vid_port_get_stream(pjmedia_vid_port *vp)
@@ -498,13 +513,11 @@
 */
 
-static pj_status_t vidstream_event_cb(pjmedia_vid_dev_stream *stream,
-				      void *user_data,
-				      pjmedia_vid_event *event)
-{
-    pjmedia_vid_port *vp = (pjmedia_vid_port*)user_data;
+static pj_status_t vidstream_event_cb(pjmedia_event_subscription *esub,
+				      pjmedia_event *event)
+{
+    pjmedia_vid_port *vp = (pjmedia_vid_port*)esub->user_data;
     
-    if (vp->strm_cb.on_event_cb)
-        return (*vp->strm_cb.on_event_cb)(stream, vp->strm_cb_data, event);
-    return PJ_SUCCESS;
+    /* Just republish the event to our client */
+    return pjmedia_event_publish(&vp->epub, event);
 }
 
@@ -530,5 +543,5 @@
     if (frame->bit_info & PJMEDIA_VID_CODEC_EVENT_FMT_CHANGED) {
         const pjmedia_video_format_detail *vfd;
-        pjmedia_vid_event pevent;
+        pjmedia_event pevent;
         pj_status_t status;
 
@@ -593,9 +606,8 @@
 
         /* Notify application of the format change. */
-        pevent.event_type = PJMEDIA_EVENT_FMT_CHANGED;
-	pjmedia_format_copy(&pevent.event_desc.fmt_change.new_format,
+        pjmedia_event_init(&pevent, PJMEDIA_EVENT_FMT_CHANGED, NULL, &vp->epub);
+	pjmedia_format_copy(&pevent.data.fmt_changed.new_fmt,
 			    &vp->client_port->info.fmt);
-        if (vp->strm_cb.on_event_cb)
-            (*vp->strm_cb.on_event_cb)(vp->strm, vp->strm_cb_data, &pevent);
+	pjmedia_event_publish(&vp->epub, &pevent);
 
 	pjmedia_vid_port_start(vp);
Index: pjproject/branches/projects/2.0-dev/pjmedia/src/test/vid_dev_test.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjmedia/src/test/vid_dev_test.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjmedia/src/test/vid_dev_test.c	(revision 3617)
@@ -73,12 +73,8 @@
 }
 
-static pj_status_t vid_event_cb(pjmedia_vid_dev_stream *stream,
-				void *user_data,
-				pjmedia_vid_event *event)
-{
-    PJ_UNUSED_ARG(stream);
-    PJ_UNUSED_ARG(user_data);
-    
-    if (event->event_type == PJMEDIA_EVENT_WINDOW_CLOSE)
+static pj_status_t vid_event_cb(pjmedia_event_subscription *esub,
+				pjmedia_event *event)
+{
+    if (event->type == PJMEDIA_EVENT_WND_CLOSED)
         is_quitting = PJ_TRUE;
 
@@ -95,4 +91,5 @@
     pjmedia_video_format_detail *vfd;
     pjmedia_vid_cb cb;
+    pjmedia_event_subscription esub;
     pj_status_t status;
     int rc = 0, i;
@@ -160,7 +157,8 @@
 
     /* Set event handler */
-    pj_bzero(&cb, sizeof(cb));
-    cb.on_event_cb = vid_event_cb;
-    pjmedia_vid_port_set_cb(renderer, &cb, NULL);
+    pjmedia_event_subscription_init(&esub, &vid_event_cb, NULL);
+    pjmedia_event_subscribe(
+	    pjmedia_vid_port_get_event_publisher(renderer),
+	    &esub);
 
     /* Connect capture to renderer */
Index: pjproject/branches/projects/2.0-dev/pjsip-apps/src/samples/aviplay.c
===================================================================
--- pjproject/branches/projects/2.0-dev/pjsip-apps/src/samples/aviplay.c	(revision 3616)
+++ pjproject/branches/projects/2.0-dev/pjsip-apps/src/samples/aviplay.c	(revision 3617)
@@ -104,17 +104,14 @@
 } codec_port_data_t;
 
-static pj_status_t avi_event_cb(pjmedia_vid_dev_stream *stream,
-				void *user_data,
-				pjmedia_vid_event *event)
+static pj_status_t avi_event_cb(pjmedia_event_subscription *esub,
+				pjmedia_event *event)
 {
-    avi_port_t *ap = (avi_port_t *)user_data;
-    
-    PJ_UNUSED_ARG(stream);
-    
-    switch (event->event_type) {
-    case PJMEDIA_EVENT_WINDOW_CLOSE:
+    avi_port_t *ap = (avi_port_t *)esub->user_data;
+    
+    switch (event->type) {
+    case PJMEDIA_EVENT_WND_CLOSED:
         ap->is_quitting = PJ_TRUE;
         break;
-    case PJMEDIA_EVENT_MOUSEBUTTONDOWN:
+    case PJMEDIA_EVENT_MOUSE_BTN_DOWN:
         if (ap->is_running) {
             pjmedia_vid_port_stop(ap->vid_port);
@@ -191,4 +188,5 @@
     pjmedia_port *vid_port = NULL, *aud_port = NULL;
     pjmedia_vid_codec *codec=NULL;
+    pjmedia_event_subscription esub;
     avi_port_t avi_port;
     
@@ -395,9 +393,14 @@
 	
         pj_bzero(&cb, sizeof(cb));
-        cb.on_event_cb = avi_event_cb;
         avi_port.snd_port = snd_port;
         avi_port.vid_port = renderer;
         avi_port.is_running = PJ_TRUE;
         pjmedia_vid_port_set_cb(renderer, &cb, &avi_port);
+
+        /* subscribe events */
+        pjmedia_event_subscription_init(&esub, &avi_event_cb, &avi_port);
+        pjmedia_event_subscribe(
+        	pjmedia_vid_port_get_event_publisher(renderer),
+        	&esub);
 
         if (snd_port) {
