Patch - MP3 Frame validation
Paul Kelly <[email protected]>
| Newsgroups | gmane.comp.audio.icecast.devel |
|---|---|
| Message-ID | <[email protected]> |
(I'm aware that this patch is not likely to be accepted in the upstream source because Icecast doesn't really support MP3, but I thought it worthwhile to send it here in case anyone wishes to modify their local source with it.) Background: Icecast does not make any attempt to demarcate the boundaries between MP3 frames, and when a listening client connects to the server it generally is sent an initial partial frame that can't be decoded. This is not a problem for almost all client players. It becomes a problem however when a "pre-roll" intro clip is used. When Icecast connects the listener to the main stream after playing the intro clip, it will very likely cut in in the middle of a frame, which causes a problem for some players. Flash player in particular exhibits strange behaviour with the audio cutting in and out every few seconds. Pausing the player and resuming cures the problem. A likely explanation for this is as follows (a best guess; may contain some slight inaccuracies and/or generalisations): Adobe doesn't provide a facility in Flash Player to play a continuous audio stream via HTTP (e.g. from Icecast). Flash-based MP3 players typically get around this by downloading the stream in lots of separate overlapping chunks and playing these as if they were individual files. When one chunk is getting near the end, it starts playing the next chunk and then cross-fades into it. In effect there are two mini-MP3 players operating with constant cross-fading between them. It looks like what is happening is that the player that was playing when the junk data occured at the crossover has got stuck, but that the other one is still functioning normally. Therefore silence is heard when the "stuck" player is supposed to be doing its bit, but normal audio when it's the other one's turn. The attached patch address the issue by modifying Icecast so that it parses the MP3 data and makes sure it only starts streaming to a client at a frame boundary. It is in production use on a number of Icecast servers and is working well. Note that the "channels" member of the mpeg_frame_t struct (and the 4 lines of code in the validate_header() function that calculate its value) are not strictly necessary for the frame validation, but are included for completeness. I hope it is useful for someone. Best regards _______________________________________________ Icecast-dev mailing list [email protected] http://lists.xiph.org/mailman/listinfo/icecast-dev
icecast-mp3-validation.diff
(text/x-diff, 7 KB)
--- icecast-2.3.2/src/format_mp3.c~ 2007-10-19 03:02:35.000000000 +0000
+++ icecast-2.3.2/src/format_mp3.c 2011-09-01 15:14:04.076836673 +0000
@@ -483,6 +483,162 @@
return 1;
}
+static int bitrate_table[2][3][14] =
+{
+ {
+ /* MPEG-2 Layer III */
+ { 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160},
+ /* MPEG-2 Layer II */
+ { 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160},
+ /* MPEG-2 Layer I */
+ {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}
+ },
+ {
+ /* MPEG-1 Layer III */
+ {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320},
+ /* MPEG-1 Layer II */
+ {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384},
+ /* MPEG-1 Layer I */
+ {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}
+ }
+};
+
+static int samplerate_table[4][3] =
+{
+ {11025, 12000, 8000}, /* MPEG-2 LSF */
+ { 0, 0, 0}, /* Reserved */
+ {22050, 24000, 16000}, /* MPEG-2 */
+ {44100, 48000, 32000} /* MPEG-1 */
+};
+
+static int framesize_table[2][3] =
+{
+ /* L.III L.II L.I */
+ { 576, 1152, 384}, /* MPEG-2 */
+ { 1152, 1152, 384} /* MPEG-1 */
+};
+
+static int slotsize_table[3] =
+{
+ 1, /* L. III */
+ 1, /* L. II */
+ 4 /* L. I */
+};
+
+static int validate_header(mpeg_frame_t *fr)
+{
+ unsigned char version_code, layer_code, bitrate_code,
+ samplerate_code, padding;
+ char message[200];
+
+#define MAX_FRAME_LEN 2880 /* 160kbps Layer II @ 8kHz */
+
+ /* Check sync word is present */
+ if (fr->data[0] != 0xff || (fr->data[1] & 0xe0) != 0xe0)
+ goto invalid_frame;
+
+ /* Validate header by checking no reserved values are present */
+ if ( (version_code = (fr->data[1] & 0x18) >> 3) == 1
+ || (layer_code = (fr->data[1] & 0x06) >> 1) == 0
+ || (bitrate_code = (fr->data[2] & 0xf0) >> 4) == 15
+ || (samplerate_code = (fr->data[2] & 0x0c) >> 2) == 3)
+ goto invalid_frame;
+
+ if (bitrate_code == 0) /* Free-format bitrate */
+ /* We can't calculate the frame length anyway from this so can go no
+ * further with validation, so return the header as invalid. This is
+ * arguably a bug. */
+ goto invalid_frame;
+
+ /* Calculate data length of frame */
+ fr->kbps = bitrate_table[version_code & 1][layer_code - 1][bitrate_code - 1];
+ fr->sample_rate_Hz = samplerate_table[version_code][samplerate_code];
+ padding = (fr->data[2] & 0x02) >> 1;
+ fr->bytes = (framesize_table[version_code & 1][layer_code - 1] / 8
+ / slotsize_table[layer_code - 1] * fr->kbps * 1000
+ / fr->sample_rate_Hz + padding)
+ * slotsize_table[layer_code - 1];
+
+ if (fr->bytes <= 0 || fr->bytes > MAX_FRAME_LEN)
+ goto invalid_frame;
+
+ if ((fr->data[3] & 0xc0) >> 6 == 3) /* mono */
+ fr->channels = 1;
+ else
+ fr->channels = 2;
+
+ return fr->bytes;
+
+invalid_frame:
+ fr->bytes = -1;
+ return -1;
+}
+
+/* Parse the MP3 data (also handles MPEG audio layers I/II) to check if there
+ * is a partial frame at the end of the data. If so the partial data is stored
+ * in the MP3 state (where it will be appended to the next time complete_read()
+ * is called) and the modified refbuf containing only complete frames is
+ * returned.
+ * Note that incomplete frames occuring at the *start* of the data buffer are
+ * ignored. This is so that huge frames (greater than the REFBUF size) can still
+ * be handled correctly.
+ */
+static void remove_partial_frames(refbuf_t *refbuf, mp3_state *source_mp3)
+{
+ int frame_offset = 0, valid_frames = 0;
+
+ /* while there are enough bytes remaining for a valid header */
+ while (refbuf->len - frame_offset >= 4)
+ {
+ mpeg_frame_t fr;
+
+ /* check the header is valid and determine the total frame length */
+ fr.data = (unsigned char *)refbuf->data+frame_offset;
+ validate_header(&fr);
+
+ /* If any frames are bigger than the refbuf size, then we need to leave
+ * them intact and just put up with the fact they will be split up. Such
+ * huge frames should be very rare in practice. */
+ if (fr.bytes > REFBUF_SIZE)
+ return;
+
+ if (fr.bytes > 0) /* if header is valid */
+ {
+ if(frame_offset + fr.bytes > refbuf->len)
+ /* this frame extends beyond the buffer */
+ break;
+
+ valid_frames++;
+ /* Skip to start of next frame */
+ frame_offset += fr.bytes;
+ continue;
+ }
+
+ /* Validation failed: shift forward by one byte and keep searching for header */
+ frame_offset++;
+ }
+
+ if (frame_offset == refbuf->len || valid_frames == 0)
+ /* buffer contained only either wholly complete or wholly partial frames */
+ return;
+
+ /* Allocate a new refbuf to hold the partial last frame. When complete_read()
+ * is called again it will append to this. */
+ source_mp3->read_data = refbuf_new (REFBUF_SIZE);
+ source_mp3->read_count = refbuf->len - frame_offset;
+
+ /* Copy the partial frame into the new refbuf and reduce the byte count for
+ * the existing refbuf accordingly. */
+ memcpy (source_mp3->read_data->data, refbuf->data+frame_offset,
+ source_mp3->read_count);
+ refbuf->len = frame_offset;
+
+ /* Finally adjust the metadata interval offset to avoid "double-counting" of
+ * the bytes in the partial frame. */
+ source_mp3->offset -= source_mp3->read_count;
+
+ return;
+}
/* read an mp3 stream which does not have shoutcast style metadata */
static refbuf_t *mp3_get_no_meta (source_t *source)
@@ -503,7 +659,10 @@
}
refbuf->associated = source_mp3->metadata;
refbuf_addref (source_mp3->metadata);
+
+ remove_partial_frames(refbuf, source_mp3);
refbuf->sync_point = 1;
+
return refbuf;
}
@@ -623,6 +782,8 @@
}
refbuf->associated = source_mp3->metadata;
refbuf_addref (source_mp3->metadata);
+
+ remove_partial_frames(refbuf, source_mp3);
refbuf->sync_point = 1;
return refbuf;
--- icecast-2.3.2/src/format_mp3.h~ 2006-09-21 01:49:16.000000000 +0000
+++ icecast-2.3.2/src/format_mp3.h 2011-08-31 16:34:05.888838280 +0000
@@ -37,6 +37,16 @@
char build_metadata[4081];
} mp3_state;
+typedef struct
+{
+ unsigned char *data; /* Pointer to complete MPEG audio frame (including
+ * sync word and header */
+ int bytes; /* Length of MPEG frame in bytes */
+ int kbps; /* Bitrate in kilobits per second */
+ int sample_rate_Hz; /* Sample rate in hertz */
+ int channels; /* Number of channels (mono/stereo) */
+} mpeg_frame_t;
+
int format_mp3_get_plugin(struct source_tag *src);
#endif /* __FORMAT_MP3_H__ */