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(×tamp);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(×tamp, 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包录制功能源码详解
引: janus-gateway在配置文件设置后,可以实现对videoroom插件的每个publisher的音频,视频,数据的RTP流录制成mjr文件。 对于音频,视频的mjr文件,可以使用自带的postprocessing工具janus-pp-rec转成mp4文件。 每个pu…...

nginx+keepalived实现nginx高可用集群以及nginx实现Gateway网关服务集群
一、前言 1、简介 Nginx作为一款高性能的Web服务器和反向代理服务器,被广泛使用。且现如今很多高并发场景需要后端服务集群部署,因此nginx也需要支持集群部署从而避免单点故障的问题。 本文将详细介绍使用 KeepalivedNginx 来实现Nginx的高可用集群和N…...
主键、外键、建表范式、MySQL索引、用户管理
1 案例1:主键 1.1 问题 完成如下练习: 练习主键的创建、查看、删除、添加、验证主键练习复合主键的使用练习与auto_increment连用的效果 1.2 方案 主键使用规则: 表头值不允许重复,不允许赋NULL值一个表中只能有一个primary…...

探究前端路由hash和history的实现原理(包教包会)
今天我们来讲一讲前端中很重要的一个部分路由(router),想必前端小伙伴对‘路由’一词都不会感到陌生。但是如果哪天面试官问你,能大概说一说前端路由的实现原理吗? 你又会如何应对呢? 今天勇宝就带着大家一…...

幻兽帕鲁服务器多少钱?有买过的吗?
幻兽帕鲁服务器多少钱?太卷了,降价到24元1个月,阿里云4核16G10M游戏服务器26元1个月、149元半年,腾讯云4核16G游戏服务器32元、312元一年,华为云26元,京东云主机也是26元起。云服务器吧yunfuwuqiba.com给大…...

MCU独立按键单控LED实现
##江科大视频学习,并且对具体的一些小细节进行更详细的分析。 什么是独立按键? 轻触按键:相当于是一种电子开关,按下开头接通,松开时开头断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开。 注意…...

[数据集][目标检测]游泳者溺水数据集VOC+YOLO格式2类别895张
数据集制作单位:未来自主研究中心(FIRC) 数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):895 标注数量(xml文件个数):…...
2402C++,C++使用单链列表
原文 #include <windows.h> #include <malloc.h> #include <stdio.h>//用于列表项的结构;第一个成员是SLIST_ENTRY结构,其他成员是数据.在此,数据只是测试 typedef struct _PROGRAM_ITEM {SLIST_ENTRY ItemEntry;ULONG Signature; } PROGRAM_ITEM, *PPROGR…...
《Docker极简教程》--Docker服务管理和监控--Docker服务的监控
Docker监控的必要性在于确保容器化环境的稳定性、性能和安全性。以下是几个关键原因: 性能优化和故障排除:监控可以帮助识别容器化应用程序的性能问题,并快速进行故障排除。通过监控关键指标,如CPU利用率、内存使用、网络流量等&…...

C++初阶 | [八] (下) vector 模拟实现
摘要:vector 模拟实现讲解(附代码示例),隐藏的浅拷贝,迭代器失效 在进行 vector 的模拟实现之前,我们先粗略浏览一下 stl_vector.h 文件中的源码来确定模拟实现的大体框架。 这里提供一些粗略浏览源码的技巧…...

信息安全计划
任何管理人员或人力资源专业人士都知道,除非彻底记录标准和实践,否则永远无法真正实施和执行标准和实践。正如您可能想象的那样,在保护您的网络、技术和数据系统免受网络威胁以及在发生这些事件时规划最及时、高效和有效的响应时,…...
【更新完毕】2024牛客寒假算法基础集训营6 题解 | JorbanS
文章目录 [A - 宇宙的终结](https://ac.nowcoder.com/acm/contest/67746/A)[B - 爱恨的纠葛](https://ac.nowcoder.com/acm/contest/67746/B)[C - 心绪的解剖](https://ac.nowcoder.com/acm/contest/67746/C)[D - 友谊的套路](https://ac.nowcoder.com/acm/contest/67746/D)[E …...

FL Studio All Plugins Edition2024中文完整版Win/Mac
FL Studio All Plugins Edition,常被誉为数字音频工作站(DAW)的佼佼者,是音乐制作人和声音工程师钟爱的工具。它集音频录制、编辑、混音以及MIDI制作为一体,为用户提供了从创作到最终作品输出的完整工作流程。这个版本…...

神经网络系列---归一化
文章目录 归一化批量归一化预测阶段 测试阶段γ和β(注意)举例 层归一化前向传播反向传播 归一化 批量归一化 (Batch Normalization)在训练过程中的数学公式可以概括如下: 给定一个小批量数据 B { x 1 , x 2 , … …...

2023 龙蜥操作系统大会演讲实录:《兼容龙蜥的云原生大模型数据计算系统——πDataCS》
本文主要分三部分内容:第一部分介绍拓数派公司,第二部分介绍 πDataCS 产品,最后介绍 πDataCS 与龙蜥在生态上的合作。 杭州拓数派科技发展有限公司(简称“拓数派”,英文名称“OpenPie”)是国内基础数据计…...

【Vue渗透】Vue站点渗透思路
原文地址 极核GetShell 前言 本文经验适用于前端用Webpack打包的Vue站点,阅读完本文,可以识别出Webpack打包的Vue站点,同时可以发现该Vue站点的路由。 成果而言:可能可以发现未授权访问。 识别Vue 识别出Webpack打包的Vue站…...

主数据管理是数字化转型成功的基石——江淮汽车案例分享
汽车行业数字化转型的背景 在新冠疫情导火索的影响下,经济全球化政治基础逐渐动摇。作为全球最大的汽车市场,我国的汽车市场逐渐由增量转为存量市场。 在数字化改革大背景下,随着工业4.0时代的到来,江淮汽车集团力争实现十四五数…...
【Spring连载】使用Spring Data访问 MongoDB(十一)----加密Encryption (CSFLE)
[TOC](【Spring连载】使用Spring Data访问 MongoDB(十一)----加密Encryption (CSFLE)) 一级目录 二级目录 三级目录...

【postgresql】数据表id自增与python sqlachemy结合实例
需求: postgresql实现一个建表语句,表名:student,字段id,name,age, 要求:每次添加一个数据id会自动增加1 在PostgreSQL中,您可以使用SERIAL或BIGSERIAL数据类型来自动生成主键ID。以下是一个创建名为stude…...
什么是索引?在 MySQL 中有哪些类型的索引?它们各自的优势和劣势是什么?
什么是索引?在 MySQL 中有哪些类型的索引?它们各自的优势和劣势是什么? 索引是数据库中用于帮助快速查询数据的一种数据结构。在 MySQL 中,索引可以显著提高查询性能,因为它允许数据库系统不必扫描整个表来找到相关数据…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...

基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...

Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...

华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
comfyui 工作流中 图生视频 如何增加视频的长度到5秒
comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

医疗AI模型可解释性编程研究:基于SHAP、LIME与Anchor
1 医疗树模型与可解释人工智能基础 医疗领域的人工智能应用正迅速从理论研究转向临床实践,在这一过程中,模型可解释性已成为确保AI系统被医疗专业人员接受和信任的关键因素。基于树模型的集成算法(如RandomForest、XGBoost、LightGBM)因其卓越的预测性能和相对良好的解释性…...
比特币:固若金汤的数字堡垒与它的四道防线
第一道防线:机密信函——无法破解的哈希加密 将每一笔比特币交易比作一封在堡垒内部传递的机密信函。 解释“哈希”(Hashing)就是一种军事级的加密术(SHA-256),能将信函内容(交易细节…...