Changeset 540 for pjproject/trunk
- Timestamp:
- Jun 22, 2006 6:51:03 PM (18 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
pjproject/trunk/pjsip-apps/src/samples/siprtp.c
r531 r540 97 97 98 98 99 /* A bidirectional media stream */99 /* A bidirectional media stream created when the call is active. */ 100 100 struct media_stream 101 101 { 102 102 /* Static: */ 103 pj_uint16_t port; /* RTP port (RTCP is +1) */ 103 unsigned call_index; /* Call owner. */ 104 unsigned media_index; /* Media index in call. */ 105 pjmedia_transport *transport; /* To send/recv RTP/RTCP */ 106 107 /* Active? */ 108 pj_bool_t active; /* Non-zero if is in call. */ 104 109 105 110 /* Current stream info: */ … … 111 116 unsigned bytes_per_frame; /* frame size. */ 112 117 113 /* Sockets: */114 pj_sock_t rtp_sock; /* RTP socket. */115 pj_sock_t rtcp_sock; /* RTCP socket. */116 117 118 /* RTP session: */ 118 119 pjmedia_rtp_session out_sess; /* outgoing RTP session */ … … 123 124 124 125 /* Thread: */ 125 pj_bool_t thread_quit_flag; /* worker thread quit flag*/126 pj_thread_t *thread; /* RTP/RTCP worker thread*/126 pj_bool_t thread_quit_flag; /* Stop media thread. */ 127 pj_thread_t *thread; /* Media thread. */ 127 128 }; 128 129 129 130 131 /* This is a call structure that is created when the application starts 132 * and only destroyed when the application quits. 133 */ 130 134 struct call 131 135 { … … 133 137 pjsip_inv_session *inv; 134 138 unsigned media_count; 135 struct media_stream media[ 2];139 struct media_stream media[1]; 136 140 pj_time_val start_time; 137 141 pj_time_val response_time; … … 142 146 143 147 148 /* Application's global variables */ 144 149 static struct app 145 150 { … … 151 156 int sip_port; 152 157 int rtp_start_port; 153 char *local_addr;158 pj_str_t local_addr; 154 159 pj_str_t local_uri; 155 160 pj_str_t local_contact; … … 169 174 pjsip_endpoint *sip_endpt; 170 175 pj_bool_t thread_quit; 171 pj_thread_t * thread[1];176 pj_thread_t *sip_thread[1]; 172 177 173 178 pjmedia_endpt *med_endpt; … … 196 201 197 202 /* Worker thread prototype */ 198 static int worker_thread(void *arg);203 static int sip_worker_thread(void *arg); 199 204 200 205 /* Create SDP for call */ … … 208 213 /* Destroy the call's media */ 209 214 static void destroy_call_media(unsigned call_index); 215 216 /* Destroy media. */ 217 static void destroy_media(); 218 219 /* This callback is called by media transport on receipt of RTP packet. */ 220 static void on_rx_rtp(void *user_data, const void *pkt, pj_ssize_t size); 221 222 /* This callback is called by media transport on receipt of RTCP packet. */ 223 static void on_rx_rtcp(void *user_data, const void *pkt, pj_ssize_t size); 210 224 211 225 /* Display error */ … … 247 261 { 4, "G723", 8000, 6400, 30, "G.723.1" }, 248 262 { 8, "PCMA", 8000, 64000, 20, "G.711 ALaw" }, 249 { 18, "G729", 8000, 8000, 20, "G.729" },263 { 18, "G729", 8000, 8000, 20, "G.729" }, 250 264 }; 251 265 … … 256 270 static pj_status_t init_sip() 257 271 { 272 unsigned i; 258 273 pj_status_t status; 259 274 … … 268 283 app.pool = pj_pool_create(&app.cp.factory, "app", 1000, 1000, NULL); 269 284 270 /* Create global endpoint: */ 271 { 272 const pj_str_t *hostname; 273 const char *endpt_name; 274 275 /* Endpoint MUST be assigned a globally unique name. 276 * The name will be used as the hostname in Warning header. 277 */ 278 279 /* For this implementation, we'll use hostname for simplicity */ 280 hostname = pj_gethostname(); 281 endpt_name = hostname->ptr; 282 283 /* Create the endpoint: */ 284 285 status = pjsip_endpt_create(&app.cp.factory, endpt_name, 286 &app.sip_endpt); 287 PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); 288 } 285 /* Create the endpoint: */ 286 status = pjsip_endpt_create(&app.cp.factory, pj_gethostname()->ptr, 287 &app.sip_endpt); 288 PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); 289 289 290 290 … … 299 299 addr.sin_port = pj_htons((pj_uint16_t)app.sip_port); 300 300 301 if (app.local_addr ) {302 addrname.host = pj_str(app.local_addr);301 if (app.local_addr.slen) { 302 addrname.host = app.local_addr; 303 303 addrname.port = app.sip_port; 304 304 } 305 305 306 306 status = pjsip_udp_transport_start( app.sip_endpt, &addr, 307 (app.local_addr ? &addrname:NULL),307 (app.local_addr.slen ? &addrname:NULL), 308 308 1, NULL); 309 309 if (status != PJ_SUCCESS) { … … 343 343 PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); 344 344 345 /* Init calls */ 346 for (i=0; i<app.max_calls; ++i) 347 app.call[i].index = i; 345 348 346 349 /* Done */ … … 358 361 app.thread_quit = 1; 359 362 for (i=0; i<app.thread_count; ++i) { 360 if (app. thread[i]) {361 pj_thread_join(app. thread[i]);362 pj_thread_destroy(app. thread[i]);363 app. thread[i] = NULL;363 if (app.sip_thread[i]) { 364 pj_thread_join(app.sip_thread[i]); 365 pj_thread_destroy(app.sip_thread[i]); 366 app.sip_thread[i] = NULL; 364 367 } 365 368 } … … 377 380 static pj_status_t init_media() 378 381 { 379 pj_ioqueue_t *ioqueue;380 382 unsigned i, count; 381 383 pj_uint16_t rtp_port; 382 pj_str_t temp;383 pj_sockaddr_in addr;384 384 pj_status_t status; 385 386 387 /* Get the ioqueue from the SIP endpoint */388 ioqueue = pjsip_endpt_get_ioqueue(app.sip_endpt);389 385 390 386 … … 392 388 * initialized. 393 389 */ 394 status = pjmedia_endpt_create(&app.cp.factory, ioqueue, 1, 395 &app.med_endpt); 390 status = pjmedia_endpt_create(&app.cp.factory, NULL, 1, &app.med_endpt); 396 391 PJ_ASSERT_RETURN(status == PJ_SUCCESS, status); 397 392 398 393 399 /* Add G711 codec*/394 /* Must register codecs to be supported */ 400 395 pjmedia_codec_g711_init(app.med_endpt); 401 402 /* Determine address to bind socket */403 pj_memset(&addr, 0, sizeof(addr));404 addr.sin_family = PJ_AF_INET;405 i = pj_inet_aton(pj_cstr(&temp, app.local_addr), &addr.sin_addr);406 if (i == 0) {407 PJ_LOG(3,(THIS_FILE,408 "Error: invalid local address %s (expecting IP)",409 app.local_addr));410 return -1;411 }412 396 413 397 /* RTP port counter */ 414 398 rtp_port = (pj_uint16_t)(app.rtp_start_port & 0xFFFE); 415 399 416 417 /* Init media sockets. */ 400 /* Init media transport for all calls. */ 418 401 for (i=0, count=0; i<app.max_calls; ++i, ++count) { 419 402 420 int retry;421 422 app.call[i].index = i;423 424 /* Repeat binding media socket to next port when fails to bind425 * to current port number.426 */427 retry = 0;428 status = -1; 429 for (retry=0; status!=PJ_SUCCESS && retry<100; ++retry,rtp_port+=2) {430 struct media_stream *m = &app.call[i].media[0];431 432 m->port = rtp_port;433 434 /* Create and bind RTP socket */435 status = pj_sock_socket(PJ_AF_INET, PJ_SOCK_DGRAM, 0,436 &m->rtp_sock);437 if (status != PJ_SUCCESS)438 goto on_error;439 440 addr.sin_port = pj_htons(rtp_port);441 status = pj_sock_bind(m->rtp_sock, &addr, sizeof(addr));442 if (status != PJ_SUCCESS) {443 pj_sock_close(m->rtp_sock), m->rtp_sock=0;444 continue;403 unsigned j; 404 405 /* Create transport for each media in the call */ 406 for (j=0; j<PJ_ARRAY_SIZE(app.call[0].media); ++j) { 407 /* Repeat binding media socket to next port when fails to bind 408 * to current port number. 409 */ 410 int retry; 411 412 app.call[i].media[j].call_index = i; 413 app.call[i].media[j].media_index = j; 414 415 status = -1; 416 for (retry=0; retry<100; ++retry,rtp_port+=2) { 417 struct media_stream *m = &app.call[i].media[j]; 418 419 status = pjmedia_transport_udp_create2(app.med_endpt, 420 "siprtp", 421 &app.local_addr, 422 rtp_port, 0, 423 &m->transport); 424 if (status == PJ_SUCCESS) { 425 rtp_port += 2; 426 break; 427 } 445 428 } 446 447 448 /* Create and bind RTCP socket */449 status = pj_sock_socket(PJ_AF_INET, PJ_SOCK_DGRAM, 0,450 &m->rtcp_sock);451 if (status != PJ_SUCCESS)452 goto on_error;453 454 addr.sin_port = pj_htons((pj_uint16_t)(rtp_port+1));455 status = pj_sock_bind(m->rtcp_sock, &addr, sizeof(addr));456 if (status != PJ_SUCCESS) {457 pj_sock_close(m->rtp_sock), m->rtp_sock=0;458 pj_sock_close(m->rtcp_sock), m->rtcp_sock=0;459 continue;460 }461 462 429 } 463 430 464 431 if (status != PJ_SUCCESS) 465 432 goto on_error; 466 467 433 } 468 434 … … 471 437 472 438 on_error: 473 for (i=0; i<count; ++i) { 474 struct media_stream *m = &app.call[i].media[0]; 475 476 pj_sock_close(m->rtp_sock), m->rtp_sock=0; 477 pj_sock_close(m->rtcp_sock), m->rtcp_sock=0; 478 } 479 439 destroy_media(); 480 440 return status; 481 441 } … … 490 450 491 451 for (i=0; i<app.max_calls; ++i) { 492 struct media_stream *m = &app.call[i].media[0]; 493 494 if (m->rtp_sock) 495 pj_sock_close(m->rtp_sock), m->rtp_sock = 0; 496 497 if (m->rtcp_sock) 498 pj_sock_close(m->rtcp_sock), m->rtcp_sock = 0; 452 unsigned j; 453 for (j=0; j<PJ_ARRAY_SIZE(app.call[0].media); ++j) { 454 struct media_stream *m = &app.call[i].media[j]; 455 456 if (m->transport) { 457 pjmedia_transport_close(m->transport); 458 m->transport = NULL; 459 } 460 } 499 461 } 500 462 … … 795 757 796 758 797 /* Worker thread */798 static int worker_thread(void *arg)759 /* Worker thread for SIP */ 760 static int sip_worker_thread(void *arg) 799 761 { 800 762 PJ_UNUSED_ARG(arg); … … 864 826 app.sip_port = 5060; 865 827 app.rtp_start_port = RTP_START_PORT; 866 app.local_addr = ip_addr;828 app.local_addr = pj_str(ip_addr); 867 829 app.log_level = 5; 868 830 app.app_log_level = 3; … … 899 861 break; 900 862 case 'i': 901 app.local_addr = pj_ optarg;863 app.local_addr = pj_str(pj_optarg); 902 864 break; 903 865 … … 942 904 943 905 /* Build local URI and contact */ 944 pj_ansi_sprintf( local_uri, "sip:%s:%d", app.local_addr , app.sip_port);906 pj_ansi_sprintf( local_uri, "sip:%s:%d", app.local_addr.ptr, app.sip_port); 945 907 app.local_uri = pj_str(local_uri); 946 908 app.local_contact = app.local_uri; … … 966 928 pjmedia_sdp_media *m; 967 929 pjmedia_sdp_attr *attr; 930 pjmedia_transport_udp_info tpinfo; 968 931 struct media_stream *audio = &call->media[0]; 969 932 970 933 PJ_ASSERT_RETURN(pool && p_sdp, PJ_EINVAL); 971 934 935 936 /* Get transport info */ 937 pjmedia_transport_udp_get_info(audio->transport, &tpinfo); 972 938 973 939 /* Create and initialize basic SDP session */ … … 988 954 sdp->conn->net_type = pj_str("IN"); 989 955 sdp->conn->addr_type = pj_str("IP4"); 990 sdp->conn->addr = pj_str(app.local_addr);956 sdp->conn->addr = app.local_addr; 991 957 992 958 … … 1003 969 /* Standard media info: */ 1004 970 m->desc.media = pj_str("audio"); 1005 m->desc.port = audio->port;971 m->desc.port = pj_ntohs(tpinfo.skinfo.rtp_addr_name.sin_port); 1006 972 m->desc.port_count = 1; 1007 973 m->desc.transport = pj_str("RTP/AVP"); … … 1068 1034 #endif 1069 1035 1036 1037 /* 1038 * This callback is called by media transport on receipt of RTP packet. 1039 */ 1040 static void on_rx_rtp(void *user_data, const void *pkt, pj_ssize_t size) 1041 { 1042 struct media_stream *strm; 1043 pj_status_t status; 1044 const pjmedia_rtp_hdr *hdr; 1045 const void *payload; 1046 unsigned payload_len; 1047 1048 strm = user_data; 1049 1050 /* Discard packet if media is inactive */ 1051 if (!strm->active) 1052 return; 1053 1054 /* Check for errors */ 1055 if (size < 0) { 1056 app_perror(THIS_FILE, "RTP recv() error", -size); 1057 return; 1058 } 1059 1060 /* Decode RTP packet. */ 1061 status = pjmedia_rtp_decode_rtp(&strm->in_sess, 1062 pkt, size, 1063 &hdr, &payload, &payload_len); 1064 if (status != PJ_SUCCESS) { 1065 app_perror(THIS_FILE, "RTP decode error", status); 1066 return; 1067 } 1068 1069 //PJ_LOG(4,(THIS_FILE, "Rx seq=%d", pj_ntohs(hdr->seq))); 1070 1071 /* Update the RTCP session. */ 1072 pjmedia_rtcp_rx_rtp(&strm->rtcp, pj_ntohs(hdr->seq), 1073 pj_ntohl(hdr->ts), payload_len); 1074 1075 /* Update RTP session */ 1076 pjmedia_rtp_session_update(&strm->in_sess, hdr, NULL); 1077 1078 } 1079 1080 /* 1081 * This callback is called by media transport on receipt of RTCP packet. 1082 */ 1083 static void on_rx_rtcp(void *user_data, const void *pkt, pj_ssize_t size) 1084 { 1085 struct media_stream *strm; 1086 1087 strm = user_data; 1088 1089 /* Discard packet if media is inactive */ 1090 if (!strm->active) 1091 return; 1092 1093 /* Check for errors */ 1094 if (size < 0) { 1095 app_perror(THIS_FILE, "Error receiving RTCP packet", -size); 1096 return; 1097 } 1098 1099 /* Update RTCP session */ 1100 pjmedia_rtcp_rx_rtcp(&strm->rtcp, pkt, size); 1101 } 1102 1103 1070 1104 /* 1071 1105 * Media thread … … 1085 1119 boost_priority(); 1086 1120 1121 /* Let things settle */ 1122 pj_thread_sleep(1000); 1087 1123 1088 1124 msec_interval = strm->samples_per_frame * 1000 / strm->clock_rate; … … 1097 1133 1098 1134 while (!strm->thread_quit_flag) { 1099 pj_fd_set_t set;1100 1135 pj_timestamp now, lesser; 1101 1136 pj_time_val timeout; 1102 int rc; 1137 pj_bool_t send_rtp, send_rtcp; 1138 1139 send_rtp = send_rtcp = PJ_FALSE; 1103 1140 1104 1141 /* Determine how long to sleep */ 1105 if (next_rtp.u64 < next_rtcp.u64) 1142 if (next_rtp.u64 < next_rtcp.u64) { 1106 1143 lesser = next_rtp; 1107 else 1144 send_rtp = PJ_TRUE; 1145 } else { 1108 1146 lesser = next_rtcp; 1147 send_rtcp = PJ_TRUE; 1148 } 1109 1149 1110 1150 pj_get_timestamp(&now); … … 1122 1162 } 1123 1163 1124 PJ_FD_ZERO(&set); 1125 PJ_FD_SET(strm->rtp_sock, &set); 1126 PJ_FD_SET(strm->rtcp_sock, &set); 1127 1128 rc = pj_sock_select(FD_SETSIZE, &set, NULL, NULL, &timeout); 1129 1130 if (rc < 0) { 1131 pj_thread_sleep(10); 1132 continue; 1133 } 1134 1135 if (rc > 0 && PJ_FD_ISSET(strm->rtp_sock, &set)) { 1136 1137 /* 1138 * Process incoming RTP packet. 1139 */ 1140 pj_status_t status; 1141 pj_ssize_t size; 1142 const pjmedia_rtp_hdr *hdr; 1143 const void *payload; 1144 unsigned payload_len; 1145 1146 size = sizeof(packet); 1147 status = pj_sock_recv(strm->rtp_sock, packet, &size, 0); 1148 if (status != PJ_SUCCESS) { 1149 app_perror(THIS_FILE, "RTP recv() error", status); 1150 pj_thread_sleep(10); 1151 continue; 1152 } 1153 1154 1155 /* Decode RTP packet. */ 1156 status = pjmedia_rtp_decode_rtp(&strm->in_sess, 1157 packet, size, 1158 &hdr, 1159 &payload, &payload_len); 1160 if (status != PJ_SUCCESS) { 1161 app_perror(THIS_FILE, "RTP decode error", status); 1162 continue; 1163 } 1164 1165 /* Update the RTCP session. */ 1166 pjmedia_rtcp_rx_rtp(&strm->rtcp, pj_ntohs(hdr->seq), 1167 pj_ntohl(hdr->ts), payload_len); 1168 1169 /* Update RTP session */ 1170 pjmedia_rtp_session_update(&strm->in_sess, hdr, NULL); 1171 } 1172 1173 if (rc > 0 && PJ_FD_ISSET(strm->rtcp_sock, &set)) { 1174 1175 /* 1176 * Process incoming RTCP 1177 */ 1178 pj_status_t status; 1179 pj_ssize_t size; 1180 1181 size = sizeof(packet); 1182 status = pj_sock_recv( strm->rtcp_sock, packet, &size, 0); 1183 if (status != PJ_SUCCESS) { 1184 app_perror(THIS_FILE, "Error receiving RTCP packet", status); 1185 pj_thread_sleep(10); 1186 } else 1187 pjmedia_rtcp_rx_rtcp(&strm->rtcp, packet, size); 1188 } 1189 1164 /* Wait for next interval */ 1165 //if (timeout.sec!=0 && timeout.msec!=0) { 1166 pj_thread_sleep(PJ_TIME_VAL_MSEC(timeout)); 1167 if (strm->thread_quit_flag) 1168 break; 1169 //} 1190 1170 1191 1171 pj_get_timestamp(&now); 1192 1172 1193 if ( next_rtp.u64 <= now.u64) {1173 if (send_rtp || next_rtp.u64 <= now.u64) { 1194 1174 /* 1195 1175 * Time to send RTP packet. … … 1208 1188 if (status == PJ_SUCCESS) { 1209 1189 1190 //PJ_LOG(4,(THIS_FILE, "\t\tTx seq=%d", pj_ntohs(hdr->seq))); 1191 1210 1192 /* Copy RTP header to packet */ 1211 1193 pj_memcpy(packet, hdr, hdrlen); … … 1216 1198 /* Send RTP packet */ 1217 1199 size = hdrlen + strm->bytes_per_frame; 1218 status = pj_sock_sendto( strm->rtp_sock, packet, &size, 0, 1219 &strm->si.rem_addr, 1220 sizeof(strm->si.rem_addr)); 1221 1200 status = pjmedia_transport_send_rtp(strm->transport, 1201 packet, size); 1222 1202 if (status != PJ_SUCCESS) 1223 1203 app_perror(THIS_FILE, "Error sending RTP packet", status); 1224 1204 1205 } else { 1206 pj_assert(!"RTP encode() error"); 1225 1207 } 1226 1208 … … 1233 1215 1234 1216 1235 if ( next_rtcp.u64 <= now.u64) {1217 if (send_rtcp || next_rtcp.u64 <= now.u64) { 1236 1218 /* 1237 1219 * Time to send RTCP packet. … … 1239 1221 pjmedia_rtcp_pkt *rtcp_pkt; 1240 1222 int rtcp_len; 1241 pj_sockaddr_in rem_addr;1242 1223 pj_ssize_t size; 1243 int port;1244 1224 pj_status_t status; 1245 1225 … … 1248 1228 1249 1229 1250 /* Calculate address based on RTP address */1251 rem_addr = strm->si.rem_addr;1252 port = pj_ntohs(strm->si.rem_addr.sin_port) + 1;1253 rem_addr.sin_port = pj_htons((pj_uint16_t)port);1254 1255 1230 /* Send packet */ 1256 1231 size = rtcp_len; 1257 status = pj _sock_sendto(strm->rtcp_sock, rtcp_pkt, &size, 0,1258 &rem_addr, sizeof(rem_addr));1232 status = pjmedia_transport_send_rtcp(strm->transport, 1233 rtcp_pkt, size); 1259 1234 if (status != PJ_SUCCESS) { 1260 1235 app_perror(THIS_FILE, "Error sending RTCP packet", status); … … 1339 1314 1340 1315 1316 /* Attach media to transport */ 1317 status = pjmedia_transport_attach(audio->transport, audio, 1318 &audio->si.rem_addr, 1319 sizeof(pj_sockaddr_in), 1320 &on_rx_rtp, 1321 &on_rx_rtcp); 1322 if (status != PJ_SUCCESS) { 1323 app_perror(THIS_FILE, "Error on pjmedia_transport_attach()", status); 1324 return; 1325 } 1341 1326 1342 1327 /* Start media thread. */ … … 1346 1331 if (status != PJ_SUCCESS) { 1347 1332 app_perror(THIS_FILE, "Error creating media thread", status); 1348 } 1333 return; 1334 } 1335 1336 /* Set the media as active */ 1337 audio->active = PJ_TRUE; 1349 1338 } 1350 1339 … … 1357 1346 1358 1347 if (audio->thread) { 1348 1349 audio->active = PJ_FALSE; 1350 1359 1351 audio->thread_quit_flag = 1; 1360 1352 pj_thread_join(audio->thread); … … 1363 1355 audio->thread_quit_flag = 0; 1364 1356 1365 /* Flush RTP/RTCP packets */ 1366 { 1367 pj_fd_set_t set; 1368 pj_time_val timeout = {0, 0}; 1369 char packet[1500]; 1370 pj_ssize_t size; 1371 pj_status_t status; 1372 int rc; 1373 1374 do { 1375 PJ_FD_ZERO(&set); 1376 PJ_FD_SET(audio->rtp_sock, &set); 1377 PJ_FD_SET(audio->rtcp_sock, &set); 1378 1379 rc = pj_sock_select(FD_SETSIZE, &set, NULL, NULL, &timeout); 1380 if (rc > 0 && PJ_FD_ISSET(audio->rtp_sock, &set)) { 1381 size = sizeof(packet); 1382 status = pj_sock_recv(audio->rtp_sock, packet, &size, 0); 1383 1384 } 1385 if (rc > 0 && PJ_FD_ISSET(audio->rtcp_sock, &set)) { 1386 size = sizeof(packet); 1387 status = pj_sock_recv(audio->rtcp_sock, packet, &size, 0); 1388 } 1389 1390 } while (rc > 0); 1391 } 1357 pjmedia_transport_detach(audio->transport, audio); 1392 1358 } 1393 1359 } … … 1431 1397 continue; 1432 1398 hangup_call(i); 1399 } 1400 1401 /* Wait until all calls are terminated */ 1402 for (i=0; i<app.max_calls; ++i) { 1403 while (app.call[i].inv) 1404 pj_thread_sleep(10); 1433 1405 } 1434 1406 } … … 1682 1654 /* Start worker threads */ 1683 1655 for (i=0; i<app.thread_count; ++i) { 1684 pj_thread_create( app.pool, "app", & worker_thread, NULL,1685 0, 0, &app. thread[i]);1656 pj_thread_create( app.pool, "app", &sip_worker_thread, NULL, 1657 0, 0, &app.sip_thread[i]); 1686 1658 } 1687 1659
Note: See TracChangeset
for help on using the changeset viewer.