[PATCH] Add MPEG4 Low Overhead Audio Stream demuxer

Janne Grunau janne
Sun Aug 1 22:25:43 CEST 2010


LOAS is just a thin synchronization layer around LATM and is the actual
format used in MPEG-TS with stream_type=0x11.

The demuxer code contains already all code necessary for a real LATM
demuxer but it is pointless since it would require another format to
signal if the AudioMuxElement includes an audio configuration.

The demuxer should support multiple programs (streams) but is only
tested with single stream samples. It misses support for MPEG4 Audio
features without decoding support in libavcodec.
---
 Changelog                |    1 +
 libavformat/Makefile     |    1 +
 libavformat/allformats.c |    1 +
 libavformat/avformat.h   |    2 +-
 libavformat/latmdec.c    |  574 ++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 578 insertions(+), 1 deletions(-)
 create mode 100644 libavformat/latmdec.c

diff --git a/Changelog b/Changelog
index eded417..c44a25a 100644
--- a/Changelog
+++ b/Changelog
@@ -27,6 +27,7 @@ version <next>:
 - SubRip subtitle file muxer and demuxer
 - Chinese AVS encoding via libxavs
 - ffprobe -show_packets option added
+- LOAS/LATM demuxer added
 
 
 
diff --git a/libavformat/Makefile b/libavformat/Makefile
index f73bc54..e41d62f 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -101,6 +101,7 @@ OBJS-$(CONFIG_ISS_DEMUXER)               += iss.o
 OBJS-$(CONFIG_IV8_DEMUXER)               += iv8.o
 OBJS-$(CONFIG_IVF_DEMUXER)               += ivfdec.o riff.o
 OBJS-$(CONFIG_LMLM4_DEMUXER)             += lmlm4.o
+OBJS-$(CONFIG_LOAS_DEMUXER)              += latmdec.o
 OBJS-$(CONFIG_M4V_DEMUXER)               += raw.o
 OBJS-$(CONFIG_M4V_MUXER)                 += raw.o
 OBJS-$(CONFIG_MATROSKA_DEMUXER)          += matroskadec.o matroska.o \
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index 80475b9..2d28335 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -107,6 +107,7 @@ void av_register_all(void)
     REGISTER_DEMUXER  (IV8, iv8);
     REGISTER_DEMUXER  (IVF, ivf);
     REGISTER_DEMUXER  (LMLM4, lmlm4);
+    REGISTER_DEMUXER  (LOAS, loas);
     REGISTER_MUXDEMUX (M4V, m4v);
     REGISTER_MUXER    (MD5, md5);
     REGISTER_MUXDEMUX (MATROSKA, matroska);
diff --git a/libavformat/avformat.h b/libavformat/avformat.h
index 452aea6..d85d83c 100644
--- a/libavformat/avformat.h
+++ b/libavformat/avformat.h
@@ -22,7 +22,7 @@
 #define AVFORMAT_AVFORMAT_H
 
 #define LIBAVFORMAT_VERSION_MAJOR 52
-#define LIBAVFORMAT_VERSION_MINOR 78
+#define LIBAVFORMAT_VERSION_MINOR 79
 #define LIBAVFORMAT_VERSION_MICRO  0
 
 #define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
diff --git a/libavformat/latmdec.c b/libavformat/latmdec.c
new file mode 100644
index 0000000..57d762d
--- /dev/null
+++ b/libavformat/latmdec.c
@@ -0,0 +1,574 @@
+/*
+ * copyright (c) 2009 Paul Kendall <paul at kcbbs.gen.nz>
+ * copyright (c) 2010 Janne Grunau <janne-ffmpeg at jannau.net>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * LATM/LOAS demuxer
+ */
+
+#include "avformat.h"
+#include "libavcodec/avcodec.h"
+#include "libavcodec/get_bits.h"
+#include "libavcodec/put_bits.h"
+#include "libavcodec/mpeg4audio.h"
+
+
+#define LOAS_SYNC_WORD 0x2B7       /// LOAS sync word, 11 bits
+
+#define LATM_MAX_FRAME_SIZE  (8*1024)
+
+struct latm_stream_config {
+    uint32_t          frame_length;
+    uint32_t          mux_slot_length;
+    uint8_t           frame_length_type;
+    uint8_t           extra[64];
+    int               extrasize;
+    MPEG4AudioConfig  config;
+};
+
+struct latm_demux_context {
+    uint8_t                    audio_mux_version_A;
+    uint8_t                    same_time_framing;
+    uint8_t                    num_sub_frames;
+    uint8_t                    num_programs;
+    uint8_t                    num_layers[16];
+    struct latm_stream_config  streams[16];
+    int                        stream_cnt;
+    int                        cur_stream;
+    int                        has_config;
+    int                        read_data;
+    AVFormatContext           *s;
+    GetBitContext             *gb;
+    uint8_t                   *buf;
+    int                        pos;
+    int                        len;
+    uint64_t                   pkt_cnt;
+};
+
+
+static inline uint32_t latm_get_value(GetBitContext *gb)
+{
+    uint8_t num_bytes = get_bits(gb, 2);
+    return get_bits_long(gb, num_bytes * 8);
+}
+
+static inline void copy_bits(PutBitContext *pb, GetBitContext *gb, int bits)
+{
+    put_bits(pb, bits, get_bits(gb, bits));
+}
+
+static void read_ga_specific_config(int audio_object_type, GetBitContext *gb,
+                                    PutBitContext *pb)
+{
+    int depends_on_coder;
+    int ext_flag;
+
+    copy_bits(pb, gb, 1);                        // framelen_flag
+    depends_on_coder = get_bits(gb, 1);
+    put_bits(pb, 1, depends_on_coder);
+    if (depends_on_coder)
+        copy_bits(pb, gb, 14);                   // delay
+    ext_flag = get_bits(gb, 1);
+    put_bits(pb, 1, ext_flag);
+
+    if (audio_object_type == AOT_AAC_SCALABLE ||
+        audio_object_type == AOT_ER_AAC_SCALABLE)
+        copy_bits(pb, gb, 3);                    // layer number
+
+    if (!ext_flag)
+        return;
+
+    if (audio_object_type == AOT_ER_BSAC) {
+        copy_bits(pb, gb, 5);                    // numOfSubFrame
+        copy_bits(pb, gb, 11);                   // layer_length
+    } else if (audio_object_type == AOT_ER_AAC_LC ||
+               audio_object_type == AOT_ER_AAC_LTP ||
+               audio_object_type == AOT_ER_AAC_SCALABLE ||
+               audio_object_type == AOT_ER_AAC_LD)
+        copy_bits(pb, gb, 3);                    // stuff
+    copy_bits(pb, gb, 1);                        // extflag3
+}
+
+static int read_audio_specific_config(struct latm_stream_config *stream,
+                                      GetBitContext *gb)
+{
+    PutBitContext pb;
+    int ret = 0;
+    int audio_object_type;
+    int sampling_frequency_index;
+
+    MPEG4AudioConfig *c = &stream->config;
+
+    init_put_bits(&pb, stream->extra, sizeof(stream->extra));
+
+    c->sbr = -1;
+
+    audio_object_type = get_bits(gb, 5);
+    put_bits(&pb, 5, audio_object_type);
+    if (audio_object_type == AOT_ESCAPE) {
+        uint8_t extended = get_bits(gb, 6);
+        put_bits(&pb, 6, extended);
+        audio_object_type = AOT_ESCAPE + extended + 1;
+    }
+    c->object_type = audio_object_type;
+
+    sampling_frequency_index = get_bits(gb, 4);
+    put_bits(&pb, 4, sampling_frequency_index);
+    c->sampling_index = sampling_frequency_index;
+    c->sample_rate =
+            ff_mpeg4audio_sample_rates[sampling_frequency_index];
+    if (sampling_frequency_index == 0x0f) {
+        c->sample_rate = get_bits_long(gb, 24);
+        put_bits(&pb, 24, c->sample_rate);
+    }
+    c->chan_config = get_bits(gb, 4);
+    put_bits(&pb, 4, c->chan_config);
+
+    if (c->chan_config < FF_ARRAY_ELEMS(ff_mpeg4audio_channels))
+        c->channels = ff_mpeg4audio_channels[c->chan_config];
+
+    if (audio_object_type == AOT_AAC_MAIN ||
+        audio_object_type == AOT_AAC_LC ||
+        audio_object_type == AOT_AAC_SSR ||
+        audio_object_type == AOT_AAC_LTP ||
+        audio_object_type == AOT_AAC_SCALABLE ||
+        audio_object_type == AOT_TWINVQ) {
+        read_ga_specific_config(audio_object_type, gb, &pb);
+    } else if (audio_object_type == AOT_SBR) {
+        c->sbr = 1;
+        sampling_frequency_index = get_bits(gb, 4);
+        c->ext_sampling_index = sampling_frequency_index;
+        c->ext_sample_rate =
+                ff_mpeg4audio_sample_rates[sampling_frequency_index];
+        if (sampling_frequency_index == 0x0f) {
+            c->ext_sample_rate = get_bits_long(gb, 24);
+            put_bits(&pb, 24, c->ext_sample_rate);
+        }
+        c->object_type = get_bits(gb, 5);
+        put_bits(&pb, 5, c->object_type);
+    } else if (audio_object_type >= AOT_ER_AAC_LC) {
+        read_ga_specific_config(audio_object_type, gb, &pb);
+        copy_bits(&pb, gb, 2);                   // epConfig
+    }
+
+    if (c->sbr == -1 && c->sample_rate <= 24000)
+        c->sample_rate *= 2;
+
+    // count the extradata
+    ret = put_bits_count(&pb);
+    stream->extrasize = (ret + 7) / 8;
+
+    flush_put_bits(&pb);
+    return ret;
+}
+
+static void copy_audio_specific_config(struct latm_stream_config *dst,
+                                       struct latm_stream_config *src)
+{
+    dst->frame_length_type = src->frame_length_type;
+    dst->frame_length      = src->frame_length;
+    dst->mux_slot_length   = src->mux_slot_length;
+    dst->extrasize         = src->extrasize;
+
+    memcpy(dst->extra,   src->extra,   sizeof(*dst->extra));
+    memcpy(&dst->config, &src->config, sizeof(dst->config));
+}
+
+static void read_stream_mux_config(struct AVFormatContext *s, GetBitContext *gb)
+{
+    struct latm_demux_context *ctx = s->priv_data;
+    struct latm_stream_config *stream_cfg, *prev_stream_cfg = NULL;
+    AVStream *st = NULL;
+    uint8_t program, layer;
+    int use_same_config=0;
+
+    int audio_mux_version = get_bits(gb, 1);
+
+    ctx->audio_mux_version_A = 0;
+    if (audio_mux_version)
+        ctx->audio_mux_version_A = get_bits(gb, 1);
+
+    if (!ctx->audio_mux_version_A) {
+
+        if (audio_mux_version)
+            latm_get_value(gb);                 // taraFullness
+
+        // reset stream count
+        ctx->stream_cnt        = 0;
+
+        ctx->same_time_framing = get_bits(gb, 1) + 1;
+        ctx->num_sub_frames    = get_bits(gb, 6) + 1;
+        ctx->num_programs      = get_bits(gb, 4) + 1;
+
+        // for each program
+        for (program = 0; program < ctx->num_programs; program++) {
+            ctx->num_layers[program] = get_bits(gb, 3) + 1;
+
+            // for each layer
+            for (layer = 0; layer < ctx->num_layers[program]; layer++) {
+
+                if (layer)
+                    av_log_missing_feature(s, "multiple layers are not "
+                                           "supported", 1);
+
+                use_same_config = !ctx->stream_cnt ? 0 : get_bits(gb, 1);
+                stream_cfg = &ctx->streams[ctx->stream_cnt];
+                if (s->nb_streams <= ctx->stream_cnt) {
+                    st = av_new_stream(s, ctx->stream_cnt);
+                    av_log(s, AV_LOG_DEBUG, "added stream %d for program %d, "
+                           "layer %d\n", ctx->stream_cnt, program, layer);
+                } else
+                    st = s->streams[ctx->stream_cnt];
+
+                if (!use_same_config) {
+                    if (!audio_mux_version) {
+                        read_audio_specific_config(stream_cfg, gb);
+                    } else {
+                        uint32_t ascLen = latm_get_value(gb);
+                        ascLen -= read_audio_specific_config(stream_cfg, gb);
+                        skip_bits_long(gb, ascLen);
+                    }
+                } else {
+                    copy_audio_specific_config(stream_cfg, prev_stream_cfg);
+                }
+
+                st->codec->extradata = av_malloc(stream_cfg->extrasize +
+                                                 FF_INPUT_BUFFER_PADDING_SIZE);
+                if (st->codec->extradata) {
+                    st->codec->extradata_size = stream_cfg->extrasize;
+                    memcpy(st->codec->extradata, stream_cfg->extra,
+                           stream_cfg->extrasize);
+                }
+
+                av_set_pts_info(st, 33, 1, stream_cfg->config.sample_rate);
+                st->codec->codec_type = AVMEDIA_TYPE_AUDIO;
+                st->codec->codec_id   = CODEC_ID_AAC;
+                st->codec->sample_rate = stream_cfg->config.sample_rate;
+                st->codec->channels    = stream_cfg->config.channels;
+
+                stream_cfg->frame_length_type = get_bits(gb, 3);
+                switch (stream_cfg->frame_length_type) {
+                    case 0:
+                        skip_bits(gb, 8);       // latmBufferFullness
+                        if (!ctx->same_time_framing && layer > 0) {
+                            int cur_aot  = stream_cfg->config.object_type;
+                            int prev_aot = prev_stream_cfg->config.object_type;
+                            if ((cur_aot  == AOT_AAC_SCALABLE ||
+                                 cur_aot  == AOT_ER_AAC_SCALABLE) &&
+                                (prev_aot == AOT_CELP         ||
+                                 prev_aot == AOT_ER_CELP))
+                                skip_bits(gb, 6);// core_frame_offset
+                        }
+                        break;
+                    case 1:
+                        stream_cfg->frame_length = get_bits(gb, 9);
+                        break;
+                    case 3:
+                    case 4:
+                    case 5:
+                        skip_bits(gb, 6);       // CELP frame length table index
+                        break;
+                    case 6:
+                    case 7:
+                        skip_bits(gb, 1);       // HVXC frame length table index
+                        break;
+                }
+
+                ctx->stream_cnt++;
+                prev_stream_cfg = stream_cfg;
+            }
+        }
+
+        if (get_bits(gb, 1)) {                  // other data
+            if (audio_mux_version) {
+                latm_get_value(gb);             // other_data_bits
+            } else {
+                int esc;
+                do {
+                    esc = get_bits(gb, 1);
+                    skip_bits(gb, 8);
+                } while (esc);
+            }
+        }
+
+        if (get_bits(gb, 1))                     // crc present
+            skip_bits(gb, 8);                    // config_crc
+    }
+}
+
+static int read_payload_length_info(struct latm_demux_context *ctx)
+{
+    uint8_t stream, tmp;
+    if (ctx->same_time_framing) {
+        for (stream = 0; stream < ctx->stream_cnt; stream++) {
+            struct latm_stream_config *st = &ctx->streams[stream];
+            if (st->frame_length_type == 0) {
+                int mux_slot_length = 0;
+                do {
+                    tmp = get_bits(ctx->gb, 8);
+                    mux_slot_length += tmp;
+                } while (tmp == 255);
+                st->mux_slot_length = mux_slot_length;
+            } else if (st->frame_length_type == 3 ||
+                       st->frame_length_type == 5 ||
+                       st->frame_length_type == 7) {
+                skip_bits(ctx->gb, 2);          // mux_slot_length_coded
+            }
+        }
+    } else {
+        uint8_t num_chunk= get_bits(ctx->gb, 4);
+        av_log(ctx->s, AV_LOG_ERROR, "!allStreamsSameTimeFraming not handled "
+               "%d\n", num_chunk);
+    }
+    return 0;
+}
+
+static int latm_read_audio_mux_element_config(AVFormatContext *s,
+                                              int mux_config_present)
+{
+    struct latm_demux_context *ctx = s->priv_data;
+    uint8_t use_same_mux;
+
+    if (mux_config_present) {
+        use_same_mux = get_bits(ctx->gb, 1);
+        if (!use_same_mux) {
+            read_stream_mux_config(s, ctx->gb);
+            ctx->has_config = 1;
+        }
+    }
+
+    return ctx->has_config;
+}
+
+static int latm_read_audio_mux_element_data(AVFormatContext *s,
+                                            AVPacket *pkt)
+{
+    struct latm_demux_context *ctx = s->priv_data;
+    struct latm_stream_config *cfg = &ctx->streams[ctx->cur_stream];
+    int j;
+
+    if (!ctx->audio_mux_version_A) {
+
+        pkt->pts  = pkt->dts = 2048 * ctx->pkt_cnt;
+        pkt->data = av_malloc(cfg->mux_slot_length);
+        pkt->size = cfg->mux_slot_length;
+        pkt->stream_index = ctx->cur_stream++;
+        //TODO: try to use byte copying
+        for (j = 0; j < cfg->mux_slot_length; j++)
+            pkt->data[j] = get_bits(ctx->gb, 8);
+        //av_hex_dump_log(s, AV_LOG_INFO, pkt->data, pkt->size);
+    } else {
+        av_log(s, AV_LOG_ERROR, "invalid latm syntax\n");
+        return -1;
+    }
+
+    return 0;
+}
+
+/** LOAS */
+static int loas_read_audio_sync_stream(AVFormatContext *s,
+                                       struct latm_demux_context *latmctx)
+{
+    ByteIOContext *pb = s->pb;
+    int muxlength;
+    uint8_t *buf = latmctx->buf + latmctx->pos;
+    int size     = latmctx->len - latmctx->pos;
+
+    while (size >= 3) {
+
+        if ((buf[0] << 3 | buf[1] >> 5) == LOAS_SYNC_WORD) {
+
+            muxlength = ((buf[1] & 0x1f) << 8) | buf[2];
+            if (muxlength+3 > size) {
+                memmove(latmctx->buf, buf, size);
+                buf          = latmctx->buf;
+                latmctx->pos = 0;
+                latmctx->len = size + get_buffer(pb, latmctx->buf+size,
+                                                 LATM_MAX_FRAME_SIZE-size);
+            } else {
+                //av_log(s, AV_LOG_INFO, "LOAS frame of %d bytes\n", muxlength);
+                return muxlength+3;
+            }
+        } else
+            latmctx->pos++;
+
+        size = latmctx->len - latmctx->pos;
+    }
+
+    return -1;
+}
+
+/**
+ * Read packets until a config or data is found
+ *
+ * @param pkt output packet, NULL for read_header
+ * @param nb_packets number of LOAS frames probing for audio config
+ * @return 0 on success, < 0 on error.
+ */
+static int handle_packets(struct AVFormatContext *s, AVPacket *pkt,
+                          int nb_packets)
+{
+    struct latm_demux_context *latmctx = s->priv_data;
+
+    int has_config, ret, packet_num=0;
+
+    while (!latmctx->read_data) {
+
+        ret = loas_read_audio_sync_stream(s, latmctx);
+        if (ret < 0)
+            return -1;
+
+        init_get_bits(latmctx->gb, latmctx->buf+latmctx->pos+3, (ret-3)*8);
+        latmctx->pos += ret;
+
+        has_config = latm_read_audio_mux_element_config(s, 1);
+
+        if (nb_packets) {
+            if (has_config) {
+                latmctx->has_config = has_config;
+                return 0;
+            }
+            if (packet_num > nb_packets)
+                return -1;
+        }
+
+        if (latmctx->has_config)
+            latmctx->read_data = 1;
+    }
+
+    read_payload_length_info(latmctx);
+
+    ret = latm_read_audio_mux_element_data(s, pkt);
+    if (latmctx->cur_stream >= latmctx->stream_cnt || ret < 0) {
+        latmctx->read_data  = 0;
+        latmctx->cur_stream = 0;
+        latmctx->pkt_cnt++;
+    }
+
+    return ret;
+}
+
+
+static int loas_probe(AVProbeData *pd)
+{
+    int i=0, len=0, score=0;
+    uint8_t *buf = pd->buf;
+
+    while (i+3 < pd->buf_size && score < AVPROBE_SCORE_MAX) {
+        if ((buf[i] << 3 | (buf[i+1] & 0xe0) >> 5) == LOAS_SYNC_WORD) {
+            if (len || !i) {
+                score += AVPROBE_SCORE_MAX/4;
+            } else
+                score++;
+            len = (buf[i+1] & 0x1f) << 8 | buf[i+2];
+            dprintf(NULL, "found LOAS_SYNC_WORD at 0x%x. "
+                    "len = %d, score = %d\n", i, len, score);
+            i += len+3;
+        } else {
+            if (len && score > 0)
+                score -= AVPROBE_SCORE_MAX/4;
+            len = 0;
+            i++;
+        }
+    }
+
+    return FFMAX(0, FFMIN(score, AVPROBE_SCORE_MAX));
+}
+
+
+static int loas_read_header(AVFormatContext *s,
+                            AVFormatParameters *ap)
+{
+    ByteIOContext *pb = s->pb;
+    struct latm_demux_context *latmctx = s->priv_data;
+
+    latmctx->s = s;
+
+    s->ctx_flags |= AVFMTCTX_NOHEADER;
+
+    if (!latmctx->buf) {
+        latmctx->pos = 0;
+        latmctx->buf = av_malloc(LATM_MAX_FRAME_SIZE);
+        latmctx->len = get_buffer(pb, latmctx->buf, LATM_MAX_FRAME_SIZE);
+    }
+
+    if (!latmctx->gb)
+        latmctx->gb = av_malloc(sizeof(*latmctx->gb));
+
+    handle_packets(s, NULL, 100);
+
+    url_fseek(pb, 0, SEEK_SET);
+    latmctx->len = get_buffer(pb, latmctx->buf, LATM_MAX_FRAME_SIZE);
+    latmctx->pos = 0;
+
+    return latmctx->has_config ? 0 : -1;
+}
+
+
+static int loas_read_packet(struct AVFormatContext *s, AVPacket *pkt)
+{
+    struct latm_demux_context *latmctx = s->priv_data;
+    ByteIOContext *pb = s->pb;
+
+    latmctx->s = s;
+
+    if (!latmctx->buf) {
+        latmctx->buf = av_malloc(LATM_MAX_FRAME_SIZE);
+        latmctx->pos = 0;
+        latmctx->len = get_buffer(pb, latmctx->buf, LATM_MAX_FRAME_SIZE);
+    }
+
+    if (!latmctx->gb)
+        latmctx->gb = av_malloc(sizeof(*latmctx->gb));
+
+    return handle_packets(s, pkt, 0);
+}
+
+static int latm_close(struct AVFormatContext *avctx)
+{
+    struct latm_demux_context *latmctx = avctx->priv_data;
+
+    av_freep(&latmctx->buf);
+    av_freep(&latmctx->gb);
+    return 0;
+}
+
+/*
+AVInputFormat latm_demuxer = {
+    "latm",
+    NULL_IF_CONFIG_SMALL("Low-overhead MPEG-4 Audio Transport Multiplex"),
+    sizeof(struct latm_demux_context),
+    latm_probe,
+    latm_read_header,
+    latm_read_packet,
+    latm_close,
+};
+*/
+
+AVInputFormat loas_demuxer = {
+    "loas",
+    NULL_IF_CONFIG_SMALL("MPEG4 Low-overhead Audio Stream"),
+    sizeof(struct latm_demux_context),
+    loas_probe,
+    loas_read_header,
+    loas_read_packet,
+    latm_close,
+};
-- 
1.7.2


--vtzGhvizbBRQ85DL--



More information about the ffmpeg-devel mailing list