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}