001/*******************************************************************************
002 * Copyright (c) 2024, 2026, Olivier Ayache.  All rights reserved.
003 *
004 * This file is part of AVPKit.
005 *
006 * AVPKit is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Lesser General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * AVPKit is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with AVPKit.  If not, see <http://www.gnu.org/licenses/>.
018 *******************************************************************************/
019
020package com.avpkit.mediatool.demos;
021
022import java.io.File;
023
024import com.avpkit.mediatool.IMediaReader;
025import com.avpkit.mediatool.IMediaViewer;
026import com.avpkit.mediatool.IMediaWriter;
027import com.avpkit.mediatool.MediaToolAdapter;
028import com.avpkit.mediatool.ToolFactory;
029import com.avpkit.mediatool.event.AudioSamplesEvent;
030import com.avpkit.mediatool.event.IAddStreamEvent;
031import com.avpkit.mediatool.event.IAudioSamplesEvent;
032import com.avpkit.mediatool.event.ICloseCoderEvent;
033import com.avpkit.mediatool.event.ICloseEvent;
034import com.avpkit.mediatool.event.IOpenCoderEvent;
035import com.avpkit.mediatool.event.IOpenEvent;
036import com.avpkit.mediatool.event.IVideoPictureEvent;
037import com.avpkit.mediatool.event.VideoPictureEvent;
038import com.avpkit.core.IAudioSamples;
039import com.avpkit.core.IVideoPicture;
040
041import static java.lang.System.out;
042import static java.lang.System.exit;
043
044/** 
045 * A very simple media transcoder which uses {@link IMediaReader}, {@link
046 * IMediaWriter} and {@link IMediaViewer}.
047 */
048
049public class ConcatenateAudioAndVideo
050{
051  /**
052   * Concatenate two files.
053   * 
054   * @param args 3 strings; an input file 1, input file 2, and an output file.
055   */
056  
057  public static void main(String[] args)
058  {
059    if (args.length < 3)
060    {
061      out.println("Concatent two files.  The destination " +
062        "format will be guessed from the file extention.");
063      out.println("");
064      out.println("   ConcatentateTwoFiles <source-file1> <source-file2> <destination-file>");
065      out.println("");
066      out.println(
067        "The destination type will be guess from the supplied file extsion.");
068      exit(0);
069    }
070
071    File source1 = new File(args[0]);
072    File source2 = new File(args[1]);
073    
074    if (!source1.exists())
075    {
076      out.println("Source file does not exist: " + source1);
077      exit(0);
078    }
079
080    if (!source2.exists())
081    {
082      out.println("Source file does not exist: " + source2);
083      exit(0);
084    }
085
086    concatenate(args[0], args[1], args[2]);
087  }
088
089  /**
090   * Concatenate two source files into one destination file.
091   * 
092   * @param sourceUrl1 the file which will appear first in the output
093   * @param sourceUrl2 the file which will appear second in the output
094   * @param destinationUrl the file which will be produced
095   */
096
097  public static void concatenate(String sourceUrl1, String sourceUrl2,
098    String destinationUrl)
099  {
100    out.printf("transcode %s + %s -> %s\n", sourceUrl1, sourceUrl2,
101      destinationUrl);
102
103    //////////////////////////////////////////////////////////////////////
104    //                                                                  //
105    // NOTE: be sure that the audio and video parameters match those of //
106    // your input media                                                 //
107    //                                                                  //
108    //////////////////////////////////////////////////////////////////////
109
110    // video parameters
111
112    final int videoStreamIndex = 0;
113    final int videoStreamId = 0;
114    final int width = 480;
115    final int height = 360;
116
117    // audio parameters
118
119    final int audioStreamIndex = 1;
120    final int audioStreamId = 0;
121    final int channelCount = 2;
122    final int sampleRate = 44100; // Hz
123
124    // create the first media reader
125
126    IMediaReader reader1 = ToolFactory.makeReader(sourceUrl1);
127
128    // create the second media reader
129
130    IMediaReader reader2 = ToolFactory.makeReader(sourceUrl2);
131
132    // create the media concatenator
133
134    MediaConcatenator concatenator = new MediaConcatenator(audioStreamIndex,
135      videoStreamIndex);
136
137    // concatenator listens to both readers
138
139    reader1.addListener(concatenator);
140    reader2.addListener(concatenator);
141
142    // create the media writer which listens to the concatenator
143
144    IMediaWriter writer = ToolFactory.makeWriter(destinationUrl);
145    concatenator.addListener(writer);
146
147    // add the video stream
148
149    writer.addVideoStream(videoStreamIndex, videoStreamId, width, height);
150
151    // add the audio stream
152
153    writer.addAudioStream(audioStreamIndex, audioStreamId, channelCount,
154      sampleRate);
155
156    // read packets from the first source file until done
157
158    while (reader1.readPacket() == null)
159      ;
160
161    // read packets from the second source file until done
162
163    while (reader2.readPacket() == null)
164      ;
165
166    // close the writer
167
168    writer.close();
169  }
170  
171  static class MediaConcatenator extends MediaToolAdapter
172  {
173    // the current offset
174    
175    private long mOffset = 0;
176    
177    // the next video timestamp
178    
179    private long mNextVideo = 0;
180    
181    // the next audio timestamp
182    
183    private long mNextAudio = 0;
184
185    // the index of the audio stream
186    
187    private final int mAudoStreamIndex;
188    
189    // the index of the video stream
190    
191    private final int mVideoStreamIndex;
192    
193    /**
194     * Create a concatenator.
195     * 
196     * @param audioStreamIndex index of audio stream
197     * @param videoStreamIndex index of video stream
198     */
199    
200    public MediaConcatenator(int audioStreamIndex, int videoStreamIndex)
201    {
202      mAudoStreamIndex = audioStreamIndex;
203      mVideoStreamIndex = videoStreamIndex;
204    }
205    
206    public void onAudioSamples(IAudioSamplesEvent event)
207    {
208      IAudioSamples samples = event.getAudioSamples();
209      
210      // set the new time stamp to the original plus the offset established
211      // for this media file
212
213      long newTimeStamp = samples.getTimeStamp() + mOffset;
214
215      // keep track of predicted time of the next audio samples, if the end
216      // of the media file is encountered, then the offset will be adjusted
217      // to this time.
218
219      mNextAudio = samples.getNextPts();
220
221      // set the new timestamp on audio samples
222
223      samples.setTimeStamp(newTimeStamp);
224
225      // create a new audio samples event with the one true audio stream
226      // index
227
228      super.onAudioSamples(new AudioSamplesEvent(this, samples,
229        mAudoStreamIndex));
230    }
231
232    public void onVideoPicture(IVideoPictureEvent event)
233    {
234      IVideoPicture picture = event.getMediaData();
235      long originalTimeStamp = picture.getTimeStamp();
236
237      // set the new time stamp to the original plus the offset established
238      // for this media file
239
240      long newTimeStamp = originalTimeStamp + mOffset;
241
242      // keep track of predicted time of the next video picture, if the end
243      // of the media file is encountered, then the offset will be adjusted
244      // to this this time.
245      //
246      // You'll note in the audio samples listener above we used
247      // a method called getNextPts().  Video pictures don't have
248      // a similar method because frame-rates can be variable, so
249      // we don't now.  The minimum thing we do know though (since
250      // all media containers require media to have monotonically
251      // increasing time stamps), is that the next video timestamp
252      // should be at least one tick ahead.  So, we fake it.
253      
254      mNextVideo = originalTimeStamp + 1;
255
256      // set the new timestamp on video samples
257
258      picture.setTimeStamp(newTimeStamp);
259
260      // create a new video picture event with the one true video stream
261      // index
262
263      super.onVideoPicture(new VideoPictureEvent(this, picture,
264        mVideoStreamIndex));
265    }
266    
267    public void onClose(ICloseEvent event)
268    {
269      // update the offset by the larger of the next expected audio or video
270      // frame time
271
272      mOffset = Math.max(mNextVideo, mNextAudio);
273
274      if (mNextAudio < mNextVideo)
275      {
276        // In this case we know that there is more video in the
277        // last file that we read than audio. Technically you
278        // should pad the audio in the output file with enough
279        // samples to fill that gap, as many media players (e.g.
280        // Quicktime, Microsoft Media Player, MPlayer) actually
281        // ignore audio time stamps and just play audio sequentially.
282        // If you don't pad, in those players it may look like
283        // audio and video is getting out of sync.
284
285        // However kiddies, this is demo code, so that code
286        // is left as an exercise for the readers. As a hint,
287        // see the IAudioSamples.defaultPtsToSamples(...) methods.
288      }
289    }
290
291    public void onAddStream(IAddStreamEvent event)
292    {
293      // overridden to ensure that add stream events are not passed down
294      // the tool chain to the writer, which could cause problems
295    }
296
297    public void onOpen(IOpenEvent event)
298    {
299      // overridden to ensure that open events are not passed down the tool
300      // chain to the writer, which could cause problems
301    }
302
303    public void onOpenCoder(IOpenCoderEvent event)
304    {
305      // overridden to ensure that open coder events are not passed down the
306      // tool chain to the writer, which could cause problems
307    }
308
309    public void onCloseCoder(ICloseCoderEvent event)
310    {
311      // overridden to ensure that close coder events are not passed down the
312      // tool chain to the writer, which could cause problems
313    }
314  }
315}