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}