/**
* Copyright 2012 JogAmp Community. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those of the
* authors and should not be interpreted as representing official policies, either expressed
* or implied, of JogAmp Community.
*/
package com.jogamp.opengl.test.junit.jogl.demos.es2.av;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL2ES2;
import com.jogamp.opengl.GLAnimatorControl;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLCapabilities;
import com.jogamp.opengl.GLEventListener;
import com.jogamp.opengl.GLException;
import com.jogamp.opengl.GLProfile;
import com.jogamp.common.net.Uri;
import com.jogamp.common.util.InterruptSource;
import com.jogamp.graph.curve.Region;
import com.jogamp.graph.curve.opengl.GLRegion;
import com.jogamp.graph.curve.opengl.RegionRenderer;
import com.jogamp.graph.font.Font;
import com.jogamp.junit.util.JunitTracer;
import com.jogamp.newt.Window;
import com.jogamp.newt.event.KeyAdapter;
import com.jogamp.newt.event.KeyEvent;
import com.jogamp.newt.event.KeyListener;
import com.jogamp.newt.event.WindowAdapter;
import com.jogamp.newt.event.WindowEvent;
import com.jogamp.newt.opengl.GLWindow;
import com.jogamp.opengl.JoglVersion;
import com.jogamp.opengl.test.junit.graph.TextRendererGLELBase;
import com.jogamp.opengl.test.junit.jogl.demos.es2.TextureSequenceCubeES2;
import com.jogamp.opengl.test.junit.util.MiscUtils;
import com.jogamp.opengl.util.Animator;
import com.jogamp.opengl.util.av.GLMediaPlayer;
import com.jogamp.opengl.util.av.GLMediaPlayer.GLMediaEventListener;
import com.jogamp.opengl.util.av.GLMediaPlayer.StreamException;
import com.jogamp.opengl.util.av.GLMediaPlayerFactory;
import com.jogamp.opengl.util.texture.TextureSequence.TextureFrame;
/**
* Simple cube movie player w/ aspect ration true projection on a cube.
*/
public class MovieCube implements GLEventListener {
public static final float zoom_def = -2.77f;
private static boolean waitForKey = false;
private final float zoom0, rotx, roty;
private TextureSequenceCubeES2 cube=null;
private GLMediaPlayer mPlayer=null;
private int swapInterval = 1;
private boolean swapIntervalSet = true;
private long lastPerfPos = 0;
private volatile boolean resetGLState = false;
/** Blender's Big Buck Bunny: 24f 416p H.264, AAC 48000 Hz, 2 ch, mpeg stream. */
public static final Uri defURI;
static {
Uri _defURI = null;
try {
// Blender's Big Buck Bunny Trailer: 24f 640p VP8, Vorbis 44100Hz mono, WebM/Matroska Stream.
// _defURI = new URI("http://video.webmfiles.org/big-buck-bunny_trailer.webm");
_defURI = Uri.cast("http://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4");
} catch (final URISyntaxException e) {
e.printStackTrace();
}
defURI = _defURI;
}
/**
* Default constructor which also issues {@link #initStream(URI, int, int, int)} w/ default values
* and polls until the {@link GLMediaPlayer} is {@link GLMediaPlayer.State#Initialized}.
* If {@link GLMediaEventListener#EVENT_CHANGE_EOS} is reached, the stream is started over again.
* <p>
* This default constructor is merely useful for some <i>drop-in</i> test, e.g. using an applet.
* </p>
*/
public MovieCube() throws IOException, URISyntaxException {
this(zoom_def, 0f, 0f, true);
mPlayer.addEventListener(new GLMediaEventListener() {
@Override
public void newFrameAvailable(final GLMediaPlayer ts, final TextureFrame newFrame, final long when) { }
@Override
public void attributesChanged(final GLMediaPlayer mp, final int event_mask, final long when) {
System.err.println("MovieCube AttributesChanges: events_mask 0x"+Integer.toHexString(event_mask)+", when "+when);
System.err.println("MovieCube State: "+mp);
if( 0 != ( GLMediaEventListener.EVENT_CHANGE_SIZE & event_mask ) ) {
resetGLState();
}
if( 0 != ( GLMediaEventListener.EVENT_CHANGE_EOS & event_mask ) ) {
new InterruptSource.Thread() {
public void run() {
// loop for-ever ..
mPlayer.seek(0);
mPlayer.play();
} }.start();
}
}
});
initStream(defURI, GLMediaPlayer.STREAM_ID_AUTO, GLMediaPlayer.STREAM_ID_AUTO, GLMediaPlayer.TEXTURE_COUNT_DEFAULT);
StreamException se = null;
while( null == se && GLMediaPlayer.State.Initialized != mPlayer.getState() ) {
try {
Thread.sleep(16);
} catch (final InterruptedException e) { }
se = mPlayer.getStreamException();
}
if( null != se ) {
se.printStackTrace();
throw new RuntimeException(se);
}
}
/**
* Custom constructor, user needs to issue {@link #initStream(URI, int, int, int)} afterwards.
*/
public MovieCube(final float zoom0, final float rotx, final float roty, final boolean showText) throws IOException {
this.zoom0 = zoom0;
this.rotx = rotx;
this.roty = roty;
this.showText = showText;
mPlayer = GLMediaPlayerFactory.createDefault();
}
public void initStream(final Uri streamLoc, final int vid, final int aid, final int textureCount) {
mPlayer.initStream(streamLoc, vid, aid, textureCount);
System.out.println("pC.1b "+mPlayer);
}
public void setSwapInterval(final int v) { this.swapInterval = v; }
public GLMediaPlayer getGLMediaPlayer() { return mPlayer; }
public void resetGLState() {
resetGLState = true;
}
final int[] textSampleCount = { 4 };
private final class InfoTextRendererGLELBase extends TextRendererGLELBase {
private static final float z_diff = 0.001f;
private final Font font = getFont(0, 0, 0);
private final float fontSize1 = 12;
private final float fontSize2 = 10;
private final GLRegion regionFPS;
private float pixelSize1, pixelSize2, underlineSize;
InfoTextRendererGLELBase(final int rmode, final boolean lowPerfDevice) {
// FIXME: Graph TextRenderer does not AA well w/o MSAA and FBO
super(rmode, MovieCube.this.textSampleCount);
this.setRendererCallbacks(RegionRenderer.defaultBlendEnable, RegionRenderer.defaultBlendDisable);
if( lowPerfDevice ) {
regionFPS = null;
} else {
regionFPS = GLRegion.create(renderModes, null);
System.err.println("RegionFPS "+Region.getRenderModeString(renderModes)+", sampleCount "+textSampleCount[0]+", class "+regionFPS.getClass().getName());
}
staticRGBAColor[0] = 0.1f;
staticRGBAColor[1] = 0.1f;
staticRGBAColor[2] = 0.1f;
staticRGBAColor[3] = 1.0f;
}
@Override
public void init(final GLAutoDrawable drawable) {
// non-exclusive mode!
this.setSharedPMVMatrix(cube.pmvMatrix);
super.init(drawable);
pixelSize1 = font.getPixelSize(fontSize1, dpiH);
pixelSize2 = font.getPixelSize(fontSize2, dpiH);
pixelScale = 1.0f / ( pixelSize1 * 20f );
// underlineSize: 'underline' amount of pixel below 0/0 (Note: lineGap is negative)
final Font.Metrics metrics = font.getMetrics();
final float lineGap = metrics.getLineGap(pixelSize1);
final float descent = metrics.getDescent(pixelSize1);
underlineSize = descent - lineGap;
System.err.println("XXX: dpiH "+dpiH+", fontSize "+fontSize1+", pixelSize "+pixelSize1+", pixelScale "+pixelScale+", fLG "+lineGap+", fDesc "+descent+", underlineSize "+underlineSize);
}
@Override
public void dispose(final GLAutoDrawable drawable) {
if( null != regionFPS ) {
regionFPS.destroy(drawable.getGL().getGL2ES2());
}
super.dispose(drawable);
}
@Override
public void display(final GLAutoDrawable drawable) {
final GLAnimatorControl anim = drawable.getAnimator();
final float lfps = null != anim ? anim.getLastFPS() : 0f;
final float tfps = null != anim ? anim.getTotalFPS() : 0f;
final boolean hasVideo = GLMediaPlayer.STREAM_ID_NONE != mPlayer.getVID();
final float pts = ( hasVideo ? mPlayer.getVideoPTS() : mPlayer.getAudioPTS() ) / 1000f;
// Note: MODELVIEW is from [ -1 .. 1 ]
// dy: position right above video rectangle (bottom text line)
final float aspect = (float)mPlayer.getWidth() / (float)mPlayer.getHeight();
final float aspect_h = 1f/aspect;
final float dy = 1f-aspect_h;
// yoff1: position right above video rectangle (bottom text line)
// less than underlineSize, so 'underline' pixels are above video.
final float yoff1 = dy-(pixelScale*underlineSize);
// yoff2: position right below video rectangle (bottom text line)
final float yoff2 = 2f-dy;
/**
System.err.println("XXX: a "+aspect+", aspect_h "+aspect_h+", dy "+dy+
"; underlineSize "+underlineSize+" "+(pixelScale*underlineSize)+
"; yoff "+yoff1+", yoff2 "+yoff2); */
final GL gl = drawable.getGL();
final String ptsPrec = null != regionFPS ? "3.1" : "3.0";
final String text1 = String.format("%0"+ptsPrec+"f/%0"+ptsPrec+"f s, %s (%01.2fx, vol %01.2f), a %01.2f, fps %02.1f -> %02.1f / %02.1f, v-sync %d",
pts, mPlayer.getDuration() / 1000f,
mPlayer.getState().toString().toLowerCase(), mPlayer.getPlaySpeed(), mPlayer.getAudioVolume(),
aspect, mPlayer.getFramerate(), lfps, tfps, swapInterval);
final String text2 = String.format("audio: id %d, kbps %d, codec %s",
mPlayer.getAID(), mPlayer.getAudioBitrate()/1000, mPlayer.getAudioCodec());
final String text3 = String.format("video: id %d, kbps %d, codec %s",
mPlayer.getVID(), mPlayer.getVideoBitrate()/1000, mPlayer.getVideoCodec());
final String text4 = mPlayer.getUri().path.decode();
if( displayOSD && null != renderer ) {
gl.glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
if( null != regionFPS ) {
renderString(drawable, font, pixelSize1, text1, 1 /* col */, -1 /* row */, -1+z_diff, yoff1, 1f+z_diff, regionFPS); // no-cache
} else {
renderString(drawable, font, pixelSize1, text1, 1 /* col */, -1 /* row */, -1+z_diff, yoff1, 1f+z_diff, true);
}
renderString(drawable, font, pixelSize2, text2, 1 /* col */, 0 /* row */, -1+z_diff, yoff2, 1f+z_diff, true);
renderString(drawable, font, pixelSize2, text3, 1 /* col */, 1 /* row */, -1+z_diff, yoff2, 1f+z_diff, true);
renderString(drawable, font, pixelSize2, text4, 1 /* col */, 2 /* row */, -1+z_diff, yoff2, 1f+z_diff, true);
}
} };
private InfoTextRendererGLELBase textRendererGLEL = null;
final boolean showText;
private boolean displayOSD = true;
private final KeyListener keyAction = new KeyAdapter() {
public void keyReleased(final KeyEvent e) {
if( e.isAutoRepeat() ) {
return;
}
System.err.println("MC "+e);
final int pts0 = GLMediaPlayer.STREAM_ID_NONE != mPlayer.getVID() ? mPlayer.getVideoPTS() : mPlayer.getAudioPTS();
int pts1 = 0;
switch(e.getKeySymbol()) {
case KeyEvent.VK_V: {
switch(swapInterval) {
case 0: swapInterval = -1; break;
case -1: swapInterval = 1; break;
case 1: swapInterval = 0; break;
default: swapInterval = 1; break;
}
swapIntervalSet = true;
break;
}
case KeyEvent.VK_O: displayOSD = !displayOSD; break;
case KeyEvent.VK_RIGHT: pts1 = pts0 + 1000; break;
case KeyEvent.VK_UP: pts1 = pts0 + 10000; break;
case KeyEvent.VK_PAGE_UP: pts1 = pts0 + 30000; break;
case KeyEvent.VK_LEFT: pts1 = pts0 - 1000; break;
case KeyEvent.VK_DOWN: pts1 = pts0 - 10000; break;
case KeyEvent.VK_PAGE_DOWN: pts1 = pts0 - 30000; break;
case KeyEvent.VK_ESCAPE:
case KeyEvent.VK_HOME:
case KeyEvent.VK_BACK_SPACE: {
mPlayer.seek(0);
break;
}
case KeyEvent.VK_SPACE: {
if(GLMediaPlayer.State.Paused == mPlayer.getState()) {
mPlayer.play();
} else {
mPlayer.pause(false);
}
break;
}
case KeyEvent.VK_MULTIPLY:
mPlayer.setPlaySpeed(1.0f);
break;
case KeyEvent.VK_SUBTRACT: {
float playSpeed = mPlayer.getPlaySpeed();
if( e.isShiftDown() ) {
playSpeed /= 2.0f;
} else {
playSpeed -= 0.1f;
}
mPlayer.setPlaySpeed(playSpeed);
} break;
case KeyEvent.VK_ADD: {
float playSpeed = mPlayer.getPlaySpeed();
if( e.isShiftDown() ) {
playSpeed *= 2.0f;
} else {
playSpeed += 0.1f;
}
mPlayer.setPlaySpeed(playSpeed);
} break;
case KeyEvent.VK_M: {
float audioVolume = mPlayer.getAudioVolume();
if( audioVolume > 0.5f ) {
audioVolume = 0f;
} else {
audioVolume = 1f;
}
mPlayer.setAudioVolume(audioVolume);
} break;
}
if( 0 != pts1 ) {
mPlayer.seek(pts1);
}
}
};
@Override
public void init(final GLAutoDrawable drawable) {
if(null == mPlayer) {
throw new InternalError("mPlayer null");
}
if( GLMediaPlayer.State.Uninitialized == mPlayer.getState() ) {
throw new IllegalStateException("mPlayer in uninitialized state: "+mPlayer);
}
if( GLMediaPlayer.STREAM_ID_NONE == mPlayer.getVID() ) {
// throw new IllegalStateException("mPlayer has no VID/stream selected: "+mPlayer);
}
resetGLState = false;
final GL2ES2 gl = drawable.getGL().getGL2ES2();
System.err.println(JoglVersion.getGLInfo(gl, null));
cube = new TextureSequenceCubeES2(mPlayer, false, zoom0, rotx, roty);
if(waitForKey) {
JunitTracer.waitForKey("Init>");
}
if( GLMediaPlayer.State.Initialized == mPlayer.getState() ) {
try {
mPlayer.initGL(gl);
} catch (final Exception e) {
e.printStackTrace();
if(null != mPlayer) {
mPlayer.destroy(gl);
mPlayer = null;
}
throw new GLException(e);
}
}
cube.init(drawable);
mPlayer.play();
System.out.println("play.0 "+mPlayer);
boolean added;
final Object upstreamWidget = drawable.getUpstreamWidget();
if (upstreamWidget instanceof Window) {
final Window window = (Window) upstreamWidget;
window.addKeyListener(keyAction);
added = true;
} else { added = false; }
System.err.println("MC.init: kl-added "+added+", "+drawable.getClass().getName());
if( showText ) {
final int rmode = drawable.getChosenGLCapabilities().getSampleBuffers() ? 0 : Region.VBAA_RENDERING_BIT;
final boolean lowPerfDevice = gl.isGLES();
textRendererGLEL = new InfoTextRendererGLELBase(rmode, lowPerfDevice);
drawable.addGLEventListener(textRendererGLEL);
}
}
@Override
public void reshape(final GLAutoDrawable drawable, final int x, final int y, final int width, final int height) {
if(null == mPlayer) { return; }
cube.reshape(drawable, x, y, width, height);
}
@Override
public void dispose(final GLAutoDrawable drawable) {
System.err.println(Thread.currentThread()+" MovieCube.dispose ... ");
if( null != textRendererGLEL ) {
drawable.disposeGLEventListener(textRendererGLEL, true);
textRendererGLEL = null;
}
disposeImpl(drawable, true);
}
private void disposeImpl(final GLAutoDrawable drawable, final boolean disposePlayer) {
if(null == mPlayer) { return; }
final Object upstreamWidget = drawable.getUpstreamWidget();
if (upstreamWidget instanceof Window) {
final Window window = (Window) upstreamWidget;
window.removeKeyListener(keyAction);
}
final GL2ES2 gl = drawable.getGL().getGL2ES2();
if( disposePlayer ) {
mPlayer.destroy(gl);
mPlayer=null;
}
cube.dispose(drawable);
cube=null;
}
@Override
public void display(final GLAutoDrawable drawable) {
if( swapIntervalSet ) {
final GL2ES2 gl = drawable.getGL().getGL2ES2();
final int _swapInterval = swapInterval;
gl.setSwapInterval(_swapInterval); // in case switching the drawable (impl. may bound attribute there)
drawable.getAnimator().resetFPSCounter();
swapInterval = gl.getSwapInterval();
System.err.println("Swap Interval: "+_swapInterval+" -> "+swapInterval);
swapIntervalSet = false;
}
if(null == mPlayer) { return; }
if( resetGLState ) {
resetGLState = false;
System.err.println("XXX resetGLState");
disposeImpl(drawable, false);
init(drawable);
reshape(drawable, 0, 0, drawable.getSurfaceWidth(), drawable.getSurfaceHeight());
}
final long currentPos = System.currentTimeMillis();
if( currentPos - lastPerfPos > 2000 ) {
System.err.println( mPlayer.getPerfString() );
lastPerfPos = currentPos;
}
cube.display(drawable);
}
public static void main(final String[] args) throws IOException, InterruptedException, URISyntaxException {
int swapInterval = 1;
int width = 800;
int height = 600;
int textureCount = GLMediaPlayer.TEXTURE_COUNT_DEFAULT; // default - threaded
boolean forceES2 = false;
boolean forceES3 = false;
boolean forceGL3 = false;
boolean forceGLDef = false;
int vid = GLMediaPlayer.STREAM_ID_AUTO;
int aid = GLMediaPlayer.STREAM_ID_AUTO;
final boolean origSize;
String url_s=null, file_s=null;
{
boolean _origSize = false;
for(int i=0; i<args.length; i++) {
if(args[i].equals("-vid")) {
i++;
vid = MiscUtils.atoi(args[i], vid);
} else if(args[i].equals("-aid")) {
i++;
aid = MiscUtils.atoi(args[i], aid);
} else if(args[i].equals("-width")) {
i++;
width = MiscUtils.atoi(args[i], width);
} else if(args[i].equals("-height")) {
i++;
height = MiscUtils.atoi(args[i], height);
} else if(args[i].equals("-osize")) {
_origSize = true;
} else if(args[i].equals("-textureCount")) {
i++;
textureCount = MiscUtils.atoi(args[i], textureCount);
} else if(args[i].equals("-url")) {
i++;
url_s = args[i];
} else if(args[i].equals("-file")) {
i++;
file_s = args[i];
} else if(args[i].equals("-es2")) {
forceES2 = true;
} else if(args[i].equals("-es3")) {
forceES3 = true;
} else if(args[i].equals("-gl3")) {
forceGL3 = true;
} else if(args[i].equals("-gldef")) {
forceGLDef = true;
} else if(args[i].equals("-vsync")) {
i++;
swapInterval = MiscUtils.atoi(args[i], swapInterval);
} else if(args[i].equals("-wait")) {
waitForKey = true;
}
}
origSize = _origSize;
}
final Uri streamLoc;
if( null != url_s ) {
streamLoc = Uri.cast( url_s );
} else if( null != file_s ) {
streamLoc = Uri.valueOf(new File(file_s));
} else {
streamLoc = defURI;
}
System.err.println("url_s "+url_s);
System.err.println("file_s "+file_s);
System.err.println("stream "+streamLoc);
System.err.println("vid "+vid+", aid "+aid);
System.err.println("textureCount "+textureCount);
System.err.println("forceES2 "+forceES2);
System.err.println("forceES3 "+forceES3);
System.err.println("forceGL3 "+forceGL3);
System.err.println("forceGLDef "+forceGLDef);
System.err.println("swapInterval "+swapInterval);
final MovieCube mc = new MovieCube(zoom_def, 0f, 0f, true);
mc.setSwapInterval(swapInterval);
final GLProfile glp;
if(forceGLDef) {
glp = GLProfile.getDefault();
} else if(forceGL3) {
glp = GLProfile.get(GLProfile.GL3);
} else if(forceES3) {
glp = GLProfile.get(GLProfile.GLES3);
} else if(forceES2) {
glp = GLProfile.get(GLProfile.GLES2);
} else {
glp = GLProfile.getGL2ES2();
}
System.err.println("GLProfile: "+glp);
final GLCapabilities caps = new GLCapabilities(glp);
// caps.setAlphaBits(4); // NOTE_ALPHA_BLENDING: We go w/o alpha and blending!
final GLWindow window = GLWindow.create(caps);
final Animator anim = new Animator(window);
window.addWindowListener(new WindowAdapter() {
public void windowDestroyed(final WindowEvent e) {
anim.stop();
}
});
window.setSize(width, height);
window.setVisible(true);
System.err.println("Chosen: "+window.getChosenGLCapabilities());
anim.start();
mc.mPlayer.addEventListener(new GLMediaEventListener() {
@Override
public void newFrameAvailable(final GLMediaPlayer ts, final TextureFrame newFrame, final long when) {
}
@Override
public void attributesChanged(final GLMediaPlayer mp, final int event_mask, final long when) {
System.err.println("MovieCube AttributesChanges: events_mask 0x"+Integer.toHexString(event_mask)+", when "+when);
System.err.println("MovieCube State: "+mp);
if( 0 != ( GLMediaEventListener.EVENT_CHANGE_SIZE & event_mask ) ) {
if( origSize ) {
window.setSurfaceSize(mp.getWidth(), mp.getHeight());
}
// window.disposeGLEventListener(ms, false /* remove */ );
mc.resetGLState();
}
if( 0 != ( GLMediaEventListener.EVENT_CHANGE_INIT & event_mask ) ) {
window.addGLEventListener(mc);
anim.setUpdateFPSFrames(60, null);
anim.resetFPSCounter();
}
if( 0 != ( GLMediaEventListener.EVENT_CHANGE_PLAY & event_mask ) ) {
anim.resetFPSCounter();
}
if( 0 != ( ( GLMediaEventListener.EVENT_CHANGE_ERR | GLMediaEventListener.EVENT_CHANGE_EOS ) & event_mask ) ) {
final StreamException se = mc.mPlayer.getStreamException();
if( null != se ) {
se.printStackTrace();
}
new InterruptSource.Thread() {
public void run() {
window.destroy();
} }.start();
}
}
});
mc.initStream(streamLoc, vid, aid, textureCount);
}
}