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 = 6
		bitrate = 128000
		fir_freq = 10
		audiocodec = "opus"
		videocodec = "h264"
		record = true
		rec_dir = "/data/PJT-janus/record-samples"
}

二、录制初始化

当客户端为发布者,且发送的message为"configure"类型时,

将初始化录制, 将初始化音频、视频和数据文件的存储路径、文件名后,

打开文件以获得文件句柄后,写入文件头。

cpp 复制代码
// 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 */
			
		}		
	}
}
cpp 复制代码
/* 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;
}
cpp 复制代码
/**********  创建本发布者对应的音频、视频、数据录制文件 *******************/
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;
	}
}
cpp 复制代码
// 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字节的帧长度;

最后: 写入帧数据;

cpp 复制代码
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;
}

四、录制结束

对录制文件重命名后,

关闭文件句柄;

cpp 复制代码
/* 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;
}
相关推荐
数据知道1 天前
指纹浏览器:DNS 泄漏防范与 WebRTC 本地 IP 屏蔽的底层实现
爬虫·网络协议·tcp/ip·安全·webrtc·数据采集·指纹浏览器
换个昵称都难2 天前
webrtc源码解析概要介绍
webrtc
换个昵称都难2 天前
WebRTC 完整调用流程(前端纯 JS 实现,最简可运行)
webrtc
换个昵称都难3 天前
webrtc 拥塞控制GCC 和PCC
webrtc
Cxiaomu3 天前
React接入WebRTC实时视频实践
react.js·音视频·webrtc
AndyHuang19763 天前
WebRTC 强制 Relay 模式下 TCP 重连失败深度排查与优化实战
webrtc
换个昵称都难3 天前
webrtc pacing 平滑发包模块
webrtc
换个昵称都难3 天前
webrtc 音频混音介绍
音视频·webrtc
换个昵称都难4 天前
webrtc QOS-RemoteBitrateEstimator接收端带宽估计(1)
webrtc
换个昵称都难4 天前
webrtc QOS-RemoteBitrateEstimator接收端带宽估计-四个实例(2)
webrtc