format_opus.c 6.52 KB
Newer Older
giles's avatar
giles committed
1 2
/* Icecast
 *
3
 *  This program is distributed under the GNU General Public License,
4 5 6
 *  version 2. A copy of this license is included with this source.
 *  At your option, this specific source file can also be distributed
 *  under the GNU GPL version 3.
giles's avatar
giles committed
7
 *
8
 * Copyright 2012,      David Richards, Mozilla Foundation,
giles's avatar
giles committed
9 10 11 12 13 14 15 16 17 18 19
 *                      and others (see AUTHORS for details).
 */


/* Ogg codec handler for opus streams */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
20
#include <string.h>
giles's avatar
giles committed
21 22 23
#include <ogg/ogg.h>

#include "format_opus.h"
24
#include "stats.h"
giles's avatar
giles committed
25 26 27 28 29 30 31 32
#include "refbuf.h"
#include "client.h"

#define CATMODULE "format-opus"
#include "logging.h"

static void opus_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
{
33 34
    stats_event(ogg_info->mount, "audio_channels", NULL);
    stats_event(ogg_info->mount, "audio_samplerate", NULL);
Philipp Schafft's avatar
Philipp Schafft committed
35 36
    ogg_stream_clear(&codec->os);
    free(codec);
giles's avatar
giles committed
37 38
}

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
static uint32_t __read_header_u32be_unaligned(const unsigned char *in)
{
    uint32_t ret = 0;
    ret += in[3];
    ret <<= 8;
    ret += in[2];
    ret <<= 8;
    ret += in[1];
    ret <<= 8;
    ret += in[0];
    return ret;
}

static void __handle_header_opushead(ogg_state_t *ogg_info, ogg_packet *packet)
{
    if (packet->bytes < 19) {
        ICECAST_LOG_WARN("Bad Opus header: header too small, expected at least 19 byte, got %li", (long int)packet->bytes);
        return; /* Invalid OpusHead */
    }

    if ((packet->packet[8] & 0xF0) != 0) {
        ICECAST_LOG_WARN("Bad Opus header: bad header version, expected major 0, got %i", (int)packet->packet[8]);
        return; /* Invalid OpusHead Version */
    }

    stats_event_args(ogg_info->mount, "audio_channels", "%ld", (long int)packet->packet[9]);
    stats_event_args(ogg_info->mount, "audio_samplerate", "%ld", (long int)__read_header_u32be_unaligned(packet->packet+12));
}

static void __handle_header_opustags(ogg_state_t *ogg_info, ogg_packet *packet, format_plugin_t *plugin) 
{
    size_t comments;
    size_t next;
    size_t left = packet->bytes;
    size_t buflen = 0;
    char *buf = NULL;
    char *buf_new;
    const void *p = packet->packet;

    if (packet->bytes < 16) {
        ICECAST_LOG_WARN("Bad Opus header: header too small, expected at least 16 byte, got %li", (long int)packet->bytes);
        return; /* Invalid OpusHead */
    }

    /* Skip header "OpusTags" */
    p += 8;
    left -= 8;

    /* Now the vendor string follows. We just skip it. */
    next = __read_header_u32be_unaligned(p);
    p += 4;
    left -= 4;

    if (left < (next + 4)) {
        ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header.");
        return;
    }
    p += next;
    left -= next;

    /* Next is the comment counter. */
    comments = __read_header_u32be_unaligned(p);
    p += 4;
    left -= 4;

    /* Ok, next (comments) blocks follows, each composed of 4 byte length followed by the data */
    if (left < (comments * 4)) {
        ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header.");
        return;
    }

    vorbis_comment_clear(&plugin->vc);
    vorbis_comment_init(&plugin->vc);

    while (comments) {
        next = __read_header_u32be_unaligned(p);
        p += 4;
        left -= 4;

        if (left < next) {
            if (buf)
                free(buf);
            vorbis_comment_clear(&plugin->vc);
            vorbis_comment_init(&plugin->vc);
            ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header.");
            return;
        }

        if ((next + 1) > buflen) {
            buf_new = realloc(buf, next + 1);
            if (buf_new) {
                buf = buf_new;
                buflen = next + 1;
            }
        }

        if (buflen >= (next + 1)) {
            memcpy(buf, p, next);
            buf[next] = 0;
            vorbis_comment_add(&plugin->vc, buf);
        }

        p += next;
        left -= next;

        comments--;
        if (comments && left < 4) {
            if (buf)
                free(buf);
            vorbis_comment_clear(&plugin->vc);
            vorbis_comment_init(&plugin->vc);
            ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header.");
            return;
        }
    }

    if (buf)
        free(buf);

    ogg_info->log_metadata = 1;
}

static void __handle_header(ogg_state_t *ogg_info,
        ogg_codec_t *codec, ogg_packet *packet, format_plugin_t *plugin)
{
    ICECAST_LOG_DEBUG("Got Opus header");
    if (packet->bytes < 8) {
        ICECAST_LOG_DEBUG("Not a real header, less than 8 bytes in size.");
        return; /* packet is not a header */
    }

    if (strncmp((const char*)packet->packet, "OpusHead", 8) == 0) {
171
        ICECAST_LOG_DEBUG("Got Opus header: OpusHead");
172 173
        __handle_header_opushead(ogg_info, packet);
    } else if (strncmp((const char*)packet->packet, "OpusTags", 8) == 0) {
174
        ICECAST_LOG_DEBUG("Got Opus header: OpusTags");
175 176 177 178 179 180
        __handle_header_opustags(ogg_info, packet, plugin);
    } else {
        ICECAST_LOG_DEBUG("Unknown header or data.");
        return; /* Unknown header or data */
    }
}
giles's avatar
giles committed
181 182

static refbuf_t *process_opus_page (ogg_state_t *ogg_info,
183
        ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
giles's avatar
giles committed
184 185 186 187 188 189 190 191 192 193 194 195
{
    refbuf_t *refbuf;

    if (codec->headers < 2)
    {
        ogg_packet packet;

        ogg_stream_pagein (&codec->os, page);
        while (ogg_stream_packetout (&codec->os, &packet) > 0)
        {
           /* first time around (normal case) yields comments */
           codec->headers++;
196
            __handle_header(ogg_info, codec, &packet, plugin);
giles's avatar
giles committed
197 198 199 200 201 202 203 204 205 206 207 208 209
        }
        /* add header page to associated list */
        format_ogg_attach_header (ogg_info, page);
        return NULL;
    }
    refbuf = make_refbuf_with_page (page);
    return refbuf;
}


ogg_codec_t *initial_opus_page (format_plugin_t *plugin, ogg_page *page)
{
    ogg_state_t *ogg_info = plugin->_state;
Philipp Schafft's avatar
Philipp Schafft committed
210
    ogg_codec_t *codec = calloc(1, sizeof (ogg_codec_t));
giles's avatar
giles committed
211 212
    ogg_packet packet;

Philipp Schafft's avatar
Philipp Schafft committed
213 214
    ogg_stream_init(&codec->os, ogg_page_serialno (page));
    ogg_stream_pagein(&codec->os, page);
giles's avatar
giles committed
215

Philipp Schafft's avatar
Philipp Schafft committed
216
    ogg_stream_packetout(&codec->os, &packet);
giles's avatar
giles committed
217

218
    ICECAST_LOG_DEBUG("checking for opus codec");
219
    if (packet.bytes < 8 || strncmp((char *)packet.packet, "OpusHead", 8) != 0)
giles's avatar
giles committed
220
    {
Philipp Schafft's avatar
Philipp Schafft committed
221 222
        ogg_stream_clear(&codec->os);
        free(codec);
giles's avatar
giles committed
223 224
        return NULL;
    }
225
    __handle_header(ogg_info, codec, &packet, plugin);
226
    ICECAST_LOG_INFO("seen initial opus header");
giles's avatar
giles committed
227 228
    codec->process_page = process_opus_page;
    codec->codec_free = opus_codec_free;
229
    codec->name = "Opus";
giles's avatar
giles committed
230 231 232 233 234
    codec->headers = 1;
    format_ogg_attach_header (ogg_info, page);
    return codec;
}