libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

mediautil.c (20510B)


      1 /*
      2      This file is part of libextractor.
      3      Copyright (C) 2026 Vidyut Samanta and Christian Grothoff
      4 
      5      libextractor is free software; you can redistribute it and/or modify
      6      it under the terms of the GNU General Public License as published
      7      by the Free Software Foundation; either version 3, or (at your
      8      option) any later version.
      9 
     10      libextractor is distributed in the hope that it will be useful, but
     11      WITHOUT ANY WARRANTY; without even the implied warranty of
     12      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13      General Public License for more details.
     14 
     15      You should have received a copy of the GNU General Public License
     16      along with libextractor; see the file COPYING.  If not, write to the
     17      Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     18      Boston, MA 02110-1301, USA.
     19  */
     20 /**
     21  * @file plugins/mediautil.c
     22  * @brief helpers shared by the video thumbnail and audio preview plugins
     23  * @author Christian Grothoff
     24  */
     25 #include "platform.h"
     26 #include "mediautil.h"
     27 #include <magic.h>
     28 
     29 /**
     30  * How many bytes we give libmagic to make up its mind.
     31  */
     32 #define SNIFF_SIZE (16 * 1024)
     33 
     34 
     35 void
     36 EXTRACTOR_media_options_parse_ (const char *config,
     37                                 struct EXTRACTOR_MediaOptions *opt)
     38 {
     39   const char *pos;
     40   int explicit_deadline = 0;
     41 
     42   opt->size = 128;
     43   opt->png = 0;
     44   opt->offset = 30;
     45   opt->length = 15;
     46   opt->bitrate = 24000;
     47   opt->deadline = 10000;
     48   opt->srt_lang[0] = '\0';
     49   opt->gallery = 0;
     50   if (NULL == config)
     51     return;
     52   pos = config;
     53   while ('\0' != *pos)
     54   {
     55     unsigned long long v;
     56     char *endp;
     57 
     58     if (0 == strncmp (pos,
     59                       "format=",
     60                       strlen ("format=")))
     61     {
     62       pos += strlen ("format=");
     63       opt->png = (0 == strncmp (pos,
     64                                 "png",
     65                                 strlen ("png")));
     66     }
     67     else if (0 == strncmp (pos,
     68                            "size=",
     69                            strlen ("size=")))
     70     {
     71       pos += strlen ("size=");
     72       v = strtoull (pos, &endp, 10);
     73       if ( (endp != pos) &&
     74            (v >= 8) &&
     75            (v <= 1024) )
     76         opt->size = (unsigned int) v;
     77     }
     78     else if (0 == strncmp (pos,
     79                            "offset=",
     80                            strlen ("offset=")))
     81     {
     82       pos += strlen ("offset=");
     83       v = strtoull (pos, &endp, 10);
     84       if ( (endp != pos) &&
     85            (v <= 24 * 60 * 60) )
     86         opt->offset = (unsigned int) v;
     87     }
     88     else if (0 == strncmp (pos,
     89                            "length=",
     90                            strlen ("length=")))
     91     {
     92       pos += strlen ("length=");
     93       v = strtoull (pos, &endp, 10);
     94       if ( (endp != pos) &&
     95            (v >= 1) &&
     96            (v <= 60) )
     97         opt->length = (unsigned int) v;
     98     }
     99     else if (0 == strncmp (pos,
    100                            "bitrate=",
    101                            strlen ("bitrate=")))
    102     {
    103       pos += strlen ("bitrate=");
    104       v = strtoull (pos, &endp, 10);
    105       if ( (endp != pos) &&
    106            (v >= 6000) &&
    107            (v <= 128000) )
    108         opt->bitrate = (unsigned int) v;
    109     }
    110     else if (0 == strncmp (pos,
    111                            "deadline=",
    112                            strlen ("deadline=")))
    113     {
    114       pos += strlen ("deadline=");
    115       v = strtoull (pos, &endp, 10);
    116       if ( (endp != pos) &&
    117            (v >= 100) &&
    118            (v <= 120000) )
    119       {
    120         opt->deadline = (unsigned int) v;
    121         explicit_deadline = 1;
    122       }
    123     }
    124     else if ( (0 == strncmp (pos,
    125                              "srt:",
    126                              strlen ("srt:"))) ||
    127               (0 == strncmp (pos,
    128                              "srt=",
    129                              strlen ("srt="))) )
    130     {
    131       size_t n;
    132 
    133       pos += strlen ("srt:");
    134       n = strcspn (pos, ",");
    135       if ( (n > 0) &&
    136            (n < sizeof (opt->srt_lang)) )
    137       {
    138         memcpy (opt->srt_lang,
    139                 pos,
    140                 n);
    141         opt->srt_lang[n] = '\0';
    142       }
    143     }
    144     else if ( (0 == strncmp (pos,
    145                              "gallery:",
    146                              strlen ("gallery:"))) ||
    147               (0 == strncmp (pos,
    148                              "gallery=",
    149                              strlen ("gallery="))) )
    150     {
    151       pos += strlen ("gallery:");
    152       v = strtoull (pos, &endp, 10);
    153       if ( (endp != pos) &&
    154            (v >= 1) )
    155         opt->gallery = (v > EXTRACTOR_MEDIA_MAX_GALLERY)
    156                        ? EXTRACTOR_MEDIA_MAX_GALLERY
    157                        : (unsigned int) v;
    158     }
    159     /* advance to the next assignment */
    160     pos = strchr (pos, ',');
    161     if (NULL == pos)
    162       break;
    163     pos++;
    164   }
    165   /* a gallery or a full subtitle track means decoding across the
    166      entire file, which the single-thumbnail budget does not cover */
    167   if ( (! explicit_deadline) &&
    168        ( (0 != opt->gallery) ||
    169          ('\0' != opt->srt_lang[0]) ) )
    170     opt->deadline = 60000;
    171 }
    172 
    173 
    174 int
    175 EXTRACTOR_media_is_media_ (struct EXTRACTOR_ExtractContext *ec,
    176                            int want_audio)
    177 {
    178   magic_t magic;
    179   void *data;
    180   ssize_t iret;
    181   const char *mime;
    182   int ret;
    183 
    184   if (-1 == (iret = ec->read (ec->cls,
    185                               &data,
    186                               SNIFF_SIZE)))
    187     return 0;
    188   ret = 0;
    189   if (NULL == (magic = magic_open (MAGIC_MIME_TYPE)))
    190     return 0;
    191   if (0 == magic_load (magic, NULL))
    192   {
    193     mime = magic_buffer (magic,
    194                          data,
    195                          iret);
    196     if (NULL != mime)
    197     {
    198       if (0 == strncmp (mime,
    199                         "video/",
    200                         strlen ("video/")))
    201         ret = 1;
    202       if ( (0 != want_audio) &&
    203            (0 == strncmp (mime,
    204                           "audio/",
    205                           strlen ("audio/"))) )
    206         ret = 1;
    207       /* Ogg and Matroska files carrying video are frequently reported as
    208          "application/ogg" or "video/x-matroska"; the former would be lost
    209          here, so let it through and let the demuxer decide. */
    210       if (0 == strcmp (mime,
    211                        "application/ogg"))
    212         ret = 1;
    213     }
    214   }
    215   magic_close (magic);
    216   if (0 != ec->seek (ec->cls,
    217                      0,
    218                      SEEK_SET))
    219     return 0;
    220   return ret;
    221 }
    222 
    223 
    224 /**
    225  * Current time in milliseconds since an arbitrary epoch.
    226  *
    227  * @return the time
    228  */
    229 static uint64_t
    230 now_ms (void)
    231 {
    232   struct timespec ts;
    233 
    234   if (0 != clock_gettime (CLOCK_MONOTONIC,
    235                           &ts))
    236     return 0;
    237   return ((uint64_t) ts.tv_sec) * 1000ULL
    238          + ((uint64_t) ts.tv_nsec) / 1000000ULL;
    239 }
    240 
    241 
    242 void
    243 EXTRACTOR_media_deadline_start_ (struct EXTRACTOR_MediaDeadline *dl,
    244                                  unsigned int ms)
    245 {
    246   dl->expiration = now_ms () + ms;
    247 }
    248 
    249 
    250 int
    251 EXTRACTOR_media_deadline_expired_ (const struct EXTRACTOR_MediaDeadline *dl)
    252 {
    253   return now_ms () >= dl->expiration;
    254 }
    255 
    256 
    257 uint64_t
    258 EXTRACTOR_media_deadline_remaining_ (const struct EXTRACTOR_MediaDeadline *dl)
    259 {
    260   uint64_t now = now_ms ();
    261 
    262   if (now >= dl->expiration)
    263     return 0;
    264   return dl->expiration - now;
    265 }
    266 
    267 
    268 int
    269 EXTRACTOR_media_thumbnail_size_ (unsigned int width,
    270                                  unsigned int height,
    271                                  unsigned int box,
    272                                  unsigned int *rwidth,
    273                                  unsigned int *rheight)
    274 {
    275   uint64_t w = width;
    276   uint64_t h = height;
    277 
    278   if ( (0 == width) ||
    279        (0 == height) ||
    280        (width > EXTRACTOR_MEDIA_MAX_DIMENSION) ||
    281        (height > EXTRACTOR_MEDIA_MAX_DIMENSION) ||
    282        (((uint64_t) width) * height > EXTRACTOR_MEDIA_MAX_PIXELS) )
    283     return 0;
    284   if (h > box)
    285   {
    286     w = w * box / h;
    287     h = box;
    288   }
    289   if (w > box)
    290   {
    291     h = h * box / w;
    292     w = box;
    293   }
    294   if ( (0 == w) ||
    295        (0 == h) )
    296     return 0;
    297   /* many encoders dislike odd dimensions, and we lose nothing by rounding */
    298   w &= ~1ULL;
    299   h &= ~1ULL;
    300   if ( (0 == w) ||
    301        (0 == h) )
    302     return 0;
    303   *rwidth = (unsigned int) w;
    304   *rheight = (unsigned int) h;
    305   return 1;
    306 }
    307 
    308 
    309 void
    310 EXTRACTOR_media_fade_s16_ (int16_t *samples,
    311                            size_t num_samples,
    312                            size_t fade_samples)
    313 {
    314   size_t i;
    315 
    316   if (0 == fade_samples)
    317     return;
    318   if (fade_samples * 2 > num_samples)
    319     fade_samples = num_samples / 2;
    320   for (i = 0; i < fade_samples; i++)
    321   {
    322     samples[i] = (int16_t) (((int32_t) samples[i]) * (int32_t) i
    323                             / (int32_t) fade_samples);
    324     samples[num_samples - 1 - i] =
    325       (int16_t) (((int32_t) samples[num_samples - 1 - i]) * (int32_t) i
    326                  / (int32_t) fade_samples);
    327   }
    328 }
    329 
    330 
    331 /**
    332  * ISO 639 codes of a language: 639-1, 639-2/T, 639-2/B.
    333  */
    334 struct LanguageCodes
    335 {
    336   const char *iso1;
    337   const char *iso2t;
    338   const char *iso2b;
    339 };
    340 
    341 /**
    342  * Languages for which the three codes differ in ways a plain prefix
    343  * comparison would not catch.  Languages not listed here still match
    344  * if the user gives the same code the stream uses.
    345  */
    346 static const struct LanguageCodes languages[] = {
    347   { "ar", "ara", "ara" }, { "bg", "bul", "bul" }, { "bn", "ben", "ben" },
    348   { "bo", "bod", "tib" }, { "ca", "cat", "cat" }, { "cs", "ces", "cze" },
    349   { "cy", "cym", "wel" }, { "da", "dan", "dan" }, { "de", "deu", "ger" },
    350   { "el", "ell", "gre" }, { "en", "eng", "eng" }, { "eo", "epo", "epo" },
    351   { "es", "spa", "spa" }, { "et", "est", "est" }, { "eu", "eus", "baq" },
    352   { "fa", "fas", "per" }, { "fi", "fin", "fin" }, { "fr", "fra", "fre" },
    353   { "ga", "gle", "gle" }, { "gl", "glg", "glg" }, { "he", "heb", "heb" },
    354   { "hi", "hin", "hin" }, { "hr", "hrv", "hrv" }, { "hu", "hun", "hun" },
    355   { "hy", "hye", "arm" }, { "id", "ind", "ind" }, { "is", "isl", "ice" },
    356   { "it", "ita", "ita" }, { "ja", "jpn", "jpn" }, { "ka", "kat", "geo" },
    357   { "ko", "kor", "kor" }, { "lt", "lit", "lit" }, { "lv", "lav", "lav" },
    358   { "mk", "mkd", "mac" }, { "ms", "msa", "may" }, { "mt", "mlt", "mlt" },
    359   { "my", "mya", "bur" }, { "nb", "nob", "nob" }, { "nl", "nld", "dut" },
    360   { "nn", "nno", "nno" }, { "no", "nor", "nor" }, { "pl", "pol", "pol" },
    361   { "pt", "por", "por" }, { "ro", "ron", "rum" }, { "ru", "rus", "rus" },
    362   { "sk", "slk", "slo" }, { "sl", "slv", "slv" }, { "sq", "sqi", "alb" },
    363   { "sr", "srp", "srp" }, { "sv", "swe", "swe" }, { "sw", "swa", "swa" },
    364   { "ta", "tam", "tam" }, { "th", "tha", "tha" }, { "tr", "tur", "tur" },
    365   { "uk", "ukr", "ukr" }, { "ur", "urd", "urd" }, { "vi", "vie", "vie" },
    366   { "zh", "zho", "chi" },
    367   { NULL, NULL, NULL }
    368 };
    369 
    370 
    371 /**
    372  * Reduce a language tag to its primary subtag, lower-cased.
    373  *
    374  * @param tag language tag, e.g. "de-AT"
    375  * @param[out] out buffer for the primary subtag
    376  * @param out_size size of @a out
    377  */
    378 static void
    379 primary_subtag (const char *tag,
    380                 char *out,
    381                 size_t out_size)
    382 {
    383   size_t i;
    384 
    385   for (i = 0; (i + 1 < out_size) && ('\0' != tag[i]); i++)
    386   {
    387     if ( ('-' == tag[i]) ||
    388          ('_' == tag[i]) )
    389       break;
    390     out[i] = (char) tolower ((unsigned char) tag[i]);
    391   }
    392   out[i] = '\0';
    393 }
    394 
    395 
    396 /**
    397  * Map a primary language subtag to its row in #languages.
    398  *
    399  * @param code lower-case 2- or 3-letter code
    400  * @return index into #languages, -1 if unknown
    401  */
    402 static int
    403 language_index (const char *code)
    404 {
    405   unsigned int i;
    406 
    407   for (i = 0; NULL != languages[i].iso1; i++)
    408     if ( (0 == strcmp (code, languages[i].iso1)) ||
    409          (0 == strcmp (code, languages[i].iso2t)) ||
    410          (0 == strcmp (code, languages[i].iso2b)) )
    411       return (int) i;
    412   return -1;
    413 }
    414 
    415 
    416 int
    417 EXTRACTOR_media_lang_matches_ (const char *want,
    418                                const char *have)
    419 {
    420   char w[16];
    421   char h[16];
    422   int wi;
    423 
    424   if ( (NULL == want) ||
    425        (NULL == have) )
    426     return 0;
    427   primary_subtag (want, w, sizeof (w));
    428   primary_subtag (have, h, sizeof (h));
    429   if ( ('\0' == w[0]) ||
    430        ('\0' == h[0]) )
    431     return 0;
    432   if (0 == strcmp (w, h))
    433     return 1;
    434   wi = language_index (w);
    435   return ( (-1 != wi) &&
    436            (wi == language_index (h)) ) ? 1 : 0;
    437 }
    438 
    439 
    440 /**
    441  * Largest spread of a channel (in levels out of 256) that still
    442  * counts as "narrow".
    443  */
    444 #define FLAT_RANGE 32
    445 
    446 /**
    447  * Upper bound on the number of pixels we look at per frame.
    448  */
    449 #define FLAT_SAMPLES (256 * 1024)
    450 
    451 
    452 int
    453 EXTRACTOR_media_frame_is_flat_ (const uint8_t *rgb,
    454                                 unsigned int width,
    455                                 unsigned int height,
    456                                 size_t stride)
    457 {
    458   uint32_t hist[3][256];
    459   uint64_t total;
    460   uint64_t need;
    461   unsigned int step;
    462   unsigned int x;
    463   unsigned int y;
    464   unsigned int c;
    465 
    466   if ( (0 == width) ||
    467        (0 == height) )
    468     return 1;
    469   memset (hist, 0, sizeof (hist));
    470   step = 1;
    471   while ( ((uint64_t) width / step) * ((uint64_t) height / step)
    472           > FLAT_SAMPLES)
    473     step++;
    474   total = 0;
    475   for (y = 0; y < height; y += step)
    476   {
    477     const uint8_t *row = rgb + (size_t) y * stride;
    478 
    479     for (x = 0; x < width; x += step)
    480     {
    481       hist[0][row[3 * x]]++;
    482       hist[1][row[3 * x + 1]]++;
    483       hist[2][row[3 * x + 2]]++;
    484       total++;
    485     }
    486   }
    487   need = (total * 99 + 99) / 100;
    488   for (c = 0; c < 3; c++)
    489   {
    490     uint64_t win = 0;
    491     uint64_t best = 0;
    492     unsigned int i;
    493 
    494     /* largest population of any FLAT_RANGE-wide window */
    495     for (i = 0; i < 256; i++)
    496     {
    497       win += hist[c][i];
    498       if (i >= FLAT_RANGE)
    499         win -= hist[c][i - FLAT_RANGE];
    500       if (win > best)
    501         best = win;
    502     }
    503     if (best < need)
    504       return 0;
    505   }
    506   return 1;
    507 }
    508 
    509 
    510 void
    511 EXTRACTOR_media_gallery_layout_ (unsigned int n,
    512                                  unsigned int tile_w,
    513                                  unsigned int tile_h,
    514                                  unsigned int *cols,
    515                                  unsigned int *rows)
    516 {
    517   const double target = 16.0 / 9.0;
    518   double best_score = -1;
    519   unsigned int best_waste = 0;
    520   unsigned int c;
    521 
    522   if (0 == n)
    523     n = 1;
    524   if (0 == tile_w)
    525     tile_w = 1;
    526   if (0 == tile_h)
    527     tile_h = 1;
    528   *cols = n;
    529   *rows = 1;
    530   for (c = 1; c <= n; c++)
    531   {
    532     unsigned int r = (n + c - 1) / c;
    533     double aspect = ((double) c * tile_w) / ((double) r * tile_h);
    534     double score = (aspect > target) ? aspect / target : target / aspect;
    535     unsigned int waste = c * r - n;
    536 
    537     if ( (best_score < 0) ||
    538          (score < best_score - 1e-9) ||
    539          ( (score < best_score + 1e-9) &&
    540            (waste < best_waste) ) )
    541     {
    542       best_score = score;
    543       best_waste = waste;
    544       *cols = c;
    545       *rows = r;
    546     }
    547   }
    548 }
    549 
    550 
    551 uint64_t
    552 EXTRACTOR_media_gallery_time_ (unsigned int i,
    553                                unsigned int n,
    554                                uint64_t duration_ms)
    555 {
    556   if (0 == n)
    557     return 0;
    558   return (duration_ms / n) * i + duration_ms / (2 * (uint64_t) n);
    559 }
    560 
    561 
    562 /**
    563  * Append @a len bytes to @a srt.
    564  *
    565  * @param srt file to append to
    566  * @param data bytes to append
    567  * @param len number of bytes
    568  * @return 0 on success, -1 if full
    569  */
    570 static int
    571 srt_put (struct EXTRACTOR_MediaSrt *srt,
    572          const char *data,
    573          size_t len)
    574 {
    575   if (srt->full)
    576     return -1;
    577   if (srt->len + len + 1 > EXTRACTOR_MEDIA_MAX_SRT)
    578   {
    579     srt->full = 1;
    580     return -1;
    581   }
    582   if (srt->len + len + 1 > srt->cap)
    583   {
    584     size_t ncap = (0 == srt->cap) ? 4096 : srt->cap;
    585     char *nbuf;
    586 
    587     while (ncap < srt->len + len + 1)
    588       ncap *= 2;
    589     if (ncap > EXTRACTOR_MEDIA_MAX_SRT)
    590       ncap = EXTRACTOR_MEDIA_MAX_SRT;
    591     nbuf = realloc (srt->buf, ncap);
    592     if (NULL == nbuf)
    593     {
    594       srt->full = 1;
    595       return -1;
    596     }
    597     srt->buf = nbuf;
    598     srt->cap = ncap;
    599   }
    600   memcpy (&srt->buf[srt->len], data, len);
    601   srt->len += len;
    602   srt->buf[srt->len] = '\0';
    603   return 0;
    604 }
    605 
    606 
    607 /**
    608  * Length of the valid UTF-8 sequence at @a s.
    609  *
    610  * @param s input
    611  * @param len bytes available, at least 1
    612  * @return length of the sequence, 0 if invalid
    613  */
    614 static size_t
    615 utf8_seq_len (const unsigned char *s,
    616               size_t len)
    617 {
    618   size_t n;
    619   size_t i;
    620   uint32_t cp;
    621 
    622   if (s[0] < 0x80)
    623     return 1;
    624   if ((s[0] & 0xE0) == 0xC0)
    625     n = 2, cp = s[0] & 0x1F;
    626   else if ((s[0] & 0xF0) == 0xE0)
    627     n = 3, cp = s[0] & 0x0F;
    628   else if ((s[0] & 0xF8) == 0xF0)
    629     n = 4, cp = s[0] & 0x07;
    630   else
    631     return 0;
    632   if (n > len)
    633     return 0;
    634   for (i = 1; i < n; i++)
    635   {
    636     if ((s[i] & 0xC0) != 0x80)
    637       return 0;
    638     cp = (cp << 6) | (s[i] & 0x3F);
    639   }
    640   if ( ( (2 == n) && (cp < 0x80) ) ||
    641        ( (3 == n) && (cp < 0x800) ) ||
    642        ( (4 == n) && (cp < 0x10000) ) ||
    643        (cp > 0x10FFFF) ||
    644        ( (cp >= 0xD800) && (cp <= 0xDFFF) ) )
    645     return 0;
    646   return n;
    647 }
    648 
    649 
    650 /**
    651  * Format @a ms as "HH:MM:SS,mmm".
    652  *
    653  * @param ms time in milliseconds
    654  * @param[out] out buffer of at least 32 bytes
    655  */
    656 static void
    657 srt_time (uint64_t ms,
    658           char out[32])
    659 {
    660   snprintf (out,
    661             32,
    662             "%02llu:%02llu:%02llu,%03llu",
    663             (unsigned long long) (ms / 3600000),
    664             (unsigned long long) ((ms / 60000) % 60),
    665             (unsigned long long) ((ms / 1000) % 60),
    666             (unsigned long long) (ms % 1000));
    667 }
    668 
    669 
    670 void
    671 EXTRACTOR_media_srt_add_ (struct EXTRACTOR_MediaSrt *srt,
    672                           uint64_t start_ms,
    673                           uint64_t end_ms,
    674                           const char *text,
    675                           size_t text_len,
    676                           int is_ass)
    677 {
    678   const unsigned char *in = (const unsigned char *) text;
    679   char *clean;
    680   size_t i;
    681   size_t o;
    682   size_t line_start;
    683   char hdr[96];
    684   char t1[32];
    685   char t2[32];
    686 
    687   if ( (srt->full) ||
    688        (NULL == text) )
    689     return;
    690   i = 0;
    691   if (is_ass)
    692   {
    693     unsigned int commas = 8;
    694 
    695     if ( (text_len >= 9) &&
    696          (0 == strncmp (text, "Dialogue:", 9)) )
    697       commas = 9;
    698     while ( (i < text_len) &&
    699             (commas > 0) )
    700       if (',' == in[i++])
    701         commas--;
    702   }
    703   clean = malloc (text_len - i + 1);
    704   if (NULL == clean)
    705     return;
    706   o = 0;
    707   line_start = 0;
    708   while ( (i < text_len) &&
    709           ('\0' != in[i]) )
    710   {
    711     size_t n;
    712 
    713     if (is_ass &&
    714         ('{' == in[i]) )
    715     {
    716       /* ASS override block */
    717       while ( (i < text_len) && ('}' != in[i]) )
    718         i++;
    719       i++;
    720       continue;
    721     }
    722     if (is_ass &&
    723         ('\\' == in[i]) &&
    724         (i + 1 < text_len) &&
    725         ( ('N' == in[i + 1]) || ('n' == in[i + 1]) ||
    726           ('h' == in[i + 1]) ) )
    727     {
    728       clean[o++] = ('h' == in[i + 1]) ? ' ' : '\n';
    729       i += 2;
    730     }
    731     else if ('\r' == in[i])
    732     {
    733       i++;
    734       continue;
    735     }
    736     else
    737     {
    738       n = utf8_seq_len (&in[i], text_len - i);
    739       if (0 == n)
    740       {
    741         clean[o++] = '?';
    742         i++;
    743       }
    744       else
    745       {
    746         memcpy (&clean[o], &in[i], n);
    747         o += n;
    748         i += n;
    749       }
    750     }
    751     if ('\n' == clean[o - 1])
    752     {
    753       /* drop trailing blanks, and the line itself if it is empty */
    754       size_t e = o - 1;
    755 
    756       while ( (e > line_start) &&
    757               ( (' ' == clean[e - 1]) || ('\t' == clean[e - 1]) ) )
    758         e--;
    759       if (e == line_start)
    760         o = line_start;
    761       else
    762       {
    763         clean[e] = '\n';
    764         o = e + 1;
    765         line_start = o;
    766       }
    767     }
    768   }
    769   while ( (o > 0) &&
    770           ( ('\n' == clean[o - 1]) || (' ' == clean[o - 1]) ||
    771             ('\t' == clean[o - 1]) ) )
    772     o--;
    773   if (0 == o)
    774   {
    775     free (clean);
    776     return;
    777   }
    778   if (end_ms < start_ms)
    779     end_ms = start_ms;
    780   srt_time (start_ms, t1);
    781   srt_time (end_ms, t2);
    782   snprintf (hdr,
    783             sizeof (hdr),
    784             "%u\n%s --> %s\n",
    785             srt->count + 1,
    786             t1,
    787             t2);
    788   {
    789     size_t old_len = srt->len;
    790 
    791     if ( (0 == srt_put (srt, hdr, strlen (hdr))) &&
    792          (0 == srt_put (srt, clean, o)) &&
    793          (0 == srt_put (srt, "\n\n", 2)) )
    794     {
    795       srt->count++;
    796     }
    797     else if (NULL != srt->buf)
    798     {
    799       /* never emit a partial cue */
    800       srt->len = old_len;
    801       srt->buf[old_len] = '\0';
    802     }
    803   }
    804   free (clean);
    805 }
    806 
    807 
    808 int
    809 EXTRACTOR_media_srt_emit_ (const struct EXTRACTOR_MediaSrt *srt,
    810                            struct EXTRACTOR_ExtractContext *ec,
    811                            const char *plugin_name)
    812 {
    813   if ( (0 == srt->count) ||
    814        (0 == srt->len) )
    815     return 0;
    816   return ec->proc (ec->cls,
    817                    plugin_name,
    818                    EXTRACTOR_METATYPE_SUBTITLE_TEXT,
    819                    EXTRACTOR_METAFORMAT_UTF8,
    820                    "application/x-subrip",
    821                    srt->buf,
    822                    srt->len + 1);
    823 }
    824 
    825 
    826 void
    827 EXTRACTOR_media_srt_clear_ (struct EXTRACTOR_MediaSrt *srt)
    828 {
    829   free (srt->buf);
    830   memset (srt, 0, sizeof (*srt));
    831 }
    832 
    833 
    834 /* end of mediautil.c */