Home | History | Annotate | Download | only in builder
      1 /*
      2  * Copyright 2012 Sebastian Annies, Hamburg
      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 package com.googlecode.mp4parser.authoring.builder;
     17 
     18 import com.coremedia.iso.BoxParser;
     19 import com.coremedia.iso.IsoFile;
     20 import com.coremedia.iso.IsoTypeWriter;
     21 import com.coremedia.iso.boxes.Box;
     22 import com.coremedia.iso.boxes.CompositionTimeToSample;
     23 import com.coremedia.iso.boxes.ContainerBox;
     24 import com.coremedia.iso.boxes.DataEntryUrlBox;
     25 import com.coremedia.iso.boxes.DataInformationBox;
     26 import com.coremedia.iso.boxes.DataReferenceBox;
     27 import com.coremedia.iso.boxes.FileTypeBox;
     28 import com.coremedia.iso.boxes.HandlerBox;
     29 import com.coremedia.iso.boxes.MediaBox;
     30 import com.coremedia.iso.boxes.MediaHeaderBox;
     31 import com.coremedia.iso.boxes.MediaInformationBox;
     32 import com.coremedia.iso.boxes.MovieBox;
     33 import com.coremedia.iso.boxes.MovieHeaderBox;
     34 import com.coremedia.iso.boxes.SampleDependencyTypeBox;
     35 import com.coremedia.iso.boxes.SampleSizeBox;
     36 import com.coremedia.iso.boxes.SampleTableBox;
     37 import com.coremedia.iso.boxes.SampleToChunkBox;
     38 import com.coremedia.iso.boxes.StaticChunkOffsetBox;
     39 import com.coremedia.iso.boxes.SyncSampleBox;
     40 import com.coremedia.iso.boxes.TimeToSampleBox;
     41 import com.coremedia.iso.boxes.TrackBox;
     42 import com.coremedia.iso.boxes.TrackHeaderBox;
     43 import com.googlecode.mp4parser.authoring.DateHelper;
     44 import com.googlecode.mp4parser.authoring.Movie;
     45 import com.googlecode.mp4parser.authoring.Track;
     46 
     47 import java.io.IOException;
     48 import java.nio.ByteBuffer;
     49 import java.nio.MappedByteBuffer;
     50 import java.nio.channels.GatheringByteChannel;
     51 import java.nio.channels.ReadableByteChannel;
     52 import java.nio.channels.WritableByteChannel;
     53 import java.util.ArrayList;
     54 import java.util.Date;
     55 import java.util.HashMap;
     56 import java.util.HashSet;
     57 import java.util.LinkedList;
     58 import java.util.List;
     59 import java.util.Map;
     60 import java.util.Set;
     61 import java.util.logging.Level;
     62 import java.util.logging.Logger;
     63 
     64 import static com.googlecode.mp4parser.util.CastUtils.l2i;
     65 
     66 /**
     67  * Creates a plain MP4 file from a video. Plain as plain can be.
     68  */
     69 public class DefaultMp4Builder implements Mp4Builder {
     70 
     71     public int STEPSIZE = 64;
     72     Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>();
     73     private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName());
     74 
     75     HashMap<Track, List<ByteBuffer>> track2Sample = new HashMap<Track, List<ByteBuffer>>();
     76     HashMap<Track, long[]> track2SampleSizes = new HashMap<Track, long[]>();
     77     private FragmentIntersectionFinder intersectionFinder = new TwoSecondIntersectionFinder();
     78 
     79     public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) {
     80         this.intersectionFinder = intersectionFinder;
     81     }
     82 
     83     /**
     84      * {@inheritDoc}
     85      */
     86     public IsoFile build(Movie movie) {
     87         LOG.fine("Creating movie " + movie);
     88         for (Track track : movie.getTracks()) {
     89             // getting the samples may be a time consuming activity
     90             List<ByteBuffer> samples = track.getSamples();
     91             putSamples(track, samples);
     92             long[] sizes = new long[samples.size()];
     93             for (int i = 0; i < sizes.length; i++) {
     94                 sizes[i] = samples.get(i).limit();
     95             }
     96             putSampleSizes(track, sizes);
     97         }
     98 
     99         IsoFile isoFile = new IsoFile();
    100         // ouch that is ugly but I don't know how to do it else
    101         List<String> minorBrands = new LinkedList<String>();
    102         minorBrands.add("isom");
    103         minorBrands.add("iso2");
    104         minorBrands.add("avc1");
    105 
    106         isoFile.addBox(new FileTypeBox("isom", 0, minorBrands));
    107         isoFile.addBox(createMovieBox(movie));
    108         InterleaveChunkMdat mdat = new InterleaveChunkMdat(movie);
    109         isoFile.addBox(mdat);
    110 
    111         /*
    112         dataOffset is where the first sample starts. In this special mdat the samples always start
    113         at offset 16 so that we can use the same offset for large boxes and small boxes
    114          */
    115         long dataOffset = mdat.getDataOffset();
    116         for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
    117             long[] offsets = chunkOffsetBox.getChunkOffsets();
    118             for (int i = 0; i < offsets.length; i++) {
    119                 offsets[i] += dataOffset;
    120             }
    121         }
    122 
    123 
    124         return isoFile;
    125     }
    126 
    127     public FragmentIntersectionFinder getFragmentIntersectionFinder() {
    128         throw new UnsupportedOperationException("No fragment intersection finder in default MP4 builder!");
    129     }
    130 
    131     protected long[] putSampleSizes(Track track, long[] sizes) {
    132         return track2SampleSizes.put(track, sizes);
    133     }
    134 
    135     protected List<ByteBuffer> putSamples(Track track, List<ByteBuffer> samples) {
    136         return track2Sample.put(track, samples);
    137     }
    138 
    139     private MovieBox createMovieBox(Movie movie) {
    140         MovieBox movieBox = new MovieBox();
    141         MovieHeaderBox mvhd = new MovieHeaderBox();
    142 
    143         mvhd.setCreationTime(DateHelper.convert(new Date()));
    144         mvhd.setModificationTime(DateHelper.convert(new Date()));
    145 
    146         long movieTimeScale = getTimescale(movie);
    147         long duration = 0;
    148 
    149         for (Track track : movie.getTracks()) {
    150             long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale();
    151             if (tracksDuration > duration) {
    152                 duration = tracksDuration;
    153             }
    154 
    155 
    156         }
    157 
    158         mvhd.setDuration(duration);
    159         mvhd.setTimescale(movieTimeScale);
    160         // find the next available trackId
    161         long nextTrackId = 0;
    162         for (Track track : movie.getTracks()) {
    163             nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId;
    164         }
    165         mvhd.setNextTrackId(++nextTrackId);
    166         if (mvhd.getCreationTime() >= 1l << 32 ||
    167                 mvhd.getModificationTime() >= 1l << 32 ||
    168                 mvhd.getDuration() >= 1l << 32) {
    169             mvhd.setVersion(1);
    170         }
    171 
    172         movieBox.addBox(mvhd);
    173         for (Track track : movie.getTracks()) {
    174             movieBox.addBox(createTrackBox(track, movie));
    175         }
    176         // metadata here
    177         Box udta = createUdta(movie);
    178         if (udta != null) {
    179             movieBox.addBox(udta);
    180         }
    181         return movieBox;
    182 
    183     }
    184 
    185     /**
    186      * Override to create a user data box that may contain metadata.
    187      *
    188      * @return a 'udta' box or <code>null</code> if none provided
    189      */
    190     protected Box createUdta(Movie movie) {
    191         return null;
    192     }
    193 
    194     private TrackBox createTrackBox(Track track, Movie movie) {
    195 
    196         LOG.info("Creating Mp4TrackImpl " + track);
    197         TrackBox trackBox = new TrackBox();
    198         TrackHeaderBox tkhd = new TrackHeaderBox();
    199         int flags = 0;
    200         if (track.isEnabled()) {
    201             flags += 1;
    202         }
    203 
    204         if (track.isInMovie()) {
    205             flags += 2;
    206         }
    207 
    208         if (track.isInPreview()) {
    209             flags += 4;
    210         }
    211 
    212         if (track.isInPoster()) {
    213             flags += 8;
    214         }
    215         tkhd.setFlags(flags);
    216 
    217         tkhd.setAlternateGroup(track.getTrackMetaData().getGroup());
    218         tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
    219         // We need to take edit list box into account in trackheader duration
    220         // but as long as I don't support edit list boxes it is sufficient to
    221         // just translate media duration to movie timescale
    222         tkhd.setDuration(getDuration(track) * getTimescale(movie) / track.getTrackMetaData().getTimescale());
    223         tkhd.setHeight(track.getTrackMetaData().getHeight());
    224         tkhd.setWidth(track.getTrackMetaData().getWidth());
    225         tkhd.setLayer(track.getTrackMetaData().getLayer());
    226         tkhd.setModificationTime(DateHelper.convert(new Date()));
    227         tkhd.setTrackId(track.getTrackMetaData().getTrackId());
    228         tkhd.setVolume(track.getTrackMetaData().getVolume());
    229         tkhd.setMatrix(track.getTrackMetaData().getMatrix());
    230         if (tkhd.getCreationTime() >= 1l << 32 ||
    231                 tkhd.getModificationTime() >= 1l << 32 ||
    232                 tkhd.getDuration() >= 1l << 32) {
    233             tkhd.setVersion(1);
    234         }
    235 
    236         trackBox.addBox(tkhd);
    237 
    238 /*
    239         EditBox edit = new EditBox();
    240         EditListBox editListBox = new EditListBox();
    241         editListBox.setEntries(Collections.singletonList(
    242                 new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1)));
    243         edit.addBox(editListBox);
    244         trackBox.addBox(edit);
    245 */
    246 
    247         MediaBox mdia = new MediaBox();
    248         trackBox.addBox(mdia);
    249         MediaHeaderBox mdhd = new MediaHeaderBox();
    250         mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
    251         mdhd.setDuration(getDuration(track));
    252         mdhd.setTimescale(track.getTrackMetaData().getTimescale());
    253         mdhd.setLanguage(track.getTrackMetaData().getLanguage());
    254         mdia.addBox(mdhd);
    255         HandlerBox hdlr = new HandlerBox();
    256         mdia.addBox(hdlr);
    257 
    258         hdlr.setHandlerType(track.getHandler());
    259 
    260         MediaInformationBox minf = new MediaInformationBox();
    261         minf.addBox(track.getMediaHeaderBox());
    262 
    263         // dinf: all these three boxes tell us is that the actual
    264         // data is in the current file and not somewhere external
    265         DataInformationBox dinf = new DataInformationBox();
    266         DataReferenceBox dref = new DataReferenceBox();
    267         dinf.addBox(dref);
    268         DataEntryUrlBox url = new DataEntryUrlBox();
    269         url.setFlags(1);
    270         dref.addBox(url);
    271         minf.addBox(dinf);
    272         //
    273 
    274         SampleTableBox stbl = new SampleTableBox();
    275 
    276         stbl.addBox(track.getSampleDescriptionBox());
    277 
    278         List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries();
    279         if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) {
    280             TimeToSampleBox stts = new TimeToSampleBox();
    281             stts.setEntries(track.getDecodingTimeEntries());
    282             stbl.addBox(stts);
    283         }
    284 
    285         List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries();
    286         if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) {
    287             CompositionTimeToSample ctts = new CompositionTimeToSample();
    288             ctts.setEntries(compositionTimeToSampleEntries);
    289             stbl.addBox(ctts);
    290         }
    291 
    292         long[] syncSamples = track.getSyncSamples();
    293         if (syncSamples != null && syncSamples.length > 0) {
    294             SyncSampleBox stss = new SyncSampleBox();
    295             stss.setSampleNumber(syncSamples);
    296             stbl.addBox(stss);
    297         }
    298 
    299         if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) {
    300             SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox();
    301             sdtp.setEntries(track.getSampleDependencies());
    302             stbl.addBox(sdtp);
    303         }
    304         HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>();
    305         for (Track current : movie.getTracks()) {
    306             track2ChunkSizes.put(current, getChunkSizes(current, movie));
    307         }
    308         int[] tracksChunkSizes = track2ChunkSizes.get(track);
    309 
    310         SampleToChunkBox stsc = new SampleToChunkBox();
    311         stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>());
    312         long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size
    313         for (int i = 0; i < tracksChunkSizes.length; i++) {
    314             // The sample description index references the sample description box
    315             // that describes the samples of this chunk. My Tracks cannot have more
    316             // than one sample description box. Therefore 1 is always right
    317             // the first chunk has the number '1'
    318             if (lastChunkSize != tracksChunkSizes[i]) {
    319                 stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1));
    320                 lastChunkSize = tracksChunkSizes[i];
    321             }
    322         }
    323         stbl.addBox(stsc);
    324 
    325         SampleSizeBox stsz = new SampleSizeBox();
    326         stsz.setSampleSizes(track2SampleSizes.get(track));
    327 
    328         stbl.addBox(stsz);
    329         // The ChunkOffsetBox we create here is just a stub
    330         // since we haven't created the whole structure we can't tell where the
    331         // first chunk starts (mdat box). So I just let the chunk offset
    332         // start at zero and I will add the mdat offset later.
    333         StaticChunkOffsetBox stco = new StaticChunkOffsetBox();
    334         this.chunkOffsetBoxes.add(stco);
    335         long offset = 0;
    336         long[] chunkOffset = new long[tracksChunkSizes.length];
    337         // all tracks have the same number of chunks
    338         if (LOG.isLoggable(Level.FINE)) {
    339             LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId());
    340         }
    341 
    342 
    343         for (int i = 0; i < tracksChunkSizes.length; i++) {
    344             // The filelayout will be:
    345             // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n
    346             // calculating the offsets
    347             if (LOG.isLoggable(Level.FINER)) {
    348                 LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i);
    349             }
    350             for (Track current : movie.getTracks()) {
    351                 if (LOG.isLoggable(Level.FINEST)) {
    352                     LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId());
    353                 }
    354                 int[] chunkSizes = track2ChunkSizes.get(current);
    355                 long firstSampleOfChunk = 0;
    356                 for (int j = 0; j < i; j++) {
    357                     firstSampleOfChunk += chunkSizes[j];
    358                 }
    359                 if (current == track) {
    360                     chunkOffset[i] = offset;
    361                 }
    362                 for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
    363                     offset += track2SampleSizes.get(current)[j];
    364                 }
    365             }
    366         }
    367         stco.setChunkOffsets(chunkOffset);
    368         stbl.addBox(stco);
    369         minf.addBox(stbl);
    370         mdia.addBox(minf);
    371 
    372         return trackBox;
    373     }
    374 
    375     private class InterleaveChunkMdat implements Box {
    376         List<Track> tracks;
    377         List<ByteBuffer> samples = new ArrayList<ByteBuffer>();
    378         ContainerBox parent;
    379 
    380         long contentSize = 0;
    381 
    382         public ContainerBox getParent() {
    383             return parent;
    384         }
    385 
    386         public void setParent(ContainerBox parent) {
    387             this.parent = parent;
    388         }
    389 
    390         public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
    391         }
    392 
    393         private InterleaveChunkMdat(Movie movie) {
    394 
    395             tracks = movie.getTracks();
    396             Map<Track, int[]> chunks = new HashMap<Track, int[]>();
    397             for (Track track : movie.getTracks()) {
    398                 chunks.put(track, getChunkSizes(track, movie));
    399             }
    400 
    401             for (int i = 0; i < chunks.values().iterator().next().length; i++) {
    402                 for (Track track : tracks) {
    403 
    404                     int[] chunkSizes = chunks.get(track);
    405                     long firstSampleOfChunk = 0;
    406                     for (int j = 0; j < i; j++) {
    407                         firstSampleOfChunk += chunkSizes[j];
    408                     }
    409 
    410                     for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
    411 
    412                         ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j);
    413                         contentSize += s.limit();
    414                         samples.add((ByteBuffer) s.rewind());
    415                     }
    416 
    417                 }
    418 
    419             }
    420 
    421         }
    422 
    423         public long getDataOffset() {
    424             Box b = this;
    425             long offset = 16;
    426             while (b.getParent() != null) {
    427                 for (Box box : b.getParent().getBoxes()) {
    428                     if (b == box) {
    429                         break;
    430                     }
    431                     offset += box.getSize();
    432                 }
    433                 b = b.getParent();
    434             }
    435             return offset;
    436         }
    437 
    438 
    439         public String getType() {
    440             return "mdat";
    441         }
    442 
    443         public long getSize() {
    444             return 16 + contentSize;
    445         }
    446 
    447         private boolean isSmallBox(long contentSize) {
    448             return (contentSize + 8) < 4294967296L;
    449         }
    450 
    451 
    452         public void getBox(WritableByteChannel writableByteChannel) throws IOException {
    453             ByteBuffer bb = ByteBuffer.allocate(16);
    454             long size = getSize();
    455             if (isSmallBox(size)) {
    456                 IsoTypeWriter.writeUInt32(bb, size);
    457             } else {
    458                 IsoTypeWriter.writeUInt32(bb, 1);
    459             }
    460             bb.put(IsoFile.fourCCtoBytes("mdat"));
    461             if (isSmallBox(size)) {
    462                 bb.put(new byte[8]);
    463             } else {
    464                 IsoTypeWriter.writeUInt64(bb, size);
    465             }
    466             bb.rewind();
    467             writableByteChannel.write(bb);
    468             if (writableByteChannel instanceof GatheringByteChannel) {
    469                 List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples);
    470 
    471 
    472                 for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) {
    473                     List<ByteBuffer> sublist = nuSamples.subList(
    474                             i * STEPSIZE, // start
    475                             (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end
    476                     ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]);
    477                     do {
    478                         ((GatheringByteChannel) writableByteChannel).write(sampleArray);
    479                     } while (sampleArray[sampleArray.length - 1].remaining() > 0);
    480                 }
    481                 //System.err.println(bytesWritten);
    482             } else {
    483                 for (ByteBuffer sample : samples) {
    484                     sample.rewind();
    485                     writableByteChannel.write(sample);
    486                 }
    487             }
    488         }
    489 
    490     }
    491 
    492     /**
    493      * Gets the chunk sizes for the given track.
    494      *
    495      * @param track
    496      * @param movie
    497      * @return
    498      */
    499     int[] getChunkSizes(Track track, Movie movie) {
    500 
    501         long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie);
    502         int[] chunkSizes = new int[referenceChunkStarts.length];
    503 
    504 
    505         for (int i = 0; i < referenceChunkStarts.length; i++) {
    506             long start = referenceChunkStarts[i] - 1;
    507             long end;
    508             if (referenceChunkStarts.length == i + 1) {
    509                 end = track.getSamples().size();
    510             } else {
    511                 end = referenceChunkStarts[i + 1] - 1;
    512             }
    513 
    514             chunkSizes[i] = l2i(end - start);
    515             // The Stretch makes sure that there are as much audio and video chunks!
    516         }
    517         assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal";
    518         return chunkSizes;
    519 
    520 
    521     }
    522 
    523 
    524     private static long sum(int[] ls) {
    525         long rc = 0;
    526         for (long l : ls) {
    527             rc += l;
    528         }
    529         return rc;
    530     }
    531 
    532     protected static long getDuration(Track track) {
    533         long duration = 0;
    534         for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
    535             duration += entry.getCount() * entry.getDelta();
    536         }
    537         return duration;
    538     }
    539 
    540     public long getTimescale(Movie movie) {
    541         long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale();
    542         for (Track track : movie.getTracks()) {
    543             timescale = gcd(track.getTrackMetaData().getTimescale(), timescale);
    544         }
    545         return timescale;
    546     }
    547 
    548     public static long gcd(long a, long b) {
    549         if (b == 0) {
    550             return a;
    551         }
    552         return gcd(b, a % b);
    553     }
    554 
    555     public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) {
    556         ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size());
    557         for (ByteBuffer buffer : samples) {
    558             int lastIndex = nuSamples.size() - 1;
    559             if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() &&
    560                     nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) {
    561                 ByteBuffer oldBuffer = nuSamples.remove(lastIndex);
    562                 ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice();
    563                 // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset.
    564                 nuSamples.add(nu);
    565             } else if (lastIndex >= 0 &&
    566                     buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer &&
    567                     nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) {
    568                 // This can go wrong - but will it?
    569                 ByteBuffer oldBuffer = nuSamples.get(lastIndex);
    570                 oldBuffer.limit(buffer.limit() + oldBuffer.limit());
    571             } else {
    572                 nuSamples.add(buffer);
    573             }
    574         }
    575         return nuSamples;
    576     }
    577 }
    578