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 java.awt.image.BufferedImage;
023
024import com.avpkit.core.Global;
025import com.avpkit.core.IContainer;
026import com.avpkit.core.IPacket;
027import com.avpkit.core.IPixelFormat;
028import com.avpkit.core.IStream;
029import com.avpkit.core.IStreamCoder;
030import com.avpkit.core.ICodec;
031import com.avpkit.core.IVideoPicture;
032import com.avpkit.core.IVideoResampler;
033import com.avpkit.core.Utils;
034
035/**
036 * Takes a media container, finds the first video stream,
037 * decodes that stream, and then displays the video frames,
038 * at the frame-rate specified by the container, on a 
039 * window.
040 * @author aclarke
041 *
042 */
043public class DecodeAndPlayVideo
044{
045
046  /**
047   * Takes a media container (file) as the first argument, opens it,
048   * opens up a Swing window and displays
049   * video frames with <i>roughly</i> the right timing.
050   *  
051   * @param args Must contain one string which represents a filename
052   */
053  @SuppressWarnings("deprecation")
054  public static void main(String[] args)
055  {
056    if (args.length <= 0)
057      throw new IllegalArgumentException("must pass in a filename" +
058                " as the first argument");
059
060    String filename = args[0];
061
062    // Let's make sure that we can actually convert video pixel formats.
063    if (!IVideoResampler.isSupported(
064        IVideoResampler.Feature.FEATURE_COLORSPACECONVERSION))
065      throw new RuntimeException("you must install the GPL version" +
066                " of AVPKit (with IVideoResampler support) for " +
067                "this demo to work");
068
069    // Create a AVPKit container object
070    IContainer container = IContainer.make();
071
072    // Open up the container
073    if (container.open(filename, IContainer.Type.READ, null) < 0)
074      throw new IllegalArgumentException("could not open file: " + filename);
075
076    // query how many streams the call to open found
077    int numStreams = container.getNumStreams();
078
079    // and iterate through the streams to find the first video stream
080    int videoStreamId = -1;
081    IStreamCoder videoCoder = null;
082    for(int i = 0; i < numStreams; i++)
083    {
084      // Find the stream object
085      IStream stream = container.getStream(i);
086      // Get the pre-configured decoder that can decode this stream;
087      IStreamCoder coder = stream.getStreamCoder();
088
089      if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO)
090      {
091        videoStreamId = i;
092        videoCoder = coder;
093        break;
094      }
095    }
096    if (videoStreamId == -1)
097      throw new RuntimeException("could not find video stream in container: "
098          +filename);
099
100    /*
101     * Now we have found the video stream in this file.  Let's open up our decoder so it can
102     * do work.
103     */
104    if (videoCoder.open() < 0)
105      throw new RuntimeException("could not open video decoder for container: "
106          +filename);
107
108    IVideoResampler resampler = null;
109    if (videoCoder.getPixelType() != IPixelFormat.Type.BGR24)
110    {
111      // if this stream is not in BGR24, we're going to need to
112      // convert it.  The VideoResampler does that for us.
113      resampler = IVideoResampler.make(videoCoder.getWidth(), 
114          videoCoder.getHeight(), IPixelFormat.Type.BGR24,
115          videoCoder.getWidth(), videoCoder.getHeight(), videoCoder.getPixelType());
116      if (resampler == null)
117        throw new RuntimeException("could not create color space " +
118                        "resampler for: " + filename);
119    }
120    /*
121     * And once we have that, we draw a window on screen
122     */
123    openJavaWindow();
124
125    /*
126     * Now, we start walking through the container looking at each packet.
127     */
128    IPacket packet = IPacket.make();
129    long firstTimestampInStream = Global.NO_PTS;
130    long systemClockStartTime = 0;
131    while(container.readNextPacket(packet) >= 0)
132    {
133      /*
134       * Now we have a packet, let's see if it belongs to our video stream
135       */
136      if (packet.getStreamIndex() == videoStreamId)
137      {
138        /*
139         * We allocate a new picture to get the data out of AVPKit
140         */
141        IVideoPicture picture = IVideoPicture.make(videoCoder.getPixelType(),
142            videoCoder.getWidth(), videoCoder.getHeight());
143
144        int offset = 0;
145        while(offset < packet.getSize())
146        {
147          /*
148           * Now, we decode the video, checking for any errors.
149           * 
150           */
151          int bytesDecoded = videoCoder.decodeVideo(picture, packet, offset);
152          if (bytesDecoded < 0)
153            throw new RuntimeException("got error decoding video in: "
154                + filename);
155          offset += bytesDecoded;
156
157          /*
158           * Some decoders will consume data in a packet, but will not be able to construct
159           * a full video picture yet.  Therefore you should always check if you
160           * got a complete picture from the decoder
161           */
162          if (picture.isComplete())
163          {
164            IVideoPicture newPic = picture;
165            /*
166             * If the resampler is not null, that means we didn't get the
167             * video in BGR24 format and
168             * need to convert it into BGR24 format.
169             */
170            if (resampler != null)
171            {
172              // we must resample
173              newPic = IVideoPicture.make(resampler.getOutputPixelFormat(),
174                  picture.getWidth(), picture.getHeight());
175              if (resampler.resample(newPic, picture) < 0)
176                throw new RuntimeException("could not resample video from: "
177                    + filename);
178            }
179            if (newPic.getPixelType() != IPixelFormat.Type.BGR24)
180              throw new RuntimeException("could not decode video" +
181                        " as BGR 24 bit data in: " + filename);
182
183            /**
184             * We could just display the images as quickly as we decode them,
185             * but it turns out we can decode a lot faster than you think.
186             * 
187             * So instead, the following code does a poor-man's version of
188             * trying to match up the frame-rate requested for each
189             * IVideoPicture with the system clock time on your computer.
190             * 
191             * Remember that all AVPKit IAudioSamples and IVideoPicture objects
192             * always give timestamps in Microseconds, relative to the first
193             * decoded item. If instead you used the packet timestamps, they can
194             * be in different units depending on your IContainer, and IStream
195             * and things can get hairy quickly.
196             */
197            if (firstTimestampInStream == Global.NO_PTS)
198            {
199              // This is our first time through
200              firstTimestampInStream = picture.getTimeStamp();
201              // get the starting clock time so we can hold up frames
202              // until the right time.
203              systemClockStartTime = System.currentTimeMillis();
204            } else {
205              long systemClockCurrentTime = System.currentTimeMillis();
206              long millisecondsClockTimeSinceStartofVideo =
207                systemClockCurrentTime - systemClockStartTime;
208              // compute how long for this frame since the first frame in the
209              // stream.
210              // remember that IVideoPicture and IAudioSamples timestamps are
211              // always in MICROSECONDS,
212              // so we divide by 1000 to get milliseconds.
213              long millisecondsStreamTimeSinceStartOfVideo =
214                (picture.getTimeStamp() - firstTimestampInStream)/1000;
215              final long millisecondsTolerance = 50; // and we give ourselfs 50 ms of tolerance
216              final long millisecondsToSleep = 
217                (millisecondsStreamTimeSinceStartOfVideo -
218                  (millisecondsClockTimeSinceStartofVideo +
219                      millisecondsTolerance));
220              if (millisecondsToSleep > 0)
221              {
222                try
223                {
224                  Thread.sleep(millisecondsToSleep);
225                }
226                catch (InterruptedException e)
227                {
228                  // we might get this when the user closes the dialog box, so
229                  // just return from the method.
230                  return;
231                }
232              }
233            }
234
235            // And finally, convert the BGR24 to an Java buffered image
236            BufferedImage javaImage = Utils.videoPictureToImage(newPic);
237
238            // and display it on the Java Swing window
239            updateJavaWindow(javaImage);
240          }
241        }
242      }
243      else
244      {
245        /*
246         * This packet isn't part of our video stream, so we just
247         * silently drop it.
248         */
249        do {} while(false);
250      }
251
252    }
253    /*
254     * Technically since we're exiting anyway, these will be cleaned up by 
255     * the garbage collector... but because we're nice people and want
256     * to be invited places for Christmas, we're going to show how to clean up.
257     */
258    if (videoCoder != null)
259    {
260      videoCoder.close();
261      videoCoder = null;
262    }
263    if (container !=null)
264    {
265      container.close();
266      container = null;
267    }
268    closeJavaWindow();
269
270  }
271
272  /**
273   * The window we'll draw the video on.
274   * 
275   */
276  private static VideoImage mScreen = null;
277
278  private static void updateJavaWindow(BufferedImage javaImage)
279  {
280    mScreen.setImage(javaImage);
281  }
282
283  /**
284   * Opens a Swing window on screen.
285   */
286  private static void openJavaWindow()
287  {
288    mScreen = new VideoImage();
289  }
290
291  /**
292   * Forces the swing thread to terminate; I'm sure there is a right
293   * way to do this in swing, but this works too.
294   */
295  private static void closeJavaWindow()
296  {
297    System.exit(0);
298  }
299}