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;
021import java.awt.AWTException;
022import java.awt.Rectangle;
023import java.awt.Robot;
024import java.awt.Toolkit;
025import java.awt.image.BufferedImage;
026
027import com.avpkit.core.ICodec;
028import com.avpkit.core.IContainer;
029import com.avpkit.core.IPacket;
030import com.avpkit.core.IPixelFormat;
031import com.avpkit.core.IRational;
032import com.avpkit.core.IStream;
033import com.avpkit.core.IStreamCoder;
034import com.avpkit.core.IVideoPicture;
035import com.avpkit.core.video.ConverterFactory;
036import com.avpkit.core.video.IConverter;
037
038/**
039 * This demonstration application shows how to use AVPKit and Java to take
040 * snapshots of your screen and encode them in a video.
041 * 
042 * <p>
043 * You can find the original code <a href="http://flexingineer.blogspot.com/2009/05/encoding-video-from-series-of-images.html"
044 * > here </a>.
045 * </p>
046 * 
047 * @author Denis Zgonjanin
048 * @author aclarke
049 * 
050 */
051
052public class CaptureScreenToFile
053{
054  private final IContainer outContainer;
055  private final IStream outStream;
056  private final IStreamCoder outStreamCoder;
057
058  private final IRational frameRate;
059
060  private final Robot robot;
061  private final Toolkit toolkit;
062  private final Rectangle screenBounds;
063
064  private long firstTimeStamp=-1;
065
066  /**
067   * Takes up to one argument, which just gives a file name to encode as. We
068   * guess which video codec to use based on the filename.
069   * 
070   * @param args
071   *          The filename to write to.
072   */
073
074  public static void main(String[] args)
075  {
076    String outFile = null;
077    if (args.length < 1)
078    {
079      outFile = "output.mp4";
080    }
081    else if (args.length == 1)
082    {
083      outFile = args[0];
084    }
085    else if (args.length > 1)
086    {
087      System.err.println("Must pass in an output file name");
088      return;
089    }
090    try
091    {
092      CaptureScreenToFile videoEncoder = new CaptureScreenToFile(outFile);
093      int index = 0;
094      while (index < 15*videoEncoder.frameRate.getDouble())
095      {
096        System.out.println("encoded image");
097        videoEncoder.encodeImage(videoEncoder.takeSingleSnapshot());
098
099        try
100        {
101          // sleep for framerate milliseconds
102          Thread.sleep((long) (1000 / videoEncoder.frameRate.getDouble()));
103        }
104        catch (InterruptedException e)
105        {
106          e.printStackTrace(System.out);
107        }
108        index++;
109      }
110      videoEncoder.closeStreams();
111    }
112    catch (RuntimeException e)
113    {
114      System.err.println("we can't get permission to capture the screen");
115    }
116  }
117
118  /**
119   * Create the demonstration object with lots of defaults. Throws an exception
120   * if we can't get the screen or open the file.
121   * 
122   * @param outFile
123   *          File to write to.
124   */
125  public CaptureScreenToFile(String outFile)
126  {
127    try
128    {
129      robot = new Robot();
130    }
131    catch (AWTException e)
132    {
133      System.out.println(e.getMessage());
134      throw new RuntimeException(e);
135    }
136    toolkit = Toolkit.getDefaultToolkit();
137    screenBounds = new Rectangle(toolkit.getScreenSize());
138
139    // Change this to change the frame rate you record at
140    frameRate = IRational.make(3, 1);
141
142    outContainer = IContainer.make();
143
144    int retval = outContainer.open(outFile, IContainer.Type.WRITE, null);
145    if (retval < 0)
146      throw new RuntimeException("could not open output file");
147
148    ICodec codec = ICodec.guessEncodingCodec(null, null, outFile, null,
149        ICodec.Type.CODEC_TYPE_VIDEO);
150    if (codec == null)
151      throw new RuntimeException("could not guess a codec");
152
153    outStream = outContainer.addNewStream(codec);
154    outStreamCoder = outStream.getStreamCoder();
155
156    outStreamCoder.setNumPicturesInGroupOfPictures(30);
157    outStreamCoder.setCodec(codec);
158
159    outStreamCoder.setBitRate(25000);
160    outStreamCoder.setBitRateTolerance(9000);
161
162    int width = toolkit.getScreenSize().width;
163    int height = toolkit.getScreenSize().height;
164
165    outStreamCoder.setPixelType(IPixelFormat.Type.YUV420P);
166    outStreamCoder.setHeight(height);
167    outStreamCoder.setWidth(width);
168    outStreamCoder.setFlag(IStreamCoder.Flags.FLAG_QSCALE, true);
169    outStreamCoder.setGlobalQuality(0);
170
171    outStreamCoder.setFrameRate(frameRate);
172    outStreamCoder.setTimeBase(IRational.make(frameRate.getDenominator(),
173        frameRate.getNumerator()));
174
175    retval = outStreamCoder.open(null, null);
176    if (retval < 0)
177      throw new RuntimeException("could not open input decoder");
178    retval = outContainer.writeHeader();
179    if (retval < 0)
180      throw new RuntimeException("could not write file header");
181  }
182
183  /**
184   * Encode the given image to the file and increment our time stamp.
185   * 
186   * @param originalImage
187   *          an image of the screen.
188   */
189
190  public void encodeImage(BufferedImage originalImage)
191  {
192    BufferedImage worksWithAVPKitBufferedImage = convertToType(originalImage,
193        BufferedImage.TYPE_3BYTE_BGR);
194    IPacket packet = IPacket.make();
195
196    long now = System.currentTimeMillis();
197    if (firstTimeStamp == -1)
198      firstTimeStamp = now;
199    
200    IConverter converter = null;
201    try
202    {
203      converter = ConverterFactory.createConverter(
204          worksWithAVPKitBufferedImage, IPixelFormat.Type.YUV420P);
205    }
206    catch (UnsupportedOperationException e)
207    {
208      System.out.println(e.getMessage());
209      e.printStackTrace(System.out);
210    }
211
212    long timeStamp = (now - firstTimeStamp)*1000; // convert to microseconds
213    IVideoPicture outFrame = converter.toPicture(worksWithAVPKitBufferedImage,
214        timeStamp);
215
216    outFrame.setQuality(0);
217    int retval = outStreamCoder.encodeVideo(packet, outFrame, 0);
218    if (retval < 0)
219      throw new RuntimeException("could not encode video");
220    if (packet.isComplete())
221    {
222      retval = outContainer.writePacket(packet);
223      if (retval < 0)
224        throw new RuntimeException("could not save packet to container");
225    }
226
227  }
228
229  /**
230   * Close out the file we're currently working on.
231   */
232
233  public void closeStreams()
234  {
235    int retval = outContainer.writeTrailer();
236    if (retval < 0)
237      throw new RuntimeException("Could not write trailer to output file");
238  }
239
240  /**
241   * Use the AWT robot to take a snapshot of the current screen.
242   * 
243   * @return a picture of the desktop
244   */
245
246  public BufferedImage takeSingleSnapshot()
247  {
248    return robot.createScreenCapture(this.screenBounds);
249  }
250
251  /**
252   * Convert a {@link BufferedImage} of any type, to {@link BufferedImage} of a
253   * specified type. If the source image is the same type as the target type,
254   * then original image is returned, otherwise new image of the correct type is
255   * created and the content of the source image is copied into the new image.
256   * 
257   * @param sourceImage
258   *          the image to be converted
259   * @param targetType
260   *          the desired BufferedImage type
261   * 
262   * @return a BufferedImage of the specifed target type.
263   * 
264   * @see BufferedImage
265   */
266
267  public static BufferedImage convertToType(BufferedImage sourceImage,
268      int targetType)
269  {
270    BufferedImage image;
271
272    // if the source image is already the target type, return the source image
273
274    if (sourceImage.getType() == targetType)
275      image = sourceImage;
276
277    // otherwise create a new image of the target type and draw the new
278    // image
279
280    else
281    {
282      image = new BufferedImage(sourceImage.getWidth(),
283          sourceImage.getHeight(), targetType);
284      image.getGraphics().drawImage(sourceImage, 0, 0, null);
285    }
286
287    return image;
288  }
289
290}