Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media.cts;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.media.MediaCodec;
     22 import android.media.MediaCodecInfo.CodecCapabilities;
     23 import android.media.MediaFormat;
     24 import android.os.Bundle;
     25 import android.test.AndroidTestCase;
     26 import android.util.Log;
     27 
     28 import com.android.cts.media.R;
     29 
     30 import java.io.InputStream;
     31 import java.nio.ByteBuffer;
     32 
     33 /**
     34  * Basic verification test for vp8 encoder.
     35  *
     36  * A raw yv12 stream is encoded and written to an IVF
     37  * file, which is later decoded by vp8 decoder to verify
     38  * frames are at least decodable.
     39  */
     40 public class Vp8EncoderTest extends AndroidTestCase {
     41 
     42     private static final String TAG = "VP8EncoderTest";
     43     private static final String VP8_MIME = "video/x-vnd.on2.vp8";
     44     private static final String VPX_DECODER_NAME = "OMX.google.vp8.decoder";
     45     private static final String VPX_ENCODER_NAME = "OMX.google.vp8.encoder";
     46     private static final String BASIC_IVF = "video_176x144_vp8_basic.ivf";
     47     private static final long DEFAULT_TIMEOUT_US = 5000;
     48 
     49     private Resources mResources;
     50     private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
     51     private ByteBuffer[] mInputBuffers;
     52     private ByteBuffer[] mOutputBuffers;
     53 
     54     @Override
     55     public void setContext(Context context) {
     56         super.setContext(context);
     57         mResources = mContext.getResources();
     58     }
     59 
     60     /**
     61      * A basic test for VP8 encoder.
     62      *
     63      * Encodes a raw stream with default configuration options,
     64      * and then decodes it to verify the bitstream.
     65      */
     66     public void testBasic() throws Exception {
     67         encode(BASIC_IVF,
     68                R.raw.video_176x144_yv12,
     69                176,  // width
     70                144,  // height
     71                30);  // framerate
     72         decode(BASIC_IVF);
     73     }
     74 
     75     /**
     76      * Check if MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME is honored.
     77      *
     78      * At frame 15, request a sync frame. If one does not occur by EOF the
     79      * encoder fails. The test does not verify the output stream.
     80      */
     81     public void testSyncFrame() throws Exception {
     82         encodeSyncFrame(R.raw.video_176x144_yv12,
     83                         176, // width
     84                         144, // height
     85                         30); // framerate
     86     }
     87 
     88     /**
     89      * Check if MediaCodec.PARAMETER_KEY_VIDEO_BITRATE is honored.
     90      *
     91      * Run the sample multiple times. Request periodic changes to the
     92      * bitrate and ensure the encoder responds.
     93      */
     94     public void testVariableBitrate() throws Exception {
     95         encodeVariableBitrate(R.raw.video_176x144_yv12,
     96                               176, // width
     97                               144, // height
     98                               30); // framerate
     99     }
    100 
    101     /**
    102      * A basic check if an encoded stream is decodable.
    103      *
    104      * The most basic confirmation we can get about a frame
    105      * being properly encoded is trying to decode it.
    106      * (Especially in realtime mode encode output is non-
    107      * deterministic, therefore a more thorough check like
    108      * md5 sum comparison wouldn't work.)
    109      *
    110      * Indeed, MediaCodec will raise an IllegalStateException
    111      * whenever vp8 decoder fails to decode a frame, and
    112      * this test uses that fact to verify the bitstream.
    113      *
    114      * @param filename  The name of the IVF file containing encoded bitsream.
    115      */
    116     private void decode(String filename) throws Exception {
    117         IvfReader ivf = null;
    118         try {
    119             ivf = new IvfReader(filename);
    120             int frameWidth = ivf.getWidth();
    121             int frameHeight = ivf.getHeight();
    122             int frameCount = ivf.getFrameCount();
    123 
    124             assertTrue(frameWidth > 0);
    125             assertTrue(frameHeight > 0);
    126             assertTrue(frameCount > 0);
    127 
    128             MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME,
    129                                                                ivf.getWidth(),
    130                                                                ivf.getHeight());
    131 
    132             Log.d(TAG, "Creating decoder");
    133             MediaCodec decoder = MediaCodec.createByCodecName(VPX_DECODER_NAME);
    134             decoder.configure(format,
    135                               null,  // surface
    136                               null,  // crypto
    137                               0);  // flags
    138             decoder.start();
    139 
    140             mInputBuffers = decoder.getInputBuffers();
    141             mOutputBuffers = decoder.getOutputBuffers();
    142 
    143             // decode loop
    144             int frameIndex = 0;
    145             boolean sawOutputEOS = false;
    146             boolean sawInputEOS = false;
    147 
    148             while (!sawOutputEOS) {
    149                 if (!sawInputEOS) {
    150                     int inputBufIndex = decoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
    151                     if (inputBufIndex >= 0) {
    152                         byte[] frame = ivf.readFrame(frameIndex);
    153 
    154                         if (frameIndex == frameCount - 1) {
    155                             sawInputEOS = true;
    156                         }
    157 
    158                         mInputBuffers[inputBufIndex].clear();
    159                         mInputBuffers[inputBufIndex].put(frame);
    160                         mInputBuffers[inputBufIndex].rewind();
    161 
    162                         Log.d(TAG, "Decoding frame at index " + frameIndex);
    163                         try {
    164                             decoder.queueInputBuffer(
    165                                     inputBufIndex,
    166                                     0,  // offset
    167                                     frame.length,
    168                                     frameIndex,
    169                                     sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
    170                         } catch (IllegalStateException ise) {
    171                             //That is all what is passed from MediaCodec in case of
    172                             //decode failure.
    173                             fail("Failed to decode frame at index " + frameIndex);
    174                         }
    175                         frameIndex++;
    176                     }
    177                 }
    178 
    179                 int result = decoder.dequeueOutputBuffer(mBufferInfo, DEFAULT_TIMEOUT_US);
    180                 if (result >= 0) {
    181                     int outputBufIndex = result;
    182                     if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    183                         sawOutputEOS = true;
    184                     }
    185                     decoder.releaseOutputBuffer(outputBufIndex, false);
    186                 } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    187                     mOutputBuffers = decoder.getOutputBuffers();
    188                 }
    189             }
    190             decoder.stop();
    191             decoder.release();
    192         } finally {
    193             if (ivf != null) {
    194                 ivf.close();
    195             }
    196         }
    197     }
    198 
    199     /**
    200      * A basic vp8 encode loop.
    201      *
    202      * MediaCodec will raise an IllegalStateException
    203      * whenever vp8 encoder fails to encode a frame.
    204      *
    205      * In addition to that written IVF file can be tested
    206      * to be decodable in order to verify the bitstream produced.
    207      *
    208      * Color format of input file should be YUV420, and frameWidth,
    209      * frameHeight should be supplied correctly as raw input file doesn't
    210      * include any header data.
    211      *
    212      * @param outputFilename  The name of the IVF file to write encoded bitsream
    213      * @param rawInputFd      File descriptor for the raw input file (YUV420)
    214      * @param frameWidth      Frame width of input file
    215      * @param frameHeight     Frame height of input file
    216      * @param frameRate       Frame rate of input file in frames per second
    217      */
    218     private void encode(String outputFilename, int rawInputFd,
    219                        int frameWidth, int frameHeight, int frameRate) throws Exception {
    220         // Create a media format signifying desired output
    221         MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME, frameWidth, frameHeight);
    222         format.setInteger(MediaFormat.KEY_BIT_RATE, 100000);
    223         format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    224                           CodecCapabilities.COLOR_FormatYUV420Planar);
    225         format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    226 
    227         Log.d(TAG, "Creating encoder");
    228         MediaCodec encoder;
    229         encoder = MediaCodec.createByCodecName(VPX_ENCODER_NAME);
    230         encoder.configure(format,
    231                           null,  // surface
    232                           null,  // crypto
    233                           MediaCodec.CONFIGURE_FLAG_ENCODE);
    234         encoder.start();
    235 
    236         mInputBuffers = encoder.getInputBuffers();
    237         mOutputBuffers = encoder.getOutputBuffers();
    238 
    239         InputStream rawStream = null;
    240         IvfWriter ivf = null;
    241 
    242         try {
    243             rawStream = mResources.openRawResource(rawInputFd);
    244             ivf = new IvfWriter(outputFilename, frameWidth, frameHeight);
    245             // encode loop
    246             long presentationTimeUs = 0;
    247             int inputFrameIndex = 0;
    248             int outputFrameIndex = 0;
    249             boolean sawInputEOS = false;
    250             boolean sawOutputEOS = false;
    251 
    252             while (!sawOutputEOS) {
    253                 if (!sawInputEOS) {
    254                     int inputBufIndex = encoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
    255                     if (inputBufIndex >= 0) {
    256                         // YUV420 has 3 planes. Y is full size. U and V are each half size (1/4 the
    257                         // pixels).
    258                         int frameSize = frameWidth * frameHeight * 3 / 2;
    259 
    260                         byte[] frame = new byte[frameSize];
    261                         int bytesRead = rawStream.read(frame);
    262 
    263                         if (bytesRead == -1) {
    264                             sawInputEOS = true;
    265                             bytesRead = 0;
    266                         }
    267 
    268                         mInputBuffers[inputBufIndex].clear();
    269                         mInputBuffers[inputBufIndex].put(frame);
    270                         mInputBuffers[inputBufIndex].rewind();
    271 
    272                         presentationTimeUs = (inputFrameIndex * 1000000) / frameRate;
    273                         Log.d(TAG, "Encoding frame at index " + inputFrameIndex);
    274                         encoder.queueInputBuffer(
    275                                 inputBufIndex,
    276                                 0,  // offset
    277                                 bytesRead,  // size
    278                                 presentationTimeUs,
    279                                 sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
    280 
    281                         inputFrameIndex++;
    282                     }
    283                 }
    284 
    285                 int result = encoder.dequeueOutputBuffer(mBufferInfo, DEFAULT_TIMEOUT_US);
    286                 if (result >= 0) {
    287                     int outputBufIndex = result;
    288                     byte[] buffer = new byte[mBufferInfo.size];
    289                     mOutputBuffers[outputBufIndex].rewind();
    290                     mOutputBuffers[outputBufIndex].get(buffer, 0, mBufferInfo.size);
    291 
    292                     if ((outputFrameIndex == 0)
    293                         && ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) == 0)) {
    294                       throw new RuntimeException("First frame is not a sync frame.");
    295 
    296                     }
    297 
    298                     if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    299                         sawOutputEOS = true;
    300                     } else {
    301                         ivf.writeFrame(buffer, mBufferInfo.presentationTimeUs);
    302                     }
    303                     encoder.releaseOutputBuffer(outputBufIndex,
    304                                                 false);  // render
    305 
    306                     outputFrameIndex++;
    307                 } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    308                     mOutputBuffers = encoder.getOutputBuffers();
    309                 }
    310             }
    311 
    312             encoder.stop();
    313             encoder.release();
    314         } finally {
    315             if (ivf != null) {
    316                 ivf.close();
    317             }
    318 
    319             if (rawStream != null) {
    320                 rawStream.close();
    321             }
    322         }
    323     }
    324 
    325 
    326     /**
    327      * Request Sync Frames
    328      *
    329      * MediaCodec will raise an IllegalStateException
    330      * whenever vp8 encoder fails to encode a frame.
    331      *
    332      * This presumes a file with 28 frames. Under normal circumstances there
    333      * would only be one sync frame: the first one. This test will request an
    334      * additional sync frame at 15 and ensure that it occurs by EOF.
    335      *
    336      * Color format of input file should be YUV420, and frameWidth,
    337      * frameHeight should be supplied correctly as raw input file doesn't
    338      * include any header data.
    339      *
    340      * @param rawInputFd      File descriptor for the raw input file (YUV420)
    341      * @param frameWidth      Frame width of input file
    342      * @param frameHeight     Frame height of input file
    343      * @param frameRate       Frame rate of input file in frames per second
    344      */
    345     private void encodeSyncFrame(int rawInputFd, int frameWidth,
    346                                  int frameHeight, int frameRate) throws Exception {
    347         // Create a media format signifying desired output
    348         MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME, frameWidth, frameHeight);
    349         format.setInteger(MediaFormat.KEY_BIT_RATE, 100000);
    350         format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    351                           CodecCapabilities.COLOR_FormatYUV420Planar);
    352         format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    353 
    354         Log.d(TAG, "Creating encoder");
    355         MediaCodec encoder;
    356         encoder = MediaCodec.createByCodecName(VPX_ENCODER_NAME);
    357         encoder.configure(format,
    358                           null,  // surface
    359                           null,  // crypto
    360                           MediaCodec.CONFIGURE_FLAG_ENCODE);
    361         encoder.start();
    362 
    363         mInputBuffers = encoder.getInputBuffers();
    364         mOutputBuffers = encoder.getOutputBuffers();
    365 
    366         InputStream rawStream = null;
    367 
    368         try {
    369             rawStream = mResources.openRawResource(rawInputFd);
    370             // encode loop
    371             long presentationTimeUs = 0;
    372             int inputFrameIndex = 0;
    373             boolean sawInputEOS = false;
    374             boolean sawOutputEOS = false;
    375             boolean syncFrameRequested = false;
    376             boolean matchedSyncFrame = false;
    377 
    378             while (!sawOutputEOS) {
    379                 if (!sawInputEOS) {
    380                     int inputBufIndex = encoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
    381                     if (inputBufIndex >= 0) {
    382                         int frameSize = frameWidth * frameHeight * 3 / 2;
    383 
    384                         byte[] frame = new byte[frameSize];
    385                         int bytesRead = rawStream.read(frame);
    386 
    387                         if (bytesRead == -1) {
    388                             sawInputEOS = true;
    389                             bytesRead = 0;
    390                         }
    391 
    392                         mInputBuffers[inputBufIndex].clear();
    393                         mInputBuffers[inputBufIndex].put(frame);
    394                         mInputBuffers[inputBufIndex].rewind();
    395 
    396                         if (inputFrameIndex == 15) {
    397                             Log.d(TAG, "Requesting sync frame at index " + inputFrameIndex);
    398                             Bundle syncFrame = new Bundle();
    399                             syncFrame.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
    400                             encoder.setParameters(syncFrame);
    401                             syncFrameRequested = true;
    402                         }
    403 
    404                         presentationTimeUs = (inputFrameIndex * 1000000) / frameRate;
    405                         encoder.queueInputBuffer(
    406                                 inputBufIndex,
    407                                 0,  // offset
    408                                 bytesRead,  // size
    409                                 presentationTimeUs,
    410                                 sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
    411 
    412                         inputFrameIndex++;
    413                     }
    414                 }
    415 
    416                 int result = encoder.dequeueOutputBuffer(mBufferInfo, DEFAULT_TIMEOUT_US);
    417                 if (result >= 0) {
    418                     if (syncFrameRequested && ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0)) {
    419                         Log.d(TAG, "Found sync frame");
    420                         matchedSyncFrame = true;
    421                     }
    422 
    423                     if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    424                         sawOutputEOS = true;
    425                     }
    426 
    427                     encoder.releaseOutputBuffer(result,
    428                                                 false);  // render
    429 
    430                 } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    431                     mOutputBuffers = encoder.getOutputBuffers();
    432                 }
    433             }
    434 
    435             if (!matchedSyncFrame) {
    436                 throw new RuntimeException("Requested sync frame did not occur");
    437             }
    438 
    439             encoder.stop();
    440             encoder.release();
    441         } finally {
    442             if (rawStream != null) {
    443                 rawStream.close();
    444             }
    445         }
    446     }
    447 
    448 
    449     /**
    450      * Adjust bitrate
    451      *
    452      * MediaCodec will raise an IllegalStateException
    453      * whenever vp8 encoder fails to encode a frame.
    454      *
    455      * Encode the file three times: once at the initial bitrate, once at an
    456      * increased bitrate, and once at a decreased bitrate. Record the frame
    457      * sizes that are returned and verify a strict ordering.
    458      *
    459      * Color format of input file should be YUV420, and frameWidth,
    460      * frameHeight should be supplied correctly as raw input file doesn't
    461      * include any header data.
    462      *
    463      * @param rawInputFd      File descriptor for the raw input file (YUV420)
    464      * @param frameWidth      Frame width of input file
    465      * @param frameHeight     Frame height of input file
    466      * @param frameRate       Frame rate of input file in frames per second
    467      */
    468     private void encodeVariableBitrate(int rawInputFd, int frameWidth,
    469                                        int frameHeight, int frameRate) throws Exception {
    470         // Create a media format signifying desired output
    471         MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME, frameWidth, frameHeight);
    472         format.setInteger(MediaFormat.KEY_BIT_RATE, 75000);
    473         format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    474                           CodecCapabilities.COLOR_FormatYUV420Planar);
    475         format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    476 
    477         Log.d(TAG, "Creating encoder");
    478         MediaCodec encoder;
    479         encoder = MediaCodec.createByCodecName(VPX_ENCODER_NAME);
    480         encoder.configure(format,
    481                           null,  // surface
    482                           null,  // crypto
    483                           MediaCodec.CONFIGURE_FLAG_ENCODE);
    484         encoder.start();
    485 
    486         mInputBuffers = encoder.getInputBuffers();
    487         mOutputBuffers = encoder.getOutputBuffers();
    488 
    489         InputStream rawStream = null;
    490 
    491         int iteration = 0;
    492         int[] bits = new int[100];
    493 
    494         try {
    495             rawStream = mResources.openRawResource(rawInputFd);
    496             /* Doc says this is not the default:
    497              * http://developer.android.com/reference/java/io/InputStream.html#markSupported()
    498              * but it returns true so using .reset() instead of close/open
    499              */
    500             if (rawStream.markSupported()) Log.d(TAG, "Stream marking supported");
    501             rawStream.mark(1000000);
    502 
    503             // encode loop
    504             long presentationTimeUs = 0;
    505             int inputFrameIndex = 0;
    506             int outputFrameIndex = 0;
    507             boolean sawInputEOS = false;
    508             boolean sawOutputEOS = false;
    509 
    510             while (!sawOutputEOS) {
    511                 if (!sawInputEOS) {
    512                     int inputBufIndex = encoder.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
    513                     if (inputBufIndex >= 0) {
    514                         int frameSize = frameWidth * frameHeight * 3 / 2;
    515 
    516                         byte[] frame = new byte[frameSize];
    517                         int bytesRead = rawStream.read(frame);
    518 
    519                         if (bytesRead == -1) {
    520                             if (iteration < 2) {
    521                                 rawStream.reset();
    522                                 Bundle bitrate = new Bundle();
    523                                 if (iteration == 0) {
    524                                     bitrate.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, 150000);
    525                                     Log.d(TAG, "Setting bitrate to 150000");
    526                                 } else {
    527                                     bitrate.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, 25000);
    528                                     Log.d(TAG, "Setting bitrate to 25000");
    529                                 }
    530                                 encoder.setParameters(bitrate);
    531 
    532                                 iteration++;
    533                                 continue;
    534                             } else {
    535                                 sawInputEOS = true;
    536                                 bytesRead = 0;
    537                             }
    538                         }
    539 
    540                         mInputBuffers[inputBufIndex].clear();
    541                         mInputBuffers[inputBufIndex].put(frame);
    542                         mInputBuffers[inputBufIndex].rewind();
    543 
    544                         presentationTimeUs = (inputFrameIndex * 1000000) / frameRate;
    545                         encoder.queueInputBuffer(
    546                                 inputBufIndex,
    547                                 0,  // offset
    548                                 bytesRead,  // size
    549                                 presentationTimeUs,
    550                                 sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
    551 
    552                         inputFrameIndex++;
    553                     }
    554                 }
    555 
    556                 int result = encoder.dequeueOutputBuffer(mBufferInfo, DEFAULT_TIMEOUT_US);
    557                 if (result >= 0) {
    558 
    559                     bits[outputFrameIndex] = mBufferInfo.size;
    560 
    561                     if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    562                         sawOutputEOS = true;
    563                     }
    564 
    565                     encoder.releaseOutputBuffer(result,
    566                                                 false);  // render
    567 
    568                     outputFrameIndex++;
    569 
    570                 } else if (result == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    571                     mOutputBuffers = encoder.getOutputBuffers();
    572                 }
    573             }
    574 
    575             // 29 frames per run
    576             int i;
    577             int sum = 0;
    578             int frames = 29;
    579             for(i = 0; i < frames; i++)
    580               sum += bits[i];
    581             int midBitrateAvg = sum / frames;
    582 
    583             sum = 0;
    584             for(; i < frames * 2; i++)
    585               sum += bits[i];
    586             int highBitrateAvg = sum / frames;
    587 
    588             sum = 0;
    589             for(; i < frames * 3; i++)
    590               sum += bits[i];
    591             int lowBitrateAvg = sum / frames;
    592 
    593             // For the given bitrates we expect mid ~= 350, high ~= 575 and low ~= 150
    594             // bytes per frame
    595             if ((midBitrateAvg + 100) > highBitrateAvg)
    596                 throw new RuntimeException("Bitrate did not increase when requesting higher bitrate");
    597             if ((lowBitrateAvg + 100) > midBitrateAvg)
    598                 throw new RuntimeException("Bitrate did not decrease when requesting lower bitrate");
    599 
    600 
    601             encoder.stop();
    602             encoder.release();
    603         } finally {
    604             if (rawStream != null) {
    605                 rawStream.close();
    606             }
    607         }
    608     }
    609 }
    610