Statistics
| Branch: | Revision:

janus-gateway / record.c @ becc0c84

History | View | Annotate | Download (9.15 KB)

1
/*! \file    record.h
2
 * \author   Lorenzo Miniero <lorenzo@meetecho.com>
3
 * \copyright GNU General Public License v3
4
 * \brief    Audio/Video recorder
5
 * \details  Implementation of a simple recorder utility that plugins
6
 * can make use of to record audio/video frames to a Janus file. This
7
 * file just saves RTP frames in a structured way, so that they can be
8
 * post-processed later on to get a valid container file (e.g., a .opus
9
 * file for Opus audio or a .webm file for VP8 video) and keep things
10
 * simpler on the plugin and core side.
11
 * \note If you want to record both audio and video, you'll have to use
12
 * two different recorders. Any muxing in the same container will have
13
 * to be done in the post-processing phase.
14
 * 
15
 * \ingroup core
16
 * \ref core
17
 */
18
 
19
#include <arpa/inet.h>
20
#include <sys/stat.h>
21
#include <errno.h>
22

    
23
#include <glib.h>
24
#include <jansson.h>
25

    
26
#include "record.h"
27
#include "debug.h"
28
#include "utils.h"
29

    
30
#define htonll(x) ((1==htonl(1)) ? (x) : ((gint64)htonl((x) & 0xFFFFFFFF) << 32) | htonl((x) >> 32))
31
#define ntohll(x) ((1==ntohl(1)) ? (x) : ((gint64)ntohl((x) & 0xFFFFFFFF) << 32) | ntohl((x) >> 32))
32

    
33

    
34
/* Info header in the structured recording */
35
static const char *header = "MJR00001";
36
/* Frame header in the structured recording */
37
static const char *frame_header = "MEETECHO";
38

    
39
/* Whether the filenames should have a temporary extension, while saving, or not (default=false) */
40
static gboolean rec_tempname = FALSE;
41
/* Extension to add in case tempnames is true (default="tmp" --> ".tmp") */
42
static char *rec_tempext = NULL;
43

    
44
void janus_recorder_init(gboolean tempnames, const char *extension) {
45
        JANUS_LOG(LOG_INFO, "Initializing recorder code\n");
46
        if(tempnames) {
47
                rec_tempname = TRUE;
48
                if(extension == NULL) {
49
                        rec_tempext = g_strdup("tmp");
50
                        JANUS_LOG(LOG_INFO, "  -- No extension provided, using default one (tmp)");
51
                } else {
52
                        rec_tempext = g_strdup(extension);
53
                        JANUS_LOG(LOG_INFO, "  -- Using temporary extension .%s", rec_tempext);
54
                }
55
        }
56
}
57

    
58
void janus_recorder_deinit(void) {
59
        rec_tempname = FALSE;
60
        g_free(rec_tempext);
61
}
62

    
63

    
64
janus_recorder *janus_recorder_create(const char *dir, const char *codec, const char *filename) {
65
        janus_recorder_medium type = JANUS_RECORDER_AUDIO;
66
        if(codec == NULL) {
67
                JANUS_LOG(LOG_ERR, "Missing codec information\n");
68
                return NULL;
69
        }
70
        if(!strcasecmp(codec, "vp8") || !strcasecmp(codec, "vp9") || !strcasecmp(codec, "h264")) {
71
                type = JANUS_RECORDER_VIDEO;
72
                if(!strcasecmp(codec, "vp9")) {
73
                        JANUS_LOG(LOG_WARN, "The post-processor currently doesn't support VP9: recording anyway for the future\n");
74
                }
75
        } else if(!strcasecmp(codec, "opus") || !strcasecmp(codec, "g711") || !strcasecmp(codec, "pcmu") || !strcasecmp(codec, "pcma")) {
76
                type = JANUS_RECORDER_AUDIO;
77
                if(!strcasecmp(codec, "pcmu") || !strcasecmp(codec, "pcma"))
78
                        codec = "g711";
79
        } else if(!strcasecmp(codec, "text")) {
80
                /* FIXME We only handle text on data channels, so that's the only thing we can save too */
81
                type = JANUS_RECORDER_DATA;
82
        } else {
83
                /* We don't recognize the codec: while we might go on anyway, we'd rather fail instead */
84
                JANUS_LOG(LOG_ERR, "Unsupported codec '%s'\n", codec);
85
                return NULL;
86
        }
87
        /* Create the recorder */
88
        janus_recorder *rc = g_malloc0(sizeof(janus_recorder));
89
        if(rc == NULL) {
90
                JANUS_LOG(LOG_FATAL, "Memory error!\n");
91
                return NULL;
92
        }
93
        rc->dir = NULL;
94
        rc->filename = NULL;
95
        rc->file = NULL;
96
        rc->codec = g_strdup(codec);
97
        rc->created = janus_get_real_time();
98
        if(dir != NULL) {
99
                /* Check if this directory exists, and create it if needed */
100
                struct stat s;
101
                int err = stat(dir, &s);
102
                if(err == -1) {
103
                        if(ENOENT == errno) {
104
                                /* Directory does not exist, try creating it */
105
                                if(janus_mkdir(dir, 0755) < 0) {
106
                                        JANUS_LOG(LOG_ERR, "mkdir error: %d\n", errno);
107
                                        return NULL;
108
                                }
109
                        } else {
110
                                JANUS_LOG(LOG_ERR, "stat error: %d\n", errno);
111
                                return NULL;
112
                        }
113
                } else {
114
                        if(S_ISDIR(s.st_mode)) {
115
                                /* Directory exists */
116
                                JANUS_LOG(LOG_VERB, "Directory exists: %s\n", dir);
117
                        } else {
118
                                /* File exists but it's not a directory? */
119
                                JANUS_LOG(LOG_ERR, "Not a directory? %s\n", dir);
120
                                return NULL;
121
                        }
122
                }
123
        }
124
        char newname[1024];
125
        memset(newname, 0, 1024);
126
        if(filename == NULL) {
127
                /* Choose a random username */
128
                if(!rec_tempname) {
129
                        /* Use .mjr as an extension right away */
130
                        g_snprintf(newname, 1024, "janus-recording-%"SCNu32".mjr", janus_random_uint32());
131
                } else {
132
                        /* Append the temporary extension to .mjr, we'll rename when closing */
133
                        g_snprintf(newname, 1024, "janus-recording-%"SCNu32".mjr.%s", janus_random_uint32(), rec_tempext);
134
                }
135
        } else {
136
                /* Just append the extension */
137
                if(!rec_tempname) {
138
                        /* Use .mjr as an extension right away */
139
                        g_snprintf(newname, 1024, "%s.mjr", filename);
140
                } else {
141
                        /* Append the temporary extension to .mjr, we'll rename when closing */
142
                        g_snprintf(newname, 1024, "%s.mjr.%s", filename, rec_tempext);
143
                }
144
        }
145
        /* Try opening the file now */
146
        if(dir == NULL) {
147
                rc->file = fopen(newname, "wb");
148
        } else {
149
                char path[1024];
150
                memset(path, 0, 1024);
151
                g_snprintf(path, 1024, "%s/%s", dir, newname);
152
                rc->file = fopen(path, "wb");
153
        }
154
        if(rc->file == NULL) {
155
                JANUS_LOG(LOG_ERR, "fopen error: %d\n", errno);
156
                return NULL;
157
        }
158
        if(dir)
159
                rc->dir = g_strdup(dir);
160
        rc->filename = g_strdup(newname);
161
        rc->type = type;
162
        /* Write the first part of the header */
163
        fwrite(header, sizeof(char), strlen(header), rc->file);
164
        rc->writable = 1;
165
        /* We still need to also write the info header first */
166
        rc->header = 0;
167
        janus_mutex_init(&rc->mutex);
168
        return rc;
169
}
170

    
171
int janus_recorder_save_frame(janus_recorder *recorder, char *buffer, uint length) {
172
        if(!recorder)
173
                return -1;
174
        janus_mutex_lock_nodebug(&recorder->mutex);
175
        if(!buffer || length < 1) {
176
                janus_mutex_unlock_nodebug(&recorder->mutex);
177
                return -2;
178
        }
179
        if(!recorder->file) {
180
                janus_mutex_unlock_nodebug(&recorder->mutex);
181
                return -3;
182
        }
183
        if(!recorder->writable) {
184
                janus_mutex_unlock_nodebug(&recorder->mutex);
185
                return -4;
186
        }
187
        if(!recorder->header) {
188
                /* Write info header as a JSON formatted info */
189
                json_t *info = json_object();
190
                /* FIXME Codecs should be configurable in the future */
191
                const char *type = NULL;
192
                if(recorder->type == JANUS_RECORDER_AUDIO)
193
                        type = "a";
194
                else if(recorder->type == JANUS_RECORDER_VIDEO)
195
                        type = "v";
196
                else if(recorder->type == JANUS_RECORDER_DATA)
197
                        type = "d";
198
                json_object_set_new(info, "t", json_string(type));                                                                /* Audio/Video/Data */
199
                json_object_set_new(info, "c", json_string(recorder->codec));                                        /* Media codec */
200
                json_object_set_new(info, "s", json_integer(recorder->created));                                /* Created time */
201
                json_object_set_new(info, "u", json_integer(janus_get_real_time()));                        /* First frame written time */
202
                gchar *info_text = json_dumps(info, JSON_PRESERVE_ORDER);
203
                json_decref(info);
204
                uint16_t info_bytes = htons(strlen(info_text));
205
                fwrite(&info_bytes, sizeof(uint16_t), 1, recorder->file);
206
                fwrite(info_text, sizeof(char), strlen(info_text), recorder->file);
207
                free(info_text);
208
                /* Done */
209
                recorder->header = 1;
210
        }
211
        /* Write frame header */
212
        fwrite(frame_header, sizeof(char), strlen(frame_header), recorder->file);
213
        uint16_t header_bytes = htons(recorder->type == JANUS_RECORDER_DATA ? (length+sizeof(gint64)) : length);
214
        fwrite(&header_bytes, sizeof(uint16_t), 1, recorder->file);
215
        if(recorder->type == JANUS_RECORDER_DATA) {
216
                /* If it's data, then we need to prepend timing related info, as it's not there by itself */
217
                gint64 now = htonll(janus_get_real_time());
218
                fwrite(&now, sizeof(gint64), 1, recorder->file);
219
        }
220
        /* Save packet on file */
221
        int temp = 0, tot = length;
222
        while(tot > 0) {
223
                temp = fwrite(buffer+length-tot, sizeof(char), tot, recorder->file);
224
                if(temp <= 0) {
225
                        JANUS_LOG(LOG_ERR, "Error saving frame...\n");
226
                        janus_mutex_unlock_nodebug(&recorder->mutex);
227
                        return -5;
228
                }
229
                tot -= temp;
230
        }
231
        /* Done */
232
        janus_mutex_unlock_nodebug(&recorder->mutex);
233
        return 0;
234
}
235

    
236
int janus_recorder_close(janus_recorder *recorder) {
237
        if(!recorder || !recorder->writable)
238
                return -1;
239
        janus_mutex_lock_nodebug(&recorder->mutex);
240
        recorder->writable = 0;
241
        if(recorder->file) {
242
                fseek(recorder->file, 0L, SEEK_END);
243
                size_t fsize = ftell(recorder->file);
244
                fseek(recorder->file, 0L, SEEK_SET);
245
                JANUS_LOG(LOG_INFO, "File is %zu bytes: %s\n", fsize, recorder->filename);
246
        }
247
        if(rec_tempname) {
248
                /* We need to rename the file, to remove the temporary extension */
249
                char newname[1024];
250
                memset(newname, 0, 1024);
251
                g_snprintf(newname, strlen(recorder->filename)-strlen(rec_tempext), "%s", recorder->filename);
252
                if(rename(recorder->filename, newname) != 0) {
253
                        JANUS_LOG(LOG_ERR, "Error renaming %s to %s...\n", recorder->filename, newname);
254
                } else {
255
                        JANUS_LOG(LOG_INFO, "Recording renamed: %s\n", newname);
256
                        g_free(recorder->filename);
257
                        recorder->filename = g_strdup(newname);
258
                }
259
        }
260
        janus_mutex_unlock_nodebug(&recorder->mutex);
261
        return 0;
262
}
263

    
264
int janus_recorder_free(janus_recorder *recorder) {
265
        if(!recorder)
266
                return -1;
267
        janus_recorder_close(recorder);
268
        janus_mutex_lock_nodebug(&recorder->mutex);
269
        g_free(recorder->dir);
270
        recorder->dir = NULL;
271
        g_free(recorder->filename);
272
        recorder->filename = NULL;
273
        fclose(recorder->file);
274
        recorder->file = NULL;
275
        g_free(recorder->codec);
276
        recorder->codec = NULL;
277
        janus_mutex_unlock_nodebug(&recorder->mutex);
278
        g_free(recorder);
279
        return 0;
280
}