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}