gstrtpvorbispay.c 29.9 KB
Newer Older
Wim Taymans's avatar
Wim Taymans committed
1
/* GStreamer
2
 * Copyright (C) <2006> Wim Taymans <wim.taymans@gmail.com>
Wim Taymans's avatar
Wim Taymans committed
3 4 5 6 7 8 9 10 11 12 13 14 15
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
Tim-Philipp Müller's avatar
Tim-Philipp Müller committed
16 17
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
Wim Taymans's avatar
Wim Taymans committed
18 19 20 21 22 23 24 25 26
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <string.h>

#include <gst/rtp/gstrtpbuffer.h>
27
#include <gst/audio/audio.h>
Wim Taymans's avatar
Wim Taymans committed
28

29
#include "fnv1hash.h"
Wim Taymans's avatar
Wim Taymans committed
30
#include "gstrtpvorbispay.h"
31
#include "gstrtputils.h"
Wim Taymans's avatar
Wim Taymans committed
32 33 34 35 36

GST_DEBUG_CATEGORY_STATIC (rtpvorbispay_debug);
#define GST_CAT_DEFAULT (rtpvorbispay_debug)

/* references:
37
 * http://www.rfc-editor.org/rfc/rfc5215.txt
Wim Taymans's avatar
Wim Taymans committed
38 39 40 41 42 43 44 45
 */

static GstStaticPadTemplate gst_rtp_vorbis_pay_src_template =
GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("application/x-rtp, "
        "media = (string) \"audio\", "
46
        "payload = (int) " GST_RTP_PAYLOAD_DYNAMIC_STRING ", "
47
        "clock-rate = (int) [1, MAX ], " "encoding-name = (string) \"VORBIS\""
Wim Taymans's avatar
Wim Taymans committed
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
        /* All required parameters
         *
         * "encoding-params = (string) <num channels>"
         * "configuration = (string) ANY"
         */
    )
    );

static GstStaticPadTemplate gst_rtp_vorbis_pay_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-vorbis")
    );

63 64 65 66 67 68 69 70
#define DEFAULT_CONFIG_INTERVAL 0

enum
{
  PROP_0,
  PROP_CONFIG_INTERVAL
};

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
71
#define gst_rtp_vorbis_pay_parent_class parent_class
Wim Taymans's avatar
Wim Taymans committed
72
G_DEFINE_TYPE (GstRtpVorbisPay, gst_rtp_vorbis_pay, GST_TYPE_RTP_BASE_PAYLOAD);
Wim Taymans's avatar
Wim Taymans committed
73

Wim Taymans's avatar
Wim Taymans committed
74
static gboolean gst_rtp_vorbis_pay_setcaps (GstRTPBasePayload * basepayload,
Wim Taymans's avatar
Wim Taymans committed
75
    GstCaps * caps);
Wim Taymans's avatar
Wim Taymans committed
76 77
static GstStateChangeReturn gst_rtp_vorbis_pay_change_state (GstElement *
    element, GstStateChange transition);
Wim Taymans's avatar
Wim Taymans committed
78
static GstFlowReturn gst_rtp_vorbis_pay_handle_buffer (GstRTPBasePayload * pad,
Wim Taymans's avatar
Wim Taymans committed
79
    GstBuffer * buffer);
Wim Taymans's avatar
Wim Taymans committed
80
static gboolean gst_rtp_vorbis_pay_sink_event (GstRTPBasePayload * payload,
81
    GstEvent * event);
Wim Taymans's avatar
Wim Taymans committed
82

83 84 85 86 87
static gboolean gst_rtp_vorbis_pay_parse_id (GstRTPBasePayload * basepayload,
    guint8 * data, guint size);
static gboolean gst_rtp_vorbis_pay_finish_headers (GstRTPBasePayload *
    basepayload);

88 89 90 91 92
static void gst_rtp_vorbis_pay_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_rtp_vorbis_pay_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);

Wim Taymans's avatar
Wim Taymans committed
93 94 95
static void
gst_rtp_vorbis_pay_class_init (GstRtpVorbisPayClass * klass)
{
96
  GObjectClass *gobject_class;
Wim Taymans's avatar
Wim Taymans committed
97
  GstElementClass *gstelement_class;
Wim Taymans's avatar
Wim Taymans committed
98
  GstRTPBasePayloadClass *gstrtpbasepayload_class;
Wim Taymans's avatar
Wim Taymans committed
99

100
  gobject_class = (GObjectClass *) klass;
Wim Taymans's avatar
Wim Taymans committed
101
  gstelement_class = (GstElementClass *) klass;
Wim Taymans's avatar
Wim Taymans committed
102
  gstrtpbasepayload_class = (GstRTPBasePayloadClass *) klass;
Wim Taymans's avatar
Wim Taymans committed
103

Wim Taymans's avatar
Wim Taymans committed
104 105
  gstelement_class->change_state = gst_rtp_vorbis_pay_change_state;

Wim Taymans's avatar
Wim Taymans committed
106 107
  gstrtpbasepayload_class->set_caps = gst_rtp_vorbis_pay_setcaps;
  gstrtpbasepayload_class->handle_buffer = gst_rtp_vorbis_pay_handle_buffer;
Wim Taymans's avatar
Wim Taymans committed
108
  gstrtpbasepayload_class->sink_event = gst_rtp_vorbis_pay_sink_event;
Wim Taymans's avatar
Wim Taymans committed
109

110 111 112
  gobject_class->set_property = gst_rtp_vorbis_pay_set_property;
  gobject_class->get_property = gst_rtp_vorbis_pay_get_property;

113 114 115 116
  gst_element_class_add_static_pad_template (gstelement_class,
      &gst_rtp_vorbis_pay_src_template);
  gst_element_class_add_static_pad_template (gstelement_class,
      &gst_rtp_vorbis_pay_sink_template);
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
117

118
  gst_element_class_set_static_metadata (gstelement_class,
119
      "RTP Vorbis payloader",
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
120 121
      "Codec/Payloader/Network/RTP",
      "Payload-encode Vorbis audio into RTP packets (RFC 5215)",
122
      "Wim Taymans <wim.taymans@gmail.com>");
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
123

Wim Taymans's avatar
Wim Taymans committed
124 125
  GST_DEBUG_CATEGORY_INIT (rtpvorbispay_debug, "rtpvorbispay", 0,
      "Vorbis RTP Payloader");
126 127 128 129 130 131 132 133

  g_object_class_install_property (G_OBJECT_CLASS (klass), PROP_CONFIG_INTERVAL,
      g_param_spec_uint ("config-interval", "Config Send Interval",
          "Send Config Insertion Interval in seconds (configuration headers "
          "will be multiplexed in the data stream when detected.) (0 = disabled)",
          0, 3600, DEFAULT_CONFIG_INTERVAL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)
      );
Wim Taymans's avatar
Wim Taymans committed
134 135 136
}

static void
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
137
gst_rtp_vorbis_pay_init (GstRtpVorbisPay * rtpvorbispay)
Wim Taymans's avatar
Wim Taymans committed
138
{
139
  rtpvorbispay->last_config = GST_CLOCK_TIME_NONE;
Wim Taymans's avatar
Wim Taymans committed
140 141
}

142 143 144 145 146 147
static void
gst_rtp_vorbis_pay_clear_packet (GstRtpVorbisPay * rtpvorbispay)
{
  if (rtpvorbispay->packet)
    gst_buffer_unref (rtpvorbispay->packet);
  rtpvorbispay->packet = NULL;
148 149
  g_list_free_full (rtpvorbispay->packet_buffers,
      (GDestroyNotify) gst_buffer_unref);
150
  rtpvorbispay->packet_buffers = NULL;
151 152
}

Wim Taymans's avatar
Wim Taymans committed
153 154 155
static void
gst_rtp_vorbis_pay_cleanup (GstRtpVorbisPay * rtpvorbispay)
{
156
  gst_rtp_vorbis_pay_clear_packet (rtpvorbispay);
157 158
  g_list_free_full (rtpvorbispay->headers, (GDestroyNotify) gst_buffer_unref);
  rtpvorbispay->headers = NULL;
159
  g_free (rtpvorbispay->config_data);
160 161
  rtpvorbispay->config_data = NULL;
  rtpvorbispay->last_config = GST_CLOCK_TIME_NONE;
Wim Taymans's avatar
Wim Taymans committed
162 163
}

Wim Taymans's avatar
Wim Taymans committed
164
static gboolean
Wim Taymans's avatar
Wim Taymans committed
165
gst_rtp_vorbis_pay_setcaps (GstRTPBasePayload * basepayload, GstCaps * caps)
Wim Taymans's avatar
Wim Taymans committed
166 167
{
  GstRtpVorbisPay *rtpvorbispay;
168 169 170 171 172
  GstStructure *s;
  const GValue *array;
  gint asize, i;
  GstBuffer *buf;
  GstMapInfo map;
Wim Taymans's avatar
Wim Taymans committed
173 174 175

  rtpvorbispay = GST_RTP_VORBIS_PAY (basepayload);

176 177
  s = gst_caps_get_structure (caps, 0);

178 179
  rtpvorbispay->need_headers = TRUE;

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
  if ((array = gst_structure_get_value (s, "streamheader")) == NULL)
    goto done;

  if (G_VALUE_TYPE (array) != GST_TYPE_ARRAY)
    goto done;

  if ((asize = gst_value_array_get_size (array)) < 3)
    goto done;

  for (i = 0; i < asize; i++) {
    const GValue *value;

    value = gst_value_array_get_value (array, i);
    if ((buf = gst_value_get_buffer (value)) == NULL)
      goto null_buffer;

    gst_buffer_map (buf, &map, GST_MAP_READ);
197 198 199
    if (map.size < 1)
      goto invalid_streamheader;

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
    /* no data packets allowed */
    if ((map.data[0] & 1) == 0)
      goto invalid_streamheader;

    /* we need packets with id 1, 3, 5 */
    if (map.data[0] != (i * 2) + 1)
      goto invalid_streamheader;

    if (i == 0) {
      /* identification, we need to parse this in order to get the clock rate. */
      if (G_UNLIKELY (!gst_rtp_vorbis_pay_parse_id (basepayload, map.data,
                  map.size)))
        goto parse_id_failed;
    }
    GST_DEBUG_OBJECT (rtpvorbispay, "collecting header %d", i);
    rtpvorbispay->headers =
        g_list_append (rtpvorbispay->headers, gst_buffer_ref (buf));
    gst_buffer_unmap (buf, &map);
  }
  if (!gst_rtp_vorbis_pay_finish_headers (basepayload))
    goto finish_failed;

done:
Wim Taymans's avatar
Wim Taymans committed
223
  return TRUE;
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247

  /* ERRORS */
null_buffer:
  {
    GST_WARNING_OBJECT (rtpvorbispay, "streamheader with null buffer received");
    return FALSE;
  }
invalid_streamheader:
  {
    GST_WARNING_OBJECT (rtpvorbispay, "unable to parse initial header");
    gst_buffer_unmap (buf, &map);
    return FALSE;
  }
parse_id_failed:
  {
    GST_WARNING_OBJECT (rtpvorbispay, "unable to parse initial header");
    gst_buffer_unmap (buf, &map);
    return FALSE;
  }
finish_failed:
  {
    GST_WARNING_OBJECT (rtpvorbispay, "unable to finish headers");
    return FALSE;
  }
Wim Taymans's avatar
Wim Taymans committed
248 249 250
}

static void
251
gst_rtp_vorbis_pay_reset_packet (GstRtpVorbisPay * rtpvorbispay, guint8 VDT)
Wim Taymans's avatar
Wim Taymans committed
252 253
{
  guint payload_len;
254
  GstRTPBuffer rtp = { NULL };
Wim Taymans's avatar
Wim Taymans committed
255

256
  GST_LOG_OBJECT (rtpvorbispay, "reset packet");
Wim Taymans's avatar
Wim Taymans committed
257 258

  rtpvorbispay->payload_pos = 4;
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
259 260 261
  gst_rtp_buffer_map (rtpvorbispay->packet, GST_MAP_READ, &rtp);
  payload_len = gst_rtp_buffer_get_payload_len (&rtp);
  gst_rtp_buffer_unmap (&rtp);
Wim Taymans's avatar
Wim Taymans committed
262 263 264
  rtpvorbispay->payload_left = payload_len - 4;
  rtpvorbispay->payload_duration = 0;
  rtpvorbispay->payload_F = 0;
265
  rtpvorbispay->payload_VDT = VDT;
Wim Taymans's avatar
Wim Taymans committed
266 267 268
  rtpvorbispay->payload_pkts = 0;
}

269
static void
270 271
gst_rtp_vorbis_pay_init_packet (GstRtpVorbisPay * rtpvorbispay, guint8 VDT,
    GstClockTime timestamp)
272
{
273
  GST_LOG_OBJECT (rtpvorbispay, "starting new packet, VDT: %d", VDT);
274

275
  gst_rtp_vorbis_pay_clear_packet (rtpvorbispay);
276 277 278

  /* new packet allocate max packet size */
  rtpvorbispay->packet =
Wim Taymans's avatar
Wim Taymans committed
279
      gst_rtp_buffer_new_allocate_len (GST_RTP_BASE_PAYLOAD_MTU
280 281
      (rtpvorbispay), 0, 0);
  gst_rtp_vorbis_pay_reset_packet (rtpvorbispay, VDT);
282

283
  GST_BUFFER_PTS (rtpvorbispay->packet) = timestamp;
284 285
}

Wim Taymans's avatar
Wim Taymans committed
286 287 288 289 290 291
static GstFlowReturn
gst_rtp_vorbis_pay_flush_packet (GstRtpVorbisPay * rtpvorbispay)
{
  GstFlowReturn ret;
  guint8 *payload;
  guint hlen;
292
  GstRTPBuffer rtp = { NULL };
293
  GList *l;
Wim Taymans's avatar
Wim Taymans committed
294 295

  /* check for empty packet */
296
  if (!rtpvorbispay->packet || rtpvorbispay->payload_pos <= 4)
Wim Taymans's avatar
Wim Taymans committed
297 298
    return GST_FLOW_OK;

299
  GST_LOG_OBJECT (rtpvorbispay, "flushing packet");
Wim Taymans's avatar
Wim Taymans committed
300

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
301 302
  gst_rtp_buffer_map (rtpvorbispay->packet, GST_MAP_WRITE, &rtp);

Wim Taymans's avatar
Wim Taymans committed
303
  /* fix header */
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
304
  payload = gst_rtp_buffer_get_payload (&rtp);
Wim Taymans's avatar
Wim Taymans committed
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
  /*
   *  0                   1                   2                   3
   *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * |                     Ident                     | F |VDT|# pkts.|
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *
   * F: Fragment type (0=none, 1=start, 2=cont, 3=end)
   * VDT: Vorbis data type (0=vorbis, 1=config, 2=comment, 3=reserved)
   * pkts: number of packets.
   */
  payload[0] = (rtpvorbispay->payload_ident >> 16) & 0xff;
  payload[1] = (rtpvorbispay->payload_ident >> 8) & 0xff;
  payload[2] = (rtpvorbispay->payload_ident) & 0xff;
  payload[3] = (rtpvorbispay->payload_F & 0x3) << 6 |
      (rtpvorbispay->payload_VDT & 0x3) << 4 |
      (rtpvorbispay->payload_pkts & 0xf);

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
323 324
  gst_rtp_buffer_unmap (&rtp);

Wim Taymans's avatar
Wim Taymans committed
325 326
  /* shrink the buffer size to the last written byte */
  hlen = gst_rtp_buffer_calc_header_len (0);
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
327
  gst_buffer_resize (rtpvorbispay->packet, 0, hlen + rtpvorbispay->payload_pos);
Wim Taymans's avatar
Wim Taymans committed
328

329 330
  GST_BUFFER_DURATION (rtpvorbispay->packet) = rtpvorbispay->payload_duration;

331 332
  for (l = g_list_last (rtpvorbispay->packet_buffers); l; l = l->prev) {
    GstBuffer *buf = GST_BUFFER_CAST (l->data);
333
    gst_rtp_copy_audio_meta (rtpvorbispay, rtpvorbispay->packet, buf);
334 335 336 337 338
    gst_buffer_unref (buf);
  }
  g_list_free (rtpvorbispay->packet_buffers);
  rtpvorbispay->packet_buffers = NULL;

Wim Taymans's avatar
Wim Taymans committed
339 340
  /* push, this gives away our ref to the packet, so clear it. */
  ret =
Wim Taymans's avatar
Wim Taymans committed
341
      gst_rtp_base_payload_push (GST_RTP_BASE_PAYLOAD (rtpvorbispay),
Wim Taymans's avatar
Wim Taymans committed
342 343 344 345 346 347
      rtpvorbispay->packet);
  rtpvorbispay->packet = NULL;

  return ret;
}

348
static gboolean
Wim Taymans's avatar
Wim Taymans committed
349
gst_rtp_vorbis_pay_finish_headers (GstRTPBasePayload * basepayload)
350 351 352
{
  GstRtpVorbisPay *rtpvorbispay = GST_RTP_VORBIS_PAY (basepayload);
  GList *walk;
353
  guint length, size, n_headers, configlen, extralen;
354
  gchar *cstr, *configuration;
355
  guint8 *data, *config;
356
  guint32 ident;
357
  gboolean res;
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381

  GST_DEBUG_OBJECT (rtpvorbispay, "finish headers");

  if (!rtpvorbispay->headers)
    goto no_headers;

  /* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * |                     Number of packed headers                  |
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * |                          Packed header                        |
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * |                          Packed header                        |
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * |                          ....                                 |
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   *
   * We only construct a config containing 1 packed header like this:
   *
   *  0                   1                   2                   3
   *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
382 383 384 385 386 387 388 389 390
   * |                   Ident                       | length       ..
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * ..              | n. of headers |    length1    |    length2   ..
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * ..              |             Identification Header            ..
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * .................................................................
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * ..              |         Comment Header                       ..
391
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
392
   * .................................................................
393
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
394
   * ..                        Comment Header                        |
395 396 397
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   * |                          Setup Header                        ..
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
398 399
   * .................................................................
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
400 401 402 403
   * ..                         Setup Header                         |
   * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   */

404 405 406 407 408
  /* we need 4 bytes for the number of headers (which is always 1), 3 bytes for
   * the ident, 2 bytes for length, 1 byte for n. of headers. */
  size = 4 + 3 + 2 + 1;

  /* count the size of the headers first and update the hash */
409
  length = 0;
410
  n_headers = 0;
411
  ident = fnv1_hash_32_new ();
412
  extralen = 1;
413 414
  for (walk = rtpvorbispay->headers; walk; walk = g_list_next (walk)) {
    GstBuffer *buf = GST_BUFFER_CAST (walk->data);
Wim Taymans's avatar
Wim Taymans committed
415 416
    GstMapInfo map;
    guint bsize;
417

Wim Taymans's avatar
Wim Taymans committed
418
    bsize = gst_buffer_get_size (buf);
419 420 421 422 423 424 425 426
    length += bsize;
    n_headers++;

    /* count number of bytes needed for length fields, we don't need this for
     * the last header. */
    if (g_list_next (walk)) {
      do {
        size++;
427
        extralen++;
428 429 430 431
        bsize >>= 7;
      } while (bsize);
    }
    /* update hash */
Wim Taymans's avatar
Wim Taymans committed
432 433 434
    gst_buffer_map (buf, &map, GST_MAP_READ);
    ident = fnv1_hash_32_update (ident, map.data, map.size);
    gst_buffer_unmap (buf, &map);
435 436
  }

437 438 439
  /* packet length is header size + packet length */
  configlen = size + length;
  config = data = g_malloc (configlen);
440

441 442 443 444 445 446
  /* number of packed headers, we only pack 1 header */
  data[0] = 0;
  data[1] = 0;
  data[2] = 0;
  data[3] = 1;

447 448 449
  ident = fnv1_hash_32_to_24 (ident);
  rtpvorbispay->payload_ident = ident;
  GST_DEBUG_OBJECT (rtpvorbispay, "ident 0x%08x", ident);
450 451

  /* take lower 3 bytes */
452 453 454
  data[4] = (ident >> 16) & 0xff;
  data[5] = (ident >> 8) & 0xff;
  data[6] = ident & 0xff;
455

456 457 458 459 460 461 462 463 464 465 466 467
  /* store length of all vorbis headers */
  data[7] = ((length) >> 8) & 0xff;
  data[8] = (length) & 0xff;

  /* store number of headers minus one. */
  data[9] = n_headers - 1;
  data += 10;

  /* store length for each header */
  for (walk = rtpvorbispay->headers; walk; walk = g_list_next (walk)) {
    GstBuffer *buf = GST_BUFFER_CAST (walk->data);
    guint bsize, size, temp;
468
    guint flag;
469 470 471 472 473

    /* only need to store the length when it's not the last header */
    if (!g_list_next (walk))
      break;

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
474
    bsize = gst_buffer_get_size (buf);
475 476 477 478 479 480 481 482 483

    /* calc size */
    size = 0;
    do {
      size++;
      bsize >>= 7;
    } while (bsize);
    temp = size;

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
484
    bsize = gst_buffer_get_size (buf);
485
    /* write the size backwards */
486
    flag = 0;
487 488
    while (size) {
      size--;
489
      data[size] = (bsize & 0x7f) | flag;
490
      bsize >>= 7;
491
      flag = 0x80;              /* Flag bit on all bytes of the length except the last */
492 493 494
    }
    data += temp;
  }
495 496 497 498 499

  /* copy header data */
  for (walk = rtpvorbispay->headers; walk; walk = g_list_next (walk)) {
    GstBuffer *buf = GST_BUFFER_CAST (walk->data);

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
500 501
    gst_buffer_extract (buf, 0, data, gst_buffer_get_size (buf));
    data += gst_buffer_get_size (buf);
502
  }
503
  rtpvorbispay->need_headers = FALSE;
504

505
  /* serialize to base64 */
506
  configuration = g_base64_encode (config, configlen);
507 508

  /* store for later re-sending */
509
  g_free (rtpvorbispay->config_data);
510 511 512 513 514 515
  rtpvorbispay->config_size = configlen - 4 - 3 - 2;
  rtpvorbispay->config_data = g_malloc (rtpvorbispay->config_size);
  rtpvorbispay->config_extra_len = extralen;
  memcpy (rtpvorbispay->config_data, config + 4 + 3 + 2,
      rtpvorbispay->config_size);

516
  g_free (config);
517 518 519

  /* configure payloader settings */
  cstr = g_strdup_printf ("%d", rtpvorbispay->channels);
Wim Taymans's avatar
Wim Taymans committed
520
  gst_rtp_base_payload_set_options (basepayload, "audio", TRUE, "VORBIS",
521
      rtpvorbispay->rate);
522
  res =
Wim Taymans's avatar
Wim Taymans committed
523
      gst_rtp_base_payload_set_outcaps (basepayload, "encoding-params",
524
      G_TYPE_STRING, cstr, "configuration", G_TYPE_STRING, configuration, NULL);
525 526 527
  g_free (cstr);
  g_free (configuration);

528
  return res;
529 530 531 532 533 534 535 536 537

  /* ERRORS */
no_headers:
  {
    GST_DEBUG_OBJECT (rtpvorbispay, "finish headers");
    return FALSE;
  }
}

538
static gboolean
Wim Taymans's avatar
Wim Taymans committed
539
gst_rtp_vorbis_pay_parse_id (GstRTPBasePayload * basepayload, guint8 * data,
540
    guint size)
Wim Taymans's avatar
Wim Taymans committed
541
{
542
  GstRtpVorbisPay *rtpvorbispay = GST_RTP_VORBIS_PAY (basepayload);
543 544
  guint8 channels;
  gint32 rate, version;
Wim Taymans's avatar
Wim Taymans committed
545

546 547
  if (G_UNLIKELY (size < 16))
    goto too_short;
Wim Taymans's avatar
Wim Taymans committed
548

549 550 551
  if (G_UNLIKELY (memcmp (data, "\001vorbis", 7)))
    goto invalid_start;
  data += 7;
Wim Taymans's avatar
Wim Taymans committed
552

553 554 555
  if (G_UNLIKELY ((version = GST_READ_UINT32_LE (data)) != 0))
    goto invalid_version;
  data += 4;
Wim Taymans's avatar
Wim Taymans committed
556

557 558
  if (G_UNLIKELY ((channels = *data++) < 1))
    goto invalid_channels;
Wim Taymans's avatar
Wim Taymans committed
559

560 561
  if (G_UNLIKELY ((rate = GST_READ_UINT32_LE (data)) < 1))
    goto invalid_rate;
Wim Taymans's avatar
Wim Taymans committed
562

563 564 565
  /* all fine, store the values */
  rtpvorbispay->channels = channels;
  rtpvorbispay->rate = rate;
Wim Taymans's avatar
Wim Taymans committed
566

567
  return TRUE;
Wim Taymans's avatar
Wim Taymans committed
568

569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
  /* ERRORS */
too_short:
  {
    GST_ELEMENT_ERROR (basepayload, STREAM, DECODE,
        ("Identification packet is too short, need at least 16, got %d", size),
        (NULL));
    return FALSE;
  }
invalid_start:
  {
    GST_ELEMENT_ERROR (basepayload, STREAM, DECODE,
        ("Invalid header start in identification packet"), (NULL));
    return FALSE;
  }
invalid_version:
  {
    GST_ELEMENT_ERROR (basepayload, STREAM, DECODE,
        ("Invalid version, expected 0, got %d", version), (NULL));
    return FALSE;
  }
invalid_rate:
  {
    GST_ELEMENT_ERROR (basepayload, STREAM, DECODE,
        ("Invalid rate %d", rate), (NULL));
    return FALSE;
  }
invalid_channels:
  {
    GST_ELEMENT_ERROR (basepayload, STREAM, DECODE,
        ("Invalid channels %d", channels), (NULL));
    return FALSE;
Wim Taymans's avatar
Wim Taymans committed
600 601 602 603
  }
}

static GstFlowReturn
604
gst_rtp_vorbis_pay_payload_buffer (GstRtpVorbisPay * rtpvorbispay, guint8 VDT,
605 606
    GstBuffer * buffer, guint8 * data, guint size, GstClockTime timestamp,
    GstClockTime duration, guint not_in_length)
Wim Taymans's avatar
Wim Taymans committed
607
{
608
  GstFlowReturn ret = GST_FLOW_OK;
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
609
  guint newsize;
Wim Taymans's avatar
Wim Taymans committed
610
  guint packet_len;
611
  GstClockTime newduration;
Wim Taymans's avatar
Wim Taymans committed
612
  gboolean flush;
613 614 615
  guint plen;
  guint8 *ppos, *payload;
  gboolean fragmented;
616
  GstRTPBuffer rtp = { NULL };
Wim Taymans's avatar
Wim Taymans committed
617 618 619 620 621 622 623 624 625 626

  /* size increases with packet length and 2 bytes size eader. */
  newduration = rtpvorbispay->payload_duration;
  if (duration != GST_CLOCK_TIME_NONE)
    newduration += duration;

  newsize = rtpvorbispay->payload_pos + 2 + size;
  packet_len = gst_rtp_buffer_calc_packet_len (newsize, 0, 0);

  /* check buffer filled against length and max latency */
627 628
  flush = gst_rtp_base_payload_is_filled (GST_RTP_BASE_PAYLOAD (rtpvorbispay),
      packet_len, newduration);
Wim Taymans's avatar
Wim Taymans committed
629 630
  /* we can store up to 15 vorbis packets in one RTP packet. */
  flush |= (rtpvorbispay->payload_pkts == 15);
631 632 633
  /* flush if we have a new VDT */
  if (rtpvorbispay->packet)
    flush |= (rtpvorbispay->payload_VDT != VDT);
Wim Taymans's avatar
Wim Taymans committed
634
  if (flush)
635
    ret = gst_rtp_vorbis_pay_flush_packet (rtpvorbispay);
Wim Taymans's avatar
Wim Taymans committed
636

637 638 639
  if (ret != GST_FLOW_OK)
    goto done;

640
  /* create new packet if we must */
641 642 643
  if (!rtpvorbispay->packet) {
    gst_rtp_vorbis_pay_init_packet (rtpvorbispay, VDT, timestamp);
  }
644

Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
645 646
  gst_rtp_buffer_map (rtpvorbispay->packet, GST_MAP_WRITE, &rtp);
  payload = gst_rtp_buffer_get_payload (&rtp);
647 648 649 650 651
  ppos = payload + rtpvorbispay->payload_pos;
  fragmented = FALSE;

  /* put buffer in packet, it either fits completely or needs to be fragmented
   * over multiple RTP packets. */
652
  do {
653 654
    plen = MIN (rtpvorbispay->payload_left - 2, size);

655
    GST_LOG_OBJECT (rtpvorbispay, "append %u bytes", plen);
656 657

    /* data is copied in the payload with a 2 byte length header */
658 659 660 661 662
    ppos[0] = ((plen - not_in_length) >> 8) & 0xff;
    ppos[1] = ((plen - not_in_length) & 0xff);
    if (plen)
      memcpy (&ppos[2], data, plen);

663 664 665 666 667 668 669 670 671 672 673 674
    if (buffer) {
      if (!rtpvorbispay->packet_buffers
          || rtpvorbispay->packet_buffers->data != (gpointer) buffer)
        rtpvorbispay->packet_buffers =
            g_list_prepend (rtpvorbispay->packet_buffers,
            gst_buffer_ref (buffer));
    } else {
      GList *l;

      for (l = rtpvorbispay->headers; l; l = l->next)
        rtpvorbispay->packet_buffers =
            g_list_prepend (rtpvorbispay->packet_buffers,
675
            gst_buffer_ref (l->data));
676 677
    }

678 679 680
    /* only first (only) configuration cuts length field */
    /* NOTE: spec (if any) is not clear on this ... */
    not_in_length = 0;
Wim Taymans's avatar
Wim Taymans committed
681

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
    size -= plen;
    data += plen;

    rtpvorbispay->payload_pos += plen + 2;
    rtpvorbispay->payload_left -= plen + 2;

    if (fragmented) {
      if (size == 0)
        /* last fragment, set F to 0x3. */
        rtpvorbispay->payload_F = 0x3;
      else
        /* fragment continues, set F to 0x2. */
        rtpvorbispay->payload_F = 0x2;
    } else {
      if (size > 0) {
        /* fragmented packet starts, set F to 0x1, mark ourselves as
         * fragmented. */
        rtpvorbispay->payload_F = 0x1;
        fragmented = TRUE;
      }
    }
    if (fragmented) {
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
704
      gst_rtp_buffer_unmap (&rtp);
705 706 707 708 709 710 711
      /* fragmented packets are always flushed and have ptks of 0 */
      rtpvorbispay->payload_pkts = 0;
      ret = gst_rtp_vorbis_pay_flush_packet (rtpvorbispay);

      if (size > 0) {
        /* start new packet and get pointers. VDT stays the same. */
        gst_rtp_vorbis_pay_init_packet (rtpvorbispay,
712
            rtpvorbispay->payload_VDT, timestamp);
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
713 714
        gst_rtp_buffer_map (rtpvorbispay->packet, GST_MAP_WRITE, &rtp);
        payload = gst_rtp_buffer_get_payload (&rtp);
715 716 717 718 719 720 721 722 723
        ppos = payload + rtpvorbispay->payload_pos;
      }
    } else {
      /* unfragmented packet, update stats for next packet, size == 0 and we
       * exit the while loop */
      rtpvorbispay->payload_pkts++;
      if (duration != GST_CLOCK_TIME_NONE)
        rtpvorbispay->payload_duration += duration;
    }
724
  } while (size && ret == GST_FLOW_OK);
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
725 726 727 728

  if (rtp.buffer)
    gst_rtp_buffer_unmap (&rtp);

729 730
done:

731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
  return ret;
}

static GstFlowReturn
gst_rtp_vorbis_pay_handle_buffer (GstRTPBasePayload * basepayload,
    GstBuffer * buffer)
{
  GstRtpVorbisPay *rtpvorbispay;
  GstFlowReturn ret;
  GstMapInfo map;
  gsize size;
  guint8 *data;
  GstClockTime duration, timestamp;
  guint8 VDT;

  rtpvorbispay = GST_RTP_VORBIS_PAY (basepayload);

  gst_buffer_map (buffer, &map, GST_MAP_READ);
  data = map.data;
  size = map.size;
  duration = GST_BUFFER_DURATION (buffer);
752
  timestamp = GST_BUFFER_PTS (buffer);
753 754 755 756

  GST_LOG_OBJECT (rtpvorbispay, "size %" G_GSIZE_FORMAT
      ", duration %" GST_TIME_FORMAT, size, GST_TIME_ARGS (duration));

757
  if (G_UNLIKELY (size < 1))
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
    goto wrong_size;

  /* find packet type */
  if (data[0] & 1) {
    /* header */
    if (data[0] == 1) {
      /* identification, we need to parse this in order to get the clock rate. */
      if (G_UNLIKELY (!gst_rtp_vorbis_pay_parse_id (basepayload, data, size)))
        goto parse_id_failed;
      VDT = 1;
    } else if (data[0] == 3) {
      /* comment */
      VDT = 2;
    } else if (data[0] == 5) {
      /* setup */
      VDT = 1;
    } else
      goto unknown_header;
  } else
    /* data */
    VDT = 0;

  /* we need to collect the headers and construct a config string from them */
  if (VDT != 0) {
782
    rtpvorbispay->need_headers = TRUE;
783
    if (!rtpvorbispay->need_headers && VDT == 1) {
784 785
      GST_INFO_OBJECT (rtpvorbispay, "getting new headers, replace existing");
      g_list_free_full (rtpvorbispay->headers,
786
          (GDestroyNotify) gst_buffer_unref);
787 788
      rtpvorbispay->headers = NULL;
    }
789
    GST_DEBUG_OBJECT (rtpvorbispay, "collecting header");
790 791 792 793 794
    /* append header to the list of headers, or replace
     * if the same type of header was already in there.
     *
     * This prevents storing an infinite amount of e.g. comment headers, there
     * must only be one */
795
    gst_buffer_unmap (buffer, &map);
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812

    if (rtpvorbispay->headers) {
      gboolean found = FALSE;
      GList *l;
      guint8 new_header_type;

      gst_buffer_extract (buffer, 0, &new_header_type, 1);

      for (l = rtpvorbispay->headers; l; l = l->next) {
        GstBuffer *header = l->data;
        guint8 header_type;

        if (gst_buffer_extract (header, 0, &header_type, 1)
            && header_type == new_header_type) {
          found = TRUE;
          gst_buffer_unref (header);
          l->data = buffer;
813
          break;
814 815 816 817 818 819 820 821
        }
      }
      if (!found)
        rtpvorbispay->headers = g_list_append (rtpvorbispay->headers, buffer);
    } else {
      rtpvorbispay->headers = g_list_append (rtpvorbispay->headers, buffer);
    }

822 823
    ret = GST_FLOW_OK;
    goto done;
824 825 826
  } else if (rtpvorbispay->headers && rtpvorbispay->need_headers) {
    if (!gst_rtp_vorbis_pay_finish_headers (basepayload))
      goto header_error;
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
  }

  /* there is a config request, see if we need to insert it */
  if (rtpvorbispay->config_interval > 0 && rtpvorbispay->config_data) {
    gboolean send_config = FALSE;

    if (rtpvorbispay->last_config != -1) {
      guint64 diff;

      GST_LOG_OBJECT (rtpvorbispay,
          "now %" GST_TIME_FORMAT ", last config %" GST_TIME_FORMAT,
          GST_TIME_ARGS (timestamp), GST_TIME_ARGS (rtpvorbispay->last_config));

      /* calculate diff between last config in milliseconds */
      if (timestamp > rtpvorbispay->last_config) {
        diff = timestamp - rtpvorbispay->last_config;
      } else {
        diff = 0;
      }

      GST_DEBUG_OBJECT (rtpvorbispay,
          "interval since last config %" GST_TIME_FORMAT, GST_TIME_ARGS (diff));

      /* bigger than interval, queue config */
      /* FIXME should convert timestamps to running time */
      if (GST_TIME_AS_SECONDS (diff) >= rtpvorbispay->config_interval) {
        GST_DEBUG_OBJECT (rtpvorbispay, "time to send config");
        send_config = TRUE;
      }
    } else {
      /* no known previous config time, send now */
      GST_DEBUG_OBJECT (rtpvorbispay, "no previous config time, send now");
      send_config = TRUE;
    }

    if (send_config) {
      /* we need to send config now first */
      /* different TDT type forces flush */
      gst_rtp_vorbis_pay_payload_buffer (rtpvorbispay, 1,
866
          NULL, rtpvorbispay->config_data, rtpvorbispay->config_size,
867 868 869 870 871 872 873 874
          timestamp, GST_CLOCK_TIME_NONE, rtpvorbispay->config_extra_len);

      if (timestamp != -1) {
        rtpvorbispay->last_config = timestamp;
      }
    }
  }

875 876
  ret =
      gst_rtp_vorbis_pay_payload_buffer (rtpvorbispay, VDT, buffer, data, size,
877 878
      timestamp, duration, 0);

Wim Taymans's avatar
Wim Taymans committed
879
  gst_buffer_unmap (buffer, &map);
Wim Taymans's avatar
Wim Taymans committed
880 881
  gst_buffer_unref (buffer);

882
done:
Wim Taymans's avatar
Wim Taymans committed
883
  return ret;
884 885 886 887 888

  /* ERRORS */
wrong_size:
  {
    GST_ELEMENT_WARNING (rtpvorbispay, STREAM, DECODE,
889
        ("Invalid packet size (1 < %" G_GSIZE_FORMAT ")", size), (NULL));
Wim Taymans's avatar
Wim Taymans committed
890
    gst_buffer_unmap (buffer, &map);
Wim Taymans's avatar
Wim Taymans committed
891
    gst_buffer_unref (buffer);
892 893 894 895
    return GST_FLOW_OK;
  }
parse_id_failed:
  {
Wim Taymans's avatar
Wim Taymans committed
896
    gst_buffer_unmap (buffer, &map);
Wim Taymans's avatar
Wim Taymans committed
897
    gst_buffer_unref (buffer);
898 899 900 901 902
    return GST_FLOW_ERROR;
  }
unknown_header:
  {
    GST_ELEMENT_WARNING (rtpvorbispay, STREAM, DECODE,
903
        (NULL), ("Ignoring unknown header received"));
Wim Taymans's avatar
Wim Taymans committed
904
    gst_buffer_unmap (buffer, &map);
Wim Taymans's avatar
Wim Taymans committed
905
    gst_buffer_unref (buffer);
906 907 908 909 910 911
    return GST_FLOW_OK;
  }
header_error:
  {
    GST_ELEMENT_WARNING (rtpvorbispay, STREAM, DECODE,
        (NULL), ("Error initializing header config"));
Wim Taymans's avatar
Wim Taymans committed
912
    gst_buffer_unmap (buffer, &map);
Wim Taymans's avatar
Wim Taymans committed
913
    gst_buffer_unref (buffer);
914 915
    return GST_FLOW_OK;
  }
Wim Taymans's avatar
Wim Taymans committed
916 917
}

918
static gboolean
Wim Taymans's avatar
Wim Taymans committed
919
gst_rtp_vorbis_pay_sink_event (GstRTPBasePayload * payload, GstEvent * event)
920
{
Mark Nauwelaerts's avatar
Mark Nauwelaerts committed
921
  GstRtpVorbisPay *rtpvorbispay = GST_RTP_VORBIS_PAY (payload);
922 923 924 925 926 927 928 929 930

  switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_FLUSH_STOP:
      gst_rtp_vorbis_pay_clear_packet (rtpvorbispay);
      break;
    default:
      break;
  }
  /* false to let parent handle event as well */
Wim Taymans's avatar
Wim Taymans committed
931
  return GST_RTP_BASE_PAYLOAD_CLASS (parent_class)->sink_event (payload, event);
932 933
}

Wim Taymans's avatar
Wim Taymans committed
934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
static GstStateChangeReturn
gst_rtp_vorbis_pay_change_state (GstElement * element,
    GstStateChange transition)
{
  GstRtpVorbisPay *rtpvorbispay;
  GstStateChangeReturn ret;

  rtpvorbispay = GST_RTP_VORBIS_PAY (element);

  switch (transition) {
    default:
      break;
  }

  ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);

  switch (transition) {
    case GST_STATE_CHANGE_PAUSED_TO_READY:
      gst_rtp_vorbis_pay_cleanup (rtpvorbispay);
      break;
    default:
      break;
  }
  return ret;
}

960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
static void
gst_rtp_vorbis_pay_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstRtpVorbisPay *rtpvorbispay;

  rtpvorbispay = GST_RTP_VORBIS_PAY (object);

  switch (prop_id) {
    case PROP_CONFIG_INTERVAL:
      rtpvorbispay->config_interval = g_value_get_uint (value);
      break;
    default:
      break;
  }
}

static void
gst_rtp_vorbis_pay_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstRtpVorbisPay *rtpvorbispay;

  rtpvorbispay = GST_RTP_VORBIS_PAY (object);

  switch (prop_id) {
    case PROP_CONFIG_INTERVAL:
      g_value_set_uint (value, rtpvorbispay->config_interval);
      break;
    default:
      break;
  }
}

Wim Taymans's avatar
Wim Taymans committed
994 995 996 997
gboolean
gst_rtp_vorbis_pay_plugin_init (GstPlugin * plugin)
{
  return gst_element_register (plugin, "rtpvorbispay",
998
      GST_RANK_SECONDARY, GST_TYPE_RTP_VORBIS_PAY);
Wim Taymans's avatar
Wim Taymans committed
999
}