plugins/ogg: Add support for theora videos
[platform/upstream/lightmediascanner.git] / src / plugins / ogg / ogg.c
1 /**
2  * Copyright (C) 2007 by INdT
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  *
18  * @author Renato Chencarek <renato.chencarek@openbossa.org>
19  * @author Eduardo Lima (Etrunko) <eduardo.lima@indt.org.br>
20  *
21  */
22
23 /**
24  * @brief
25  *
26  * ogg file parser.
27  *
28  * Reference:
29  *   http://xiph.org/ogg/doc/libogg/decoding.html
30  *   http://xiph.org/vorbis/doc/libvorbis/overview.html
31  */
32
33 #include <lightmediascanner_plugin.h>
34 #include <lightmediascanner_db.h>
35 #include <lightmediascanner_utils.h>
36 #include <shared/util.h>
37
38 #include <assert.h>
39 #include <errno.h>
40 #include <stdlib.h>
41 #include <string.h>
42 #include <theora/theoradec.h>
43
44 #include "lms_ogg_private.h"
45
46 #ifndef CHUNKSIZE
47 int CHUNKSIZE = 4096;
48 #endif
49
50 #define MAX_CHUNKS_PER_PAGE 10
51
52 struct stream {
53     struct lms_stream base;
54     int serial;
55     int remain_headers;
56     ogg_stream_state *os;
57     union {
58         struct {
59             vorbis_comment vc;
60             vorbis_info vi;
61         } audio;
62         struct {
63             th_comment tc;
64             th_info ti;
65             th_setup_info *tsi;
66         } video;
67     };
68 };
69
70 struct ogg_info {
71     struct lms_string_size title;
72     struct lms_string_size artist;
73     struct lms_string_size album;
74     struct lms_string_size genre;
75     enum lms_stream_type type;
76     unsigned char trackno;
77     unsigned int channels;
78     unsigned int sampling_rate;
79     unsigned int bitrate;
80
81     struct stream *streams;
82 };
83
84 static const struct lms_string_size _container = LMS_STATIC_STRING_SIZE("ogg");
85 static const struct lms_string_size _audio_codec =
86     LMS_STATIC_STRING_SIZE("vorbis");
87 static const struct lms_string_size _video_codec =
88     LMS_STATIC_STRING_SIZE("theora");
89
90 static long int
91 _id3_tag_size(FILE *file)
92 {
93     unsigned char tmp[4];
94     long int size;
95
96     if (fread(tmp, 1, 4, file) == 4) {
97         if (tmp[0] == 'I' && tmp[1] == 'D' &&
98             tmp[2] == '3' && tmp[3] < 0xFF) {
99             fseek(file, 2, SEEK_CUR);
100             if (fread(tmp, 1, 4, file) == 4) {
101                 size = 10 +   ( (long)(tmp[3])
102                               | ((long)(tmp[2]) << 7)
103                               | ((long)(tmp[1]) << 14)
104                               | ((long)(tmp[0]) << 21) );
105
106                 return size;
107             }
108         }
109     }
110     return 0L;
111 }
112
113 static void
114 _set_lms_info(struct lms_string_size *info, const char *tag)
115 {
116     int size;
117
118     if (!info || !tag)
119         return;
120
121     size = strlen(tag);
122
123     if (!size)
124         return;
125
126     info->len = size;
127     info->str = malloc(size * sizeof(char));
128     memcpy(info->str, tag, size);
129     lms_string_size_strip_and_free(info);
130 }
131
132 static bool _ogg_read_page(FILE *fp, ogg_sync_state *osync, ogg_page *page)
133 {
134     int i;
135
136     for (i = 0; i < MAX_CHUNKS_PER_PAGE && ogg_sync_pageout(osync, page) != 1;
137          i++) {
138         lms_ogg_buffer_t buffer = lms_get_ogg_sync_buffer(osync, CHUNKSIZE);
139         int bytes = fread(buffer, 1, CHUNKSIZE, fp);
140
141         /* EOF */
142         if (bytes == 0)
143             return false;
144
145         ogg_sync_wrote(osync, bytes);
146     }
147
148     if (i > MAX_CHUNKS_PER_PAGE)
149         return false;
150
151     return true;
152 }
153
154 static struct stream *_stream_new(int serial, int id)
155 {
156     struct stream *s;
157
158     s = calloc(1, sizeof(*s));
159     if (!s)
160         return NULL;
161
162     s->serial = serial;
163     s->os = lms_create_ogg_stream(serial);
164
165     s->base.type = LMS_STREAM_TYPE_UNKNOWN;
166     s->base.stream_id = id;
167
168     return s;
169 }
170
171 static void _stream_free(struct stream *s)
172 {
173     switch (s->base.type) {
174     case LMS_STREAM_TYPE_UNKNOWN:
175     case LMS_STREAM_TYPE_SUBTITLE:
176         break;
177     case LMS_STREAM_TYPE_AUDIO:
178         vorbis_comment_clear(&s->audio.vc);
179         vorbis_info_clear(&s->audio.vi);
180         break;
181     case LMS_STREAM_TYPE_VIDEO:
182         th_comment_clear(&s->video.tc);
183         th_info_clear(&s->video.ti);
184         th_setup_free(s->video.tsi);
185         free(s->base.video.aspect_ratio.str);
186         break;
187     }
188
189     lms_destroy_ogg_stream(s->os);
190     free(s);
191 }
192
193 static struct stream *_info_find_stream(struct ogg_info *info, int serial)
194 {
195     struct stream *s;
196
197     for (s = info->streams; s; s = (struct stream *) s->base.next) {
198         if (s->serial == serial)
199             return s;
200     }
201
202     return NULL;
203 }
204
205 static struct stream *_info_prepend_stream(struct ogg_info *info, int serial,
206                                            int id)
207 {
208     struct stream *s = _stream_new(serial, id);
209     if (!s)
210         return NULL;
211     s->base.next = (struct lms_stream *) info->streams;
212     info->streams = s;
213     return s;
214 }
215
216 static int _stream_handle_page(struct stream *s, ogg_page *page)
217 {
218     ogg_packet packet;
219     int r;
220
221     if (!s->os)
222         s->os = lms_create_ogg_stream(s->serial);
223
224     if (ogg_stream_pagein(s->os, page) < 0)
225         return -1;
226
227     do {
228         r = ogg_stream_packetout(s->os, &packet);
229         if (r == 0)
230             return 1;
231         if (r == -1)
232             return -1;
233
234         switch (s->base.type) {
235         case LMS_STREAM_TYPE_UNKNOWN:
236             th_info_init(&s->video.ti);
237             th_comment_init(&s->video.tc);
238             s->video.tsi = NULL;
239             if (th_decode_headerin(&s->video.ti, &s->video.tc, &s->video.tsi,
240                                    &packet) != TH_ENOTFORMAT) {
241                 s->base.type = LMS_STREAM_TYPE_VIDEO;
242                 s->remain_headers = 2;
243             } else {
244                 th_info_clear(&s->video.ti);
245                 th_comment_clear(&s->video.tc);
246                 if (s->video.tsi)
247                     th_setup_free(s->video.tsi);
248                 vorbis_info_init(&s->audio.vi);
249                 vorbis_comment_init(&s->audio.vc);
250                 if (vorbis_synthesis_headerin(&s->audio.vi, &s->audio.vc,
251                                               &packet) != 0) {
252                     vorbis_info_clear(&s->audio.vi);
253                     vorbis_comment_clear(&s->audio.vc);
254                     s->remain_headers = -1;
255                     return 1;
256                 }
257
258                 s->base.type = LMS_STREAM_TYPE_AUDIO;
259                 s->remain_headers = 2;
260             }
261             break;
262         case LMS_STREAM_TYPE_VIDEO:
263             assert(s->remain_headers > 0);
264             r = th_decode_headerin(&s->video.ti, &s->video.tc, &s->video.tsi,
265                                    &packet);
266             if (r < 0) {
267                 s->remain_headers = -1;
268                 return 1;
269             }
270
271             s->remain_headers--;
272             if (!s->remain_headers)
273                 return 0;
274             break;
275         case LMS_STREAM_TYPE_AUDIO:
276             assert(s->remain_headers > 0);
277             r = vorbis_synthesis_headerin(&s->audio.vi, &s->audio.vc, &packet);
278             if (r != 0) {
279                 s->remain_headers = -1;
280                 return 1;
281             }
282
283             s->remain_headers--;
284             if (!s->remain_headers)
285                 return 0;
286             break;
287         default:
288             s->remain_headers = -1;
289             return 1;
290         }
291     } while (1);
292
293     return 1;
294 }
295
296 static void _parse_theora_and_vorbis_streams(struct ogg_info *info,
297                                              struct stream *video_stream)
298 {
299     struct stream *s, *prev, *next;
300     const char *tag;
301
302     /* filter unknown or incomplete streams */
303
304     for (s = info->streams, next = NULL; s; s = next) {
305         next = (struct stream *) s->base.next;
306         if (s->base.type != LMS_STREAM_TYPE_UNKNOWN && s->remain_headers == 0)
307             break;
308         _stream_free(s);
309     }
310
311     info->streams = s;
312     if (!s)
313         return;
314
315     for (prev = s, s = next; s; s = next) {
316         next = (struct stream *) s->base.next;
317         if (s->base.type != LMS_STREAM_TYPE_UNKNOWN && s->remain_headers == 0) {
318             prev = s;
319         } else {
320             prev->base.next = (struct lms_stream *) next;
321             _stream_free(s);
322         }
323     }
324
325     /* add information to each stream */
326     for (s = info->streams; s; s = (struct stream *) s->base.next) {
327         if (s->base.type == LMS_STREAM_TYPE_AUDIO) {
328             s->base.codec = _audio_codec;
329             s->base.audio.channels = s->audio.vi.channels;
330             s->base.audio.bitrate = s->audio.vi.bitrate_nominal;
331         } else if (s->base.type == LMS_STREAM_TYPE_VIDEO) {
332             unsigned int num, den;
333
334             s->base.codec = _video_codec;
335             s->base.video.bitrate = s->video.ti.target_bitrate;
336             s->base.video.width = s->video.ti.frame_width;
337             s->base.video.height = s->video.ti.frame_height;
338             num = s->video.ti.fps_numerator;
339             den = s->video.ti.fps_denominator;
340             if (num && den)
341                 s->base.video.framerate = (double)(num) / den;
342
343             num = s->video.ti.aspect_numerator;
344             den = s->video.ti.aspect_denominator;
345             if (num && den) {
346                 reduce_gcd(num, den, &num, &den);
347                 asprintf(&s->base.video.aspect_ratio.str, "%u:%u", num, den);
348                 s->base.video.aspect_ratio.len =
349                     s->base.video.aspect_ratio.str ?
350                     strlen(s->base.video.aspect_ratio.str) : 0;
351             }
352         }
353     }
354
355     /* query the first video stream about relevant metdata */
356     tag = th_comment_query(&video_stream->video.tc, (char *) "TITLE", 0);
357     _set_lms_info(&info->title, tag);
358
359     tag = th_comment_query(&video_stream->video.tc, (char *) "ARTIST", 0);
360     _set_lms_info(&info->artist, tag);
361 }
362
363 static void _parse_vorbis_stream(struct ogg_info *info, struct stream *s)
364 {
365     const char *tag;
366
367     info->channels = s->audio.vi.channels;
368     info->sampling_rate = s->audio.vi.rate;
369     info->bitrate = s->audio.vi.bitrate_nominal;
370
371     tag = vorbis_comment_query(&s->audio.vc, "TITLE", 0);
372     _set_lms_info(&info->title, tag);
373
374     tag = vorbis_comment_query(&s->audio.vc, "ARTIST", 0);
375     _set_lms_info(&info->artist, tag);
376
377     tag = vorbis_comment_query(&s->audio.vc, "ALBUM", 0);
378     _set_lms_info(&info->album, tag);
379
380     tag = vorbis_comment_query(&s->audio.vc, "GENRE", 0);
381     _set_lms_info(&info->genre, tag);
382
383     tag = vorbis_comment_query(&s->audio.vc, "TRACKNUMBER", 0);
384     if (tag)
385         info->trackno = atoi(tag);
386 }
387
388 static int _parse_ogg(const char *filename, struct ogg_info *info)
389 {
390     FILE *fp;
391     ogg_page page;
392     ogg_sync_state *osync;
393     int r = 0;
394     /* no numeration in the protocol, start arbitrarily from 1 */
395     int id = 0;
396     /* the 1st audio stream, the one used if audio */
397     struct stream *s, *audio_stream = NULL, *video_stream = NULL;
398
399     if (!filename)
400         return -1;
401
402     fp = fopen(filename, "rb");
403     if (fp == NULL)
404         return -1;
405
406     /* Skip ID3 on the beginning */
407     fseek(fp, _id3_tag_size(fp), SEEK_SET);
408
409     osync = lms_create_ogg_sync();
410     while (_ogg_read_page(fp, osync, &page)) {
411         int serial = ogg_page_serialno(&page);
412
413         s = _info_find_stream(info, serial);
414
415         /* A new page for a stream that has all the headers already */
416         if (s) {
417             if (s->remain_headers == 0)
418                 break;
419             else if (s->remain_headers < 0)
420                 continue;
421         } else {
422             /* We didn't find the stream, but we are not at its start page
423              * neither: it's an unknown stream, go to the next one */
424             if (!ogg_page_bos(&page))
425                 continue;
426
427             s = _info_prepend_stream(info, serial, ++id);
428             if (!s) {
429                 r = -ENOMEM;
430                 goto done;
431             }
432         }
433
434         r = _stream_handle_page(s, &page);
435         if (r < 0)
436             goto done;
437         if (r > 0)
438             continue;
439
440         if (s->remain_headers == 0) {
441             if (s->base.type == LMS_STREAM_TYPE_AUDIO && !audio_stream)
442                 audio_stream = s;
443             else if (s->base.type == LMS_STREAM_TYPE_VIDEO && !video_stream)
444                 video_stream = s;
445         }
446     }
447
448     if (video_stream) {
449         _parse_theora_and_vorbis_streams(info, video_stream);
450         info->type = LMS_STREAM_TYPE_VIDEO;
451     } else if (audio_stream) {
452         _parse_vorbis_stream(info, audio_stream);
453         info->type = LMS_STREAM_TYPE_AUDIO;
454     }
455
456 done:
457     lms_destroy_ogg_sync(osync);
458     fclose(fp);
459
460     return r;
461 }
462
463
464 static const char _name[] = "ogg";
465 static const struct lms_string_size _exts[] = {
466     LMS_STATIC_STRING_SIZE(".ogg"),
467     LMS_STATIC_STRING_SIZE(".ogv")
468 };
469 static const char *_cats[] = {
470     "multimedia",
471     "audio",
472     NULL
473 };
474 static const char *_authors[] = {
475     "Renato Chencarek",
476     "Eduardo Lima (Etrunko)",
477     NULL
478 };
479
480 struct plugin {
481     struct lms_plugin plugin;
482     lms_db_audio_t *audio_db;
483     lms_db_video_t *video_db;
484 };
485
486 static void *
487 _match(struct plugin *p, const char *path, int len, int base)
488 {
489     long i;
490
491     i = lms_which_extension(path, len, _exts, LMS_ARRAY_SIZE(_exts));
492     if (i < 0)
493       return NULL;
494     else
495       return (void*)(i + 1);
496 }
497
498 static int
499 _parse(struct plugin *plugin, struct lms_context *ctxt,
500        const struct lms_file_info *finfo, void *match)
501 {
502     struct ogg_info info = { .type = LMS_STREAM_TYPE_UNKNOWN };
503     int r;
504
505     r = _parse_ogg(finfo->path, &info);
506     if (r != 0)
507       goto done;
508
509     if (!info.title.str)
510         info.title = str_extract_name_from_path(finfo->path, finfo->path_len,
511                                                 finfo->base,
512                                                 &_exts[((long) match) - 1],
513                                                 NULL);
514     if (info.title.str)
515         lms_charset_conv(ctxt->cs_conv, &info.title.str, &info.title.len);
516     if (info.artist.str)
517         lms_charset_conv(ctxt->cs_conv, &info.artist.str, &info.artist.len);
518
519     if (info.type == LMS_STREAM_TYPE_AUDIO) {
520         struct lms_audio_info audio_info = { };
521
522         if (info.album.str)
523             lms_charset_conv(ctxt->cs_conv, &info.album.str, &info.album.len);
524         if (info.genre.str)
525             lms_charset_conv(ctxt->cs_conv, &info.genre.str, &info.genre.len);
526
527         audio_info.id = finfo->id;
528         audio_info.title = info.title;
529         audio_info.artist = info.artist;
530         audio_info.album = info.album;
531         audio_info.genre = info.genre;
532
533         audio_info.trackno = info.trackno;
534         audio_info.container = _container;
535         audio_info.codec = _audio_codec;
536         audio_info.channels = info.channels;
537         audio_info.sampling_rate = info.sampling_rate;
538         audio_info.bitrate = info.bitrate;
539
540         r = lms_db_audio_add(plugin->audio_db, &audio_info);
541     } else if (info.type == LMS_STREAM_TYPE_VIDEO) {
542         struct lms_video_info video_info = { };
543
544         video_info.id = finfo->id;
545         video_info.title = info.title;
546         video_info.artist = info.artist;
547         video_info.streams = (struct lms_stream *) info.streams;
548         r = lms_db_video_add(plugin->video_db, &video_info);
549     }
550
551 done:
552     while (info.streams) {
553         struct stream *s = info.streams;
554         info.streams = (struct stream *) s->base.next;
555         _stream_free(s);
556     }
557
558     free(info.title.str);
559     free(info.artist.str);
560     free(info.album.str);
561     free(info.genre.str);
562
563     return r;
564 }
565
566 static int
567 _setup(struct plugin *plugin, struct lms_context *ctxt)
568 {
569     plugin->audio_db = lms_db_audio_new(ctxt->db);
570     if (!plugin->audio_db)
571         return -1;
572     plugin->video_db = lms_db_video_new(ctxt->db);
573     if (!plugin->video_db)
574         return -1;
575
576     return 0;
577 }
578
579 static int
580 _start(struct plugin *plugin, struct lms_context *ctxt)
581 {
582     int r;
583     r = lms_db_audio_start(plugin->audio_db);
584     r |= lms_db_video_start(plugin->video_db);
585     return r;
586 }
587
588 static int
589 _finish(struct plugin *plugin, struct lms_context *ctxt)
590 {
591     if (plugin->audio_db)
592         lms_db_audio_free(plugin->audio_db);
593     if (plugin->video_db)
594         lms_db_video_free(plugin->video_db);
595     return 0;
596 }
597
598 static int
599 _close(struct plugin *plugin)
600 {
601     free(plugin);
602     return 0;
603 }
604
605 API struct lms_plugin *
606 lms_plugin_open(void)
607 {
608     struct plugin *plugin;
609
610     plugin = (struct plugin *)malloc(sizeof(*plugin));
611     plugin->plugin.name = _name;
612     plugin->plugin.match = (lms_plugin_match_fn_t)_match;
613     plugin->plugin.parse = (lms_plugin_parse_fn_t)_parse;
614     plugin->plugin.close = (lms_plugin_close_fn_t)_close;
615     plugin->plugin.setup = (lms_plugin_setup_fn_t)_setup;
616     plugin->plugin.start = (lms_plugin_start_fn_t)_start;
617     plugin->plugin.finish = (lms_plugin_finish_fn_t)_finish;
618
619     return (struct lms_plugin *)plugin;
620 }
621
622 API const struct lms_plugin_info *
623 lms_plugin_info(void)
624 {
625     static struct lms_plugin_info info = {
626         _name,
627         _cats,
628         "OGG files",
629         PACKAGE_VERSION,
630         _authors,
631         "http://lms.garage.maemo.org"
632     };
633
634     return &info;
635 }