janus-gateway的videoroom插件的RTP包录制功能源码详解

2024-02-25 15:44

本文主要是介绍janus-gateway的videoroom插件的RTP包录制功能源码详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引:

janus-gateway在配置文件设置后,可以实现对videoroom插件的每个publisher的音频,视频,数据的RTP流录制成mjr文件。

对于音频,视频的mjr文件,可以使用自带的postprocessing工具janus-pp-rec转成mp4文件。

每个publisher音频和视频mjr文件是分立的两个文件,需要使用ffmpeg将两个合成一个mp4文件。

janus-gateway的原生代码中的录制功能是通过配置文件实现,只能配置成要么录,要么不录。如果要通过客户端的信令进行可控的频繁开关,则需要修改源码实现。

如果要对videoroom的publisher的RTP流转成RTMP流推送出去,可以使用第三方的enhanced-videoroom插件实现。

一、配置文件的录制参数设置

etc/janus/janus.plugin.videoroom.jcfg
房间中和录制相关的参数

# room-<unique room ID>: {
# description = This is my awesome room
...
# record = true|false (whether this room should be recorded, default=false)
# rec_dir = <folder where recordings should be stored, when enabled>
# lock_record = true|false (whether recording can only be started/stopped if the secret
#            is provided, or using the global enable_recording request, default=false)
#}
配置实例
room-1234: {description = "Demo Room"secret = "adminpwd"publishers = 6bitrate = 128000fir_freq = 10audiocodec = "opus"videocodec = "h264"record = truerec_dir = "/data/PJT-janus/record-samples"
}

二、录制初始化

当客户端为发布者,且发送的message为"configure"类型时,
将初始化录制, 将初始化音频、视频和数据文件的存储路径、文件名后,
打开文件以获得文件句柄后,写入文件头。

// janus_videoroom.c
static json_t *janus_videoroom_process_synchronous_request(janus_videoroom_session *session, json_t *message) {if(!strcasecmp(request_text, "create")) {/* Create a new VideoRoom *//* Added by Hank, For recording: */// if(rec_dir) {// videoroom->rec_dir = g_strdup(json_string_value(rec_dir));if (g_record_root_path != NULL) {videoroom->rec_dir = g_strdup(g_record_root_path);// 修改文件存储路径,在原有的录制根目录下,添加 /年月日/房间号/char new_rec_dir_arr[255] = {0};time_t timestamp = time(NULL); struct tm *local_time = localtime(&timestamp);char formatted_date[11]={0};strftime(formatted_date,sizeof(formatted_date), "%Y%m%d",local_time);g_snprintf(new_rec_dir_arr, 255, "%s/%s/%s/",videoroom->rec_dir, formatted_date, videoroom->room_id_str);char *old_rec_dir = videoroom->rec_dir;char *new_rec_dir = g_strdup(new_rec_dir_arr);videoroom->rec_dir = new_rec_dir;g_free(old_rec_dir);		/* END-OF-Hank */}		}
}
/* Thread to handle incoming messages * 当有房间“configure"消息时,* 进行本房间的发布者对应的视频、音频、数据录制文件创建*/
static void *janus_videoroom_handler(void *data) {while(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {msg = g_async_queue_pop(messages);janus_videoroom *videoroom = NULL;janus_videoroom_publisher *participant = NULL;janus_videoroom_subscriber *subscriber = NULL;janus_mutex_lock(&sessions_mutex);janus_videoroom_session *session = janus_videoroom_lookup_session(msg->handle);janus_mutex_unlock(&sessions_mutex);if(session->participant_type == janus_videoroom_p_type_none) {...} else if(session->participant_type == janus_videoroom_p_type_publisher) {/* 当 request_text = "configure" 时 */json_t *request = json_object_get(root, "request");const char *request_text = json_string_value(request);if(!strcasecmp(request_text, "join") || !strcasecmp(request_text, "joinandconfigure")) {...} else if(!strcasecmp(request_text, "configure") || !strcasecmp(request_text, "publish")) {/* 录制相关配置,并创建本publisher的Video/Audio/Data录制文件  */gboolean record_locked = FALSE;if((record || recfile) && participant->room->lock_record && participant->room->room_secret) {JANUS_CHECK_SECRET(participant->room->room_secret, root, "secret", error_code, error_cause,JANUS_VIDEOROOM_ERROR_MISSING_ELEMENT, JANUS_VIDEOROOM_ERROR_INVALID_ELEMENT, JANUS_VIDEOROOM_ERROR_UNAUTHORIZED);if(error_code != 0) {/* Wrong secret provided, we'll prevent the recording state from being changed */record_locked = TRUE;}}janus_mutex_lock(&participant->rec_mutex);gboolean prev_recording_active = participant->recording_active;if(record && !record_locked) {participant->recording_active = json_is_true(record);JANUS_LOG(LOG_VERB, "Setting record property: %s (room %s, user %s)\n",participant->recording_active ? "true" : "false", participant->room_id_str, participant->user_id_str);}if(recfile && !record_locked) {participant->recording_base = g_strdup(json_string_value(recfile));JANUS_LOG(LOG_VERB, "Setting recording basename: %s (room %s, user %s)\n",participant->recording_base, participant->room_id_str, participant->user_id_str);}/* Do we need to do something with the recordings right now? */if(participant->recording_active != prev_recording_active) {/* Something changed */if(!participant->recording_active) {/* Not recording (anymore?) */janus_videoroom_recorder_close(participant);} else if(participant->recording_active && g_atomic_int_get(&participant->session->started)) {/* We've started recording, send a PLI/FIR and go on */GList *temp = participant->streams;while(temp) {janus_videoroom_publisher_stream *ps = (janus_videoroom_publisher_stream *)temp->data;janus_videoroom_recorder_create(participant, participant->audio, participant->video, participant->data); }janus_mutex_unlock(&participant->rec_mutex);。。。}}janus_videoroom_message_free(msg);continue;}}} // end of while(g_atomic_int_get(&initialized) ...)return NULL;
}
/**********  创建本发布者对应的音频、视频、数据录制文件 *******************/
static void janus_videoroom_recorder_create(janus_videoroom_publisher *participant, gboolean audio, gboolean video, gboolean data) {char filename[255];janus_recorder *rc = NULL;gint64 now = janus_get_real_time();// 设置音频文件的存储路径和文件名if(audio && participant->arc == NULL) {memset(filename, 0, 255);if(participant->recording_base) {/* Use the filename and path we have been provided */g_snprintf(filename, 255, "%s-audio", participant->recording_base);rc = janus_recorder_create(participant->room->rec_dir,janus_audiocodec_name(participant->acodec), filename);if(rc == NULL) {JANUS_LOG(LOG_ERR, "Couldn't open an audio recording file for this publisher!\n");}} else {/* Build a filename */g_snprintf(filename, 255, "videoroom-%s-user-%s-%"SCNi64"-audio",participant->room_id_str, participant->user_id_str, now);rc = janus_recorder_create(participant->room->rec_dir,janus_audiocodec_name(participant->acodec), filename);if(rc == NULL) {JANUS_LOG(LOG_ERR, "Couldn't open an audio recording file for this publisher!\n");}}/* If media is encrypted, mark it in the recording */if(participant->e2ee)janus_recorder_encrypted(rc);participant->arc = rc;}// 设置视频文件的存储路径和文件名if(video && participant->vrc == NULL) {janus_rtp_switching_context_reset(&participant->rec_ctx);janus_rtp_simulcasting_context_reset(&participant->rec_simctx);participant->rec_simctx.substream_target = 2;participant->rec_simctx.templayer_target = 2;memset(filename, 0, 255);if(participant->recording_base) {/* Use the filename and path we have been provided */g_snprintf(filename, 255, "%s-video", participant->recording_base);rc = janus_recorder_create_full(participant->room->rec_dir,janus_videocodec_name(participant->vcodec), participant->vfmtp, filename);if(rc == NULL) {JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this publisher!\n");}} else {/* Build a filename */g_snprintf(filename, 255, "videoroom-%s-user-%s-%"SCNi64"-video",participant->room_id_str, participant->user_id_str, now);rc = janus_recorder_create_full(participant->room->rec_dir,janus_videocodec_name(participant->vcodec), participant->vfmtp, filename);if(rc == NULL) {JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this publisher!\n");}}/* If media is encrypted, mark it in the recording */if(participant->e2ee)janus_recorder_encrypted(rc);participant->vrc = rc;}// 设置数据文件的存储路径和文件名if(data && participant->drc == NULL) {memset(filename, 0, 255);if(participant->recording_base) {/* Use the filename and path we have been provided */g_snprintf(filename, 255, "%s-data", participant->recording_base);rc = janus_recorder_create(participant->room->rec_dir,"text", filename);if(rc == NULL) {JANUS_LOG(LOG_ERR, "Couldn't open an data recording file for this publisher!\n");}} else {/* Build a filename */g_snprintf(filename, 255, "videoroom-%s-user-%s-%"SCNi64"-data",participant->room_id_str, participant->user_id_str, now);rc = janus_recorder_create(participant->room->rec_dir,"text", filename);if(rc == NULL) {JANUS_LOG(LOG_ERR, "Couldn't open an data recording file for this publisher!\n");}}/* Media encryption doesn't apply to data channels */participant->drc = rc;}
}
// record.c
/* Info header in the structured recording */
static const char *header = "MJR00002";
/* Frame header in the structured recording */
static const char *frame_header = "MEET";janus_recorder *janus_recorder_create(const char *dir, const char *codec, const char *filename) {/* Same as janus_recorder_create_full, but with no fmtp */return janus_recorder_create_full(dir, codec, NULL, filename);
}/* 
打开文件;
写入文件头;MJR00002
*/
janus_recorder *janus_recorder_create_full(const char *dir, const char *codec, const char *fmtp, const char *filename) {janus_recorder_medium type = JANUS_RECORDER_AUDIO;if(codec == NULL) {JANUS_LOG(LOG_ERR, "Missing codec information\n");return NULL;}if(!strcasecmp(codec, "vp8") || !strcasecmp(codec, "vp9") || !strcasecmp(codec, "h264")|| !strcasecmp(codec, "av1") || !strcasecmp(codec, "h265")) {type = JANUS_RECORDER_VIDEO;} else if(!strcasecmp(codec, "opus") || !strcasecmp(codec, "multiopus")|| !strcasecmp(codec, "g711") || !strcasecmp(codec, "pcmu") || !strcasecmp(codec, "pcma")|| !strcasecmp(codec, "g722")) {type = JANUS_RECORDER_AUDIO;} else if(!strcasecmp(codec, "text")) {/* FIXME We only handle text on data channels, so that's the only thing we can save too */type = JANUS_RECORDER_DATA;} else {/* We don't recognize the codec: while we might go on anyway, we'd rather fail instead */JANUS_LOG(LOG_ERR, "Unsupported codec '%s'\n", codec);return NULL;}/* Create the recorder */janus_recorder *rc = g_malloc0(sizeof(janus_recorder));janus_refcount_init(&rc->ref, janus_recorder_free);rc->dir = NULL;rc->filename = NULL;rc->file = NULL;rc->codec = g_strdup(codec);rc->fmtp = fmtp ? g_strdup(fmtp) : NULL;rc->created = janus_get_real_time();const char *rec_dir = NULL;const char *rec_file = NULL;char *copy_for_parent = NULL;char *copy_for_base = NULL;/* 检查路径和文件名是否合规 */if(filename != NULL) {/* Helper copies to avoid overwriting */copy_for_parent = g_strdup(filename);copy_for_base = g_strdup(filename);/* Get filename parent folder */const char *filename_parent = dirname(copy_for_parent);/* Get filename base file */const char *filename_base = basename(copy_for_base);if(!dir) {/* If dir is NULL we have to create filename_parent and filename_base */rec_dir = filename_parent;rec_file = filename_base;} else {/* If dir is valid we have to create dir and filename*/rec_dir = dir;rec_file = filename;if(strcasecmp(filename_parent, ".") || strcasecmp(filename_base, filename)) {JANUS_LOG(LOG_WARN, "Unsupported combination of dir and filename %s %s\n", dir, filename);}}}// 检查路径是否存在,如果不存在,则创建路径if(rec_dir != NULL) {/* Check if this directory exists, and create it if needed */struct stat s;int err = stat(rec_dir, &s);if(err == -1) {if(ENOENT == errno) {/* Directory does not exist, try creating it */if(janus_mkdir(rec_dir, 0755) < 0) {JANUS_LOG(LOG_ERR, "mkdir (%s) error: %d (%s)\n", rec_dir, errno, strerror(errno));janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}} else {JANUS_LOG(LOG_ERR, "stat (%s) error: %d (%s)\n", rec_dir, errno, strerror(errno));janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}} else {if(S_ISDIR(s.st_mode)) {/* Directory exists */JANUS_LOG(LOG_VERB, "Directory exists: %s\n", rec_dir);} else {/* File exists but it's not a directory? */JANUS_LOG(LOG_ERR, "Not a directory? %s\n", rec_dir);janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}}}char newname[1024];memset(newname, 0, 1024);// 给文件名加上.mjr的后缀if(rec_file == NULL) {/* Choose a random username */if(!rec_tempname) {/* Use .mjr as an extension right away */g_snprintf(newname, 1024, "janus-recording-%"SCNu32".mjr", janus_random_uint32());} else {/* Append the temporary extension to .mjr, we'll rename when closing */g_snprintf(newname, 1024, "janus-recording-%"SCNu32".mjr.%s", janus_random_uint32(), rec_tempext);}} else {/* Just append the extension */if(!rec_tempname) {/* Use .mjr as an extension right away */g_snprintf(newname, 1024, "%s.mjr", rec_file);} else {/* Append the temporary extension to .mjr, we'll rename when closing */g_snprintf(newname, 1024, "%s.mjr.%s", rec_file, rec_tempext);}}/* 打开文件,准备写入 */if(rec_dir == NULL) {/* Make sure folder to save to is not protected */if(janus_is_folder_protected(newname)) {JANUS_LOG(LOG_ERR, "Target recording path '%s' is in protected folder...\n", newname);janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}rc->file = fopen(newname, "wb");} else {char path[1024];memset(path, 0, 1024);g_snprintf(path, 1024, "%s/%s", rec_dir, newname);/* Make sure folder to save to is not protected */if(janus_is_folder_protected(path)) {JANUS_LOG(LOG_ERR, "Target recording path '%s' is in protected folder...\n", path);janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}rc->file = fopen(path, "wb");}if(rc->file == NULL) {JANUS_LOG(LOG_ERR, "fopen error: %d\n", errno);janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}if(rec_dir)rc->dir = g_strdup(rec_dir);rc->filename = g_strdup(newname);rc->type = type;/* 写入文件头: static const char *header = "MJR00002";*/size_t res = fwrite(header, sizeof(char), strlen(header), rc->file);if(res != strlen(header)) {JANUS_LOG(LOG_ERR, "Couldn't write .mjr header (%zu != %zu, %s)\n",res, strlen(header), strerror(errno));janus_recorder_destroy(rc);g_free(copy_for_parent);g_free(copy_for_base);return NULL;}g_atomic_int_set(&rc->writable, 1);/* 除了写入上面的文件头外,还需要写入信息头, 所以在这里将写入信息头的标志置0*/g_atomic_int_set(&rc->header, 0);janus_mutex_init(&rc->mutex);/* Done */g_atomic_int_set(&rc->destroyed, 0);g_free(copy_for_parent);g_free(copy_for_base);return rc;
}

三、录制数据

对每个接收到的RTP包:
首先:如果是第一个RTP包,则需要先写信息头到文件;
然后:
      写入4字节的帧头"MEET";
      写入4字节的帧时间戳;
      写入2字节的帧长度;
最后: 写入帧数据;

void janus_videoroom_incoming_rtp(janus_plugin_session *handle, janus_plugin_rtp *pkt) {
static void janus_videoroom_incoming_rtp_internal(janus_videoroom_session *session, janus_videoroom_publisher *participant, janus_plugin_rtp *pkt) {if(handle == NULL || g_atomic_int_get(&handle->stopped) || g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized))return;janus_videoroom_session *session = (janus_videoroom_session *)handle->plugin_handle;if(!session || g_atomic_int_get(&session->destroyed) || session->participant_type != janus_videoroom_p_type_publisher)return;janus_videoroom_publisher *participant = janus_videoroom_session_get_publisher_nodebug(session);if(participant == NULL)return;if(g_atomic_int_get(&participant->destroyed) || participant->kicked || participant->room == NULL) {janus_videoroom_publisher_dereference_nodebug(participant);return;}janus_videoroom *videoroom = participant->room;gboolean video = pkt->video;char *buf = pkt->buffer;uint16_t len = pkt->length;/* 写入帧数据到录制文件  */if(!video || (participant->ssrc[0] == 0 && participant->rid[0] == NULL)) {janus_recorder_save_frame(video ? participant->vrc : participant->arc, buf, len);} else {/* We're simulcasting, save the best video quality */gboolean save = janus_rtp_simulcasting_context_process_rtp(&participant->rec_simctx,buf, len, participant->ssrc, participant->rid, participant->vcodec, &participant->rec_ctx);if(save) {uint32_t seq_number = ntohs(rtp->seq_number);uint32_t timestamp = ntohl(rtp->timestamp);uint32_t ssrc = ntohl(rtp->ssrc);janus_rtp_header_update(rtp, &participant->rec_ctx, TRUE, 0);/* We use a fixed SSRC for the whole recording */rtp->ssrc = participant->ssrc[0];janus_recorder_save_frame(participant->vrc, buf, len);/* Restore the header, as it will be needed by subscribers */rtp->ssrc = htonl(ssrc);rtp->timestamp = htonl(timestamp);rtp->seq_number = htons(seq_number);}}
}// record.c
int janus_recorder_save_frame(janus_recorder *recorder, char *buffer, uint length) {if(!recorder)return -1;janus_mutex_lock_nodebug(&recorder->mutex);if(!buffer || length < 1) {janus_mutex_unlock_nodebug(&recorder->mutex);return -2;}if(!recorder->file) {janus_mutex_unlock_nodebug(&recorder->mutex);return -3;}if(!g_atomic_int_get(&recorder->writable)) {janus_mutex_unlock_nodebug(&recorder->mutex);return -4;}gint64 now = janus_get_monotonic_time();// 如果是第一个包,则需要准备好信息头的数据, 将它的长度和内容写入到文件if(!g_atomic_int_get(&recorder->header)) {/* Write info header as a JSON formatted info */json_t *info = json_object();/* FIXME Codecs should be configurable in the future */const char *type = NULL;if(recorder->type == JANUS_RECORDER_AUDIO)type = "a";else if(recorder->type == JANUS_RECORDER_VIDEO)type = "v";else if(recorder->type == JANUS_RECORDER_DATA)type = "d";json_object_set_new(info, "t", json_string(type));								/* Audio/Video/Data */json_object_set_new(info, "c", json_string(recorder->codec));					/* Media codec */if(recorder->fmtp)json_object_set_new(info, "f", json_string(recorder->fmtp));				/* Codec-specific info */json_object_set_new(info, "s", json_integer(recorder->created));				/* Created time */json_object_set_new(info, "u", json_integer(janus_get_real_time()));			/* First frame written time *//* If media will be end-to-end encrypted, mark it in the recording header */if(recorder->encrypted)json_object_set_new(info, "e", json_true());gchar *info_text = json_dumps(info, JSON_PRESERVE_ORDER);json_decref(info);uint16_t info_bytes = htons(strlen(info_text));// 将信息头的长度(info_bytes)写入文件size_t res = fwrite(&info_bytes, sizeof(uint16_t), 1, recorder->file);if(res != 1) {JANUS_LOG(LOG_WARN, "Couldn't write size of JSON header in .mjr file (%zu != %zu, %s), expect issues post-processing\n",res, sizeof(uint16_t), strerror(errno));}// 将信息头的内容(info_text) 写入文件res = fwrite(info_text, sizeof(char), strlen(info_text), recorder->file);if(res != strlen(info_text)) {JANUS_LOG(LOG_WARN, "Couldn't write JSON header in .mjr file (%zu != %zu, %s), expect issues post-processing\n",res, strlen(info_text), strerror(errno));}free(info_text);/* Done */recorder->started = now;// 将是否写入信息头的标志置 1 ; g_atomic_int_set(&recorder->header, 1);}/* Write frame header (fixed part[4], timestamp[4], length[2]) 写入4个字节长度的固定内容的mjr包头:static const char *frame_header = "MEET";*/size_t res = fwrite(frame_header, sizeof(char), strlen(frame_header), recorder->file);if(res != strlen(frame_header)) {JANUS_LOG(LOG_WARN, "Couldn't write frame header in .mjr file (%zu != %zu, %s), expect issues post-processing\n",res, strlen(frame_header), strerror(errno));}// 写入4个字节长度的时间戳uint32_t timestamp = (uint32_t)(now > recorder->started ? ((now - recorder->started)/1000) : 0);timestamp = htonl(timestamp);res = fwrite(&timestamp, sizeof(uint32_t), 1, recorder->file);if(res != 1) {JANUS_LOG(LOG_WARN, "Couldn't write frame timestamp in .mjr file (%zu != %zu, %s), expect issues post-processing\n",res, sizeof(uint32_t), strerror(errno));}// 写入2个字节长度的帧长度uint16_t header_bytes = htons(recorder->type == JANUS_RECORDER_DATA ? (length+sizeof(gint64)) : length);res = fwrite(&header_bytes, sizeof(uint16_t), 1, recorder->file);if(res != 1) {JANUS_LOG(LOG_WARN, "Couldn't write size of frame in .mjr file (%zu != %zu, %s), expect issues post-processing\n",res, sizeof(uint16_t), strerror(errno));}if(recorder->type == JANUS_RECORDER_DATA) {/* If it's data, then we need to prepend timing related info, as it's not there by itself */gint64 now = htonll(janus_get_real_time());res = fwrite(&now, sizeof(gint64), 1, recorder->file);if(res != 1) {JANUS_LOG(LOG_WARN, "Couldn't write data timestamp in .mjr file (%zu != %zu, %s), expect issues post-processing\n",res, sizeof(gint64), strerror(errno));}}/* Save packet on file 写入帧数据到文件*/int temp = 0, tot = length;while(tot > 0) {temp = fwrite(buffer+length-tot, sizeof(char), tot, recorder->file);if(temp <= 0) {JANUS_LOG(LOG_ERR, "Error saving frame...\n");janus_mutex_unlock_nodebug(&recorder->mutex);return -5;}tot -= temp;}/* Done */janus_mutex_unlock_nodebug(&recorder->mutex);return 0;
}

四、录制结束

对录制文件重命名后,
关闭文件句柄;

/* Thread responsible for a specific remote publisher */
static void *janus_videoroom_remote_publisher_thread(void *user_data) {/* If we got here, the remote publisher has been removed from the* room: let's notify all other publishers in the room */janus_mutex_lock(&publisher->rec_mutex);g_free(publisher->recording_base);publisher->recording_base = NULL;// 结束录制,看是否要对录制文件进行重命名janus_videoroom_recorder_close(publisher);janus_mutex_unlock(&publisher->rec_mutex);
}	// janus_videoroom.c
static void janus_videoroom_recorder_close(janus_videoroom_publisher *participant) {if(participant->arc) {janus_recorder *rc = participant->arc;participant->arc = NULL;janus_recorder_close(rc);JANUS_LOG(LOG_INFO, "Closed audio recording %s\n", rc->filename ? rc->filename : "??");janus_recorder_destroy(rc);}if(participant->vrc) {janus_recorder *rc = participant->vrc;participant->vrc = NULL;janus_recorder_close(rc);JANUS_LOG(LOG_INFO, "Closed video recording %s\n", rc->filename ? rc->filename : "??");janus_recorder_destroy(rc);}if(participant->drc) {janus_recorder *rc = participant->drc;participant->drc = NULL;janus_recorder_close(rc);JANUS_LOG(LOG_INFO, "Closed data recording %s\n", rc->filename ? rc->filename : "??");janus_recorder_destroy(rc);}
}//record.c
// 结束录制,看是否要对录制文件进行重命名
int janus_recorder_close(janus_recorder *recorder) {if(!recorder || !g_atomic_int_compare_and_exchange(&recorder->writable, 1, 0))return -1;janus_mutex_lock_nodebug(&recorder->mutex);if(recorder->file) {fseek(recorder->file, 0L, SEEK_END);size_t fsize = ftell(recorder->file);fseek(recorder->file, 0L, SEEK_SET);JANUS_LOG(LOG_INFO, "File is %zu bytes: %s\n", fsize, recorder->filename);}if(rec_tempname) {/* We need to rename the file, to remove the temporary extension */char newname[1024];memset(newname, 0, 1024);g_snprintf(newname, strlen(recorder->filename)-strlen(rec_tempext), "%s", recorder->filename);char oldpath[1024];memset(oldpath, 0, 1024);char newpath[1024];memset(newpath, 0, 1024);if(recorder->dir) {g_snprintf(newpath, 1024, "%s/%s", recorder->dir, newname);g_snprintf(oldpath, 1024, "%s/%s", recorder->dir, recorder->filename);} else {g_snprintf(newpath, 1024, "%s", newname);g_snprintf(oldpath, 1024, "%s", recorder->filename);}if(rename(oldpath, newpath) != 0) {JANUS_LOG(LOG_ERR, "Error renaming %s to %s...\n", recorder->filename, newname);} else {JANUS_LOG(LOG_INFO, "Recording renamed: %s\n", newname);g_free(recorder->filename);recorder->filename = g_strdup(newname);}}janus_mutex_unlock_nodebug(&recorder->mutex);return 0;
}

这篇关于janus-gateway的videoroom插件的RTP包录制功能源码详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/745937

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP