plugins/ogg: Refactor to conquer
[platform/upstream/lightmediascanner.git] / src / plugins / ogg / ogg.c
index e810be4..59cc962 100644 (file)
  * @brief
  *
  * ogg file parser.
+ *
+ * Reference:
+ *   http://xiph.org/ogg/doc/libogg/decoding.html
+ *   http://xiph.org/vorbis/doc/libvorbis/overview.html
  */
 
 #include <lightmediascanner_plugin.h>
 #include <lightmediascanner_utils.h>
 #include <shared/util.h>
 
+#include <assert.h>
+#include <errno.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include "lms_ogg_private.h"
 
+#ifndef CHUNKSIZE
+int CHUNKSIZE = 4096;
+#endif
+
+#define MAX_CHUNKS_PER_PAGE 10
+
+struct stream {
+    struct lms_stream base;
+    int serial;
+    int remain_headers;
+    ogg_stream_state *os;
+    union {
+        struct {
+            vorbis_comment vc;
+            vorbis_info vi;
+        } audio;
+    };
+};
+
+struct ogg_info {
+    struct lms_string_size title;
+    struct lms_string_size artist;
+    struct lms_string_size album;
+    struct lms_string_size genre;
+    enum lms_stream_type type;
+    unsigned char trackno;
+    unsigned int channels;
+    unsigned int sampling_rate;
+    unsigned int bitrate;
+
+    struct stream *streams;
+};
+
 static const struct lms_string_size _container = LMS_STATIC_STRING_SIZE("ogg");
-static const struct lms_string_size _codec = LMS_STATIC_STRING_SIZE("vorbis");
+static const struct lms_string_size _audio_codec =
+    LMS_STATIC_STRING_SIZE("vorbis");
 
 static long int
 _id3_tag_size(FILE *file)
@@ -62,170 +102,246 @@ _id3_tag_size(FILE *file)
     return 0L;
 }
 
-
-static int
-_get_vorbis_headers(FILE *file, vorbis_info *vi, vorbis_comment *vc)
+static void
+_set_lms_info(struct lms_string_size *info, const char *tag)
 {
-    lms_ogg_buffer_t buffer = NULL;
-    int bytes = 0, i = 0, chunks = 0;
-    ogg_packet header;
+    int size;
 
-    ogg_page og;
-    ogg_sync_state *osync = NULL;
-    ogg_stream_state *os = NULL;
+    if (!info || !tag)
+        return;
 
-    int serial = 0;
-#ifndef CHUNKSIZE
-    int CHUNKSIZE = 4096;
-#endif
-    int nheaders = 0;
-    int ret = -1;
+    size = strlen(tag);
 
-    /* Initialize stuff */
-    memset(&header, 0, sizeof(ogg_packet));
-    memset(&og, 0, sizeof(ogg_page));
+    if (!size)
+        return;
 
-    osync = lms_create_ogg_sync();
+    info->len = size;
+    info->str = malloc(size * sizeof(char));
+    memcpy(info->str, tag, size);
+    lms_string_size_strip_and_free(info);
+}
+
+static bool _ogg_read_page(FILE *fp, ogg_sync_state *osync, ogg_page *page)
+{
+    int i;
 
-    vorbis_info_init(vi);
-    vorbis_comment_init(vc);
+    for (i = 0; i < MAX_CHUNKS_PER_PAGE && ogg_sync_pageout(osync, page) != 1;
+         i++) {
+        lms_ogg_buffer_t buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
+        int bytes = fread(buffer, 1, CHUNKSIZE, fp);
 
-    while (1) {
-        buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
-        bytes = fread(buffer, 1, CHUNKSIZE, file);
+        /* EOF */
+        if (bytes == 0)
+            return false;
 
         ogg_sync_wrote(osync, bytes);
+    }
 
-        if (ogg_sync_pageout(osync, &og) == 1)
-            break;
+    if (i > MAX_CHUNKS_PER_PAGE)
+        return false;
 
-        if (chunks++ >= 10)
-            goto end;
-    }
+    return true;
+}
 
-    serial = ogg_page_serialno(&og);
-    os = lms_create_ogg_stream(serial);
-
-    if (ogg_stream_pagein(os, &og) < 0 ||
-        ogg_stream_packetout(os, &header) != 1 ||
-        vorbis_synthesis_headerin(vi, vc, &header) != 0)
-        goto end;
-
-    i = 1;
-    nheaders = 3;
-    while (i < nheaders) {
-        while (i < nheaders) {
-            int result = ogg_sync_pageout(osync, &og);
-            if (result == 0)
-                break;
-            else if (result == 1) {
-                ogg_stream_pagein(os, &og);
-                while (i < nheaders) {
-                    result = ogg_stream_packetout(os, &header);
-                    if (result == 0)
-                        break;
-                    if (result == -1)
-                        goto end;
-
-                    vorbis_synthesis_headerin(vi, vc, &header);
-                    i++;
-                }
-            }
-        }
+static struct stream *_stream_new(int serial, int id)
+{
+    struct stream *s;
 
-        buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
-        bytes = fread(buffer, 1, CHUNKSIZE, file);
+    s = calloc(1, sizeof(*s));
+    if (!s)
+        return NULL;
 
-        if (bytes == 0 && i < 2)
-            goto end;
+    s->serial = serial;
+    s->os = lms_create_ogg_stream(serial);
 
-        ogg_sync_wrote(osync, bytes);
-    }
+    s->base.type = LMS_STREAM_TYPE_UNKNOWN;
+    s->base.stream_id = id;
 
-    ret = 0;
+    return s;
+}
 
-end:
-    if (ret) {
-        vorbis_comment_clear(vc);
-        vorbis_info_clear(vi);
+static void _stream_free(struct stream *s)
+{
+    switch (s->base.type) {
+    case LMS_STREAM_TYPE_UNKNOWN:
+    case LMS_STREAM_TYPE_VIDEO:
+    case LMS_STREAM_TYPE_SUBTITLE:
+        break;
+    case LMS_STREAM_TYPE_AUDIO:
+        vorbis_comment_clear(&s->audio.vc);
+        vorbis_info_clear(&s->audio.vi);
+        break;
     }
 
-    if (os)
-        lms_destroy_ogg_stream(os);
+    lms_destroy_ogg_stream(s->os);
+    free(s);
+}
+
+static struct stream *_info_find_stream(struct ogg_info *info, int serial)
+{
+    struct stream *s;
 
-    if (osync)
-        lms_destroy_ogg_sync(osync);
+    for (s = info->streams; s; s = (struct stream *) s->base.next) {
+        if (s->serial == serial)
+            return s;
+    }
 
-    return ret;
+    return NULL;
 }
 
-static void
-_set_lms_info(struct lms_string_size *info, const char *tag)
+static struct stream *_info_prepend_stream(struct ogg_info *info, int serial,
+                                           int id)
 {
-    int size;
+    struct stream *s = _stream_new(serial, id);
+    if (!s)
+        return NULL;
+    s->base.next = (struct lms_stream *) info->streams;
+    info->streams = s;
+    return s;
+}
 
-    if (!info || !tag)
-        return;
+static int _stream_handle_page(struct stream *s, ogg_page *page)
+{
+    ogg_packet packet;
+    int r;
 
-    size = strlen(tag);
+    if (!s->os)
+        s->os = lms_create_ogg_stream(s->serial);
 
-    if (!size)
-        return;
+    if (ogg_stream_pagein(s->os, page) < 0)
+        return -1;
 
-    info->len = size;
-    info->str = malloc(size * sizeof(char));
-    memcpy(info->str, tag, size);
-    lms_string_size_strip_and_free(info);
+    do {
+        r = ogg_stream_packetout(s->os, &packet);
+        if (r == 0)
+            return 0;
+        if (r == -1)
+            return -1;
+
+        switch (s->base.type) {
+        case LMS_STREAM_TYPE_UNKNOWN:
+            vorbis_info_init(&s->audio.vi);
+            vorbis_comment_init(&s->audio.vc);
+            r = vorbis_synthesis_headerin(&s->audio.vi, &s->audio.vc, &packet);
+            if (r != 0)
+                return -1;
+
+            s->base.type = LMS_STREAM_TYPE_AUDIO;
+            s->remain_headers = 2;
+            break;
+        case LMS_STREAM_TYPE_AUDIO:
+            assert(s->remain_headers > 0);
+            r = vorbis_synthesis_headerin(&s->audio.vi, &s->audio.vc, &packet);
+            if (r != 0)
+                return -1;
+
+            s->remain_headers--;
+            if (!s->remain_headers)
+                return 0;
+            break;
+        default:
+            return 0;
+        }
+    } while (1);
+
+    return 0;
 }
 
-static int
-_parse_ogg(const char *filename, struct lms_audio_info *info)
+static void _parse_vorbis_stream(struct ogg_info *info, struct stream *s)
 {
-    vorbis_comment vc;
-    vorbis_info vi;
-    FILE *file;
     const char *tag;
 
+    info->channels = s->audio.vi.channels;
+    info->sampling_rate = s->audio.vi.rate;
+    info->bitrate = s->audio.vi.bitrate_nominal;
+
+    tag = vorbis_comment_query(&s->audio.vc, "TITLE", 0);
+    _set_lms_info(&info->title, tag);
+
+    tag = vorbis_comment_query(&s->audio.vc, "ARTIST", 0);
+    _set_lms_info(&info->artist, tag);
+
+    tag = vorbis_comment_query(&s->audio.vc, "ALBUM", 0);
+    _set_lms_info(&info->album, tag);
+
+    tag = vorbis_comment_query(&s->audio.vc, "GENRE", 0);
+    _set_lms_info(&info->genre, tag);
+
+    tag = vorbis_comment_query(&s->audio.vc, "TRACKNUMBER", 0);
+    if (tag)
+        info->trackno = atoi(tag);
+}
+
+static int _parse_ogg(const char *filename, struct ogg_info *info)
+{
+    FILE *fp;
+    ogg_page page;
+    ogg_sync_state *osync;
+    int r;
+    /* no numeration in the protocol, start arbitrarily from 1 */
+    int id = 0;
+    /* the 1st audio stream, the one used if audio */
+    struct stream *s, *audio_stream = NULL;
+
     if (!filename)
         return -1;
 
-    file = fopen(filename, "rb");
-    if (file == NULL)
+    fp = fopen(filename, "rb");
+    if (fp == NULL)
         return -1;
 
-    fseek(file, _id3_tag_size(file), SEEK_SET);
+    /* Skip ID3 on the beginning */
+    fseek(fp, _id3_tag_size(fp), SEEK_SET);
 
-    if (_get_vorbis_headers(file, &vi, &vc) != 0)
-        return -1;
+    osync = lms_create_ogg_sync();
+    while (_ogg_read_page(fp, osync, &page)) {
+        int serial = ogg_page_serialno(&page);
 
-    tag = vorbis_comment_query(&vc, "TITLE", 0);
-    _set_lms_info(&info->title, tag);
+        s = _info_find_stream(info, serial);
 
-    tag = vorbis_comment_query(&vc, "ARTIST", 0);
-    _set_lms_info(&info->artist, tag);
+        /* A new page for a stream that has all the headers already: stop
+         * parsing */
+        if (s && s->remain_headers == 0)
+            break;
 
-    tag = vorbis_comment_query(&vc, "ALBUM", 0);
-    _set_lms_info(&info->album, tag);
+        if (!s) {
+            /* We didn't find the stream, but we are not at its start page
+             * neither: it's an unknown stream, go to the next one */
+            if (!ogg_page_bos(&page))
+                continue;
 
-    tag = vorbis_comment_query(&vc, "TRACKNUMBER", 0);
-    if (tag)
-        info->trackno = atoi(tag);
+            s = _info_prepend_stream(info, serial, ++id);
+            if (!s) {
+                r = -ENOMEM;
+                goto done;
+            }
+        }
 
-    tag = vorbis_comment_query(&vc, "GENRE", 0);
-    _set_lms_info(&info->genre, tag);
+        r = _stream_handle_page(s, &page);
+        if (r < 0)
+            goto done;
+
+        if (s->remain_headers == 0 && !audio_stream &&
+            s->base.type == LMS_STREAM_TYPE_AUDIO) {
+            info->type = s->base.type;
+            audio_stream = s;
+        }
+    }
 
-    info->container = _container;
-    info->codec = _codec;
+    if (audio_stream)
+        _parse_vorbis_stream(info, audio_stream);
 
-    info->channels = vi.channels;
-    info->sampling_rate = vi.rate;
-    info->bitrate = vi.bitrate_nominal;
+    while (info->streams) {
+        s = info->streams;
+        info->streams = (struct stream *) s->base.next;
+        _stream_free(s);
+    }
 
-    fclose(file);
-    vorbis_comment_clear(&vc);
-    vorbis_info_clear(&vi);
+done:
+    lms_destroy_ogg_sync(osync);
+    fclose(fp);
 
-    return 0;
+    return r;
 }
 
 
@@ -263,9 +379,10 @@ _match(struct plugin *p, const char *path, int len, int base)
 }
 
 static int
-_parse(struct plugin *plugin, struct lms_context *ctxt, const struct lms_file_info *finfo, void *match)
+_parse(struct plugin *plugin, struct lms_context *ctxt,
+       const struct lms_file_info *finfo, void *match)
 {
-    struct lms_audio_info info = { };
+    struct ogg_info info = { .type = LMS_STREAM_TYPE_UNKNOWN };
     int r;
 
     r = _parse_ogg(finfo->path, &info);
@@ -278,18 +395,35 @@ _parse(struct plugin *plugin, struct lms_context *ctxt, const struct lms_file_in
                                                 &_exts[((long) match) - 1],
                                                 NULL);
     if (info.title.str)
-      lms_charset_conv(ctxt->cs_conv, &info.title.str, &info.title.len);
+        lms_charset_conv(ctxt->cs_conv, &info.title.str, &info.title.len);
     if (info.artist.str)
-      lms_charset_conv(ctxt->cs_conv, &info.artist.str, &info.artist.len);
-    if (info.album.str)
-      lms_charset_conv(ctxt->cs_conv, &info.album.str, &info.album.len);
-    if (info.genre.str)
-      lms_charset_conv(ctxt->cs_conv, &info.genre.str, &info.genre.len);
-
-    info.id = finfo->id;
-    r = lms_db_audio_add(plugin->audio_db, &info);
+        lms_charset_conv(ctxt->cs_conv, &info.artist.str, &info.artist.len);
+
+    if (info.type == LMS_STREAM_TYPE_AUDIO) {
+        struct lms_audio_info audio_info = { };
+
+        if (info.album.str)
+            lms_charset_conv(ctxt->cs_conv, &info.album.str, &info.album.len);
+        if (info.genre.str)
+            lms_charset_conv(ctxt->cs_conv, &info.genre.str, &info.genre.len);
+
+        audio_info.id = finfo->id;
+        audio_info.title = info.title;
+        audio_info.artist = info.artist;
+        audio_info.album = info.album;
+        audio_info.genre = info.genre;
+
+        audio_info.trackno = info.trackno;
+        audio_info.container = _container;
+        audio_info.codec = _audio_codec;
+        audio_info.channels = info.channels;
+        audio_info.sampling_rate = info.sampling_rate;
+        audio_info.bitrate = info.bitrate;
+
+        r = lms_db_audio_add(plugin->audio_db, &audio_info);
+    }
 
- done:
+done:
     free(info.title.str);
     free(info.artist.str);
     free(info.album.str);