付智勇

声网SDK更新版本

正在显示 21 个修改的文件 包含 912 行增加332 行删除
[repoFolder:AgoraRTCEngine, branch:release/1.14.0, commit:006e78c2faf6b8e940b1cf5f77b68f7237fc72b0]
[repoFolder:media_sdk2, branch:release/1.14.0, commit:ada1b263b3ad653865adda54a0adaed027a362b8]
[repoFolder:ServerSDK-Video, branch:release/1.14.0, commit:79be5171ac5731ebb060e5e72512335ddb545ace]
... ...
#pragma once
namespace agora {
namespace recording {
typedef unsigned char uchar_t;
typedef unsigned int uint_t;
typedef unsigned int uid_t;
enum ERROR_CODE_TYPE {
ERR_OK = 0,
//1~1000
ERR_FAILED = 1,
ERR_INVALID_ARGUMENT = 2,
ERR_INTERNAL_FAILED = 3,
};
enum STAT_CODE_TYPE {
STAT_OK = 0,
STAT_ERR_FROM_ENGINE = 1,
STAT_ERR_ARS_JOIN_CHANNEL = 2,
STAT_ERR_CREATE_PROCESS = 3,
STAT_ERR_MIXED_INVALID_VIDEO_PARAM = 4,
STAT_POLL_ERR = 0x8,
STAT_POLL_HANG_UP = 0x10,
STAT_POLL_NVAL = 0x20,
};
enum LEAVE_PATH_CODE {
LEAVE_CODE_INIT = 0,
LEAVE_CODE_SIG = 1<<1,
LEAVE_CODE_NO_USERS = 1<<2,
LEAVE_CODE_TIMER_CATCH = 1<<3,
LEAVE_CODE_CLIENT_LEAVE = 1 << 4,
};
enum WARN_CODE_TYPE {
WARN_NO_AVAILABLE_CHANNEL = 103,
WARN_LOOKUP_CHANNEL_TIMEOUT = 104,
WARN_LOOKUP_CHANNEL_REJECTED = 105,
WARN_OPEN_CHANNEL_TIMEOUT = 106,
WARN_OPEN_CHANNEL_REJECTED = 107,
};
enum CHANNEL_PROFILE_TYPE
{
CHANNEL_PROFILE_COMMUNICATION = 0,
CHANNEL_PROFILE_LIVE_BROADCASTING = 1,
};
enum USER_OFFLINE_REASON_TYPE
{
USER_OFFLINE_QUIT = 0,
USER_OFFLINE_DROPPED = 1,
USER_OFFLINE_BECOME_AUDIENCE = 2,
};
enum REMOTE_VIDEO_STREAM_TYPE
{
REMOTE_VIDEO_STREAM_HIGH = 0,
REMOTE_VIDEO_STREAM_LOW = 1,
};
enum AUDIO_FRAME_TYPE {
AUDIO_FRAME_RAW_PCM = 0,
AUDIO_FRAME_AAC = 1,
};
enum VIDEO_FRAME_TYPE {
VIDEO_FRAME_RAW_YUV = 0,
VIDEO_FRAME_H264 = 1,
};
class AudioPcmFrame {
friend class RecordingEngineImpl;
public:
AudioPcmFrame(uint_t frame_ms, uint_t sample_rates, uint_t samples);
~AudioPcmFrame();
public:
uint_t frame_ms_;
uint_t channels_; // 1
uint_t sample_bits_; // 16
uint_t sample_rates_; // 8k, 16k, 32k
uint_t samples_;
uchar_t *pcmBuf_;
uint_t pcmBufSize_;
private:
std::string buf_; // samples * sample_bits_ / CHAR_BIT * channels_
};
class AudioAacFrame {
public:
explicit AudioAacFrame(uint_t frame_ms);
~AudioAacFrame();
public:
uint_t frame_ms_;
std::string buf_;
};
struct AudioFrame {
AUDIO_FRAME_TYPE type;
union {
AudioPcmFrame *pcm;
AudioAacFrame *aac;
} frame;
};
class VideoYuvFrame {
friend class RecordingEngineImpl;
public:
VideoYuvFrame(uint_t frame_ms, uint_t width, uint_t height, uint_t ystride,
uint_t ustride, uint_t vstride);
~VideoYuvFrame();
uint_t frame_ms_;
uchar_t *ybuf_;
uchar_t *ubuf_;
uchar_t *vbuf_;
uint_t width_;
uint_t height_;
uint_t ystride_;
uint_t ustride_;
uint_t vstride_;
//all
uchar_t *buf_;
uint_t bufSize_;
private:
std::string data_;
};
struct VideoH264Frame {
friend class RecordingEngineImpl;
public:
uint_t frame_ms;
uint_t frame_num;
//all
uchar_t *buf_;
uint_t bufSize_;
private:
std::string payload;
};
struct VideoFrame {
VIDEO_FRAME_TYPE type;
union {
VideoYuvFrame *yuv;
VideoH264Frame *h264;
} frame;
int rotation; // 0, 90, 180, 270
};
typedef struct VideoMixingLayout
{
struct Region {
uid_t uid;
double x;//[0,1]
double y;//[0,1]
double width;//[0,1]
double height;//[0,1]
int zOrder; //optional, [0, 100] //0 (default): bottom most, 100: top most
// Optional
// [0, 1.0] where 0 denotes throughly transparent, 1.0 opaque
double alpha;
int renderMode;//RENDER_MODE_HIDDEN: Crop, RENDER_MODE_FIT: Zoom to fit
Region()
:uid(0)
, x(0)
, y(0)
, width(0)
, height(0)
, zOrder(0)
, alpha(1.0)
, renderMode(1)
{}
};
int canvasWidth;
int canvasHeight;
const char* backgroundColor;//e.g. "#C0C0C0" in RGB
int regionCount;
const Region* regions;
const char* appData;
int appDataLength;
VideoMixingLayout()
:canvasWidth(0)
, canvasHeight(0)
, backgroundColor(NULL)
, regionCount(0)
, regions(NULL)
, appData(NULL)
, appDataLength(0)
{}
} VideoMixingLayout;
typedef struct UserJoinInfos {
const char* recordingDir;
//new attached info add below
UserJoinInfos():
recordingDir(NULL)
{}
}UserJoinInfos;
class IRecordingEngineEventHandler {
public:
virtual ~IRecordingEngineEventHandler() {}
/**
* Callback when an error occurred during the runtime of recording engine
*
*
* @param error Error code
* @param stat_code state code
*
*/
virtual void onError(int error, STAT_CODE_TYPE stat_code) = 0;
/**
* Callback when an warning occurred during the runtime of recording engine
*
*
* @param warn warning code
*
*/
virtual void onWarning(int warn) = 0;
/**
* Callback when the user hase successfully joined the specified channel
*
*
* @param channelID channel ID
* @param uid User ID
*
*/
virtual void onJoinChannelSuccess(const char * channelId, uid_t uid) = 0;
/**
* Callback when recording application successfully left the channel
*
*
* @param code leave path code
*
*/
virtual void onLeaveChannel(LEAVE_PATH_CODE code) = 0;
/**
* Callback when another user successfully joined the channel
*
*
* @param uid user ID
* @param infos user join information
*
*/
virtual void onUserJoined(uid_t uid, UserJoinInfos &infos) = 0;
/**
* Callback when a user left the channel or gone offline
*
*
* @param uid user ID
* @param reason offline reason
*
*/
virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) = 0;
/**
* Callback when received a audio frame
*
*
* @param uid user ID
* @param frame pointer to received audio frame
*
*/
virtual void audioFrameReceived(unsigned int uid, const AudioFrame *frame) const = 0;
/**
* Callback when received a video frame
*
*
* @param uid user ID
* @param frame pointer to received video frame
*
*/
virtual void videoFrameReceived(unsigned int uid, const VideoFrame *frame) const = 0;
};
typedef struct RecordingConfig {
bool isAudioOnly;
bool isMixingEnabled;
bool mixedVideoAudio;
char * mixResolution;
char * decryptionMode;
char * secret;
char * appliteDir;
char * recordFileRootDir;
char * cfgFilePath;
bool decodeAudio;
bool decodeVideo;
int lowUdpPort;
int highUdpPort;
int idleLimitSec;
CHANNEL_PROFILE_TYPE channelProfile;
RecordingConfig(): channelProfile(CHANNEL_PROFILE_COMMUNICATION),
isAudioOnly(false),
isMixingEnabled(false),
mixResolution(NULL),
decryptionMode(NULL),
secret(NULL),
idleLimitSec(300),
appliteDir(NULL),
recordFileRootDir(NULL),
cfgFilePath(NULL),
lowUdpPort(0),
highUdpPort(0),
decodeAudio(false),
decodeVideo(false),
mixedVideoAudio(false)
{}
} RecordingConfig;
typedef struct RecordingEngineProperties {
char* recordingDir;
RecordingEngineProperties(): recordingDir(NULL)
{}
}RecordingEngineProperties;
class IRecordingEngine{
public:
/**
* create a new recording engine instance
*
* @param appId The App ID issued to the application developers by Agora.io.
* @param eventHandler the callback interface
*
* @return a recording engine instance pointer
*/
static IRecordingEngine* createAgoraRecordingEngine(const char * appId, IRecordingEngineEventHandler *eventHandler);
virtual ~IRecordingEngine() {}
/**
* This method lets the recording engine join a channel, and start recording
*
* @param channelKey This parameter is optional if the user uses a static key, or App ID. In this case, pass NULL as the parameter value. More details refer to http://docs-origin.agora.io/en/user_guide/Component_and_Others/Dynamic_Key_User_Guide.html
* @param channelId A string providing the unique channel id for the AgoraRTC session
* @param uid The uid of recording client
* @param config The config of current recording
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int joinChannel(const char * channelKey, const char *channelId, uid_t uid, const RecordingConfig &config) = 0;
/**
* set the layout of video mixing
*
* @param layout layout setting
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int setVideoMixingLayout(const VideoMixingLayout &layout) = 0;
/**
* Stop recording
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int leaveChannel() = 0;
/**
* release recording engine
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int release() = 0;
/**
* Get recording properties
*/
virtual const RecordingEngineProperties* getProperties() = 0;
};
}
}
... ...
... ... @@ -2,6 +2,7 @@
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <cinttypes>
#include <cstdint>
... ... @@ -9,87 +10,138 @@
#include <stdarg.h>
#include <cassert>
#include <syslog.h>
#include "base/atomic.h"
#include "base/mutexer.h"
namespace agora {
namespace base {
enum log_levels {
DEBUG_LOG = LOG_DEBUG, /* 7 debug-level messages */
INFO_LOG = LOG_INFO, /* 6 informational */
NOTICE_LOG = LOG_NOTICE, /* 5 normal but significant condition */
WARN_LOG = LOG_WARNING, /* 4 warning conditions */
ERROR_LOG = LOG_ERR, /* 3 error conditions */
FATAL_LOG = LOG_CRIT, /* 2 critical conditions */
DEBUG_LOG = LOG_DEBUG, /* 7 debug-level messages */
INFO_LOG = LOG_INFO, /* 6 informational */
NOTICE_LOG = LOG_NOTICE, /* 5 normal but significant condition */
WARN_LOG = LOG_WARNING, /* 4 warning conditions */
ERROR_LOG = LOG_ERR, /* 3 error conditions */
FATAL_LOG = LOG_CRIT, /* 2 critical conditions */
};
struct log_config {
static int enabled_level;
static uint64_t dropped_count;
enum log_facility {
CRON_LOG_FCLT = LOG_CRON,
DAEMON_LOG_FCLT = LOG_DAEMON,
FTP_LOG_FCLT = LOG_FTP,
NEWS_LOG_FCLT = LOG_NEWS,
AUTH_LOG_FCLT = LOG_AUTH, /* DEPRECATED */
SYSLOG_LOG_FCLT = LOG_SYSLOG,
USER_LOG_FCLT = LOG_USER,
UUCP_LOG_FCLT = LOG_UUCP,
LOCAL0_LOG_FCLT = LOG_LOCAL0,
LOCAL1_LOG_FCLT = LOG_LOCAL1,
LOCAL2_LOG_FCLT = LOG_LOCAL2,
LOCAL3_LOG_FCLT = LOG_LOCAL3,
LOCAL4_LOG_FCLT = LOG_LOCAL4,
LOCAL5_LOG_FCLT = LOG_LOCAL5,
LOCAL6_LOG_FCLT = LOG_LOCAL6,
LOCAL7_LOG_FCLT = LOG_LOCAL7,
};
static uint32_t drop_cancel;
const static uint32_t DROP_COUNT = 1000;
#define LOG_FACILITY_MASK LOG_FACMASK
static inline void enable_debug(bool enabled) {
if (enabled) {
log_config::enabled_level = DEBUG_LOG;
} else {
log_config::enabled_level = INFO_LOG;
class log_config {
public:
static inline void enable_debug(bool enabled) {
if (enabled) {
log_config::enabled_level = DEBUG_LOG;
} else {
log_config::enabled_level = INFO_LOG;
}
}
}
static bool set_drop_cannel(uint32_t cancel) {
if (cancel > DROP_COUNT) {
drop_cancel = DROP_COUNT;
return false;
static bool set_drop_cannel(uint32_t cancel) {
if (cancel > DROP_COUNT) {
drop_cancel = DROP_COUNT;
return false;
}
drop_cancel = cancel;
return true;
}
drop_cancel = cancel;
return true;
}
static inline bool log_enabled(log_levels level) {
if (level <= enabled_level) {
return true;
}
static inline bool log_enabled(log_levels level) {
if (level <= enabled_level) {
return true;
++dropped_count;
return (dropped_count % DROP_COUNT < drop_cancel);
}
++dropped_count;
return (dropped_count % DROP_COUNT < drop_cancel);
}
/**
* get log_config current facility
*
* @return uint32: get log_config current facility .
*/
static uint32_t getFacility() { return log_config::facility; }
/**
* change log_config Facility per your specific purpose like agora::base::LOCAL5_LOG_FCLT
* Default:USER_LOG_FCLT.
* eg,
* agora::base::log_config::setFacility(agora::base::USER_LOG_FCLT);
*
* @param fac facility setting
*/
static void setFacility(uint32_t fac) { log_config::facility = fac & LOG_FACILITY_MASK; }
static inline void lock(){ logger_mutex.lock(); }
static inline void unlock(){ logger_mutex.unlock(); }
static inline bool trylock(){ return logger_mutex.trylock(); }
private:
static Mutexer logger_mutex;
static int enabled_level;
static uint64_t dropped_count;
static uint32_t drop_cancel;
const static uint32_t DROP_COUNT = 1000;
static uint32_t facility;
};
inline void open_log() {
::openlog(NULL, LOG_PID|LOG_NDELAY, LOG_USER|LOG_DAEMON);
}
inline void log(log_levels level, const char* format, ...) {
if (! log_config::log_enabled(level)) {
return;
}
va_list args;
va_start(args, format);
::vsyslog(level, format, args);
va_end(args);
}
void open_log();
inline void close_log() {
::closelog();
::closelog();
}
void log_dir(const char* logdir, log_levels level, const char* format, ...);
void log(log_levels level, const char* format, ...);
}
}
#define LOG(level, fmt, ...) log(agora::base::level ## _LOG, \
"(%d) %s:%d: " fmt, getpid(), __FILE__, __LINE__, ##__VA_ARGS__)
"(%d) %s:%d: " fmt, getpid(), __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_DIR(logdir, level, fmt, ...) log_dir(logdir, agora::base::level ## _LOG, \
"(%d) %s:%d: " fmt, getpid(), __FILE__, __LINE__, ##__VA_ARGS__)
#define LOG_DIR_IF(logdir, level, cond, fmt, ...) \
if (cond) { \
log_dir(logdir, agora::base::level ## _LOG, \
"(%d) %s:%d: " fmt, getpid(), __FILE__, __LINE__, ##__VA_ARGS__); \
}
#define LOG_IF(level, cond, ...) \
if (cond) { \
LOG(level, __VA_ARGS__); \
}
if (cond) { \
LOG(level, __VA_ARGS__); \
}
#define LOG_EVERY_N(level, N, ...) \
{ \
{ \
static unsigned int count = 0; \
if (++count % N == 0) \
LOG(level, __VA_ARGS__); \
}
LOG(level, __VA_ARGS__); \
}
... ...
#ifndef __MUTEXER_H__
#define __MUTEXER_H__
#if defined(_WIN32)
#include <windows.h>
#else
#include <pthread.h>
#endif
namespace agora {
namespace base {
class Mutexer
{
public:
Mutexer();
virtual ~Mutexer();
virtual void lock();
virtual void unlock();
virtual bool trylock();
private:
#ifdef _WIN32
CRITICAL_SECTION crit;
#else
pthread_mutex_t mutex_;
#endif
};
}
}
#endif//__MUTEXER_H__
... ...
... ... @@ -18,6 +18,20 @@ using std::cerr;
using std::endl;
using agora::base::opt_parser;
using agora::recording::VideoFrame;
using agora::recording::AudioFrame;
struct MixModeSettings {
int m_height;
int m_width;
bool m_videoMix;
MixModeSettings():
m_height(0),
m_width(0),
m_videoMix(false)
{};
};
class AgoraRecorder : public agora::recording::IRecordingEngineEventHandler {
public:
... ... @@ -26,35 +40,47 @@ class AgoraRecorder : public agora::recording::IRecordingEngineEventHandler {
~AgoraRecorder();
bool createChannel(const string &appid, const string &channelKey, const string &name, uid_t uid,
bool decodeAudio, bool decodeVideo, agora::recording::RecordingConfig &config);
agora::recording::RecordingConfig &config);
int setVideoMixLayout();
int setVideoMixLayout();
bool leaveChannel();
bool release();
bool stopped() const;
inline void updateMixModeSetting(int width, int height, bool isVideoMix) {
m_mixRes.m_width = width;
m_mixRes.m_height = height;
m_mixRes.m_videoMix = isVideoMix;
}
const agora::recording::RecordingEngineProperties* getRecorderProperties();
private:
virtual void onError(int error);
virtual void onError(int error, agora::recording::STAT_CODE_TYPE stat_code);
virtual void onWarning(int warn);
virtual void onJoinChannelSuccess(const char * channelId, uid_t uid);
virtual void onLeaveChannel();
virtual void onLeaveChannel(agora::recording::LEAVE_PATH_CODE code);
virtual void onUserJoined(uid_t uid, agora::recording::UserJoinInfos &infos);
virtual void onUserOffline(uid_t uid, agora::recording::USER_OFFLINE_REASON_TYPE reason);
virtual void audioFrameReceived(unsigned int uid, const AudioFrame *frame) const;
virtual void videoFrameReceived(unsigned int uid, const VideoFrame *frame) const;
private:
atomic_bool_t m_stopped;
agora::recording::IRecordingEngine *m_recorder;
std::vector<agora::recording::uid_t> m_peers;
std::string m_recordingDir;
std::string m_logdir;
MixModeSettings m_mixRes;
};
AgoraRecorder::AgoraRecorder(): IRecordingEngineEventHandler() {
m_recorder = NULL;
m_stopped.store(false);
m_recordingDir = "./";
}
AgoraRecorder::~AgoraRecorder() {
... ... @@ -78,7 +104,6 @@ bool AgoraRecorder::release() {
bool AgoraRecorder::createChannel(const string &appid, const string &channelKey, const string &name,
uint32_t uid,
bool decodeAudio, bool decodeVideo,
agora::recording::RecordingConfig &config)
{
if ((m_recorder = agora::recording::IRecordingEngine::createAgoraRecordingEngine(appid.c_str(), this)) == NULL)
... ... @@ -97,19 +122,22 @@ bool AgoraRecorder::leaveChannel() {
return true;
}
//Customize the layout of video under video mixing model
int AgoraRecorder::setVideoMixLayout()
{
LOG(INFO, "setVideoMixLayout: user size: %d", m_peers.size());
if(!m_mixRes.m_videoMix) return 0;
LOG_DIR(m_logdir.c_str(), INFO, "setVideoMixLayout: user size: %d", m_peers.size());
agora::recording::VideoMixingLayout layout;
layout.canvasWidth = 360;
layout.canvasHeight = 640;
layout.canvasWidth = m_mixRes.m_width;
layout.canvasHeight = m_mixRes.m_height;
layout.backgroundColor = "#23b9dc";
layout.regionCount = m_peers.size();
if (!m_peers.empty()) {
LOG(INFO, "setVideoMixLayout: peers not empty");
LOG_DIR(m_logdir.c_str(), INFO, "setVideoMixLayout: peers not empty");
agora::recording::VideoMixingLayout::Region * regionList = new agora::recording::VideoMixingLayout::Region[m_peers.size()];
regionList[0].uid = m_peers[0];
... ... @@ -121,11 +149,11 @@ int AgoraRecorder::setVideoMixLayout()
regionList[0].alpha = 1.f;
regionList[0].renderMode = 0;
LOG(INFO, "region 0 uid: %d, x: %f, y: %f, width: %f, height: %f, zOrder: %d, alpha: %f", regionList[0].uid, regionList[0].x, regionList[0].y, regionList[0].width, regionList[0].height, regionList[0].zOrder, regionList[0].alpha);
LOG_DIR(m_logdir.c_str(), INFO, "region 0 uid: %d, x: %f, y: %f, width: %f, height: %f, zOrder: %d, alpha: %f", regionList[0].uid, regionList[0].x, regionList[0].y, regionList[0].width, regionList[0].height, regionList[0].zOrder, regionList[0].alpha);
float canvasWidth = 360.0;
float canvasHeight = 640.0;
float canvasWidth = m_mixRes.m_width;
float canvasHeight = m_mixRes.m_height;
float viewWidth = 0.3;
float viewHEdge = 0.025;
... ... @@ -150,7 +178,7 @@ int AgoraRecorder::setVideoMixLayout()
}
layout.regions = regionList;
// LOG(INFO, "region 0 uid: %d, x: %f, y: %f, width: %f, height: %f, zOrder: %d, alpha: %f", regionList[0].uid, regionList[0].x, regionList[0].y, regionList[0].width, regionList[0].height, regionList[0].zOrder, regionList[0].alpha);
// LOG_DIR(m_logdir.c_str(), INFO, "region 0 uid: %d, x: %f, y: %f, width: %f, height: %f, zOrder: %d, alpha: %f", regionList[0].uid, regionList[0].x, regionList[0].y, regionList[0].width, regionList[0].height, regionList[0].zOrder, regionList[0].alpha);
}
else {
layout.regions = NULL;
... ... @@ -159,8 +187,12 @@ int AgoraRecorder::setVideoMixLayout()
m_recorder->setVideoMixingLayout(layout);
}
void AgoraRecorder::onError(int error) {
cerr << "Error: " << error << endl;
const agora::recording::RecordingEngineProperties* AgoraRecorder::getRecorderProperties(){
return m_recorder->getProperties();
}
void AgoraRecorder::onError(int error, agora::recording::STAT_CODE_TYPE stat_code) {
cerr << "Error: " << error <<",with stat_code:"<< stat_code << endl;
leaveChannel();
}
... ... @@ -173,24 +205,88 @@ void AgoraRecorder::onJoinChannelSuccess(const char * channelId, uid_t uid) {
cout << "join channel Id: " << channelId << ", with uid: " << uid << endl;
}
void AgoraRecorder::onLeaveChannel() {
cout << "leave channel" << endl;
void AgoraRecorder::onLeaveChannel(agora::recording::LEAVE_PATH_CODE code) {
cout << "leave channel with code:" << code << endl;
}
void AgoraRecorder::onUserJoined(unsigned uid, agora::recording::UserJoinInfos &infos) {
cout << "User " << uid << " joined, RecordingDir:" << (infos.recordingDir? infos.recordingDir:"NULL") <<endl;
m_peers.push_back(uid);
if(infos.recordingDir)
{
m_recordingDir = std::string(infos.recordingDir);
m_logdir = m_recordingDir;
}
m_peers.push_back(uid);
//When the user joined, we can re-layout the canvas
setVideoMixLayout();
}
void AgoraRecorder::onUserOffline(unsigned uid, agora::recording::USER_OFFLINE_REASON_TYPE reason) {
cout << "User " << uid << " offline, reason: " << reason << endl;
m_peers.erase(std::remove(m_peers.begin(), m_peers.end(), uid), m_peers.end());
//When the user is offline, we can re-layout the canvas
setVideoMixLayout();
}
void AgoraRecorder::audioFrameReceived(unsigned int uid, const AudioFrame *pframe) const {
char uidbuf[65];
snprintf(uidbuf, sizeof(uidbuf),"%u", uid);
std::string info_name = m_recordingDir + std::string(uidbuf) /*+ timestamp_per_join_*/ +".pcm";
FILE *fp = fopen(info_name.c_str(), "a+b");
if (pframe->type == agora::recording::AUDIO_FRAME_RAW_PCM) {
cout << "User " << uid << ", received a raw PCM frame" << endl;
//cout << "User " << uid << ", received a raw PCM frame, timestamp: " << f->frame_ms_ << endl;
agora::recording::AudioPcmFrame *f = pframe->frame.pcm;
::fwrite(f->pcmBuf_, 1, f->pcmBufSize_, fp);
} else if (pframe->type == agora::recording::AUDIO_FRAME_AAC) {
cout << "User " << uid << ", received an AAC frame" << endl;
}
::fclose(fp);
}
void AgoraRecorder::videoFrameReceived(unsigned int uid, const VideoFrame *pframe) const {
char uidbuf[65];
snprintf(uidbuf, sizeof(uidbuf),"%u", uid);
char * suffix=".vtmp";
if (pframe->type == agora::recording::VIDEO_FRAME_RAW_YUV) {
agora::recording::VideoYuvFrame *f = pframe->frame.yuv;
suffix=".yuv";
cout << "User " << uid << ", received a yuv frame, width: "
<< f->width_ << ", height: " << f->height_ ;
cout<<",ystide:"<<f->ystride_<< ",ustride:"<<f->ustride_<<",vstride:"<<f->vstride_;
cout<< endl;
} else {
suffix=".h264";
agora::recording::VideoH264Frame *f = pframe->frame.h264;
cout << "User " << uid << ", received an h264 frame, timestamp: "
<< f->frame_ms << ", frame no: " << f->frame_num << endl;
}
std::string info_name = m_recordingDir + std::string(uidbuf) /*+ timestamp_per_join_ */+ std::string(suffix);
FILE *fp = fopen(info_name.c_str(), "a+b");
//store it as file
if (pframe->type == agora::recording::VIDEO_FRAME_RAW_YUV) {
agora::recording::VideoYuvFrame *f = pframe->frame.yuv;
::fwrite(f->buf_, 1, f->bufSize_, fp);
}
else {
agora::recording::VideoH264Frame *f = pframe->frame.h264;
::fwrite(f->buf_, 1, f->bufSize_, fp);
}
::fclose(fp);
}
atomic_bool_t g_bSignalStop;
void signal_handler(int signo) {
... ... @@ -205,27 +301,38 @@ int main(int argc, char * const argv[]) {
string appId;
string channelKey;
string name;
bool decodeAudio = false;
bool decodeVideo = false;
uint32_t channelProfile;
uint32_t channelProfile = 0;
string decryptionMode;
string secret;
string mixResolution("360,640,15,500");
int idleLimitSec=30*60;//30min
int idleLimitSec=5*60;//300s
string applitePath;
string appliteLogPath;
string recordFileRootDir=".";
string recordFileRootDir = "";
string cfgFilePath = "";
int lowUdpPort = 0;//40000;
int highUdpPort = 0;//40004;
bool isAudioOnly=0;
bool isMixingEnabled=0;
bool mixedVideoAudio=0;
bool getAudioFrame = false;
bool getVideoFrame = false;
int width = 0;
int height = 0;
int fps = 0;
int kbps = 0;
/**
* change log_config Facility per your specific purpose like agora::base::LOCAL5_LOG_FCLT
* Default:USER_LOG_FCLT.
* agora::base::log_config::setFacility(agora::base::LOCAL5_LOG_FCLT);
*/
g_bSignalStop = false;
signal(SIGQUIT, signal_handler);
signal(SIGABRT, signal_handler);
... ... @@ -234,8 +341,6 @@ int main(int argc, char * const argv[]) {
opt_parser parser;
parser.add_long_opt("appId", &appId, "App Id/must", agora::base::opt_parser::require_argu);
parser.add_long_opt("uid", &uid, "User Id default is 0/must", agora::base::opt_parser::require_argu);
... ... @@ -248,20 +353,25 @@ int main(int argc, char * const argv[]) {
parser.add_long_opt("isAudioOnly", &isAudioOnly, "Default 0:ARS (0:1)/option");
parser.add_long_opt("isMixingEnabled", &isMixingEnabled, "Mixing Enable? (0:1)/option");
parser.add_long_opt("mixResolution", &mixResolution, "change default resolution for vdieo mix mode/option");
parser.add_long_opt("mixedVideoAudio", &mixedVideoAudio, "mixVideoAudio:(0:seperated Audio,Video) (1:mixed Audio & Video), default is 0 /option");
parser.add_long_opt("decryptionMode", &decryptionMode, "decryption Mode, default is NULL/option");
parser.add_long_opt("secret", &secret, "input secret when enable decryptionMode/option");
parser.add_long_opt("idle", &idleLimitSec, "Default 300s/option");
parser.add_long_opt("idle", &idleLimitSec, "Default 300s, should be above 3s/option");
parser.add_long_opt("recordFileRootDir", &recordFileRootDir, "recording file root dir/option");
parser.add_long_opt("lowUdpPort", &lowUdpPort, "default is random value/option");
parser.add_long_opt("highUdpPort", &highUdpPort, "default is random value/option");
parser.add_long_opt("getAudioFrame", &getAudioFrame, "default 0 (0:save as file, 1:get framebuffer) /option");
parser.add_long_opt("getVideoFrame", &getVideoFrame, "default 0 (0:save as file, 1:get framebuffer) /option");
parser.add_long_opt("cfgFilePath", &cfgFilePath, "config file path / option");
if (!parser.parse_opts(argc, argv) || appId.empty() || name.empty()) {
std::string usage = "Usage: \n\
./RECORD_APP --appId STRING --uid UINTEGER32 --channel STRING --appliteDir STRING --channelKey STRING --channelProfile UINTEGER32 --isAudioOnly 0/1 --isMixingEnabled 0/1 --decryptionMode STRING --secret STRING --idle INTEGER32 --recordFileRootDir STRING --lowUdpPort INTEGER32 --highUdpPort INTEGER32\n \
./RECORD_APP --appId STRING --uid UINTEGER32 --channel STRING --appliteDir STRING --channelKey STRING --channelProfile UINTEGER32 --isAudioOnly 0/1 --isMixingEnabled 0/1 --decryptionMode STRING --secret STRING --idle INTEGER32 --recordFileRootDir STRING --cfgFilePath STRING --lowUdpPort INTEGER32 --highUdpPort INTEGER32\n \
--appId (App Id/must) \n \
--uid (User Id default is 0/must) \n \
--channel (Channel Id/must) \n \
... ... @@ -270,16 +380,39 @@ int main(int argc, char * const argv[]) {
--channelProfile (channel_profile:(0:COMMUNICATION),(1:broadcast) default is 0/option) \n \
--isAudioOnly (Default 0:ARS (0:1)/option) \n \
--isMixingEnabled (Mixing Enable? (0:1)/option) \n \
--mixResolution (format:width,high,fps,kbps 'change default resolution for video mix mode/option') \n \
--mixedVideoAudio (0:seperated Audio,Video) (1:mixed Audio & Video), default is 0, only for mix mode \n \
--decryptionMode (decryption Mode, default is NULL/option) \n \
--secret (input secret when enable decryptionMode/option) \n \
--idle (Default 300s/option) \n \
--idle (Default 300s, should be above 3s/option) \n \
--recordFileRootDir (recording file root dir/option) \n \
--lowUdpPort (default is random value/option) \n \
--highUdpPort (default is random value/option)";
--highUdpPort (default is random value/option) \n \
--getAudioFrame (default 0 (0:save as file, 1:get framebuffer) /option) \n \
--getVideoFrame (default 0 (0:save as file, 1:get framebuffer) /option)\n \
--cfgFilePath (config file path/option_)";
std::cerr << usage << std::endl;
return -1;
}
if(!recordFileRootDir.empty() && !cfgFilePath.empty()){
LOG(ERROR,"Client can't set both recordFileRootDir and cfgFilePath");
return -1;
}
if(recordFileRootDir.empty() && cfgFilePath.empty())
recordFileRootDir = ".";
//Once recording video under video mixing model, client needs to config width, height, fps and kbps
if(isMixingEnabled && !isAudioOnly) {
if(4 != sscanf(mixResolution.c_str(), "%d,%d,%d,%d", &width,
&height, &fps, &kbps)) {
LOG(ERROR, "Illegal resolution: %s", mixResolution.c_str());
return -1;
}
}
LOG(INFO, "uid %" PRIu32 " from vendor %s is joining channel %s",
uid, appId.c_str(), name.c_str());
... ... @@ -291,9 +424,12 @@ int main(int argc, char * const argv[]) {
config.isAudioOnly = isAudioOnly;
config.isMixingEnabled = isMixingEnabled;
config.mixResolution = (isMixingEnabled && !isAudioOnly)? const_cast<char*>(mixResolution.c_str()):NULL;
config.mixedVideoAudio = mixedVideoAudio;
config.appliteDir = const_cast<char*>(applitePath.c_str());
config.recordFileRootDir = const_cast<char*>(recordFileRootDir.c_str());
config.cfgFilePath = const_cast<char*>(cfgFilePath.c_str());
config.secret = secret.empty()? NULL:const_cast<char*>(secret.c_str());
config.decryptionMode = decryptionMode.empty()? NULL:const_cast<char*>(decryptionMode.c_str());
... ... @@ -301,12 +437,17 @@ int main(int argc, char * const argv[]) {
config.lowUdpPort = lowUdpPort;
config.highUdpPort = highUdpPort;
config.decodeAudio = getAudioFrame;
config.decodeVideo = getVideoFrame;
recorder.updateMixModeSetting(width, height, isMixingEnabled ? !isAudioOnly:false);
if (!recorder.createChannel(appId, channelKey, name, uid, decodeAudio, decodeVideo, config)) {
if (!recorder.createChannel(appId, channelKey, name, uid, config)) {
cerr << "Failed to create agora channel: " << name << endl;
return -1;
}
cout << "Recording directory is " << recorder.getRecorderProperties()->recordingDir << endl;
while (!recorder.stopped() && !g_bSignalStop) {
sleep(1);
}
... ...
const callfile = require('child_process');
const recordInfo =require( '../../../model/RecordInfoModel');
const recordStatus =require( '../../../model/RecordStatusModel')
const uuid = require('../../UuidUtil');
const moment = require('moment')
function shell(){
}
shell.prototype.Recording =async(appId,uid,channel,channelKey,body)=>{
// return new Promise((resolve, reject) =>{
try {
body.id = uuid.db32()
body.createTime = new Date().getTime();
var channelInfo = await recordStatus.findOne({where:{channel:channel}});
let r = await recordInfo.create(body);
if(channelInfo){
if(channelInfo.status == 0){
var channelInfo = await recordStatus.update({status:1},{where:{channel:channel}});
}else if(channelInfo.status == 1){
throw false ;
}
}else{
var channelInfo = await recordStatus.create({id:uuid.db32(),channel:channel,status:1});
}
let shell =`cd /netWorkSchool/Agora_Recording_SDK_for_Linux_FULL/samples/ && ./Recorder_local`+
` --appId ` +appId +
//` --uid ` +uid +
` --channel ` + channel +
//` --channelKey ` + channelKey +
" --appliteDir \`pwd\`/../bin";
console.log(shell)
await callfile.exec(shell)
return true
} catch (error) {
throw error
}
// })
}
module.exports = new shell();
\ No newline at end of file
... ...
... ... @@ -17,6 +17,10 @@ class AudioClip:
self.start_time = []
self.end_time = []
def update_audio_info(self, i, stime, etime):
self.start_time[i] = stime
self.end_time[i] = etime
def put_file(self, name):
if not (name in self.filename):
self.filename.append(name)
... ... @@ -31,27 +35,24 @@ class AudioClip:
def print_filename(self):
str = ""
for i in range(self.num):
if i > 0:
len = self.start_time[i] - self.end_time[i-1]
else:
len = self.start_time[0]
if len < 0.001:
len = 0.001
str = str + ("-f lavfi -t %.3f -i anullsrc=channel_layout=mono:sample_rate=48000 " % len)
str = str + ("-i %s " % self.filename[i])
return str
def print_filter(self):
str = ""
allch = ""
for i in range(self.num):
tmp = "[%d]adelay=%d[ad%d];" % ( (i), int(self.start_time[i]*1000)+1, (i))
allch = allch + ("[ad%d]" % i)
str = str + tmp
str = str + ("%s amix=inputs=%d:dropout_transition=0.5[audio]" % (allch, self.num))
return str
def print_audio_info(self, i):
print "Audio Clip %d: %s: start_time=%.3f, end_time=%.3f" % (i, self.filename[i], self.start_time[i], self.end_time[i])
def print_ffmpeg(self, output_file):
if self.num > 1:
str = "ffmpeg " + self.print_filename()
str = str + "-filter_complex \"%s\" " % self.print_filter()
str = str + "-map \"[audio]\" -to %f -y %s" % (self.max_length(), output_file)
str = str + "-filter_complex \"concat=n=%d:v=0:a=1[audio]\" " % (self.num * 2)
str = str + " -map \"[audio]\" -to %f -y %s" % (self.max_length(), output_file)
elif self.num == 1:
str = "ffmpeg -i %s -c:a copy %s" % (self.filename[0], output_file)
else:
... ... @@ -70,6 +71,14 @@ class VideoClip:
self.audio_start_time = 0.0
self.audio_end_time = 0.0
def update_audio_info(self, audio_stime, audio_etime):
self.audio_start_time = audio_stime
self.audio_end_time = audio_etime
def update_video_info(self, i, video_stime, video_etime):
self.start_time[i] = video_stime
self.end_time[i] = video_etime
def put_file(self, name):
if not (name in self.filename):
self.filename.append(name)
... ... @@ -130,7 +139,7 @@ class VideoClip:
map_option = "-map \"[audio]\" -map \"[video]\" -c:a aac"
else:
map_option = "-map 0:a:0 -map \"[video]\" -c:a copy"
str = str + "%s -c:v libx264 -preset veryfast -to %f -y %s" % (map_option, self.max_length(), output_file)
str = str + " %s -c:v libx264 -preset veryfast -to %f -y %s" % (map_option, self.max_length(), output_file)
return str
def print_audio_info(self):
... ... @@ -141,42 +150,37 @@ class VideoClip:
(i, self.filename[i], self.start_time[i], self.end_time[i], self.width[i], self.height[i])
if len(sys.argv) <= 1:
print "Usage: python video_convert.py path_of_folder"
quit()
def UidFileConvert(uid_file, suffix, option):
child_env = os.environ.copy()
folder_name = sys.argv[1]
print "Folder name:"+folder_name
if not os.path.isdir(folder_name):
print "Folder "+folder_name+" does not exit"
quit()
os.chdir(folder_name)
child_env = os.environ.copy()
all_uid_file = glob.glob("uid_*.txt")
for uid_file in all_uid_file:
uid = os.path.splitext(uid_file)[0][4:]
print "UID:"+uid
clip = VideoClip()
audio_clip = AudioClip()
with open(uid_file) as f:
av_1st_stime = [False]
for line in f:
items = line.split(" ")
#audio file
if items[1][-3:] == "aac":
index = audio_clip.put_file(items[1])
if items[2] == "create":
audio_clip.start_time[index] = float(items[0])
if not av_1st_stime[0]:
av_1st_stime.append(float(items[0])) #mark it.
audio_clip.start_time[index] = float(items[0])
elif items[2] == "close":
audio_clip.end_time[index] = float(items[0])
audio_clip.end_time[index] = float(items[0])
#video file
if items[1][-3:] == "mp4":
index = clip.put_file(items[1])
if items[2] == "create":
clip.start_time[index] = float(items[0])
if not av_1st_stime[0]:
av_1st_stime.append(float(items[0])) #mark it.
clip.start_time[index] = float(items[0])
elif items[2] == "info":
clip.start_time[index] = float(items[0])
clip.width[index] = int(items[3][6:])
... ... @@ -190,24 +194,37 @@ for uid_file in all_uid_file:
if items[1][-4:] == "webm":
index = clip.put_file(items[1])
if items[2] == "create":
clip.start_time[index] = float(items[0])
if not av_1st_stime[0]:
av_1st_stime.append(float(items[0])) #mark it.
clip.start_time[index] = float(items[0])
elif items[2] == "info":
clip.start_time[index] = float(items[0])
clip.width[index] = int(items[3][6:])
clip.height[index] = int(items[4][7:])
rotation = int(items[5][9:])
if rotation == 90 or rotation == 270:
clip.width[index], clip.height[index] = clip.height[index], clip.width[index]
clip.start_time[index] = float(items[0])
clip.width[index] = int(items[3][6:])
clip.height[index] = int(items[4][7:])
rotation = int(items[5][9:])
if rotation == 90 or rotation == 270:
clip.width[index], clip.height[index] = clip.height[index], clip.width[index]
elif items[2] == "close":
clip.end_time[index] = float(items[0])
if not option:
clip.update_audio_info(clip.audio_start_time - av_1st_stime[1],
clip.audio_end_time - av_1st_stime[1])
for i in range(audio_clip.num):
audio_clip.update_audio_info(i, audio_clip.start_time[i] - av_1st_stime[1],
audio_clip.end_time[i] - av_1st_stime[1])
for i in range(clip.num):
clip.update_video_info(i, clip.start_time[i] - av_1st_stime[1],
clip.end_time[i] - av_1st_stime[1])
clip.print_audio_info()
for i in range(audio_clip.num):
audio_clip.print_audio_info(i)
for i in range(clip.num):
clip.print_video_info(i)
if audio_clip.num > 1:
print "Generate Audio File"
tmp_audio = uid+"_tmp.m4a"
... ... @@ -225,7 +242,7 @@ for uid_file in all_uid_file:
if clip.num > 0:
print "Generate MP4 file:"
print "Output resolution:", clip.max_resolution()
output_file = uid+"_av"+".mp4"
output_file = uid + suffix + ".mp4"
#print clip.print_filter()
command = clip.print_ffmpeg(output_file)
else:
... ... @@ -239,10 +256,92 @@ for uid_file in all_uid_file:
print subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, env=child_env).stdout.read()
print "\n\n"
#write a convert done file
f = open("convert-done.txt", "w+")
f.close()
#remove tmp files
os.system('rm -f *_tmp.m4a')
return
class UserInfo:
def __init__(self, ts, path):
self.last_ts = ts
self.path = path
def SessionConvert(folder_name, opt):
if not os.path.isdir(folder_name):
print "Folder " + folder_name + " does not exit"
return
os.chdir(folder_name)
os.system('rm -f *_merge.txt')
all_uid_file = sorted(glob.glob("uid_*.txt"))
if opt == 0:
for uid_file in all_uid_file:
UidFileConvert(uid_file, "_av", 0)
#write a convert done file
f = open("convert-done.txt", "w")
f.close()
return
#merge
dict_uid = dict()
for uid_file in all_uid_file:
uid = uid_file.split("_")[1]
if not dict_uid.has_key(uid):
dict_uid[uid] = UserInfo(0.0, "uid_" + uid + "_merge.txt")
#remove tmp files
os.system('rm -f *_tmp.m4a')
start_ts = -1.0
last_ts = 0
with open(uid_file) as f:
for line in f:
items = line.split(" ")
if opt == 2 and items[1].split(".")[1] != "aac":
continue
elif opt == 3 and items[1].split(".")[1] == "aac":
continue
if start_ts < 0:
if float(items[0]) > 0:
start_ts = float(items[0])
else:
start_ts = 0
items[0] = "%.3f" % (float(items[0]))
last_ts = float(items[0])
file = open(dict_uid[uid].path, "a")
file.write(' '.join(items))
file.close()
dict_uid[uid].last_ts = last_ts + 0.1
all_merge_file = glob.glob("*_merge.txt")
for merge_file in all_merge_file:
if opt == 1:
UidFileConvert(merge_file, "_av", opt)
else:
UidFileConvert(merge_file, "", opt)
os.system('rm -f *_merge.txt')
#write a convert done file
f = open("convert-done.txt", "w")
f.close()
return
def Usage():
print "Usage: python video_convert.py session_folder [-m/-am/-vm]"
quit()
#Entrance
if len(sys.argv) <= 1:
Usage()
folder_name = sys.argv[1]
if len(sys.argv) == 2:
SessionConvert(folder_name, 0)
elif sys.argv[2] == "-m": # merge audio and video
SessionConvert(folder_name, 1)
elif sys.argv[2] == "-am": # merge audio
SessionConvert(folder_name, 2)
elif sys.argv[2] == "-vm": #merge video
SessionConvert(folder_name, 3)
else:
Usage()
... ...
[repoFolder:AgoraRTCEngine, branch:release/1.12.0, commit:89171d96ae055f7809ad9b455256272eee0a0b43]
[repoFolder:media_sdk2, branch:release/1.12.0, commit:a442f9afe9b468f3f14930db4f3c01f73de90ea3]
[repoFolder:ServerSDK-Video, branch:release/1.12.0, commit:a4f0d4c22aa9d93b9cd0f71967d4f4b763e46190]
#pragma once
namespace agora {
namespace recording {
typedef unsigned char uchar_t;
typedef unsigned int uint_t;
typedef unsigned int uid_t;
enum ERROR_CODE_TYPE {
ERR_OK = 0,
//1~1000
ERR_FAILED = 1,
ERR_INVALID_ARGUMENT = 2,
ERR_INTERNAL_FAILED = 3,
};
enum WARN_CODE_TYPE {
WARN_NO_AVAILABLE_CHANNEL = 103,
WARN_LOOKUP_CHANNEL_TIMEOUT = 104,
WARN_LOOKUP_CHANNEL_REJECTED = 105,
WARN_OPEN_CHANNEL_TIMEOUT = 106,
WARN_OPEN_CHANNEL_REJECTED = 107,
};
enum CHANNEL_PROFILE_TYPE
{
CHANNEL_PROFILE_COMMUNICATION = 0,
CHANNEL_PROFILE_LIVE_BROADCASTING = 1,
};
enum USER_OFFLINE_REASON_TYPE
{
USER_OFFLINE_QUIT = 0,
USER_OFFLINE_DROPPED = 1,
USER_OFFLINE_BECOME_AUDIENCE = 2,
};
enum REMOTE_VIDEO_STREAM_TYPE
{
REMOTE_VIDEO_STREAM_HIGH = 0,
REMOTE_VIDEO_STREAM_LOW = 1,
};
typedef struct VideoMixingLayout
{
struct Region {
uid_t uid;
double x;//[0,1]
double y;//[0,1]
double width;//[0,1]
double height;//[0,1]
int zOrder; //optional, [0, 100] //0 (default): bottom most, 100: top most
// Optional
// [0, 1.0] where 0 denotes throughly transparent, 1.0 opaque
double alpha;
int renderMode;//RENDER_MODE_HIDDEN: Crop, RENDER_MODE_FIT: Zoom to fit
Region()
:uid(0)
, x(0)
, y(0)
, width(0)
, height(0)
, zOrder(0)
, alpha(1.0)
, renderMode(1)
{}
};
int canvasWidth;
int canvasHeight;
const char* backgroundColor;//e.g. "#C0C0C0" in RGB
int regionCount;
const Region* regions;
const char* appData;
int appDataLength;
VideoMixingLayout()
:canvasWidth(0)
, canvasHeight(0)
, backgroundColor(NULL)
, regionCount(0)
, regions(NULL)
, appData(NULL)
, appDataLength(0)
{}
} VideoMixingLayout;
typedef struct UserJoinInfos {
const char* recordingDir;
//new attached info add below
UserJoinInfos():
recordingDir(NULL)
{}
}UserJoinInfos;
class IRecordingEngineEventHandler {
public:
virtual ~IRecordingEngineEventHandler() {}
virtual void onError(int error) = 0;
virtual void onWarning(int warn) = 0;
virtual void onJoinChannelSuccess(const char * channelId, uid_t uid) = 0;
virtual void onLeaveChannel() = 0;
virtual void onUserJoined(uid_t uid, UserJoinInfos &infos) = 0;
virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) = 0;
};
typedef struct RecordingConfig {
CHANNEL_PROFILE_TYPE channelProfile;
bool isAudioOnly;
bool isMixingEnabled;
char * decryptionMode;
char * secret;
int idleLimitSec;
char * appliteDir;
// char * appliteLogDir;
char * recordFileRootDir;
int lowUdpPort;
int highUdpPort;
RecordingConfig(): channelProfile(CHANNEL_PROFILE_COMMUNICATION),
isAudioOnly(false),
isMixingEnabled(false),
decryptionMode(NULL),
secret(NULL),
idleLimitSec(300),
appliteDir(NULL),
// appliteLogDir(NULL),
recordFileRootDir(NULL),
lowUdpPort(0),
highUdpPort(0)
{}
} RecordingConfig;
class IRecordingEngine{
public:
/**
* create a new recording engine instance
*
* @param appId The App ID issued to the application developers by Agora.io.
* @param eventHandler the callback interface
*
* @return a recording engine instance pointer
*/
static IRecordingEngine* createAgoraRecordingEngine(const char * appId, IRecordingEngineEventHandler *eventHandler);
virtual ~IRecordingEngine() {}
/**
* This method lets the recording engine join a channel, and start recording
*
* @param channelKey This parameter is optional if the user uses a static key, or App ID. In this case, pass NULL as the parameter value. More details refer to http://docs-origin.agora.io/en/user_guide/Component_and_Others/Dynamic_Key_User_Guide.html
* @param channelId A string providing the unique channel id for the AgoraRTC session
* @param uid The uid of recording client
* @param config The config of current recording
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int joinChannel(const char * channelKey, const char *channelId, uid_t uid, const RecordingConfig &config) = 0;
/**
* set the layout of video mixing
*
* @param layout layout setting
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int setVideoMixingLayout(const VideoMixingLayout &layout) = 0;
/**
* Stop recording
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int leaveChannel() = 0;
/**
* release recording engine
*
* @return 0: Method call succeeded. <0: Method call failed.
*/
virtual int release() = 0;
};
}
}