format_opus.c 6.59 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
 *                      and others (see AUTHORS for details).
10
 * Copyright 2014-2018, Philipp "ph3-der-loewe" Schafft <lion@lion.leolix.org>,
giles's avatar
giles committed
11 12 13 14 15 16 17 18 19 20
 */


/* Ogg codec handler for opus streams */

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

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

#include "format_opus.h"
25
#include "stats.h"
giles's avatar
giles committed
26 27 28 29 30 31 32 33
#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)
{
34 35
    stats_event(ogg_info->mount, "audio_channels", NULL);
    stats_event(ogg_info->mount, "audio_samplerate", NULL);
Philipp Schafft's avatar
Philipp Schafft committed
36 37
    ogg_stream_clear(&codec->os);
    free(codec);
giles's avatar
giles committed
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 171
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) {
172
        ICECAST_LOG_DEBUG("Got Opus header: OpusHead");
173 174
        __handle_header_opushead(ogg_info, packet);
    } else if (strncmp((const char*)packet->packet, "OpusTags", 8) == 0) {
175
        ICECAST_LOG_DEBUG("Got Opus header: OpusTags");
176 177 178 179 180 181
        __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
182 183

static refbuf_t *process_opus_page (ogg_state_t *ogg_info,
184
        ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
giles's avatar
giles committed
185 186 187 188 189 190 191 192 193 194 195 196
{
    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++;
197
            __handle_header(ogg_info, codec, &packet, plugin);
giles's avatar
giles committed
198 199 200 201 202 203 204 205 206 207 208 209 210
        }
        /* 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
211
    ogg_codec_t *codec = calloc(1, sizeof (ogg_codec_t));
giles's avatar
giles committed
212 213
    ogg_packet packet;

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

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

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