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.core.demos; 021 022import javax.sound.sampled.AudioFormat; 023import javax.sound.sampled.AudioSystem; 024import javax.sound.sampled.DataLine; 025import javax.sound.sampled.LineUnavailableException; 026import javax.sound.sampled.SourceDataLine; 027 028import com.avpkit.core.Global; 029import com.avpkit.core.IAudioSamples; 030import com.avpkit.core.IContainer; 031import com.avpkit.core.IPacket; 032import com.avpkit.core.IPixelFormat; 033import com.avpkit.core.IStream; 034import com.avpkit.core.IStreamCoder; 035import com.avpkit.core.ICodec; 036import com.avpkit.core.IVideoPicture; 037import com.avpkit.core.IVideoResampler; 038import com.avpkit.core.Utils; 039 040/** 041 * Takes a media container, finds the first video stream, 042 * decodes that stream, and then plays the audio and video. 043 * 044 * This code does a VERY coarse job of matching time-stamps, and thus 045 * the audio and video will float in and out of slight sync. Getting 046 * time-stamps syncing-up with audio is very system dependent and left 047 * as an exercise for the reader. 048 * 049 * @author aclarke 050 * 051 */ 052public class DecodeAndPlayAudioAndVideo 053{ 054 055 /** 056 * The audio line we'll output sound to; it'll be the default audio device on your system if available 057 */ 058 private static SourceDataLine mLine; 059 060 /** 061 * The window we'll draw the video on. 062 * 063 */ 064 private static VideoImage mScreen = null; 065 066 private static long mSystemVideoClockStartTime; 067 068 private static long mFirstVideoTimestampInStream; 069 070 /** 071 * Takes a media container (file) as the first argument, opens it, 072 * plays audio as quickly as it can, and opens up a Swing window and displays 073 * video frames with <i>roughly</i> the right timing. 074 * 075 * @param args Must contain one string which represents a filename 076 */ 077 @SuppressWarnings("deprecation") 078 public static void main(String[] args) 079 { 080 if (args.length <= 0) 081 throw new IllegalArgumentException("must pass in a filename as the first argument"); 082 083 String filename = args[0]; 084 085 // Let's make sure that we can actually convert video pixel formats. 086 if (!IVideoResampler.isSupported(IVideoResampler.Feature.FEATURE_COLORSPACECONVERSION)) 087 throw new RuntimeException("you must install the GPL version of AVPKit (with IVideoResampler support) for this demo to work"); 088 089 // Create a AVPKit container object 090 IContainer container = IContainer.make(); 091 092 // Open up the container 093 if (container.open(filename, IContainer.Type.READ, null) < 0) 094 throw new IllegalArgumentException("could not open file: " + filename); 095 096 // query how many streams the call to open found 097 int numStreams = container.getNumStreams(); 098 099 // and iterate through the streams to find the first audio stream 100 int videoStreamId = -1; 101 IStreamCoder videoCoder = null; 102 int audioStreamId = -1; 103 IStreamCoder audioCoder = null; 104 for(int i = 0; i < numStreams; i++) 105 { 106 // Find the stream object 107 IStream stream = container.getStream(i); 108 // Get the pre-configured decoder that can decode this stream; 109 IStreamCoder coder = stream.getStreamCoder(); 110 111 if (videoStreamId == -1 && coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) 112 { 113 videoStreamId = i; 114 videoCoder = coder; 115 } 116 else if (audioStreamId == -1 && coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) 117 { 118 audioStreamId = i; 119 audioCoder = coder; 120 } 121 } 122 if (videoStreamId == -1 && audioStreamId == -1) 123 throw new RuntimeException("could not find audio or video stream in container: "+filename); 124 125 /* 126 * Check if we have a video stream in this file. If so let's open up our decoder so it can 127 * do work. 128 */ 129 IVideoResampler resampler = null; 130 if (videoCoder != null) 131 { 132 if(videoCoder.open() < 0) 133 throw new RuntimeException("could not open audio decoder for container: "+filename); 134 135 if (videoCoder.getPixelType() != IPixelFormat.Type.BGR24) 136 { 137 // if this stream is not in BGR24, we're going to need to 138 // convert it. The VideoResampler does that for us. 139 resampler = IVideoResampler.make(videoCoder.getWidth(), videoCoder.getHeight(), IPixelFormat.Type.BGR24, 140 videoCoder.getWidth(), videoCoder.getHeight(), videoCoder.getPixelType()); 141 if (resampler == null) 142 throw new RuntimeException("could not create color space resampler for: " + filename); 143 } 144 /* 145 * And once we have that, we draw a window on screen 146 */ 147 openJavaVideo(); 148 } 149 150 if (audioCoder != null) 151 { 152 if (audioCoder.open() < 0) 153 throw new RuntimeException("could not open audio decoder for container: "+filename); 154 155 /* 156 * And once we have that, we ask the Java Sound System to get itself ready. 157 */ 158 try 159 { 160 openJavaSound(audioCoder); 161 } 162 catch (LineUnavailableException ex) 163 { 164 throw new RuntimeException("unable to open sound device on your system when playing back container: "+filename); 165 } 166 } 167 168 169 /* 170 * Now, we start walking through the container looking at each packet. 171 */ 172 IPacket packet = IPacket.make(); 173 mFirstVideoTimestampInStream = Global.NO_PTS; 174 mSystemVideoClockStartTime = 0; 175 while(container.readNextPacket(packet) >= 0) 176 { 177 /* 178 * Now we have a packet, let's see if it belongs to our video stream 179 */ 180 if (packet.getStreamIndex() == videoStreamId) 181 { 182 /* 183 * We allocate a new picture to get the data out of AVPKit 184 */ 185 IVideoPicture picture = IVideoPicture.make(videoCoder.getPixelType(), 186 videoCoder.getWidth(), videoCoder.getHeight()); 187 188 /* 189 * Now, we decode the video, checking for any errors. 190 * 191 */ 192 int bytesDecoded = videoCoder.decodeVideo(picture, packet, 0); 193 if (bytesDecoded < 0) 194 throw new RuntimeException("got error decoding audio in: " + filename); 195 196 /* 197 * Some decoders will consume data in a packet, but will not be able to construct 198 * a full video picture yet. Therefore you should always check if you 199 * got a complete picture from the decoder 200 */ 201 if (picture.isComplete()) 202 { 203 IVideoPicture newPic = picture; 204 /* 205 * If the resampler is not null, that means we didn't get the video in BGR24 format and 206 * need to convert it into BGR24 format. 207 */ 208 if (resampler != null) 209 { 210 // we must resample 211 newPic = IVideoPicture.make(resampler.getOutputPixelFormat(), picture.getWidth(), picture.getHeight()); 212 if (resampler.resample(newPic, picture) < 0) 213 throw new RuntimeException("could not resample video from: " + filename); 214 } 215 if (newPic.getPixelType() != IPixelFormat.Type.BGR24) 216 throw new RuntimeException("could not decode video as BGR 24 bit data in: " + filename); 217 218 long delay = millisecondsUntilTimeToDisplay(newPic); 219 // if there is no audio stream; go ahead and hold up the main thread. We'll end 220 // up caching fewer video pictures in memory that way. 221 try 222 { 223 if (delay > 0) 224 Thread.sleep(delay); 225 } 226 catch (InterruptedException e) 227 { 228 return; 229 } 230 231 // And finally, convert the picture to an image and display it 232 233 mScreen.setImage(Utils.videoPictureToImage(newPic)); 234 } 235 } 236 else if (packet.getStreamIndex() == audioStreamId) 237 { 238 /* 239 * We allocate a set of samples with the same number of channels as the 240 * coder tells us is in this buffer. 241 * 242 * We also pass in a buffer size (1024 in our example), although AVPKit 243 * will probably allocate more space than just the 1024 (it's not important why). 244 */ 245 IAudioSamples samples = IAudioSamples.make(1024, audioCoder.getChannels()); 246 247 /* 248 * A packet can actually contain multiple sets of samples (or frames of samples 249 * in audio-decoding speak). So, we may need to call decode audio multiple 250 * times at different offsets in the packet's data. We capture that here. 251 */ 252 int offset = 0; 253 254 /* 255 * Keep going until we've processed all data 256 */ 257 while(offset < packet.getSize()) 258 { 259 int bytesDecoded = audioCoder.decodeAudio(samples, packet, offset); 260 if (bytesDecoded < 0) 261 throw new RuntimeException("got error decoding audio in: " + filename); 262 offset += bytesDecoded; 263 /* 264 * Some decoder will consume data in a packet, but will not be able to construct 265 * a full set of samples yet. Therefore you should always check if you 266 * got a complete set of samples from the decoder 267 */ 268 if (samples.isComplete()) 269 { 270 // note: this call will block if Java's sound buffers fill up, and we're 271 // okay with that. That's why we have the video "sleeping" occur 272 // on another thread. 273 playJavaSound(samples); 274 } 275 } 276 } 277 else 278 { 279 /* 280 * This packet isn't part of our video stream, so we just silently drop it. 281 */ 282 do {} while(false); 283 } 284 285 } 286 /* 287 * Technically since we're exiting anyway, these will be cleaned up by 288 * the garbage collector... but because we're nice people and want 289 * to be invited places for Christmas, we're going to show how to clean up. 290 */ 291 if (videoCoder != null) 292 { 293 videoCoder.close(); 294 videoCoder = null; 295 } 296 if (audioCoder != null) 297 { 298 audioCoder.close(); 299 audioCoder = null; 300 } 301 if (container !=null) 302 { 303 container.close(); 304 container = null; 305 } 306 closeJavaSound(); 307 closeJavaVideo(); 308 } 309 310 private static long millisecondsUntilTimeToDisplay(IVideoPicture picture) 311 { 312 /** 313 * We could just display the images as quickly as we decode them, but it turns 314 * out we can decode a lot faster than you think. 315 * 316 * So instead, the following code does a poor-man's version of trying to 317 * match up the frame-rate requested for each IVideoPicture with the system 318 * clock time on your computer. 319 * 320 * Remember that all AVPKit IAudioSamples and IVideoPicture objects always 321 * give timestamps in Microseconds, relative to the first decoded item. If 322 * instead you used the packet timestamps, they can be in different units depending 323 * on your IContainer, and IStream and things can get hairy quickly. 324 */ 325 long millisecondsToSleep = 0; 326 if (mFirstVideoTimestampInStream == Global.NO_PTS) 327 { 328 // This is our first time through 329 mFirstVideoTimestampInStream = picture.getTimeStamp(); 330 // get the starting clock time so we can hold up frames 331 // until the right time. 332 mSystemVideoClockStartTime = System.currentTimeMillis(); 333 millisecondsToSleep = 0; 334 } else { 335 long systemClockCurrentTime = System.currentTimeMillis(); 336 long millisecondsClockTimeSinceStartofVideo = systemClockCurrentTime - mSystemVideoClockStartTime; 337 // compute how long for this frame since the first frame in the stream. 338 // remember that IVideoPicture and IAudioSamples timestamps are always in MICROSECONDS, 339 // so we divide by 1000 to get milliseconds. 340 long millisecondsStreamTimeSinceStartOfVideo = (picture.getTimeStamp() - mFirstVideoTimestampInStream)/1000; 341 final long millisecondsTolerance = 50; // and we give ourselfs 50 ms of tolerance 342 millisecondsToSleep = (millisecondsStreamTimeSinceStartOfVideo - 343 (millisecondsClockTimeSinceStartofVideo+millisecondsTolerance)); 344 } 345 return millisecondsToSleep; 346 } 347 348 /** 349 * Opens a Swing window on screen. 350 */ 351 private static void openJavaVideo() 352 { 353 mScreen = new VideoImage(); 354 } 355 356 /** 357 * Forces the swing thread to terminate; I'm sure there is a right 358 * way to do this in swing, but this works too. 359 */ 360 private static void closeJavaVideo() 361 { 362 System.exit(0); 363 } 364 365 private static void openJavaSound(IStreamCoder aAudioCoder) throws LineUnavailableException 366 { 367 AudioFormat audioFormat = new AudioFormat(aAudioCoder.getSampleRate(), 368 (int)IAudioSamples.findSampleBitDepth(aAudioCoder.getSampleFormat()), 369 aAudioCoder.getChannels(), 370 true, /* core defaults to signed 16 bit samples */ 371 false); 372 DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); 373 mLine = (SourceDataLine) AudioSystem.getLine(info); 374 /** 375 * if that succeeded, try opening the line. 376 */ 377 mLine.open(audioFormat); 378 /** 379 * And if that succeed, start the line. 380 */ 381 mLine.start(); 382 383 384 } 385 386 private static void playJavaSound(IAudioSamples aSamples) 387 { 388 /** 389 * We're just going to dump all the samples into the line. 390 */ 391 byte[] rawBytes = aSamples.getData().getByteArray(0, aSamples.getSize()); 392 mLine.write(rawBytes, 0, aSamples.getSize()); 393 } 394 395 private static void closeJavaSound() 396 { 397 if (mLine != null) 398 { 399 /* 400 * Wait for the line to finish playing 401 */ 402 mLine.drain(); 403 /* 404 * Close the line. 405 */ 406 mLine.close(); 407 mLine=null; 408 } 409 } 410}