/* * opsu! - an open-source osu! client * Copyright (C) 2014-2017 Jeffrey Han * * opsu! is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * opsu! is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with opsu!. If not, see <http://www.gnu.org/licenses/>. */ package itdelatrisu.opsu.video; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import org.lwjgl.opengl.GL11; import org.newdawn.slick.Image; import org.newdawn.slick.SlickException; import net.indiespot.media.VideoStream; import net.indiespot.media.impl.VideoMetadata; /** * Video player (with no audio). * * @author Riven (base, heavily modified) */ public class Video implements Closeable { /** The source file. */ private final File file; /** The video metadata. */ private final VideoMetadata metadata; /** The frame interval. */ private final long frameInterval; /** The current video frame. */ private final Image image; /** The video stream. */ private VideoStream videoStream; /** The initial frame time. */ private long initFrame; /** The current frame index. */ private int videoIndex; /** Whether the video is finished playing. */ private boolean finished; /** Whether video data has been initialized. */ private boolean initialized; /** Whether the video stream is closed. */ private boolean closed; /** The time when the video was paused, or 0 if not paused. */ private long pauseFrame; /** * Creates a video from the source file. * Call {@link #seek(int)} to load the video stream. * @param file the source video file */ public Video(File file) throws IOException, SlickException { this.file = file; this.metadata = FFmpeg.extractMetadata(file); this.frameInterval = (long) (1000_000_000L / metadata.framerate); this.image = new Image(metadata.width, metadata.height, Image.FILTER_LINEAR); this.finished = false; this.initialized = false; this.closed = false; this.pauseFrame = 0; } /** * Seeks to a position. * @param msec the time offset (in milliseconds) */ public void seek(int msec) throws IOException { if (videoStream != null && !closed) videoStream.close(); this.videoStream = getVideoStreamAtOffset(Math.max(0, msec)); this.finished = false; this.initialized = false; this.pauseFrame = 0; } /** Returns a video stream from the given millisecond offset. */ private VideoStream getVideoStreamAtOffset(int msec) throws IOException { InputStream rgb24Stream = FFmpeg.extractVideoAsRGB24(file, msec); return new VideoStream(rgb24Stream, metadata); } /** Returns whether the video has started playing. */ public boolean isStarted() { return initialized; } /** Returns whether the video has finished playing. */ public boolean isFinished() { return finished; } /** Pauses the video. */ public void pause() { if (pauseFrame == 0) pauseFrame = System.nanoTime(); } /** Resumes the video. */ public void resume() { if (pauseFrame > 0) { if (initialized) initFrame += (System.nanoTime() - pauseFrame); pauseFrame = 0; } } /** * Renders the current frame. * @param x the top-left x coordinate * @param y the top-left y coordinate * @param width the width to render at * @param height the height to render at * @param alpha the alpha level to render at */ public void render(int x, int y, int width, int height, float alpha) { image.setAlpha(alpha); image.draw(x, y, width, height); } /** Returns whether the next frame time has passed. */ private boolean isTimeForNextFrame(long syncTime) { return hasVideoBacklogOver(0, syncTime); } /** Returns whether the frame backlog is over the given number of frames. */ private boolean hasVideoBacklogOver(int frameCount, long syncTime) { return (videoIndex + frameCount) * frameInterval <= syncTime; } /** * Updates the video, syncing based on an internal timer. */ public void update() { update(System.nanoTime() - initFrame); } /** * Updates the video, syncing based on a given time. * @param syncTime the millisecond time the video should sync to (in the * forward direction only), relative to the initial time * passed to {@link #seek(int)} */ public void update(int syncTime) { update(syncTime * 1_000_000L); } /** * Updates the video. * @param syncTime the nanosecond time the video should sync to (forward direction only) */ private void update(long syncTime) { if (finished || closed || pauseFrame > 0 || videoStream == null) return; // initialize frames if (!initialized) { videoIndex = 0; initFrame = System.nanoTime(); if (pauseFrame != 0) pauseFrame = initFrame; // change pixel store alignment to prevent distortion GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1); initialized = true; } if (!isTimeForNextFrame(syncTime)) return; // grab and skip frames (if needed) ByteBuffer texBuffer = null; final int backlog = 5; int framesRead = 0; do { // free extra frames if (framesRead > 0) { videoStream.freeFrameData(texBuffer); texBuffer = null; videoIndex++; } // grab next frame texBuffer = videoStream.pollFrameData(); if (texBuffer == VideoStream.EOF) { finished = true; return; } if (texBuffer == null) return; framesRead++; } while (hasVideoBacklogOver(backlog, syncTime)); // render to texture GL11.glBindTexture(GL11.GL_TEXTURE_2D, image.getTexture().getTextureID()); GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, metadata.width, metadata.height, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, texBuffer); videoStream.freeFrameData(texBuffer); videoIndex++; } @Override public void close() throws IOException { if (!closed) { closed = true; videoStream.close(); try { image.destroy(); } catch (SlickException e) {} } } }