All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.monte.media.avi.TechSmithCodec Maven / Gradle / Ivy

There is a newer version: 1.1
Show newest version
/*
 * @(#)TechSmithCodec.java 
 *
 * Copyright (c) 2011 Werner Randelshofer, Goldau, Switzerland.
 * All rights reserved.
 *
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.avi;

import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferUShort;
import java.awt.image.Raster;
import java.awt.Point;
import java.awt.image.DirectColorModel;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.ColorModel;
import java.util.Hashtable;
import org.monte.media.Format;
import java.awt.image.WritableRaster;
import java.awt.Rectangle;
import org.monte.media.AbstractVideoCodec;
import org.monte.media.Buffer;
import org.monte.media.io.SeekableByteArrayOutputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import org.monte.media.BufferFlag;
import static org.monte.media.VideoFormatKeys.*;
import static org.monte.media.BufferFlag.*;

/**
 * {@code TechSmithCodec} (tscc) encodes a BufferedImage as a byte[] array. 

* The TechSmith codec works with AVI and QuickTime.

This codec supports * encoding from a {@code BufferedImage} into the file format, and decoding from * the file format to a {@code BufferedImage}.

This codec does not * encode the color palette of an image. This must be done separately.

* Supported input formats:

    {@code Format} with * {@code BufferedImage.class}, any width, any height, depth=8,16 or 24.
* Supported output formats:
    {@code Format} with {@code byte[].class}, same * width and height as input format, depth=8,16 or 24.
The codec supports * lossless delta- and key-frame encoding of images with 8, 16 or 24 bits per * pixel.

Compression of a frame is performed in two steps: In the first, * step a frame is compressed line by line from bottom to top. In the second * step the resulting data is compressed again using zlib compression.

Apart * from the second compression step and the support for 16- and 24-bit data, * this encoder is identical to the {@link org.monte.media.avi.RunLengthCodec}.

Each line of a * frame is compressed individually. A line consists of two-byte op-codes * optionally followed by data. The end of the line is marked with the EOL * op-code.

The following op-codes are supported:

  • {@code 0x00 0x00} *
    Marks the end of a line.
  • * *
  • {@code 0x00 0x01}
    Marks the end of the bitmap.
  • * *
  • {@code 0x00 0x02 x y}
    Marks a delta (skip). {@code x} and {@code y} * indicate the horizontal and vertical offset from the current position. * {@code x} and {@code y} are unsigned 8-bit values.
  • * *
  • {@code 0x00 n pixel{n} 0x00?}
    Marks a literal run. {@code n} gives * the number of 8-, 16- or 24-bit pixels that follow. {@code n} must be between * 3 and 255. If n is odd and 8-bit pixels are used, a pad byte with the value * 0x00 must be added.
  • {@code n pixel}
    Marks a repetition. * {@code n} gives the number of times the given pixel is repeated. {@code n} * must be between 1 and 255.
Example: *
 * Compressed data         Expanded data
 *
 * 03 04                   04 04 04
 * 05 06                   06 06 06 06 06
 * 00 03 45 56 67 00       45 56 67
 * 02 78                   78 78
 * 00 02 05 01             Move 5 right and 1 down
 * 02 78                   78 78
 * 00 00                   End of line
 * 09 1E                   1E 1E 1E 1E 1E 1E 1E 1E 1E
 * 00 01                   End of RLE bitmap
 * 
* * References:
http://wiki.multimedia.cx/index.php?title=TechSmith_Screen_Capture_Codec
* * * @author Werner Randelshofer * @version $Id: TechSmithCodec.java 299 2013-01-03 07:40:18Z werner $ */ public class TechSmithCodec extends AbstractVideoCodec { private TechSmithCodecCore state; private Object previousPixels; private int frameCounter; private Object oldPixels; private Object newPixels; public TechSmithCodec() { super(new Format[]{ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE, FixedFrameRateKey, true), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 8), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 16), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 24), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 8), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 16), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 24), // }, new Format[]{ new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_JAVA, EncodingKey, ENCODING_BUFFERED_IMAGE, FixedFrameRateKey, true), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 8), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 16), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_AVI, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 24), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 8), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 16), // new Format(MediaTypeKey, MediaType.VIDEO, MimeTypeKey, MIME_QUICKTIME, EncodingKey, ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE, CompressorNameKey, COMPRESSOR_NAME_AVI_TECHSMITH_SCREEN_CAPTURE, DataClassKey, byte[].class, FixedFrameRateKey, true, DepthKey, 24), // }); name = "TechSmith Screen Capture"; } @Override public void reset() { state = null; frameCounter = 0; } @Override public int process(Buffer in, Buffer out) { if (state == null) { state = new TechSmithCodecCore(); } if (in.isFlag(BufferFlag.DISCARD)) { out.setMetaTo(in); return CODEC_OK; } if (outputFormat.get(EncodingKey).equals(ENCODING_AVI_TECHSMITH_SCREEN_CAPTURE)) { return encode(in, out); } else { return decode(in, out); } } public int decode(Buffer in, Buffer out) { out.setMetaTo(in); out.format = outputFormat; out.length = 1; out.offset = 0; if (state == null) { state = new TechSmithCodecCore(); } int width = outputFormat.get(WidthKey); int height = outputFormat.get(HeightKey); int inputDepth = inputFormat.get(DepthKey); int outputDepth = outputFormat.get(DepthKey); boolean isKeyFrame; try { if (outputDepth == 8) { if (!(newPixels instanceof byte[]) || ((byte[]) newPixels).length != width * height) { newPixels = new byte[width * height]; } isKeyFrame = state.decode8((byte[]) in.data, in.offset, in.length, (byte[]) newPixels, (byte[]) newPixels, width, height, false); } else { if (!(newPixels instanceof int[]) || ((int[]) newPixels).length != width * height) { newPixels = new int[width * height]; } if (inputDepth == 8) { isKeyFrame = state.decode8((byte[]) in.data, in.offset, in.length, (int[]) newPixels, (int[]) newPixels, width, height, false); } else if (inputDepth == 16) { isKeyFrame = state.decode16((byte[]) in.data, in.offset, in.length, (int[]) newPixels, (int[]) newPixels, width, height, false); } else { isKeyFrame = state.decode24((byte[]) in.data, in.offset, in.length, (int[]) newPixels, (int[]) newPixels, width, height, false); } } } catch (IOException e) { e.printStackTrace(); out.setFlag(DISCARD); return CODEC_FAILED; } MyBufferedImage img = null; if (out.data instanceof MyBufferedImage) { img = (MyBufferedImage) out.data; } switch (outputDepth) { case 8: { int imgType = BufferedImage.TYPE_BYTE_INDEXED; // FIXME - Don't hardcode this value if (img == null || img.getWidth() != width || img.getHeight() != height || img.getType() != imgType) { int[] cmap = new int[256]; IndexColorModel icm = new IndexColorModel(8, 256, cmap, 0, false, -1, DataBuffer.TYPE_BYTE); img = new MyBufferedImage(width, height, imgType, icm); } else { MyBufferedImage oldImg = img; img = new MyBufferedImage(oldImg.getColorModel(), oldImg.getRaster(), oldImg.isAlphaPremultiplied(), null); } int[] cmap = new int[256];//state.getPalette(); for (int i = 0; i < 256; i++) { cmap[i] = 255 << 24 | i | i << 8 | i << 16; } IndexColorModel icm = new IndexColorModel(8, 256, cmap, 0, false, -1, DataBuffer.TYPE_BYTE); img.setColorModel(icm); byte[] pixels = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); System.arraycopy((byte[]) newPixels, 0, pixels, 0, width * height); } break; case 15: { int imgType = BufferedImage.TYPE_USHORT_555_RGB; if (img == null || img.getWidth() != width || img.getHeight() != height || img.getType() != imgType) { DirectColorModel cm = new DirectColorModel(15, 0x1f << 10, 0x1f << 5, 0x1f << 0); img = new MyBufferedImage(cm, Raster.createWritableRaster(cm.createCompatibleSampleModel(width, height), new Point(0, 0)), false, new Hashtable()); } else { MyBufferedImage oldImg = img; img = new MyBufferedImage(oldImg.getColorModel(), oldImg.getRaster(), oldImg.isAlphaPremultiplied(), null); } short[] pixels = ((DataBufferUShort) img.getRaster().getDataBuffer()).getData(); System.arraycopy((short[]) newPixels, 0, pixels, 0, width * height); } break; case 16: { //int imgType = BufferedImage.TYPE_USHORT_565_RGB; int imgType = BufferedImage.TYPE_INT_RGB; if (img == null || img.getWidth() != width || img.getHeight() != height || img.getType() != imgType) { //DirectColorModel cm = new DirectColorModel(24, 0x1f << 11, 0x3f << 5, 0x1f << 0); DirectColorModel cm = new DirectColorModel(24, 0xff << 16, 0xff << 8, 0xff << 0); img = new MyBufferedImage(cm, Raster.createWritableRaster(cm.createCompatibleSampleModel(width, height), new Point(0, 0)), false, new Hashtable()); } else { MyBufferedImage oldImg = img; img = new MyBufferedImage(oldImg.getColorModel(), oldImg.getRaster(), oldImg.isAlphaPremultiplied(), null); } /* short[] pixels = ((DataBufferUShort) img.getRaster().getDataBuffer()).getData(); System.arraycopy((short[]) newPixels, 0, pixels, 0, width * height); */ int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData(); System.arraycopy((int[]) newPixels, 0, pixels, 0, width * height); } break; case 24: { int imgType = BufferedImage.TYPE_INT_RGB; if (img == null || img.getWidth() != width || img.getHeight() != height || img.getType() != imgType) { DirectColorModel cm = new DirectColorModel(24, 0xff << 16, 0xff << 8, 0xff << 0); img = new MyBufferedImage(cm, Raster.createWritableRaster(cm.createCompatibleSampleModel(width, height), new Point(0, 0)), false, new Hashtable()); } else { MyBufferedImage oldImg = img; img = new MyBufferedImage(oldImg.getColorModel(), oldImg.getRaster(), oldImg.isAlphaPremultiplied(), null); } int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData(); System.arraycopy((int[]) newPixels, 0, pixels, 0, width * height); } break; default: throw new UnsupportedOperationException("Unsupported depth:" + outputDepth); } out.setFlag(KEYFRAME, isKeyFrame); out.data = img; return CODEC_OK; } public int encode(Buffer in, Buffer out) { out.setMetaTo(in); out.format = outputFormat; if (in.isFlag(DISCARD)) { return CODEC_OK; } SeekableByteArrayOutputStream tmp; if (out.data instanceof byte[]) { tmp = new SeekableByteArrayOutputStream((byte[]) out.data); } else { tmp = new SeekableByteArrayOutputStream(); } boolean isKeyframe = frameCounter == 0 || frameCounter % outputFormat.get(KeyFrameIntervalKey, outputFormat.get(FrameRateKey).intValue()) == 0; out.setFlag(KEYFRAME, isKeyframe); out.clearFlag(SAME_DATA); frameCounter++; // Handle sub-image Rectangle r; int scanlineStride; if (in.data instanceof BufferedImage) { BufferedImage image = (BufferedImage) in.data; WritableRaster raster = image.getRaster(); scanlineStride = raster.getSampleModel().getWidth(); r = raster.getBounds(); r.x -= raster.getSampleModelTranslateX(); r.y -= raster.getSampleModelTranslateY(); out.header = image.getColorModel(); } else { r = new Rectangle(0, 0, outputFormat.get(WidthKey), outputFormat.get(HeightKey)); scanlineStride = outputFormat.get(WidthKey); out.header = null; } int offset = r.x + r.y * scanlineStride; try { switch (outputFormat.get(DepthKey)) { case 8: { byte[] pixels = getIndexed8(in); if (pixels == null) { out.setFlag(DISCARD); return CODEC_OK; } if (isKeyframe) { state.encodeKey8(tmp, pixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } else { if (in.isFlag(SAME_DATA)) { state.encodeSameDelta8(tmp, pixels, (byte[]) previousPixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } else { state.encodeDelta8(tmp, pixels, (byte[]) previousPixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } out.clearFlag(KEYFRAME); } if (previousPixels == null) { previousPixels = pixels.clone(); } else { System.arraycopy(pixels, 0, (byte[]) previousPixels, 0, pixels.length); } break; } case 16: { short[] pixels = getRGB15(in); // 16-bit TSCC is actually just 15-bit if (pixels == null) { out.setFlag(DISCARD); return CODEC_OK; } if (isKeyframe) { state.encodeKey16(tmp, pixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } else { if (in.isFlag(SAME_DATA)) { state.encodeSameDelta16(tmp, pixels, (short[]) previousPixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } else { state.encodeDelta16(tmp, pixels, (short[]) previousPixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } } if (previousPixels == null) { previousPixels = pixels.clone(); } else { System.arraycopy(pixels, 0, (short[]) previousPixels, 0, pixels.length); } break; } case 24: { int[] pixels = getRGB24(in); if (pixels == null) { out.setFlag(DISCARD); return CODEC_OK; } if (isKeyframe) { state.encodeKey24(tmp, pixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); out.setFlag(KEYFRAME); } else { if (in.isFlag(SAME_DATA)) { state.encodeSameDelta24(tmp, pixels, (int[]) previousPixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } else { state.encodeDelta24(tmp, pixels, (int[]) previousPixels, outputFormat.get(WidthKey), outputFormat.get(HeightKey), offset, scanlineStride); } out.clearFlag(KEYFRAME); } if (previousPixels == null) { previousPixels = pixels.clone(); } else { System.arraycopy(pixels, 0, (int[]) previousPixels, 0, pixels.length); } break; } default: { out.setFlag(DISCARD); return CODEC_FAILED; } } out.format = outputFormat; out.data = tmp.getBuffer(); out.offset = 0; out.sampleCount = 1; out.length = tmp.size(); return CODEC_OK; } catch (IOException ex) { ex.printStackTrace(); out.setFlag(DISCARD); return CODEC_OK; } } private static class MyBufferedImage extends BufferedImage { private ColorModel colorModel; public MyBufferedImage(ColorModel cm, WritableRaster raster, boolean isRasterPremultiplied, Hashtable properties) { super(cm, raster, isRasterPremultiplied, properties); colorModel = cm; } public MyBufferedImage(int width, int height, int imageType, IndexColorModel cm) { super(width, height, imageType, cm); colorModel = cm; } public MyBufferedImage(int width, int height, int imageType) { super(width, height, imageType); } @Override public ColorModel getColorModel() { return colorModel; } public void setColorModel(ColorModel newValue) { this.colorModel = newValue; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy