[xiph-cvs] cvs commit: theora/lib encode.c encoder_internal.h toplevel.c
Monty
xiphmont at xiph.org
Mon Sep 23 16:18:10 PDT 2002
xiphmont 02/09/23 19:18:09
Modified: examples encoder_example.c
include/theora theora.h
lib encode.c encoder_internal.h toplevel.c
Log:
Encoder works.
Monty
Revision Changes Path
1.2 +264 -27 theora/examples/encoder_example.c
Index: encoder_example.c
===================================================================
RCS file: /usr/local/cvsroot/theora/examples/encoder_example.c,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -r1.1 -r1.2
--- encoder_example.c 23 Sep 2002 09:15:03 -0000 1.1
+++ encoder_example.c 23 Sep 2002 23:18:05 -0000 1.2
@@ -12,7 +12,7 @@
function: example encoder application; makes an Ogg Theora/Vorbis
file from YUV4MPEG2 and WAV input
- last mod: $Id: encoder_example.c,v 1.1 2002/09/23 09:15:03 xiphmont Exp $
+ last mod: $Id: encoder_example.c,v 1.2 2002/09/23 23:18:05 xiphmont Exp $
********************************************************************/
@@ -43,7 +43,7 @@
int audio_ch=0;
int audio_hz=0;
-float audio_q=-99;
+float audio_q=1;
int audio_r=-1;
int video_x=0;
@@ -56,7 +56,7 @@
int video_ad=0;
int video_r=-1;
-int video_q=-1;
+int video_q=10;
static void usage(void){
fprintf(stderr,
@@ -157,6 +157,9 @@
ret=fread(buffer,1,4,test);
if(ret<4)goto riff_err;
+ fprintf(stderr,"File %s is 16 bit %d channel %d Hz RIFF WAV audio.\n",
+ f,audio_ch,audio_hz);
+
return;
}
}
@@ -211,6 +214,10 @@
}
video=test;
+
+ fprintf(stderr,"File %s is %dx%d %.02f fps YUV12 video.\n",
+ f,video_x,video_y,(double)video_hzn/video_hzd);
+
return;
}
}
@@ -223,11 +230,185 @@
yuv_err:
fprintf(stderr,"EOF parsing YUV4MPEG2 file %s.\n",f);
exit(1);
+
+}
+
+int spinner=0;
+char *spinascii="|/-\\";
+void spinnit(void){
+ spinner++;
+ if(spinner==4)spinner=0;
+ fprintf(stderr,"\r%c",spinascii[spinner]);
+}
+int fetch_and_process_audio(FILE *audio,ogg_page *audiopage,
+ ogg_stream_state *vo,
+ vorbis_dsp_state *vd,
+ vorbis_block *vb,
+ int audioflag){
+ ogg_packet op;
+ int i,j;
+
+ while(audio && !audioflag){
+ /* process any audio already buffered */
+ spinnit();
+ if(ogg_stream_pageout(vo,audiopage)>0) return 1;
+ if(ogg_stream_eos(vo))return 0;
+
+ {
+ /* read and process more audio */
+ signed char readbuffer[4096];
+ int toread=4096/2/audio_ch;
+ int bytesread=fread(readbuffer,1,toread*2*audio_ch,audio);
+ int sampread=bytesread/2/audio_ch;
+ float **vorbis_buffer;
+ int count=0;
+
+ if(bytesread<=0){
+ /* end of file. this can be done implicitly, but it's
+ easier to see here in non-clever fashion. Tell the
+ library we're at end of stream so that it can handle the
+ last frame and mark end of stream in the output properly */
+ vorbis_analysis_wrote(vd,0);
+ }else{
+ vorbis_buffer=vorbis_analysis_buffer(vd,sampread);
+ /* uninterleave samples */
+ for(i=0;i<sampread;i++){
+ for(j=0;j<audio_ch;j++){
+ vorbis_buffer[j][i]=((readbuffer[count+1]<<8)|
+ (0x00ff&(int)readbuffer[count]))/32768.f;
+ count+=2;
+ }
+ }
+
+ vorbis_analysis_wrote(vd,sampread);
+
+ }
+
+ while(vorbis_analysis_blockout(vd,vb)==1){
+
+ /* analysis, assume we want to use bitrate management */
+ vorbis_analysis(vb,NULL);
+ vorbis_bitrate_addblock(vb);
+
+ /* weld packets into the bitstream */
+ while(vorbis_bitrate_flushpacket(vd,&op))
+ ogg_stream_packetin(vo,&op);
+
+ }
+ }
+ }
+
+ return audioflag;
+}
+
+int fetch_and_process_video(FILE *video,ogg_page *videopage,
+ ogg_stream_state *to,
+ theora_state *td,
+ int videoflag){
+ /* You'll go to Hell for using static variables */
+ static int state=-1;
+ static signed char *yuvframe[2];
+ yuv_buffer yuv;
+ ogg_packet op;
+ int i;
+
+ if(state==-1){
+ /* initialize the double frame buffer */
+ yuvframe[0]=malloc(video_x*video_y*3/2);
+ yuvframe[1]=malloc(video_x*video_y*3/2);
+
+ state=0;
+ }
+
+ /* is there a video page flushed? If not, work until there is. */
+ while(!videoflag){
+ spinnit();
+
+ if(ogg_stream_pageout(to,videopage)>0) return 1;
+ if(ogg_stream_eos(to)) return 0;
+
+ {
+ /* read and process more video */
+ /* video strategy reads one frame ahead so we know when we're
+ at end of stream and can mark last video frame as such
+ (vorbis audio has to flush one frame past last video frame
+ due to overlap and thus doesn't need this extra work */
+
+ /* have two frame buffers full (if possible) before
+ proceeding. after first pass and until eos, one will
+ always be full when we get here */
+
+ for(i=state;i<2;i++){
+ char frame[6];
+ int ret=fread(frame,1,6,video);
+
+ if(ret<6)break;
+ if(memcmp(frame,"FRAME\n",6)){
+ fprintf(stderr,"Loss of framing in YUV input data\n");
+ exit(1);
+ }
+
+ ret=fread(yuvframe[i],1,video_x*video_y*3/2,video);
+ if(ret!=video_x*video_y*3/2)break;
+
+ state++;
+ }
+
+ if(state<1){
+ /* can't get here unless YUV4MPEG stream has no video */
+ fprintf(stderr,"Video input contains no frames.\n");
+ exit(1);
+ }
+
+ /* Theora is a one-frame-in,one-frame-out system; subit a frame
+ for compression and pull out the packet */
+
+ /* center crop the input to a /16 size */
+ {
+ int x_adj= (video_x-video_x_adj)/2;
+ int y_adj= (video_y-video_y_adj)/2;
+
+ yuv.y_width=video_x_adj;
+ yuv.y_height=video_y_adj;
+ yuv.y_stride=video_x;
+
+ yuv.uv_width=video_x_adj/2;
+ yuv.uv_height=video_y_adj/2;
+ yuv.uv_stride=video_x/2;
+
+ yuv.y= yuvframe[0]+
+ video_x*y_adj + x_adj;
+ yuv.u= yuvframe[0]+ video_x*video_y +
+ (video_x/2)*(y_adj/2) + x_adj/2;
+ yuv.v= yuvframe[0]+ video_x*video_y*5/4 +
+ (video_x/2)*(y_adj/2) + x_adj/2;
+ }
+
+ theora_encode_YUVin(td,&yuv);
+
+ /* if there's only one frame, it's the last in the stream */
+ if(state<2)
+ theora_encode_packetout(td,1,&op);
+ else
+ theora_encode_packetout(td,0,&op);
+
+ ogg_stream_packetin(to,&op);
+
+ {
+ signed char *temp=yuvframe[0];
+ yuvframe[0]=yuvframe[1];
+ yuvframe[1]=temp;
+ state--;
+ }
+
+ }
+ }
+ return videoflag;
}
int main(int argc,char *argv[]){
- int c,long_option_index,ret;
+ int c,long_option_index,ret,i,j;
ogg_stream_state to; /* take physical pages, weld into a logical
stream of packets */
@@ -246,9 +427,15 @@
vorbis_dsp_state vd; /* central working state for the packet->PCM decoder */
vorbis_block vb; /* local working space for packet->PCM decode */
- yuv_buffer yuv;
-
-
+ int audioflag=0;
+ int videoflag=0;
+ int akbps=0;
+ int vkbps=0;
+
+ ogg_int64_t audio_bytesout=0;
+ ogg_int64_t video_bytesout=0;
+ double timebase;
+
#ifdef _WIN32 /* We need to set stdin/stdout to binary mode. Damn windows. */
/* if we were reading/writing a file, it would also need to in
binary mode, eg, fopen("file.wav","wb"); */
@@ -261,7 +448,7 @@
while((c=getopt_long(argc,argv,optstring,options,&long_option_index))!=EOF){
switch(c){
case 'a':
- audio_q=atof(optarg)*.1;
+ audio_q=atof(optarg)*.099;
if(audio_q<-.1 || audio_q>1){
fprintf(stderr,"Illegal audio quality (choose -1 through 10)\n");
exit(1);
@@ -309,7 +496,7 @@
/* yayness. Set up Ogg output stream */
srand(time(NULL));
ogg_stream_init(&vo,rand());
- ogg_stream_init(&to,rand());
+ ogg_stream_init(&to,rand()); /* oops, add one ot the above */
/* Set up Theora encoder */
if(!video){
@@ -324,14 +511,16 @@
ti.height=video_y_adj;
ti.fps_numerator=video_hzn;
ti.fps_denominator=video_hzd;
+ ti.aspect_numerator=video_an;
+ ti.aspect_denominator=video_ad;
ti.target_bitrate=video_r;
- ti.quality=video_r;
+ ti.quality=video_q;
- ti.droppedframes_p=0;
- ti.quickcompress_p=1;
+ ti.dropframes_p=0;
+ ti.quick_p=1;
ti.keyframe_auto_p=1;
- ti.keyframe_frequency=128;
- ti.keyframe_frequency_force=128;
+ ti.keyframe_frequency=64;
+ ti.keyframe_frequency_force=64;
ti.keyframe_data_target_bitrate=video_r*1.5;
ti.keyframe_auto_threshold=80;
ti.keyframe_mindistance=8;
@@ -343,9 +532,9 @@
if(audio){
vorbis_info_init(&vi);
if(audio_q>-99)
- ret = vorbis_encode_init_vbr(&vi,2,44100,.4);
+ ret = vorbis_encode_init_vbr(&vi,audio_ch,audio_hz,audio_q);
else
- ret = vorbis_encode_init(&vi,2,44100,-1,128000,-1);
+ ret = vorbis_encode_init(&vi,audio_ch,audio_hz,-1,audio_r,-1);
if(ret){
fprintf(stderr,"The Vorbis encoder could not set up a mode according to\n"
"the requested quality or bitrate.\n\n");
@@ -396,27 +585,75 @@
}
}
- /* setup complete. Raw processing loop! */
+ /* setup complete. Raw processing loop */
+ fprintf(stderr,"Compressing....\n");
while(1){
+ ogg_page audiopage;
+ ogg_page videopage;
- /* is there an audio page ready to flush? If not, work until there is. */
-
-
-
- /* is there a video page ready to flush? If not, work until there is. */
-
+ /* is there an audio page flushed? If not, fetch one if possible */
+ audioflag=fetch_and_process_audio(audio,&audiopage,&vo,&vd,&vb,audioflag);
+ /* is there a video page flushed? If not, fetch one if possible */
+ videoflag=fetch_and_process_video(video,&videopage,&to,&td,videoflag);
+
/* no pages of either? Must be end of stream. */
- if(ogg_stream_pageout(&to,&og)<=0 && ogg_stream_pageout(&vo,&og)<=0)break;
+ if(!audioflag && !videoflag)break;
/* which is earlier; the end of the audio page or the end of the
video page? Flush the earlier to stream */
+ {
+ int audio_or_video=-1;
+ double audiotime=
+ audioflag?vorbis_granule_time(&vd,ogg_page_granulepos(&audiopage)):-1;
+ double videotime=
+ videoflag?theora_granule_time(&td,ogg_page_granulepos(&videopage)):-1;
+
+ if(!audioflag){
+ audio_or_video=1;
+ } else if(!videoflag) {
+ audio_or_video=0;
+ } else {
+ if(audiotime<videotime)
+ audio_or_video=0;
+ else
+ audio_or_video=1;
+ }
+ if(audio_or_video==1){
+ /* flush a video page */
+ video_bytesout+=fwrite(videopage.header,1,videopage.header_len,stdout);
+ video_bytesout+=fwrite(videopage.body,1,videopage.body_len,stdout);
+ videoflag=0;
+ timebase=videotime;
+
+ }else{
+ /* flush an audio page */
+ audio_bytesout+=fwrite(audiopage.header,1,audiopage.header_len,stdout);
+ audio_bytesout+=fwrite(audiopage.body,1,audiopage.body_len,stdout);
+ audioflag=0;
+ timebase=audiotime;
+ }
+ {
+ int hundredths=timebase*100-(long)timebase*100;
+ int seconds=(long)timebase%60;
+ int minutes=((long)timebase/60)%60;
+ int hours=(long)timebase/3600;
+
+ if(audio_or_video)
+ vkbps=rint(video_bytesout*8./timebase*.001);
+ else
+ akbps=rint(audio_bytesout*8./timebase*.001);
+
+ fprintf(stderr,
+ "\r %d:%02d:%02d.%02d audio: %dkbps video: %dkbps ",
+ hours,minutes,seconds,hundredths,akbps,vkbps);
+ }
+ }
+
}
- fprintf(stderr,
- "\r \n"
- "done.\n\n");
+ fprintf(stderr,"\r \ndone.\n\n");
return(0);
<p><p>1.3 +6 -6 theora/include/theora/theora.h
Index: theora.h
===================================================================
RCS file: /usr/local/cvsroot/theora/include/theora/theora.h,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- theora.h 23 Sep 2002 09:15:03 -0000 1.2
+++ theora.h 23 Sep 2002 23:18:06 -0000 1.3
@@ -11,7 +11,7 @@
********************************************************************
function:
- last mod: $Id: theora.h,v 1.2 2002/09/23 09:15:03 xiphmont Exp $
+ last mod: $Id: theora.h,v 1.3 2002/09/23 23:18:06 xiphmont Exp $
********************************************************************/
@@ -21,7 +21,8 @@
#include <ogg/ogg.h>
typedef struct{
- void *internal;
+ void *internal_encode;
+ void *internal_decode;
} theora_state;
typedef struct {
@@ -48,6 +49,7 @@
ogg_uint32_t aspect_denominator;
int target_bitrate;
int quality;
+ int quick_p; /* quick encode/decode */
/* decode only */
unsigned char version_major;
@@ -56,7 +58,6 @@
/* encode only */
int dropframes_p;
- int quickcompress_p;
int keyframe_auto_p;
ogg_uint32_t keyframe_frequency;
ogg_uint32_t keyframe_frequency_force; /* also used for decode init to
@@ -84,13 +85,12 @@
extern int theora_encode_packetout( theora_state *t, int last_p,
ogg_packet *op);
extern int theora_encode_header(theora_state *t, ogg_packet *op);
-extern void theora_encode_clear(theora_state *t);
extern int theora_decode_header(theora_info *c, ogg_packet *op);
extern int theora_decode_init(theora_state *th, theora_info *c);
-extern void theora_decode_clear(theora_state *th);
extern int theora_decode_packetin(theora_state *th,ogg_packet *op);
extern int theora_decode_YUVout(theora_state *th,yuv_buffer *yuv);
-extern double theora_packet_time(theora_state *th,ogg_packet *op);
+extern double theora_granule_time(theora_state *th,ogg_int64_t granulepos);
+extern void theora_clear(theora_state *t);
<p><p>1.8 +3 -3 theora/lib/encode.c
Index: encode.c
===================================================================
RCS file: /usr/local/cvsroot/theora/lib/encode.c,v
retrieving revision 1.7
retrieving revision 1.8
diff -u -r1.7 -r1.8
--- encode.c 23 Sep 2002 08:31:02 -0000 1.7
+++ encode.c 23 Sep 2002 23:18:06 -0000 1.8
@@ -11,7 +11,7 @@
********************************************************************
function:
- last mod: $Id: encode.c,v 1.7 2002/09/23 08:31:02 xiphmont Exp $
+ last mod: $Id: encode.c,v 1.8 2002/09/23 23:18:06 xiphmont Exp $
********************************************************************/
@@ -1192,7 +1192,7 @@
cpi->InterTripOutThresh = (5000<<12);
cpi->MVChangeFactor = MVChangeFactorTable[QIndex]; /* 0.9 */
- if ( cpi->QuickCompress ) {
+ if ( cpi->pb.info.quick_p ) {
cpi->ExhaustiveSearchThresh = (1000<<12);
cpi->FourMVThreshold = (2500<<12);
} else {
@@ -1300,7 +1300,7 @@
if ( BestError > cpi->MinImprovementForNewMV ) {
/* Use a mix of heirachical and exhaustive searches for
quick mode. */
- if ( cpi->QuickCompress ) {
+ if ( cpi->pb.info.quick_p ) {
MBInterMVError = GetMBMVInterError( cpi, cpi->pb.LastFrameRecon,
YFragIndex, PixelsPerLine,
cpi->MVPixelOffsetY,
<p><p>1.9 +1 -2 theora/lib/encoder_internal.h
Index: encoder_internal.h
===================================================================
RCS file: /usr/local/cvsroot/theora/lib/encoder_internal.h,v
retrieving revision 1.8
retrieving revision 1.9
diff -u -r1.8 -r1.9
--- encoder_internal.h 23 Sep 2002 09:15:04 -0000 1.8
+++ encoder_internal.h 23 Sep 2002 23:18:07 -0000 1.9
@@ -11,7 +11,7 @@
********************************************************************
function:
- last mod: $Id: encoder_internal.h,v 1.8 2002/09/23 09:15:04 xiphmont Exp $
+ last mod: $Id: encoder_internal.h,v 1.9 2002/09/23 23:18:07 xiphmont Exp $
********************************************************************/
@@ -457,7 +457,6 @@
/* Compressor Configuration */
SCAN_CONFIG_DATA ScanConfig;
CONFIG_TYPE2 Configuration;
- int QuickCompress;
int GoldenFrameEnabled;
int InterPrediction;
int MotionCompensation;
<p><p>1.9 +32 -32 theora/lib/toplevel.c
Index: toplevel.c
===================================================================
RCS file: /usr/local/cvsroot/theora/lib/toplevel.c,v
retrieving revision 1.8
retrieving revision 1.9
diff -u -r1.8 -r1.9
--- toplevel.c 23 Sep 2002 09:15:04 -0000 1.8
+++ toplevel.c 23 Sep 2002 23:18:07 -0000 1.9
@@ -11,7 +11,7 @@
********************************************************************
function:
- last mod: $Id: toplevel.c,v 1.8 2002/09/23 09:15:04 xiphmont Exp $
+ last mod: $Id: toplevel.c,v 1.9 2002/09/23 23:18:07 xiphmont Exp $
********************************************************************/
@@ -785,7 +785,7 @@
CP_INSTANCE *cpi;
memset(th, 0, sizeof(*th));
- th->internal=cpi=_ogg_calloc(1,sizeof(*cpi));
+ th->internal_encode=cpi=_ogg_calloc(1,sizeof(*cpi));
c->version_major=VERSION_MAJOR;
c->version_minor=VERSION_MINOR;
@@ -918,7 +918,7 @@
ogg_int32_t i;
unsigned char *LocalDataPtr;
unsigned char *InputDataPtr;
- CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal);
+ CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal_encode);
if(!cpi->readyflag)return OC_EINVAL;
if(cpi->doneflag)return OC_EINVAL;
@@ -980,7 +980,7 @@
}
int theora_encode_packetout( theora_state *t, int last_p, ogg_packet *op){
- CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal);
+ CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal_encode);
long bytes=oggpackB_bytes(&cpi->oggbuffer);
if(!bytes)return(0);
@@ -995,7 +995,7 @@
op->packetno=cpi->CurrentFrame;
op->granulepos=
- ((cpi->CurrentFrame-cpi->LastKeyFrame+1)<<cpi->pb.keyframe_granule_shift)+
+ ((cpi->CurrentFrame-cpi->LastKeyFrame)<<cpi->pb.keyframe_granule_shift)+
cpi->LastKeyFrame-1;
cpi->packetflag=0;
@@ -1005,7 +1005,7 @@
}
int theora_encode_header(theora_state *t, ogg_packet *op){
- CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal);
+ CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal_encode);
oggpackB_reset(&cpi->oggbuffer);
oggpackB_write(&cpi->oggbuffer,0x80,8);
@@ -1046,9 +1046,10 @@
return(0);
}
-void theora_encode_clear(theora_state *t){
+void theora_clear(theora_state *t){
if(t){
- CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal);
+ CP_INSTANCE *cpi=(CP_INSTANCE *)(t->internal_encode);
+ PB_INSTANCE *pbi=(PB_INSTANCE *)(t->internal_decode);
if(cpi){
@@ -1061,7 +1062,17 @@
ClearPPInstance(&cpi->pp);
}
- t->internal=NULL;
+
+ if(pbi){
+
+ ClearFragmentInfo(pbi);
+ ClearFrameInfo(pbi);
+ ClearPBInstance(pbi);
+
+ }
+
+ t->internal_encode=NULL;
+ t->internal_decode=NULL;
}
}
@@ -1112,7 +1123,7 @@
int theora_decode_init(theora_state *th, theora_info *c){
PB_INSTANCE *pbi;
- th->internal=pbi=_ogg_calloc(1,sizeof(*pbi));
+ th->internal_decode=pbi=_ogg_calloc(1,sizeof(*pbi));
InitPBInstance(pbi);
memcpy(&pbi->info,c,sizeof(*c));
@@ -1138,23 +1149,9 @@
}
-void theora_decode_clear(theora_state *th){
-
- if(th){
- if(th->internal){
- PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal);
-
- ClearFragmentInfo(pbi);
- ClearFrameInfo(pbi);
- ClearPBInstance(pbi);
-
- }
- }
-}
-
int theora_decode_packetin(theora_state *th,ogg_packet *op){
int ret;
- PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal);
+ PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal_decode);
pbi->DecoderErrorCode = 0;
oggpackB_readinit(&pbi->opb,op->packet,op->bytes);
@@ -1174,7 +1171,7 @@
}
int theora_decode_YUVout(theora_state *th,yuv_buffer *yuv){
- PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal);
+ PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal_decode);
yuv->y_width = pbi->info.width;
yuv->y_height = pbi->info.height;
@@ -1199,12 +1196,15 @@
/* returns, in seconds, absolute time of current packet in given
logical stream */
-double theora_packet_time(theora_state *th,ogg_packet *op){
- PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal);
-
- if(op->granulepos>=0){
- ogg_int64_t iframe=op->granulepos>>pbi->keyframe_granule_shift;
- ogg_int64_t pframe=op->granulepos-(iframe<<pbi->keyframe_granule_shift);
+double theora_granule_time(theora_state *th,ogg_int64_t granulepos){
+ CP_INSTANCE *cpi=(CP_INSTANCE *)(th->internal_encode);
+ PB_INSTANCE *pbi=(PB_INSTANCE *)(th->internal_decode);
+
+ if(cpi)pbi=&cpi->pb;
+
+ if(granulepos>=0){
+ ogg_int64_t iframe=granulepos>>pbi->keyframe_granule_shift;
+ ogg_int64_t pframe=granulepos-(iframe<<pbi->keyframe_granule_shift);
return (iframe+pframe)*
((double)pbi->info.fps_denominator/pbi->info.fps_numerator);
<p><p>--- >8 ----
List archives: http://www.xiph.org/archives/
Ogg project homepage: http://www.xiph.org/ogg/
To unsubscribe from this list, send a message to 'cvs-request at xiph.org'
containing only the word 'unsubscribe' in the body. No subject is needed.
Unsubscribe messages sent to the list will be ignored/filtered.
More information about the commits
mailing list