diff --git a/doc/handbook/MonteMedia-Handbook.md b/doc/handbook/MonteMedia-Handbook.md index 6bb4496..b6801d0 100644 --- a/doc/handbook/MonteMedia-Handbook.md +++ b/doc/handbook/MonteMedia-Handbook.md @@ -63,25 +63,30 @@ To read video frames from a movie file, you have to perform the following steps: movie file. ```java - BufferedImage[]readMovie(File file)throws IOException{ - ArrayList frames=new ArrayList (); - MovieReader in=Registry.getInstance().getReader(file); - Format format=new Format(DataClassKey,BufferedImage.class); - int track=in.findTrack(0,new Format(MediaTypeKey,MediaType.VIDEO)); - Codec codec=Registry.getInstance().getCodec(in.getFormat(track),format); - try{ - Buffer inbuf=new Buffer();Buffer codecbuf=new Buffer(); - do{ - in.read(track,inbuf); - codec.process(inbuf,codecbuf); - if(!codecbuf.isFlag(BufferFlag.DISCARD)){ - frames.add(Images.cloneImage((BufferedImage)codecbuf.data)); +class HowToReadVideoFramesFromAMovieFile { + + BufferedImage[] readMovie(File file) throws IOException { + ArrayList frames = new ArrayList(); + MovieReader in = Registry.getInstance().getReader(file); + Format format = new Format(DataClassKey, BufferedImage.class); + int track = in.findTrack(0, new Format(MediaTypeKey, MediaType.VIDEO)); + Codec codec = Registry.getInstance().getCodec(in.getFormat(track), format); + try { + Buffer inbuf = new Buffer(); + Buffer codecbuf = new Buffer(); + do { + in.read(track, inbuf); + codec.process(inbuf, codecbuf); + if (!codecbuf.isFlag(BufferFlag.DISCARD)) { + frames.add(Images.cloneImage((BufferedImage) codecbuf.data)); + } + } while (!inbuf.isFlag(BufferFlag.END_OF_MEDIA)); + } finally { + in.close(); } - }while(!inbuf.isFlag(BufferFlag.END_OF_MEDIA)); - }finally{ - in.close(); - } - return frames.toArray(new BufferedImage[frames.size()]);} + return frames.toArray(new BufferedImage[frames.size()]); + } +} ``` ### Writing video frames into a movie file @@ -96,23 +101,27 @@ To write video frames from into movie file, you have to perform the following st The example code below shows how to write an array of BufferedImages into a movie file. ```java -void writeMovie(File file,BufferedImage[]frames)throws IOException{ - MovieWriter out=Registry.getInstance().getWriter(file); - Format format=new Format(MediaTypeKey,MediaType.VIDEO, // EncodingKey, ENCODING_AVI_MJPG, - FrameRateKey,new Rational(30,1),// - WidthKey,frames[0].getWidth(), // - HeightKey,frames[0].getHeight(),// DepthKey, 24 +class HowToWriteVideoFramesIntoAMovieFile { + + void writeMovie(File file, BufferedImage[] frames) throws IOException { + MovieWriter out = Registry.getInstance().getWriter(file); + Format format = new Format(MediaTypeKey, MediaType.VIDEO, // EncodingKey, ENCODING_AVI_MJPG, + FrameRateKey, new Rational(30, 1),// + WidthKey, frames[0].getWidth(), // + HeightKey, frames[0].getHeight() ); - int track=out.addTrack(format); - try{ - Buffer buf=new Buffer(); - buf.format=new Format(DataClassKey,BufferedImage.class);buf.sampleDuration=format.get(FrameRateKey).inverse(); - for(int i=0;i queue; + private final Integer audioTrack; + private final Long startTime; + private Long totalSampleCount; + private ScheduledFuture future; + private Long sequenceNumber; + + private float audioLevelLeft = AudioSystem.NOT_SPECIFIED; + private float audioLevelRight = AudioSystem.NOT_SPECIFIED; + private final AtomicLong stopTime = new AtomicLong(Long.MAX_VALUE); + + public AudioGrabber(final Mixer mixer, final Format audioFormat, final int audioTrack, long startTime, BlockingQueue queue) + throws LineUnavailableException { + this.audioTrack = audioTrack; + this.queue = queue; + this.startTime = startTime; + + DataLine.Info lineInfo = new DataLine.Info(TargetDataLine.class, AudioFormatKeys.toAudioFormat(audioFormat)); + this.line = initializeAudioLine(mixer, lineInfo); + + configureAudioLine(); + } + + private TargetDataLine initializeAudioLine(Mixer mixer, DataLine.Info lineInfo) throws LineUnavailableException { + if (mixer != null) { + return (TargetDataLine) mixer.getLine(lineInfo); + } else { + return (TargetDataLine) AudioSystem.getLine(lineInfo); + } + } + + private void configureAudioLine() throws LineUnavailableException { + unmuteLine(); + setMinimumVolume(); + line.open(); + line.start(); + } + + private void unmuteLine() { + try {// Make sure the line is not muted + BooleanControl muteControl = (BooleanControl) line.getControl(BooleanControl.Type.MUTE); + muteControl.setValue(false); + } catch (IllegalArgumentException e) { + // We can't unmute the line from Java + } + } + + private void setMinimumVolume() { + try { // Make sure the volume of the line is bigger than 0.2 + FloatControl volumeControl = (FloatControl) line.getControl(FloatControl.Type.VOLUME); + volumeControl.setValue(Math.max(volumeControl.getValue(), 0.2f)); + } catch (IllegalArgumentException e) { + // We can't change the volume from Java + } + } + + public void setFuture(ScheduledFuture future) { + this.future = future; + } + + @Override + public void close() { + line.close(); + } + + public void setStopTime(long newValue) { + this.stopTime.set(newValue); + } + + public long getStopTime() { + return this.stopTime.get(); + } + + @Override + public void run() { + Buffer buf = new Buffer(); + AudioFormat lineFormat = line.getFormat(); + buf.format = fromAudioFormat(lineFormat).append(SilenceBugKey, true); + + int bufferSize = calculateBufferSize(lineFormat); + byte[] bdat = new byte[bufferSize]; + buf.data = bdat; + + int count = line.read(bdat, 0, bdat.length); + if (count > 0) { + processAudioData(buf, lineFormat, bdat, count); + } + } + + private int calculateBufferSize(AudioFormat lineFormat) { + // For even sample rates, we select a buffer size that can + // hold half a second of audio. This allows audio/video interleave + // twice a second, as recommended for AVI and QuickTime movies. + // For odd sample rates, we have to select a buffer size that can hold + // one second of audio. + int bufferSize = lineFormat.getFrameSize() * (int) lineFormat.getSampleRate(); + if (((int) lineFormat.getSampleRate() & 1) == 0) { + bufferSize /= 2; + } + return bufferSize; + } + + private void processAudioData(Buffer buf, AudioFormat lineFormat, byte[] bdat, Integer count) { + computeAudioLevel(bdat, count, lineFormat); + setBufferProperties(buf, lineFormat, count); + + if (isRecordingComplete(buf)) { + truncateBuffer(buf, lineFormat); + future.cancel(false); + } + + if (buf.sampleCount > 0) { + enqueueBuffer(buf); + } + + totalSampleCount += buf.sampleCount; + } + + private void setBufferProperties(Buffer buf, AudioFormat lineFormat, int count) { + Rational sampleRate = Rational.valueOf(lineFormat.getSampleRate()); + Rational frameRate = Rational.valueOf(lineFormat.getFrameRate()); + + buf.sampleCount = count / (lineFormat.getSampleSizeInBits() / 8 * lineFormat.getChannels()); + buf.sampleDuration = sampleRate.inverse(); + buf.offset = 0; + buf.sequenceNumber = sequenceNumber++; + buf.length = count; + buf.track = audioTrack; + buf.timeStamp = new Rational(totalSampleCount, 1).divide(frameRate); + } + + private boolean isRecordingComplete(Buffer buf) { + Rational stopTS = new Rational(getStopTime() - startTime, 1000); + return buf.timeStamp.add(buf.sampleDuration.multiply(buf.sampleCount)).compareTo(stopTS) > 0; + } + + private void truncateBuffer(Buffer buf, AudioFormat lineFormat) { + Rational stopTS = new Rational(getStopTime() - startTime, 1000); + buf.sampleCount = Math.max(0, (int) Math.ceil(stopTS.subtract(buf.timeStamp).divide(buf.sampleDuration).floatValue())); + buf.length = buf.sampleCount * (lineFormat.getSampleSizeInBits() / 8 * lineFormat.getChannels()); + } + + private void enqueueBuffer(Buffer buf) { + try { + queue.put(buf); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + /** + * Calculates the root-mean-square average of continuous samples. For + * four samples, the formula looks like this: + *
+     * rms = sqrt( (x0^2 + x1^2 + x2^2 + x3^2) / 4)
+     * 
Resources: + * http://www.jsresources.org/faq_audio.html#calculate_power + * + * @param data + * @param length + * @param format + */ + private void computeAudioLevel(byte[] data, Integer length, AudioFormat format) { + audioLevelLeft = audioLevelRight = AudioSystem.NOT_SPECIFIED; + if (format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED)) { + switch (format.getSampleSizeInBits()) { + case 8: + computeAudioLevel8Bit(data, length, format); + break; + case 16: + computeAudioLevel16Bit(data, length, format); + break; + } + } + } + + private void computeAudioLevel8Bit(byte[] data, Integer length, AudioFormat format) { + switch (format.getChannels()) { + case 1: + audioLevelLeft = computeAudioLevelSigned8(data, 0, length, format.getFrameSize()); + break; + case 2: + audioLevelLeft = computeAudioLevelSigned8(data, 0, length, format.getFrameSize()); + audioLevelRight = computeAudioLevelSigned8(data, 1, length, format.getFrameSize()); + break; + } + } + + private void computeAudioLevel16Bit(byte[] data, Integer length, AudioFormat format) { + if (format.isBigEndian()) { + switch (format.getChannels()) { + case 1: + audioLevelLeft = computeAudioLevelSigned16BE(data, 0, length, format.getFrameSize()); + break; + case 2: + audioLevelLeft = computeAudioLevelSigned16BE(data, 0, length, format.getFrameSize()); + audioLevelRight = computeAudioLevelSigned16BE(data, 2, length, format.getFrameSize()); + break; + } + } else { + switch (format.getChannels()) { + case 1: + audioLevelLeft = computeAudioLevelSigned16LE(data, 0, length, format.getFrameSize()); + break; + case 2: + audioLevelLeft = computeAudioLevelSigned16LE(data, 0, length, format.getFrameSize()); + audioLevelRight = computeAudioLevelSigned16LE(data, 2, length, format.getFrameSize()); + break; + } + } + } + + private float computeAudioLevelSigned16BE(byte[] data, int offset, int length, int stride) { + double sum = 0; + for (int i = offset; i < length; i += stride) { + int value = ((data[i]) << 8) | (data[i + 1] & 0xff); + sum += value * value; + } + double rms = Math.sqrt(sum / ((length - offset) / stride)); + return (float) (rms / 32768); + } + + private float computeAudioLevelSigned16LE(byte[] data, int offset, int length, int stride) { + double sum = 0; + for (int i = offset; i < length; i += stride) { + int value = ((data[i + 1]) << 8) | (data[i] & 0xff); + sum += value * value; + } + double rms = Math.sqrt(sum / ((length - offset) / stride)); + return (float) (rms / 32768); + } + + private float computeAudioLevelSigned8(byte[] data, int offset, int length, int stride) { + double sum = 0; + for (int i = offset; i < length; i += stride) { + int value = data[i]; + + // FIXME - The java audio system records silence as -128 instead of 0. + if (value != -128) { + sum += value * value; + } + } + double rms = Math.sqrt(sum / ((double) length / stride)); + return (float) (rms / 128); + } + + public float getAudioLevelLeft() { + return audioLevelLeft; + } + + public float getAudioLevelRight() { + return audioLevelRight; + } +} \ No newline at end of file diff --git a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/Main.java b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/Main.java index 2da1117..fbe6910 100755 --- a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/Main.java +++ b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/Main.java @@ -127,7 +127,7 @@ private class Handler implements ChangeListener { @Override public void stateChanged(ChangeEvent e) { ScreenRecorder r = screenRecorder; - if (r != null && r.getState() == ScreenRecorder.State.FAILED) { + if (r != null && r.getState() == State.FAILED) { recordingFailed(r.getStateMessage()); } } @@ -149,92 +149,6 @@ public void stateChanged(ChangeEvent e) { private Timer timer; private String infoLabelText; - private static class AudioItem { - - private String title; - private int sampleRate; - private int bitsPerSample; - - public AudioItem(String title, int sampleRate, int bitsPerSample) { - this.title = title; - this.sampleRate = sampleRate; - this.bitsPerSample = bitsPerSample; - } - - @Override - public String toString() { - return title; - } - } - - private static class AreaItem { - - private String title; - /** - * Area or null for entire screen. - */ - private Dimension inputDimension; - /** - * null if same value as input dimension. - */ - private Dimension outputDimension; - /** - * SwingConstants.CENTER, .NORTH_WEST, SOUTH_WEST. - */ - private int alignment; - private Point location; - - public AreaItem(String title, Dimension dim, int alignment) { - this(title, dim, null, alignment, new Point(0, 0)); - } - - public AreaItem(String title, Dimension inputDim, Dimension outputDim, int alignment, Point location) { - this.title = title; - this.inputDimension = inputDim; - this.outputDimension = outputDim; - this.alignment = alignment; - this.location = location; - } - - @Override - public String toString() { - return title; - } - - public Rectangle getBounds(GraphicsConfiguration cfg) { - Rectangle areaRect = null; - if (inputDimension != null) { - areaRect = new Rectangle(0, 0, inputDimension.width, inputDimension.height); - } - outputDimension = outputDimension; - Rectangle screenBounds = cfg.getBounds(); - if (areaRect == null) { - areaRect = (Rectangle) screenBounds.clone(); - } - switch (alignment) { - case SwingConstants.CENTER: - areaRect.x = screenBounds.x + (screenBounds.width - areaRect.width) / 2; - areaRect.y = screenBounds.y + (screenBounds.height - areaRect.height) / 2; - break; - case SwingConstants.NORTH_WEST: - areaRect.x = screenBounds.x; - areaRect.y = screenBounds.y; - break; - case SwingConstants.SOUTH_WEST: - areaRect.x = screenBounds.x; - areaRect.y = screenBounds.y + screenBounds.height - areaRect.height; - break; - default: - break; - } - areaRect.translate(location.x, location.y); - - areaRect = areaRect.intersection(screenBounds); - return areaRect; - - } - } - private static class AudioSourceItem { private String title; @@ -744,15 +658,15 @@ private void start() throws IOException, AWTException { mouseRate = 0; break; case 0: - crsr = ScreenRecorder.ENCODING_BLACK_CURSOR; + crsr = MouseConfigs.ENCODING_BLACK_CURSOR; mouseRate = 30; break; case 1: - crsr = ScreenRecorder.ENCODING_WHITE_CURSOR; + crsr = MouseConfigs.ENCODING_WHITE_CURSOR; mouseRate = 30; break; case 2: - crsr = ScreenRecorder.ENCODING_YELLOW_CURSOR; + crsr = MouseConfigs.ENCODING_YELLOW_CURSOR; mouseRate = 30; break; } @@ -872,10 +786,8 @@ protected void construct() throws Exception { @Override protected void finished() { - ScreenRecorder.State state = r.getState(); setSettingsEnabled(true); startStopButton.setEnabled(true); - //startStopButton.setText("Start"); timeLabel.setText("--:--"); } }.start(); diff --git a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/MouseConfigs.java b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/MouseConfigs.java new file mode 100644 index 0000000..e082147 --- /dev/null +++ b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/MouseConfigs.java @@ -0,0 +1,25 @@ +/* + * @(#)MouseConfigs.java + * Copyright © 2024 Werner Randelshofer, Switzerland. MIT License. + */ + +package org.monte.media.screenrecorder; + +/** + * Configuration options for recording the mouse cursor. + */ +public class MouseConfigs { + + /** + * "Encoding" for black mouse cursor. + */ + public final static String ENCODING_BLACK_CURSOR = "black"; + /** + * "Encoding" for white mouse cursor. + */ + public final static String ENCODING_WHITE_CURSOR = "white"; + /** + * "Encoding" for yellow mouse cursor. + */ + public final static String ENCODING_YELLOW_CURSOR = "yellow"; +} diff --git a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/MouseGrabber.java b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/MouseGrabber.java new file mode 100644 index 0000000..a921c6b --- /dev/null +++ b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/MouseGrabber.java @@ -0,0 +1,176 @@ +/* + * @(#)MouseGrabber.java + * Copyright © 2024 Werner Randelshofer, Switzerland. MIT License. + */ + +package org.monte.media.screenrecorder; + +import org.monte.media.av.Buffer; +import org.monte.media.av.Format; +import org.monte.media.math.Rational; + +import java.awt.*; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * MouseGrabber is responsible for capturing mouse movements and clicks + * during screen recording. It runs periodically to check for mouse state + * changes and enqueues these changes for processing by the {@link ScreenRecorder}. + */ +class MouseGrabber implements Runnable, AutoCloseable { + + /** + * Previously captured mouse location. This is used to coalesce mouse + * captures if the mouse has not been moved. + */ + private final Point prevCapturedMouseLocation = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); + private final ScheduledThreadPoolExecutor timer; + private final ScreenRecorder recorder; + private final GraphicsDevice captureDevice; + private final Rectangle captureArea; + private final BlockingQueue mouseCaptures; + private volatile long stopTime = Long.MAX_VALUE; + private final long startTime; + private final Format format; + private ScheduledFuture future; + private volatile boolean mousePressed; + private volatile boolean mouseWasPressed; + private volatile boolean mousePressedRecorded; + private final Rectangle cursorImageArea; + private final Point cursorOffset; + + /** + * Constructs a new MouseGrabber. + * + * @param recorder The ScreenRecorder instance + * @param startTime The start time of the recording + * @param timer The ScheduledThreadPoolExecutor for scheduling tasks + */ + public MouseGrabber(ScreenRecorder recorder, long startTime, ScheduledThreadPoolExecutor timer) { + this.timer = timer; + this.recorder = recorder; + this.format = recorder.mouseFormat; + this.startTime = startTime; + this.captureDevice = recorder.getCaptureDevice(); + this.captureArea = recorder.getCaptureArea(); + this.mouseCaptures = recorder.getMouseCaptures(); + this.cursorImageArea = new Rectangle(0, 0, recorder.getCursorImg().getWidth(), recorder.getCursorImg().getHeight()); + this.cursorOffset = recorder.getCursorOffset(); + } + + /** + * Sets the future for this MouseGrabber task. + * + * @param future The ScheduledFuture for this task + */ + public void setFuture(ScheduledFuture future) { + this.future = future; + } + + /** + * Sets the mouse pressed state. + * + * @param newValue The new mouse pressed state + */ + public void setMousePressed(boolean newValue) { + if (newValue) { + mouseWasPressed = true; + } + mousePressed = newValue; + } + + @Override + public void run() { + try { + grabMouse(); + } catch (Throwable ex) { + timer.shutdown(); + recorder.recordingFailed(ex); + } + } + + /** + * Sets the stop time for the mouse grabber. + * + * @param newValue The new stop time + */ + public void setStopTime(long newValue) { + this.stopTime = newValue; + } + + /** + * Gets the current stop time. + * + * @return The current stop time + */ + public long getStopTime() { + return this.stopTime; + } + + /** + * Captures the mouse cursor position and state. + */ + private void grabMouse() throws InterruptedException { + long now = System.currentTimeMillis(); + if (now > getStopTime()) { + future.cancel(false); + return; + } + + PointerInfo info = MouseInfo.getPointerInfo(); + Point currentMouseLocation = getCursorPosition(info); + + if (hasMouseStateChanged(currentMouseLocation)) { + enqueueMouse(currentMouseLocation, now); + updatePreviousState(currentMouseLocation); + } + + mouseWasPressed = mousePressed; + } + + private Point getCursorPosition(PointerInfo info) { + Point p = info.getLocation(); + cursorImageArea.setLocation(p.x + cursorOffset.x, p.y + cursorOffset.y); + + if (isCursorOutsideCaptureArea(info)) { + // If the cursor is outside the capture region, we + // assign Integer.MAX_VALUE to its location. + // This ensures that all mouse movements outside of the + // capture region get coalesced. + p.setLocation(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + return p; + } + + private boolean isCursorOutsideCaptureArea(PointerInfo info) { + return !info.getDevice().equals(captureDevice) + || !captureArea.intersects(cursorImageArea); + } + + private boolean hasMouseStateChanged(Point currentMouseLocation) { + return !currentMouseLocation.equals(prevCapturedMouseLocation) + || mouseWasPressed != mousePressedRecorded; + } + + private void enqueueMouse(Point currentMouseLocation, long timestamp) throws InterruptedException { + Buffer buf = new Buffer(); + buf.format = format; + buf.timeStamp = new Rational(timestamp, 1000); + buf.data = currentMouseLocation; + buf.header = mouseWasPressed; + mouseCaptures.put(buf); + } + + private void updatePreviousState(Point currentMouseLocation) { + prevCapturedMouseLocation.setLocation(currentMouseLocation); + mousePressedRecorded = mouseWasPressed; + } + + @Override + public void close() { + //! Implement any necessary cleanup here + } +} \ No newline at end of file diff --git a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenGrabber.java b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenGrabber.java new file mode 100644 index 0000000..fd79530 --- /dev/null +++ b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenGrabber.java @@ -0,0 +1,256 @@ +package org.monte.media.screenrecorder; + +import org.monte.media.av.Buffer; +import org.monte.media.av.Format; +import org.monte.media.av.FormatKeys; +import org.monte.media.color.Colors; +import org.monte.media.math.Rational; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ScheduledFuture; + +import static org.monte.media.av.FormatKeys.EncodingKey; +import static org.monte.media.av.FormatKeys.FrameRateKey; +import static org.monte.media.av.FormatKeys.MediaTypeKey; +import static org.monte.media.av.codec.video.VideoFormatKeys.DepthKey; +import static org.monte.media.av.codec.video.VideoFormatKeys.ENCODING_BUFFERED_IMAGE; + +/** + * ScreenGrabber is responsible for capturing screen updates. + * It runs periodically and enqueues these changes for processing by the {@link ScreenRecorder}. + */ +class ScreenGrabber implements Runnable, AutoCloseable { + + private final Point prevDrawnMouseLocation = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); + private volatile long stopTime = Long.MAX_VALUE; + private boolean prevMousePressed; + /** + * Holds the screen capture made with AWT Robot. + */ + private BufferedImage screenCapture; + private final ScreenRecorder recorder; + /** + * The AWT Robot which we use for capturing the screen. + */ + private final Robot robot; + private final Rectangle captureArea; + /** + * Holds the composed image (screen capture and super-imposed mouse + * cursor). This is the image that is written into the video track of + * the file. + */ + private final BufferedImage videoImg; + /** + * Graphics object for drawing into {@code videoImg}. + */ + private final Graphics2D videoGraphics; + private final Format mouseFormat; + /** + * Holds the mouse captures made with {@code MouseInfo}. + */ + private final ArrayBlockingQueue mouseCaptures; + /** + * The time the previous screen frame was captured. + */ + private Rational prevScreenCaptureTime; + private final BufferedImage cursorImg; + private final BufferedImage cursorImgPressed; + /** + * Previously draw mouse location. This is used to have the last mouse + * location at hand, when a new screen capture has been created, but the + * mouse has not been moved. + */ + private final Point cursorOffset; + private final int videoTrack; + private final long startTime; + private ScheduledFuture future; + private long sequenceNumber; + + public ScreenGrabber(ScreenRecorder recorder, long startTime) throws AWTException, IOException { + this.recorder = recorder; + this.captureArea = recorder.getCaptureArea(); + this.robot = new Robot(recorder.getCaptureDevice()); + this.mouseFormat = recorder.mouseFormat; + this.mouseCaptures = recorder.getMouseCaptures(); + this.cursorImg = recorder.getCursorImg(); + this.cursorImgPressed = recorder.getCursorImgPressed(); + this.cursorOffset = recorder.getCursorOffset(); + this.videoTrack = recorder.videoTrackId; + this.prevScreenCaptureTime = new Rational(startTime, 1000); + this.startTime = startTime; + + Format screenFormat = recorder.getScreenFormat(); + this.videoImg = createVideoImage(screenFormat); + this.videoGraphics = initializeVideoGraphics(); + } + + private BufferedImage createVideoImage(Format screenFormat) throws IOException { + int depth = screenFormat.get(DepthKey, 24); + return switch (depth) { + case 24 -> new BufferedImage(captureArea.width, captureArea.height, BufferedImage.TYPE_INT_RGB); + case 16 -> new BufferedImage(captureArea.width, captureArea.height, BufferedImage.TYPE_USHORT_555_RGB); + case 8 -> + new BufferedImage(captureArea.width, captureArea.height, BufferedImage.TYPE_BYTE_INDEXED, Colors.createMacColors()); + default -> throw new IOException("Unsupported color depth: " + depth); + }; + } + + private Graphics2D initializeVideoGraphics() { + Graphics2D graphics = videoImg.createGraphics(); + graphics.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); + graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); + return graphics; + } + + public void setFuture(ScheduledFuture future) { + this.future = future; + } + + public void setStopTime(long newValue) { + stopTime = newValue; + } + + public long getStopTime() { + return this.stopTime; + } + + @Override + public void run() { + try { + grabScreen(); + } catch (Throwable ex) { + ex.printStackTrace(); + recorder.recordingFailed(ex); + } + } + + /** + * Grabs a screen, generates video images with pending mouse captures + * and writes them into the movie file. + */ + private void grabScreen() throws IOException, InterruptedException { + BufferedImage previousScreenCapture = screenCapture; + long timeBeforeCapture = System.currentTimeMillis(); + + try { + screenCapture = robot.createScreenCapture(captureArea); + } catch (IllegalMonitorStateException e) { + // Log the error and return instead of throwing an exception + System.err.println("Screen capture failed: " + e.getMessage()); + return; + } + + long timeAfterCapture = System.currentTimeMillis(); + previousScreenCapture = (previousScreenCapture == null) ? screenCapture : previousScreenCapture; + videoGraphics.drawImage(previousScreenCapture, 0, 0, null); + + processMouseCaptures(timeBeforeCapture, timeAfterCapture, previousScreenCapture); + + if (timeBeforeCapture > getStopTime()) { + future.cancel(false); + } + } + + private void processMouseCaptures(long timeBeforeCapture, long timeAfterCapture, BufferedImage previousScreenCapture) throws IOException, InterruptedException { + Buffer buf = new Buffer(); + buf.format = new Format(MediaTypeKey, FormatKeys.MediaType.VIDEO, EncodingKey, ENCODING_BUFFERED_IMAGE); + + boolean hasMouseCapture = processMouseCapturesWithCursor(timeBeforeCapture, timeAfterCapture, previousScreenCapture, buf); + + if (!hasMouseCapture && shouldCaptureFrame(timeAfterCapture)) { + captureFrameWithoutMouseMovement(timeAfterCapture, buf); + } + } + + private boolean processMouseCapturesWithCursor(long timeBeforeCapture, long timeAfterCapture, BufferedImage previousScreenCapture, Buffer buf) throws IOException, InterruptedException { + boolean hasMouseCapture = false; + if (mouseFormat != null && mouseFormat.get(FrameRateKey).intValue() > 0) { + while (!mouseCaptures.isEmpty() && shouldProcessMouseCapture(timeAfterCapture)) { + Buffer mouseCapture = mouseCaptures.poll(); + if (isValidMouseCapture(mouseCapture, timeBeforeCapture)) { + hasMouseCapture = true; + processMouseCapture(Objects.requireNonNull(mouseCapture), previousScreenCapture, buf); + } + } + } + return hasMouseCapture; + } + + private boolean shouldProcessMouseCapture(long timeAfterCapture) { + return Objects.requireNonNull(mouseCaptures.peek()).timeStamp.compareTo(new Rational(timeAfterCapture, 1000)) < 0; + } + + private boolean isValidMouseCapture(Buffer mouseCapture, long timeBeforeCapture) { + return Objects.requireNonNull(mouseCapture).timeStamp.compareTo(prevScreenCaptureTime) > 0 + && mouseCapture.timeStamp.compareTo(new Rational(timeBeforeCapture, 1000)) < 0 + && mouseCapture.timeStamp.compareTo(new Rational(getStopTime(), 1000)) <= 0; + } + + private void processMouseCapture(Buffer mouseCapture, BufferedImage previousScreenCapture, Buffer buf) throws IOException, InterruptedException { + Point mcp = (Point) mouseCapture.data; + prevMousePressed = (Boolean) mouseCapture.header; + prevDrawnMouseLocation.setLocation(mcp.x - captureArea.x, mcp.y - captureArea.y); + Point p = prevDrawnMouseLocation; + + drawCursor(p); + writeBuffer(buf, mouseCapture.timeStamp, p); + eraseCursor(p, previousScreenCapture); + } + + private void drawCursor(Point p) { + BufferedImage cursorToDraw = prevMousePressed ? cursorImgPressed : cursorImg; + videoGraphics.drawImage(cursorToDraw, p.x + cursorOffset.x, p.y + cursorOffset.y, null); + } + + private void writeBuffer(Buffer buf, Rational timestamp, Point p) throws IOException, InterruptedException { + buf.clearFlags(); + buf.data = videoImg; + buf.sampleDuration = timestamp.subtract(prevScreenCaptureTime); + buf.timeStamp = prevScreenCaptureTime.subtract(new Rational(startTime, 1000)); + buf.track = videoTrack; + buf.sequenceNumber = sequenceNumber++; + buf.header = p.x == Integer.MAX_VALUE ? null : p; + recorder.write(buf); + prevScreenCaptureTime = timestamp; + } + + private void eraseCursor(Point p, BufferedImage previousScreenCapture) { + videoGraphics.drawImage(previousScreenCapture, + p.x + cursorOffset.x, p.y + cursorOffset.y, + p.x + cursorOffset.x + cursorImg.getWidth() - 1, p.y + cursorOffset.y + cursorImg.getHeight() - 1, + p.x + cursorOffset.x, p.y + cursorOffset.y, + p.x + cursorOffset.x + cursorImg.getWidth() - 1, p.y + cursorOffset.y + cursorImg.getHeight() - 1, + null); + } + + private boolean shouldCaptureFrame(long timeAfterCapture) { + return prevScreenCaptureTime.compareTo(new Rational(getStopTime(), 1000)) < 0; + } + + private void captureFrameWithoutMouseMovement(long timeAfterCapture, Buffer buf) throws IOException, InterruptedException { + Point p = prevDrawnMouseLocation; + drawCursor(p); + + buf.data = videoImg; + buf.sampleDuration = new Rational(timeAfterCapture, 1000).subtract(prevScreenCaptureTime); + buf.timeStamp = prevScreenCaptureTime.subtract(new Rational(startTime, 1000)); + buf.track = videoTrack; + buf.sequenceNumber = sequenceNumber++; + buf.header = p.x == Integer.MAX_VALUE ? null : p; + recorder.write(buf); + prevScreenCaptureTime = new Rational(timeAfterCapture, 1000); + + eraseCursor(p, screenCapture); + } + + @Override + public void close() { + videoGraphics.dispose(); + videoImg.flush(); + } +} \ No newline at end of file diff --git a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenRecorder.java b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenRecorder.java index 00426c1..0bab997 100755 --- a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenRecorder.java +++ b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/ScreenRecorder.java @@ -12,7 +12,6 @@ import org.monte.media.av.FormatKeys.MediaType; import org.monte.media.av.MovieWriter; import org.monte.media.av.Registry; -import org.monte.media.av.codec.audio.AudioFormatKeys; import org.monte.media.av.codec.video.ScaleImageCodec; import org.monte.media.avi.AVIWriter; import org.monte.media.beans.AbstractStateModel; @@ -21,14 +20,9 @@ import org.monte.media.math.Rational; import org.monte.media.quicktime.QuickTimeWriter; -import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.BooleanControl; -import javax.sound.sampled.DataLine; -import javax.sound.sampled.FloatControl; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.Mixer; -import javax.sound.sampled.TargetDataLine; import javax.swing.*; import java.awt.*; import java.awt.event.AWTEventListener; @@ -43,7 +37,6 @@ import java.util.Date; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; @@ -63,8 +56,6 @@ import static org.monte.media.av.codec.audio.AudioFormatKeys.SampleRateKey; import static org.monte.media.av.codec.audio.AudioFormatKeys.SampleSizeInBitsKey; import static org.monte.media.av.codec.audio.AudioFormatKeys.SignedKey; -import static org.monte.media.av.codec.audio.AudioFormatKeys.SilenceBugKey; -import static org.monte.media.av.codec.audio.AudioFormatKeys.fromAudioFormat; import static org.monte.media.av.codec.video.VideoFormatKeys.COMPRESSOR_NAME_QUICKTIME_ANIMATION; import static org.monte.media.av.codec.video.VideoFormatKeys.CompressorNameKey; import static org.monte.media.av.codec.video.VideoFormatKeys.DepthKey; @@ -86,36 +77,18 @@ *

* This recorder uses four threads. Three capture threads for screen, mouse * cursor and audio, and one output thread for the movie writer. - *

- * FIXME - This class is a horrible mess. * * @author Werner Randelshofer */ public class ScreenRecorder extends AbstractStateModel { - public enum State { - - DONE, FAILED, RECORDING, FAILING - } - private State state = State.DONE; private Throwable stateMessage = null; - /** - * "Encoding" for black mouse cursor. - */ - public final static String ENCODING_BLACK_CURSOR = "black"; - /** - * "Encoding" for white mouse cursor. - */ - public final static String ENCODING_WHITE_CURSOR = "white"; - /** - * "Encoding" for yellow mouse cursor. - */ - public final static String ENCODING_YELLOW_CURSOR = "yellow"; + /** * The file format. "AVI" or "QuickTime" */ - private Format fileFormat; + private final Format fileCodecFormat; /** * The input video format for cursor capture. "black" or "white". */ @@ -123,15 +96,15 @@ public enum State { /** * The input video format for screen capture. */ - private Format screenFormat; + private final Format screenFormat; /** * The input and output format for audio capture. */ - private Format audioFormat; + private final Format audioFormat; /** * The bounds of the graphics device that we capture with AWT Robot. */ - private Rectangle captureArea; + private final Rectangle captureArea; /** * The writer for the movie file. */ @@ -177,10 +150,7 @@ public enum State { * Hot spot of the mouse cursor in cursorImg. */ private Point cursorOffset; - /** - * Object for thread synchronization. - */ - private final Object sync = new Object(); + private ArrayBlockingQueue writerQueue; /** * This codec encodes a video frame. @@ -193,19 +163,15 @@ public enum State { */ private Rational outputTime; private Rational ffrDuration; - private ArrayList recordedFiles; - /** - * Id of the video track. - */ - protected int videoTrack = 0; - /** - * Id of the audio track. - */ - protected int audioTrack = 1; + private final ArrayList recordedFiles = new ArrayList<>(); + + protected int videoTrackId = 0; + + protected int audioTrackId = 1; /** * The device from which screen captures are generated. */ - private GraphicsDevice captureDevice; + private final GraphicsDevice captureDevice; private AudioGrabber audioGrabber; private ScreenGrabber screenGrabber; protected MouseGrabber mouseGrabber; @@ -231,168 +197,162 @@ public enum State { */ public ScreenRecorder(GraphicsConfiguration cfg) throws IOException, AWTException { this(cfg, null, - // the file format - new Format(MediaTypeKey, MediaType.FILE, - MimeTypeKey, MIME_QUICKTIME), - // - // the output format for screen capture - new Format(MediaTypeKey, MediaType.VIDEO, - EncodingKey, ENCODING_QUICKTIME_ANIMATION, - CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_ANIMATION, - DepthKey, 24, FrameRateKey, new Rational(15, 1)), - // - // the output format for mouse capture - new Format(MediaTypeKey, MediaType.VIDEO, - EncodingKey, ENCODING_BLACK_CURSOR, + new Format(MediaTypeKey, MediaType.FILE, MimeTypeKey, MIME_QUICKTIME), + new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_QUICKTIME_ANIMATION, CompressorNameKey, COMPRESSOR_NAME_QUICKTIME_ANIMATION, DepthKey, 24, + FrameRateKey, new Rational(15, 1)), + new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, MouseConfigs.ENCODING_BLACK_CURSOR, FrameRateKey, new Rational(30, 1)), - // - // the output format for audio capture - new Format(MediaTypeKey, MediaType.AUDIO, - EncodingKey, ENCODING_QUICKTIME_TWOS_PCM, - FrameRateKey, new Rational(48000, 1), - SampleSizeInBitsKey, 16, - ChannelsKey, 2, SampleRateKey, new Rational(48000, 1), - SignedKey, true, ByteOrderKey, ByteOrder.BIG_ENDIAN)); + new Format(MediaTypeKey, MediaType.AUDIO, EncodingKey, ENCODING_QUICKTIME_TWOS_PCM, + FrameRateKey, new Rational(48000, 1), SampleSizeInBitsKey, 16, ChannelsKey, 2, + SampleRateKey, new Rational(48000, 1), SignedKey, true, ByteOrderKey, ByteOrder.BIG_ENDIAN)); } /** - * Creates a screen recorder. + * Creates a screen recorder with custom formats. * - * @param cfg Graphics configuration of the capture screen. - * @param fileFormat The file format "AVI" or "QuickTime". - * @param screenFormat The video format for screen capture. - * @param mouseFormat The video format for mouse capture. The - * {@code EncodingKey} must be ENCODING_BLACK_CURSOR or - * ENCODING_WHITE_CURSOR. The {@code SampleRateKey} can be independent from - * the {@code screenFormat}. Specify null if you don't want to capture the - * mouse cursor. - * @param audioFormat The audio format for audio capture. Specify null if - * you don't want audio capture. + * @param cfg Graphics configuration of the capture screen. + * @param fileCodecFormat The file format "AVI" or "QuickTime". + * @param screenFormat The video format for screen capture. + * @param mouseFormat The video format for mouse capture. + * @param audioFormat The audio format for audio capture. */ - public ScreenRecorder(GraphicsConfiguration cfg, - Format fileFormat, - Format screenFormat, - Format mouseFormat, - Format audioFormat) throws IOException, AWTException { - this(cfg, null, fileFormat, screenFormat, mouseFormat, audioFormat); + public ScreenRecorder(GraphicsConfiguration cfg, Format fileCodecFormat, Format screenFormat, + Format mouseFormat, Format audioFormat) throws IOException, AWTException { + this(cfg, null, fileCodecFormat, screenFormat, mouseFormat, audioFormat); } /** - * Creates a screen recorder. + * Creates a screen recorder with a defined capture area. * - * @param cfg Graphics configuration of the capture screen. - * @param captureArea Defines the area of the screen that shall be captured. - * @param fileFormat The file format "AVI" or "QuickTime". - * @param screenFormat The video format for screen capture. - * @param mouseFormat The video format for mouse capture. The - * {@code EncodingKey} must be ENCODING_BLACK_CURSOR or - * ENCODING_WHITE_CURSOR. The {@code SampleRateKey} can be independent from - * the {@code screenFormat}. Specify null if you don't want to capture the - * mouse cursor. - * @param audioFormat The audio format for audio capture. Specify null if - * you don't want audio capture. + * @param cfg Graphics configuration of the capture screen. + * @param captureArea Defines the area of the screen that shall be captured. + * @param fileCodecFormat The file format "AVI" or "QuickTime". + * @param screenFormat The video format for screen capture. + * @param mouseFormat The video format for mouse capture. + * @param audioFormat The audio format for audio capture. */ - public ScreenRecorder(GraphicsConfiguration cfg, - Rectangle captureArea, - Format fileFormat, - Format screenFormat, - Format mouseFormat, - Format audioFormat) throws IOException, AWTException { - this(cfg, null, fileFormat, screenFormat, mouseFormat, audioFormat, null); + public ScreenRecorder(GraphicsConfiguration cfg, Rectangle captureArea, Format fileCodecFormat, + Format screenFormat, Format mouseFormat, Format audioFormat) + throws IOException, AWTException { + this(cfg, captureArea, fileCodecFormat, screenFormat, mouseFormat, audioFormat, null); } /** - * Creates a screen recorder. + * Creates a screen recorder with a defined capture area and movie folder. * - * @param cfg Graphics configuration of the capture screen. - * @param captureArea Defines the area of the screen that shall be captured. - * @param fileFormat The file format "AVI" or "QuickTime". - * @param screenFormat The video format for screen capture. - * @param mouseFormat The video format for mouse capture. The - * {@code EncodingKey} must be ENCODING_BLACK_CURSOR or - * ENCODING_WHITE_CURSOR. The {@code SampleRateKey} can be independent from - * the {@code screenFormat}. Specify null if you don't want to capture the - * mouse cursor. - * @param audioFormat The audio format for audio capture. Specify null if - * you don't want audio capture. - * @param movieFolder Where to store the movie - */ - public ScreenRecorder(GraphicsConfiguration cfg, - Rectangle captureArea, - Format fileFormat, - Format screenFormat, - Format mouseFormat, - Format audioFormat, + * @param cfg Graphics configuration of the capture screen. + * @param captureArea Defines the area of the screen that shall be captured. + * @param fileCodecFormat The file format "AVI" or "QuickTime". + * @param screenFormat The video format for screen capture. + * @param mouseFormat The video format for mouse capture. + * @param audioFormat The audio format for audio capture. + * @param movieFolder Where to store the movie. + */ + public ScreenRecorder(GraphicsConfiguration cfg, Rectangle captureArea, Format fileCodecFormat, + Format screenFormat, Format mouseFormat, Format audioFormat, File movieFolder) throws IOException, AWTException { - this.fileFormat = fileFormat; + this.fileCodecFormat = fileCodecFormat; this.screenFormat = screenFormat; - this.mouseFormat = mouseFormat; - if (this.mouseFormat == null) { - this.mouseFormat = new Format(FrameRateKey, new Rational(0, 0), EncodingKey, ENCODING_BLACK_CURSOR); - } + this.mouseFormat = mouseFormat != null ? mouseFormat : new Format(FrameRateKey, new Rational(0, 0), EncodingKey, MouseConfigs.ENCODING_BLACK_CURSOR); this.audioFormat = audioFormat; - this.recordedFiles = new ArrayList(); + this.captureDevice = cfg.getDevice(); - this.captureArea = (captureArea == null) ? cfg.getBounds() : captureArea; + this.captureArea = captureArea != null ? captureArea : cfg.getBounds(); + initializeMouseCapture(mouseFormat); + this.movieFolder = initializeMovieFolder(movieFolder); + } + + private void initializeMouseCapture(Format mouseFormat) throws IOException { if (mouseFormat != null && mouseFormat.get(FrameRateKey).intValue() > 0) { - mouseCaptures = new ArrayBlockingQueue(mouseFormat.get(FrameRateKey).intValue() * 2); - if (this.mouseFormat.get(EncodingKey).equals(ENCODING_BLACK_CURSOR)) { - cursorImg = Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/Cursor.black.png")); - cursorImgPressed = Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/Cursor.black.pressed.png")); - } else if (this.mouseFormat.get(EncodingKey).equals(ENCODING_YELLOW_CURSOR)) { - cursorImg = Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/Cursor.yellow.png")); - cursorImgPressed = Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/Cursor.yellow.pressed.png")); - } else { - cursorImg = Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/Cursor.white.png")); - cursorImgPressed = Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/Cursor.white.pressed.png")); + mouseCaptures = new ArrayBlockingQueue<>(mouseFormat.get(FrameRateKey).intValue() * 2); + switch (mouseFormat.get(EncodingKey)) { + case MouseConfigs.ENCODING_BLACK_CURSOR: + cursorImg = loadCursorImage("Cursor.black.png"); + cursorImgPressed = loadCursorImage("Cursor.black.pressed.png"); + break; + case MouseConfigs.ENCODING_YELLOW_CURSOR: + cursorImg = loadCursorImage("Cursor.yellow.png"); + cursorImgPressed = loadCursorImage("Cursor.yellow.pressed.png"); + break; + default: + cursorImg = loadCursorImage("Cursor.white.png"); + cursorImgPressed = loadCursorImage("Cursor.white.pressed.png"); + break; } cursorOffset = new Point(cursorImg.getWidth() / -2, cursorImg.getHeight() / -2); } - this.movieFolder = movieFolder; - if (this.movieFolder == null) { - if (System.getProperty("os.name").toLowerCase().startsWith("windows")) { - this.movieFolder = new File(System.getProperty("user.home") + File.separator + "Videos"); - } else { - this.movieFolder = new File(System.getProperty("user.home") + File.separator + "Movies"); - } - } + } + + public Format getScreenFormat() { + return screenFormat; + } + + public Point getCursorOffset() { + return cursorOffset; + } + + public BufferedImage getCursorImgPressed() { + return cursorImgPressed; + } + + public BufferedImage getCursorImg() { + return cursorImg; + } + + public ArrayBlockingQueue getMouseCaptures() { + return mouseCaptures; + } + + public GraphicsDevice getCaptureDevice() { + return captureDevice; + } + public Rectangle getCaptureArea() { + return captureArea; + } + + + private BufferedImage loadCursorImage(String imagePath) throws IOException { + return Images.toBufferedImage(Images.createImage(ScreenRecorder.class, "images/" + imagePath)); + } + + private File initializeMovieFolder(File movieFolder) { + if (movieFolder != null) { + return movieFolder; + } + String userHome = System.getProperty("user.home"); + String osName = System.getProperty("os.name").toLowerCase(); + String folderName = osName.startsWith("windows") ? "Videos" : "Movies"; + return new File(userHome + File.separator + folderName); } protected MovieWriter createMovieWriter() throws IOException { - File f = createMovieFile(fileFormat); + File f = createMovieFile(fileCodecFormat); recordedFiles.add(f); - MovieWriter mw = w = Registry.getInstance().getWriter(fileFormat, f); + MovieWriter mw = w = Registry.getInstance().getWriter(fileCodecFormat, f); if (w == null) { - throw new IOException("Error no writer found for file format: " + fileFormat + "."); + throw new IOException("Error no writer found for file format: " + fileCodecFormat + "."); } // Create the video encoder Rational videoRate = Rational.max(screenFormat.get(FrameRateKey), mouseFormat.get(FrameRateKey)); ffrDuration = videoRate.inverse(); - Format videoInputFormat = screenFormat.prepend(MediaTypeKey, MediaType.VIDEO, - EncodingKey, ENCODING_BUFFERED_IMAGE, - WidthKey, captureArea.width, - HeightKey, captureArea.height, - FrameRateKey, videoRate); - Format videoOutputFormat = screenFormat.prepend( - FrameRateKey, videoRate, - MimeTypeKey, fileFormat.get(MimeTypeKey))// - // - .append(// - WidthKey, captureArea.width, - HeightKey, captureArea.height); - - videoTrack = w.addTrack(videoOutputFormat); + Format videoInputFormat = screenFormat + .prepend(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_BUFFERED_IMAGE, WidthKey, captureArea.width, HeightKey, captureArea.height, FrameRateKey, videoRate); + Format videoOutputFormat = screenFormat + .prepend(FrameRateKey, videoRate, MimeTypeKey, fileCodecFormat.get(MimeTypeKey)) + .append(WidthKey, captureArea.width, HeightKey, captureArea.height); + + videoTrackId = w.addTrack(videoOutputFormat); if (audioFormat != null) { - audioTrack = w.addTrack(audioFormat); + audioTrackId = w.addTrack(audioFormat); } - Codec encoder = Registry.getInstance().getEncoder(w.getFormat(videoTrack)); + Codec encoder = Registry.getInstance().getEncoder(w.getFormat(videoTrackId)); if (encoder == null) { - throw new IOException("No encoder for format " + w.getFormat(videoTrack)); + throw new IOException("No encoder for format " + w.getFormat(videoTrackId)); } frameEncoder = encoder; frameEncoder.setInputFormat(videoInputFormat); @@ -404,8 +364,7 @@ protected MovieWriter createMovieWriter() throws IOException { // If the capture area does not have the same dimensions as the // video format, create a codec chain which scales the image before // performing the frame encoding. - if (!videoInputFormat.intersectKeys(WidthKey, HeightKey).matches( - videoOutputFormat.intersectKeys(WidthKey, HeightKey))) { + if (!videoInputFormat.intersectKeys(WidthKey, HeightKey).matches(videoOutputFormat.intersectKeys(WidthKey, HeightKey))) { ScaleImageCodec sic = new ScaleImageCodec(); sic.setInputFormat(videoInputFormat); sic.setOutputFormat(videoOutputFormat.intersectKeys(WidthKey, HeightKey).append(videoInputFormat)); @@ -416,7 +375,7 @@ protected MovieWriter createMovieWriter() throws IOException { if (screenFormat.get(DepthKey) == 8) { if (w instanceof AVIWriter) { AVIWriter aviw = (AVIWriter) w; - aviw.setPalette(videoTrack, Colors.createMacColors()); + aviw.setPalette(videoTrackId, Colors.createMacColors()); } else if (w instanceof QuickTimeWriter) { QuickTimeWriter qtw = (QuickTimeWriter) w; // do not set palette due to a bug @@ -444,9 +403,7 @@ public List getCreatedMovieFiles() { * You can override this method, if you would like to create a movie file at * a different location. * - * @param fileFormat * @return the file - * @throws IOException */ protected File createMovieFile(Format fileFormat) throws IOException { if (!movieFolder.exists()) { @@ -457,9 +414,7 @@ protected File createMovieFile(Format fileFormat) throws IOException { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd 'at' HH.mm.ss"); - File f = new File(movieFolder,// - "ScreenRecording " + dateFormat.format(new Date()) + "." + Registry.getInstance().getExtension(fileFormat)); - return f; + return new File(movieFolder, "ScreenRecording " + dateFormat.format(new Date()) + "." + Registry.getInstance().getExtension(fileFormat)); } /** @@ -568,233 +523,6 @@ private void abortScreenCapture() { } } - private static class ScreenGrabber implements Runnable { - - /** - * Previously draw mouse location. This is used to have the last mouse - * location at hand, when a new screen capture has been created, but the - * mouse has not been moved. - */ - private Point prevDrawnMouseLocation = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); - private boolean prevMousePressed = false; - /** - * Holds the screen capture made with AWT Robot. - */ - private BufferedImage screenCapture; - private ScreenRecorder recorder; - private ScheduledThreadPoolExecutor screenTimer; - /** - * The AWT Robot which we use for capturing the screen. - */ - private Robot robot; - private Rectangle captureArea; - /** - * Holds the composed image (screen capture and super-imposed mouse - * cursor). This is the image that is written into the video track of - * the file. - */ - private BufferedImage videoImg; - /** - * Graphics object for drawing into {@code videoImg}. - */ - private Graphics2D videoGraphics; - private final Format mouseFormat; - /** - * Holds the mouse captures made with {@code MouseInfo}. - */ - private ArrayBlockingQueue mouseCaptures; - /** - * The time the previous screen frame was captured. - */ - private Rational prevScreenCaptureTime; - private final Object sync; - private BufferedImage cursorImg, cursorImgPressed; - private Point cursorOffset; - private int videoTrack; - private long startTime; - private volatile long stopTime = Long.MAX_VALUE; - private ScheduledFuture future; - private long sequenceNumber; - - public void setFuture(ScheduledFuture future) { - this.future = future; - } - - public synchronized void setStopTime(long newValue) { - this.stopTime = newValue; - } - - public synchronized long getStopTime() { - return this.stopTime; - } - - public ScreenGrabber(ScreenRecorder recorder, long startTime) throws AWTException, IOException { - this.recorder = recorder; - this.captureArea = recorder.captureArea; - this.robot = new Robot(recorder.captureDevice); - this.mouseFormat = recorder.mouseFormat; - this.mouseCaptures = recorder.mouseCaptures; - this.sync = recorder.sync; - this.cursorImg = recorder.cursorImg; - this.cursorImgPressed = recorder.cursorImgPressed; - this.cursorOffset = recorder.cursorOffset; - this.videoTrack = recorder.videoTrack; - this.prevScreenCaptureTime = new Rational(startTime, 1000); - this.startTime = startTime; - - Format screenFormat = recorder.screenFormat; - if (screenFormat.get(DepthKey, 24) == 24) { - videoImg = new BufferedImage(this.captureArea.width, this.captureArea.height, BufferedImage.TYPE_INT_RGB); - } else if (screenFormat.get(DepthKey) == 16) { - videoImg = new BufferedImage(this.captureArea.width, this.captureArea.height, BufferedImage.TYPE_USHORT_555_RGB); - } else if (screenFormat.get(DepthKey) == 8) { - videoImg = new BufferedImage(this.captureArea.width, this.captureArea.height, BufferedImage.TYPE_BYTE_INDEXED, Colors.createMacColors()); - } else { - throw new IOException("Unsupported color depth " + screenFormat.get(DepthKey)); - } - videoGraphics = videoImg.createGraphics(); - videoGraphics.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); - videoGraphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); - videoGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); - } - - @Override - public void run() { - try { - grabScreen(); - } catch (Throwable ex) { - ex.printStackTrace(); - screenTimer.shutdown(); - recorder.recordingFailed(ex); - } - } - - /** - * Grabs a screen, generates video images with pending mouse captures - * and writes them into the movie file. - */ - private void grabScreen() throws IOException, InterruptedException { - // Capture the screen - BufferedImage previousScreenCapture = screenCapture; - long timeBeforeCapture = System.currentTimeMillis(); - try { - screenCapture = robot.createScreenCapture(captureArea); - } catch (IllegalMonitorStateException e) { - //IOException ioe= new IOException("Could not grab screen"); - //ioe.initCause(e); - //throw ioe; - // Screen capture failed due to a synchronization error - return; - } - long timeAfterCapture = System.currentTimeMillis(); - if (previousScreenCapture == null) { - previousScreenCapture = screenCapture; - } - videoGraphics.drawImage(previousScreenCapture, 0, 0, null); - - Buffer buf = new Buffer(); - buf.format = new Format(MediaTypeKey, MediaType.VIDEO, EncodingKey, ENCODING_BUFFERED_IMAGE); - // Generate video frames with mouse cursor painted on them - boolean hasMouseCapture = false; - if (mouseFormat != null && mouseFormat.get(FrameRateKey).intValue() > 0) { - while (!mouseCaptures.isEmpty() && mouseCaptures.peek().timeStamp.compareTo(new Rational(timeAfterCapture, 1000)) < 0) { - Buffer mouseCapture = mouseCaptures.poll(); - if (mouseCapture.timeStamp.compareTo(prevScreenCaptureTime) > 0) { - if (mouseCapture.timeStamp.compareTo(new Rational(timeBeforeCapture, 1000)) < 0) { - previousScreenCapture = screenCapture; - videoGraphics.drawImage(previousScreenCapture, 0, 0, null); - } - - Point mcp = (Point) mouseCapture.data; - prevMousePressed = (Boolean) mouseCapture.header; - prevDrawnMouseLocation.setLocation(mcp.x - captureArea.x, mcp.y - captureArea.y); - Point p = prevDrawnMouseLocation; - - long localStopTime = getStopTime(); - if (mouseCapture.timeStamp.compareTo(new Rational(localStopTime, 1000)) > 0) { - break; - } - { - hasMouseCapture = true; - - // draw cursor - if (prevMousePressed) { - videoGraphics.drawImage(cursorImgPressed, p.x + cursorOffset.x, p.y + cursorOffset.y, null); - } else { - videoGraphics.drawImage(cursorImg, p.x + cursorOffset.x, p.y + cursorOffset.y, null); - } - buf.clearFlags(); - buf.data = videoImg; - buf.sampleDuration = mouseCapture.timeStamp.subtract(prevScreenCaptureTime); - buf.timeStamp = prevScreenCaptureTime.subtract(new Rational(startTime, 1000)); - buf.track = videoTrack; - buf.sequenceNumber = sequenceNumber++; - - // Fudge mouse position into the header - buf.header = p.x == Integer.MAX_VALUE ? null : p; - recorder.write(buf); - prevScreenCaptureTime = mouseCapture.timeStamp; - - // erase cursor - videoGraphics.drawImage(previousScreenCapture, // - p.x + cursorOffset.x, p.y + cursorOffset.y,// - p.x + cursorOffset.x + cursorImg.getWidth() - 1, p.y + cursorOffset.y + cursorImg.getHeight() - 1,// - p.x + cursorOffset.x, p.y + cursorOffset.y,// - p.x + cursorOffset.x + cursorImg.getWidth() - 1, p.y + cursorOffset.y + cursorImg.getHeight() - 1,// - null); - } - - } - } - - if (!hasMouseCapture && prevScreenCaptureTime.compareTo(new Rational(getStopTime(), 1000)) < 0) { - Point p = prevDrawnMouseLocation; - if (p != null) { - if (prevMousePressed) { - videoGraphics.drawImage(cursorImgPressed, p.x + cursorOffset.x, p.y + cursorOffset.y, null); - } else { - videoGraphics.drawImage(cursorImg, p.x + cursorOffset.x, p.y + cursorOffset.y, null); - } - } - - buf.data = videoImg; - buf.sampleDuration = new Rational(timeAfterCapture, 1000).subtract(prevScreenCaptureTime); - buf.timeStamp = prevScreenCaptureTime.subtract(new Rational(startTime, 1000)); - buf.track = videoTrack; - buf.sequenceNumber = sequenceNumber++; - buf.header = p.x == Integer.MAX_VALUE ? null : p; - recorder.write(buf); - prevScreenCaptureTime = new Rational(timeAfterCapture, 1000); - if (p != null) {//erase cursor - videoGraphics.drawImage(previousScreenCapture, // - p.x + cursorOffset.x, p.y + cursorOffset.y,// - p.x + cursorOffset.x + cursorImg.getWidth() - 1, p.y + cursorOffset.y + cursorImg.getHeight() - 1,// - p.x + cursorOffset.x, p.y + cursorOffset.y,// - p.x + cursorOffset.x + cursorImg.getWidth() - 1, p.y + cursorOffset.y + cursorImg.getHeight() - 1,// - null); - } - } - } else if (prevScreenCaptureTime.compareTo(new Rational(getStopTime(), 1000)) < 0) { - buf.data = videoImg; - buf.sampleDuration = new Rational(timeAfterCapture, 1000).subtract(prevScreenCaptureTime); - buf.timeStamp = prevScreenCaptureTime.subtract(new Rational(startTime, 1000)); - buf.track = videoTrack; - buf.sequenceNumber = sequenceNumber++; - buf.header = null; // no mouse position has been recorded for this frame - recorder.write(buf); - prevScreenCaptureTime = new Rational(timeAfterCapture, 1000); - } - - if (timeBeforeCapture > getStopTime()) { - future.cancel(false); - } - } - - public void close() { - videoGraphics.dispose(); - videoImg.flush(); - } - } /** * Starts mouse capture. @@ -855,9 +583,7 @@ protected void waitUntilMouseCaptureStopped() throws InterruptedException { if (mouseCaptureTimer != null) { try { mouseFuture.get(); - } catch (InterruptedException ex) { - } catch (CancellationException ex) { - } catch (ExecutionException ex) { + } catch (InterruptedException | CancellationException | ExecutionException ignored) { } mouseCaptureTimer.shutdown(); mouseCaptureTimer.awaitTermination(5000, TimeUnit.MILLISECONDS); @@ -867,109 +593,6 @@ protected void waitUntilMouseCaptureStopped() throws InterruptedException { } } - protected static class MouseGrabber implements Runnable { - - /** - * Previously captured mouse location. This is used to coalesce mouse - * captures if the mouse has not been moved. - */ - private Point prevCapturedMouseLocation = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); - private ScheduledThreadPoolExecutor timer; - private ScreenRecorder recorder; - private GraphicsDevice captureDevice; - private Rectangle captureArea; - private BlockingQueue mouseCaptures; - private volatile long stopTime = Long.MAX_VALUE; - private long startTime; - private Format format; - private ScheduledFuture future; - private volatile boolean mousePressed; - private volatile boolean mouseWasPressed; - private volatile boolean mousePressedRecorded; - private Rectangle cursorImageArea; - private Point cursorOffset; - - public MouseGrabber(ScreenRecorder recorder, long startTime, ScheduledThreadPoolExecutor timer) { - this.timer = timer; - this.format = recorder.mouseFormat; - this.captureDevice = recorder.captureDevice; - this.captureArea = recorder.captureArea; - this.mouseCaptures = recorder.mouseCaptures; - this.startTime = startTime; - this.cursorImageArea = new Rectangle(0, 0, recorder.cursorImg.getWidth(), recorder.cursorImg.getHeight()); - this.cursorOffset = recorder.cursorOffset; - } - - public void setFuture(ScheduledFuture future) { - this.future = future; - } - - public void setMousePressed(boolean newValue) { - if (newValue) { - mouseWasPressed = true; - } - mousePressed = newValue; - } - - @Override - public void run() { - try { - grabMouse(); - } catch (Throwable ex) { - //ex.printStackTrace(); - timer.shutdown(); - recorder.recordingFailed(ex); - } - } - - public synchronized void setStopTime(long newValue) { - this.stopTime = newValue; - } - - public synchronized long getStopTime() { - return this.stopTime; - } - - /** - * Captures the mouse cursor. - */ - private void grabMouse() throws InterruptedException { - long now = System.currentTimeMillis(); - if (now > getStopTime()) { - future.cancel(false); - return; - } - PointerInfo info = MouseInfo.getPointerInfo(); - Point p = info.getLocation(); - cursorImageArea.x = p.x + cursorOffset.x; - cursorImageArea.y = p.y + cursorOffset.y; - if (!info.getDevice().equals(captureDevice) - || !captureArea.intersects(cursorImageArea)) { - // If the cursor is outside the capture region, we - // assign Integer.MAX_VALUE to its location. - // This ensures that all mouse movements outside of the - // capture region get coallesced. - p.setLocation(Integer.MAX_VALUE, Integer.MAX_VALUE); - } - - // Only create a new capture event if the location has changed - // or the mouse state has changed. - if (!p.equals(prevCapturedMouseLocation) || mouseWasPressed != mousePressedRecorded) { - Buffer buf = new Buffer(); - buf.format = format; - buf.timeStamp = new Rational(now, 1000); - buf.data = p; - buf.header = mouseWasPressed; - mousePressedRecorded = mouseWasPressed; - mouseCaptures.put(buf); - prevCapturedMouseLocation.setLocation(p); - } - mouseWasPressed = mousePressed; - } - - public void close() { - } - } /** * Starts audio capture. @@ -977,7 +600,7 @@ public void close() { private void startAudioCapture() throws LineUnavailableException { audioCaptureTimer = new ScheduledThreadPoolExecutor(1); int delay = 500; - audioGrabber = new AudioGrabber(mixer, audioFormat, audioTrack, recordingStartTime, writerQueue); + audioGrabber = new AudioGrabber(mixer, audioFormat, audioTrackId, recordingStartTime, writerQueue); audioFuture = audioCaptureTimer.scheduleWithFixedDelay(audioGrabber, 0, 10, TimeUnit.MILLISECONDS); audioGrabber.setFuture(audioFuture); } @@ -1027,211 +650,6 @@ public float getAudioLevelRight() { return AudioSystem.NOT_SPECIFIED; } - /** - * This runnable grabs audio samples and enqueues them into the specified - * BlockingQueue. This runnable must be called twice a second. - */ - private static class AudioGrabber implements Runnable { - - final private TargetDataLine line; - final private BlockingQueue queue; - final private Format audioFormat; - final private int audioTrack; - final private long startTime; - private volatile long stopTime = Long.MAX_VALUE; - private long totalSampleCount; - private ScheduledFuture future; - private long sequenceNumber; - private float audioLevelLeft = AudioSystem.NOT_SPECIFIED; - private float audioLevelRight = AudioSystem.NOT_SPECIFIED; - private Mixer mixer; - - public AudioGrabber(Mixer mixer, Format audioFormat, int audioTrack, long startTime, BlockingQueue queue) - throws LineUnavailableException { - this.mixer = mixer; - this.audioFormat = audioFormat; - this.audioTrack = audioTrack; - this.queue = queue; - this.startTime = startTime; - DataLine.Info lineInfo = new DataLine.Info( - TargetDataLine.class, AudioFormatKeys.toAudioFormat(audioFormat)); - - if (mixer != null) { - line = (TargetDataLine) mixer.getLine(lineInfo); - } else { - - line = (TargetDataLine) AudioSystem.getLine(lineInfo); - } - - // Make sure the line is not muted - try { - BooleanControl ctrl = (BooleanControl) line.getControl(BooleanControl.Type.MUTE); - ctrl.setValue(false); - } catch (IllegalArgumentException e) { - // We can't unmute the line from Java - } - // Make sure the volume of the line is bigger than 0.2 - try { - FloatControl ctrl = (FloatControl) line.getControl(FloatControl.Type.VOLUME); - ctrl.setValue(Math.max(ctrl.getValue(), 0.2f)); - } catch (IllegalArgumentException e) { - // We can't change the volume from Java - } - line.open(); - line.start(); - } - - public void setFuture(ScheduledFuture future) { - this.future = future; - } - - public void close() { - line.close(); - } - - public synchronized void setStopTime(long newValue) { - this.stopTime = newValue; - } - - public synchronized long getStopTime() { - return this.stopTime; - } - - @Override - public void run() { - Buffer buf = new Buffer(); - AudioFormat lineFormat = line.getFormat(); - buf.format = fromAudioFormat(lineFormat).append(SilenceBugKey, true); - - // For even sample rates, we select a buffer size that can - // hold half a second of audio. This allows audio/video interleave - // twice a second, as recommended for AVI and QuickTime movies. - // For odd sample rates, we have to select a buffer size that can hold - // one second of audio. - int bufferSize = lineFormat.getFrameSize() * (int) lineFormat.getSampleRate(); - if (((int) lineFormat.getSampleRate() & 1) == 0) { - bufferSize /= 2; - } - - byte bdat[] = new byte[bufferSize]; - buf.data = bdat; - Rational sampleRate = Rational.valueOf(lineFormat.getSampleRate()); - Rational frameRate = Rational.valueOf(lineFormat.getFrameRate()); - int count = line.read(bdat, 0, bdat.length); - if (count > 0) { - computeAudioLevel(bdat, count, lineFormat); - buf.sampleCount = count / (lineFormat.getSampleSizeInBits() / 8 * lineFormat.getChannels()); - buf.sampleDuration = sampleRate.inverse(); - buf.offset = 0; - buf.sequenceNumber = sequenceNumber++; - buf.length = count; - buf.track = audioTrack; - buf.timeStamp = new Rational(totalSampleCount, 1).divide(frameRate); - - // Check if recording should be stopped - Rational stopTS = new Rational(getStopTime() - startTime, 1000); - if (buf.timeStamp.add(buf.sampleDuration.multiply(buf.sampleCount)).compareTo(stopTS) > 0) { - // we recorded too much => truncate the buffer - buf.sampleCount = Math.max(0, (int) Math.ceil(stopTS.subtract(buf.timeStamp).divide(buf.sampleDuration).floatValue())); - buf.length = buf.sampleCount * (lineFormat.getSampleSizeInBits() / 8 * lineFormat.getChannels()); - - future.cancel(false); - } - if (buf.sampleCount > 0) { - try { - queue.put(buf); - } catch (InterruptedException ex) { - // nothing to do - } - } - totalSampleCount += buf.sampleCount; - } - } - - /** - * Calculates the root-mean-square average of continuous samples. For - * four samples, the formula looks like this: - *

-         * rms = sqrt( (x0^2 + x1^2 + x2^2 + x3^2) / 4)
-         * 
Resources: - * http://www.jsresources.org/faq_audio.html#calculate_power - * - * @param data - * @param length - * @param format - */ - private void computeAudioLevel(byte[] data, int length, AudioFormat format) { - audioLevelLeft = audioLevelRight = AudioSystem.NOT_SPECIFIED; - if (format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED)) { - switch (format.getSampleSizeInBits()) { - case 8: - switch (format.getChannels()) { - case 1: - audioLevelLeft = computeAudioLevelSigned8(data, 0, length, format.getFrameSize()); - break; - case 2: - audioLevelLeft = computeAudioLevelSigned8(data, 0, length, format.getFrameSize()); - audioLevelRight = computeAudioLevelSigned8(data, 1, length, format.getFrameSize()); - break; - } - break; - case 16: - if (format.isBigEndian()) { - switch (format.getChannels()) { - case 1: - audioLevelLeft = computeAudioLevelSigned16BE(data, 0, length, format.getFrameSize()); - break; - case 2: - audioLevelLeft = computeAudioLevelSigned16BE(data, 0, length, format.getFrameSize()); - audioLevelRight = computeAudioLevelSigned16BE(data, 2, length, format.getFrameSize()); - break; - } - } else { - switch (format.getChannels()) { - case 1: - break; - case 2: - break; - } - } - break; - } - } - } - - private float computeAudioLevelSigned16BE(byte[] data, int offset, int length, int stride) { - double sum = 0; - for (int i = offset; i < length; i += stride) { - int value = ((data[i]) << 8) | (data[i + 1] & 0xff); - sum += value * value; - } - double rms = Math.sqrt(sum / ((length - offset) / stride)); - return (float) (rms / 32768); - } - - private float computeAudioLevelSigned8(byte[] data, int offset, int length, int stride) { - double sum = 0; - for (int i = offset; i < length; i += stride) { - int value = data[i]; - - // FIXME - The java audio system records silence as -128 instead of 0. - if (value != -128) { - sum += value * value; - } - } - double rms = Math.sqrt(sum / ((length) / stride)); - return (float) (rms / 128); - } - - public float getAudioLevelLeft() { - return audioLevelLeft; - } - - public float getAudioLevelRight() { - return audioLevelRight; - } - } - /** * Starts file writing. */ @@ -1260,16 +678,13 @@ public void run() { writerThread.start(); } - private void recordingFailed(final Throwable msg) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - try { - stop(); - setState(State.FAILED, msg); - } catch (IOException ex2) { - ex2.printStackTrace(); - } + public void recordingFailed(final Throwable msg) { + SwingUtilities.invokeLater(() -> { + try { + stop(); + setState(State.FAILED, msg); + } catch (IOException ex2) { + ex2.printStackTrace(); } }); } @@ -1392,8 +807,8 @@ protected void write(Buffer buf) throws IOException, InterruptedException { if (writer == null) { return; } - if (buf.track == videoTrack) { - if (writer.getFormat(videoTrack).get(FixedFrameRateKey, false) == false) { + if (buf.track == videoTrackId) { + if (writer.getFormat(videoTrackId).get(FixedFrameRateKey, false) == false) { // variable frame rate is supported => easy Buffer wbuf = new Buffer(); frameEncoder.process(buf, wbuf); @@ -1448,7 +863,7 @@ private void doWrite(Buffer buf) throws IOException { // FIXME - this does not guarantee that audio and video track have // the same duration long now = System.currentTimeMillis(); - if (buf.track == videoTrack && buf.isFlag(BufferFlag.KEYFRAME) + if (buf.track == videoTrackId && buf.isFlag(BufferFlag.KEYFRAME) && (mw.isDataLimitReached() || now - fileStartTime > maxRecordingTime)) { final MovieWriter closingWriter = mw; new Thread() { diff --git a/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/State.java b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/State.java new file mode 100644 index 0000000..abe6de9 --- /dev/null +++ b/org.monte.media.screenrecorder/src/main/java/org.monte.media.screenrecorder/org/monte/media/screenrecorder/State.java @@ -0,0 +1,14 @@ +/* + * @(#)State.java + * Copyright © 2024 Werner Randelshofer, Switzerland. MIT License. + */ + +package org.monte.media.screenrecorder; + +/** + * States of the screen recorder. + */ +public enum State { + + DONE, FAILED, RECORDING, FAILING +} diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/Buffer.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/Buffer.java index 98013d5..582080e 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/Buffer.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/Buffer.java @@ -107,23 +107,27 @@ public void setMetaTo(Buffer that) { * {@code data} and {@code header}, so that these fields in that buffer can * be discarded without affecting the contents of this buffer. *

- * FIXME - This method does not always create a copy!! + * Returns {@link Codec#CODEC_FAILED} or {@link Codec#CODEC_OK} */ - public void setDataTo(Buffer that) { + public int setDataTo(Buffer that) { this.offset = that.offset; this.length = that.length; - this.data = copy(that.data, this.data); - this.header = copy(that.header, this.header); - + try { + this.data = copy(that.data, this.data); + this.header = copy(that.header, this.header); + } catch (UnsupportedOperationException e) { + return Codec.CODEC_FAILED; + } + return Codec.CODEC_OK; } - private Object copy(Object from, Object into) { + private Object copy(Object from, Object into) throws UnsupportedOperationException { if (from instanceof byte[]) { byte[] b = (byte[]) from; if (!(into instanceof byte[]) || ((byte[]) into).length < b.length) { into = new byte[b.length]; } - System.arraycopy(b, 0, (byte[]) into, 0, b.length); + System.arraycopy(b, 0, into, 0, b.length); } else if (from instanceof BufferedImage) { // FIXME - Try to reuse BufferedImage in output! BufferedImage img = (BufferedImage) from; @@ -138,9 +142,7 @@ private Object copy(Object from, Object into) { into = from; } } else { - // FIXME - This is very fragile, since we do not know, if the - // input data stays valid until the output data is processed! - into = from; + throw new UnsupportedOperationException(); } return into; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/AdjustTimeCodec.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/AdjustTimeCodec.java index 0f5351b..c407209 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/AdjustTimeCodec.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/AdjustTimeCodec.java @@ -46,13 +46,13 @@ public Format setInputFormat(Format f) { @Override public int process(Buffer in, Buffer out) { out.setMetaTo(in); - out.setDataTo(in); + int flags = out.setDataTo(in); if (mediaTime != null) { out.timeStamp = mediaTime; mediaTime = mediaTime.add(out.sampleDuration.multiply(out.sampleCount)); } - return CODEC_OK; + return flags; } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/PassThroughCodec.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/PassThroughCodec.java index 1b010e8..689e1d9 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/PassThroughCodec.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/PassThroughCodec.java @@ -36,7 +36,6 @@ public Format setInputFormat(Format f) { @Override public int process(Buffer in, Buffer out) { out.setMetaTo(in); - out.setDataTo(in); - return CODEC_OK; + return out.setDataTo(in); } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/TrimTimeCodec.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/TrimTimeCodec.java index 9db33b4..c836d5e 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/TrimTimeCodec.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/time/TrimTimeCodec.java @@ -107,10 +107,11 @@ public int process(Buffer in, Buffer out) { // Buffer is fully inside time range } } + int flags = CODEC_OK; if (!out.isFlag(BufferFlag.DISCARD)) { - out.setDataTo(in); + flags = out.setDataTo(in); } - return CODEC_OK; + return flags; } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/video/FrameRateConverter.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/video/FrameRateConverter.java index e421821..03c09e2 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/video/FrameRateConverter.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/av/codec/video/FrameRateConverter.java @@ -126,14 +126,11 @@ public int process(Buffer in, Buffer out) { // Produce time converted frame. out.format = outputFormat; - out.setDataTo(in); + int flags = out.setDataTo(in); out.timeStamp = outputTime; out.sampleDuration = outputDuration; outputTime = outputTime.add(outputDuration); - // System.out.println("FrameRateConverter inTS=" + in.timeStamp + " outTS=" + out.timeStamp + " inDur=" + in.sampleDuration + " outDur=" + out.sampleDuration.toDescriptiveString()); - - - return CODEC_OK; + return flags; } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ColorModels.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ColorModels.java index 1a14366..5960133 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ColorModels.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ColorModels.java @@ -30,9 +30,8 @@ private ColorModels() { * Returns a descriptive string for the provided color model. */ public static String toString(ColorModel cm) { - StringBuffer buf = new StringBuffer(); - if (cm instanceof DirectColorModel) { - DirectColorModel dcm = (DirectColorModel) cm; + StringBuilder buf = new StringBuilder(); + if (cm instanceof DirectColorModel dcm) { buf.append("Direct Color Model "); int[] masks = dcm.getMasks(); @@ -58,12 +57,11 @@ public static String toString(ColorModel cm) { buf.append(totalBits); buf.append(" Bit "); Arrays.sort(entries); - for (int i = 0; i < entries.length; i++) { - buf.append(entries[i]); + for (MaskEntry entry : entries) { + buf.append(entry); } - } else if (cm instanceof IndexColorModel) { + } else if (cm instanceof IndexColorModel icm) { buf.append("Index Color Model "); - IndexColorModel icm = (IndexColorModel) cm; int mapSize = icm.getMapSize(); buf.append(icm.getMapSize()); buf.append(" Colors"); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/Histogram.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/Histogram.java index f3e17d5..6ba85fc 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/Histogram.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/Histogram.java @@ -13,7 +13,7 @@ */ public class Histogram { - private int[][] bins = new int[3][256]; + private final int[][] bins = new int[3][256]; public void countPixels(WritableRaster raster) { int n = raster.getNumBands(); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ICCPackedColorModel.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ICCPackedColorModel.java index ad45ac4..5c7d2d8 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ICCPackedColorModel.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/ICCPackedColorModel.java @@ -114,24 +114,22 @@ public boolean isCompatibleRaster(Raster raster) { @Override final public int[] getComponents(Object pixel, int[] components, int offset) { - int intpixel = 0; - switch (transferType) { - case DataBuffer.TYPE_BYTE: - byte bdata[] = (byte[]) pixel; - intpixel = bdata[0] & 0xff; - break; - case DataBuffer.TYPE_USHORT: - short sdata[] = (short[]) pixel; - intpixel = sdata[0] & 0xffff; - break; - case DataBuffer.TYPE_INT: - int idata[] = (int[]) pixel; - intpixel = idata[0]; - break; - default: - throw new UnsupportedOperationException("This method has not been " + - "implemented for transferType " + transferType); - } + int intpixel = switch (transferType) { + case DataBuffer.TYPE_BYTE -> { + byte[] bdata = (byte[]) pixel; + yield bdata[0] & 0xff; + } + case DataBuffer.TYPE_USHORT -> { + short[] sdata = (short[]) pixel; + yield sdata[0] & 0xffff; + } + case DataBuffer.TYPE_INT -> { + int[] idata = (int[]) pixel; + yield idata[0]; + } + default -> throw new UnsupportedOperationException("This method has not been " + + "implemented for transferType " + transferType); + }; return getComponents(intpixel, components, offset); } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/WhiteBalance.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/WhiteBalance.java index 503abb9..f568806 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/WhiteBalance.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/color/WhiteBalance.java @@ -19,9 +19,9 @@ *

* References:
* [Ken09] Kenfack, Pierre Marie (2009). Implementierung und Vergleich - * verschiedener Algorithmen zur Bildsensorkalibrierung. Fraunhofer ITWM. - * http://www.itwm.fraunhofer.de/fileadmin/ITWM-Media/Abteilungen/BV/Pdf/ - * Diplomarbeit_Kenfack.pdf + * verschiedener Algorithmen zur Bildsensorkalibrierung. Fraunhofer ITWM. + * Diplomarbeit_Kenfack.pdf + * *

* [Lam05] Edmund Lam, Combining gray world and retinex theory for automatic * white balance in digital photography, Consumer Electronics, 2005. @@ -90,11 +90,10 @@ public static BufferedImage whiteBalanceGreyworldYCC(BufferedImage img, float[] int NGray = 0, NColor = 0; double cbGraySum = 0, cbColorSum = 0; double crGraySum = 0, crColorSum = 0; - for (int i = 0; i < p.length; i++) { - int px = p[i]; + for (int px : p) { rgb[0] = ((px & 0xff0000) >> 16) / 255f; rgb[1] = ((px & 0xff00) >> 8) / 255f; - rgb[2] = ((px & 0xff) >> 0) / 255f; + rgb[2] = ((px & 0xff)) / 255f; ColorModels.RGBtoYCC(rgb, ycc); if ((abs(ycc[1]) + abs(ycc[2])) / ycc[0] > T) { NColor++; @@ -166,11 +165,10 @@ public static BufferedImage whiteBalanceGreyworldYUV(BufferedImage img, float[] int NGray = 0, NColor = 0; double UGraySum = 0, UColorSum = 0; double VGraySum = 0, VColorSum = 0; - for (int i = 0; i < p.length; i++) { - int px = p[i]; + for (int px : p) { rgb[0] = ((px & 0xff0000) >> 16) / 255f; rgb[1] = ((px & 0xff00) >> 8) / 255f; - rgb[2] = ((px & 0xff) >> 0) / 255f; + rgb[2] = ((px & 0xff)) / 255f; ColorModels.RGBtoYUV(rgb, yuv); if ((abs(yuv[1]) + abs(yuv[2])) / yuv[0] > T) { NColor++; @@ -324,11 +322,10 @@ public static double[] whiteBalanceGreyworld(Histogram rgbHist) { fb = 1; } - double[] matrix = {// + return new double[]{// fr, 0, 0,// 0, fg, 0,// 0, 0, fb}; - return matrix; } /** @@ -370,11 +367,10 @@ public static double[] whiteBalanceRetinex(Histogram rgbHist) { Bgain = 1; } - double[] matrix = {// + return new double[]{// Rgain, 0, 0,// 0, 1, 0,// 0, 0, Bgain}; - return matrix; } @@ -439,10 +435,10 @@ public static double[] whiteBalanceQM(Histogram rgbHist) { double[] Rmunu = LinearEquations.solve(R2sum, Rsum, R2max, Rmax, Gsum, Gmax); double[] Bmunu = LinearEquations.solve(B2sum, Bsum, B2max, Bmax, Gsum, Gmax); - double[] vector = { + // + return new double[]{ Rmunu[0], Rmunu[1], Bmunu[0], Bmunu[1]// }; - return vector; } public static double[] whiteBalanceQM(long[][] rgbBins) { @@ -486,9 +482,8 @@ public static double[] whiteBalanceQM(long[][] rgbBins) { double[] Rmunu = LinearEquations.solve(R2sum, Rsum, R2max, Rmax, Gsum, Gmax); double[] Bmunu = LinearEquations.solve(B2sum, Bsum, B2max, Bmax, Gsum, Gmax); - double[] vector = { + return new double[]{ Rmunu[0], Rmunu[1], Bmunu[0], Bmunu[1]// }; - return vector; } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/EXIFReader.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/EXIFReader.java index 3c00a1b..28182ec 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/EXIFReader.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/EXIFReader.java @@ -44,11 +44,11 @@ import java.util.TreeSet; /** - * Reads EXIF and MP meta data from a JPEG, MPO or AVI file.

Creates a tree + * Reads EXIF and MP metadata from a JPEG, MPO or AVI file.

Creates a tree * structure of {@code DefaultMutableTreeNode}s. Nodes with a String user object - * describe the hierarchy of the meta data. Nodes with an MetaDataEntry as user - * object hold the actual meta data.

Sources:

Exchangeable image file - * format for digital still cameras: EXIF Version 2.2. (April, 2002). Standard + * describe the hierarchy of the metadata. Nodes with an MetaDataEntry as user + * object hold the actual metadata.

Sources:

Exchangeable image file + * format for digital still cameras: EXIF Version 2.2. (April 2002). Standard * of Japan Electronics and Information Technology Industries Association. JEITA * CP-3451. http://www.exif.org/Exif2-2.PDF @@ -328,8 +328,7 @@ public void visitChunk(RIFFChunk group, RIFFChunk chunk) throws ParseException, } else if (chunk.getID() == strd_ID) { trackNode = new TIFFDirectory(TrackTagSet.getInstance(), null, trackCount - 1, null, null, new FileSegment(chunk.getScan(), chunk.getSize())); root.add(trackNode); - ByteArrayImageInputStream in = new ByteArrayImageInputStream(chunk.getData(), 8, (int) chunk.getSize() - 8, ByteOrder.LITTLE_ENDIAN); - try { + try (ByteArrayImageInputStream in = new ByteArrayImageInputStream(chunk.getData(), 8, (int) chunk.getSize() - 8, ByteOrder.LITTLE_ENDIAN)) { TIFFInputStream tin = new TIFFInputStream(in, ByteOrder.LITTLE_ENDIAN, 0); ArrayList tiffSeg = new ArrayList(); tiffSeg.add(new FileSegment(chunk.getScan() + 8, chunk.getSize() - 8)); @@ -338,11 +337,8 @@ public void visitChunk(RIFFChunk group, RIFFChunk chunk) throws ParseException, //} //System.out.println("EXIFReader.readRIFF magic:" + RIFFParser.idToString(magic)); } catch (IOException ex) { - ParseException e = new ParseException("Error parsing AVI strd chunk."); - e.initCause(ex); + ParseException e = new ParseException("Error parsing AVI strd chunk.", ex); throw e; - } finally { - in.close(); } if (isFirstImageOnly()) { throw new AbortException(); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/MPEntryTagSet.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/MPEntryTagSet.java index 78cbf51..e43bfed 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/MPEntryTagSet.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/exif/MPEntryTagSet.java @@ -15,7 +15,7 @@ import static org.monte.media.tiff.TIFFTag.SRATIONAL_MASK; /** - * Syntethic tags for the entry information Multi-Picture format (MPF) tags + * Synthetic tags for the entry information Multi-Picture format (MPF) tags * as found in MPO image files generated by Fujifilm Finepix Real 3D W1 * cameras. *

diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileComparator.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileComparator.java index a60a737..629c629 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileComparator.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileComparator.java @@ -14,7 +14,7 @@ */ public class FileComparator implements Comparator { - private OSXCollator collator = new OSXCollator(); + private final OSXCollator collator = new OSXCollator(); @Override public int compare(File o1, File o2) { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileMultiplexer.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileMultiplexer.java index 3fadec4..08b5b33 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileMultiplexer.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/FileMultiplexer.java @@ -21,11 +21,11 @@ */ public class FileMultiplexer implements Multiplexer { - private File dir; - private String baseName; - private String extension; + private final File dir; + private final String baseName; + private final String extension; private long position = 0; - private int minDigits = 4; + private final int minDigits = 4; public FileMultiplexer(File dir, String baseName, String extension) { this.dir = dir; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/ImageSequenceWriter.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/ImageSequenceWriter.java index aecbdb9..b07b819 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/ImageSequenceWriter.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/ImageSequenceWriter.java @@ -43,7 +43,7 @@ */ public class ImageSequenceWriter implements MovieWriter { - private Format fileFormat = new Format(MediaTypeKey, MediaType.FILE); + private final Format fileFormat = new Format(MediaTypeKey, MediaType.FILE); @Override public int addTrack(Format format) throws IOException { @@ -65,7 +65,7 @@ public Format getFormat(int track) { throw new UnsupportedOperationException("Not supported yet."); } - private class VideoTrack { + private static class VideoTrack { Format videoFormat; File dir; @@ -87,7 +87,7 @@ public VideoTrack(File dir, String filenameFormatter, Format fmt, Codec codec, i } } - private ArrayList tracks = new ArrayList(); + private final ArrayList tracks = new ArrayList(); /** * Adds a video track. @@ -151,7 +151,7 @@ public void write(int track, BufferedImage image, long duration) throws IOExcept public void write(int track, Buffer buf) throws IOException { VideoTrack t = tracks.get(track); - // FIXME - Meybe we should not have built-in support for some data types? + // FIXME - Maybe we should not have built-in support for some data types? if (buf.data instanceof BufferedImage) { if (t.outputBuffer == null) { t.outputBuffer = new Buffer(); @@ -166,11 +166,8 @@ public void write(int track, Buffer buf) throws IOException { File file = new File(t.dir, String.format(t.nameFormat, t.count + 1)); if (buf.data instanceof byte[]) { - FileOutputStream out = new FileOutputStream(file); - try { + try (FileOutputStream out = new FileOutputStream(file)) { out.write((byte[]) buf.data, buf.offset, buf.length); - } finally { - out.close(); } } else if (buf.data instanceof File) { IOStreams.copy((File) buf.data, file); @@ -187,11 +184,8 @@ public void writeSample(int track, byte[] data, int off, int len, long duration, File file = new File(t.dir, String.format(t.nameFormat, t.count + 1)); - FileOutputStream out = new FileOutputStream(file); - try { + try (FileOutputStream out = new FileOutputStream(file)) { out.write(data, off, len); - } finally { - out.close(); } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/TrackDemultiplexer.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/TrackDemultiplexer.java index 6ec5cec..54f89a9 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/TrackDemultiplexer.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/imgseq/TrackDemultiplexer.java @@ -16,7 +16,7 @@ */ public class TrackDemultiplexer implements Demultiplexer { - private Track[] tracks; + private final Track[] tracks; public TrackDemultiplexer(Track[] tracks) { this.tracks = tracks.clone(); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageInputStream.java index 511825b..f860ab5 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageInputStream.java @@ -29,7 +29,7 @@ public class ByteArrayImageInputStream extends ImageInputStreamImpl2 { * stream; element buf[streamPos] is * the next byte to be read. */ - protected byte buf[]; + protected byte[] buf; /** * The index one greater than the last valid character in the input diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageOutputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageOutputStream.java index 000a2c8..cad1b10 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageOutputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ByteArrayImageOutputStream.java @@ -37,7 +37,7 @@ public class ByteArrayImageOutputStream extends ImageOutputStreamImpl { * stream; element buf[streamPos] is * the next byte to be read. */ - protected byte buf[]; + protected byte[] buf; /** * The index one greater than the last valid character in the input * stream buffer. diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/FilterImageInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/FilterImageInputStream.java index ca0d46a..85ecbf2 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/FilterImageInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/FilterImageInputStream.java @@ -29,7 +29,7 @@ public int read() throws IOException { } @Override - public int read(byte b[], int off, int len) throws IOException { + public int read(byte[] b, int off, int len) throws IOException { flushBits(); return in.read(b, off, len); } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/IOStreams.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/IOStreams.java index 0e6f9b0..dd83c0d 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/IOStreams.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/IOStreams.java @@ -23,7 +23,7 @@ public class IOStreams { * * @param source the source file * @param target the target file - * @throws IOException + * @throws IOException if an I/O error occurs */ public static void copy(File source, File target) throws IOException { Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); @@ -36,7 +36,7 @@ public static void copy(File source, File target) throws IOException { * @param source the source stream * @param target the target stream * @return number of copied bytes - * @throws IOException + * @throws IOException if an I/O error occurs */ public static long copy(InputStream source, OutputStream target) throws IOException { long n = 0L; @@ -55,7 +55,7 @@ public static long copy(InputStream source, OutputStream target) throws IOExcept * @param source the source stream * @param target the target stream * @return number of copied bytes - * @throws IOException + * @throws IOException if an I/O error occurs */ public static long copy(InputStream source, ImageOutputStream target) throws IOException { long count = 0L; @@ -75,7 +75,7 @@ public static long copy(InputStream source, ImageOutputStream target) throws IOE * @param target the target stream * @param n the maximal number of bytes to copy * @return actual number of copied bytes - * @throws IOException + * @throws IOException if an I/O error occurs */ public static long copy(InputStream source, ImageOutputStream target, long n) throws IOException { long count = 0L; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageInputStreamAdapter.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageInputStreamAdapter.java index 28f3518..2ae8316 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageInputStreamAdapter.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageInputStreamAdapter.java @@ -15,7 +15,7 @@ * @author Werner Randelshofer */ public class ImageInputStreamAdapter extends InputStream { - private ImageInputStream iis; + private final ImageInputStream iis; public ImageInputStreamAdapter(ImageInputStream iis) { this.iis = iis; @@ -36,7 +36,6 @@ public ImageInputStreamAdapter(ImageInputStream iis) { * @return the next byte of data, or -1 if the end of the * stream is reached. * @throws IOException if an I/O error occurs. - * @see java.io.FilterInputStream#in */ @Override public int read() throws IOException { @@ -63,10 +62,10 @@ public int read() throws IOException { * len is negative, or len is greater than * b.length - off * @throws IOException if an I/O error occurs. - * @see java.io.FilterInputStream#in + * @see java.io.FilterInputStream */ @Override - public int read(byte b[], int off, int len) throws IOException { + public int read(byte[] b, int off, int len) throws IOException { return iis.read(b, off, len); } @@ -105,7 +104,7 @@ public int available() throws IOException { * method simply performs in.close(). * * @throws IOException if an I/O error occurs. - * @see java.io.FilterInputStream#in + * @see java.io.FilterInputStream */ @Override public void close() throws IOException { @@ -125,7 +124,6 @@ public void close() throws IOException { * * @param readlimit the maximum limit of bytes that can be read before * the mark position becomes invalid. - * @see java.io.FilterInputStream#in * @see java.io.FilterInputStream#reset() */ @Override @@ -151,7 +149,6 @@ public synchronized void mark(int readlimit) { * * @throws IOException if the stream has not been marked or if the * mark has been invalidated. - * @see java.io.FilterInputStream#in * @see java.io.FilterInputStream#mark(int) */ @Override @@ -168,7 +165,6 @@ public synchronized void reset() throws IOException { * @return true if this stream type supports the * mark and reset method; * false otherwise. - * @see java.io.FilterInputStream#in * @see java.io.InputStream#mark(int) * @see java.io.InputStream#reset() */ diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageOutputStreamAdapter.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageOutputStreamAdapter.java index ab3068c..b1dec31 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageOutputStreamAdapter.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/ImageOutputStreamAdapter.java @@ -68,7 +68,7 @@ public void write(int b) throws IOException { * @see java.io.FilterOutputStream#write(byte[], int, int) */ @Override - public void write(byte b[]) throws IOException { + public void write(byte[] b) throws IOException { write(b, 0, b.length); } @@ -105,7 +105,6 @@ public void write(byte b[], int off, int len) throws IOException { * calls the flush method of its underlying output stream. * * @throws IOException if an I/O error occurs. - * @see java.io.FilterOutputStream#out */ @Override public void flush() throws IOException { @@ -122,7 +121,6 @@ public void flush() throws IOException { * * @throws IOException if an I/O error occurs. * @see java.io.FilterOutputStream#flush() - * @see java.io.FilterOutputStream#out */ @Override public void close() throws IOException { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/StreamPosTokenizer.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/StreamPosTokenizer.java index d08c541..5a5c640 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/StreamPosTokenizer.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/StreamPosTokenizer.java @@ -35,9 +35,9 @@ public class StreamPosTokenizer * rlw */ private int startpos = -1, endpos = -1; - private LinkedList unread = new LinkedList(); + private final LinkedList unread = new LinkedList<>(); - private char buf[] = new char[20]; + private char[] buf = new char[20]; /** * The next character to be considered by the nextToken method. May also @@ -67,7 +67,7 @@ public class StreamPosTokenizer private char[] slashStar = new char[]{'/', '*'}; private char[] starSlash = new char[]{'*', '/'}; - private byte ctype[] = new byte[256]; + private final byte[] ctype = new byte[256]; private static final byte CT_WHITESPACE = 1; private static final byte CT_DIGIT = 2; private static final byte CT_ALPHA = 4; @@ -188,7 +188,7 @@ public StreamPosTokenizer(Reader r) { /** * Sets the reader for the tokenizer. * - * @param r + * @param r the reader */ public void setReader(Reader r) { this.reader = r; @@ -506,7 +506,7 @@ public int nextToken() throws IOException { pushedBack = false; return ttype; } - byte ct[] = ctype; + byte[] ct = ctype; sval = null; int c = peekc; @@ -569,11 +569,9 @@ public int nextToken() throws IOException { ctype = c < 256 ? ct[c] : CT_ALPHA; } - // rlw startpos = readpos - 1; - // rlw hexadecimal - hex: + // parse hexadecimal if (((ctype & CT_DIGIT) != 0) && c == '0' && isParseHexNumbers) { c = read(); @@ -619,32 +617,18 @@ public int nextToken() throws IOException { c = read(); if (c != '.' && (c < '0' || c > '9')) { peekc = c; - // rlw - if (('-' & CT_ALPHA) != 0) { - unread(c); - c = '-'; - break digit; - } else { - endpos = readpos - 1; - return ttype = '-'; - } + unread(c); + c = '-'; + break digit; } neg = true; } else if (c == '+') { c = read(); if (c != '.' && (c < '0' || c > '9')) { peekc = c; - // rlw - if (('+' & CT_ALPHA) != 0) { - unread(c); - c = '+'; - break digit; - } else { - endpos = readpos - 1; - return ttype = '-'; - } + endpos = readpos - 1; + return ttype = '-'; } - neg = false; } double v = 0; @@ -673,21 +657,15 @@ else if ('0' <= c && c <= '9') { v = v / denom; } nval = neg ? -v : v; - // rlw endpos = (c == -1) ? readpos - 1 : readpos - 2; if (digits == 0) { - if (('.' & CT_ALPHA) != 0) { - unread(c); - if (neg) { - unread('.'); - c = '-'; - } else { - read(); // consume full stop - c = '.'; - } - break digit; + unread(c); + if (neg) { + unread('.'); + c = '-'; } else { - return ttype = '.'; + read(); // consume full stop + c = '.'; } } else { if (isParseExponents) { @@ -706,7 +684,6 @@ else if ('0' <= c && c <= '9') { neg = true; } v = 0; - decexp = 0; while (true) { if ('0' <= c && c <= '9') { digits++; @@ -728,7 +705,7 @@ else if ('0' <= c && c <= '9') { int i = 0; do { if (i >= buf.length) { - char nb[] = new char[buf.length * 2]; + char[] nb = new char[buf.length * 2]; System.arraycopy(buf, 0, nb, 0, buf.length); buf = nb; } @@ -740,7 +717,7 @@ else if ('0' <= c && c <= '9') { sval = String.copyValueOf(buf, 0, i); if (forceLower) sval = sval.toLowerCase(); - // rlw EOF must be treated specially + // EOF must be treated specially endpos = (c == -1) ? readpos - 1 : readpos - 2; return ttype = TT_WORD; } @@ -771,29 +748,16 @@ else if ('0' <= c && c <= '9') { } else d = c2; } else { - switch (c) { - case 'a': - c = 0x7; - break; - case 'b': - c = '\b'; - break; - case 'f': - c = 0xC; - break; - case 'n': - c = '\n'; - break; - case 'r': - c = '\r'; - break; - case 't': - c = '\t'; - break; - case 'v': - c = 0xB; - break; - } + c = switch (c) { + case 'a' -> 0x7; + case 'b' -> '\b'; + case 'f' -> 0xC; + case 'n' -> '\n'; + case 'r' -> '\r'; + case 't' -> '\t'; + case 'v' -> 0xB; + default -> c; + }; d = read(); } } else { @@ -801,7 +765,7 @@ else if ('0' <= c && c <= '9') { d = read(); } if (i >= buf.length) { - char nb[] = new char[buf.length * 2]; + char[] nb = new char[buf.length * 2]; System.arraycopy(buf, 0, nb, 0, buf.length); buf = nb; } @@ -1111,7 +1075,7 @@ public String toString() { ret = "NOTHING"; break; default: { - char s[] = new char[3]; + char[] s = new char[3]; s[0] = s[2] = '\''; s[1] = (char) ttype; ret = new String(s); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageInputStream.java index 46ab085..c36d723 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageInputStream.java @@ -14,9 +14,9 @@ */ public class SubImageInputStream extends ImageInputStreamImpl2 { - private ImageInputStream in; - private long offset; - private long length; + private final ImageInputStream in; + private final long offset; + private final long length; public SubImageInputStream(ImageInputStream in, long offset, long length) throws IOException { this.in = in; @@ -54,8 +54,7 @@ public int read(byte[] b, int off, int len) throws IOException { if (av <= 0) { return -1; } else { - int result = in.read(b, off, (int) Math.min(len, av)); - return result; + return in.read(b, off, (int) Math.min(len, av)); } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageOutputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageOutputStream.java index 6d3883c..bd888bc 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageOutputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/SubImageOutputStream.java @@ -17,13 +17,13 @@ public class SubImageOutputStream extends ImageOutputStreamImpl { private ImageOutputStream out; - private long offset; + private final long offset; private long length; /** * Whether flush and close request shall be forwarded to underlying stream. */ - private boolean forwardFlushAndClose; + private final boolean forwardFlushAndClose; public SubImageOutputStream(ImageOutputStream out, ByteOrder bo, boolean forwardFlushAndClose) throws IOException { this(out, out.getStreamPosition(), bo, forwardFlushAndClose); @@ -62,8 +62,7 @@ public int read(byte[] b, int off, int len) throws IOException { if (av <= 0) { return -1; } else { - int result = out.read(b, off, (int) Math.min(len, av)); - return result; + return out.read(b, off, (int) Math.min(len, av)); } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/UncachedImageInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/UncachedImageInputStream.java index 7db2685..93aadac 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/UncachedImageInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/io/UncachedImageInputStream.java @@ -18,7 +18,7 @@ */ public class UncachedImageInputStream extends ImageInputStreamImpl2 { - private InputStream in; + private final InputStream in; public UncachedImageInputStream(InputStream in) { this(in, ByteOrder.BIG_ENDIAN); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/jfif/JFIFOutputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/jfif/JFIFOutputStream.java index d4a2042..8bcb362 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/jfif/JFIFOutputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/jfif/JFIFOutputStream.java @@ -33,15 +33,15 @@ public class JFIFOutputStream extends OutputStream { /** - * This hash set holds the Id's of markers which stand alone, - * respectively do no have a data segment. + * This hash set holds the ids of markers which stand alone, + * respectively do not have a data segment. */ - private final HashSet standaloneMarkers = new HashSet(); + private final HashSet standaloneMarkers = new HashSet<>(); /** - * This hash set holds the Id's of markers which have a data - * segment followed by a entropy-coded data segment. + * This hash set holds the ids of markers which have a data + * segment followed by an entropy-coded data segment. */ - private final HashSet doubleSegMarkers = new HashSet(); + private final HashSet doubleSegMarkers = new HashSet<>(); /** * Start of image */ @@ -113,7 +113,7 @@ public class JFIFOutputStream extends OutputStream { public final static int RST7_MARKER = 0xffd7; private ImageOutputStream out; private long streamOffset; - private Stack stack = new Stack(); + private Stack stack = new Stack<>(); public JFIFOutputStream(ImageOutputStream out) throws IOException { this.out = out; @@ -229,7 +229,7 @@ public void close() throws IOException { */ @Override public void write(byte[] b, int off, int len) throws IOException { - if (stack.size() == 0 || standaloneMarkers.contains(stack.peek().marker)) { + if (stack.isEmpty() || standaloneMarkers.contains(stack.peek().marker)) { writeStuffed(b, off, len); } else { writeNonstuffed(b, off, len); @@ -242,7 +242,7 @@ public void write(byte[] b, int off, int len) throws IOException { */ @Override public void write(int b) throws IOException { - if (stack.size() == 0 || standaloneMarkers.contains(stack.peek().marker)) { + if (stack.isEmpty() || standaloneMarkers.contains(stack.peek().marker)) { writeStuffed(b); } else { writeNonstuffed(b); @@ -309,9 +309,10 @@ private class Segment { protected boolean finished; /** - * Creates a new Chunk at the current position of the ImageOutputStream. + * Creates a new Segment at the current position of the ImageOutputStream. * - * @param chunkType The chunkType of the chunk. A string with a length of 4 characters. + * @param marker The marker of the segment. + * @throws IOException if an I/O error occurs. */ public Segment(int marker) throws IOException { this.marker = marker; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/math/Rational.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/math/Rational.java index 83d6828..3ad1a8b 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/math/Rational.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/math/Rational.java @@ -143,11 +143,10 @@ private Rational add(long thatNum, long thatDen, boolean reduceFraction) { // FIXME - handle overflow long s = scm(this.den, thatDen); - Rational result = new Rational( + + return new Rational( this.num * (s / this.den) + thatNum * (s / thatDen), s, reduceFraction); - - return result; } public Rational subtract(Rational that) { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mjpg/MJPGImageReader.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mjpg/MJPGImageReader.java index 8df20c7..cda86a8 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mjpg/MJPGImageReader.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mjpg/MJPGImageReader.java @@ -35,7 +35,7 @@ */ public class MJPGImageReader extends ImageReader { - private static DirectColorModel RGB = new DirectColorModel(24, 0xff0000, 0xff00, 0xff, 0x0); + private static final DirectColorModel RGB = new DirectColorModel(24, 0xff0000, 0xff00, 0xff, 0x0); /** * When we read the header, we read the whole image. */ diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mp3/MP3AudioInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mp3/MP3AudioInputStream.java index bbfd08a..17122af 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mp3/MP3AudioInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mp3/MP3AudioInputStream.java @@ -25,7 +25,7 @@ */ public class MP3AudioInputStream extends AudioInputStream { - private MP3ElementaryInputStream in; + private final MP3ElementaryInputStream in; /** * Creates an MP3AudioInputStream and reads the stream until the first diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOFiles.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOFiles.java index c64f8d2..a2d05b8 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOFiles.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOFiles.java @@ -40,18 +40,11 @@ public static ArrayList splitMPOFile(File f) throws IOException { JFIFInputStream in = new JFIFInputStream(f); for (JFIFInputStream.Segment seg = in.getNextSegment(); seg != null; seg = in.getNextSegment()) { if (seg.marker == JFIFInputStream.SOI_MARKER) { - String ext; - switch (imgCount++) { - case 0: - ext = "_l.JPG"; - break; - case 1: - ext = "_r.JPG"; - break; - default: - ext = "_" + imgCount + ".JPG"; - break; - } + String ext = switch (imgCount++) { + case 0 -> "_l.JPG"; + case 1 -> "_r.JPG"; + default -> "_" + imgCount + ".JPG"; + }; String name = f.getName(); int p = name.lastIndexOf('.'); if (p == -1) { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOImageReader.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOImageReader.java index 2d6cf82..95902fa 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOImageReader.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/mpo/MPOImageReader.java @@ -180,8 +180,7 @@ private void readHeader() throws IOException { TIFFNode imageNode = metaDataTree.getChildAt(i); for (Iterator e = imageNode.preorderIterator(); e.hasNext(); ) { TIFFNode node = e.next(); - if (node instanceof TIFFDirectory) { - TIFFDirectory dir = (TIFFDirectory) node; + if (node instanceof TIFFDirectory dir) { if ((mde = dir.getField(BaselineTagSet.ImageWidth)) != null) { width[i] = ((Number) mde.getData()).intValue(); } @@ -210,10 +209,9 @@ private void readHeader() throws IOException { int index = 0; for (Iterator e = er.getMetaDataTree().preorderIterator(); e.hasNext(); ) { TIFFNode n = e.next(); - if (n instanceof TIFFDirectory) { - TIFFDirectory dir = (TIFFDirectory) n; + if (n instanceof TIFFDirectory dir) { if (dir.getName() != null && dir.getName().equals("MPEntry")) { - long dirOffset = dir.getFileSegments().get(0).getOffset(); + long dirOffset = dir.getFileSegments().get(0).offset(); TIFFField offsetField = dir.getField(MPEntryTagSet.IndividualImageDataOffset); TIFFField lengthField = dir.getField(MPEntryTagSet.IndividualImageSize); if (offsetField != null && lengthField != null) { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomInputStream.java index 07846c6..68f6ad1 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomInputStream.java @@ -12,7 +12,7 @@ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.GregorianCalendar; @@ -24,7 +24,7 @@ public class DataAtomInputStream extends FilterInputStream { protected static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, GregorianCalendar.JANUARY, 1).getTimeInMillis(); - private byte byteBuffer[] = new byte[8]; + private final byte[] byteBuffer = new byte[8]; public DataAtomInputStream(InputStream in) { super(in); @@ -102,7 +102,7 @@ public final void readFully(byte b[], int off, int len) throws IOException { * Reads a 32-bit Mac timestamp (seconds since 1902). * * @return date - * @throws java.io.IOException + * @throws java.io.IOException if an I/O error occurs */ public Date readMacTimestamp() throws IOException { long timestamp = ((long) readInt()) & 0xffffffffL; @@ -143,13 +143,7 @@ public double readFixed8D8() throws IOException { public String readType() throws IOException { readFully(byteBuffer, 0, 4); - try { - return new String(byteBuffer, 0, 4, "ASCII"); - } catch (UnsupportedEncodingException ex) { - InternalError ie = new InternalError("ASCII not supported"); - ie.initCause(ex); - throw ie; - } + return new String(byteBuffer, 0, 4, StandardCharsets.US_ASCII); } public String readPString() throws IOException { @@ -164,13 +158,7 @@ public String readPString() throws IOException { byte[] b = size <= byteBuffer.length ? byteBuffer : new byte[size]; readFully(b, 0, size); - try { - return new String(b, 0, size, "ASCII"); - } catch (UnsupportedEncodingException ex) { - InternalError ie = new InternalError("ASCII not supported"); - ie.initCause(ex); - throw ie; - } + return new String(b, 0, size, StandardCharsets.US_ASCII); } /** @@ -191,12 +179,6 @@ public String readPString(int fixedSize) throws IOException { byte[] b = fixedSize <= byteBuffer.length ? byteBuffer : new byte[fixedSize]; readFully(b, 0, fixedSize); - try { - return new String(b, 0, fixedSize, "ASCII"); - } catch (UnsupportedEncodingException ex) { - InternalError ie = new InternalError("ASCII not supported"); - ie.initCause(ex); - throw ie; - } + return new String(b, 0, fixedSize, StandardCharsets.US_ASCII); } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomOutputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomOutputStream.java index 7d09d6e..233b093 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomOutputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/DataAtomOutputStream.java @@ -6,11 +6,11 @@ import org.monte.media.io.ByteArray; -import javax.imageio.stream.ImageOutputStreamImpl; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.GregorianCalendar; @@ -22,14 +22,13 @@ */ public class DataAtomOutputStream extends FilterOutputStream { - ImageOutputStreamImpl impl; protected static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, GregorianCalendar.JANUARY, 1).getTimeInMillis(); /** * The number of bytes written to the data output stream so far. * If this counter overflows, it will be wrapped to Integer.MAX_VALUE. */ protected long written; - private byte byteBuffer[] = new byte[8]; + private final byte[] byteBuffer = new byte[8]; public DataAtomOutputStream(OutputStream out) { super(out); @@ -46,7 +45,7 @@ public void writeType(String s) throws IOException { } try { - out.write(s.getBytes("ASCII"), 0, 4); + out.write(s.getBytes(StandardCharsets.US_ASCII), 0, 4); incCount(4); } catch (UnsupportedEncodingException e) { throw new InternalError(e.toString()); @@ -80,7 +79,7 @@ public final void writeByte(int v) throws IOException { * @see FilterOutputStream#out */ @Override - public synchronized void write(byte b[], int off, int len) + public synchronized void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); incCount(len); @@ -119,10 +118,10 @@ public void writeInt(int v) throws IOException { } /** - * Writes an unsigned 32 bit integer value. + * Writes an unsigned 32-bit integer value. * - * @param v The value - * @throws IOException + * @param v The value to be written. + * @throws IOException if an I/O error occurs. */ public void writeUInt(long v) throws IOException { ByteArray.setIntBE(byteBuffer, 0, (int) v); @@ -130,10 +129,10 @@ public void writeUInt(long v) throws IOException { } /** - * Writes a signed 16 bit integer value. + * Writes a signed 16-bit integer value. * - * @param v The value - * @throws IOException + * @param v The value to be written. + * @throws IOException if an I/O error occurs. */ public void writeShort(int v) throws IOException { ByteArray.setShortBE(byteBuffer, 0, (short) v); @@ -168,8 +167,8 @@ public void writeBCD4(int v) throws IOException { /** * Writes a 32-bit Mac timestamp (seconds since 1902). * - * @param date - * @throws IOException + * @param date the date to be converted to a Mac timestamp + * @throws IOException if an I/O error occurs */ public void writeMacTimestamp(Date date) throws IOException { long millis = date.getTime(); @@ -241,14 +240,14 @@ public void writeFixed8D8(double f) throws IOException { /** * Writes a Pascal String. * - * @param s - * @throws IOException + * @param s the string to be written + * @throws IOException if an I/O error occurs */ public void writePString(String s) throws IOException { if (s.length() > 0xffff) { throw new IllegalArgumentException("String too long for PString"); } - if (s.length() != 0 && s.length() < 256) { + if (!s.isEmpty() && s.length() < 256) { out.write(s.length()); } else { out.write(0); @@ -261,17 +260,17 @@ public void writePString(String s) throws IOException { } /** - * Writes a Pascal String padded to the specified fixed size in bytes + * Writes a Pascal String padded to the specified fixed size in bytes. * - * @param s + * @param s the string to be written * @param length the fixed size in bytes - * @throws IOException + * @throws IOException if an I/O error occurs */ public void writePString(String s, int length) throws IOException { if (s.length() > length) { throw new IllegalArgumentException("String too long for PString of length " + length); } - if (s.length() != 0 && s.length() < 256) { + if (!s.isEmpty() && s.length() < 256) { out.write(s.length()); } else { out.write(0); @@ -322,7 +321,7 @@ public void writeShorts(short[] s, int off, int len) throws IOException { for (int i = 0; i < len; i++) { short v = s[off + i]; b[boff++] = (byte) (v >>> 8); - b[boff++] = (byte) (v >>> 0); + b[boff++] = (byte) (v); } write(b, 0, len * 2); @@ -341,7 +340,7 @@ public void writeInts(int[] i, int off, int len) throws IOException { b[boff++] = (byte) (v >>> 24); b[boff++] = (byte) (v >>> 16); b[boff++] = (byte) (v >>> 8); - b[boff++] = (byte) (v >>> 0); + b[boff++] = (byte) (v); } write(b, 0, len * 4); @@ -366,7 +365,7 @@ public void writeInts24(int[] i, int off, int len) throws IOException { //b[boff++] = (byte)(v >>> 24); b[boff++] = (byte) (v >>> 16); b[boff++] = (byte) (v >>> 8); - b[boff++] = (byte) (v >>> 0); + b[boff++] = (byte) (v); } write(b, 0, len * 3); @@ -378,7 +377,6 @@ public void writeInts24(int[] i, int off, int len) throws IOException { * If the counter overflows, it will be wrapped to Integer.MAX_VALUE. * * @return the value of the written field. - * @see java.io.DataOutputStream#written */ public final long size() { return written; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QTFFImageInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QTFFImageInputStream.java index aaa3069..0f8272a 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QTFFImageInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QTFFImageInputStream.java @@ -8,8 +8,8 @@ import javax.imageio.stream.ImageInputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.GregorianCalendar; @@ -31,8 +31,8 @@ public QTFFImageInputStream(ImageInputStream in) { /** * Reads a 32-bit Mac timestamp (seconds since 1902). * - * @return date - * @throws java.io.IOException + * @return the date corresponding to the Mac timestamp + * @throws IOException if an I/O error occurs */ public Date readMacTimestamp() throws IOException { long timestamp = ((long) readInt()) & 0xffffffffL; @@ -68,18 +68,12 @@ public double readFixed8D8() throws IOException { int wholePart = fixed >>> 8; int fractionPart = fixed & 0xff; - return new Double(wholePart + fractionPart / 256f); + return (wholePart + fractionPart / 256d); } public String readType() throws IOException { readFully(byteBuf, 0, 4); - try { - return new String(byteBuf, 0, 4, "ASCII"); - } catch (UnsupportedEncodingException ex) { - InternalError ie = new InternalError("ASCII not supported"); - ie.initCause(ex); - throw ie; - } + return new String(byteBuf, 0, 4, StandardCharsets.US_ASCII); } public String readPString() throws IOException { @@ -94,13 +88,7 @@ public String readPString() throws IOException { byte[] b = (size <= byteBuf.length) ? byteBuf : new byte[size]; readFully(b, 0, size); - try { - return new String(b, 0, size, "ASCII"); - } catch (UnsupportedEncodingException ex) { - InternalError ie = new InternalError("ASCII not supported"); - ie.initCause(ex); - throw ie; - } + return new String(b, 0, size, StandardCharsets.US_ASCII); } /** @@ -120,28 +108,22 @@ public String readPString(int fixedSize) throws IOException { skipBytes(remaining - size); } - try { - return new String(b, 0, size, "ASCII"); - } catch (UnsupportedEncodingException ex) { - InternalError ie = new InternalError("ASCII not supported"); - ie.initCause(ex); - throw ie; - } + return new String(b, 0, size, StandardCharsets.US_ASCII); } public int readUnsignedBCD4() throws IOException { readFully(byteBuf, 0, 2); int value = min(9, (byteBuf[0] >>> 4) & 0x0f) * 1000// - + min(9, (byteBuf[1] >>> 0) & 0x0f) * 100// - + min(9, (byteBuf[2] >>> 0) & 0x0f) * 10// - + min(9, (byteBuf[2] >>> 0) & 0x0f) * 1; + + min(9, byteBuf[0] & 0x0f) * 100// + + min(9, (byteBuf[1] >>> 4) & 0x0f) * 10// + + min(9, byteBuf[1] & 0x0f); return value; } public int readUnsignedBCD2() throws IOException { readFully(byteBuf, 0, 1); - int value = min(9, (byteBuf[2] >>> 0) & 0x0f) * 10// - + min(9, (byteBuf[2] >>> 0) & 0x0f) * 1; - return value; + // + return min(9, (byteBuf[2] >>> 4) & 0x0f) * 10// + + min(9, (byteBuf[2]) & 0x0f); } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeDeserializer.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeDeserializer.java index 69a9176..144c28b 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeDeserializer.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeDeserializer.java @@ -165,6 +165,7 @@ protected void parseRecursively(QTFFImageInputStream in, long remainingSize, Qui if (null != t) { switch (t) { case "ftyp": + parseFileType(in, atom.size - atom.headerSize, m); break; case "wide": break; @@ -488,7 +489,7 @@ protected void parseEditList(QTFFImageInputStream in, long remainingSize, QuickT t.editList.add(edit); } - remainingSize -= 8 + numberOfEntries * 12; + remainingSize -= 8 + numberOfEntries * 12L; } /** diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReader.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReader.java index a2b07b1..44db665 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReader.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReader.java @@ -106,7 +106,7 @@ protected void ensureRealized() throws IOException { * @param img An image that can be reused if it fits the media format of the * track. Pass null to create a new image on each read. * @return An image or null if the end of the media has been reached. - * @throws IOException + * @throws IOException if an I/O error occurs */ public BufferedImage read(int track, BufferedImage img) throws IOException { ensureRealized(); @@ -201,8 +201,7 @@ public Rational getDuration() throws IOException { public Rational getDuration(int track) throws IOException { ensureRealized(); QuickTimeMeta.Track tr = meta.tracks.get(track); - Rational trackDuration = new Rational(tr.duration, meta.timeScale); - return trackDuration; + return new Rational(tr.duration, meta.timeScale); } @Override diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReaderSpi.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReaderSpi.java index c3ebf71..26a18c7 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReaderSpi.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeReaderSpi.java @@ -10,8 +10,6 @@ import javax.imageio.stream.ImageInputStream; import java.io.File; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -21,7 +19,7 @@ */ public class QuickTimeReaderSpi implements MovieReaderSpi { - private final static List extensions = Collections.unmodifiableList(Arrays.asList(new String[]{"mov"})); + private final static List extensions = List.of(new String[]{"mov"}); @Override public QuickTimeReader create(ImageInputStream in) throws IOException { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeWriterSpi.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeWriterSpi.java index db896a2..035393f 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeWriterSpi.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/QuickTimeWriterSpi.java @@ -11,8 +11,6 @@ import javax.imageio.stream.ImageOutputStream; import java.io.File; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -22,7 +20,7 @@ */ public class QuickTimeWriterSpi implements MovieWriterSpi { - private final static List extensions = Collections.unmodifiableList(Arrays.asList(new String[]{"mov"})); + private final static List extensions = List.of(new String[]{"mov"}); @Override public MovieWriter create(File file) throws IOException { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/AppleClosedCaptionCodec.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/AppleClosedCaptionCodec.java index 4d80099..eead194 100644 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/AppleClosedCaptionCodec.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/AppleClosedCaptionCodec.java @@ -99,7 +99,6 @@ public int decode(Buffer in, Buffer out) { break; } case "ccdp": {// CTA-708 - String text = new Cta708Parser().parseToStringWithOpCodes(iis); out.data = text; break; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/CmdToken.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/CmdToken.java index c4ac63c..a5c0562 100644 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/CmdToken.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/CmdToken.java @@ -65,8 +65,7 @@ public CmdToken(int channel, Command command) { } public int getChannel() { - int channel = ((code & 0b0000_1000_0000_0000) >> 11) + 1; - return channel; + return ((code & 0b0000_1000_0000_0000) >> 11) + 1; } /** diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/PacToken.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/PacToken.java index 67f8570..118c313 100644 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/PacToken.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta608/PacToken.java @@ -107,8 +107,7 @@ public int getRow() { } public int getChannel() { - int channel = ((code & 0b0000_1000_0000_0000) >> 11) + 1; - return channel; + return ((code & 0b0000_1000_0000_0000) >> 11) + 1; } public Attributes getTextAttributes() { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta708/CCDataInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta708/CCDataInputStream.java index 383a940..b25c035 100644 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta708/CCDataInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/quicktime/codec/text/cta708/CCDataInputStream.java @@ -33,7 +33,7 @@ * */ public class CCDataInputStream extends InputStream { - private InputStream in; + private final InputStream in; /** * Number of remaining triplets times 2. *

diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/riff/RIFFPrimitivesInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/riff/RIFFPrimitivesInputStream.java index 7ab76ae..4c9a3a2 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/riff/RIFFPrimitivesInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/riff/RIFFPrimitivesInputStream.java @@ -24,7 +24,7 @@ */ public class RIFFPrimitivesInputStream extends FilterInputStream { private long scan, mark; - private byte byteBuffer[] = new byte[8]; + private final byte[] byteBuffer = new byte[8]; /** * Creates a new instance. diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/FileSegment.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/FileSegment.java index f458539..13f2fd6 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/FileSegment.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/FileSegment.java @@ -12,21 +12,6 @@ * * @author Werner Randelshofer */ -public class FileSegment { +public record FileSegment(long offset, long length) { - private long offset; - private long length; - - public FileSegment(long offset, long length) { - this.offset = offset; - this.length = length; - } - - public long getLength() { - return length; - } - - public long getOffset() { - return offset; - } } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFD.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFD.java index c15075a..418942d 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFD.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFD.java @@ -23,15 +23,15 @@ public class IFD { /** - * The offset of this IFD inside of the TIFF input stream when it was + * The offset of this IFD inside the TIFF input stream when it was * read from the input stream. */ - private long offset; + private final long offset; /** * Whether this IFD has a nextOffset field. */ - private boolean hasNextOffset; + private final boolean hasNextOffset; /** * The offset of the next IFD. @@ -42,7 +42,7 @@ public class IFD { /** * The entries of this IFD. */ - private ArrayList entries; + private final ArrayList entries; public IFD(long offset, boolean hasNextOffset) { this.offset = offset; diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFDEntry.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFDEntry.java index 249ccd7..26c582f 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFDEntry.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/IFDEntry.java @@ -28,24 +28,24 @@ public class IFDEntry { /** * The Tag number that identifies the field. */ - private int tagNumber; + private final int tagNumber; /** * The field Type. */ - private int typeNumber; + private final int typeNumber; /** * The number of values, Count of the indicated Type. */ - private long count; + private final long count; /** * The Value Offset stores the value or the offset of the value depending * on typeNumber and on count. */ - private long valueOffset; + private final long valueOffset; /** * The Entry Offset stores the location of the entry in the file. */ - private long entryOffset; + private final long entryOffset; /** * The IFD Offset stores the location of the IFD in the file. */ @@ -103,66 +103,64 @@ public long getIFDOffset() { } public boolean isDataInValueOffset() { - switch (IFDDataType.valueOf(typeNumber)) { - case ASCII://8-bit byte that contains a 7-bit ASCII code; the last byte + return switch (IFDDataType.valueOf(typeNumber)) { + case ASCII ->//8-bit byte that contains a 7-bit ASCII code; the last byte //must be NUL (binary zero). - return false; - case BYTE://8-bit unsigned integer. - return count <= 4; - case SHORT://16-bit (2-byte) unsigned integer. - return count <= 2; - case LONG://32-bit (4-byte) unsigned integer. - return count <= 1; - case RATIONAL://Two LONGs: the first represents the numerator of a fraction; the second, the denominator. - return false; - case SBYTE: //An 8-bit signed (twos-complement) integer. - return count <= 4; - case UNDEFINED://An 8-bit byte that may contain anything, depending on the definition of the field. - return count <= 4; - case SSHORT://A 16-bit (2-byte) signed (twos-complement) integer. - return count <= 2; - case SLONG://A 32-bit (4-byte) signed (twos-complement) integer. - return count <= 1; - case SRATIONAL://Two SLONG’s: the first represents the numerator of a fraction, the second the denominator. - return false; - case FLOAT://Single precision (4-byte) IEEE prettyFormat. - return count <= 1; - case DOUBLE:// Double precision (8-byte) IEEE prettyFormat. - return false; - default: - return true; - } + false; + case BYTE ->//8-bit unsigned integer. + count <= 4; + case SHORT ->//16-bit (2-byte) unsigned integer. + count <= 2; + case LONG ->//32-bit (4-byte) unsigned integer. + count <= 1; + case RATIONAL ->//Two LONGs: the first represents the numerator of a fraction; the second, the denominator. + false; + case SBYTE -> //An 8-bit signed (twos-complement) integer. + count <= 4; + case UNDEFINED ->//An 8-bit byte that may contain anything, depending on the definition of the field. + count <= 4; + case SSHORT ->//A 16-bit (2-byte) signed (twos-complement) integer. + count <= 2; + case SLONG ->//A 32-bit (4-byte) signed (twos-complement) integer. + count <= 1; + case SRATIONAL ->//Two SLONG’s: the first represents the numerator of a fraction, the second the denominator. + false; + case FLOAT ->//Single precision (4-byte) IEEE prettyFormat. + count <= 1; + case DOUBLE ->// Double precision (8-byte) IEEE prettyFormat. + false; + default -> true; + }; } public long getLength() { - switch (IFDDataType.valueOf(typeNumber)) { - case ASCII://8-bit byte that contains a 7-bit ASCII code; the last byte - return count; - case BYTE://8-bit unsigned integer. - return count; - case SHORT://16-bit (2-byte) unsigned integer. - return count * 2; - case LONG://32-bit (4-byte) unsigned integer. - return count * 4; - case RATIONAL://Two LONGs: the first represents the numerator of a fraction; the second, the denominator. - return count * 8; - case SBYTE: //An 8-bit signed (twos-complement) integer. - return count; - case UNDEFINED://An 8-bit byte that may contain anything, depending on the definition of the field. - return count; - case SSHORT://A 16-bit (2-byte) signed (twos-complement) integer. - return count * 2; - case SLONG://A 32-bit (4-byte) signed (twos-complement) integer. - return count * 4; - case SRATIONAL://Two SLONG’s: the first represents the numerator of a fraction, the second the denominator. - return count * 8; - case FLOAT://Single precision (4-byte) IEEE prettyFormat. - return count * 4; - case DOUBLE:// Double precision (8-byte) IEEE prettyFormat. - return count * 8; - default: - return 0; - } + return switch (IFDDataType.valueOf(typeNumber)) { + case ASCII ->//8-bit byte that contains a 7-bit ASCII code; the last byte + count; + case BYTE ->//8-bit unsigned integer. + count; + case SHORT ->//16-bit (2-byte) unsigned integer. + count * 2; + case LONG ->//32-bit (4-byte) unsigned integer. + count * 4; + case RATIONAL ->//Two LONGs: the first represents the numerator of a fraction; the second, the denominator. + count * 8; + case SBYTE -> //An 8-bit signed (twos-complement) integer. + count; + case UNDEFINED ->//An 8-bit byte that may contain anything, depending on the definition of the field. + count; + case SSHORT ->//A 16-bit (2-byte) signed (twos-complement) integer. + count * 2; + case SLONG ->//A 32-bit (4-byte) signed (twos-complement) integer. + count * 4; + case SRATIONAL ->//Two SLONG’s: the first represents the numerator of a fraction, the second the denominator. + count * 8; + case FLOAT ->//Single precision (4-byte) IEEE prettyFormat. + count * 4; + case DOUBLE ->// Double precision (8-byte) IEEE prettyFormat. + count * 8; + default -> 0; + }; } /** @@ -352,8 +350,7 @@ public String toString(Enum tagName) { } buf.append(d[i]); } - } else if (data instanceof Object[]) { - Object[] d = (Object[]) data; + } else if (data instanceof Object[] d) { for (int i = 0; i < d.length; i++) { if (i != 0) { buf.append(','); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFDirectory.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFDirectory.java index ee0993b..1741009 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFDirectory.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFDirectory.java @@ -15,11 +15,11 @@ public class TIFFDirectory extends TIFFNode { /** * The tag set of this directory. */ - private TagSet tagSet; + private final TagSet tagSet; /** * The index of this directory. */ - private int index; + private final int index; /** @@ -136,10 +136,11 @@ public ArrayList getFileSegments() { } /** - * Returns a TIFFField with the specified tag. If a child node with + * Returns a TIFFField with the specified tag if a child node with * this tag exists. * - * @param tag + * @param tag The tag to search for. + * @return The TIFFField with the specified tag, or null if no such field exists. */ public TIFFField getField(TIFFTag tag) { for (TIFFNode node : getChildren()) { @@ -153,10 +154,11 @@ public TIFFField getField(TIFFTag tag) { } /** - * Returns the value of the TIFFField with the specified tag. If a child node with + * Returns the value of the TIFFField with the specified tag if a child node with * this tag exists. * - * @param tag + * @param tag The tag to search for. + * @return The value of the TIFFField with the specified tag, or null if no such field exists. */ public Object getData(TIFFTag tag) { TIFFField field = getField(tag); diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFInputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFInputStream.java index dda40b8..d6e4c2d 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFInputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFInputStream.java @@ -36,7 +36,7 @@ public class TIFFInputStream extends InputStream { /** * The underlying input stream. */ - private ImageInputStream in; + private final ImageInputStream in; public TIFFInputStream(ImageInputStream in) throws IOException { this.in = in; @@ -162,7 +162,7 @@ private void readFully(byte b[]) throws IOException { } - private void readFully(byte b[], int off, int len) throws IOException { + private void readFully(byte[] b, int off, int len) throws IOException { if (len < 0) { throw new IndexOutOfBoundsException(); } diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFNode.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFNode.java index abc7ac8..0439fa5 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFNode.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFNode.java @@ -24,7 +24,7 @@ public abstract class TIFFNode { /** * The children. */ - private ArrayList children = new ArrayList(); + private final ArrayList children = new ArrayList(); private TIFFNode parent; /** * The IFDEntry from which this node was read. @@ -93,7 +93,7 @@ public void removeFromParent() { private static class PreorderIterator implements Iterator { - private Stack> stack = new Stack>(); + private final Stack> stack = new Stack>(); private TIFFNode current; private PreorderIterator(TIFFNode root) { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFOutputStream.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFOutputStream.java index 1c48077..8c0f6fe 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFOutputStream.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFOutputStream.java @@ -23,8 +23,8 @@ */ public class TIFFOutputStream extends OutputStream { - private ImageOutputStream out; - private long offset; + private final ImageOutputStream out; + private final long offset; private Stack ifdStack = new Stack(); private enum State { diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFTag.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFTag.java index 2b26949..e0a9ba0 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFTag.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TIFFTag.java @@ -39,11 +39,11 @@ public class TIFFTag { public final static int SRATIONAL_MASK = 1 << IFDDataType.SRATIONAL.getTypeNumber(); public final static int UNDEFINED_MASK = 1 << IFDDataType.UNDEFINED.getTypeNumber(); public final static int ALL_MASK = -1; - private String name; - private int number; + private final String name; + private final int number; private int dataTypes; private TagSet tagSet; - private ValueFormatter formatter; + private final ValueFormatter formatter; /** * Constructs a TIFFTag with a given name, tag number, set of legal data types, diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TagSet.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TagSet.java index d9d4062..861d33e 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TagSet.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/tiff/TagSet.java @@ -19,8 +19,8 @@ */ public abstract class TagSet { - private HashMap tagsByNumber = new HashMap(); - private String name; + private final HashMap tagsByNumber = new HashMap(); + private final String name; public TagSet(String name, TIFFTag[] tags) { this.name = name; @@ -38,16 +38,7 @@ public TagSet(String name, TIFFTag[] tags) { */ public TIFFTag getTag(int tagNumber) { TIFFTag tag = tagsByNumber.get(tagNumber); - if (tag == null) { - synchronized (this) { - tag = tagsByNumber.get(tagNumber); - if (tag == null) { - tag = new TIFFTag("unknown", tagNumber, TIFFTag.ALL_MASK, null); - tagsByNumber.put(tagNumber, tag); - } - } - } - return tag; + return (tag == null) ? new TIFFTag("unknown", tagNumber, TIFFTag.ALL_MASK, null) : tag; } /** diff --git a/org.monte.media/src/main/java/org.monte.media/org/monte/media/util/Methods.java b/org.monte.media/src/main/java/org.monte.media/org/monte/media/util/Methods.java index 6afa44e..1ccd12d 100755 --- a/org.monte.media/src/main/java/org.monte.media/org/monte/media/util/Methods.java +++ b/org.monte.media/src/main/java/org.monte.media/org/monte/media/util/Methods.java @@ -29,15 +29,14 @@ private Methods() { * @param obj The object on which to invoke the method. * @param methodName The name of the method. * @return The return value of the method. - * @return NoSuchMethodException if the method does not exist or is not - * accessible. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invoke(Object obj, String methodName) throws NoSuchMethodException { try { Method method = obj.getClass().getMethod(methodName, new Class[0]); - Object result = method.invoke(obj, new Object[0]); - return result; + return method.invoke(obj, new Object[0]); } catch (IllegalAccessException e) { throw new NoSuchMethodException(methodName + " is not accessible"); } catch (InvocationTargetException e) { @@ -51,16 +50,16 @@ public static Object invoke(Object obj, String methodName) * * @param obj The object on which to invoke the method. * @param methodName The name of the method. - * @param stringParameter The String parameter - * @return The return value of the method or METHOD_NOT_FOUND. - * @return NoSuchMethodException if the method does not exist or is not accessible. + * @param stringParameter The String parameter. + * @return The return value of the method. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invoke(Object obj, String methodName, String stringParameter) throws NoSuchMethodException { try { Method method = obj.getClass().getMethod(methodName, new Class[]{String.class}); - Object result = method.invoke(obj, new Object[]{stringParameter}); - return result; + return method.invoke(obj, new Object[]{stringParameter}); } catch (IllegalAccessException e) { throw new NoSuchMethodException(methodName + " is not accessible"); } catch (InvocationTargetException e) { @@ -68,21 +67,20 @@ public static Object invoke(Object obj, String methodName, String stringParamete throw new InternalError(e.getMessage()); } } - /** * Invokes the specified accessible parameterless method if it exists. * * @param clazz The class on which to invoke the method. * @param methodName The name of the method. - * @return The return value of the method or METHOD_NOT_FOUND. - * @return NoSuchMethodException if the method does not exist or is not accessible. + * @return The return value of the method. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invokeStatic(Class clazz, String methodName) throws NoSuchMethodException { try { Method method = clazz.getMethod(methodName, new Class[0]); - Object result = method.invoke(null, new Object[0]); - return result; + return method.invoke(null, new Object[0]); } catch (IllegalAccessException e) { throw new NoSuchMethodException(methodName + " is not accessible"); } catch (InvocationTargetException e) { @@ -90,14 +88,14 @@ public static Object invokeStatic(Class clazz, String methodName) throw new InternalError(e.getMessage()); } } - /** * Invokes the specified accessible parameterless method if it exists. * * @param clazz The class on which to invoke the method. * @param methodName The name of the method. * @return The return value of the method. - * @return NoSuchMethodException if the method does not exist or is not accessible. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invokeStatic(String clazz, String methodName) throws NoSuchMethodException { @@ -107,16 +105,16 @@ public static Object invokeStatic(String clazz, String methodName) throw new NoSuchMethodException("class " + clazz + " not found"); } } - /** - * Invokes the specified parameterless method if it exists. + * Invokes the specified method with a single parameter if it exists. * * @param clazz The class on which to invoke the method. * @param methodName The name of the method. * @param type The parameter type. * @param value The parameter value. * @return The return value of the method. - * @return NoSuchMethodException if the method does not exist or is not accessible. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invokeStatic(Class clazz, String methodName, Class type, Object value) throws NoSuchMethodException { @@ -124,21 +122,21 @@ public static Object invokeStatic(Class clazz, String methodName, Class ty } /** - * Invokes the specified parameterless method if it exists. + * Invokes a static method on the specified class with the given parameter types and values. * * @param clazz The class on which to invoke the method. - * @param methodName The name of the method. - * @param types The parameter types. - * @param values The parameter values. - * @return The return value of the method. - * @return NoSuchMethodException if the method does not exist or is not accessible. + * @param methodName The name of the method to invoke. + * @param types The parameter types of the method. + * @param values The parameter values to pass to the method. + * @return The return value of the invoked method. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invokeStatic(Class clazz, String methodName, Class[] types, Object[] values) throws NoSuchMethodException { try { Method method = clazz.getMethod(methodName, types); - Object result = method.invoke(null, values); - return result; + return method.invoke(null, values); } catch (IllegalAccessException e) { throw new NoSuchMethodException(methodName + " is not accessible"); } catch (InvocationTargetException e) { @@ -155,7 +153,8 @@ public static Object invokeStatic(Class clazz, String methodName, Class[] * @param types The parameter types. * @param values The parameter values. * @return The return value of the method. - * @return NoSuchMethodException if the method does not exist or is not accessible. + * + * @throws NoSuchMethodException if the method does not exist or is not accessible. */ public static Object invokeStatic(String clazz, String methodName, Class[] types, Object[] values) @@ -166,7 +165,7 @@ public static Object invokeStatic(String clazz, String methodName, throw new NoSuchMethodException("class " + clazz + " not found"); } } - + /** * Invokes the specified parameterless method if it exists. * @@ -182,9 +181,7 @@ public static Object invokeStatic(String clazz, String methodName, Class[] types, Object[] values, Object defaultValue) { try { return invokeStatic(Class.forName(clazz), methodName, types, values); - } catch (ClassNotFoundException e) { - return defaultValue; - } catch (NoSuchMethodException e) { + } catch (ClassNotFoundException | NoSuchMethodException e) { return defaultValue; } } @@ -202,11 +199,7 @@ public static int invokeGetter(Object obj, String methodName, int defaultValue) Method method = obj.getClass().getMethod(methodName, new Class[0]); Object result = method.invoke(obj, new Object[0]); return ((Integer) result).intValue(); - } catch (NoSuchMethodException e) { - return defaultValue; - } catch (IllegalAccessException e) { - return defaultValue; - } catch (InvocationTargetException e) { + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return defaultValue; } } @@ -224,11 +217,7 @@ public static long invokeGetter(Object obj, String methodName, long defaultValue Method method = obj.getClass().getMethod(methodName, new Class[0]); Object result = method.invoke(obj, new Object[0]); return ((Long) result).longValue(); - } catch (NoSuchMethodException e) { - return defaultValue; - } catch (IllegalAccessException e) { - return defaultValue; - } catch (InvocationTargetException e) { + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return defaultValue; } } @@ -246,11 +235,7 @@ public static boolean invokeGetter(Object obj, String methodName, boolean defaul Method method = obj.getClass().getMethod(methodName, new Class[0]); Object result = method.invoke(obj, new Object[0]); return ((Boolean) result).booleanValue(); - } catch (NoSuchMethodException e) { - return defaultValue; - } catch (IllegalAccessException e) { - return defaultValue; - } catch (InvocationTargetException e) { + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return defaultValue; } } @@ -266,13 +251,9 @@ public static boolean invokeGetter(Object obj, String methodName, boolean defaul public static Object invokeGetter(Object obj, String methodName, Object defaultValue) { try { Method method = obj.getClass().getMethod(methodName, new Class[0]); - Object result = method.invoke(obj, new Object[0]); - return result; - } catch (NoSuchMethodException e) { - return defaultValue; - } catch (IllegalAccessException e) { - return defaultValue; - } catch (InvocationTargetException e) { + + return method.invoke(obj); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return defaultValue; } } @@ -289,12 +270,8 @@ public static boolean invokeStaticGetter(Class clazz, String methodName, bool try { Method method = clazz.getMethod(methodName, new Class[0]); Object result = method.invoke(null, new Object[0]); - return ((Boolean) result).booleanValue(); - } catch (NoSuchMethodException e) { - return defaultValue; - } catch (IllegalAccessException e) { - return defaultValue; - } catch (InvocationTargetException e) { + return (Boolean) result; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return defaultValue; } } @@ -347,7 +324,7 @@ public static Object invoke(Object obj, String methodName, float newValue) throws NoSuchMethodException { try { Method method = obj.getClass().getMethod(methodName, new Class[]{Float.TYPE}); - return method.invoke(obj, new Object[]{new Float(newValue)}); + return method.invoke(obj, new Object[]{newValue}); } catch (IllegalAccessException e) { throw new NoSuchMethodException(methodName + " is not accessible"); } catch (InvocationTargetException e) { @@ -465,10 +442,7 @@ public static void invokeIfExistsWithEnum(Object obj, String methodName, String new Object[]{enumClass, enumValueName} ); invoke(obj, methodName, enumClass, enumValue); - } catch (ClassNotFoundException e) { - // ignore - e.printStackTrace(); - } catch (NoSuchMethodException e) { + } catch (ClassNotFoundException | NoSuchMethodException e) { // ignore e.printStackTrace(); }