com.replaymod.replaystudio.io.ReplayOutputStream.java Source code

Java tutorial

Introduction

Here is the source code for com.replaymod.replaystudio.io.ReplayOutputStream.java

Source

/*
 * This file is part of ReplayStudio, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2016 johni0702 <https://github.com/johni0702>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.replaymod.replaystudio.io;

import com.google.gson.Gson;
import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.Studio;
import com.replaymod.replaystudio.replay.Replay;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.studio.protocol.StudioCodec;
import com.replaymod.replaystudio.studio.protocol.StudioCompression;
import com.replaymod.replaystudio.studio.protocol.StudioSession;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.spacehq.mc.protocol.packet.ingame.server.ServerSetCompressionPacket;
import org.spacehq.netty.buffer.ByteBuf;
import org.spacehq.netty.buffer.ByteBufAllocator;
import org.spacehq.netty.buffer.PooledByteBufAllocator;
import org.spacehq.netty.handler.codec.EncoderException;
import org.spacehq.packetlib.packet.Packet;

import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static com.replaymod.replaystudio.util.Utils.writeInt;

/**
 * Output stream capable of writing {@link org.spacehq.packetlib.packet.Packet}s and (optionally)
 * {@link ReplayMetaData}.
 */
public class ReplayOutputStream extends OutputStream {

    private static final Gson GSON = new Gson();
    private static final ByteBufAllocator ALLOC = PooledByteBufAllocator.DEFAULT;

    /**
     * Meta data for the current replay. Gets written after all packets are written.
     */
    private final ReplayMetaData metaData;

    /**
     * The actual output stream.
     * If we write to a ZIP output stream, this is the same as {@link #zipOut}.
     */
    private final OutputStream out;

    /**
     * If we write to a ZIP output stream instead of just raw data, this holds a reference to that output stream.
     */
    private final ZipOutputStream zipOut;

    /**
     * The studio session.
     */
    private final StudioSession session;

    /**
     * The studio codec.
     */
    private final StudioCodec codec;

    /**
     * The studio compression. May be null if no compression is applied at the moment.
     */
    private StudioCompression compression = null;

    /**
     * Duration of the replay written. This gets updated with each packet and is afterwards used to set the
     * duration in the replay meta data.
     */
    private int duration;

    /**
     * Creates a new replay output stream which will not compress packets written to it nor write any meta data.
     * The resulting output can be read directly by a {@link ReplayInputStream}.
     * @param studio The studio
     * @param out The actual output stream
     */
    public ReplayOutputStream(Studio studio, OutputStream out) {
        this.session = new StudioSession(studio, false);
        this.codec = new StudioCodec(session);
        this.out = out;
        this.zipOut = null;
        this.metaData = null;
    }

    /**
     * Creates a new replay output stream which will write its packets and the specified meta data
     * in a zip output stream according to the MCPR format.
     *
     * @param studio The studio
     * @param out The actual output stream
     * @param metaData The meta data written to the output
     * @throws IOException If an exception occurred while writing the first entry to the zip output stream
     */
    public ReplayOutputStream(Studio studio, OutputStream out, ReplayMetaData metaData) throws IOException {
        this.session = new StudioSession(studio, false);
        this.codec = new StudioCodec(session);
        if (metaData == null) {
            metaData = new ReplayMetaData();
            metaData.setSingleplayer(false);
            metaData.setServerName(studio.getName() + " v" + studio.getVersion());
            metaData.setDate(System.currentTimeMillis());
        }
        metaData.setFileFormat("MCPR");
        metaData.setFileFormatVersion(1);
        metaData.setGenerator("ReplayStudio v" + studio.getVersion());
        this.metaData = metaData;

        this.out = zipOut = new ZipOutputStream(out);

        zipOut.putNextEntry(new ZipEntry("recording.tmcpr"));

    }

    @Override
    public void write(int b) throws IOException {
        out.write(b);
    }

    /**
     * Writes the specified packet data to the underlying output stream.
     * @param data The packet data
     * @throws IOException - if an I/O error occurs.
     *      In particular, an IOException may be thrown if the output stream has been closed.
     * @see #write(long, org.spacehq.packetlib.packet.Packet)
     */
    public void write(PacketData data) throws IOException {
        write(data.getTime(), data.getPacket());
    }

    /**
     * Writes the specified packet data to the underlying output stream.
     * @param time The timestamp
     * @param packet The packet
     * @throws IOException - if an I/O error occurs.
     *      In particular, an IOException may be thrown if the output stream has been closed.
     * @see #write(PacketData)
     */
    public void write(long time, Packet packet) throws IOException {
        if (duration < time) {
            duration = (int) time;
        }

        ByteBuf encoded = ALLOC.buffer();
        try {
            codec.encode(null, packet, encoded);
        } catch (Exception e) {
            throw new EncoderException(ToStringBuilder.reflectionToString(packet), e);
        }

        ByteBuf compressed;
        if (compression == null) {
            compressed = encoded;
        } else {
            compressed = ALLOC.buffer();
            try {
                compression.encode(null, encoded, compressed);
            } catch (Exception e) {
                throw new EncoderException(ToStringBuilder.reflectionToString(packet), e);
            }
            encoded.release();
        }

        int length = compressed.readableBytes();
        writeInt(out, (int) time);
        writeInt(out, length);
        compressed.readBytes(out, length);

        compressed.release();

        if (packet instanceof ServerSetCompressionPacket) {
            int threshold = ((ServerSetCompressionPacket) packet).getThreshold();
            if (threshold == -1) {
                compression = null;
            } else {
                compression = new StudioCompression(session);
                session.setCompressionThreshold(threshold);
            }
        }
    }

    /**
     * Starts a new entry in this replay zip file.
     * The previous entry is therefore closed.
     * @param name Name of the new entry
     */
    public void nextEntry(String name) throws IOException {
        if (zipOut != null) {
            zipOut.closeEntry();
            zipOut.putNextEntry(new ZipEntry(name));
        } else {
            throw new UnsupportedOperationException("Cannot start new entry when writing raw replay output.");
        }
    }

    @Override
    public void close() throws IOException {
        if (zipOut != null) {
            zipOut.closeEntry();

            metaData.setDuration(duration);
            zipOut.putNextEntry(new ZipEntry("metaData.json"));
            zipOut.write(GSON.toJson(metaData).getBytes());
            zipOut.closeEntry();
        }
        out.close();
    }

    /**
     * Writes the specified replay file to the output stream.
     * The output stream is closed when writing is done.
     * @param studio The studio
     * @param output The output stream
     * @param replay The replay
     * @throws IOException - if an I/O error occurs.
     */
    public static void writeReplay(Studio studio, OutputStream output, Replay replay) throws IOException {
        ReplayOutputStream out = new ReplayOutputStream(studio, output, replay.getMetaData());
        for (PacketData data : replay) {
            out.write(data);
        }
        out.close();
    }

    /**
     * Writes the specified packets to the output stream in the order of their occurrence.
     * The output stream is not closed when done allowing for further writing.
     * @param studio The studio
     * @param output The output stream
     * @param packets Iterable of packet data
     * @throws IOException - if an I/O error occurs.
     */
    public static void writePackets(Studio studio, OutputStream output, Iterable<PacketData> packets)
            throws IOException {
        ReplayOutputStream out = new ReplayOutputStream(studio, output);
        for (PacketData data : packets) {
            out.write(data);
        }
    }

}