15dbd6af9bc0c5a3adc46b0718b8477ce9b77b77
[asterisk/asterisk.git] / jitterbuf.c
1 /*
2  * jitterbuf: an application-independent jitterbuffer
3  *
4  * Copyrights:
5  * Copyright (C) 2004-2005, Horizon Wimba, Inc.
6  *
7  * Contributors:
8  * Steve Kann <stevek@stevek.com>
9  *
10  * This program is free software, distributed under the terms of
11  * the GNU Lesser (Library) General Public License
12  *
13  * Copyright on this file is disclaimed to Digium for inclusion in Asterisk
14  */
15
16 #include "jitterbuf.h"
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h>
20
21 /* define these here, just for ancient compiler systems */
22 #define JB_LONGMAX 2147483647L
23 #define JB_LONGMIN (-JB_LONGMAX - 1L)
24
25 #define jb_warn(...) (warnf ? warnf(__VA_ARGS__) : (void)0)
26 #define jb_err(...) (errf ? errf(__VA_ARGS__) : (void)0)
27 #define jb_dbg(...) (dbgf ? dbgf(__VA_ARGS__) : (void)0)
28
29 #ifdef DEEP_DEBUG
30 #define jb_dbg2(...) (dbgf ? dbgf(__VA_ARGS__) : (void)0)
31 #else
32 #define jb_dbg2(...) ((void)0)
33 #endif
34
35 static jb_output_function_t warnf, errf, dbgf;
36
37 void jb_setoutput(jb_output_function_t warn, jb_output_function_t err, jb_output_function_t dbg) 
38 {
39         warnf = warn;
40         errf = err;
41         dbgf = dbg;
42 }
43
44 static void increment_losspct(jitterbuf *jb) 
45 {
46         jb->info.losspct = (100000 + 499 * jb->info.losspct)/500;    
47 }
48
49 static void decrement_losspct(jitterbuf *jb) 
50 {
51         jb->info.losspct = (499 * jb->info.losspct)/500;    
52 }
53
54
55 static void jb_dbginfo(jitterbuf *jb);
56
57
58 void jb_reset(jitterbuf *jb) 
59 {
60         memset(jb,0,sizeof(jitterbuf));
61
62         /* initialize length */
63         jb->info.current = jb->info.target = 0; 
64         jb->info.silence = 1; 
65 }
66
67 jitterbuf * jb_new() 
68 {
69         jitterbuf *jb;
70
71
72         jb = malloc(sizeof(jitterbuf));
73         if (!jb) 
74                 return NULL;
75
76         jb_reset(jb);
77
78         jb_dbg2("jb_new() = %x\n", jb);
79         return jb;
80 }
81
82 void jb_destroy(jitterbuf *jb) 
83 {
84         jb_frame *frame; 
85         jb_dbg2("jb_destroy(%x)\n", jb);
86
87         /* free all the frames on the "free list" */
88         frame = jb->free;
89         while (frame != NULL) {
90                 jb_frame *next = frame->next;
91                 free(frame);
92                 frame = next;
93         }
94
95         /* free ourselves! */ 
96         free(jb);
97 }
98
99
100
101 /* simple history manipulation */
102 /* maybe later we can make the history buckets variable size, or something? */
103 /* drop parameter determines whether we will drop outliers to minimize
104  * delay */
105 static int longcmp(const void *a, const void *b) 
106 {
107         return *(long *)a - *(long *)b;
108 }
109
110 static void history_put(jitterbuf *jb, long ts, long now) 
111 {
112         long delay = now - ts;
113         long kicked;
114
115         /* don't add special/negative times to history */
116         if (ts <= 0) 
117                 return;
118
119         kicked = jb->history[jb->hist_ptr & JB_HISTORY_SZ];
120
121         jb->history[(jb->hist_ptr++) % JB_HISTORY_SZ] = delay;
122
123         /* optimization; the max/min buffers don't need to be recalculated, if this packet's
124          * entry doesn't change them.  This happens if this packet is not involved, _and_ any packet
125          * that got kicked out of the history is also not involved 
126          * We do a number of comparisons, but it's probably still worthwhile, because it will usually
127          * succeed, and should be a lot faster than going through all 500 packets in history */
128         if (!jb->hist_maxbuf_valid)
129                 return;
130
131         /* don't do this until we've filled history 
132          * (reduces some edge cases below) */
133         if (jb->hist_ptr < JB_HISTORY_SZ)
134                 goto invalidate;
135
136         /* if the new delay would go into min */
137         if (delay < jb->hist_minbuf[JB_HISTORY_MAXBUF_SZ-1])
138                 goto invalidate;
139     
140         /* or max.. */
141         if (delay > jb->hist_maxbuf[JB_HISTORY_MAXBUF_SZ-1])
142                 goto invalidate;
143
144         /* or the kicked delay would be in min */
145         if (kicked <= jb->hist_minbuf[JB_HISTORY_MAXBUF_SZ-1]) 
146                 goto invalidate;
147
148         if (kicked >= jb->hist_maxbuf[JB_HISTORY_MAXBUF_SZ-1]) 
149                 goto invalidate;
150
151         /* if we got here, we don't need to invalidate, 'cause this delay didn't 
152          * affect things */
153         return;
154         /* end optimization */
155
156
157 invalidate:
158         jb->hist_maxbuf_valid = 0;
159         return;
160 }
161
162 static void history_calc_maxbuf(jitterbuf *jb) 
163 {
164         int i,j;
165
166         if (jb->hist_ptr == 0) 
167                 return;
168
169
170         /* initialize maxbuf/minbuf to the latest value */
171         for (i=0;i<JB_HISTORY_MAXBUF_SZ;i++) {
172 /*
173  * jb->hist_maxbuf[i] = jb->history[(jb->hist_ptr-1) % JB_HISTORY_SZ];
174  * jb->hist_minbuf[i] = jb->history[(jb->hist_ptr-1) % JB_HISTORY_SZ];
175  */
176                 jb->hist_maxbuf[i] = JB_LONGMIN;
177                 jb->hist_minbuf[i] = JB_LONGMAX;
178         }
179
180         /* use insertion sort to populate maxbuf */
181         /* we want it to be the top "n" values, in order */
182
183         /* start at the beginning, or JB_HISTORY_SZ frames ago */
184         i = (jb->hist_ptr > JB_HISTORY_SZ) ? (jb->hist_ptr - JB_HISTORY_SZ) : 0; 
185
186         for (;i<jb->hist_ptr;i++) {
187                 long toins = jb->history[i % JB_HISTORY_SZ];
188
189                 /* if the maxbuf should get this */
190                 if (toins > jb->hist_maxbuf[JB_HISTORY_MAXBUF_SZ-1])  {
191
192                         /* insertion-sort it into the maxbuf */
193                         for (j=0;j<JB_HISTORY_MAXBUF_SZ;j++) {
194                                 /* found where it fits */
195                                 if (toins > jb->hist_maxbuf[j]) {
196                                         /* move over */
197                                         memmove(jb->hist_maxbuf+j+1,jb->hist_maxbuf+j, (JB_HISTORY_MAXBUF_SZ-(j+1)) * sizeof(long));
198                                         /* insert */
199                                         jb->hist_maxbuf[j] = toins;
200
201                                         break;
202                                 }
203                         }
204                 }
205
206                 /* if the minbuf should get this */
207                 if (toins < jb->hist_minbuf[JB_HISTORY_MAXBUF_SZ-1])  {
208
209                         /* insertion-sort it into the maxbuf */
210                         for (j=0;j<JB_HISTORY_MAXBUF_SZ;j++) {
211                                 /* found where it fits */
212                                 if (toins < jb->hist_minbuf[j]) {
213                                         /* move over */
214                                         memmove(jb->hist_minbuf+j+1,jb->hist_minbuf+j, (JB_HISTORY_MAXBUF_SZ-(j+1)) * sizeof(long));
215                                         /* insert */
216                                         jb->hist_minbuf[j] = toins;
217
218                                         break;
219                                 }
220                         }
221                 }
222
223                 if (0) { 
224                         int k;
225                         fprintf(stderr, "toins = %ld\n", toins);
226                         fprintf(stderr, "maxbuf =");
227                         for (k=0;k<JB_HISTORY_MAXBUF_SZ;k++) 
228                                 fprintf(stderr, "%ld ", jb->hist_maxbuf[k]);
229                         fprintf(stderr, "\nminbuf =");
230                         for (k=0;k<JB_HISTORY_MAXBUF_SZ;k++) 
231                                 fprintf(stderr, "%ld ", jb->hist_minbuf[k]);
232                         fprintf(stderr, "\n");
233                 }
234         }
235
236         jb->hist_maxbuf_valid = 1;
237 }
238
239 static void history_get(jitterbuf *jb) 
240 {
241         long max, min, jitter;
242         int index;
243         int count;
244
245         if (!jb->hist_maxbuf_valid) 
246                 history_calc_maxbuf(jb);
247
248         /* count is how many items in history we're examining */
249         count = (jb->hist_ptr < JB_HISTORY_SZ) ? jb->hist_ptr : JB_HISTORY_SZ;
250
251         /* index is the "n"ths highest/lowest that we'll look for */
252         index = count * JB_HISTORY_DROPPCT / 100;
253
254         /* sanity checks for index */
255         if (index > (JB_HISTORY_MAXBUF_SZ - 1)) 
256                 index = JB_HISTORY_MAXBUF_SZ - 1;
257
258
259         if (index < 0) {
260                 jb->info.min = 0;
261                 jb->info.jitter = 0;
262                 return;
263         }
264
265         max = jb->hist_maxbuf[index];
266         min = jb->hist_minbuf[index];
267
268         jitter = max - min;
269
270         /* these debug stmts compare the difference between looking at the absolute jitter, and the
271          * values we get by throwing away the outliers */
272         /*
273         fprintf(stderr, "[%d] min=%d, max=%d, jitter=%d\n", index, min, max, jitter);
274         fprintf(stderr, "[%d] min=%d, max=%d, jitter=%d\n", 0, jb->hist_minbuf[0], jb->hist_maxbuf[0], jb->hist_maxbuf[0]-jb->hist_minbuf[0]);
275         */
276
277         jb->info.min = min;
278         jb->info.jitter = jitter;
279 }
280
281 static void queue_put(jitterbuf *jb, void *data, int type, long ms, long ts) 
282 {
283         jb_frame *frame;
284         jb_frame *p;
285
286         frame = jb->free;
287         if (frame) {
288                 jb->free = frame->next;
289         } else {
290                 frame = malloc(sizeof(jb_frame));
291         }
292
293         if (!frame) {
294                 jb_err("cannot allocate frame\n");
295                 return;
296         }
297
298         jb->info.frames_cur++;
299
300         frame->data = data;
301         frame->ts = ts;
302         frame->ms = ms;
303         frame->type = type;
304
305         /* 
306          * frames are a circular list, jb-frames points to to the lowest ts, 
307          * jb->frames->prev points to the highest ts
308          */
309
310         if (!jb->frames) {  /* queue is empty */
311                 jb->frames = frame;
312                 frame->next = frame;
313                 frame->prev = frame;
314                 frame->prev = jb->frames->prev;
315
316                 frame->next->prev = frame;
317                 frame->prev->next = frame;
318
319                 jb->frames = frame;
320         } else { 
321                 p = jb->frames;
322
323                 /* frame is out of order */
324                 if (ts < p->prev->ts) jb->info.frames_ooo++;
325
326                 while (ts < p->prev->ts && p->prev != jb->frames) 
327                         p = p->prev;
328
329                 frame->next = p;
330                 frame->prev = p->prev;
331
332                 frame->next->prev = frame;
333                 frame->prev->next = frame;
334         }
335 }
336
337 static long queue_next(jitterbuf *jb) 
338 {
339         if (jb->frames) 
340                 return jb->frames->ts;
341         else 
342                 return -1;
343 }
344
345 static long queue_last(jitterbuf *jb) 
346 {
347         if (jb->frames) 
348                 return jb->frames->prev->ts;
349         else 
350                 return -1;
351 }
352
353 static jb_frame *_queue_get(jitterbuf *jb, long ts, int all) 
354 {
355         jb_frame *frame;
356         frame = jb->frames;
357
358         if (!frame)
359                 return NULL;
360
361         /*jb_warn("queue_get: ASK %ld FIRST %ld\n", ts, frame->ts); */
362
363         if (all || ts > frame->ts) {
364                 /* remove this frame */
365                 frame->prev->next = frame->next;
366                 frame->next->prev = frame->prev;
367
368                 if (frame->next == frame)
369                         jb->frames = NULL;
370                 else
371                         jb->frames = frame->next;
372
373
374                 /* insert onto "free" single-linked list */
375                 frame->next = jb->free;
376                 jb->free = frame;
377
378                 jb->info.frames_cur--;
379
380                 /* we return the frame pointer, even though it's on free list, 
381                  * but caller must copy data */
382                 return frame;
383         } 
384
385         return NULL;
386 }
387
388 static jb_frame *queue_get(jitterbuf *jb, long ts) 
389 {
390         return _queue_get(jb,ts,0);
391 }
392
393 static jb_frame *queue_getall(jitterbuf *jb) 
394 {
395         return _queue_get(jb,0,1);
396 }
397
398 /* some diagnostics */
399 static void jb_dbginfo(jitterbuf *jb) 
400 {
401         if (dbgf == NULL) 
402                 return;
403
404         jb_dbg("\njb info: fin=%ld fout=%ld flate=%ld flost=%ld fdrop=%ld fcur=%ld\n",
405                 jb->info.frames_in, jb->info.frames_out, jb->info.frames_late, jb->info.frames_lost, jb->info.frames_dropped, jb->info.frames_cur);
406         
407         jb_dbg("jitter=%ld current=%ld target=%ld min=%ld sil=%d len=%d len/fcur=%ld\n",
408                 jb->info.jitter, jb->info.current, jb->info.target, jb->info.min, jb->info.silence, jb->info.current - jb->info.min, 
409                 jb->info.frames_cur ? (jb->info.current - jb->info.min)/jb->info.frames_cur : -8);
410         if (jb->info.frames_in > 0) 
411                 jb_dbg("jb info: Loss PCT = %ld%%, Late PCT = %ld%%\n",
412                         jb->info.frames_lost * 100/(jb->info.frames_in + jb->info.frames_lost), 
413                         jb->info.frames_late * 100/jb->info.frames_in);
414         jb_dbg("jb info: queue %d -> %d.  last_ts %d (queue len: %d) last_ms %d\n",
415                 queue_next(jb), 
416                 queue_last(jb),
417                 jb->info.last_voice_ts, 
418                 queue_last(jb) - queue_next(jb),
419                 jb->info.last_voice_ms);
420 }
421
422 #ifdef DEEP_DEBUG
423 static void jb_chkqueue(jitterbuf *jb) 
424 {
425         int i=0;
426         jb_frame *p = jb->frames;
427
428         if (!p) {
429                 return;
430         }
431
432         do {
433                 if (p->next == NULL)  {
434                         jb_err("Queue is BROKEN at item [%d]", i);      
435                 }
436                 i++;
437                 p=p->next;
438         } while (p->next != jb->frames);
439 }
440
441 static void jb_dbgqueue(jitterbuf *jb) 
442 {
443         int i=0;
444         jb_frame *p = jb->frames;
445
446         jb_dbg("queue: ");
447
448         if (!p) {
449                 jb_dbg("EMPTY\n");
450                 return;
451         }
452
453         do {
454                 jb_dbg("[%d]=%ld ", i++, p->ts);
455                 p=p->next;
456         } while (p->next != jb->frames);
457
458         jb_dbg("\n");
459 }
460 #endif
461
462 int jb_put(jitterbuf *jb, void *data, int type, long ms, long ts, long now) 
463 {
464         jb_dbg2("jb_put(%x,%x,%ld,%ld,%ld)\n", jb, data, ms, ts, now);
465
466         jb->info.frames_in++;
467
468         if (type == JB_TYPE_VOICE) {
469                 /* presently, I'm only adding VOICE frames to history and drift calculations; mostly because with the
470                  * IAX integrations, I'm sending retransmitted control frames with their awkward timestamps through */
471                 history_put(jb,ts,now);
472         }
473
474         queue_put(jb,data,type,ms,ts);
475
476         return JB_OK;
477 }
478
479
480 static int _jb_get(jitterbuf *jb, jb_frame *frameout, long now) 
481 {
482         jb_frame *frame;
483         long diff;
484
485         /*if ((now - jb_next(jb)) > 2 * jb->info.last_voice_ms) jb_warn("SCHED: %ld", (now - jb_next(jb))); */
486         /* get jitter info */
487         history_get(jb);
488
489
490         /* target */
491         jb->info.target = jb->info.jitter + jb->info.min + 2 * jb->info.last_voice_ms; 
492
493         /* if a hard clamp was requested, use it */
494         if ((jb->info.max_jitterbuf) && ((jb->info.target - jb->info.min) > jb->info.max_jitterbuf)) {
495                 jb_dbg("clamping target from %d to %d\n", (jb->info.target - jb->info.min), jb->info.max_jitterbuf);
496                         jb->info.target = jb->info.min + jb->info.max_jitterbuf;
497         }
498
499         diff = jb->info.target - jb->info.current;
500
501         /* jb_warn("diff = %d lms=%d last = %d now = %d\n", diff,  */
502         /*      jb->info.last_voice_ms, jb->info.last_adjustment, now); */
503
504         /* move up last_voice_ts; it is now the expected voice ts */
505         jb->info.last_voice_ts += jb->info.last_voice_ms;
506
507         /* let's work on non-silent case first */
508         if (!jb->info.silence) { 
509                 /* we want to grow */
510                 if ((diff > 0) && 
511                         /* we haven't grown in 2 frames' length */
512                         (((jb->info.last_adjustment + 2 * jb->info.last_voice_ms ) < now) || 
513                         /* we need to grow more than the "length" we have left */
514                         (diff > queue_last(jb)  - queue_next(jb)) ) ) {
515                         
516                         jb->info.current += jb->info.last_voice_ms;
517                         jb->info.last_adjustment = now;
518                         jb_dbg("G");
519                         return JB_INTERP;
520                 }
521
522                 frame = queue_get(jb, jb->info.last_voice_ts - jb->info.current);
523
524                 /* not a voice frame; just return it. */
525                 if (frame && frame->type != JB_TYPE_VOICE) {
526                         /* rewind last_voice_ts, since this isn't voice */
527                         jb->info.last_voice_ts -= jb->info.last_voice_ms;
528
529                         if (frame->type == JB_TYPE_SILENCE) 
530                                 jb->info.silence = 1;
531
532                         *frameout = *frame;
533                         jb->info.frames_out++;
534                         jb_dbg("o");
535                         return JB_OK;
536                 }
537
538
539                 /* voice frame is late */
540                 if (frame && frame->ts + jb->info.current < jb->info.last_voice_ts - jb->info.last_voice_ms ) {
541                         *frameout = *frame;
542                         /* rewind last_voice, since we're just dumping */
543                         jb->info.last_voice_ts -= jb->info.last_voice_ms;
544                         jb->info.frames_out++;
545                         decrement_losspct(jb);
546                         jb->info.frames_late++;
547                         jb->info.frames_lost--;
548                         jb_dbg("l");
549                         /*jb_warn("\nlate: wanted=%ld, this=%ld, next=%ld\n", jb->info.last_voice_ts - jb->info.current, frame->ts, queue_next(jb));
550                         jb_warninfo(jb); */
551                         return JB_DROP;
552                 }
553
554                 /* keep track of frame sizes, to allow for variable sized-frames */
555                 if (frame && frame->ms > 0) {
556                         jb->info.last_voice_ms = frame->ms;
557                 }
558
559                 /* we want to shrink; shrink at 1 frame / 500ms */
560                 if (diff < -2 * jb->info.last_voice_ms && 
561                 ((!frame && jb->info.last_adjustment + 80 < now) || 
562                         (jb->info.last_adjustment + 500 < now))) {
563
564                         /* don't increment last_ts ?? */
565                         jb->info.last_voice_ts -= jb->info.last_voice_ms;
566                         jb->info.current -= jb->info.last_voice_ms;
567                         jb->info.last_adjustment = now;
568
569                         if (frame) {
570                                 *frameout = *frame;
571                                 jb->info.frames_out++;
572                                 decrement_losspct(jb);
573                                 jb->info.frames_dropped++;
574                                 jb_dbg("s");
575                                 return JB_DROP;
576                         } else {
577                                 increment_losspct(jb);
578                                 jb_dbg("S");
579                                 return JB_NOFRAME;
580                         }
581                 }
582
583                 /* lost frame */
584                 if (!frame) {
585                         /* this is a bit of a hack for now, but if we're close to
586                          * target, and we find a missing frame, it makes sense to
587                          * grow, because the frame might just be a bit late;
588                          * otherwise, we presently get into a pattern where we return
589                          * INTERP for the lost frame, then it shows up next, and we
590                          * throw it away because it's late */
591                         /* I've recently only been able to replicate this using
592                          * iaxclient talking to app_echo on asterisk.  In this case,
593                          * my outgoing packets go through asterisk's (old)
594                          * jitterbuffer, and then might get an unusual increasing delay 
595                          * there if it decides to grow?? */
596                         /* Update: that might have been a different bug, that has been fixed..
597                          * But, this still seemed like a good idea, except that it ended up making a single actual
598                          * lost frame get interpolated two or more times, when there was "room" to grow, so it might
599                          * be a bit of a bad idea overall */
600                         /*if (diff > -1 * jb->info.last_voice_ms) { 
601                                 jb->info.current += jb->info.last_voice_ms;
602                                 jb->info.last_adjustment = now;
603                                 jb_warn("g");
604                                 return JB_INTERP;
605                         } */
606                         jb->info.frames_lost++;
607                         increment_losspct(jb);
608                         jb_dbg("L");
609                         return JB_INTERP;
610                 }
611
612                 /* normal case; return the frame, increment stuff */
613                 *frameout = *frame;
614                 jb->info.frames_out++;
615                 decrement_losspct(jb);
616                 jb_dbg("v");
617                 return JB_OK;
618         } else {     
619                 /* TODO: after we get the non-silent case down, we'll make the
620                  * silent case -- basically, we'll just grow and shrink faster
621                  * here, plus handle last_voice_ts a bit differently */
622       
623                 /* to disable silent special case altogether, just uncomment this: */
624                 /* jb->info.silence = 0; */
625
626                 frame = queue_get(jb, now - jb->info.current);
627                 if (!frame) {
628                         return JB_NOFRAME;
629                 }
630                 if (frame && frame->type == JB_TYPE_VOICE) {
631                         /* try setting current to target right away here */
632                         jb->info.current = jb->info.target;
633                         jb->info.silence = 0;
634                         jb->info.last_voice_ts = frame->ts + jb->info.current + frame->ms;
635                         jb->info.last_voice_ms = frame->ms;
636                         *frameout = *frame;
637                         jb_dbg("V");
638                         return JB_OK;
639                 }
640                 /* normal case; in silent mode, got a non-voice frame */
641                 *frameout = *frame;
642                 return JB_OK;
643         }
644 }
645
646 long jb_next(jitterbuf *jb) 
647 {
648         if (jb->info.silence) {
649                 long next = queue_next(jb);
650                 if (next > 0) { 
651                         history_get(jb);
652                         return next + jb->info.target;
653                 }
654                 else 
655                         return JB_LONGMAX;
656         } else {
657                 return jb->info.last_voice_ts + jb->info.last_voice_ms;
658         }
659 }
660
661 int jb_get(jitterbuf *jb, jb_frame *frameout, long now) 
662 {
663         int ret = _jb_get(jb,frameout,now);
664 #if 0
665         static int lastts=0;
666         int thists = ((ret == JB_OK) || (ret == JB_DROP)) ? frameout->ts : 0;
667         jb_warn("jb_get(%x,%x,%ld) = %d (%d)\n", jb, frameout, now, ret, thists);
668         if (thists && thists < lastts) jb_warn("XXXX timestamp roll-back!!!\n");
669         lastts = thists;
670 #endif
671         if(ret == JB_INTERP) 
672                 frameout->ms = jb->info.last_voice_ms;
673         
674         return ret;
675 }
676
677 int jb_getall(jitterbuf *jb, jb_frame *frameout) 
678 {
679         jb_frame *frame;
680         frame = queue_getall(jb);
681
682         if (!frame) {
683                 return JB_NOFRAME;
684         }
685
686         *frameout = *frame;
687         return JB_OK;
688 }
689
690
691 int jb_getinfo(jitterbuf *jb, jb_info *stats) 
692 {
693
694         history_get(jb);
695
696         *stats = jb->info;
697
698         return JB_OK;
699 }
700
701 int jb_setinfo(jitterbuf *jb, jb_info *settings) 
702 {
703         /* take selected settings from the struct */
704
705         jb->info.max_jitterbuf = settings->max_jitterbuf;
706
707         return JB_OK;
708 }
709
710