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;
021
022import java.util.concurrent.TimeUnit;
023import java.util.concurrent.atomic.AtomicReference;
024
025import com.avpkit.ferry.IBuffer;
026import com.avpkit.ferry.JNIReference;
027import com.avpkit.core.Global;
028import com.avpkit.core.IAudioSamples;
029import com.avpkit.core.IVideoPicture;
030import com.avpkit.core.IPixelFormat;
031import com.avpkit.core.ITimeValue;
032import com.avpkit.core.video.IConverter;
033import com.avpkit.core.video.ConverterFactory;
034
035import java.awt.image.BufferedImage;
036
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * A collection of useful utilities for creating blank {@link IVideoPicture} objects
042 * and managing audio time stamp to sample conversions.
043 * 
044 * @author aclarke
045 *
046 */
047public class Utils
048{
049  static private final Logger log = LoggerFactory.getLogger(Utils.class);
050  static {
051    log.trace("Utils loaded");
052  }
053
054  /**
055   * Get a new blank frame object encoded in {@link IPixelFormat.Type#YUV420P} format.
056   * 
057   * If crossHatchW and crossHatchH are greater than zero, then we'll draw a cross-hatch
058   * pattern with boxes of those sizes alternating on the screen.  This can be useful to
059   * spot coding and translation errors.  Word of warning though -- due to how the YUV420P
060   * colorspace works, you'll need to make sure crossHatchW and crossHatchH are even numbers
061   * to avoid unslightly u/v lines at box edges.
062   * 
063   * @param w width of object
064   * @param h height of object
065   * @param yColor Y component of background color.
066   * @param uColor U component of background color.
067   * @param vColor V component of background color.
068   * @param pts The time stamp, in microseconds, you want this frame to have, or {@link Global#NO_PTS} for none.
069   * @param crossHatchW width of cross hatch on image, or <=0 for none
070   * @param crossHatchH height of cross hatch on image, or <= 0 for none
071   * @param crossHatchYColor Y component of cross-hatch color.
072   * @param crossHatchUColor U component of cross-hatch color.
073   * @param crossHatchVColor V component of cross-hatch color.
074   * @return A new frame, or null if we can't create it.
075   */
076  public static IVideoPicture getBlankFrame(
077      int w,
078      int h, 
079      int yColor,
080      int uColor,
081      int vColor,
082      long pts,
083      int crossHatchW,
084      int crossHatchH,
085      int crossHatchYColor,
086      int crossHatchUColor,
087      int crossHatchVColor)
088  {
089    IVideoPicture frame = IVideoPicture.make(IPixelFormat.Type.YUV420P, w, h);
090    if (frame == null)
091      throw new OutOfMemoryError("could not allocate frame");
092    
093    if (frame != null)
094    {
095      AtomicReference<JNIReference> ref = new AtomicReference<JNIReference>(
096          null);
097      IBuffer data = null;
098      try
099      {
100        data = frame.getData();
101        int bufSize = frame.getSize();
102        java.nio.ByteBuffer buffer = data.getByteBuffer(0, bufSize, ref);
103        if (buffer != null)
104        {
105          // we have the raw data; now we set it to the specified YUV value.
106          int lineLength = 0;
107          int offset = 0;
108
109          // first let's check the L
110          offset = 0;
111          lineLength = frame.getDataLineSize(0);
112          int sliceLen = lineLength * h;
113          for (int i = offset; i < offset + sliceLen; i++)
114          {
115            int x = (i - offset) % lineLength;
116            int y = (i - offset) / lineLength;
117            if (crossHatchH > 0
118                && crossHatchW > 0
119                && (((x / crossHatchW) % 2 == 1 && (y / crossHatchH) % 2 == 0) || ((x / crossHatchW) % 2 == 0 && (y / crossHatchH) % 2 == 1)))
120              buffer.put(i, (byte) crossHatchYColor);
121            else
122              buffer.put(i, (byte) yColor);
123          }
124
125          // now, check the U value
126          offset = (frame.getDataLineSize(0) * h);
127          lineLength = frame.getDataLineSize(1);
128          sliceLen = lineLength * ((h + 1) / 2);
129          for (int i = offset; i < offset + sliceLen; i++)
130          {
131            if (crossHatchH > 0 && crossHatchW > 0)
132            {
133              // put x and y in bottom right of pixel range
134              int x = ((i - offset) % lineLength) * 2;
135              int y = ((i - offset) / lineLength) * 2;
136
137              int[] xCoords = new int[]
138              {
139                  x, x + 1, x, x + 1
140              };
141              int[] yCoords = new int[]
142              {
143                  y, y, y + 1, y + 1
144              };
145              int finalColor = 0;
146              for (int j = 0; j < xCoords.length; j++)
147              {
148                int color = uColor;
149                x = xCoords[j];
150                y = yCoords[j];
151                if (((x / crossHatchW) % 2 == 1 && (y / crossHatchH) % 2 == 0)
152                    || ((x / crossHatchW) % 2 == 0 && (y / crossHatchH) % 2 == 1))
153                {
154                  color = crossHatchUColor;
155                }
156                finalColor += color;
157              }
158              finalColor /= xCoords.length;
159              buffer.put(i, (byte) finalColor);
160            }
161            else
162              buffer.put(i, (byte) uColor);
163          }
164
165          // and finally the V
166          offset = (frame.getDataLineSize(0) * h)
167              + (frame.getDataLineSize(1) * ((h + 1) / 2));
168          lineLength = frame.getDataLineSize(2);
169          sliceLen = lineLength * ((h + 1) / 2);
170          for (int i = offset; i < offset + sliceLen; i++)
171          {
172            if (crossHatchH > 0 && crossHatchW > 0)
173            {
174              // put x and y in bottom right of pixel range
175              int x = ((i - offset) % lineLength) * 2;
176              int y = ((i - offset) / lineLength) * 2;
177
178              int[] xCoords = new int[]
179              {
180                  x, x + 1, x, x + 1
181              };
182              int[] yCoords = new int[]
183              {
184                  y, y, y + 1, y + 1
185              };
186              int finalColor = 0;
187              for (int j = 0; j < xCoords.length; j++)
188              {
189                int color = vColor;
190                x = xCoords[j];
191                y = yCoords[j];
192                if (((x / crossHatchW) % 2 == 1 && (y / crossHatchH) % 2 == 0)
193                    || ((x / crossHatchW) % 2 == 0 && (y / crossHatchH) % 2 == 1))
194                  color = crossHatchVColor;
195                finalColor += color;
196              }
197              finalColor /= xCoords.length;
198              buffer.put(i, (byte) finalColor);
199            }
200            else
201              buffer.put(i, (byte) vColor);
202          }
203        }
204        // set it complete
205        frame.setComplete(true, IPixelFormat.Type.YUV420P, w, h, pts);
206      }
207      finally
208      {
209        if (data != null)
210          data.delete();
211        if (ref.get() != null)
212          ref.get().delete();
213      }
214
215    }
216    return frame;
217  }
218  
219  /**
220   * Get a new blank frame object encoded in {@link IPixelFormat.Type#YUV420P} format.
221   * 
222   * @param w width of object
223   * @param h height of object
224   * @param y Y component of background color.
225   * @param u U component of background color.
226   * @param v V component of background color.
227   * @param pts The time stamp, in microseconds, you want this frame to have, or {@link Global#NO_PTS} for none.
228   * @return A new frame, or null if we can't create it.
229   */
230  public static IVideoPicture getBlankFrame(int w, int h, int y, int u, int v, long pts)
231  {
232    return getBlankFrame(w, h, y, u, v, pts, 0, 0, 0, 0, 0);
233  }
234  
235  /**
236   * Returns a blank frame with a green-screen background.
237   * 
238   * @see #getBlankFrame(int, int, int, int, int, long)
239   * 
240   * @param w width in pixels
241   * @param h height in pixels 
242   * @param pts presentation timestamp (in {@link TimeUnit#MICROSECONDS} you want set
243   * @return a new blank frame
244   */
245  public static IVideoPicture getBlankFrame(int w, int h, int pts)
246  {
247    return getBlankFrame(w, h, 0, 0, 0, pts);
248  }
249
250  /**
251   * For a given sample rate, returns how long it would take to play a number of samples.
252   * @param numSamples The number of samples you want to find the duration for
253   * @param sampleRate The sample rate in Hz
254   * @return The duration it would take to play numSamples of sampleRate audio
255   */
256  public static ITimeValue samplesToTimeValue(long numSamples, int sampleRate)
257  {
258    if (sampleRate <= 0)
259      throw new IllegalArgumentException("sampleRate must be greater than zero");
260    
261    return ITimeValue.make(
262        IAudioSamples.samplesToDefaultPts(numSamples, sampleRate),
263        ITimeValue.Unit.MICROSECONDS);
264  }
265  
266  /**
267   * For a given time duration and sample rate, return the number of samples it would take to fill.
268   * @param duration duration
269   * @param sampleRate sample rate of audio
270   * @return number of samples required to fill that duration
271   */
272  public static long timeValueToSamples(ITimeValue duration, int sampleRate)
273  {
274    if (duration == null)
275      throw new IllegalArgumentException("must pass in a valid duration");
276    if (sampleRate <= 0)
277      throw new IllegalArgumentException("sampleRate must be greater than zero");
278    return IAudioSamples.defaultPtsToSamples(duration.get(ITimeValue.Unit.MICROSECONDS), sampleRate); 
279  }
280  
281  /**
282   * Convert an {@link IVideoPicture} to a {@link BufferedImage}.  The
283   * input picture should be of type {@link IPixelFormat.Type#BGR24}
284   * to avoid making unnecessary copies.
285   *
286   * The image data ultimately resides in java memory space, which
287   * means the caller does not need to concern themselves with memory
288   * management issues.
289   *
290   * @param picture The {@link IVideoPicture} to be converted.
291   * 
292   * @return the resultant {@link BufferedImage} which will contain
293   * the video frame.
294   * 
295   * @throws IllegalArgumentException if the passed {@link
296   * IVideoPicture} is NULL;
297   * @throws IllegalArgumentException if the passed {@link
298   * IVideoPicture} is not complete.
299   *
300   * @deprecated Image and picture conversion functionality has been
301   * replaced by {@link com.avpkit.core.video.ConverterFactory}.  The
302   * current implementation of {@link #videoPictureToImage} creates a new
303   * {@link com.avpkit.core.video.IConverter} on each call, which is
304   * not very efficient.
305   */
306  
307  @Deprecated
308    public static BufferedImage videoPictureToImage(IVideoPicture picture)
309  {
310    // if the pictre is NULL, throw up
311    
312    if (picture == null)
313      throw new IllegalArgumentException("The video picture is NULL.");
314
315    // create the converter
316
317    IConverter converter = ConverterFactory.createConverter(
318      ConverterFactory.XUGGLER_BGR_24, picture);
319
320    // return the conveter
321
322    return converter.toImage(picture);
323  }
324  
325  /**
326   * Convert a {@link BufferedImage} to an {@link IVideoPicture} of
327   * type {@link IPixelFormat.Type#BGR24}.  The {@link BufferedImage} must be a 
328   * {@link BufferedImage#TYPE_3BYTE_BGR} type.
329   *
330   * @param image The source {@link BufferedImage}.
331   * @param pts The presentation time stamp of the picture.
332   *
333   * @return An {@link IVideoPicture} in {@link
334   * IPixelFormat.Type#BGR24} format.
335   *
336   * @throws IllegalArgumentException if the passed {@link
337   * BufferedImage} is NULL;
338   * @throws IllegalArgumentException if the passed {@link
339   * BufferedImage} is not of type {@link BufferedImage#TYPE_3BYTE_BGR}.
340   * @throws IllegalArgumentException if the underlying data buffer of
341   * the {@link BufferedImage} is composed of types other bytes or
342   * integers.
343   *
344   * @deprecated Image and picture conversion functionality has been
345   * replaced by {@link com.avpkit.core.video.ConverterFactory}.  The
346   * current implementation of {@link #imageToVideoPicture} creates a new
347   * {@link com.avpkit.core.video.IConverter} on each call, which is
348   * not very efficient.
349   */
350  
351  @Deprecated
352    public static IVideoPicture imageToVideoPicture(BufferedImage image, long pts)
353  {
354    // if the image is NULL, throw up
355    
356    if (image == null)
357      throw new IllegalArgumentException("The image is NULL.");
358
359    if (image.getType() != BufferedImage.TYPE_3BYTE_BGR)
360      throw new IllegalArgumentException(
361        "The image is of type #" + image.getType() +
362        " but is required to be BufferedImage.TYPE_3BYTE_BGR of type #" +
363        BufferedImage.TYPE_3BYTE_BGR + ".");
364
365    // create the converter
366
367    IConverter converter = ConverterFactory.createConverter(
368      image, IPixelFormat.Type.BGR24);
369
370    // return the converted picture
371
372    return converter.toPicture(image, pts);
373  }
374}