引:

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"类型时,
将初始化录制, 将初始化音频、视频和数据文件的存储路径、文件名后,
打开文件以获得文件句柄后,写入文件头。

// 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;
}
02-25 07:40