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.ICodec;
025import com.avpkit.core.IContainer;
026import com.avpkit.core.IContainerFormat;
027import com.avpkit.core.IError;
028import com.avpkit.core.IMetaData;
029import com.avpkit.core.IPacket;
030import com.avpkit.core.IPixelFormat;
031import com.avpkit.core.IStream;
032import com.avpkit.core.IStreamCoder;
033import com.avpkit.core.IVideoPicture;
034import com.avpkit.core.IVideoResampler;
035import com.avpkit.core.Utils;
036
037/**
038 * Takes a FFMPEG device driver name (ex: "video4linux2"), and a device name (ex: /dev/video0), and displays video
039 * from that device.  For example, a web camera.
040 * <p>
041 * For example, to play the default camera on these operating systems:
042 * <ul>
043 * <li>Microsoft Windows:<pre>java -cp %XUGGLE_HOME%\share\java\jars\avpkit-core.jar com.avpkit.core.demos.DisplayWebcamVideo vfwcap 0</pre></li>
044 * <li>Linux:<pre>java -cp $XUGGLE_HOME/share/java/jars/avpkit-core.jar com.avpkit.core.demos.DisplayWebcamVideo video4linux2 /dev/video0</pre></li>
045 * </ul>
046 * </p>
047 * @author aclarke
048 *
049 */
050public class DisplayWebcamVideo
051{
052  /**
053   * Takes a FFMPEG webcam driver name, and a device name, opens the
054   * webcam, and displays its video in a Swing window.
055   * <p>
056   * Examples of device formats are:
057   * </p>
058   * <table border="1">
059   * <thead>
060   *  <tr>
061   *  <td>OS</td>
062   *  <td>Driver Name</td>
063   *  <td>Sample Device Name</td>
064   *  </tr>
065   *  </thead>
066   *  <tbody>
067   *  <tr>
068   *  <td>Windows</td>
069   *  <td>vfwcap</td>
070   *  <td>0</td>
071   *  </tr>
072   *  <tr>
073   *  <td>Linux</td>
074   *  <td>video4linux2</td>
075   *  <td>/dev/video0</td>
076   *  </tr>
077   *  </tbody>
078   *  </table>
079   * 
080   * <p>
081   * Webcam support is very limited; you can't query what devices are
082   * available, nor can you query what their capabilities are without
083   * actually opening the device.  Sorry, but that's how FFMPEG rolls.
084   * </p>
085   * 
086   * @param args Must contain two strings: a FFMPEG driver name and a device name
087   *   (which is dependent on the FFMPEG driver).
088   */
089  @SuppressWarnings("deprecation")
090  public static void main(String[] args)
091  {
092    if (args.length != 2)
093      throw new IllegalArgumentException("must pass in driver and device name");
094
095    String driverName = args[0];
096    String deviceName=  args[1];
097
098    // Let's make sure that we can actually convert video pixel formats.
099    if (!IVideoResampler.isSupported(IVideoResampler.Feature.FEATURE_COLORSPACECONVERSION))
100      throw new RuntimeException("you must install the GPL version of AVPKit (with IVideoResampler support) for this demo to work");
101
102    // Create a AVPKit container object
103    IContainer container = IContainer.make();
104
105    // Tell AVPKit about the device format
106    IContainerFormat format = IContainerFormat.make();
107    if (format.setInputFormat(driverName) < 0)
108      throw new IllegalArgumentException("couldn't open webcam device: " + driverName);
109
110    // devices, unlike most files, need to have parameters set in order
111    // for AVPKit to know how to configure them, for a webcam, these
112    // parameters make sense
113
114    IMetaData params = IMetaData.make();
115    
116    params.setValue("framerate", "30/1");
117    params.setValue("video_size", "320x240");    
118
119    // Open up the container
120    int retval = container.open(deviceName, IContainer.Type.READ, format,
121        false, true, params, null);
122    if (retval < 0)
123    {
124      // This little trick converts the non friendly integer return value into
125      // a slightly more friendly object to get a human-readable error name
126      IError error = IError.make(retval);
127      throw new IllegalArgumentException("could not open file: " + deviceName + "; Error: " + error.getDescription());
128    }      
129
130    // query how many streams the call to open found
131    int numStreams = container.getNumStreams();
132
133    // and iterate through the streams to find the first video stream
134    int videoStreamId = -1;
135    IStreamCoder videoCoder = null;
136    for(int i = 0; i < numStreams; i++)
137    {
138      // Find the stream object
139      IStream stream = container.getStream(i);
140      // Get the pre-configured decoder that can decode this stream;
141      IStreamCoder coder = stream.getStreamCoder();
142
143      if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO)
144      {
145        videoStreamId = i;
146        videoCoder = coder;
147        break;
148      }
149    }
150    if (videoStreamId == -1)
151      throw new RuntimeException("could not find video stream in container: "+deviceName);
152
153    /*
154     * Now we have found the video stream in this file.  Let's open up our decoder so it can
155     * do work.
156     */
157    if (videoCoder.open() < 0)
158      throw new RuntimeException("could not open video decoder for container: "+deviceName);
159
160    IVideoResampler resampler = null;
161    if (videoCoder.getPixelType() != IPixelFormat.Type.BGR24)
162    {
163      // if this stream is not in BGR24, we're going to need to
164      // convert it.  The VideoResampler does that for us.
165      resampler = IVideoResampler.make(videoCoder.getWidth(), videoCoder.getHeight(), IPixelFormat.Type.BGR24,
166          videoCoder.getWidth(), videoCoder.getHeight(), videoCoder.getPixelType());
167      if (resampler == null)
168        throw new RuntimeException("could not create color space resampler for: " + deviceName);
169    }
170    /*
171     * And once we have that, we draw a window on screen
172     */
173    openJavaWindow();
174
175    /*
176     * Now, we start walking through the container looking at each packet.
177     */
178    IPacket packet = IPacket.make();
179    while(container.readNextPacket(packet) >= 0)
180    {
181      /*
182       * Now we have a packet, let's see if it belongs to our video stream
183       */
184      if (packet.getStreamIndex() == videoStreamId)
185      {
186        /*
187         * We allocate a new picture to get the data out of AVPKit
188         */
189        IVideoPicture picture = IVideoPicture.make(videoCoder.getPixelType(),
190            videoCoder.getWidth(), videoCoder.getHeight());
191
192        int offset = 0;
193        while(offset < packet.getSize())
194        {
195          /*
196           * Now, we decode the video, checking for any errors.
197           * 
198           */
199          int bytesDecoded = videoCoder.decodeVideo(picture, packet, offset);
200          if (bytesDecoded < 0)
201            throw new RuntimeException("got error decoding video in: " + deviceName);
202          offset += bytesDecoded;
203
204          /*
205           * Some decoders will consume data in a packet, but will not be able to construct
206           * a full video picture yet.  Therefore you should always check if you
207           * got a complete picture from the decoder
208           */
209          if (picture.isComplete())
210          {
211            IVideoPicture newPic = picture;
212            /*
213             * If the resampler is not null, that means we didn't get the video in BGR24 format and
214             * need to convert it into BGR24 format.
215             */
216            if (resampler != null)
217            {
218              // we must resample
219              newPic = IVideoPicture.make(resampler.getOutputPixelFormat(), picture.getWidth(), picture.getHeight());
220              if (resampler.resample(newPic, picture) < 0)
221                throw new RuntimeException("could not resample video from: " + deviceName);
222            }
223            if (newPic.getPixelType() != IPixelFormat.Type.BGR24)
224              throw new RuntimeException("could not decode video as BGR 24 bit data in: " + deviceName);
225
226            // Convert the BGR24 to an Java buffered image
227            BufferedImage javaImage = Utils.videoPictureToImage(newPic);
228
229            // and display it on the Java Swing window
230            updateJavaWindow(javaImage);
231          }
232        }
233      }
234      else
235      {
236        /*
237         * This packet isn't part of our video stream, so we just silently drop it.
238         */
239        do {} while(false);
240      }
241
242    }
243    /*
244     * Technically since we're exiting anyway, these will be cleaned up by 
245     * the garbage collector... but because we're nice people and want
246     * to be invited places for Christmas, we're going to show how to clean up.
247     */
248    if (videoCoder != null)
249    {
250      videoCoder.close();
251      videoCoder = null;
252    }
253    if (container !=null)
254    {
255      container.close();
256      container = null;
257    }
258    closeJavaWindow();
259
260  }
261
262  /**
263   * The window we'll draw the video on.
264   * 
265   */
266  private static VideoImage mScreen = null;
267
268  private static void updateJavaWindow(BufferedImage javaImage)
269  {
270    mScreen.setImage(javaImage);
271  }
272
273  /**
274   * Opens a Swing window on screen.
275   */
276  private static void openJavaWindow()
277  {
278    mScreen = new VideoImage();
279  }
280
281  /**
282   * Forces the swing thread to terminate; I'm sure there is a right
283   * way to do this in swing, but this works too.
284   */
285  private static void closeJavaWindow()
286  {
287    System.exit(0);
288  }
289
290}