Re #2181: Initial version of video conference implementation.

git-svn-id: https://svn.pjsip.org/repos/pjproject/trunk@5939 74dad513-b988-da41-8d7b-12977e46ad98
This commit is contained in:
Nanang Izzuddin 2019-03-05 06:23:02 +00:00
parent 6b9212dcb4
commit eafc04d5bf
20 changed files with 2452 additions and 96 deletions

View File

@ -73,7 +73,7 @@ export PJMEDIA_OBJS += $(OS_OBJS) $(M_OBJS) $(CC_OBJS) $(HOST_OBJS) \
stream.o stream_info.o tonegen.o transport_adapter_sample.o \
transport_ice.o transport_loop.o transport_srtp.o transport_udp.o \
types.o vid_codec.o vid_codec_util.o \
vid_port.o vid_stream.o vid_stream_info.o vid_tee.o \
vid_port.o vid_stream.o vid_stream_info.o vid_conf.o \
wav_player.o wav_playlist.o wav_writer.o wave.o \
wsola.o audiodev.o videodev.o

View File

@ -7006,6 +7006,10 @@
RelativePath="..\src\pjmedia\vid_codec_util.c"
>
</File>
<File
RelativePath="..\src\pjmedia\vid_conf.c"
>
</File>
<File
RelativePath="..\src\pjmedia\vid_port.c"
>
@ -7695,6 +7699,10 @@
RelativePath="..\include\pjmedia\vid_codec_util.h"
>
</File>
<File
RelativePath="..\include\pjmedia\vid_conf.h"
>
</File>
<File
RelativePath="..\include\pjmedia\vid_port.h"
>

View File

@ -68,10 +68,11 @@
#include <pjmedia/transport_loop.h>
#include <pjmedia/transport_srtp.h>
#include <pjmedia/transport_udp.h>
#include <pjmedia/vid_port.h>
#include <pjmedia/vid_codec.h>
#include <pjmedia/vid_conf.h>
#include <pjmedia/vid_port.h>
#include <pjmedia/vid_stream.h>
#include <pjmedia/vid_tee.h>
//#include <pjmedia/vid_tee.h>
#include <pjmedia/wav_playlist.h>
#include <pjmedia/wav_port.h>
#include <pjmedia/wave.h>

View File

@ -122,6 +122,12 @@ typedef struct pjmedia_converter
} pjmedia_converter;
/**
* Settings for pjmedia_converter_convert2().
*/
typedef void pjmedia_converter_convert_setting;
/**
* Converter factory operation.
*/
@ -157,19 +163,19 @@ struct pjmedia_converter_factory_op
struct pjmedia_converter_op
{
/**
* Convert the buffer in the source frame and save the result in the
* Convert the buffer of the source frame and save the result in the
* buffer of the destination frame, according to conversion format that
* was specified when the converter was created.
*
* Note that application should use #pjmedia_converter_convert() instead
* of calling this function directly.
*
* @param cv The converter instance.
* @param src_frame The source frame.
* @param dst_frame The destination frame.
* @param cv The converter instance.
* @param src_frame The source frame.
* @param dst_frame The destination frame.
*
* @return PJ_SUCCESS if conversion has been performed
* successfully.
* @return PJ_SUCCESS if conversion has been performed
* successfully.
*/
pj_status_t (*convert)(pjmedia_converter *cv,
pjmedia_frame *src_frame,
@ -181,10 +187,40 @@ struct pjmedia_converter_op
* Note that application should use #pjmedia_converter_destroy() instead
* of calling this function directly.
*
* @param cv The converter.
* @param cv The converter.
*/
void (*destroy)(pjmedia_converter *cv);
/**
* Convert a region in the buffer of the source frame and put the result
* into a region in the buffer of the destination frame, according to
* conversion format that was specified when the converter was created.
*
* Note that application should use #pjmedia_converter_convert2() instead
* of calling this function directly.
*
* @param cv The converter instance.
* @param src_frame The source frame.
* @param src_frame_size The source frame size.
* @param src_reg_pos The source region position.
* @param dst_frame The destination frame.
* @param dst_frame_size The destination frame size.
* @param dst_reg_pos The destination region position.
* @param param This is unused for now and must be NULL.
*
* @return PJ_SUCCESS if conversion has been performed
* successfully.
*/
pj_status_t (*convert2)(pjmedia_converter *cv,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
pjmedia_converter_convert_setting
*param);
};
@ -302,6 +338,35 @@ PJ_DECL(pj_status_t) pjmedia_converter_convert(pjmedia_converter *cv,
pjmedia_frame *src_frame,
pjmedia_frame *dst_frame);
/**
* Convert a region in the buffer of the source frame and put the result
* into a region in the buffer of the destination frame, according to
* conversion format that was specified when the converter was created.
*
* @param cv The converter instance.
* @param src_frame The source frame.
* @param src_frame_size The source frame size.
* @param src_reg_pos The source region position.
* @param dst_frame The destination frame.
* @param dst_frame_size The destination frame size.
* @param dst_reg_pos The destination region position.
* @param param This is unused for now and must be NULL.
*
* @return PJ_SUCCESS if conversion has been performed
* successfully.
*/
PJ_DECL(pj_status_t) pjmedia_converter_convert2(
pjmedia_converter *cv,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
pjmedia_converter_convert_setting
*param);
/**
* Destroy the converter.
*

View File

@ -197,6 +197,7 @@ PJ_INLINE(const char*) pjmedia_sig_name(pjmedia_obj_sig sig, char buf[])
#define PJMEDIA_SIG_CLASS_VID_OTHER(c,d) PJMEDIA_SIGNATURE('V','O',c,d)
#define PJMEDIA_SIG_IS_CLASS_VID_OTHER(s) ((s)>>24=='V' && (s)>>16=='O')
#define PJMEDIA_SIG_VID_CONF PJMEDIA_SIG_CLASS_VID_OTHER('C','F')
#define PJMEDIA_SIG_VID_PORT PJMEDIA_SIG_CLASS_VID_OTHER('P','O')

View File

@ -0,0 +1,294 @@
/* $Id$ */
/*
* Copyright (C) 2019 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_VID_CONF_H__
#define __PJMEDIA_VID_CONF_H__
/**
* @file vid_conf.h
* @brief Video conference bridge.
*/
#include <pjmedia/port.h>
/**
* @addtogroup PJMEDIA_VID_CONF Video conference bridge
* @ingroup PJMEDIA_PORT
* @brief Video conference bridge implementation
* destination
* @{
*
* This describes the video conference bridge implementation in PJMEDIA. The
* conference bridge provides powerful and efficient mechanism to route the
* video flow and combine multiple video data from multiple video sources.
*/
PJ_BEGIN_DECL
/**
* Opaque type for video conference bridge.
*/
typedef struct pjmedia_vid_conf pjmedia_vid_conf;
/**
* Enumeration of video conference layout mode.
*/
typedef enum pjmedia_vid_conf_layout
{
/**
* In mixing video from multiple sources, each source will occupy about
* the same size in the mixing result frame at all time.
*/
PJMEDIA_VID_CONF_LAYOUT_DEFAULT,
/**
* Warning: this is not implemented yet.
*
* In mixing video from multiple sources, one specified participant
* (or source port) will be the focus (i.e: occupy bigger portion
* than the others).
*/
PJMEDIA_VID_CONF_LAYOUT_SELECTIVE_FOCUS,
/**
* Warning: this is not implemented yet.
*
* In mixing video from multiple sources, one participant will be the
* focus at a time (i.e: occupy bigger portion than the others), and
* after some interval the focus will be shifted to another participant,
* so each participant will have the same focus duration.
*/
PJMEDIA_VID_CONF_LAYOUT_INTERVAL_FOCUS,
/**
* Warning: this is not implemented yet.
*
* In mixing video from multiple sources, each participant (or source
* port) will have specific layout configuration.
*/
PJMEDIA_VID_CONF_LAYOUT_CUSTOM,
} pjmedia_vid_conf_layout;
/**
* Video conference bridge settings.
*/
typedef struct pjmedia_vid_conf_setting
{
/**
* Maximum number of slots or media ports can be registered to the bridge.
*
* Default: 32
*/
unsigned max_slot_cnt;
/**
* Frame rate the bridge will operate at. For video playback smoothness,
* ideally the bridge frame rate should be the common multiple of the
* frame rates of the ports. Otherwise, ports whose unaligned frame rates
* may experience jitter. For example, if the application will work with
* frame rates of 10, 15, and 30 fps, setting this to 30 should be okay.
* But if it also needs to handle 20 fps, better setting this to 60.
*
* Default: 60 (frames per second)
*/
unsigned frame_rate;
/**
* Layout setting, see pjmedia_vid_conf_layout.
*
* Default: PJMEDIA_VID_CONF_LAYOUT_DEFAULT
*/
unsigned layout;
} pjmedia_vid_conf_setting;
/**
* Video conference bridge port info.
*/
typedef struct pjmedia_vid_conf_port_info
{
unsigned slot; /**< Slot index. */
pj_str_t name; /**< Port name. */
pjmedia_format format; /**< Format. */
unsigned listener_cnt; /**< Number of listeners. */
unsigned *listener_slots; /**< Array of listeners. */
unsigned transmitter_cnt; /**< Number of transmitter. */
unsigned *transmitter_slots; /**< Array of transmitter. */
} pjmedia_vid_conf_port_info;
/**
* Initialize video conference settings with default values.
*
* @param opt The settings to be initialized.
*/
PJ_DECL(void) pjmedia_vid_conf_setting_default(pjmedia_vid_conf_setting *opt);
/**
* Create a video conference bridge.
*
* @param pool The memory pool.
* @param opt The video conference settings.
* @param p_vid_conf Pointer to receive the video conference bridge.
*
* @return PJ_SUCCESS on success, or the appropriate
* error code.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_create(
pj_pool_t *pool,
const pjmedia_vid_conf_setting *opt,
pjmedia_vid_conf **p_vid_conf);
/**
* Destroy video conference bridge.
*
* @param vid_conf The video conference bridge.
*
* @return PJ_SUCCESS on success.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_destroy(pjmedia_vid_conf *vid_conf);
/**
* Add a media port to the video conference bridge.
*
* @param vid_conf The video conference bridge.
* @param pool The memory pool, the brige will create new pool
* based on this pool factory for this media port.
* @param port The media port to be added.
* @param name Name to be assigned to the slot. If not set, it will
* be set to the media port name.
* @param opt The option, for future use, currently this must
* be NULL.
* @param p_slot Pointer to receive the slot index of the port in
* the conference bridge.
*
* @return PJ_SUCCESS on success, or the appropriate error
* code.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_add_port(pjmedia_vid_conf *vid_conf,
pj_pool_t *pool,
pjmedia_port *port,
const pj_str_t *name,
void *opt,
unsigned *p_slot);
/**
* Remove a media port from the video conference bridge.
*
* @param vid_conf The video conference bridge.
* @param slot The media port's slot index to be removed.
*
* @return PJ_SUCCESS on success, or the appropriate error
* code.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_remove_port(pjmedia_vid_conf *vid_conf,
unsigned slot);
/**
* Get number of ports currently registered in the video conference bridge.
*
* @param vid_conf The video conference bridge.
*
* @return Number of ports currently registered to the video
* conference bridge.
*/
PJ_DECL(unsigned) pjmedia_vid_conf_get_port_count(pjmedia_vid_conf *vid_conf);
/**
* Enumerate occupied slots in the video conference bridge.
*
* @param conf The video conference bridge.
* @param slots Array of slot to be filled in.
* @param count On input, specifies the maximum number of slot
* in the array. On return, it will be filled with
* the actual number of slot.
*
* @return PJ_SUCCESS on success.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_enum_ports(pjmedia_vid_conf *vid_conf,
unsigned slots[],
unsigned *count);
/**
* Get port info.
*
* @param vid_conf The video conference bridge.
* @param slot Slot index.
* @param info Pointer to receive the info.
*
* @return PJ_SUCCESS on success.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_get_port_info(
pjmedia_vid_conf *vid_conf,
unsigned slot,
pjmedia_vid_conf_port_info *info);
/**
* Enable unidirectional video flow from the specified source slot to
* the specified sink slot.
*
* @param conf The video conference bridge.
* @param src_slot Source slot.
* @param sink_slot Sink slot.
* @param opt The option, for future use, currently this must
* be NULL.
*
* @return PJ_SUCCES on success.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_connect_port(
pjmedia_vid_conf *vid_conf,
unsigned src_slot,
unsigned sink_slot,
void *opt);
/**
* Disconnect unidirectional video flow from the specified source to
* the specified sink slot.
*
* @param conf The video conference bridge.
* @param src_slot Source slot.
* @param sink_slot Sink slot.
*
* @return PJ_SUCCESS on success.
*/
PJ_DECL(pj_status_t) pjmedia_vid_conf_disconnect_port(
pjmedia_vid_conf *vid_conf,
unsigned src_slot,
unsigned sink_slot);
PJ_END_DECL
/**
* @}
*/
#endif /* __PJMEDIA_VID_CONF_H__ */

View File

@ -152,6 +152,21 @@ PJ_DECL(pj_status_t)
pjmedia_vid_port_set_clock_src( pjmedia_vid_port *vid_port,
pjmedia_clock_src *clocksrc );
/**
* Subscribe media event notifications from the specified media port.
* Sample use case is that renderer video port needs to monitor stream port
* events so renderer can adjust its param whenever stream port detects
* format change.
*
* @param vid_port The video port.
* @param port The media port whose events to be monitored.
*
* @return PJ_SUCCESS on success or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjmedia_vid_port_subscribe_event(
pjmedia_vid_port *vid_port,
pjmedia_port *port);
/**
* Connect the video port to a downstream (slave) media port. This operation
* is only valid for video ports created with active interface selected.

View File

@ -191,4 +191,20 @@ PJ_DEF(void) pjmedia_converter_destroy(pjmedia_converter *cv)
(*cv->op->destroy)(cv);
}
PJ_DEF(pj_status_t) pjmedia_converter_convert2(
pjmedia_converter *cv,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
void *param)
{
if (!cv->op->convert2)
return PJ_ENOTSUP;
return (*cv->op->convert2)(cv, src_frame, src_frame_size, src_pos,
dst_frame, dst_frame_size, dst_pos, param);
}

View File

@ -33,6 +33,16 @@ static void factory_destroy_factory(pjmedia_converter_factory *cf);
static pj_status_t libswscale_conv_convert(pjmedia_converter *converter,
pjmedia_frame *src_frame,
pjmedia_frame *dst_frame);
static pj_status_t libswscale_conv_convert2(
pjmedia_converter *converter,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
pjmedia_converter_convert_setting
*param);
static void libswscale_conv_destroy(pjmedia_converter *converter);
@ -59,7 +69,8 @@ static pjmedia_converter_factory_op libswscale_factory_op =
static pjmedia_converter_op liswscale_converter_op =
{
&libswscale_conv_convert,
&libswscale_conv_destroy
&libswscale_conv_destroy,
&libswscale_conv_convert2
};
static pj_status_t factory_create_converter(pjmedia_converter_factory *cf,
@ -164,6 +175,75 @@ static pj_status_t libswscale_conv_convert(pjmedia_converter *converter,
return PJ_SUCCESS;
}
static pj_status_t libswscale_conv_convert2(
pjmedia_converter *converter,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
pjmedia_converter_convert_setting
*param)
{
struct ffmpeg_converter *fcv = (struct ffmpeg_converter*)converter;
struct fmt_info *src = &fcv->src,
*dst = &fcv->dst;
int h;
unsigned j;
pjmedia_rect_size orig_src_size;
pjmedia_rect_size orig_dst_size;
PJ_UNUSED_ARG(param);
/* Save original conversion sizes */
orig_src_size = src->apply_param.size;
orig_dst_size = dst->apply_param.size;
/* Set the first act buffer from src frame, and overwrite size. */
src->apply_param.buffer = src_frame->buf;
src->apply_param.size = *src_frame_size;
(*src->fmt_info->apply_fmt)(src->fmt_info, &src->apply_param);
/* Set the last act buffer from dst frame, and overwrite size. */
dst->apply_param.buffer = dst_frame->buf;
dst->apply_param.size = *dst_frame_size;
(*dst->fmt_info->apply_fmt)(dst->fmt_info, &dst->apply_param);
for (j = 0; j < src->fmt_info->plane_cnt; ++j) {
pjmedia_video_apply_fmt_param *ap = &src->apply_param;
int y = src_pos->y * ap->plane_bytes[j] / ap->strides[j] /
ap->size.h;
ap->planes[j] += y * ap->strides[j] +
src_pos->x * ap->strides[j] / ap->size.w;
}
for (j = 0; j < dst->fmt_info->plane_cnt; ++j) {
pjmedia_video_apply_fmt_param *ap = &dst->apply_param;
int y = dst_pos->y * ap->plane_bytes[j] / ap->strides[j] /
ap->size.h;
ap->planes[j] += y * ap->strides[j] +
dst_pos->x * ap->strides[j] / ap->size.w;
}
/* Return back the original conversion size */
src->apply_param.size = orig_src_size;
dst->apply_param.size = orig_dst_size;
h = sws_scale(fcv->sws_ctx,
(const uint8_t* const *)src->apply_param.planes,
src->apply_param.strides,
0, src->apply_param.size.h,
dst->apply_param.planes, dst->apply_param.strides);
//sws_scale() return value can't be trusted? There are cases when
//sws_scale() returns zero but conversion seems to work okay.
//return h==(int)dst->apply_param.size.h ? PJ_SUCCESS : PJ_EUNKNOWN;
PJ_UNUSED_ARG(h);
return PJ_SUCCESS;
}
static void libswscale_conv_destroy(pjmedia_converter *converter)
{
struct ffmpeg_converter *fcv = (struct ffmpeg_converter*)converter;

View File

@ -36,6 +36,16 @@ static pj_status_t libyuv_conv_convert(pjmedia_converter *converter,
pjmedia_frame *src_frame,
pjmedia_frame *dst_frame);
static pj_status_t libyuv_conv_convert2(
pjmedia_converter *converter,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
void *param);
static void libyuv_conv_destroy(pjmedia_converter *converter);
static pjmedia_converter_factory_op libyuv_factory_op =
@ -47,7 +57,8 @@ static pjmedia_converter_factory_op libyuv_factory_op =
static pjmedia_converter_op libyuv_converter_op =
{
&libyuv_conv_convert,
&libyuv_conv_destroy
&libyuv_conv_destroy,
&libyuv_conv_convert2
};
typedef struct fmt_info
@ -346,7 +357,7 @@ static int set_converter_act(pj_uint32_t src_id,
pj_bool_t need_scale = PJ_FALSE;
/* Convert to I420 or BGRA if needed. */
if ((src_id != PJMEDIA_FORMAT_I420) || (src_id != PJMEDIA_FORMAT_BGRA)) {
if ((src_id != PJMEDIA_FORMAT_I420) && (src_id != PJMEDIA_FORMAT_BGRA)) {
pj_uint32_t next_id = get_next_conv_fmt(src_id);
if (get_converter_map(src_id, next_id, src_size, dst_size, ++act_num,
act) != PJ_SUCCESS)
@ -358,8 +369,12 @@ static int set_converter_act(pj_uint32_t src_id,
}
/* Scale if needed */
need_scale = ((src_size->w != dst_size->w) ||
(src_size->h != dst_size->h));
//need_scale = ((src_size->w != dst_size->w) ||
//(src_size->h != dst_size->h));
// Always enable scale, as this can be used for rendering a region of
// a frame to another region of similar/another frame.
need_scale = PJ_TRUE;
if (need_scale) {
if (get_converter_map(current_id, current_id, src_size, dst_size,
@ -622,6 +637,168 @@ static pj_status_t libyuv_conv_convert(pjmedia_converter *converter,
return PJ_SUCCESS;
}
static pj_status_t libyuv_conv_convert2(
pjmedia_converter *converter,
pjmedia_frame *src_frame,
const pjmedia_rect_size *src_frame_size,
const pjmedia_coord *src_pos,
pjmedia_frame *dst_frame,
const pjmedia_rect_size *dst_frame_size,
const pjmedia_coord *dst_pos,
pjmedia_converter_convert_setting
*param)
{
struct libyuv_converter *lconv = (struct libyuv_converter*)converter;
int i = 0;
fmt_info *src_info = &lconv->act[0].src_fmt_info;
fmt_info *dst_info = &lconv->act[lconv->act_num-1].dst_fmt_info;
pjmedia_rect_size orig_src_size;
pjmedia_rect_size orig_dst_size;
PJ_UNUSED_ARG(param);
/* Save original conversion sizes */
orig_src_size = src_info->apply_param.size;
orig_dst_size = dst_info->apply_param.size;
/* Set the first act buffer from src frame, and overwrite size. */
src_info->apply_param.buffer = src_frame->buf;
src_info->apply_param.size = *src_frame_size;
/* Set the last act buffer from dst frame, and overwrite size. */
dst_info->apply_param.buffer = dst_frame->buf;
dst_info->apply_param.size = *dst_frame_size;
for (i=0;i<lconv->act_num;++i) {
/* Use destination info as the source info for the next act. */
struct fmt_info *src_fmt_info = (i==0)? src_info :
&lconv->act[i-1].dst_fmt_info;
struct fmt_info *dst_fmt_info = &lconv->act[i].dst_fmt_info;
(*src_fmt_info->vid_fmt_info->apply_fmt)(src_fmt_info->vid_fmt_info,
&src_fmt_info->apply_param);
(*dst_fmt_info->vid_fmt_info->apply_fmt)(dst_fmt_info->vid_fmt_info,
&dst_fmt_info->apply_param);
/* For first and last acts, apply plane buffer offset and return back
* the original sizes.
*/
if (i == 0) {
pjmedia_video_apply_fmt_param *ap = &src_fmt_info->apply_param;
unsigned j;
for (j = 0; j < src_fmt_info->vid_fmt_info->plane_cnt; ++j) {
int y = src_pos->y * ap->plane_bytes[j] / ap->strides[j] /
ap->size.h;
ap->planes[j] += y * ap->strides[j] + src_pos->x *
ap->strides[j] / ap->size.w;
}
ap->size = orig_src_size;
}
if (i == lconv->act_num-1) {
pjmedia_video_apply_fmt_param *ap = &dst_fmt_info->apply_param;
unsigned j;
for (j = 0; j < dst_fmt_info->vid_fmt_info->plane_cnt; ++j)
{
int y = dst_pos->y * ap->plane_bytes[j] / ap->strides[j] /
ap->size.h;
ap->planes[j] += y * ap->strides[j] + dst_pos->x *
ap->strides[j] / ap->size.w;
}
ap->size = orig_dst_size;
}
switch (lconv->act[i].act_type) {
case CONV_PACK_TO_PACK:
(*lconv->act[i].method.conv_pack_to_pack)(
(const uint8*)src_fmt_info->apply_param.planes[0],
src_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.planes[0],
dst_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.size.w,
dst_fmt_info->apply_param.size.h);
break;
case CONV_PACK_TO_PLANAR:
(*lconv->act[i].method.conv_pack_to_planar)(
(const uint8*)src_fmt_info->apply_param.planes[0],
src_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.planes[0],
dst_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.planes[1],
dst_fmt_info->apply_param.strides[1],
dst_fmt_info->apply_param.planes[2],
dst_fmt_info->apply_param.strides[2],
dst_fmt_info->apply_param.size.w,
dst_fmt_info->apply_param.size.h);
break;
case CONV_PLANAR_TO_PACK:
(*lconv->act[i].method.conv_planar_to_pack)(
(const uint8*)src_fmt_info->apply_param.planes[0],
src_fmt_info->apply_param.strides[0],
(const uint8*)src_fmt_info->apply_param.planes[1],
src_fmt_info->apply_param.strides[1],
(const uint8*)src_fmt_info->apply_param.planes[2],
src_fmt_info->apply_param.strides[2],
dst_fmt_info->apply_param.planes[0],
dst_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.size.w,
dst_fmt_info->apply_param.size.h);
break;
case CONV_PLANAR_TO_PLANAR:
(*lconv->act[i].method.conv_planar_to_planar)(
(const uint8*)src_fmt_info->apply_param.planes[0],
src_fmt_info->apply_param.strides[0],
(const uint8*)src_fmt_info->apply_param.planes[1],
src_fmt_info->apply_param.strides[1],
(const uint8*)src_fmt_info->apply_param.planes[2],
src_fmt_info->apply_param.strides[2],
dst_fmt_info->apply_param.planes[0],
dst_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.planes[1],
dst_fmt_info->apply_param.strides[1],
dst_fmt_info->apply_param.planes[2],
dst_fmt_info->apply_param.strides[2],
dst_fmt_info->apply_param.size.w,
dst_fmt_info->apply_param.size.h);
break;
case SCALE_PACK:
(*lconv->act[i].method.scale_pack)(
(const uint8*)src_fmt_info->apply_param.planes[0],
src_fmt_info->apply_param.strides[0],
src_fmt_info->apply_param.size.w,
src_fmt_info->apply_param.size.h,
(uint8*)dst_fmt_info->apply_param.planes[0],
dst_fmt_info->apply_param.strides[0],
dst_fmt_info->apply_param.size.w,
dst_fmt_info->apply_param.size.h,
LIBYUV_FILTER_MODE);
break;
case SCALE_PLANAR:
(*lconv->act[i].method.scale_planar)(
(const uint8*)src_fmt_info->apply_param.planes[0],
src_fmt_info->apply_param.strides[0],
(const uint8*)src_fmt_info->apply_param.planes[1],
src_fmt_info->apply_param.strides[1],
(const uint8*)src_fmt_info->apply_param.planes[2],
src_fmt_info->apply_param.strides[2],
src_fmt_info->apply_param.size.w,
src_fmt_info->apply_param.size.h,
(uint8*)dst_fmt_info->apply_param.planes[0],
dst_fmt_info->apply_param.strides[0],
(uint8*)dst_fmt_info->apply_param.planes[1],
dst_fmt_info->apply_param.strides[1],
(uint8*)dst_fmt_info->apply_param.planes[2],
dst_fmt_info->apply_param.strides[2],
dst_fmt_info->apply_param.size.w,
dst_fmt_info->apply_param.size.h,
LIBYUV_FILTER_MODE);
break;
};
}
return PJ_SUCCESS;
}
static void libyuv_conv_destroy(pjmedia_converter *converter)
{
PJ_UNUSED_ARG(converter);

File diff suppressed because it is too large Load Diff

View File

@ -646,8 +646,10 @@ PJ_DEF(pj_status_t) pjmedia_vid_port_create( pj_pool_t *pool,
/* Always need to create media port for passive role */
vp->pasv_port = pp = PJ_POOL_ZALLOC_T(pool, vid_pasv_port);
pp->vp = vp;
pp->base.get_frame = &vid_pasv_port_get_frame;
pp->base.put_frame = &vid_pasv_port_put_frame;
if (prm->vidparam.dir & PJMEDIA_DIR_CAPTURE)
pp->base.get_frame = &vid_pasv_port_get_frame;
if (prm->vidparam.dir & PJMEDIA_DIR_RENDER)
pp->base.put_frame = &vid_pasv_port_put_frame;
pjmedia_port_info_init2(&pp->base.info, &vp->dev_name,
PJMEDIA_SIG_VID_PORT,
prm->vidparam.dir, &prm->vidparam.fmt);
@ -730,6 +732,16 @@ pjmedia_vid_port_set_clock_src( pjmedia_vid_port *vid_port,
}
PJ_DEF(pj_status_t) pjmedia_vid_port_subscribe_event(
pjmedia_vid_port *vp,
pjmedia_port *port)
{
PJ_ASSERT_RETURN(vp && port, PJ_EINVAL);
/* Subscribe to port's events */
return pjmedia_event_subscribe(NULL, &client_port_event_cb, vp, port);
}
PJ_DEF(pj_status_t) pjmedia_vid_port_connect(pjmedia_vid_port *vp,
pjmedia_port *port,
pj_bool_t destroy)
@ -1000,7 +1012,7 @@ static pj_status_t client_port_event_cb(pjmedia_event *event,
}
}
if (vp->stream_role == ROLE_PASSIVE) {
if (vp->role == ROLE_ACTIVE && vp->stream_role == ROLE_PASSIVE) {
pjmedia_clock_param clock_param;
/**
@ -1016,6 +1028,12 @@ static pj_status_t client_port_event_cb(pjmedia_event *event,
/* pjmedia_vid_port_start(vp); */
pjmedia_vid_dev_stream_start(vp->strm);
/* Update passive port info from the video stream */
if (vp->role == ROLE_PASSIVE) {
pjmedia_format_copy(&vp->pasv_port->base.info.fmt,
&event->data.fmt_changed.new_fmt);
}
}
/* Republish the event, post the event to the event manager

View File

@ -928,6 +928,14 @@ static pj_status_t put_frame(pjmedia_port *port,
/* Get frame length in timestamp unit */
rtp_ts_len = stream->frame_ts_len;
/* Empty video frame? Just update RTP timestamp for now */
if (frame->type==PJMEDIA_FRAME_TYPE_VIDEO && frame->size==0) {
pjmedia_rtp_encode_rtp(&channel->rtp, channel->pt, 1, 0,
rtp_ts_len, (const void**)&rtphdr,
&rtphdrlen);
return PJ_SUCCESS;
}
/* Init frame_out buffer. */
pj_bzero(&frame_out, sizeof(frame_out));
frame_out.buf = ((char*)channel->buf) + sizeof(pjmedia_rtp_hdr);

View File

@ -102,6 +102,7 @@
#define CMD_VIDEO_DEVICE ((CMD_VIDEO*10)+5)
#define CMD_VIDEO_CODEC ((CMD_VIDEO*10)+6)
#define CMD_VIDEO_WIN ((CMD_VIDEO*10)+7)
#define CMD_VIDEO_CONF ((CMD_VIDEO*10)+8)
/* video level 3 command */
#define CMD_VIDEO_ACC_SHOW ((CMD_VIDEO_ACC*10)+1)
@ -129,6 +130,9 @@
#define CMD_VIDEO_WIN_HIDE ((CMD_VIDEO_WIN*10)+4)
#define CMD_VIDEO_WIN_MOVE ((CMD_VIDEO_WIN*10)+5)
#define CMD_VIDEO_WIN_RESIZE ((CMD_VIDEO_WIN*10)+6)
#define CMD_VIDEO_CONF_LIST ((CMD_VIDEO_CONF*10)+1)
#define CMD_VIDEO_CONF_CONNECT ((CMD_VIDEO_CONF*10)+2)
#define CMD_VIDEO_CONF_DISCONNECT ((CMD_VIDEO_CONF*10)+3)
/* dynamic choice argument list */
#define DYN_CHOICE_START 9900
@ -2465,6 +2469,81 @@ static pj_status_t cmd_resize_vid_win(pj_cli_cmd_val *cval)
return pjsua_vid_win_set_size(wid, &size);
}
static pj_status_t cmd_vid_conf_list()
{
pjsua_conf_port_id id[100];
unsigned count = PJ_ARRAY_SIZE(id);
unsigned i;
pj_status_t status;
status = pjsua_vid_conf_enum_ports(id, &count);
if (status != PJ_SUCCESS) {
PJ_PERROR(1,(THIS_FILE, status,
"Failed enumerating video conf bridge ports"));
return status;
}
PJ_LOG(3,(THIS_FILE," Video conference has %d ports:\n", count));
PJ_LOG(3,(THIS_FILE," id name format rx tx \n"));
PJ_LOG(3,(THIS_FILE," ------------------------------------------------------------------\n"));
for (i=0; i<count; ++i) {
char li_list[PJSUA_MAX_CALLS*4];
char tr_list[PJSUA_MAX_CALLS*4];
char s[32];
unsigned j;
pjsua_vid_conf_port_info info;
pjmedia_rect_size *size;
pjmedia_ratio *fps;
pjsua_vid_conf_get_port_info(id[i], &info);
size = &info.format.det.vid.size;
fps = &info.format.det.vid.fps;
li_list[0] = '\0';
for (j=0; j<info.listener_cnt; ++j) {
char s[10];
pj_ansi_snprintf(s, sizeof(s), "%d%s",
info.listeners[j],
(j==info.listener_cnt-1)?"":",");
pj_ansi_strcat(li_list, s);
}
tr_list[0] = '\0';
for (j=0; j<info.transmitter_cnt; ++j) {
char s[10];
pj_ansi_snprintf(s, sizeof(s), "%d%s", info.transmitters[j],
(j==info.transmitter_cnt-1)?"":",");
pj_ansi_strcat(tr_list, s);
}
pjmedia_fourcc_name(info.format.id, s);
s[4] = ' ';
pj_ansi_snprintf(s+5, sizeof(s)-5, "%dx%d@%.1f",
size->w, size->h, (float)(fps->num*1.0/fps->denum));
PJ_LOG(3,(THIS_FILE,"%3d %.*s%.*s %s%.*s %s%.*s %s\n",
id[i],
(int)info.name.slen, info.name.ptr,
22-(int)info.name.slen, " ",
s,
20-pj_ansi_strlen(s), " ",
tr_list,
12-pj_ansi_strlen(tr_list), " ",
li_list));
}
return PJ_SUCCESS;
}
static pj_status_t cmd_vid_conf_connect(pj_cli_cmd_val *cval, pj_bool_t connect)
{
int P, Q;
P = (int)pj_strtol(&cval->argv[1]);
Q = (int)pj_strtol(&cval->argv[2]);
if (connect)
return pjsua_vid_conf_connect(P, Q, NULL);
else
return pjsua_vid_conf_disconnect(P, Q);
}
/* Video handler */
static pj_status_t cmd_video_handler(pj_cli_cmd_val *cval)
{
@ -2545,6 +2624,13 @@ static pj_status_t cmd_video_handler(pj_cli_cmd_val *cval)
case CMD_VIDEO_WIN_RESIZE:
status = cmd_resize_vid_win(cval);
break;
case CMD_VIDEO_CONF_LIST:
status = cmd_vid_conf_list();
break;
case CMD_VIDEO_CONF_CONNECT:
case CMD_VIDEO_CONF_DISCONNECT:
status = cmd_vid_conf_connect(cval, (cmd_id==CMD_VIDEO_CONF_CONNECT));
break;
}
return status;
@ -3050,6 +3136,17 @@ static pj_status_t add_video_command(pj_cli_t *c)
" <ARG name='height' type='int' desc='Height'/>"
" </CMD>"
" </CMD>"
" <CMD name='conf' id='6008' desc='Video conference commands'>"
" <CMD name='list' id='60081' desc='List all ports in video conference'/>"
" <CMD name='cc' id='60082' desc='Connect ports in video conference'>"
" <ARG name='source' type='int' desc='Source port ID'/>"
" <ARG name='sink' type='int' desc='Sink port ID'/>"
" </CMD>"
" <CMD name='cd' id='60083' desc='Disconnect ports in video conference'>"
" <ARG name='source' type='int' desc='Source port ID'/>"
" <ARG name='sink' type='int' desc='Sink port ID'/>"
" </CMD>"
" </CMD>"
"</CMD>";
pj_str_t xml = pj_str(video_command);

View File

@ -296,6 +296,9 @@ static void vid_show_help()
puts("| vid win show|hide ID Show/hide the specified video window ID |");
puts("| vid win move ID X Y Move window ID to position X,Y |");
puts("| vid win resize ID w h Resize window ID to the specified width, height |");
puts("| vid conf list List all video ports in video conference bridge |");
puts("| vid conf cc P Q Connect port P to Q in the video conf bridge |");
puts("| vid conf cd P Q Disconnect port P to Q in the video conf bridge |");
puts("+=============================================================================+");
printf("| Video will be %s in the next offer/answer %s |\n",
(vid_enabled? "enabled" : "disabled"), (vid_enabled? " " : ""));
@ -591,6 +594,80 @@ static void vid_handle_menu(char *menuin)
PJ_PERROR(1,(THIS_FILE, status, "Set codec size error"));
} else
goto on_error;
} else if (strcmp(argv[1], "conf")==0) {
pj_status_t status;
if (argc==3 && strcmp(argv[2], "list")==0) {
pjsua_conf_port_id id[100];
unsigned count = PJ_ARRAY_SIZE(id);
status = pjsua_vid_conf_enum_ports(id, &count);
if (status != PJ_SUCCESS) {
PJ_PERROR(1,(THIS_FILE, status,
"Failed enumerating video conf bridge ports"));
} else {
unsigned i;
printf(" Video conference has %d ports:\n", count);
printf(" id name format rx tx \n");
printf(" ------------------------------------------------------------------\n");
for (i=0; i<count; ++i) {
char li_list[PJSUA_MAX_CALLS*4];
char tr_list[PJSUA_MAX_CALLS*4];
char s[32];
unsigned j;
pjsua_vid_conf_port_info info;
pjmedia_rect_size *size;
pjmedia_ratio *fps;
pjsua_vid_conf_get_port_info(id[i], &info);
size = &info.format.det.vid.size;
fps = &info.format.det.vid.fps;
li_list[0] = '\0';
for (j=0; j<info.listener_cnt; ++j) {
char s[10];
pj_ansi_snprintf(s, sizeof(s), "%d%s",
info.listeners[j],
(j==info.listener_cnt-1)?"":",");
pj_ansi_strcat(li_list, s);
}
tr_list[0] = '\0';
for (j=0; j<info.transmitter_cnt; ++j) {
char s[10];
pj_ansi_snprintf(s, sizeof(s), "%d%s",
info.transmitters[j],
(j==info.transmitter_cnt-1)?"":",");
pj_ansi_strcat(tr_list, s);
}
pjmedia_fourcc_name(info.format.id, s);
s[4] = ' ';
pj_ansi_snprintf(s+5, sizeof(s)-5, "%dx%d@%.1f",
size->w, size->h,
(float)(fps->num*1.0/fps->denum));
printf("%3d %.*s%.*s %s%.*s %s%.*s %s\n",
id[i],
(int)info.name.slen, info.name.ptr,
22-(int)info.name.slen, " ",
s,
20-pj_ansi_strlen(s), " ",
tr_list,
12-pj_ansi_strlen(tr_list), " ",
li_list);
}
}
} else if (argc==5 && strcmp(argv[2], "cc")==0) {
int P, Q;
P = atoi(argv[3]);
Q = atoi(argv[4]);
pjsua_vid_conf_connect(P, Q, NULL);
} else if (argc==5 && strcmp(argv[2], "cd")==0) {
int P, Q;
P = atoi(argv[3]);
Q = atoi(argv[4]);
pjsua_vid_conf_disconnect(P, Q);
} else {
goto on_error;
}
} else
goto on_error;

View File

@ -4679,8 +4679,21 @@ typedef struct pjsua_call_media_info
*/
pjsua_vid_win_id win_in;
/** The video capture device for outgoing transmission,
* if any, or PJMEDIA_VID_INVALID_DEV
/**
* The video conference port number for the call in decoding
* direction.
*/
pjsua_conf_port_id dec_slot;
/**
* The video conference port number for the call in encoding
* direction.
*/
pjsua_conf_port_id enc_slot;
/**
* The video capture device for outgoing transmission,
* if any, or PJMEDIA_VID_INVALID_DEV
*/
pjmedia_vid_dev_index cap_dev;
@ -5168,6 +5181,36 @@ PJ_DECL(pj_bool_t) pjsua_call_has_media(pjsua_call_id call_id);
*/
PJ_DECL(pjsua_conf_port_id) pjsua_call_get_conf_port(pjsua_call_id call_id);
/**
* Get the video window associated with the call. Note that this function
* will only evaluate the first video stream in the call, to query any other
* video stream, use pjsua_call_get_info().
*
* @param call_id Call identification.
*
* @return Video window, or PJSUA_INVALID_ID when the
* media has not been established or is not active.
*/
PJ_DECL(pjsua_vid_win_id) pjsua_call_get_vid_win(pjsua_call_id call_id);
/**
* Get the video conference port identification associated with the call.
* Note that this function will only evaluate the first video stream in
* the call, to query any other video stream, use pjsua_call_get_info().
*
* @param call_id Call identification.
* @param dir Port direction to be queried. Valid values are
* PJMEDIA_DIR_ENCODING and PJMEDIA_DIR_DECODING only.
*
* @return Conference port ID, or PJSUA_INVALID_ID when the
* media has not been established or is not active.
*/
PJ_DECL(pjsua_conf_port_id) pjsua_call_get_vid_conf_port(
pjsua_call_id call_id,
pjmedia_dir dir);
/**
* Obtain detail information about the specified call.
*
@ -6703,7 +6746,7 @@ typedef struct pjsua_codec_info
/**
* This structure descibes information about a particular media port that
* This structure describes information about a particular media port that
* has been registered into the conference bridge. Application can query
* this info by calling #pjsua_conf_get_port_info().
*/
@ -6740,7 +6783,7 @@ typedef struct pjsua_conf_port_info
unsigned listener_cnt;
/** Array of listeners (in other words, ports where this port is
* transmitting to.
* transmitting to).
*/
pjsua_conf_port_id listeners[PJSUA_MAX_CONF_PORTS];
@ -7806,6 +7849,18 @@ PJ_DECL(pj_status_t) pjsua_vid_preview_start(pjmedia_vid_dev_index id,
*/
PJ_DECL(pjsua_vid_win_id) pjsua_vid_preview_get_win(pjmedia_vid_dev_index id);
/**
* Get video conference slot ID of the specified capture device, if any.
*
* @param id The capture device ID.
*
* @return The video conference slot ID of the specified capture
* device ID, or PJSUA_INVALID_ID if preview has not been
* started for the device.
*/
PJ_DECL(pjsua_conf_port_id) pjsua_vid_preview_get_vid_conf_port(
pjmedia_vid_dev_index id);
/**
* Stop video preview.
*
@ -7843,6 +7898,11 @@ typedef struct pjsua_vid_win_info
*/
pjmedia_vid_dev_index rdr_dev;
/**
* Renderer port ID in the video conference bridge.
*/
pjsua_conf_port_id slot_id;
/**
* Window show status. The window is hidden if false.
*/
@ -8017,6 +8077,147 @@ PJ_DECL(pj_status_t) pjsua_vid_codec_set_param(
const pjmedia_vid_codec_param *param);
/*
* Video conference API
*/
/**
* This structure describes information about a particular video media port
* that has been registered into the video conference bridge. Application
* can query this info by calling #pjsua_vid_conf_get_port_info().
*/
typedef struct pjsua_vid_conf_port_info
{
/** Conference port number. */
pjsua_conf_port_id slot_id;
/** Port name. */
pj_str_t name;
/** Format. */
pjmedia_format format;
/** Number of listeners in the array. */
unsigned listener_cnt;
/** Array of listeners (in other words, ports where this port is
* transmitting to).
*/
pjsua_conf_port_id listeners[PJSUA_MAX_CONF_PORTS];
/** Number of transmitters in the array. */
unsigned transmitter_cnt;
/** Array of transmitters (in other words, ports where this port is
* receiving from).
*/
pjsua_conf_port_id transmitters[PJSUA_MAX_CONF_PORTS];
} pjsua_vid_conf_port_info;
/**
* Get current number of active ports in the bridge.
*
* @return The number.
*/
PJ_DECL(unsigned) pjsua_vid_conf_get_active_ports(void);
/**
* Enumerate all video conference ports.
*
* @param id Array of conference port ID to be initialized.
* @param count On input, specifies max elements in the array.
* On return, it contains actual number of elements
* that have been initialized.
*
* @return PJ_SUCCESS on success, or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjsua_vid_conf_enum_ports(pjsua_conf_port_id id[],
unsigned *count);
/**
* Get information about the specified video conference port
*
* @param port_id Port identification.
* @param info Pointer to store the port info.
*
* @return PJ_SUCCESS on success, or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjsua_vid_conf_get_port_info(
pjsua_conf_port_id port_id,
pjsua_vid_conf_port_info *info);
/**
* Add arbitrary video media port to PJSUA's video conference bridge.
* Application can use this function to add the media port that it creates.
* For media ports that are created by PJSUA-LIB (such as calls, AVI player),
* PJSUA-LIB will automatically add the port to the bridge.
*
* @param pool Pool to use.
* @param port Media port to be added to the bridge.
* @param param Currently this is not used and must be set to NULL.
* @param p_id Optional pointer to receive the conference
* slot id.
*
* @return PJ_SUCCESS on success, or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjsua_vid_conf_add_port(pj_pool_t *pool,
pjmedia_port *port,
const void *param,
pjsua_conf_port_id *p_id);
/**
* Remove arbitrary slot from the video conference bridge. Application should
* only call this function if it registered the port manually with previous
* call to #pjsua_vid_conf_add_port().
*
* @param port_id The slot id of the port to be removed.
*
* @return PJ_SUCCESS on success, or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjsua_vid_conf_remove_port(pjsua_conf_port_id port_id);
/**
* Establish unidirectional video flow from souce to sink. One source
* may transmit to multiple destinations/sink. And if multiple
* sources are transmitting to the same sink, the video will be mixed
* together (currently, each source will be resized down so all sources will
* occupy the same portion in the sink video frame). Source and sink may
* refer to the same ID, effectively looping the media.
*
* If bidirectional media flow is desired, application needs to call
* this function twice, with the second one having the arguments
* reversed.
*
* @param source Port ID of the source media/transmitter.
* @param sink Port ID of the destination media/received.
* @param param Currently this is not used and must be set to NULL.
*
* @return PJ_SUCCESS on success, or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjsua_vid_conf_connect(pjsua_conf_port_id source,
pjsua_conf_port_id sink,
const void *param);
/**
* Disconnect video flow from the source to destination port.
*
* @param source Port ID of the source media/transmitter.
* @param sink Port ID of the destination media/received.
*
* @return PJ_SUCCESS on success, or the appropriate error code.
*/
PJ_DECL(pj_status_t) pjsua_vid_conf_disconnect(pjsua_conf_port_id source,
pjsua_conf_port_id sink);
/* end of VIDEO API */
/**

View File

@ -59,6 +59,8 @@ struct pjsua_call_media
/** Video stream */
struct {
pjmedia_vid_stream *stream; /**< The video stream. */
pjsua_conf_port_id strm_enc_slot; /**< Stream encode slot */
pjsua_conf_port_id strm_dec_slot; /**< Stream decode slot */
pjsua_vid_win_id cap_win_id;/**< The video capture window */
pjsua_vid_win_id rdr_win_id;/**< The video render window */
pjmedia_vid_dev_index cap_dev; /**< The video capture device */
@ -403,7 +405,8 @@ typedef struct pjsua_vid_win
unsigned ref_cnt; /**< Reference counter. */
pjmedia_vid_port *vp_cap; /**< Capture vidport. */
pjmedia_vid_port *vp_rend; /**< Renderer vidport */
pjmedia_port *tee; /**< Video tee */
pjsua_conf_port_id cap_slot; /**< Capturer conf slot */
pjsua_conf_port_id rend_slot; /**< Renderer conf slot */
pjmedia_vid_dev_index preview_cap_id;/**< Capture dev id */
pj_bool_t preview_running;/**< Preview is started*/
pj_bool_t is_native; /**< Preview is by dev */
@ -512,6 +515,7 @@ struct pjsua_data
/* For keeping video device settings */
#if PJSUA_HAS_VIDEO
pjmedia_vid_conf *vid_conf;
pj_uint32_t vid_caps[PJMEDIA_VID_DEV_MAX_DEVS];
pjmedia_vid_dev_param vid_param[PJMEDIA_VID_DEV_MAX_DEVS];
#endif

View File

@ -152,6 +152,8 @@ static void reset_call(pjsua_call_id id)
call_med->strm.a.conf_slot = PJSUA_INVALID_ID;
call_med->strm.v.cap_win_id = PJSUA_INVALID_ID;
call_med->strm.v.rdr_win_id = PJSUA_INVALID_ID;
call_med->strm.v.strm_dec_slot = PJSUA_INVALID_ID;
call_med->strm.v.strm_enc_slot = PJSUA_INVALID_ID;
call_med->call = call;
call_med->idx = i;
call_med->tp_auto_del = PJ_TRUE;
@ -2224,6 +2226,11 @@ PJ_DEF(pj_status_t) pjsua_call_get_info( pjsua_call_id call_id,
info->media[info->media_cnt].stream.vid.win_in =
call_med->strm.v.rdr_win_id;
info->media[info->media_cnt].stream.vid.dec_slot =
call_med->strm.v.strm_dec_slot;
info->media[info->media_cnt].stream.vid.enc_slot =
call_med->strm.v.strm_enc_slot;
if (call_med->strm.v.cap_win_id != PJSUA_INVALID_ID) {
cap_dev = call_med->strm.v.cap_dev;
}

View File

@ -1480,6 +1480,19 @@ pj_status_t call_media_on_event(pjmedia_event *event,
}
break;
case PJMEDIA_EVENT_FMT_CHANGED:
if (call_med->strm.v.rdr_win_id != PJSUA_INVALID_ID) {
pjsua_vid_win *w = &pjsua_var.win[call_med->strm.v.rdr_win_id];
if (event->src == w->vp_rend) {
/* Renderer just changed format, reconnect stream */
pjsua_vid_conf_disconnect(call_med->strm.v.strm_dec_slot,
w->rend_slot);
pjsua_vid_conf_connect(call_med->strm.v.strm_dec_slot,
w->rend_slot, NULL);
}
}
break;
default:
break;
}

View File

@ -26,7 +26,6 @@
#if PJSUA_HAS_VIDEO
#define ENABLE_EVENT 1
#define VID_TEE_MAX_PORT (PJSUA_MAX_CALLS + 1)
#define PJSUA_SHOW_WINDOW 1
#define PJSUA_HIDE_WINDOW 0
@ -68,6 +67,14 @@ pj_status_t pjsua_vid_subsys_init(void)
goto on_error;
}
status = pjmedia_vid_conf_create(pjsua_var.pool, NULL,
&pjsua_var.vid_conf);
if (status != PJ_SUCCESS) {
PJ_PERROR(1,(THIS_FILE, status,
"Error creating PJMEDIA video conference bridge"));
goto on_error;
}
#if PJMEDIA_HAS_VIDEO && PJMEDIA_HAS_VID_TOOLBOX_CODEC
status = pjmedia_codec_vid_toolbox_init(NULL, &pjsua_var.cp.factory);
if (status != PJ_SUCCESS) {
@ -140,6 +147,11 @@ pj_status_t pjsua_vid_subsys_destroy(void)
}
}
if (pjsua_var.vid_conf) {
pjmedia_vid_conf_destroy(pjsua_var.vid_conf);
pjsua_var.vid_conf = NULL;
}
pjmedia_vid_dev_subsys_shutdown();
#if PJMEDIA_HAS_FFMPEG_VID_CODEC
@ -514,6 +526,24 @@ PJ_DEF(pjsua_vid_win_id) pjsua_vid_preview_get_win(pjmedia_vid_dev_index id)
return vid_preview_get_win(id, PJ_TRUE);
}
/*
* Get video conference slot ID of the specified capture device.
*/
PJ_DEF(pjsua_conf_port_id) pjsua_vid_preview_get_vid_conf_port(
pjmedia_vid_dev_index id)
{
pjsua_vid_win_id wid;
pjsua_vid_win *w;
wid = vid_preview_get_win(id, PJ_TRUE);
if (wid == PJSUA_INVALID_ID)
return PJSUA_INVALID_ID;
w = &pjsua_var.win[wid];
return w->cap_slot;
}
PJ_DEF(void) pjsua_vid_win_reset(pjsua_vid_win_id wid)
{
pjsua_vid_win *w = &pjsua_var.win[wid];
@ -527,9 +557,9 @@ PJ_DEF(void) pjsua_vid_win_reset(pjsua_vid_win_id wid)
}
/* Allocate and initialize pjsua video window:
* - If the type is preview, video capture, tee, and render
* will be instantiated.
* - If the type is stream, only renderer will be created.
* - If the type is preview: capture port and render port
* will be instantiated, and connected via conf.
* - If the type is stream: only render port will be created.
*/
static pj_status_t create_vid_win(pjsua_vid_win_type type,
const pjmedia_format *fmt,
@ -672,7 +702,7 @@ static pj_status_t create_vid_win(pjsua_vid_win_type type,
w->preview_cap_id = cap_id;
/* Create capture video port */
vp_param.active = PJ_TRUE;
vp_param.active = PJ_FALSE;
vp_param.vidparam.dir = PJMEDIA_DIR_CAPTURE;
/* Update the video setting with user preference */
@ -702,14 +732,11 @@ static pj_status_t create_vid_win(pjsua_vid_win_type type,
fmt_ = vp_param.vidparam.fmt;
fmt = &fmt_;
/* Create video tee */
status = pjmedia_vid_tee_create(w->pool, fmt, VID_TEE_MAX_PORT,
&w->tee);
if (status != PJ_SUCCESS)
goto on_error;
/* Connect capturer to the video tee */
status = pjmedia_vid_port_connect(w->vp_cap, w->tee, PJ_FALSE);
/* Register capturer to the video conf */
status = pjsua_vid_conf_add_port(
w->pool,
pjmedia_vid_port_get_passive_port(w->vp_cap),
NULL, &w->cap_slot);
if (status != PJ_SUCCESS)
goto on_error;
@ -738,7 +765,7 @@ static pj_status_t create_vid_win(pjsua_vid_win_type type,
if (status != PJ_SUCCESS)
goto on_error;
vp_param.active = (w->type == PJSUA_WND_TYPE_STREAM);
vp_param.active = PJ_FALSE;
vp_param.vidparam.dir = PJMEDIA_DIR_RENDER;
vp_param.vidparam.fmt = *fmt;
vp_param.vidparam.disp_size = fmt->det.vid.size;
@ -755,12 +782,17 @@ static pj_status_t create_vid_win(pjsua_vid_win_type type,
if (status != PJ_SUCCESS)
goto on_error;
/* For preview window, connect capturer & renderer (via tee) */
if (w->type == PJSUA_WND_TYPE_PREVIEW) {
pjmedia_port *rend_port;
/* Register renderer to the video conf */
status = pjsua_vid_conf_add_port(
w->pool,
pjmedia_vid_port_get_passive_port(w->vp_rend),
NULL, &w->rend_slot);
if (status != PJ_SUCCESS)
goto on_error;
rend_port = pjmedia_vid_port_get_passive_port(w->vp_rend);
status = pjmedia_vid_tee_add_dst_port2(w->tee, 0, rend_port);
/* For preview window, connect capturer & renderer (via conf) */
if (w->type == PJSUA_WND_TYPE_PREVIEW) {
status = pjsua_vid_conf_connect(w->cap_slot, w->rend_slot, NULL);
if (status != PJ_SUCCESS)
goto on_error;
}
@ -798,21 +830,19 @@ static void free_vid_win(pjsua_vid_win_id wid)
pj_log_push_indent();
if (w->vp_cap) {
pjsua_vid_conf_remove_port(w->cap_slot);
pjmedia_event_unsubscribe(NULL, &call_media_on_event, NULL,
w->vp_cap);
pjmedia_vid_port_stop(w->vp_cap);
pjmedia_vid_port_disconnect(w->vp_cap);
pjmedia_vid_port_destroy(w->vp_cap);
}
if (w->vp_rend) {
pjsua_vid_conf_remove_port(w->rend_slot);
pjmedia_event_unsubscribe(NULL, &call_media_on_event, NULL,
w->vp_rend);
pjmedia_vid_port_stop(w->vp_rend);
pjmedia_vid_port_destroy(w->vp_rend);
}
if (w->tee) {
pjmedia_port_destroy(w->tee);
}
pjsua_vid_win_reset(wid);
pj_log_pop_indent();
@ -849,6 +879,8 @@ pj_status_t pjsua_vid_channel_init(pjsua_call_media *call_med)
call_med->strm.v.rdr_dev = acc->cfg.vid_rend_dev;
call_med->strm.v.cap_dev = acc->cfg.vid_cap_dev;
call_med->strm.v.strm_dec_slot = PJSUA_INVALID_ID;
call_med->strm.v.strm_enc_slot = PJSUA_INVALID_ID;
if (call_med->strm.v.rdr_dev == PJMEDIA_VID_DEFAULT_RENDER_DEV) {
pjmedia_vid_dev_info info;
pjmedia_vid_dev_get_info(call_med->strm.v.rdr_dev, &info);
@ -978,6 +1010,7 @@ pj_status_t pjsua_vid_channel_update(pjsua_call_media *call_med,
PJ_LOG(4,(THIS_FILE, "Setting up RX.."));
pj_log_push_indent();
/* Retrieve stream decoding port */
status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
PJMEDIA_DIR_DECODING,
&media_port);
@ -1009,9 +1042,22 @@ pj_status_t pjsua_vid_channel_update(pjsua_call_media *call_med,
call_med, w->vp_rend);
#endif
/* Connect renderer to stream */
status = pjmedia_vid_port_connect(w->vp_rend, media_port,
PJ_FALSE);
/* Register renderer to stream events */
pjmedia_vid_port_subscribe_event(w->vp_rend, media_port);
/* Register stream decoding to conf, using tmp_pool should be fine
* as bridge will create its own pool (using tmp_pool factory).
*/
status = pjsua_vid_conf_add_port(tmp_pool, media_port, NULL,
&call_med->strm.v.strm_dec_slot);
if (status != PJ_SUCCESS) {
pj_log_pop_indent();
goto on_error;
}
/* Connect stream to renderer (via conf) */
status = pjsua_vid_conf_connect(call_med->strm.v.strm_dec_slot,
w->rend_slot, NULL);
if (status != PJ_SUCCESS) {
pj_log_pop_indent();
goto on_error;
@ -1041,6 +1087,7 @@ pj_status_t pjsua_vid_channel_update(pjsua_call_media *call_med,
PJ_LOG(4,(THIS_FILE, "Setting up TX.."));
pj_log_push_indent();
/* Retrieve stream encoding port */
status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
PJMEDIA_DIR_ENCODING,
&media_port);
@ -1078,8 +1125,20 @@ pj_status_t pjsua_vid_channel_update(pjsua_call_media *call_med,
call_med, w->vp_cap);
#endif
/* Connect stream to capturer (via video window tee) */
status = pjmedia_vid_tee_add_dst_port2(w->tee, 0, media_port);
/* Register stream encoding to conf, using tmp_pool should be fine
* as bridge will create its own pool (using tmp_pool factory).
*/
status = pjsua_vid_conf_add_port(tmp_pool, media_port, NULL,
&call_med->strm.v.strm_enc_slot);
if (status != PJ_SUCCESS) {
pj_log_pop_indent();
goto on_error;
}
/* Connect capturer to stream encoding (via conf) */
status = pjsua_vid_conf_connect(w->cap_slot,
call_med->strm.v.strm_enc_slot,
NULL);
if (status != PJ_SUCCESS) {
pj_log_pop_indent();
goto on_error;
@ -1132,32 +1191,20 @@ void pjsua_vid_stop_stream(pjsua_call_media *call_med)
PJ_LOG(4,(THIS_FILE, "Stopping video stream.."));
pj_log_push_indent();
/* Unregister video stream ports (encode+decode) from conference */
pjsua_vid_conf_remove_port(call_med->strm.v.strm_enc_slot);
pjsua_vid_conf_remove_port(call_med->strm.v.strm_dec_slot);
pjmedia_vid_stream_send_rtcp_bye(strm);
if (call_med->strm.v.cap_win_id != PJSUA_INVALID_ID) {
pjmedia_port *media_port;
pjsua_vid_win *w = &pjsua_var.win[call_med->strm.v.cap_win_id];
pj_status_t status;
/* Stop the capture before detaching stream and unsubscribing event */
pjmedia_vid_port_stop(w->vp_cap);
/* Disconnect video stream from capture device */
status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
PJMEDIA_DIR_ENCODING,
&media_port);
if (status == PJ_SUCCESS) {
pjmedia_vid_tee_remove_dst_port(w->tee, media_port);
}
/* Unsubscribe event */
/* Unsubscribe event */
pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
w->vp_cap);
/* Re-start capture again, if it is used by other stream */
if (w->ref_cnt > 1)
pjmedia_vid_port_start(w->vp_cap);
/* Decrement ref count of preview video window */
dec_vid_win(call_med->strm.v.cap_win_id);
call_med->strm.v.cap_win_id = PJSUA_INVALID_ID;
}
@ -1165,11 +1212,12 @@ void pjsua_vid_stop_stream(pjsua_call_media *call_med)
if (call_med->strm.v.rdr_win_id != PJSUA_INVALID_ID) {
pjsua_vid_win *w = &pjsua_var.win[call_med->strm.v.rdr_win_id];
/* Stop the render before unsubscribing event */
/* Unsubscribe event, but stop the render first */
pjmedia_vid_port_stop(w->vp_rend);
pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
w->vp_rend);
/* Decrement ref count of stream video window */
dec_vid_win(call_med->strm.v.rdr_win_id);
call_med->strm.v.rdr_win_id = PJSUA_INVALID_ID;
}
@ -1431,6 +1479,7 @@ PJ_DEF(pj_status_t) pjsua_vid_win_get_info( pjsua_vid_win_id wid,
}
wi->rdr_dev = vparam.rend_id;
wi->slot_id = w->rend_slot;
wi->hwnd = vparam.window;
wi->show = !vparam.window_hide;
wi->pos = vparam.window_pos;
@ -2042,10 +2091,13 @@ static pj_status_t call_change_cap_dev(pjsua_call *call,
if (status == PJ_SUCCESS) {
w->preview_cap_id = cap_dev;
call_med->strm.v.cap_dev = cap_dev;
/* Yay, change capturer done! */
return PJ_SUCCESS;
}
/* No it doesn't support fast switching. Do slow switching then.. */
/* Oh no, it doesn't support fast switching. Do normal change then,
* i.e: remove the old and create a new capture.
*/
status = pjmedia_vid_stream_get_port(call_med->strm.v.stream,
PJMEDIA_DIR_ENCODING, &media_port);
if (status != PJ_SUCCESS)
@ -2054,22 +2106,12 @@ static pj_status_t call_change_cap_dev(pjsua_call *call,
pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
w->vp_cap);
/* temporarily disconnect while we operate on the tee. */
pjmedia_vid_port_disconnect(w->vp_cap);
/* Disconnect the old capture device to stream encoding port */
status = pjsua_vid_conf_disconnect(w->cap_slot,
call_med->strm.v.strm_enc_slot);
if (status != PJ_SUCCESS)
return status;
/* = Detach stream port from the old capture device's tee = */
status = pjmedia_vid_tee_remove_dst_port(w->tee, media_port);
if (status != PJ_SUCCESS) {
/* Something wrong, assume that media_port has been removed
* and continue.
*/
PJ_PERROR(4,(THIS_FILE, status,
"Warning: call %d: unable to remove video from tee",
call->index));
}
/* Reconnect again immediately. We're done with w->tee */
pjmedia_vid_port_connect(w->vp_cap, w->tee, PJ_FALSE);
/* = Attach stream port to the new capture device = */
@ -2097,11 +2139,6 @@ static pj_status_t call_change_cap_dev(pjsua_call *call,
inc_vid_win(new_wid);
new_w = &pjsua_var.win[new_wid];
/* Connect stream to capturer (via video window tee) */
status = pjmedia_vid_tee_add_dst_port2(new_w->tee, 0, media_port);
if (status != PJ_SUCCESS)
goto on_error;
if (new_w->vp_rend) {
/* Start renderer */
@ -2122,6 +2159,13 @@ static pj_status_t call_change_cap_dev(pjsua_call *call,
goto on_error;
}
/* Connect capturer to stream encoding port (via conf) */
status = pjsua_vid_conf_connect(new_w->cap_slot,
call_med->strm.v.strm_enc_slot,
NULL);
if (status != PJ_SUCCESS)
goto on_error;
/* Finally */
call_med->strm.v.cap_dev = cap_dev;
call_med->strm.v.cap_win_id = new_wid;
@ -2138,16 +2182,14 @@ on_error:
/* Unsubscribe, just in case */
pjmedia_event_unsubscribe(NULL, &call_media_on_event, call_med,
new_w->vp_cap);
/* Disconnect media port from the new capturer */
pjmedia_vid_tee_remove_dst_port(new_w->tee, media_port);
/* Release the new capturer */
dec_vid_win(new_wid);
}
/* Revert back to the old capturer */
pjmedia_vid_port_disconnect(w->vp_cap);
status = pjmedia_vid_tee_add_dst_port2(w->tee, 0, media_port);
pjmedia_vid_port_connect(w->vp_cap, w->tee, PJ_FALSE);
status = pjsua_vid_conf_connect(w->cap_slot,
call_med->strm.v.strm_enc_slot, NULL);
if (status != PJ_SUCCESS)
return status;
@ -2371,6 +2413,203 @@ PJ_DEF(pj_bool_t) pjsua_call_vid_stream_is_running( pjsua_call_id call_id,
return pjmedia_vid_stream_is_running(call_med->strm.v.stream, dir);
}
/*****************************************************************************
* Video conference
*/
/*
* Get current number of active ports in the bridge.
*/
PJ_DEF(unsigned) pjsua_vid_conf_get_active_ports(void)
{
return pjmedia_vid_conf_get_port_count(pjsua_var.vid_conf);
}
/*
* Enumerate all video conference ports.
*/
PJ_DEF(pj_status_t) pjsua_vid_conf_enum_ports( pjsua_conf_port_id id[],
unsigned *count)
{
return pjmedia_vid_conf_enum_ports(pjsua_var.vid_conf,
(unsigned*)id, count);
}
/*
* Get information about the specified video conference port
*/
PJ_DEF(pj_status_t) pjsua_vid_conf_get_port_info(
pjsua_conf_port_id port_id,
pjsua_vid_conf_port_info *info)
{
pjmedia_vid_conf_port_info cinfo;
unsigned i;
pj_status_t status;
status = pjmedia_vid_conf_get_port_info(pjsua_var.vid_conf,
(unsigned)port_id, &cinfo);
if (status != PJ_SUCCESS)
return status;
pj_bzero(info, sizeof(*info));
info->slot_id = port_id;
info->name = cinfo.name;
pjmedia_format_copy(&info->format, &cinfo.format);
/* Build array of listeners */
info->listener_cnt = cinfo.listener_cnt;
for (i=0; i<cinfo.listener_cnt; ++i) {
info->listeners[i] = cinfo.listener_slots[i];
}
/* Build array of transmitters */
info->transmitter_cnt = cinfo.transmitter_cnt;
for (i=0; i<cinfo.transmitter_cnt; ++i) {
info->transmitters[i] = cinfo.transmitter_slots[i];
}
return PJ_SUCCESS;
}
/*
* Add arbitrary video media port to PJSUA's video conference bridge.
*/
PJ_DEF(pj_status_t) pjsua_vid_conf_add_port( pj_pool_t *pool,
pjmedia_port *port,
const void *param,
pjsua_conf_port_id *p_id)
{
pj_status_t status;
PJ_UNUSED_ARG(param);
status = pjmedia_vid_conf_add_port(pjsua_var.vid_conf, pool,
port, NULL, NULL, (unsigned*)p_id);
if (status != PJ_SUCCESS) {
if (p_id)
*p_id = PJSUA_INVALID_ID;
}
return status;
}
/*
* Remove arbitrary slot from the video conference bridge.
*/
PJ_DEF(pj_status_t) pjsua_vid_conf_remove_port(pjsua_conf_port_id id)
{
return pjmedia_vid_conf_remove_port(pjsua_var.vid_conf, (unsigned)id);
}
/*
* Establish unidirectional video flow from souce to sink.
*/
PJ_DEF(pj_status_t) pjsua_vid_conf_connect( pjsua_conf_port_id source,
pjsua_conf_port_id sink,
const void *param)
{
PJ_UNUSED_ARG(param);
return pjmedia_vid_conf_connect_port(pjsua_var.vid_conf, source, sink,
NULL);
}
/*
* Disconnect video flow from the source to destination port.
*/
PJ_DEF(pj_status_t) pjsua_vid_conf_disconnect(pjsua_conf_port_id source,
pjsua_conf_port_id sink)
{
return pjmedia_vid_conf_disconnect_port(pjsua_var.vid_conf, source, sink);
}
/*
* Get the video window associated with the call.
*/
PJ_DEF(pjsua_vid_win_id) pjsua_call_get_vid_win(pjsua_call_id call_id)
{
pjsua_call *call;
pjsua_vid_win_id wid = PJSUA_INVALID_ID;
unsigned i;
PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
PJ_EINVAL);
/* Use PJSUA_LOCK() instead of acquire_call():
* https://trac.pjsip.org/repos/ticket/1371
*/
PJSUA_LOCK();
if (!pjsua_call_is_active(call_id))
goto on_return;
call = &pjsua_var.calls[call_id];
for (i = 0; i < call->med_cnt; ++i) {
if (call->media[i].type == PJMEDIA_TYPE_VIDEO &&
(call->media[i].dir & PJMEDIA_DIR_DECODING))
{
wid = call->media[i].strm.v.rdr_win_id;
break;
}
}
on_return:
PJSUA_UNLOCK();
return wid;
}
/*
* Get the video conference port identification associated with the call.
*/
PJ_DEF(pjsua_conf_port_id) pjsua_call_get_vid_conf_port(
pjsua_call_id call_id,
pjmedia_dir dir)
{
pjsua_call *call;
pjsua_conf_port_id port_id = PJSUA_INVALID_ID;
unsigned i;
PJ_ASSERT_RETURN(call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls,
PJ_EINVAL);
PJ_ASSERT_RETURN(dir==PJMEDIA_DIR_ENCODING || dir==PJMEDIA_DIR_DECODING,
PJ_EINVAL);
/* Use PJSUA_LOCK() instead of acquire_call():
* https://trac.pjsip.org/repos/ticket/1371
*/
PJSUA_LOCK();
if (!pjsua_call_is_active(call_id))
goto on_return;
call = &pjsua_var.calls[call_id];
for (i = 0; i < call->med_cnt; ++i) {
if (call->media[i].type == PJMEDIA_TYPE_VIDEO &&
(call->media[i].dir & dir))
{
port_id = (dir==PJMEDIA_DIR_ENCODING)?
call->media[i].strm.v.strm_enc_slot :
call->media[i].strm.v.strm_dec_slot;
break;
}
}
on_return:
PJSUA_UNLOCK();
return port_id;
}
#endif /* PJSUA_HAS_VIDEO */
#endif /* PJSUA_MEDIA_HAS_PJMEDIA */